diff --git a/public/images/items.png b/public/images/items.png index cb4f8fa7d06..191766f520e 100644 Binary files a/public/images/items.png and b/public/images/items.png differ diff --git a/public/images/items/catching_charm.png b/public/images/items/catching_charm.png index 9d72fe465e3..c220ff70c03 100644 Binary files a/public/images/items/catching_charm.png and b/public/images/items/catching_charm.png differ diff --git a/public/locales b/public/locales index fc4a1effd51..d600913dbf1 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit fc4a1effd5170def3c8314208a52cd0d8e6913ef +Subproject commit d600913dbf1f8b47dae8dccbd8296df78f1c51b5 diff --git a/src/battle-scene.ts b/src/battle-scene.ts index d82acec1c20..c5acadc8eb6 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -5,7 +5,7 @@ import PokemonSpecies, { allSpecies, getPokemonSpecies, PokemonSpeciesFilter } f import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils"; import * as Utils from "#app/utils"; import { ConsumableModifier, ConsumablePokemonModifier, DoubleBattleChanceBoosterModifier, ExpBalanceModifier, ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, Modifier, ModifierBar, ModifierPredicate, MultipleParticipantExpBonusModifier, overrideHeldItems, overrideModifiers, PersistentModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, RememberMoveModifier, TerastallizeModifier, TurnHeldItemTransferModifier } from "./modifier/modifier"; -import { PokeballType } from "#app/data/pokeball"; +import { PokeballType } from "#enums/pokeball"; import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "#app/data/battle-anims"; import { Phase } from "#app/phase"; import { initGameSpeed } from "#app/system/game-speed"; @@ -35,10 +35,11 @@ import { Gender } from "#app/data/gender"; import UIPlugin from "phaser3-rex-plugins/templates/ui/ui-plugin"; import { addUiThemeOverrides } from "#app/ui/ui-theme"; import PokemonData from "#app/system/pokemon-data"; -import { Nature } from "#app/data/nature"; +import { Nature } from "#enums/nature"; import { FormChangeItem, pokemonFormChanges, SpeciesFormChange, SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger } from "#app/data/pokemon-forms"; import { FormChangePhase } from "#app/phases/form-change-phase"; import { getTypeRgb } from "#app/data/type"; +import { Type } from "#enums/type"; import PokemonSpriteSparkleHandler from "#app/field/pokemon-sprite-sparkle-handler"; import CharSprite from "#app/ui/char-sprite"; import DamageNumberHandler from "#app/field/damage-number-handler"; @@ -96,7 +97,9 @@ import { ExpPhase } from "#app/phases/exp-phase"; import { ShowPartyExpBarPhase } from "#app/phases/show-party-exp-bar-phase"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { ExpGainsSpeed } from "#enums/exp-gains-speed"; +import { BattlerTagType } from "#enums/battler-tag-type"; import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#app/data/balance/starters"; +import { StatusEffect } from "#enums/status-effect"; export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1"; @@ -771,7 +774,7 @@ export default class BattleScene extends SceneBase { /** * @returns An array of {@linkcode PlayerPokemon} filtered from the player's party - * that are {@linkcode PlayerPokemon.isAllowedInBattle | allowed in battle}. + * that are {@linkcode Pokemon.isAllowedInBattle | allowed in battle}. */ public getPokemonAllowedInBattle(): PlayerPokemon[] { return this.getPlayerParty().filter(p => p.isAllowedInBattle()); @@ -1240,23 +1243,27 @@ export default class BattleScene extends SceneBase { const lastBattle = this.currentBattle; - if (lastBattle?.double && !newDouble) { - this.tryRemovePhase(p => p instanceof SwitchPhase); - } - const maxExpLevel = this.getMaxExpLevel(); this.lastEnemyTrainer = lastBattle?.trainer ?? null; this.lastMysteryEncounter = lastBattle?.mysteryEncounter; + if (newBattleType === BattleType.MYSTERY_ENCOUNTER) { + // Disable double battle on mystery encounters (it may be re-enabled as part of encounter) + newDouble = false; + } + + if (lastBattle?.double && !newDouble) { + this.tryRemovePhase(p => p instanceof SwitchPhase); + this.getPlayerField().forEach(p => p.lapseTag(BattlerTagType.COMMANDED)); + } + this.executeWithSeedOffset(() => { this.currentBattle = new Battle(this.gameMode, newWaveIndex, newBattleType, newTrainer, newDouble); }, newWaveIndex << 3, this.waveSeed); this.currentBattle.incrementTurn(this); if (newBattleType === BattleType.MYSTERY_ENCOUNTER) { - // Disable double battle on mystery encounters (it may be re-enabled as part of encounter) - this.currentBattle.double = false; // Will generate the actual Mystery Encounter during NextEncounterPhase, to ensure it uses proper biome this.currentBattle.mysteryEncounterType = mysteryEncounterType; } @@ -1278,6 +1285,8 @@ export default class BattleScene extends SceneBase { if (resetArenaState) { this.arena.resetArenaEffects(); + playerField.forEach((pokemon) => pokemon.lapseTag(BattlerTagType.COMMANDED)); + playerField.forEach((pokemon, p) => { if (pokemon.isOnField()) { this.pushPhase(new ReturnPhase(this, p)); @@ -2979,12 +2988,21 @@ export default class BattleScene extends SceneBase { updateGameInfo(): void { const gameInfo = { - playTime: this.sessionPlayTime ? this.sessionPlayTime : 0, + playTime: this.sessionPlayTime ?? 0, gameMode: this.currentBattle ? this.gameMode.getName() : "Title", biome: this.currentBattle ? getBiomeName(this.arena.biomeType) : "", - wave: this.currentBattle?.waveIndex || 0, - party: this.party ? this.party.map(p => { - return { name: p.name, level: p.level }; + wave: this.currentBattle?.waveIndex ?? 0, + party: this.party ? this.party.map((p) => { + return { + name: p.name, + form: p.getFormKey(), + types: p.getTypes().map((type) => Type[type]), + teraType: p.getTeraType() !== Type.UNKNOWN ? Type[p.getTeraType()] : "", + level: p.level, + currentHP: p.hp, + maxHP: p.getMaxHp(), + status: p.status?.effect ? StatusEffect[p.status.effect] : "" + }; }) : [], modeChain: this.ui?.getModeChain() ?? [], }; diff --git a/src/battle.ts b/src/battle.ts index 0356772bb07..75f0dff2534 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -4,7 +4,7 @@ import * as Utils from "./utils"; import Trainer, { TrainerVariant } from "./field/trainer"; import { GameMode } from "./game-mode"; import { MoneyMultiplierModifier, PokemonHeldItemModifier } from "./modifier/modifier"; -import { PokeballType } from "./data/pokeball"; +import { PokeballType } from "#enums/pokeball"; import { trainerConfigs } from "#app/data/trainer-config"; import { SpeciesFormKey } from "#enums/species-form-key"; import Pokemon, { EnemyPokemon, PlayerPokemon, QueuedMove } from "#app/field/pokemon"; diff --git a/src/data/ability.ts b/src/data/ability.ts index e8a242ae3ea..08dc1ed27a4 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1,13 +1,13 @@ import Pokemon, { EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove } from "../field/pokemon"; -import { Type } from "./type"; +import { Type } from "#enums/type"; import { Constructor } from "#app/utils"; import * as Utils from "../utils"; import { getPokemonNameWithAffix } from "../messages"; -import { Weather, WeatherType } from "./weather"; -import { BattlerTag, GroundedTag } from "./battler-tags"; -import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect"; +import { Weather } from "#app/data/weather"; +import { BattlerTag, BattlerTagLapseType, GroundedTag } from "./battler-tags"; +import { getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "#app/data/status-effect"; import { Gender } from "./gender"; -import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; +import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "../modifier/modifier"; import { TerrainType } from "./terrain"; @@ -35,6 +35,9 @@ import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { NewBattlePhase } from "#app/phases/new-battle-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; +import { PokemonAnimType } from "#enums/pokemon-anim-type"; +import { StatusEffect } from "#enums/status-effect"; +import { WeatherType } from "#enums/weather-type"; export class Ability implements Localizable { public id: Abilities; @@ -511,7 +514,11 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr { } applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { - if (move instanceof AttackMove && pokemon.getAttackTypeEffectiveness(attacker.getMoveType(move), attacker) < 2) { + const modifierValue = args.length > 0 + ? (args[0] as Utils.NumberHolder).value + : pokemon.getAttackTypeEffectiveness(attacker.getMoveType(move), attacker); + + if (move instanceof AttackMove && modifierValue < 2) { cancelled.value = true; // Suppresses "No Effect" message (args[0] as Utils.NumberHolder).value = 0; return true; @@ -578,15 +585,11 @@ export class PostDefendAbAttr extends AbAttr { export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr { applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { - const attackPriority = new Utils.IntegerHolder(move.priority); - applyMoveAttrs(IncrementMovePriorityAttr, attacker, null, move, attackPriority); - applyAbAttrs(ChangeMovePriorityAbAttr, attacker, null, simulated, move, attackPriority); - if (move.moveTarget === MoveTarget.USER || move.moveTarget === MoveTarget.NEAR_ALLY) { return false; } - if (attackPriority.value > 0 && !move.isMultiTarget()) { + if (move.getPriority(attacker) > 0 && !move.isMultiTarget()) { cancelled.value = true; return true; } @@ -2591,6 +2594,42 @@ export class PostSummonFormChangeByWeatherAbAttr extends PostSummonAbAttr { } } +/** + * Attribute implementing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander}. + * When the source of an ability with this attribute detects a Dondozo as their active ally, the source "jumps + * into the Dondozo's mouth," sharply boosting the Dondozo's stats, cancelling the source's moves, and + * causing attacks that target the source to always miss. + */ +export class CommanderAbAttr extends AbAttr { + constructor() { + super(true); + } + + override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: null, args: any[]): boolean { + // TODO: Should this work with X + Dondozo fusions? + if (pokemon.scene.currentBattle?.double && pokemon.getAlly()?.species.speciesId === Species.DONDOZO) { + // If the ally Dondozo is fainted or was previously "commanded" by + // another Pokemon, this effect cannot apply. + if (pokemon.getAlly().isFainted() || pokemon.getAlly().getTag(BattlerTagType.COMMANDED)) { + return false; + } + + if (!simulated) { + // Lapse the source's semi-invulnerable tags (to avoid visual inconsistencies) + pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); + // Play an animation of the source jumping into the ally Dondozo's mouth + pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.COMMANDER_APPLY); + // Apply boosts from this effect to the ally Dondozo + pokemon.getAlly().addTag(BattlerTagType.COMMANDED, 0, Moves.NONE, pokemon.id); + // Cancel the source Pokemon's next move (if a move is queued) + pokemon.scene.tryRemovePhase((phase) => phase instanceof MovePhase && phase.pokemon === pokemon); + } + return true; + } + return false; + } +} + export class PreSwitchOutAbAttr extends AbAttr { constructor() { super(true); @@ -5392,8 +5431,7 @@ export function initAbilities() { .attr(EffectSporeAbAttr), new Ability(Abilities.SYNCHRONIZE, 3) .attr(SyncEncounterNatureAbAttr) - .attr(SynchronizeStatusAbAttr) - .partial(), // interaction with psycho shift needs work, keeping to old Gen interaction for now + .attr(SynchronizeStatusAbAttr), new Ability(Abilities.CLEAR_BODY, 3) .attr(ProtectStatAbAttr) .ignorable(), @@ -5995,7 +6033,7 @@ export function initAbilities() { .bypassFaint(), new Ability(Abilities.CORROSION, 7) .attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ Type.STEEL, Type.POISON ]) - .edgeCase(), // Should interact correctly with magic coat/bounce (not yet implemented), fling with toxic orb (not implemented yet), and synchronize (not fully implemented yet) + .edgeCase(), // Should interact correctly with magic coat/bounce (not yet implemented) + fling with toxic orb (not implemented yet) new Ability(Abilities.COMATOSE, 7) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) @@ -6243,9 +6281,11 @@ export function initAbilities() { .attr(PreSwitchOutFormChangeAbAttr, (pokemon) => !pokemon.isFainted() ? 1 : pokemon.formIndex) .bypassFaint(), new Ability(Abilities.COMMANDER, 9) + .attr(CommanderAbAttr) + .attr(DoubleBattleChanceAbAttr) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) - .unimplemented(), + .edgeCase(), // Encore, Frenzy, and other non-`TURN_END` tags don't lapse correctly on the commanding Pokemon. new Ability(Abilities.ELECTROMORPHOSIS, 9) .attr(PostDefendApplyBattlerTagAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, BattlerTagType.CHARGED), new Ability(Abilities.PROTOSYNTHESIS, 9) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 73f8e16d573..2f57650c65d 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -1,13 +1,13 @@ import { Arena } from "#app/field/arena"; import BattleScene from "#app/battle-scene"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { BooleanHolder, NumberHolder, toDmgValue } from "#app/utils"; -import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "#app/data/move"; +import { MoveCategory, allMoves, MoveTarget } from "#app/data/move"; import { getPokemonNameWithAffix } from "#app/messages"; import Pokemon, { HitResult, PokemonMove } from "#app/field/pokemon"; -import { StatusEffect } from "#app/data/status-effect"; +import { StatusEffect } from "#enums/status-effect"; import { BattlerIndex } from "#app/battle"; -import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, InfiltratorAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability"; +import { BlockNonDirectDamageAbAttr, InfiltratorAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability"; import { Stat } from "#enums/stat"; import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims"; import i18next from "i18next"; @@ -318,17 +318,15 @@ export class ConditionalProtectTag extends ArenaTag { */ const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => { const move = allMoves[moveId]; - const priority = new NumberHolder(move.priority); const effectPhase = arena.scene.getCurrentPhase(); if (effectPhase instanceof MoveEffectPhase) { const attacker = effectPhase.getUserPokemon(); - applyMoveAttrs(IncrementMovePriorityAttr, attacker, null, move, priority); if (attacker) { - applyAbAttrs(ChangeMovePriorityAbAttr, attacker, null, false, move, priority); + return move.getPriority(attacker) > 0; } } - return priority.value > 0; + return move.priority > 0; }; /** diff --git a/src/data/balance/biomes.ts b/src/data/balance/biomes.ts index 2ce693c360b..0f4926cf7c7 100644 --- a/src/data/balance/biomes.ts +++ b/src/data/balance/biomes.ts @@ -1,4 +1,4 @@ -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import * as Utils from "#app/utils"; import { pokemonEvolutions, SpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions"; import i18next from "i18next"; @@ -7666,7 +7666,7 @@ export function initBiomes() { if (biome === Biome.END) { const biomeList = Object.keys(Biome).filter(key => !isNaN(Number(key))); biomeList.pop(); // Removes Biome.END from the list - const randIndex = Utils.randInt(biomeList.length, 1); // Will never be Biome.TOWN + const randIndex = Utils.randSeedInt(biomeList.length, 1); // Will never be Biome.TOWN biome = Biome[biomeList[randIndex]]; } const linkedBiomes: (Biome | [ Biome, integer ])[] = Array.isArray(biomeLinks[biome]) diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index 8f22b288f45..9e86ea7397b 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -1,10 +1,10 @@ import { Gender } from "#app/data/gender"; -import { PokeballType } from "#app/data/pokeball"; +import { PokeballType } from "#enums/pokeball"; import Pokemon from "#app/field/pokemon"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import * as Utils from "#app/utils"; -import { WeatherType } from "#app/data/weather"; -import { Nature } from "#app/data/nature"; +import { WeatherType } from "#enums/weather-type"; +import { Nature } from "#enums/nature"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; @@ -1005,8 +1005,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.COSMOEM, 23, null, null) ], [Species.COSMOEM]: [ - new SpeciesEvolution(Species.SOLGALEO, 53, EvolutionItem.SUN_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG), - new SpeciesEvolution(Species.LUNALA, 53, EvolutionItem.MOON_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.SOLGALEO, 1, EvolutionItem.SUN_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG), + new SpeciesEvolution(Species.LUNALA, 1, EvolutionItem.MOON_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.MELTAN]: [ new SpeciesEvolution(Species.MELMETAL, 48, null, null) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 15bc745ec9d..25d65fbc372 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -18,10 +18,9 @@ import Move, { StatusCategoryOnAllyAttr } from "#app/data/move"; import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms"; -import { StatusEffect } from "#app/data/status-effect"; +import { getStatusEffectHealText } from "#app/data/status-effect"; import { TerrainType } from "#app/data/terrain"; -import { Type } from "#app/data/type"; -import { WeatherType } from "#app/data/weather"; +import { Type } from "#enums/type"; import Pokemon, { HitResult, MoveResult } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { CommonAnimPhase } from "#app/phases/common-anim-phase"; @@ -38,6 +37,8 @@ import { Moves } from "#enums/moves"; import { PokemonAnimType } from "#enums/pokemon-anim-type"; import { Species } from "#enums/species"; import { EFFECTIVE_STATS, getStatKey, Stat, type BattleStat, type EffectiveStat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import { WeatherType } from "#enums/weather-type"; export enum BattlerTagLapseType { FAINT, @@ -909,11 +910,15 @@ export class FrenzyTag extends BattlerTag { } } -export class EncoreTag extends BattlerTag { +/** + * Applies the effects of the move Encore onto the target Pokemon + * Encore forces the target Pokemon to use its most-recent move for 3 turns + */ +export class EncoreTag extends MoveRestrictionBattlerTag { public moveId: Moves; constructor(sourceId: number) { - super(BattlerTagType.ENCORE, BattlerTagLapseType.AFTER_MOVE, 3, Moves.ENCORE, sourceId); + super(BattlerTagType.ENCORE, [ BattlerTagLapseType.CUSTOM, BattlerTagLapseType.AFTER_MOVE ], 3, Moves.ENCORE, sourceId); } /** @@ -969,6 +974,39 @@ export class EncoreTag extends BattlerTag { } } + /** + * If the encored move has run out of PP, Encore ends early. Otherwise, Encore lapses based on the AFTER_MOVE battler tag lapse type. + * @returns `true` to persist | `false` to end and be removed + */ + override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + if (lapseType === BattlerTagLapseType.CUSTOM) { + const encoredMove = pokemon.getMoveset().find(m => m?.moveId === this.moveId); + if (encoredMove && encoredMove?.getPpRatio() > 0) { + return true; + } + return false; + } else { + return super.lapse(pokemon, lapseType); + } + } + + /** + * Checks if the move matches the moveId stored within the tag and returns a boolean value + * @param move {@linkcode Moves} the move selected + * @param user N/A + * @returns `true` if the move does not match with the moveId stored and as a result, restricted + */ + override isMoveRestricted(move: Moves, _user?: Pokemon): boolean { + if (move !== this.moveId) { + return true; + } + return false; + } + + override selectionDeniedText(_pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:moveDisabled", { moveName: allMoves[move].name }); + } + onRemove(pokemon: Pokemon): void { super.onRemove(pokemon); @@ -2091,6 +2129,37 @@ export class IceFaceBlockDamageTag extends FormBlockDamageTag { } } +/** + * Battler tag indicating a Tatsugiri with {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander} + * has entered the tagged Pokemon's mouth. + */ +export class CommandedTag extends BattlerTag { + private _tatsugiriFormKey: string; + + constructor(sourceId: number) { + super(BattlerTagType.COMMANDED, BattlerTagLapseType.CUSTOM, 0, Moves.NONE, sourceId); + } + + public get tatsugiriFormKey(): string { + return this._tatsugiriFormKey; + } + + /** Caches the Tatsugiri's form key and sharply boosts the tagged Pokemon's stats */ + override onAdd(pokemon: Pokemon): void { + this._tatsugiriFormKey = this.getSourcePokemon(pokemon.scene)?.getFormKey() ?? "curly"; + pokemon.scene.unshiftPhase(new StatStageChangePhase( + pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2 + )); + } + + /** Triggers an {@linkcode PokemonAnimType | animation} of the tagged Pokemon "spitting out" Tatsugiri */ + override onRemove(pokemon: Pokemon): void { + if (this.getSourcePokemon(pokemon.scene)?.isActive(true)) { + pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.COMMANDER_REMOVE); + } + } +} + /** * Battler tag enabling the Stockpile mechanic. This tag handles: * - Stack tracking, including max limit enforcement (which is replicated in Stockpile for redundancy). @@ -2329,7 +2398,7 @@ export class HealBlockTag extends MoveRestrictionBattlerTag { } /** - * Uses DisabledTag's selectionDeniedText() message + * Uses its own unique selectionDeniedText() message */ override selectionDeniedText(pokemon: Pokemon, move: Moves): string { return i18next.t("battle:moveDisabledHealBlock", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[move].name, healBlockName: allMoves[Moves.HEAL_BLOCK].name }); @@ -2796,6 +2865,67 @@ export class PowerTrickTag extends BattlerTag { } } +/** + * Tag associated with the move Grudge. + * If this tag is active when the bearer faints from an opponent's move, the tag reduces that move's PP to 0. + * Otherwise, it lapses when the bearer makes another move. + */ +export class GrudgeTag extends BattlerTag { + constructor() { + super(BattlerTagType.GRUDGE, [ BattlerTagLapseType.CUSTOM, BattlerTagLapseType.PRE_MOVE ], 1, Moves.GRUDGE); + } + + onAdd(pokemon: Pokemon) { + super.onAdd(pokemon); + pokemon.scene.queueMessage(i18next.t("battlerTags:grudgeOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); + } + + /** + * Activates Grudge's special effect on the attacking Pokemon and lapses the tag. + * @param pokemon + * @param lapseType + * @param sourcePokemon {@linkcode Pokemon} the source of the move that fainted the tag's bearer + * @returns `false` if Grudge activates its effect or lapses + */ + override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType, sourcePokemon?: Pokemon): boolean { + if (lapseType === BattlerTagLapseType.CUSTOM && sourcePokemon) { + if (sourcePokemon.isActive() && pokemon.isOpponent(sourcePokemon)) { + const lastMove = pokemon.turnData.attacksReceived[0]; + const lastMoveData = sourcePokemon.getMoveset().find(m => m?.moveId === lastMove.move); + if (lastMoveData && lastMove.move !== Moves.STRUGGLE) { + lastMoveData.ppUsed = lastMoveData.getMovePp(); + pokemon.scene.queueMessage(i18next.t("battlerTags:grudgeLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: lastMoveData.getName() })); + } + } + return false; + } else { + return super.lapse(pokemon, lapseType); + } + } +} + +/** + * Tag used to heal the user of Psycho Shift of its status effect if Psycho Shift succeeds in transferring its status effect to the target Pokemon + */ +export class PsychoShiftTag extends BattlerTag { + constructor() { + super(BattlerTagType.PSYCHO_SHIFT, BattlerTagLapseType.AFTER_MOVE, 1, Moves.PSYCHO_SHIFT); + } + + /** + * Heals Psycho Shift's user of its status effect after it uses a move + * @returns `false` to expire the tag immediately + */ + override lapse(pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean { + if (pokemon.status && pokemon.isActive(true)) { + pokemon.scene.queueMessage(getStatusEffectHealText(pokemon.status.effect, getPokemonNameWithAffix(pokemon))); + pokemon.resetStatus(); + pokemon.updateInfo(); + } + return false; + } +} + /** * Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID. * @param sourceId - The ID of the pokemon adding the tag @@ -2932,6 +3062,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new IceFaceBlockDamageTag(tagType); case BattlerTagType.DISGUISE: return new FormBlockDamageTag(tagType); + case BattlerTagType.COMMANDED: + return new CommandedTag(sourceId); case BattlerTagType.STOCKPILING: return new StockpilingTag(sourceMove); case BattlerTagType.OCTOLOCK: @@ -2975,6 +3107,10 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new TelekinesisTag(sourceMove); case BattlerTagType.POWER_TRICK: return new PowerTrickTag(sourceMove, sourceId); + case BattlerTagType.GRUDGE: + return new GrudgeTag(); + case BattlerTagType.PSYCHO_SHIFT: + return new PsychoShiftTag(); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/challenge.ts b/src/data/challenge.ts index a64a90e5d14..af6bbf5b00f 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -7,11 +7,11 @@ import Pokemon, { PokemonMove } from "#app/field/pokemon"; import { BattleType, FixedBattleConfig } from "#app/battle"; import Trainer, { TrainerVariant } from "#app/field/trainer"; import { GameMode } from "#app/game-mode"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Challenges } from "#enums/challenges"; import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; -import { Nature } from "#app/data/nature"; +import { Nature } from "#enums/nature"; import { Moves } from "#enums/moves"; import { TypeColor, TypeShadow } from "#enums/color"; import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; diff --git a/src/data/custom-pokemon-data.ts b/src/data/custom-pokemon-data.ts index 2e94123fc84..7bc884cff50 100644 --- a/src/data/custom-pokemon-data.ts +++ b/src/data/custom-pokemon-data.ts @@ -1,5 +1,5 @@ import { Abilities } from "#enums/abilities"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { isNullOrUndefined } from "#app/utils"; import { Nature } from "#enums/nature"; diff --git a/src/data/move.ts b/src/data/move.ts index 5ae37c3b092..ed2b176f54c 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1,14 +1,15 @@ import { ChargeAnim, initMoveAnim, loadMoveAnimAssets, MoveChargeAnim } from "./battle-anims"; -import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, SubstituteTag, TrappedTag, TypeBoostTag } from "./battler-tags"; +import { CommandedTag, EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, SubstituteTag, TrappedTag, TypeBoostTag } from "./battler-tags"; import { getPokemonNameWithAffix } from "../messages"; import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon"; -import { getNonVolatileStatusEffects, getStatusEffectHealText, isNonVolatileStatusEffect, StatusEffect } from "./status-effect"; -import { getTypeDamageMultiplier, Type } from "./type"; +import { getNonVolatileStatusEffects, getStatusEffectHealText, isNonVolatileStatusEffect } from "./status-effect"; +import { getTypeDamageMultiplier } from "./type"; +import { Type } from "#enums/type"; import { Constructor, NumberHolder } from "#app/utils"; import * as Utils from "../utils"; -import { WeatherType } from "./weather"; +import { WeatherType } from "#enums/weather-type"; import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; -import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPostItemLostAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, PostDamageForceSwitchAbAttr, PostItemLostAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; +import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPostItemLostAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ChangeMovePriorityAbAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, PostDamageForceSwitchAbAttr, PostItemLostAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier"; import { BattlerIndex, BattleType } from "../battle"; import { TerrainType } from "./terrain"; @@ -38,6 +39,7 @@ import { SpeciesFormChangeRevertWeatherFormTrigger } from "./pokemon-forms"; import { GameMode } from "#app/game-mode"; import { applyChallenges, ChallengeType } from "./challenge"; import { SwitchType } from "#enums/switch-type"; +import { StatusEffect } from "enums/status-effect"; export enum MoveCategory { PHYSICAL, @@ -714,6 +716,10 @@ export default class Move implements Localizable { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { let score = 0; + if (target.getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon(target.scene) === target) { + return 20 * (target.isPlayer() === user.isPlayer() ? -1 : 1); // always -20 with how the AI handles this score + } + for (const attr of this.attrs) { // conditionals to check if the move is self targeting (if so then you are applying the move to yourself, not the target) score += attr.getTargetBenefitScore(user, !attr.selfTarget ? target : user, move) * (target !== user && attr.selfTarget ? -1 : 1); @@ -825,6 +831,15 @@ export default class Move implements Localizable { return power.value; } + + getPriority(user: Pokemon, simulated: boolean = true) { + const priority = new Utils.NumberHolder(this.priority); + + applyMoveAttrs(IncrementMovePriorityAttr, user, null, this, priority); + applyAbAttrs(ChangeMovePriorityAbAttr, user, null, simulated, this, priority); + + return priority.value; + } } export class AttackMove extends Move { @@ -2266,24 +2281,26 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr { super(false, { trigger: MoveEffectTrigger.HIT }); } - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + /** + * Applies the effect of Psycho Shift to its target + * Psycho Shift takes the user's status effect and passes it onto the target. The user is then healed after the move has been successfully executed. + * @returns `true` if Psycho Shift's effect is able to be applied to the target + */ + apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(Abilities.COMATOSE) ? StatusEffect.SLEEP : undefined); if (target.status) { return false; } else { const canSetStatus = target.canSetStatus(statusToApply, true, false, user); + const trySetStatus = canSetStatus ? target.trySetStatus(statusToApply, true, user) : false; - if (canSetStatus) { - if (user.status) { - user.scene.queueMessage(getStatusEffectHealText(user.status.effect, getPokemonNameWithAffix(user))); - } - user.resetStatus(); - user.updateInfo(); - target.trySetStatus(statusToApply, true, user); + if (trySetStatus && user.status) { + // PsychoShiftTag is added to the user if move succeeds so that the user is healed of its status effect after its move + user.addTag(BattlerTagType.PSYCHO_SHIFT); } - return canSetStatus; + return trySetStatus; } } @@ -3245,6 +3262,41 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr { } } +/** + * Attribute implementing the stat boosting effect of {@link https://bulbapedia.bulbagarden.net/wiki/Order_Up_(move) | Order Up}. + * If the user has a Pokemon with {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander} in their mouth, + * one of the user's stats are increased by 1 stage, depending on the "commanding" Pokemon's form. This effect does not respect + * effect chance, but Order Up itself may be boosted by Sheer Force. + */ +export class OrderUpStatBoostAttr extends MoveEffectAttr { + constructor() { + super(true, { trigger: MoveEffectTrigger.HIT }); + } + + override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean { + const commandedTag = user.getTag(CommandedTag); + if (!commandedTag) { + return false; + } + + let increasedStat: EffectiveStat = Stat.ATK; + switch (commandedTag.tatsugiriFormKey) { + case "curly": + increasedStat = Stat.ATK; + break; + case "droopy": + increasedStat = Stat.DEF; + break; + case "stretchy": + increasedStat = Stat.SPD; + break; + } + + user.scene.unshiftPhase(new StatStageChangePhase(user.scene, user.getBattlerIndex(), this.selfTarget, [ increasedStat ], 1)); + return true; + } +} + export class CopyStatsAttr extends MoveEffectAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { if (!super.apply(user, target, move, args)) { @@ -5852,7 +5904,13 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { return false; } + /** The {@linkcode Pokemon} to be switched out with this effect */ const switchOutTarget = this.selfSwitch ? user : target; + + // If the switch-out target is a Dondozo with a Tatsugiri in its mouth + // (e.g. when it uses Flip Turn), make it spit out the Tatsugiri before switching out. + switchOutTarget.lapseTag(BattlerTagType.COMMANDED); + if (switchOutTarget instanceof PlayerPokemon) { /** * Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch @@ -5955,6 +6013,12 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { return false; } + // Dondozo with an allied Tatsugiri in its mouth cannot be forced out + const commandedTag = switchOutTarget.getTag(BattlerTagType.COMMANDED); + if (commandedTag?.getSourcePokemon(switchOutTarget.scene)?.isActive(true)) { + return false; + } + if (!player && user.scene.currentBattle.isBattleMysteryEncounter() && !user.scene.currentBattle.mysteryEncounter?.fleeAllowed) { // Don't allow wild opponents to be force switched during MEs with flee disabled return false; @@ -6309,10 +6373,17 @@ export class RandomMovesetMoveAttr extends OverrideMoveEffectAttr { } export class RandomMoveAttr extends OverrideMoveEffectAttr { + /** + * This function exists solely to allow tests to override the randomly selected move by mocking this function. + */ + public getMoveOverride(): Moves | null { + return null; + } + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { return new Promise(resolve => { const moveIds = Utils.getEnumValues(Moves).filter(m => !allMoves[m].hasFlag(MoveFlags.IGNORE_VIRTUAL) && !allMoves[m].name.endsWith(" (N)")); - const moveId = moveIds[user.randSeedInt(moveIds.length)]; + const moveId = this.getMoveOverride() ?? moveIds[user.randSeedInt(moveIds.length)]; const moveTargets = getMoveTargets(user, moveId); if (!moveTargets.targets.length) { @@ -6695,7 +6766,8 @@ export class SketchAttr extends MoveEffectAttr { return false; } - const targetMove = target.getMoveHistory().filter(m => !m.virtual).at(-1); + const targetMove = target.getLastXMoves(-1) + .find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual); if (!targetMove) { return false; } @@ -7398,6 +7470,27 @@ export class FirstMoveCondition extends MoveCondition { } } +/** + * Condition used by the move {@link https://bulbapedia.bulbagarden.net/wiki/Upper_Hand_(move) | Upper Hand}. + * Moves with this condition are only successful when the target has selected + * a high-priority attack (after factoring in priority-boosting effects) and + * hasn't moved yet this turn. + */ +export class UpperHandCondition extends MoveCondition { + constructor() { + super((user, target, move) => { + const targetCommand = user.scene.currentBattle.turnCommands[target.getBattlerIndex()]; + + return !!targetCommand + && targetCommand.command === Command.FIGHT + && !target.turnData.acted + && !!targetCommand.move?.move + && allMoves[targetCommand.move.move].category !== MoveCategory.STATUS + && allMoves[targetCommand.move.move].getPriority(target) > 0; + }); + } +} + export class hitsSameTypeAttr extends VariableMoveTypeMultiplierAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const multiplier = args[0] as Utils.NumberHolder; @@ -8438,7 +8531,7 @@ export function initMoves() { .attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN) .condition((user, target, move) => !!user.status && (user.status.effect === StatusEffect.PARALYSIS || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.BURN)), new SelfStatusMove(Moves.GRUDGE, Type.GHOST, -1, 5, -1, 0, 3) - .unimplemented(), + .attr(AddBattlerTagAttr, BattlerTagType.GRUDGE, true, undefined, 1), new SelfStatusMove(Moves.SNATCH, Type.DARK, -1, 10, -1, 4, 3) .unimplemented(), new AttackMove(Moves.SECRET_POWER, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, 30, 0, 3) @@ -10300,8 +10393,8 @@ export function initMoves() { new AttackMove(Moves.LUMINA_CRASH, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2), new AttackMove(Moves.ORDER_UP, Type.DRAGON, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 9) - .makesContact(false) - .partial(), // No effect implemented (requires Commander) + .attr(OrderUpStatBoostAttr) + .makesContact(false), new AttackMove(Moves.JET_PUNCH, Type.WATER, MoveCategory.PHYSICAL, 60, 100, 15, -1, 1, 9) .punchingMove(), new StatusMove(Moves.SPICY_EXTRACT, Type.GRASS, -1, 15, -1, 0, 9) @@ -10515,8 +10608,7 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.HEAL_BLOCK, false, false, 2), new AttackMove(Moves.UPPER_HAND, Type.FIGHTING, MoveCategory.PHYSICAL, 65, 100, 15, 100, 3, 9) .attr(FlinchAttr) - .condition((user, target, move) => user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.command === Command.FIGHT && !target.turnData.acted && allMoves[user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].category !== MoveCategory.STATUS && allMoves[user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].priority > 0 ) // TODO: is this bang correct? - .partial(), // Should also apply when target move priority increased by ability ex. gale wings + .condition(new UpperHandCondition()), new AttackMove(Moves.MALIGNANT_CHAIN, Type.POISON, MoveCategory.SPECIAL, 100, 100, 5, 50, 0, 9) .attr(StatusEffectAttr, StatusEffect.TOXIC) ); diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index fa8e1aed1c7..9c00148fbac 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -18,7 +18,7 @@ import { randInt } from "#app/utils"; import { BattlerIndex } from "#app/battle"; import { applyModifierTypeToPlayerPokemon, catchPokemon, getHighestLevelPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { TrainerSlot } from "#app/data/trainer-config"; -import { PokeballType } from "#app/data/pokeball"; +import { PokeballType } from "#enums/pokeball"; import HeldModifierConfig from "#app/interfaces/held-modifier-config"; import { BerryType } from "#enums/berry-type"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index f2605795955..7a03e6efdd2 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -36,7 +36,7 @@ import { HeldItemRequirement, TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { AttackTypeBoosterModifierType, ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type"; import { AttackTypeBoosterModifier, diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index 5502cc7b53a..ae6cabd4dae 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -12,7 +12,7 @@ import { TrainerType } from "#enums/trainer-type"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Abilities } from "#enums/abilities"; import { applyAbilityOverrideToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { randSeedInt, randSeedShuffle } from "#app/utils"; diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index 8a814b58248..2b801d70f61 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -1,4 +1,4 @@ -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index 5794277ffe1..bbc979e844e 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -8,14 +8,14 @@ import { AbilityRequirement, CombinationPokemonRequirement, TypeRequirement } fr import { Species } from "#enums/species"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Gender } from "#app/data/gender"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { BattlerIndex } from "#app/battle"; import Pokemon, { PokemonMove } from "#app/field/pokemon"; import { Moves } from "#enums/moves"; import { EncounterBattleAnim } from "#app/data/battle-anims"; -import { WeatherType } from "#app/data/weather"; +import { WeatherType } from "#enums/weather-type"; import { isNullOrUndefined, randSeedInt } from "#app/utils"; -import { StatusEffect } from "#app/data/status-effect"; +import { StatusEffect } from "#enums/status-effect"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { applyAbilityOverrideToPokemon, applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index 90f6ee942be..b0d547e36cf 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -21,11 +21,12 @@ import PokemonData from "#app/system/pokemon-data"; import i18next from "i18next"; import { Gender, getGenderSymbol } from "#app/data/gender"; import { getNatureName } from "#app/data/nature"; -import { getPokeballAtlasKey, getPokeballTintColor, PokeballType } from "#app/data/pokeball"; +import { getPokeballAtlasKey, getPokeballTintColor } from "#app/data/pokeball"; import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { trainerNamePools } from "#app/data/trainer-names"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { addPokemonDataToDexAndValidateAchievements } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import type { PokeballType } from "#enums/pokeball"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/globalTradeSystem"; @@ -367,10 +368,11 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter!; - const modifier = encounter.misc.chosenModifier; + const modifier = encounter.misc.chosenModifier as PokemonHeldItemModifier; + const party = scene.getPlayerParty(); // Check tier of the traded item, the received item will be one tier up - const type = modifier.type.withTierFromPool(); + const type = modifier.type.withTierFromPool(ModifierPoolType.PLAYER, party); let tier = type.tier ?? ModifierTier.GREAT; // Eggs and White Herb are not in the pool if (type.id === "WHITE_HERB") { @@ -385,11 +387,11 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = tier++; } - regenerateModifierPoolThresholds(scene.getPlayerParty(), ModifierPoolType.PLAYER, 0); + regenerateModifierPoolThresholds(party, ModifierPoolType.PLAYER, 0); let item: ModifierTypeOption | null = null; // TMs excluded from possible rewards while (!item || item.type.id.includes("TM_")) { - item = getPlayerModifierTypeOptions(1, scene.getPlayerParty(), [], { guaranteedModifierTiers: [ tier ], allowLuckUpgrades: false })[0]; + item = getPlayerModifierTypeOptions(1, party, [], { guaranteedModifierTiers: [ tier ], allowLuckUpgrades: false })[0]; } encounter.setDialogueToken("itemName", item.type.name); diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts index 0ee3c57b0a2..0353d52a592 100644 --- a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -6,7 +6,7 @@ import MysteryEncounterOption, { MysteryEncounterOptionBuilder } from "#app/data import { TrainerSlot } from "#app/data/trainer-config"; import { HiddenAbilityRateBoosterModifier, IvScannerModifier } from "#app/modifier/modifier"; import { EnemyPokemon } from "#app/field/pokemon"; -import { PokeballType } from "#app/data/pokeball"; +import { PokeballType } from "#enums/pokeball"; import { PlayerGender } from "#enums/player-gender"; import { IntegerHolder, randSeedInt } from "#app/utils"; import { getPokemonSpecies } from "#app/data/pokemon-species"; diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts index 8ea19e1225b..3fb502be545 100644 --- a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -3,7 +3,7 @@ import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifi import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; -import { StatusEffect } from "#app/data/status-effect"; +import { StatusEffect } from "#enums/status-effect"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; diff --git a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts index faeba95f358..042e9278673 100644 --- a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts +++ b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts @@ -12,7 +12,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { Biome } from "#enums/biome"; import { getBiomeKey } from "#app/field/arena"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { getPartyLuckValue, modifierTypes } from "#app/modifier/modifier-type"; import { TrainerSlot } from "#app/data/trainer-config"; import { BattlerTagType } from "#enums/battler-tag-type"; diff --git a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts index 7c904bd047a..6a4c6592fda 100644 --- a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts @@ -23,7 +23,7 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/myst import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { achvs } from "#app/system/achv"; import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { getPokeballTintColor } from "#app/data/pokeball"; import { PokemonHeldItemModifier } from "#app/modifier/modifier"; diff --git a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts index 95f359547e4..77d92e3749e 100644 --- a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts @@ -8,7 +8,7 @@ import { catchPokemon, getRandomSpeciesByStarterTier, getSpriteKeysFromPokemon } import { getPokemonSpecies } from "#app/data/pokemon-species"; import { speciesStarterCosts } from "#app/data/balance/starters"; import { Species } from "#enums/species"; -import { PokeballType } from "#app/data/pokeball"; +import { PokeballType } from "#enums/pokeball"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { showEncounterDialogue } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index b1ef6cedb21..754632aedea 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -5,7 +5,7 @@ import BattleScene from "#app/battle-scene"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Species } from "#enums/species"; -import { Nature } from "#app/data/nature"; +import { Nature } from "#enums/nature"; import Pokemon, { PokemonMove } from "#app/field/pokemon"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { modifyPlayerPokemonBST } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; diff --git a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts index 7d9b531c9ab..f4446241873 100644 --- a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -10,7 +10,7 @@ import { Abilities } from "#enums/abilities"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Moves } from "#enums/moves"; import { Nature } from "#enums/nature"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { BerryType } from "#enums/berry-type"; import { Stat } from "#enums/stat"; import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms"; @@ -23,6 +23,7 @@ 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 { BattlerTagType } from "#enums/battler-tag-type"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/theWinstrateChallenge"; @@ -187,6 +188,7 @@ function endTrainerBattleAndShowDialogue(scene: BattleScene): Promise { } else { scene.arena.resetArenaEffects(); const playerField = scene.getPlayerField(); + playerField.forEach((pokemon) => pokemon.lapseTag(BattlerTagType.COMMANDED)); playerField.forEach((_, p) => scene.unshiftPhase(new ReturnPhase(scene, p))); for (const pokemon of scene.getPlayerParty()) { diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index 2e134c0b442..725c4ba79eb 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -1,6 +1,6 @@ import { Ability, allAbilities } from "#app/data/ability"; import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { getNatureName, Nature } from "#app/data/nature"; +import { getNatureName } from "#app/data/nature"; import { speciesStarterCosts } from "#app/data/balance/starters"; import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import { PokemonHeldItemModifier } from "#app/modifier/modifier"; @@ -21,6 +21,7 @@ import i18next from "i18next"; import { getStatKey } from "#enums/stat"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import type { Nature } from "#enums/nature"; /** The i18n namespace for the encounter */ const namespace = "mysteryEncounters/trainingSession"; diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index c05e707ed4d..3c541e20bf4 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -1,4 +1,4 @@ -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; diff --git a/src/data/mystery-encounters/mystery-encounter-option.ts b/src/data/mystery-encounters/mystery-encounter-option.ts index c674ebdc46e..4ff8fd95f85 100644 --- a/src/data/mystery-encounters/mystery-encounter-option.ts +++ b/src/data/mystery-encounters/mystery-encounter-option.ts @@ -2,7 +2,7 @@ import { OptionTextDisplay } from "#app/data/mystery-encounters/mystery-encounte import { Moves } from "#app/enums/moves"; import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import BattleScene from "#app/battle-scene"; -import { Type } from "../type"; +import { Type } from "#enums/type"; import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { CanLearnMoveRequirement, CanLearnMoveRequirementOptions } from "./requirements/can-learn-move-requirement"; import { isNullOrUndefined, randSeedInt } from "#app/utils"; diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index 1358c465d17..811b622de76 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -1,11 +1,11 @@ import BattleScene from "#app/battle-scene"; import { allAbilities } from "#app/data/ability"; import { EvolutionItem, pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; -import { Nature } from "#app/data/nature"; +import { Nature } from "#enums/nature"; import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms"; -import { StatusEffect } from "#app/data/status-effect"; -import { Type } from "#app/data/type"; -import { WeatherType } from "#app/data/weather"; +import { StatusEffect } from "#enums/status-effect"; +import { Type } from "#enums/type"; +import { WeatherType } from "#enums/weather-type"; import { PlayerPokemon } from "#app/field/pokemon"; import { AttackTypeBoosterModifier } from "#app/modifier/modifier"; import { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type"; diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index badbfcf7a56..e341da4e435 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -5,7 +5,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; import MysteryEncounterIntroVisuals, { MysteryEncounterSpriteConfig } from "#app/field/mystery-encounter-intro"; import * as Utils from "#app/utils"; -import { StatusEffect } from "../status-effect"; +import { StatusEffect } from "#enums/status-effect"; import MysteryEncounterDialogue, { OptionTextDisplay } from "./mystery-encounter-dialogue"; import MysteryEncounterOption, { MysteryEncounterOptionBuilder, OptionPhaseCallback } from "./mystery-encounter-option"; import { EncounterPokemonRequirement, EncounterSceneRequirement, HealthRatioRequirement, PartySizeRequirement, StatusEffectRequirement, WaveRangeRequirement } from "./mystery-encounter-requirements"; diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 429b6bfa292..c6dda134346 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -19,11 +19,11 @@ import i18next from "i18next"; import BattleScene from "#app/battle-scene"; import Trainer, { TrainerVariant } from "#app/field/trainer"; import { Gender } from "#app/data/gender"; -import { Nature } from "#app/data/nature"; +import { Nature } from "#enums/nature"; import { Moves } from "#enums/moves"; import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { TrainerConfig, trainerConfigs, TrainerSlot } from "#app/data/trainer-config"; import PokemonSpecies from "#app/data/pokemon-species"; import { Egg, IEggOptions } from "#app/data/egg"; @@ -37,6 +37,7 @@ import { GameOverPhase } from "#app/phases/game-over-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { PartyExpPhase } from "#app/phases/party-exp-phase"; import { Variant } from "#app/data/variant"; +import { StatusEffect } from "#enums/status-effect"; /** * Animates exclamation sprite over trainer's head at start of encounter diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index dd42cc9ce55..fc85754bdde 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -3,15 +3,15 @@ import i18next from "i18next"; import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { PokemonHeldItemModifier } from "#app/modifier/modifier"; import Pokemon, { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; -import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, PokeballType } from "#app/data/pokeball"; +import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor } from "#app/data/pokeball"; import { PlayerGender } from "#enums/player-gender"; import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims"; -import { getStatusEffectCatchRateMultiplier, StatusEffect } from "#app/data/status-effect"; +import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; import { achvs } from "#app/system/achv"; import { Mode } from "#app/ui/ui"; import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; import { Species } from "#enums/species"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species"; import { speciesStarterCosts } from "#app/data/balance/starters"; import { getEncounterText, queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; @@ -23,6 +23,8 @@ import { VictoryPhase } from "#app/phases/victory-phase"; import { SummaryUiMode } from "#app/ui/summary-ui-handler"; import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { Abilities } from "#enums/abilities"; +import type { PokeballType } from "#enums/pokeball"; +import { StatusEffect } from "#enums/status-effect"; /** Will give +1 level every 10 waves */ export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1; diff --git a/src/data/nature.ts b/src/data/nature.ts index edac06f1a4f..b90047ce6f0 100644 --- a/src/data/nature.ts +++ b/src/data/nature.ts @@ -3,9 +3,7 @@ import { TextStyle, getBBCodeFrag } from "../ui/text"; import { Nature } from "#enums/nature"; import { UiTheme } from "#enums/ui-theme"; import i18next from "i18next"; -import { Stat, EFFECTIVE_STATS, getShortenedStatKey } from "#app/enums/stat"; - -export { Nature }; +import { Stat, EFFECTIVE_STATS, getShortenedStatKey } from "#enums/stat"; export function getNatureName(nature: Nature, includeStatEffects: boolean = false, forStarterSelect: boolean = false, ignoreBBCode: boolean = false, uiTheme: UiTheme = UiTheme.DEFAULT): string { let ret = Utils.toReadableString(Nature[nature]); diff --git a/src/data/pokeball.ts b/src/data/pokeball.ts index 57a78e2cd61..4c9fc719a4d 100644 --- a/src/data/pokeball.ts +++ b/src/data/pokeball.ts @@ -1,9 +1,9 @@ +import { CriticalCatchChanceBoosterModifier } from "#app/modifier/modifier"; +import { NumberHolder } from "#app/utils"; import { PokeballType } from "#enums/pokeball"; import BattleScene from "../battle-scene"; import i18next from "i18next"; -export { PokeballType }; - export const MAX_PER_TYPE_POKEBALLS: integer = 99; export function getPokeballAtlasKey(type: PokeballType): string { @@ -82,11 +82,38 @@ export function getPokeballTintColor(type: PokeballType): number { } } -export function doPokeballBounceAnim(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite, y1: number, y2: number, baseBounceDuration: integer, callback: Function) { +/** + * Gets the critical capture chance based on number of mons registered in Dex and modified {@link https://bulbapedia.bulbagarden.net/wiki/Catch_rate Catch rate} + * Formula from {@link https://www.dragonflycave.com/mechanics/gen-vi-vii-capturing Dragonfly Cave Gen 6 Capture Mechanics page} + * @param scene {@linkcode BattleScene} current BattleScene + * @param modifiedCatchRate the modified catch rate as calculated in {@linkcode AttemptCapturePhase} + * @returns the chance of getting a critical capture, out of 256 + */ +export function getCriticalCaptureChance(scene: BattleScene, modifiedCatchRate: number): number { + if (scene.gameMode.isFreshStartChallenge()) { + return 0; + } + const dexCount = scene.gameData.getSpeciesCount(d => !!d.caughtAttr); + const catchingCharmMultiplier = new NumberHolder(1); + scene.findModifier(m => m instanceof CriticalCatchChanceBoosterModifier)?.apply(catchingCharmMultiplier); + const dexMultiplier = scene.gameMode.isDaily || dexCount > 800 ? 2.5 + : dexCount > 600 ? 2 + : dexCount > 400 ? 1.5 + : dexCount > 200 ? 1 + : dexCount > 100 ? 0.5 + : 0; + return Math.floor(catchingCharmMultiplier.value * dexMultiplier * Math.min(255, modifiedCatchRate) / 6); +} + +export function doPokeballBounceAnim(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite, y1: number, y2: number, baseBounceDuration: number, callback: Function, isCritical: boolean = false) { let bouncePower = 1; let bounceYOffset = y1; let bounceY = y2; const yd = y2 - y1; + const x0 = pokeball.x; + const x1 = x0 + 3; + const x2 = x0 - 3; + let critShakes = 4; const doBounce = () => { scene.tweens.add({ @@ -117,5 +144,40 @@ export function doPokeballBounceAnim(scene: BattleScene, pokeball: Phaser.GameOb }); }; - doBounce(); + const doCritShake = () => { + scene.tweens.add({ + targets: pokeball, + x: x2, + duration: 125, + ease: "Linear", + onComplete: () => { + scene.tweens.add({ + targets: pokeball, + x: x1, + duration: 125, + ease: "Linear", + onComplete: () => { + critShakes--; + if (critShakes > 0) { + doCritShake(); + } else { + scene.tweens.add({ + targets: pokeball, + x: x0, + duration: 60, + ease: "Linear", + onComplete: () => scene.time.delayedCall(500, doBounce) + }); + } + } + }); + } + }); + }; + + if (isCritical) { + scene.time.delayedCall(500, doCritShake); + } else { + doBounce(); + } } diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index 7cc20d50fb9..2db0ed54294 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -1,8 +1,8 @@ import { PokemonFormChangeItemModifier, TerastallizeModifier } from "../modifier/modifier"; import Pokemon from "../field/pokemon"; -import { StatusEffect } from "./status-effect"; +import { StatusEffect } from "#enums/status-effect"; import { MoveCategory, allMoves } from "./move"; -import { Type } from "./type"; +import { Type } from "#enums/type"; import { Constructor, nil } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; @@ -10,7 +10,7 @@ import { Species } from "#enums/species"; import { TimeOfDay } from "#enums/time-of-day"; import { getPokemonNameWithAffix } from "#app/messages"; import i18next from "i18next"; -import { WeatherType } from "./weather"; +import { WeatherType } from "#enums/weather-type"; import { Challenges } from "#app/enums/challenges"; import { SpeciesFormKey } from "#enums/species-form-key"; diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 0cdeef10d9e..e7fe902956c 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -12,7 +12,7 @@ import { uncatchableSpecies } from "#app/data/balance/biomes"; import { speciesEggMoves } from "#app/data/balance/egg-moves"; import { GrowthRate } from "#app/data/exp"; import { EvolutionLevel, SpeciesWildEvolutionDelay, pokemonEvolutions, pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { LevelMoves, pokemonFormLevelMoves, pokemonFormLevelMoves as pokemonSpeciesFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves"; import { Stat } from "#enums/stat"; import { Variant, VariantSet, variantColorCache, variantData } from "#app/data/variant"; diff --git a/src/data/status-effect.ts b/src/data/status-effect.ts index 56e754ac407..6b4e1d546df 100644 --- a/src/data/status-effect.ts +++ b/src/data/status-effect.ts @@ -2,8 +2,6 @@ import { randIntRange } from "#app/utils"; import { StatusEffect } from "#enums/status-effect"; import i18next, { ParseKeys } from "i18next"; -export { StatusEffect }; - export class Status { public effect: StatusEffect; /** Toxic damage is `1/16 max HP * toxicTurnCount` */ diff --git a/src/data/terrain.ts b/src/data/terrain.ts index d8ee8d67925..6ba9acfd166 100644 --- a/src/data/terrain.ts +++ b/src/data/terrain.ts @@ -1,8 +1,6 @@ import Pokemon from "../field/pokemon"; import Move from "./move"; -import { Type } from "./type"; -import * as Utils from "../utils"; -import { ChangeMovePriorityAbAttr, applyAbAttrs } from "./ability"; +import { Type } from "#enums/type"; import { ProtectAttr } from "./move"; import { BattlerIndex } from "#app/battle"; import i18next from "i18next"; @@ -58,10 +56,8 @@ export class Terrain { switch (this.terrainType) { case TerrainType.PSYCHIC: if (!move.hasAttr(ProtectAttr)) { - const priority = new Utils.IntegerHolder(move.priority); - applyAbAttrs(ChangeMovePriorityAbAttr, user, null, false, move, priority); // Cancels move if the move has positive priority and targets a Pokemon grounded on the Psychic Terrain - return priority.value > 0 && user.getOpponents().some(o => targets.includes(o.getBattlerIndex()) && o.isGrounded()); + return move.getPriority(user) > 0 && user.getOpponents().some(o => targets.includes(o.getBattlerIndex()) && o.isGrounded()); } } diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index 18bcec1f928..d82d568ecc6 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -2,11 +2,11 @@ import BattleScene, { startingWave } from "#app/battle-scene"; import { ModifierTypeFunc, modifierTypes } from "#app/modifier/modifier-type"; import { EnemyPokemon, PokemonMove } from "#app/field/pokemon"; import * as Utils from "#app/utils"; -import { PokeballType } from "#app/data/pokeball"; +import { PokeballType } from "#enums/pokeball"; import { pokemonEvolutions, pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions"; import PokemonSpecies, { getPokemonSpecies, PokemonSpeciesFilter } from "#app/data/pokemon-species"; import { tmSpecies } from "#app/data/balance/tms"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { doubleBattleDialogue } from "#app/data/dialogue"; import { PersistentModifier } from "#app/modifier/modifier"; import { TrainerVariant } from "#app/field/trainer"; diff --git a/src/data/type.ts b/src/data/type.ts index 483ec068d3c..6170eadc91e 100644 --- a/src/data/type.ts +++ b/src/data/type.ts @@ -1,25 +1,4 @@ -export enum Type { - UNKNOWN = -1, - NORMAL = 0, - FIGHTING, - FLYING, - POISON, - GROUND, - ROCK, - BUG, - GHOST, - STEEL, - FIRE, - WATER, - GRASS, - ELECTRIC, - PSYCHIC, - ICE, - DRAGON, - DARK, - FAIRY, - STELLAR -} +import { Type } from "#enums/type"; export type TypeDamageMultiplier = 0 | 0.125 | 0.25 | 0.5 | 1 | 2 | 4 | 8; diff --git a/src/data/weather.ts b/src/data/weather.ts index 20c03af77c8..0a76a015402 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -2,7 +2,7 @@ import { Biome } from "#enums/biome"; import { WeatherType } from "#enums/weather-type"; import { getPokemonNameWithAffix } from "../messages"; import Pokemon from "../field/pokemon"; -import { Type } from "./type"; +import { Type } from "#enums/type"; import Move, { AttackMove } from "./move"; import * as Utils from "../utils"; import BattleScene from "../battle-scene"; @@ -10,7 +10,6 @@ import { SuppressWeatherEffectAbAttr } from "./ability"; import { TerrainType, getTerrainName } from "./terrain"; import i18next from "i18next"; -export { WeatherType }; export class Weather { public weatherType: WeatherType; public turnsLeft: integer; diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 0eace9a1607..b2bbc1e6189 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -88,5 +88,8 @@ export enum BattlerTagType { IMPRISON = "IMPRISON", SYRUP_BOMB = "SYRUP_BOMB", ELECTRIFIED = "ELECTRIFIED", - TELEKINESIS = "TELEKINESIS" + TELEKINESIS = "TELEKINESIS", + COMMANDED = "COMMANDED", + GRUDGE = "GRUDGE", + PSYCHO_SHIFT = "PSYCHO_SHIFT", } diff --git a/src/enums/pokemon-anim-type.ts b/src/enums/pokemon-anim-type.ts index 5a0a0c2f622..b153fb2e652 100644 --- a/src/enums/pokemon-anim-type.ts +++ b/src/enums/pokemon-anim-type.ts @@ -12,5 +12,15 @@ export enum PokemonAnimType { * Removes a Pokemon's Substitute doll from the field. * The Pokemon then moves back to its original position. */ - SUBSTITUTE_REMOVE + SUBSTITUTE_REMOVE, + /** + * Brings Tatsugiri and Dondozo to the center of the field, with + * Tatsugiri jumping into the Dondozo's mouth + */ + COMMANDER_APPLY, + /** + * Dondozo "spits out" Tatsugiri, moving Tatsugiri back to its original + * field position. + */ + COMMANDER_REMOVE } diff --git a/src/enums/type.ts b/src/enums/type.ts new file mode 100644 index 00000000000..a04849c2ca3 --- /dev/null +++ b/src/enums/type.ts @@ -0,0 +1,22 @@ +export enum Type { + UNKNOWN = -1, + NORMAL = 0, + FIGHTING, + FLYING, + POISON, + GROUND, + ROCK, + BUG, + GHOST, + STEEL, + FIRE, + WATER, + GRASS, + ELECTRIC, + PSYCHIC, + ICE, + DRAGON, + DARK, + FAIRY, + STELLAR +} diff --git a/src/events/arena.ts b/src/events/arena.ts index c05e67d353c..b1126e5c03d 100644 --- a/src/events/arena.ts +++ b/src/events/arena.ts @@ -1,7 +1,7 @@ import { ArenaTagSide } from "#app/data/arena-tag"; import { ArenaTagType } from "#enums/arena-tag-type"; import { TerrainType } from "#app/data/terrain"; -import { WeatherType } from "#app/data/weather"; +import { WeatherType } from "#enums/weather-type"; /** Alias for all {@linkcode ArenaEvent} type strings */ export enum ArenaEventType { diff --git a/src/field/anims.ts b/src/field/anims.ts index c73c52027c5..dddf38e4a7e 100644 --- a/src/field/anims.ts +++ b/src/field/anims.ts @@ -1,5 +1,5 @@ import BattleScene from "../battle-scene"; -import { PokeballType } from "../data/pokeball"; +import { PokeballType } from "#enums/pokeball"; import * as Utils from "../utils"; export function addPokeballOpenParticles(scene: BattleScene, x: number, y: number, pokeballType: PokeballType): void { diff --git a/src/field/arena.ts b/src/field/arena.ts index abc2b89569c..da74063ac06 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -3,9 +3,9 @@ import { biomePokemonPools, BiomePoolTier, BiomeTierTrainerPools, biomeTrainerPo import { Constructor } from "#app/utils"; import * as Utils from "#app/utils"; import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species"; -import { getTerrainClearMessage, getTerrainStartMessage, getWeatherClearMessage, getWeatherStartMessage, Weather, WeatherType } from "#app/data/weather"; +import { getTerrainClearMessage, getTerrainStartMessage, getWeatherClearMessage, getWeatherStartMessage, Weather } from "#app/data/weather"; import { CommonAnim } from "#app/data/battle-anims"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import Move from "#app/data/move"; import { ArenaTag, ArenaTagSide, ArenaTrapTag, getArenaTag } from "#app/data/arena-tag"; import { BattlerIndex } from "#app/battle"; @@ -31,6 +31,7 @@ import { Abilities } from "#enums/abilities"; import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#app/data/pokemon-forms"; import { CommonAnimPhase } from "#app/phases/common-anim-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; +import { WeatherType } from "#enums/weather-type"; export class Arena { public scene: BattleScene; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 3a8ab2f6bf3..25140986f09 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -7,22 +7,23 @@ import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, Varia import { default as PokemonSpecies, PokemonSpeciesForm, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER, getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; import { starterPassiveAbilities } from "#app/data/balance/passives"; -import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils"; +import { Constructor, isNullOrUndefined, randSeedInt, type nil } from "#app/utils"; import * as Utils from "#app/utils"; -import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "#app/data/type"; +import { TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "#app/data/type"; +import { Type } from "#enums/type"; import { getLevelTotalExp } from "#app/data/exp"; import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat"; import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier, PokemonMultiHitModifier } from "#app/modifier/modifier"; -import { PokeballType } from "#app/data/pokeball"; +import { PokeballType } from "#enums/pokeball"; import { Gender } from "#app/data/gender"; import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; -import { Status, StatusEffect, getRandomStatus } from "#app/data/status-effect"; +import { Status, getRandomStatus } from "#app/data/status-effect"; import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions"; import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/tms"; import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags"; -import { WeatherType } from "#app/data/weather"; +import { WeatherType } from "#enums/weather-type"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; -import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, PostDamageForceSwitchAbAttr } from "#app/data/ability"; +import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, PostDamageForceSwitchAbAttr, CommanderAbAttr } from "#app/data/ability"; import PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -32,7 +33,7 @@ import { LevelMoves } from "#app/data/balance/pokemon-level-moves"; import { DamageAchv, achvs } from "#app/system/achv"; import { DexAttr, StarterDataEntry, StarterMoveset } from "#app/system/game-data"; import { QuantizerCelebi, argbFromRgba, rgbaFromArgb } from "@material/material-color-utilities"; -import { Nature, getNatureStatMultiplier } from "#app/data/nature"; +import { getNatureStatMultiplier } from "#app/data/nature"; import { SpeciesFormChange, SpeciesFormChangeActiveTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangePostMoveTrigger, SpeciesFormChangeStatusEffectTrigger } from "#app/data/pokemon-forms"; import { TerrainType } from "#app/data/terrain"; import { TrainerSlot } from "#app/data/trainer-config"; @@ -66,6 +67,8 @@ import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { SwitchType } from "#enums/switch-type"; import { SpeciesFormKey } from "#enums/species-form-key"; import { BASE_HIDDEN_ABILITY_CHANCE, BASE_SHINY_CHANCE, SHINY_EPIC_CHANCE, SHINY_VARIANT_CHANCE } from "#app/data/balance/rates"; +import { Nature } from "#enums/nature"; +import { StatusEffect } from "#enums/status-effect"; export enum FieldPosition { CENTER, @@ -1568,6 +1571,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @returns `true` if the pokemon is trapped */ public isTrapped(trappedAbMessages: string[] = [], simulated: boolean = true): boolean { + const commandedTag = this.getTag(BattlerTagType.COMMANDED); + if (commandedTag?.getSourcePokemon(this.scene)?.isActive(true)) { + return true; + } + if (this.isOfType(Type.GHOST)) { return false; } @@ -2841,6 +2849,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // 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); const isOneHitKo = result === HitResult.ONE_HIT_KO; @@ -2912,13 +2921,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (this.isFainted()) { // set splice index here, so future scene queues happen before FaintedPhase this.scene.setPhaseQueueSplice(); - if (!isNullOrUndefined(destinyTag) && dmg) { - // Destiny Bond will activate during FaintPhase - this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo, destinyTag, source)); - } else { - this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo)); - } + this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo, destinyTag, grudgeTag, source)); + this.destroySubstitute(); + this.lapseTag(BattlerTagType.COMMANDED); this.resetSummonData(); } @@ -2967,6 +2973,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.scene.setPhaseQueueSplice(); this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), preventEndure)); this.destroySubstitute(); + this.lapseTag(BattlerTagType.COMMANDED); this.resetSummonData(); } return damage; @@ -3049,19 +3056,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } /** @overload */ - getTag(tagType: BattlerTagType): BattlerTag | null; + getTag(tagType: BattlerTagType): BattlerTag | nil; /** @overload */ - getTag(tagType: Constructor): T | null; + getTag(tagType: Constructor): T | nil; - getTag(tagType: BattlerTagType | Constructor): BattlerTag | null { + getTag(tagType: BattlerTagType | Constructor): BattlerTag | nil { if (!this.summonData) { return null; } return (tagType instanceof Function ? this.summonData.tags.find(t => t instanceof tagType) : this.summonData.tags.find(t => t.tagType === tagType) - )!; // TODO: is this bang correct? + ); } findTag(tagFilter: ((tag: BattlerTag) => boolean)) { @@ -3079,7 +3086,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } lapseTag(tagType: BattlerTagType): boolean { - const tags = this.summonData.tags; + const tags = this.summonData?.tags; + if (isNullOrUndefined(tags)) { + return false; + } const tag = tags.find(t => t.tagType === tagType); if (tag && !(tag.lapse(this, BattlerTagLapseType.CUSTOM))) { tag.onRemove(this); @@ -3221,9 +3231,21 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.getMoveHistory().push(turnMove); } - getLastXMoves(turnCount: integer = 0): TurnMove[] { + /** + * Returns a list of the most recent move entries in this Pokemon's move history. + * The retrieved move entries are sorted in order from NEWEST to OLDEST. + * @param moveCount The number of move entries to retrieve. + * If negative, retrieve the Pokemon's entire move history (equivalent to reversing the output of {@linkcode getMoveHistory()}). + * Default is `1`. + * @returns A list of {@linkcode TurnMove}, as specified above. + */ + getLastXMoves(moveCount: number = 1): TurnMove[] { const moveHistory = this.getMoveHistory(); - return moveHistory.slice(turnCount >= 0 ? Math.max(moveHistory.length - (turnCount || 1), 0) : 0, moveHistory.length).reverse(); + if (moveCount >= 0) { + return moveHistory.slice(Math.max(moveHistory.length - moveCount, 0)).reverse(); + } else { + return moveHistory.slice(0).reverse(); + } } getMoveQueue(): QueuedMove[] { @@ -3644,6 +3666,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.scene.triggerPokemonBattleAnim(this, PokemonAnimType.SUBSTITUTE_ADD); this.getTag(SubstituteTag)!.sourceInFocus = false; } + + // If this Pokemon has Commander and Dondozo as an active ally, hide this Pokemon's sprite. + if (this.hasAbilityWithAttr(CommanderAbAttr) + && this.scene.currentBattle.double + && this.getAlly()?.species.speciesId === Species.DONDOZO) { + this.setVisible(false); + } this.summonDataPrimer = null; } this.updateInfo(); @@ -4027,8 +4056,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.resetTurnData(); if (clearEffects) { this.destroySubstitute(); - this.resetSummonData(); - this.resetBattleData(); + this.resetSummonData(); // this also calls `resetBattleSummonData` } if (hideInfo) { this.hideInfo(); @@ -5080,26 +5108,6 @@ export class EnemyPokemon extends Pokemon { } } - heal(amount: integer): integer { - if (this.isBoss()) { - const amountRatio = amount / this.getMaxHp(); - const segmentBypassCount = Math.floor(amountRatio / (1 / this.bossSegments)); - const segmentSize = this.getMaxHp() / this.bossSegments; - for (let s = 1; s < this.bossSegments; s++) { - const hpThreshold = segmentSize * s; - if (this.hp <= Math.round(hpThreshold)) { - const healAmount = Math.min(amount, this.getMaxHp() - this.hp, Math.round(hpThreshold + (segmentSize * segmentBypassCount) - this.hp)); - this.hp += healAmount; - return healAmount; - } else if (s >= this.bossSegmentIndex) { - return super.heal(amount); - } - } - } - - return super.heal(amount); - } - getFieldIndex(): integer { return this.scene.getEnemyField().indexOf(this); } diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index dfa46ce3667..ae1aef3ff88 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -3,30 +3,31 @@ import { EvolutionItem, pokemonEvolutions } from "#app/data/balance/pokemon-evol import { tmPoolTiers, tmSpecies } from "#app/data/balance/tms"; import { getBerryEffectDescription, getBerryName } from "#app/data/berry"; import { allMoves, AttackMove, selfStatLowerMoves } from "#app/data/move"; -import { getNatureName, getNatureStatMultiplier, Nature } from "#app/data/nature"; -import { getPokeballCatchMultiplier, getPokeballName, MAX_PER_TYPE_POKEBALLS, PokeballType } from "#app/data/pokeball"; +import { getNatureName, getNatureStatMultiplier } from "#app/data/nature"; +import { getPokeballCatchMultiplier, getPokeballName, MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball"; import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeCondition, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms"; -import { getStatusEffectDescriptor, StatusEffect } from "#app/data/status-effect"; -import { Type } from "#app/data/type"; +import { getStatusEffectDescriptor } from "#app/data/status-effect"; +import { Type } from "#enums/type"; import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { - AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier -} from "#app/modifier/modifier"; +import { AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier, CriticalCatchChanceBoosterModifier } from "#app/modifier/modifier"; import { ModifierTier } from "#app/modifier/modifier-tier"; import Overrides from "#app/overrides"; import { Unlockables } from "#app/system/unlockables"; import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher"; import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler"; import { getModifierTierTextTint } from "#app/ui/text"; -import { formatMoney, getEnumKeys, getEnumValues, IntegerHolder, isNullOrUndefined, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils"; +import { formatMoney, getEnumKeys, getEnumValues, isNullOrUndefined, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; import { Moves } from "#enums/moves"; +import { Nature } from "#enums/nature"; +import { PokeballType } from "#enums/pokeball"; import { Species } from "#enums/species"; import { SpeciesFormKey } from "#enums/species-form-key"; import { getStatKey, PermanentStat, Stat, TEMP_BATTLE_STATS, TempBattleStat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; import i18next from "i18next"; const outputModifierData = false; @@ -730,7 +731,7 @@ export class MoneyRewardModifierType extends ModifierType { } getDescription(scene: BattleScene): string { - const moneyAmount = new IntegerHolder(scene.getWaveMoneyAmount(this.moneyMultiplier)); + const moneyAmount = new NumberHolder(scene.getWaveMoneyAmount(this.moneyMultiplier)); scene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount); const formattedMoney = formatMoney(scene.moneyFormat, moneyAmount.value); @@ -1553,6 +1554,7 @@ export const modifierTypes = { SHINY_CHARM: () => new ModifierType("modifierType:ModifierType.SHINY_CHARM", "shiny_charm", (type, _args) => new ShinyRateBoosterModifier(type)), ABILITY_CHARM: () => new ModifierType("modifierType:ModifierType.ABILITY_CHARM", "ability_charm", (type, _args) => new HiddenAbilityRateBoosterModifier(type)), + CATCHING_CHARM: () => new ModifierType("modifierType:ModifierType.CATCHING_CHARM", "catching_charm", (type, _args) => new CriticalCatchChanceBoosterModifier(type)), IV_SCANNER: () => new ModifierType("modifierType:ModifierType.IV_SCANNER", "scanner", (type, _args) => new IvScannerModifier(type)), @@ -1619,19 +1621,21 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.POKEBALL, (party: Pokemon[]) => (hasMaximumBalls(party, PokeballType.POKEBALL)) ? 0 : 6, 6), new WeightedModifierType(modifierTypes.RARE_CANDY, 2), new WeightedModifierType(modifierTypes.POTION, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 10 || p.getHpRatio() <= 0.875) && !p.isFainted()).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 10 && p.getHpRatio() <= 0.875) && !p.isFainted()).length, 3); return thresholdPartyMemberCount * 3; }, 9), new WeightedModifierType(modifierTypes.SUPER_POTION, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 25 || p.getHpRatio() <= 0.75) && !p.isFainted()).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 25 && p.getHpRatio() <= 0.75) && !p.isFainted()).length, 3); return thresholdPartyMemberCount; }, 3), new WeightedModifierType(modifierTypes.ETHER, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed >= Math.floor(m.getMovePp() / 2)).length).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) + && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)).length).length, 3); return thresholdPartyMemberCount * 3; }, 9), new WeightedModifierType(modifierTypes.MAX_ETHER, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed >= Math.floor(m.getMovePp() / 2)).length).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) + && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)).length).length, 3); return thresholdPartyMemberCount; }, 3), new WeightedModifierType(modifierTypes.LURE, lureWeightFunc(10, 2)), @@ -1665,11 +1669,11 @@ const modifierPool: ModifierPool = { return party.filter(p => p.isFainted()).length >= Math.ceil(party.length / 2) ? 1 : 0; }, 1), new WeightedModifierType(modifierTypes.HYPER_POTION, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 100 || p.getHpRatio() <= 0.625) && !p.isFainted()).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 100 && p.getHpRatio() <= 0.625) && !p.isFainted()).length, 3); return thresholdPartyMemberCount * 3; }, 9), new WeightedModifierType(modifierTypes.MAX_POTION, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 150 || p.getHpRatio() <= 0.5) && !p.isFainted()).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => (p.getInverseHp() >= 100 && p.getHpRatio() <= 0.5) && !p.isFainted()).length, 3); return thresholdPartyMemberCount; }, 3), new WeightedModifierType(modifierTypes.FULL_RESTORE, (party: Pokemon[]) => { @@ -1679,15 +1683,17 @@ const modifierPool: ModifierPool = { } return false; })).length, 3); - const thresholdPartyMemberCount = Math.floor((Math.min(party.filter(p => (p.getInverseHp() >= 150 || p.getHpRatio() <= 0.5) && !p.isFainted()).length, 3) + statusEffectPartyMemberCount) / 2); + const thresholdPartyMemberCount = Math.floor((Math.min(party.filter(p => (p.getInverseHp() >= 100 && p.getHpRatio() <= 0.5) && !p.isFainted()).length, 3) + statusEffectPartyMemberCount) / 2); return thresholdPartyMemberCount; }, 3), new WeightedModifierType(modifierTypes.ELIXIR, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed >= Math.floor(m.getMovePp() / 2)).length).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) + && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)).length).length, 3); return thresholdPartyMemberCount * 3; }, 9), new WeightedModifierType(modifierTypes.MAX_ELIXIR, (party: Pokemon[]) => { - const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed >= Math.floor(m.getMovePp() / 2)).length).length, 3); + const thresholdPartyMemberCount = Math.min(party.filter(p => p.hp && !p.getHeldItems().some(m => m instanceof BerryModifier && m.berryType === BerryType.LEPPA) + && p.getMoveset().filter(m => m?.ppUsed && (m.getMovePp() - m.ppUsed) <= 5 && m.ppUsed > Math.floor(m.getMovePp() / 2)).length).length, 3); return thresholdPartyMemberCount; }, 3), new WeightedModifierType(modifierTypes.DIRE_HIT, 4), @@ -1790,6 +1796,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.BATON, 2), new WeightedModifierType(modifierTypes.SOUL_DEW, 7), //new WeightedModifierType(modifierTypes.OVAL_CHARM, 6), + new WeightedModifierType(modifierTypes.CATCHING_CHARM, (party: Pokemon[]) => !party[0].scene.gameMode.isFreshStartChallenge() && party[0].scene.gameData.getSpeciesCount(d => !!d.caughtAttr) > 100 ? 4 : 0, 4), new WeightedModifierType(modifierTypes.SOOTHE_BELL, (party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 0 : 4), new WeightedModifierType(modifierTypes.ABILITY_CHARM, skipInClassicAfterWave(189, 6)), new WeightedModifierType(modifierTypes.FOCUS_BAND, 5), diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 281cd94d05d..9793d0839f7 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -6,7 +6,7 @@ import { allMoves } from "#app/data/move"; import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball"; import { type FormChangeItem, SpeciesFormChangeItemTrigger, SpeciesFormChangeLapseTeraTrigger, SpeciesFormChangeTeraTrigger } from "#app/data/pokemon-forms"; import { getStatusEffectHealText } from "#app/data/status-effect"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import Pokemon, { type PlayerPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; @@ -31,6 +31,7 @@ import i18next from "i18next"; import { type DoubleBattleChanceBoosterModifierType, type EvolutionItemModifierType, type FormChangeItemModifierType, type ModifierOverride, type ModifierType, type PokemonBaseStatTotalModifierType, type PokemonExpBoosterModifierType, type PokemonFriendshipBoosterModifierType, type PokemonMoveAccuracyBoosterModifierType, type PokemonMultiHitModifierType, type TerastallizeModifierType, type TmModifierType, getModifierType, ModifierPoolType, ModifierTypeGenerator, modifierTypes, PokemonHeldItemModifierType } from "./modifier-type"; import { Color, ShadowColor } from "#enums/color"; import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#app/data/balance/starters"; +import { applyAbAttrs, CommanderAbAttr } from "#app/data/ability"; export type ModifierPredicate = (modifier: Modifier) => boolean; @@ -727,10 +728,10 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier { //Applies to items with chance of activating secondary effects ie Kings Rock getSecondaryChanceMultiplier(pokemon: Pokemon): number { // Temporary quickfix to stop game from freezing when the opponet uses u-turn while holding on to king's rock - if (!pokemon.getLastXMoves(0)[0]) { + if (!pokemon.getLastXMoves()[0]) { return 1; } - const sheerForceAffected = allMoves[pokemon.getLastXMoves(0)[0].move].chance >= 0 && pokemon.hasAbility(Abilities.SHEER_FORCE); + const sheerForceAffected = allMoves[pokemon.getLastXMoves()[0].move].chance >= 0 && pokemon.hasAbility(Abilities.SHEER_FORCE); if (sheerForceAffected) { return 0; @@ -1937,10 +1938,16 @@ export class PokemonInstantReviveModifier extends PokemonHeldItemModifier { * @returns always `true` */ override apply(pokemon: Pokemon): boolean { + // Restore the Pokemon to half HP pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(), toDmgValue(pokemon.getMaxHp() / 2), i18next.t("modifier:pokemonInstantReviveApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), false, false, true)); + // Remove the Pokemon's FAINT status pokemon.resetStatus(true, false, true); + + // Reapply Commander on the Pokemon's side of the field, if applicable + const field = pokemon.isPlayer() ? pokemon.scene.getPlayerField() : pokemon.scene.getEnemyField(); + field.forEach((p) => applyAbAttrs(CommanderAbAttr, p, null, false)); return true; } @@ -2944,6 +2951,38 @@ export class ShinyRateBoosterModifier extends PersistentModifier { } } +export class CriticalCatchChanceBoosterModifier extends PersistentModifier { + constructor(type: ModifierType, stackCount?: number) { + super(type, stackCount); + } + + match(modifier: Modifier): boolean { + return modifier instanceof CriticalCatchChanceBoosterModifier; + } + + clone(): CriticalCatchChanceBoosterModifier { + return new CriticalCatchChanceBoosterModifier(this.type, this.stackCount); + } + + /** + * Applies {@linkcode CriticalCatchChanceBoosterModifier} + * @param boost {@linkcode NumberHolder} holding the boost value + * @returns always `true` + */ + override apply(boost: NumberHolder): boolean { + // 1 stack: 2x + // 2 stack: 2.5x + // 3 stack: 3x + boost.value *= 1.5 + this.getStackCount() / 2; + + return true; + } + + getMaxStackCount(scene: BattleScene): number { + return 3; + } +} + export class LockModifierTiersModifier extends PersistentModifier { constructor(type: ModifierType, stackCount?: number) { super(type, stackCount); diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index 483e6eac943..de10d1eca45 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -2,7 +2,7 @@ import { BattlerIndex } from "#app/battle"; import BattleScene from "#app/battle-scene"; import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; import { SubstituteTag } from "#app/data/battler-tags"; -import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor } from "#app/data/pokeball"; +import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, getCriticalCaptureChance } from "#app/data/pokeball"; import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims"; import { EnemyPokemon } from "#app/field/pokemon"; @@ -52,8 +52,10 @@ export class AttemptCapturePhase extends PokemonPhase { const catchRate = pokemon.species.catchRate; const pokeballMultiplier = getPokeballCatchMultiplier(this.pokeballType); const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1; - const x = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier); - const y = Math.round(65536 / Math.sqrt(Math.sqrt(255 / x))); + const modifiedCatchRate = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier); + const shakeProbability = Math.round(65536 / Math.pow((255 / modifiedCatchRate), 0.1875)); // Formula taken from gen 6 + const criticalCaptureChance = getCriticalCaptureChance(this.scene, modifiedCatchRate); + const isCritical = pokemon.randSeedInt(256) < criticalCaptureChance; const fpOffset = pokemon.getFieldPositionOffset(); const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType); @@ -61,17 +63,19 @@ export class AttemptCapturePhase extends PokemonPhase { this.pokeball.setOrigin(0.5, 0.625); this.scene.field.add(this.pokeball); - this.scene.playSound("se/pb_throw"); + this.scene.playSound("se/pb_throw", isCritical ? { rate: 0.2 } : undefined); // Crit catch throws are higher pitched this.scene.time.delayedCall(300, () => { this.scene.field.moveBelow(this.pokeball as Phaser.GameObjects.GameObject, pokemon); }); this.scene.tweens.add({ + // Throw animation targets: this.pokeball, x: { value: 236 + fpOffset[0], ease: "Linear" }, y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" }, duration: 500, onComplete: () => { + // Ball opens this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); this.scene.playSound("se/pb_rel"); @@ -80,30 +84,33 @@ export class AttemptCapturePhase extends PokemonPhase { addPokeballOpenParticles(this.scene, this.pokeball.x, this.pokeball.y, this.pokeballType); this.scene.tweens.add({ + // Mon enters ball targets: pokemon, duration: 500, ease: "Sine.easeIn", scale: 0.25, y: 20, onComplete: () => { + // Ball closes this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); pokemon.setVisible(false); this.scene.playSound("se/pb_catch"); this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}`)); const doShake = () => { + // After the overall catch rate check, the game does 3 shake checks before confirming the catch. let shakeCount = 0; const pbX = this.pokeball.x; const shakeCounter = this.scene.tweens.addCounter({ from: 0, to: 1, - repeat: 4, + repeat: isCritical ? 2 : 4, // Critical captures only perform 1 shake check yoyo: true, ease: "Cubic.easeOut", duration: 250, repeatDelay: 500, onUpdate: t => { - if (shakeCount && shakeCount < 4) { + if (shakeCount && shakeCount < (isCritical ? 2 : 4)) { const value = t.getValue(); const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1; this.pokeball.setX(pbX + value * 4 * directionMultiplier); @@ -114,13 +121,18 @@ export class AttemptCapturePhase extends PokemonPhase { if (!pokemon.species.isObtainable()) { shakeCounter.stop(); this.failCatch(shakeCount); - } else if (shakeCount++ < 3) { - if (pokeballMultiplier === -1 || pokemon.randSeedInt(65536) < y) { + } else if (shakeCount++ < (isCritical ? 1 : 3)) { + // Shake check (skip check for critical or guaranteed captures, but still play the sound) + if (pokeballMultiplier === -1 || isCritical || modifiedCatchRate >= 255 || pokemon.randSeedInt(65536) < shakeProbability) { this.scene.playSound("se/pb_move"); } else { shakeCounter.stop(); this.failCatch(shakeCount); } + } else if (isCritical && pokemon.randSeedInt(65536) >= shakeProbability) { + // Above, perform the one shake check for critical captures after the ball shakes once + shakeCounter.stop(); + this.failCatch(shakeCount); } else { this.scene.playSound("se/pb_lock"); addPokeballCaptureStars(this.scene, this.pokeball); @@ -153,7 +165,8 @@ export class AttemptCapturePhase extends PokemonPhase { }); }; - this.scene.time.delayedCall(250, () => doPokeballBounceAnim(this.scene, this.pokeball, 16, 72, 350, doShake)); + // Ball bounces (handled in pokemon.ts) + this.scene.time.delayedCall(250, () => doPokeballBounceAnim(this.scene, this.pokeball, 16, 72, 350, doShake, isCritical)); } }); } diff --git a/src/phases/check-switch-phase.ts b/src/phases/check-switch-phase.ts index b87dff32f60..acf17c75668 100644 --- a/src/phases/check-switch-phase.ts +++ b/src/phases/check-switch-phase.ts @@ -26,25 +26,29 @@ export class CheckSwitchPhase extends BattlePhase { const pokemon = this.scene.getPlayerField()[this.fieldIndex]; + // End this phase early... + + // ...if the user is playing in Set Mode if (this.scene.battleStyle === BattleStyle.SET) { - super.end(); - return; + return super.end(); } + // ...if the checked Pokemon is somehow not on the field if (this.scene.field.getAll().indexOf(pokemon) === -1) { this.scene.unshiftPhase(new SummonMissingPhase(this.scene, this.fieldIndex)); - super.end(); - return; + return super.end(); } + // ...if there are no other allowed Pokemon in the player's party to switch with if (!this.scene.getPlayerParty().slice(1).filter(p => p.isActive()).length) { - super.end(); - return; + return super.end(); } - if (pokemon.getTag(BattlerTagType.FRENZY)) { - super.end(); - return; + // ...or if any player Pokemon has an effect that prevents the checked Pokemon from switching + if (pokemon.getTag(BattlerTagType.FRENZY) + || pokemon.isTrapped() + || this.scene.getPlayerField().some(p => p.getTag(BattlerTagType.COMMANDED))) { + return super.end(); } this.scene.ui.showText(i18next.t("battle:switchQuestion", { pokemonName: this.useName ? getPokemonNameWithAffix(pokemon) : i18next.t("battle:pokemon") }), null, () => { diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index c1d38815c2f..eab76282908 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -7,7 +7,7 @@ import { Abilities } from "#app/enums/abilities"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { Biome } from "#app/enums/biome"; import { Moves } from "#app/enums/moves"; -import { PokeballType } from "#app/enums/pokeball"; +import { PokeballType } from "#enums/pokeball"; import { FieldPosition, PlayerPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { Command } from "#app/ui/command-ui-handler"; @@ -32,6 +32,8 @@ export class CommandPhase extends FieldPhase { start() { super.start(); + this.scene.updateGameInfo(); + const commandUiHandler = this.scene.ui.handlers[Mode.COMMAND]; if (commandUiHandler) { if (this.scene.currentBattle.turn === 1 || commandUiHandler.getCursor() === Command.POKEMON) { @@ -54,6 +56,17 @@ export class CommandPhase extends FieldPhase { } } + // If the Pokemon has applied Commander's effects to its ally, skip this command + if (this.scene.currentBattle?.double && this.getPokemon().getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon(this.scene) === this.getPokemon()) { + this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: Command.FIGHT, move: { move: Moves.NONE, targets: []}, skip: true }; + } + + // Checks if the Pokemon is under the effects of Encore. If so, Encore can end early if the encored move has no more PP. + const encoreTag = this.getPokemon().getTag(BattlerTagType.ENCORE) as EncoreTag; + if (encoreTag) { + this.getPokemon().lapseTag(BattlerTagType.ENCORE); + } + if (this.scene.currentBattle.turnCommands[this.fieldIndex]?.skip) { return this.end(); } @@ -92,7 +105,7 @@ export class CommandPhase extends FieldPhase { handleCommand(command: Command, cursor: integer, ...args: any[]): boolean { const playerPokemon = this.scene.getPlayerField()[this.fieldIndex]; - let success: boolean; + let success: boolean = false; switch (command) { case Command.FIGHT: @@ -232,11 +245,8 @@ export class CommandPhase extends FieldPhase { const trapTag = playerPokemon.getTag(TrappedTag); const fairyLockTag = playerPokemon.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER); - // trapTag should be defined at this point, but just in case... if (!trapTag && !fairyLockTag) { - currentBattle.turnCommands[this.fieldIndex] = isSwitch - ? { command: Command.POKEMON, cursor: cursor, args: args } - : { command: Command.RUN }; + i18next.t(`battle:noEscape${isSwitch ? "Switch" : "Flee"}`); break; } if (!isSwitch) { @@ -272,11 +282,11 @@ export class CommandPhase extends FieldPhase { break; } - if (success!) { // TODO: is the bang correct? + if (success) { this.end(); } - return success!; // TODO: is the bang correct? + return success; } cancel() { @@ -287,26 +297,6 @@ export class CommandPhase extends FieldPhase { } } - checkFightOverride(): boolean { - const pokemon = this.getPokemon(); - - const encoreTag = pokemon.getTag(EncoreTag) as EncoreTag; - - if (!encoreTag) { - return false; - } - - const moveIndex = pokemon.getMoveset().findIndex(m => m?.moveId === encoreTag.moveId); - - if (moveIndex === -1 || !pokemon.getMoveset()[moveIndex]!.isUsable(pokemon)) { // TODO: is this bang correct? - return false; - } - - this.handleCommand(Command.FIGHT, moveIndex, false); - - return true; - } - getFieldIndex(): integer { return this.fieldIndex; } diff --git a/src/phases/enemy-command-phase.ts b/src/phases/enemy-command-phase.ts index 3647a237ef1..83a85009ae0 100644 --- a/src/phases/enemy-command-phase.ts +++ b/src/phases/enemy-command-phase.ts @@ -2,6 +2,8 @@ import BattleScene from "#app/battle-scene"; import { BattlerIndex } from "#app/battle"; import { Command } from "#app/ui/command-ui-handler"; import { FieldPhase } from "./field-phase"; +import { Abilities } from "#enums/abilities"; +import { BattlerTagType } from "#enums/battler-tag-type"; /** * Phase for determining an enemy AI's action for the next turn. @@ -34,6 +36,11 @@ export class EnemyCommandPhase extends FieldPhase { const trainer = battle.trainer; + if (battle.double && enemyPokemon.hasAbility(Abilities.COMMANDER) + && enemyPokemon.getAlly().getTag(BattlerTagType.COMMANDED)) { + this.skipTurn = true; + } + /** * If the enemy has a trainer, decide whether or not the enemy should switch * to another member in its party. diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 3e90233a38c..d66c5b66144 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -1,7 +1,7 @@ import { BattlerIndex, BattleType } from "#app/battle"; import BattleScene from "#app/battle-scene"; import { applyPostFaintAbAttrs, applyPostKnockOutAbAttrs, applyPostVictoryAbAttrs, PostFaintAbAttr, PostKnockOutAbAttr, PostVictoryAbAttr } from "#app/data/ability"; -import { BattlerTagLapseType, DestinyBondTag } from "#app/data/battler-tags"; +import { BattlerTagLapseType, DestinyBondTag, GrudgeTag } from "#app/data/battler-tags"; import { battleSpecDialogue } from "#app/data/dialogue"; import { allMoves, PostVictoryStatStageChangeAttr } from "#app/data/move"; import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; @@ -31,18 +31,24 @@ export class FaintPhase extends PokemonPhase { /** * Destiny Bond tag belonging to the currently fainting Pokemon, if applicable */ - private destinyTag?: DestinyBondTag; + private destinyTag?: DestinyBondTag | null; /** - * The source Pokemon that dealt fatal damage and should get KO'd by Destiny Bond, if applicable + * Grudge tag belonging to the currently fainting Pokemon, if applicable + */ + private grudgeTag?: GrudgeTag | null; + + /** + * The source Pokemon that dealt fatal damage */ private source?: Pokemon; - constructor(scene: BattleScene, battlerIndex: BattlerIndex, preventEndure: boolean = false, destinyTag?: DestinyBondTag, source?: Pokemon) { + constructor(scene: BattleScene, battlerIndex: BattlerIndex, preventEndure: boolean = false, destinyTag?: DestinyBondTag | null, grudgeTag?: GrudgeTag | null, source?: Pokemon) { super(scene, battlerIndex); this.preventEndure = preventEndure; this.destinyTag = destinyTag; + this.grudgeTag = grudgeTag; this.source = source; } @@ -53,6 +59,10 @@ export class FaintPhase extends PokemonPhase { this.destinyTag.lapse(this.source, BattlerTagLapseType.CUSTOM); } + if (!isNullOrUndefined(this.grudgeTag) && !isNullOrUndefined(this.source)) { + this.grudgeTag.lapse(this.getPokemon(), BattlerTagLapseType.CUSTOM, this.source); + } + if (!this.preventEndure) { const instantReviveModifier = this.scene.applyModifier(PokemonInstantReviveModifier, this.player, this.getPokemon()) as PokemonInstantReviveModifier; diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 62ece3769b5..ef863d64c50 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -43,7 +43,7 @@ import { VariableTargetAttr, } from "#app/data/move"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import Pokemon, { HitResult, MoveResult, PokemonMove } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { @@ -211,11 +211,14 @@ export class MoveEffectPhase extends PokemonPhase { && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) && !target.getTag(SemiInvulnerableTag); + /** Is the target hidden by the effects of its Commander ability? */ + const isCommanding = this.scene.currentBattle.double && target.getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon(this.scene) === target; + /** * If the move missed a target, stop all future hits against that target * and move on to the next target (if there is one). */ - if (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()]) { + if (isCommanding || (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()])) { this.stopMultiHit(target); this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) })); if (moveHistoryEntry.result === MoveResult.PENDING) { diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index b11a7d9b711..6bdef281d70 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -28,7 +28,7 @@ import { } from "#app/data/move"; import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms"; import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { getTerrainBlockMessage } from "#app/data/weather"; import { MoveUsedEvent } from "#app/events/battle-scene"; import Pokemon, { MoveResult, PokemonMove } from "#app/field/pokemon"; diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index e7d1f7e9074..2d1c3c4ae31 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -417,6 +417,7 @@ export class MysteryEncounterBattlePhase extends Phase { } } else { if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) { + scene.getPlayerField().forEach((pokemon) => pokemon.lapseTag(BattlerTagType.COMMANDED)); scene.pushPhase(new ReturnPhase(scene, 1)); } scene.pushPhase(new ToggleDoublePositionPhase(scene, false)); diff --git a/src/phases/pokemon-anim-phase.ts b/src/phases/pokemon-anim-phase.ts index 26ae11d1026..ad0be34af7d 100644 --- a/src/phases/pokemon-anim-phase.ts +++ b/src/phases/pokemon-anim-phase.ts @@ -1,8 +1,10 @@ import BattleScene from "#app/battle-scene"; import { SubstituteTag } from "#app/data/battler-tags"; -import { PokemonAnimType } from "#enums/pokemon-anim-type"; import Pokemon from "#app/field/pokemon"; import { BattlePhase } from "#app/phases/battle-phase"; +import { isNullOrUndefined } from "#app/utils"; +import { PokemonAnimType } from "#enums/pokemon-anim-type"; +import { Species } from "#enums/species"; export class PokemonAnimPhase extends BattlePhase { @@ -37,14 +39,20 @@ export class PokemonAnimPhase extends BattlePhase { case PokemonAnimType.SUBSTITUTE_REMOVE: this.doSubstituteRemoveAnim(); break; + case PokemonAnimType.COMMANDER_APPLY: + this.doCommanderApplyAnim(); + break; + case PokemonAnimType.COMMANDER_REMOVE: + this.doCommanderRemoveAnim(); + break; default: this.end(); } } - doSubstituteAddAnim(): void { + private doSubstituteAddAnim(): void { const substitute = this.pokemon.getTag(SubstituteTag); - if (substitute === null) { + if (isNullOrUndefined(substitute)) { return this.end(); } @@ -106,7 +114,7 @@ export class PokemonAnimPhase extends BattlePhase { }); } - doSubstitutePreMoveAnim(): void { + private doSubstitutePreMoveAnim(): void { if (this.fieldAssets.length !== 1) { return this.end(); } @@ -135,7 +143,7 @@ export class PokemonAnimPhase extends BattlePhase { }); } - doSubstitutePostMoveAnim(): void { + private doSubstitutePostMoveAnim(): void { if (this.fieldAssets.length !== 1) { return this.end(); } @@ -164,7 +172,7 @@ export class PokemonAnimPhase extends BattlePhase { }); } - doSubstituteRemoveAnim(): void { + private doSubstituteRemoveAnim(): void { if (this.fieldAssets.length !== 1) { return this.end(); } @@ -233,4 +241,121 @@ export class PokemonAnimPhase extends BattlePhase { } }); } + + private doCommanderApplyAnim(): void { + if (!this.scene.currentBattle?.double) { + return this.end(); + } + const dondozo = this.pokemon.getAlly(); + + if (dondozo?.species?.speciesId !== Species.DONDOZO) { + return this.end(); + } + + const tatsugiriX = this.pokemon.x + this.pokemon.getSprite().x; + const tatsugiriY = this.pokemon.y + this.pokemon.getSprite().y; + + const getSourceSprite = () => { + const sprite = this.scene.addPokemonSprite(this.pokemon, tatsugiriX, tatsugiriY, this.pokemon.getSprite().texture, this.pokemon.getSprite()!.frame.name, true); + [ "spriteColors", "fusionSpriteColors" ].map(k => sprite.pipelineData[k] = this.pokemon.getSprite().pipelineData[k]); + sprite.setPipelineData("spriteKey", this.pokemon.getBattleSpriteKey()); + sprite.setPipelineData("shiny", this.pokemon.shiny); + sprite.setPipelineData("variant", this.pokemon.variant); + sprite.setPipelineData("ignoreFieldPos", true); + sprite.setOrigin(0.5, 1); + this.pokemon.getSprite().on("animationupdate", (_anim, frame) => sprite.setFrame(frame.textureFrame)); + this.scene.field.add(sprite); + return sprite; + }; + + const sourceSprite = getSourceSprite(); + + this.pokemon.setVisible(false); + + const sourceFpOffset = this.pokemon.getFieldPositionOffset(); + const dondozoFpOffset = dondozo.getFieldPositionOffset(); + + this.scene.playSound("se/pb_throw"); + + this.scene.tweens.add({ + targets: sourceSprite, + duration: 375, + scale: 0.5, + x: { value: tatsugiriX + (dondozoFpOffset[0] - sourceFpOffset[0]) / 2, ease: "Linear" }, + y: { value: (this.pokemon.isPlayer() ? 100 : 65) + sourceFpOffset[1], ease: "Sine.easeOut" }, + onComplete: () => { + this.scene.field.bringToTop(dondozo); + this.scene.tweens.add({ + targets: sourceSprite, + duration: 375, + scale: 0.01, + x: { value: dondozo.x, ease: "Linear" }, + y: { value: dondozo.y + dondozo.height / 2, ease: "Sine.easeIn" }, + onComplete: () => { + sourceSprite.destroy(); + this.scene.playSound("battle_anims/PRSFX- Liquidation1.wav"); + this.scene.tweens.add({ + targets: dondozo, + duration: 250, + ease: "Sine.easeInOut", + scale: 0.85, + yoyo: true, + onComplete: () => this.end() + }); + } + }); + } + }); + } + + private doCommanderRemoveAnim(): void { + // Note: unlike the other Commander animation, this is played through the + // Dondozo instead of the Tatsugiri. + const tatsugiri = this.pokemon.getAlly(); + + const tatsuSprite = this.scene.addPokemonSprite( + tatsugiri, + this.pokemon.x + this.pokemon.getSprite().x, + this.pokemon.y + this.pokemon.getSprite().y + this.pokemon.height / 2, + tatsugiri.getSprite().texture, + tatsugiri.getSprite()!.frame.name, + true + ); + [ "spriteColors", "fusionSpriteColors" ].map(k => tatsuSprite.pipelineData[k] = tatsugiri.getSprite().pipelineData[k]); + tatsuSprite.setPipelineData("spriteKey", tatsugiri.getBattleSpriteKey()); + tatsuSprite.setPipelineData("shiny", tatsugiri.shiny); + tatsuSprite.setPipelineData("variant", tatsugiri.variant); + tatsuSprite.setPipelineData("ignoreFieldPos", true); + this.pokemon.getSprite().on("animationupdate", (_anim, frame) => tatsuSprite.setFrame(frame.textureFrame)); + + tatsuSprite.setOrigin(0.5, 1); + tatsuSprite.setScale(0.01); + + this.scene.field.add(tatsuSprite); + this.scene.field.bringToTop(this.pokemon); + tatsuSprite.setVisible(true); + + this.scene.tweens.add({ + targets: this.pokemon, + duration: 250, + ease: "Sine.easeInOut", + scale: 1.15, + yoyo: true, + onComplete: () => { + this.scene.playSound("battle_anims/PRSFX- Liquidation4.wav"); + this.scene.tweens.add({ + targets: tatsuSprite, + duration: 500, + scale: 1, + x: { value: tatsugiri.x + tatsugiri.getSprite().x, ease: "Linear" }, + y: { value: tatsugiri.y + tatsugiri.getSprite().y, ease: "Sine.easeIn" }, + onComplete: () => { + tatsugiri.setVisible(true); + tatsuSprite.destroy(); + this.end(); + } + }); + } + }); + } } diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index 3db98d9926c..644a6235a42 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -1,6 +1,6 @@ import BattleScene from "#app/battle-scene"; import { BattlerIndex } from "#app/battle"; -import { applyPostSummonAbAttrs, PostSummonAbAttr } from "#app/data/ability"; +import { applyAbAttrs, applyPostSummonAbAttrs, CommanderAbAttr, PostSummonAbAttr } from "#app/data/ability"; import { ArenaTrapTag } from "#app/data/arena-tag"; import { StatusEffect } from "#app/enums/status-effect"; import { PokemonPhase } from "./pokemon-phase"; @@ -28,5 +28,8 @@ export class PostSummonPhase extends PokemonPhase { } applyPostSummonAbAttrs(PostSummonAbAttr, pokemon).then(() => this.end()); + + const field = pokemon.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField(); + field.forEach((p) => applyAbAttrs(CommanderAbAttr, p, null, false)); } } diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 36db8b7a7e7..51d54315165 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -138,7 +138,6 @@ export class SwitchSummonPhase extends SummonPhase { switchedInPokemon.setAlpha(0.5); } } else { - switchedInPokemon.resetBattleData(); switchedInPokemon.resetSummonData(); } this.summon(); diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index dc3ee3f660a..b48b018a046 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -1,6 +1,6 @@ import BattleScene from "#app/battle-scene"; -import { applyAbAttrs, BypassSpeedChanceAbAttr, PreventBypassSpeedChanceAbAttr, ChangeMovePriorityAbAttr } from "#app/data/ability"; -import { allMoves, applyMoveAttrs, IncrementMovePriorityAttr, MoveHeaderAttr } from "#app/data/move"; +import { applyAbAttrs, BypassSpeedChanceAbAttr, PreventBypassSpeedChanceAbAttr } from "#app/data/ability"; +import { allMoves, MoveHeaderAttr } from "#app/data/move"; import { Abilities } from "#app/enums/abilities"; import { Stat } from "#app/enums/stat"; import Pokemon, { PokemonMove } from "#app/field/pokemon"; @@ -98,26 +98,22 @@ export class TurnStartPhase extends FieldPhase { const aMove = allMoves[aCommand.move!.move]; const bMove = allMoves[bCommand!.move!.move]; - // The game now considers priority and applies the relevant move and ability attributes - const aPriority = new Utils.IntegerHolder(aMove.priority); - const bPriority = new Utils.IntegerHolder(bMove.priority); + const aUser = this.scene.getField(true).find(p => p.getBattlerIndex() === a)!; + const bUser = this.scene.getField(true).find(p => p.getBattlerIndex() === b)!; - applyMoveAttrs(IncrementMovePriorityAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a)!, null, aMove, aPriority); - applyMoveAttrs(IncrementMovePriorityAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b)!, null, bMove, bPriority); - - applyAbAttrs(ChangeMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a)!, null, false, aMove, aPriority); - applyAbAttrs(ChangeMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b)!, null, false, bMove, bPriority); + const aPriority = aMove.getPriority(aUser, false); + const bPriority = bMove.getPriority(bUser, false); // The game now checks for differences in priority levels. // If the moves share the same original priority bracket, it can check for differences in battlerBypassSpeed and return the result. // This conditional is used to ensure that Quick Claw can still activate with abilities like Stall and Mycelium Might (attack moves only) // Otherwise, the game returns the user of the move with the highest priority. - const isSameBracket = Math.ceil(aPriority.value) - Math.ceil(bPriority.value) === 0; - if (aPriority.value !== bPriority.value) { + const isSameBracket = Math.ceil(aPriority) - Math.ceil(bPriority) === 0; + if (aPriority !== bPriority) { if (isSameBracket && battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) { return battlerBypassSpeed[a].value ? -1 : 1; } - return aPriority.value < bPriority.value ? 1 : -1; + return (aPriority < bPriority) ? 1 : -1; } } diff --git a/src/plugins/api/api-base.ts b/src/plugins/api/api-base.ts index 0740de4e675..5c1a30ff3ab 100644 --- a/src/plugins/api/api-base.ts +++ b/src/plugins/api/api-base.ts @@ -69,7 +69,9 @@ export abstract class ApiBase { "Content-Type": config.headers?.["Content-Type"] ?? "application/json", }; - console.log(`Sending ${config.method ?? "GET"} request to: `, this.base + path, config); + if (import.meta.env.DEV) { + console.log(`Sending ${config.method ?? "GET"} request to: `, this.base + path, config); + } return await fetch(this.base + path, config); } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index ede7bd0c986..6ba4933631e 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -22,7 +22,7 @@ import { vouchers, VoucherType } from "#app/system/voucher"; import { AES, enc } from "crypto-js"; import { Mode } from "#app/ui/ui"; import { clientSessionId, loggedInUser, updateUserInfo } from "#app/account"; -import { Nature } from "#app/data/nature"; +import { Nature } from "#enums/nature"; import { GameStats } from "#app/system/game-stats"; import { Tutorial } from "#app/tutorial"; import { speciesEggMoves } from "#app/data/balance/egg-moves"; @@ -33,7 +33,7 @@ import { setSettingGamepad, SettingGamepad, settingGamepadDefaults } from "#app/ import { setSettingKeyboard, SettingKeyboard } from "#app/system/settings/settings-keyboard"; import { TagAddedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena"; import * as Modifier from "#app/modifier/modifier"; -import { StatusEffect } from "#app/data/status-effect"; +import { StatusEffect } from "#enums/status-effect"; import ChallengeData from "#app/system/challenge-data"; import { Device } from "#enums/devices"; import { GameDataType } from "#enums/game-data-type"; diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 485c1d90c1d..443382186c7 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -1,8 +1,8 @@ import { BattleType } from "../battle"; import BattleScene from "../battle-scene"; import { Gender } from "../data/gender"; -import { Nature } from "../data/nature"; -import { PokeballType } from "../data/pokeball"; +import { Nature } from "#enums/nature"; +import { PokeballType } from "#enums/pokeball"; import { getPokemonSpecies } from "../data/pokemon-species"; import { Status } from "../data/status-effect"; import Pokemon, { EnemyPokemon, PokemonMove, PokemonSummonData } from "../field/pokemon"; diff --git a/src/test/abilities/battle_bond.test.ts b/src/test/abilities/battle_bond.test.ts index e9970e1c049..db7ed01e7d9 100644 --- a/src/test/abilities/battle_bond.test.ts +++ b/src/test/abilities/battle_bond.test.ts @@ -1,8 +1,9 @@ import { allMoves, MultiHitAttr, MultiHitType } from "#app/data/move"; -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/src/test/abilities/commander.test.ts b/src/test/abilities/commander.test.ts new file mode 100644 index 00000000000..99b3d50797c --- /dev/null +++ b/src/test/abilities/commander.test.ts @@ -0,0 +1,224 @@ +import { BattlerIndex } from "#app/battle"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { PokemonAnimType } from "#enums/pokemon-anim-type"; +import { EffectiveStat, Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import { WeatherType } from "#enums/weather-type"; +import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Abilities - Commander", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .startingLevel(100) + .enemyLevel(100) + .moveset([ Moves.LIQUIDATION, Moves.MEMENTO, Moves.SPLASH, Moves.FLIP_TURN ]) + .ability(Abilities.COMMANDER) + .battleType("double") + .disableCrits() + .enemySpecies(Species.SNORLAX) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.TACKLE); + + vi.spyOn(game.scene, "triggerPokemonBattleAnim").mockReturnValue(true); + }); + + it("causes the source to jump into Dondozo's mouth, granting a stat boost and hiding the source", async () => { + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + const affectedStats: EffectiveStat[] = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ]; + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + affectedStats.forEach((stat) => expect(dondozo.getStatStage(stat)).toBe(2)); + + game.move.select(Moves.SPLASH, 1); + + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + // Force both enemies to target the Tatsugiri + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER); + + await game.phaseInterceptor.to("BerryPhase", false); + game.scene.getEnemyField().forEach(enemy => expect(enemy.getLastXMoves(1)[0].result).toBe(MoveResult.MISS)); + expect(tatsugiri.isFullHp()).toBeTruthy(); + }); + + it("should activate when a Dondozo switches in and cancel the source's move", async () => { + game.override.enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.MAGIKARP, Species.DONDOZO ]); + + const tatsugiri = game.scene.getPlayerField()[0]; + + game.move.select(Moves.LIQUIDATION, 0, BattlerIndex.ENEMY); + game.doSwitchPokemon(2); + + await game.phaseInterceptor.to("MovePhase", false); + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + + const dondozo = game.scene.getPlayerField()[1]; + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(tatsugiri.getMoveHistory()).toHaveLength(0); + expect(game.scene.getEnemyField()[0].isFullHp()).toBeTruthy(); + }); + + it("source should reenter the field when Dondozo faints", async () => { + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + + game.move.select(Moves.MEMENTO, 1, BattlerIndex.ENEMY); + + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER); + + await game.setTurnOrder([ BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER ]); + + await game.phaseInterceptor.to("FaintPhase", false); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeUndefined(); + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(dondozo, PokemonAnimType.COMMANDER_REMOVE); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(tatsugiri.isFullHp()).toBeFalsy(); + }); + + it("source should still take damage from Poison while hidden", async () => { + game.override + .statusEffect(StatusEffect.POISON) + .enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + + game.move.select(Moves.SPLASH, 1); + + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(tatsugiri.isFullHp()).toBeFalsy(); + }); + + it("source should still take damage from Salt Cure while hidden", async () => { + game.override.enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + + tatsugiri.addTag(BattlerTagType.SALT_CURED, 0, Moves.NONE, game.scene.getField()[BattlerIndex.ENEMY].id); + + game.move.select(Moves.SPLASH, 1); + + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(tatsugiri.isFullHp()).toBeFalsy(); + }); + + it("source should still take damage from Sandstorm while hidden", async () => { + game.override + .weather(WeatherType.SANDSTORM) + .enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + + game.move.select(Moves.SPLASH, 1); + + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(tatsugiri.isFullHp()).toBeFalsy(); + }); + + it("should make Dondozo immune to being forced out", async () => { + game.override.enemyMoveset([ Moves.SPLASH, Moves.WHIRLWIND ]); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + + game.move.select(Moves.SPLASH, 1); + + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.forceEnemyMove(Moves.WHIRLWIND, BattlerIndex.PLAYER_2); + await game.forceEnemyMove(Moves.SPLASH); + + // Test may time out here if Whirlwind forced out a Pokemon + await game.phaseInterceptor.to("TurnEndPhase"); + expect(dondozo.isActive(true)).toBeTruthy(); + }); + + it("should interrupt the source's semi-invulnerability", async () => { + game.override + .moveset([ Moves.SPLASH, Moves.DIVE ]) + .enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.MAGIKARP, Species.DONDOZO ]); + + const tatsugiri = game.scene.getPlayerField()[0]; + + game.move.select(Moves.DIVE, 0, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, 1); + + await game.phaseInterceptor.to("CommandPhase"); + await game.toNextTurn(); + + expect(tatsugiri.getTag(BattlerTagType.UNDERWATER)).toBeDefined(); + game.doSwitchPokemon(2); + + await game.phaseInterceptor.to("MovePhase", false); + const dondozo = game.scene.getPlayerField()[1]; + expect(tatsugiri.getTag(BattlerTagType.UNDERWATER)).toBeUndefined(); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + + await game.toNextTurn(); + const enemy = game.scene.getEnemyField()[0]; + expect(enemy.isFullHp()).toBeTruthy(); + }); +}); diff --git a/src/test/abilities/corrosion.test.ts b/src/test/abilities/corrosion.test.ts new file mode 100644 index 00000000000..e607e85defb --- /dev/null +++ b/src/test/abilities/corrosion.test.ts @@ -0,0 +1,46 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Corrosion", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH ]) + .battleType("single") + .disableCrits() + .enemySpecies(Species.GRIMER) + .enemyAbility(Abilities.CORROSION) + .enemyMoveset(Moves.TOXIC); + }); + + it("If a Poison- or Steel-type Pokémon with this Ability poisons a target with Synchronize, Synchronize does not gain the ability to poison Poison- or Steel-type Pokémon.", async () => { + game.override.ability(Abilities.SYNCHRONIZE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const playerPokemon = game.scene.getPlayerPokemon(); + const enemyPokemon = game.scene.getEnemyPokemon(); + expect(playerPokemon!.status).toBeUndefined(); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + expect(playerPokemon!.status).toBeDefined(); + expect(enemyPokemon!.status).toBeUndefined(); + }); +}); diff --git a/src/test/abilities/disguise.test.ts b/src/test/abilities/disguise.test.ts index f44782207cb..07a84bd7a5a 100644 --- a/src/test/abilities/disguise.test.ts +++ b/src/test/abilities/disguise.test.ts @@ -1,10 +1,10 @@ import { BattlerIndex } from "#app/battle"; -import { StatusEffect } from "#app/data/status-effect"; import { toDmgValue } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/abilities/flash_fire.test.ts b/src/test/abilities/flash_fire.test.ts index f03e1689649..0ca55227ea4 100644 --- a/src/test/abilities/flash_fire.test.ts +++ b/src/test/abilities/flash_fire.test.ts @@ -1,11 +1,11 @@ import { BattlerIndex } from "#app/battle"; -import { StatusEffect } from "#app/data/status-effect"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { Species } from "#app/enums/species"; import { MovePhase } from "#app/phases/move-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/abilities/galvanize.test.ts b/src/test/abilities/galvanize.test.ts index 438f166174d..80e767866ea 100644 --- a/src/test/abilities/galvanize.test.ts +++ b/src/test/abilities/galvanize.test.ts @@ -1,6 +1,6 @@ import { BattlerIndex } from "#app/battle"; import { allMoves } from "#app/data/move"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Abilities } from "#app/enums/abilities"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; diff --git a/src/test/abilities/libero.test.ts b/src/test/abilities/libero.test.ts index aac1cb16d97..e7bc9eeeb63 100644 --- a/src/test/abilities/libero.test.ts +++ b/src/test/abilities/libero.test.ts @@ -1,6 +1,6 @@ import { allMoves } from "#app/data/move"; -import { Type } from "#app/data/type"; -import { Weather, WeatherType } from "#app/data/weather"; +import { Type } from "#enums/type"; +import { Weather } from "#app/data/weather"; import { PlayerPokemon } from "#app/field/pokemon"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; @@ -8,6 +8,7 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; diff --git a/src/test/abilities/magic_guard.test.ts b/src/test/abilities/magic_guard.test.ts index 8075eac66f2..7c038354748 100644 --- a/src/test/abilities/magic_guard.test.ts +++ b/src/test/abilities/magic_guard.test.ts @@ -1,12 +1,13 @@ import { ArenaTagSide, getArenaTag } from "#app/data/arena-tag"; -import { StatusEffect, getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; -import { WeatherType } from "#app/data/weather"; +import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/abilities/mimicry.test.ts b/src/test/abilities/mimicry.test.ts index 8f22de33061..29aa1d649d3 100644 --- a/src/test/abilities/mimicry.test.ts +++ b/src/test/abilities/mimicry.test.ts @@ -1,7 +1,7 @@ import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/abilities/parental_bond.test.ts b/src/test/abilities/parental_bond.test.ts index 993a5eb8344..d8f952ae6ad 100644 --- a/src/test/abilities/parental_bond.test.ts +++ b/src/test/abilities/parental_bond.test.ts @@ -1,11 +1,11 @@ -import { Stat } from "#enums/stat"; -import { StatusEffect } from "#app/data/status-effect"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { toDmgValue } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/abilities/pastel_veil.test.ts b/src/test/abilities/pastel_veil.test.ts index 6f09fd8ff06..dd8360493a1 100644 --- a/src/test/abilities/pastel_veil.test.ts +++ b/src/test/abilities/pastel_veil.test.ts @@ -1,10 +1,10 @@ import { BattlerIndex } from "#app/battle"; -import { StatusEffect } from "#app/data/status-effect"; import { Abilities } from "#app/enums/abilities"; import { CommandPhase } from "#app/phases/command-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/abilities/power_construct.test.ts b/src/test/abilities/power_construct.test.ts index bb80e9f3ac8..aaab5ddb5c4 100644 --- a/src/test/abilities/power_construct.test.ts +++ b/src/test/abilities/power_construct.test.ts @@ -1,9 +1,10 @@ -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; diff --git a/src/test/abilities/protean.test.ts b/src/test/abilities/protean.test.ts index 5f86ec4c6e6..0d7202e3f6d 100644 --- a/src/test/abilities/protean.test.ts +++ b/src/test/abilities/protean.test.ts @@ -1,6 +1,6 @@ import { allMoves } from "#app/data/move"; -import { Type } from "#app/data/type"; -import { Weather, WeatherType } from "#app/data/weather"; +import { Type } from "#enums/type"; +import { Weather } from "#app/data/weather"; import { PlayerPokemon } from "#app/field/pokemon"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; @@ -8,6 +8,7 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; diff --git a/src/test/abilities/sand_veil.test.ts b/src/test/abilities/sand_veil.test.ts index c62357f17af..ee8ca450df9 100644 --- a/src/test/abilities/sand_veil.test.ts +++ b/src/test/abilities/sand_veil.test.ts @@ -1,12 +1,12 @@ import { StatMultiplierAbAttr, allAbilities } from "#app/data/ability"; -import { Stat } from "#enums/stat"; -import { WeatherType } from "#app/data/weather"; import { CommandPhase } from "#app/phases/command-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; +import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; diff --git a/src/test/abilities/sap_sipper.test.ts b/src/test/abilities/sap_sipper.test.ts index a4ce0c1b8f6..dc254a54b54 100644 --- a/src/test/abilities/sap_sipper.test.ts +++ b/src/test/abilities/sap_sipper.test.ts @@ -8,7 +8,8 @@ import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { allMoves, RandomMoveAttr } from "#app/data/move"; // See also: TypeImmunityAbAttr describe("Abilities - Sap Sipper", () => { @@ -27,20 +28,20 @@ describe("Abilities - Sap Sipper", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleType("single"); - game.override.disableCrits(); + game.override.battleType("single") + .disableCrits() + .ability(Abilities.SAP_SIPPER) + .enemySpecies(Species.RATTATA) + .enemyAbility(Abilities.SAP_SIPPER) + .enemyMoveset(Moves.SPLASH); }); it("raises ATK stat stage by 1 and block effects when activated against a grass attack", async() => { const moveToUse = Moves.LEAFAGE; - const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([ moveToUse ]); - game.override.enemyMoveset(Moves.SPLASH); - game.override.enemySpecies(Species.DUSKULL); - game.override.enemyAbility(enemyAbility); + game.override.moveset(moveToUse); - await game.startBattle(); + await game.classicMode.startBattle([ Species.BULBASAUR ]); const enemyPokemon = game.scene.getEnemyPokemon()!; const initialEnemyHp = enemyPokemon.hp; @@ -55,14 +56,10 @@ describe("Abilities - Sap Sipper", () => { it("raises ATK stat stage by 1 and block effects when activated against a grass status move", async() => { const moveToUse = Moves.SPORE; - const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([ moveToUse ]); - game.override.enemyMoveset(Moves.SPLASH); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(enemyAbility); + game.override.moveset(moveToUse); - await game.startBattle(); + await game.classicMode.startBattle([ Species.BULBASAUR ]); const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -76,14 +73,10 @@ describe("Abilities - Sap Sipper", () => { it("do not activate against status moves that target the field", async () => { const moveToUse = Moves.GRASSY_TERRAIN; - const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([ moveToUse ]); - game.override.enemyMoveset(Moves.SPLASH); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(enemyAbility); + game.override.moveset(moveToUse); - await game.startBattle(); + await game.classicMode.startBattle([ Species.BULBASAUR ]); game.move.select(moveToUse); @@ -96,14 +89,10 @@ describe("Abilities - Sap Sipper", () => { it("activate once against multi-hit grass attacks", async () => { const moveToUse = Moves.BULLET_SEED; - const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([ moveToUse ]); - game.override.enemyMoveset(Moves.SPLASH); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(enemyAbility); + game.override.moveset(moveToUse); - await game.startBattle(); + await game.classicMode.startBattle([ Species.BULBASAUR ]); const enemyPokemon = game.scene.getEnemyPokemon()!; const initialEnemyHp = enemyPokemon.hp; @@ -118,15 +107,10 @@ describe("Abilities - Sap Sipper", () => { it("do not activate against status moves that target the user", async () => { const moveToUse = Moves.SPIKY_SHIELD; - const ability = Abilities.SAP_SIPPER; - game.override.moveset([ moveToUse ]); - game.override.ability(ability); - game.override.enemyMoveset(Moves.SPLASH); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(Abilities.NONE); + game.override.moveset(moveToUse); - await game.startBattle(); + await game.classicMode.startBattle([ Species.BULBASAUR ]); const playerPokemon = game.scene.getPlayerPokemon()!; @@ -142,18 +126,15 @@ describe("Abilities - Sap Sipper", () => { expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); }); - // TODO Add METRONOME outcome override - // To run this testcase, manually modify the METRONOME move to always give SAP_SIPPER, then uncomment - it.todo("activate once against multi-hit grass attacks (metronome)", async () => { + it("activate once against multi-hit grass attacks (metronome)", async () => { const moveToUse = Moves.METRONOME; - const enemyAbility = Abilities.SAP_SIPPER; - game.override.moveset([ moveToUse ]); - game.override.enemyMoveset([ Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE ]); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(enemyAbility); + const randomMoveAttr = allMoves[Moves.METRONOME].findAttr(attr => attr instanceof RandomMoveAttr) as RandomMoveAttr; + vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.BULLET_SEED); - await game.startBattle(); + game.override.moveset(moveToUse); + + await game.classicMode.startBattle([ Species.BULBASAUR ]); const enemyPokemon = game.scene.getEnemyPokemon()!; const initialEnemyHp = enemyPokemon.hp; @@ -168,11 +149,8 @@ describe("Abilities - Sap Sipper", () => { it("still activates regardless of accuracy check", async () => { game.override.moveset(Moves.LEAF_BLADE); - game.override.enemyMoveset(Moves.SPLASH); - game.override.enemySpecies(Species.MAGIKARP); - game.override.enemyAbility(Abilities.SAP_SIPPER); - await game.classicMode.startBattle(); + await game.classicMode.startBattle([ Species.BULBASAUR ]); const enemyPokemon = game.scene.getEnemyPokemon()!; diff --git a/src/test/abilities/schooling.test.ts b/src/test/abilities/schooling.test.ts index 5f953dbf9ab..e1ec58f517e 100644 --- a/src/test/abilities/schooling.test.ts +++ b/src/test/abilities/schooling.test.ts @@ -1,9 +1,10 @@ -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; diff --git a/src/test/abilities/shields_down.test.ts b/src/test/abilities/shields_down.test.ts index fbb2e96e463..6100d3e04d9 100644 --- a/src/test/abilities/shields_down.test.ts +++ b/src/test/abilities/shields_down.test.ts @@ -1,9 +1,10 @@ -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; diff --git a/src/test/abilities/synchronize.test.ts b/src/test/abilities/synchronize.test.ts index d34b5631271..2ae80ae9c7a 100644 --- a/src/test/abilities/synchronize.test.ts +++ b/src/test/abilities/synchronize.test.ts @@ -1,7 +1,7 @@ -import { StatusEffect } from "#app/data/status-effect"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -94,16 +94,4 @@ describe("Abilities - Synchronize", () => { expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); }); - - it("should activate with Psycho Shift after the move clears the status", async () => { - game.override.statusEffect(StatusEffect.PARALYSIS); - await game.classicMode.startBattle(); - - game.move.select(Moves.PSYCHO_SHIFT); - await game.phaseInterceptor.to("BerryPhase"); - - expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); // keeping old gen < V impl for now since it's buggy otherwise - expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); - expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); - }); }); diff --git a/src/test/abilities/wimp_out.test.ts b/src/test/abilities/wimp_out.test.ts index 6f56a2f4e7e..df965fc340d 100644 --- a/src/test/abilities/wimp_out.test.ts +++ b/src/test/abilities/wimp_out.test.ts @@ -296,7 +296,9 @@ describe("Abilities - Wimp Out", () => { Species.TYRUNT ]); - game.move.select(Moves.SPLASH); + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.ENDURE); await game.phaseInterceptor.to("TurnEndPhase"); confirmNoSwitch(); diff --git a/src/test/abilities/zen_mode.test.ts b/src/test/abilities/zen_mode.test.ts index 74575415788..4ba5e3d5929 100644 --- a/src/test/abilities/zen_mode.test.ts +++ b/src/test/abilities/zen_mode.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { DamagePhase } from "#app/phases/damage-phase"; import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; import { Mode } from "#app/ui/ui"; @@ -7,6 +7,7 @@ import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; import { SwitchType } from "#enums/switch-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; diff --git a/src/test/abilities/zero_to_hero.test.ts b/src/test/abilities/zero_to_hero.test.ts index 160b43abf1e..5702f73e6c4 100644 --- a/src/test/abilities/zero_to_hero.test.ts +++ b/src/test/abilities/zero_to_hero.test.ts @@ -1,9 +1,10 @@ -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/arena/weather_fog.test.ts b/src/test/arena/weather_fog.test.ts index db591dda2b8..8c1fcb1e3a4 100644 --- a/src/test/arena/weather_fog.test.ts +++ b/src/test/arena/weather_fog.test.ts @@ -1,9 +1,9 @@ import { allMoves } from "#app/data/move"; -import { WeatherType } from "#app/data/weather"; import { Abilities } from "#app/enums/abilities"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/src/test/arena/weather_hail.test.ts b/src/test/arena/weather_hail.test.ts index b8f4276a3d9..0b267550d75 100644 --- a/src/test/arena/weather_hail.test.ts +++ b/src/test/arena/weather_hail.test.ts @@ -1,10 +1,10 @@ -import { WeatherType } from "#app/data/weather"; +import { BattlerIndex } from "#app/battle"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { BattlerIndex } from "#app/battle"; describe("Weather - Hail", () => { let phaserGame: Phaser.Game; diff --git a/src/test/arena/weather_sandstorm.test.ts b/src/test/arena/weather_sandstorm.test.ts index 09d0c1d2b77..17ccfdee94b 100644 --- a/src/test/arena/weather_sandstorm.test.ts +++ b/src/test/arena/weather_sandstorm.test.ts @@ -1,8 +1,8 @@ -import { WeatherType } from "#app/data/weather"; import { Abilities } from "#app/enums/abilities"; import { Stat } from "#app/enums/stat"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/battle/double_battle.test.ts b/src/test/battle/double_battle.test.ts index a7585d55bab..1fc24bfc027 100644 --- a/src/test/battle/double_battle.test.ts +++ b/src/test/battle/double_battle.test.ts @@ -1,8 +1,9 @@ -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/battle/inverse_battle.test.ts b/src/test/battle/inverse_battle.test.ts index 110f92d15b7..0bda678bbd3 100644 --- a/src/test/battle/inverse_battle.test.ts +++ b/src/test/battle/inverse_battle.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; import { Challenges } from "#enums/challenges"; diff --git a/src/test/data/status-effect.test.ts b/src/test/data/status-effect.test.ts index 1b1a97fc51f..4831e8de5de 100644 --- a/src/test/data/status-effect.test.ts +++ b/src/test/data/status-effect.test.ts @@ -1,6 +1,5 @@ import { Status, - StatusEffect, getStatusEffectActivationText, getStatusEffectDescriptor, getStatusEffectHealText, @@ -11,6 +10,7 @@ import { MoveResult } from "#app/field/pokemon"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import { mockI18next } from "#test/utils/testUtils"; import i18next from "i18next"; diff --git a/src/test/field/pokemon.test.ts b/src/test/field/pokemon.test.ts index ce9862e22e4..0bfbd03e9d9 100644 --- a/src/test/field/pokemon.test.ts +++ b/src/test/field/pokemon.test.ts @@ -1,7 +1,7 @@ import { Species } from "#app/enums/species"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import GameManager from "../utils/gameManager"; -import { PokeballType } from "#app/enums/pokeball"; +import { PokeballType } from "#enums/pokeball"; import BattleScene from "#app/battle-scene"; import { Moves } from "#app/enums/moves"; diff --git a/src/test/final_boss.test.ts b/src/test/final_boss.test.ts index de2cddff944..5540d9511e4 100644 --- a/src/test/final_boss.test.ts +++ b/src/test/final_boss.test.ts @@ -1,12 +1,12 @@ -import { StatusEffect } from "#app/data/status-effect"; -import { Abilities } from "#app/enums/abilities"; -import { Biome } from "#app/enums/biome"; -import { Moves } from "#app/enums/moves"; -import { Species } from "#app/enums/species"; import { GameModes } from "#app/game-mode"; import { TurnHeldItemTransferModifier } from "#app/modifier/modifier"; +import { Abilities } from "#enums/abilities"; +import { Biome } from "#enums/biome"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import GameManager from "./utils/gameManager"; const FinalWave = { Classic: 200, diff --git a/src/test/items/toxic_orb.test.ts b/src/test/items/toxic_orb.test.ts index 583e302126c..6918d7f34f0 100644 --- a/src/test/items/toxic_orb.test.ts +++ b/src/test/items/toxic_orb.test.ts @@ -1,8 +1,8 @@ -import { StatusEffect } from "#app/data/status-effect"; import i18next from "#app/plugins/i18n"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/src/test/moves/aurora_veil.test.ts b/src/test/moves/aurora_veil.test.ts index e68117a2f59..2d7484b4eb5 100644 --- a/src/test/moves/aurora_veil.test.ts +++ b/src/test/moves/aurora_veil.test.ts @@ -1,13 +1,13 @@ import { ArenaTagSide } from "#app/data/arena-tag"; import Move, { allMoves } from "#app/data/move"; -import { WeatherType } from "#app/data/weather"; -import { Abilities } from "#app/enums/abilities"; import { ArenaTagType } from "#app/enums/arena-tag-type"; import Pokemon from "#app/field/pokemon"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { NumberHolder } from "#app/utils"; +import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/moves/camouflage.test.ts b/src/test/moves/camouflage.test.ts index acf37635c47..5773afffcc3 100644 --- a/src/test/moves/camouflage.test.ts +++ b/src/test/moves/camouflage.test.ts @@ -2,7 +2,7 @@ import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { TerrainType } from "#app/data/terrain"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { BattlerIndex } from "#app/battle"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; diff --git a/src/test/moves/dragon_cheer.test.ts b/src/test/moves/dragon_cheer.test.ts index 37b74d44360..750f09214ca 100644 --- a/src/test/moves/dragon_cheer.test.ts +++ b/src/test/moves/dragon_cheer.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; import { Abilities } from "#enums/abilities"; diff --git a/src/test/moves/dragon_rage.test.ts b/src/test/moves/dragon_rage.test.ts index 595f347a6b5..e8185f013e5 100644 --- a/src/test/moves/dragon_rage.test.ts +++ b/src/test/moves/dragon_rage.test.ts @@ -1,5 +1,5 @@ import { Stat } from "#enums/stat"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Species } from "#app/enums/species"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import { modifierTypes } from "#app/modifier/modifier-type"; diff --git a/src/test/moves/effectiveness.test.ts b/src/test/moves/effectiveness.test.ts index d1903c79844..829d4533f9b 100644 --- a/src/test/moves/effectiveness.test.ts +++ b/src/test/moves/effectiveness.test.ts @@ -1,7 +1,7 @@ import { allMoves } from "#app/data/move"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { TrainerSlot } from "#app/data/trainer-config"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Abilities } from "#app/enums/abilities"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; diff --git a/src/test/moves/electrify.test.ts b/src/test/moves/electrify.test.ts index 5d15a825688..8015dd0a74d 100644 --- a/src/test/moves/electrify.test.ts +++ b/src/test/moves/electrify.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; diff --git a/src/test/moves/encore.test.ts b/src/test/moves/encore.test.ts new file mode 100644 index 00000000000..7d8dc9658bf --- /dev/null +++ b/src/test/moves/encore.test.ts @@ -0,0 +1,116 @@ +import { BattlerTagType } from "#enums/battler-tag-type"; +import { BattlerIndex } from "#app/battle"; +import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Encore", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH, Moves.ENCORE ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([ Moves.SPLASH, Moves.TACKLE ]) + .startingLevel(100) + .enemyLevel(100); + }); + + it("should prevent the target from using any move except the last used move", async () => { + await game.classicMode.startBattle([ Species.SNORLAX ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.ENCORE); + await game.forceEnemyMove(Moves.SPLASH); + + await game.toNextTurn(); + expect(enemyPokemon.getTag(BattlerTagType.ENCORE)).toBeDefined(); + + game.move.select(Moves.SPLASH); + // The enemy AI would normally be inclined to use Tackle, but should be + // forced into using Splash. + await game.phaseInterceptor.to("BerryPhase", false); + + expect(enemyPokemon.getLastXMoves().every(turnMove => turnMove.move === Moves.SPLASH)).toBeTruthy(); + }); + + describe("should fail against the following moves:", () => { + it.each([ + { moveId: Moves.TRANSFORM, name: "Transform", delay: false }, + { moveId: Moves.MIMIC, name: "Mimic", delay: true }, + { moveId: Moves.SKETCH, name: "Sketch", delay: true }, + { moveId: Moves.ENCORE, name: "Encore", delay: false }, + { moveId: Moves.STRUGGLE, name: "Struggle", delay: false } + ])("$name", async ({ moveId, delay }) => { + game.override.enemyMoveset(moveId); + + await game.classicMode.startBattle([ Species.SNORLAX ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + if (delay) { + game.move.select(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.toNextTurn(); + } + + game.move.select(Moves.ENCORE); + + const turnOrder = delay + ? [ BattlerIndex.PLAYER, BattlerIndex.ENEMY ] + : [ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]; + await game.setTurnOrder(turnOrder); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.getTag(BattlerTagType.ENCORE)).toBeUndefined(); + }); + }); + + it("Pokemon under both Encore and Torment should alternate between Struggle and restricted move", async () => { + const turnOrder = [ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]; + game.override.moveset([ Moves.ENCORE, Moves.TORMENT, Moves.SPLASH ]); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const enemyPokemon = game.scene.getEnemyPokemon(); + game.move.select(Moves.ENCORE); + await game.setTurnOrder(turnOrder); + await game.phaseInterceptor.to("BerryPhase"); + expect(enemyPokemon?.getTag(BattlerTagType.ENCORE)).toBeDefined(); + + await game.toNextTurn(); + game.move.select(Moves.TORMENT); + await game.setTurnOrder(turnOrder); + await game.phaseInterceptor.to("BerryPhase"); + expect(enemyPokemon?.getTag(BattlerTagType.TORMENT)).toBeDefined(); + + await game.toNextTurn(); + game.move.select(Moves.SPLASH); + await game.setTurnOrder(turnOrder); + await game.phaseInterceptor.to("BerryPhase"); + const lastMove = enemyPokemon?.getLastXMoves()[0]; + expect(lastMove?.move).toBe(Moves.STRUGGLE); + }); +}); diff --git a/src/test/moves/flower_shield.test.ts b/src/test/moves/flower_shield.test.ts index 1a5cd326fd8..4c03df5212b 100644 --- a/src/test/moves/flower_shield.test.ts +++ b/src/test/moves/flower_shield.test.ts @@ -1,6 +1,6 @@ import { Stat } from "#enums/stat"; import { SemiInvulnerableTag } from "#app/data/battler-tags"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Biome } from "#app/enums/biome"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Abilities } from "#enums/abilities"; diff --git a/src/test/moves/forests_curse.test.ts b/src/test/moves/forests_curse.test.ts index c49bdab5255..010b00599a5 100644 --- a/src/test/moves/forests_curse.test.ts +++ b/src/test/moves/forests_curse.test.ts @@ -1,7 +1,7 @@ import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/moves/freeze_dry.test.ts b/src/test/moves/freeze_dry.test.ts index c09d12fa597..f766ed41a82 100644 --- a/src/test/moves/freeze_dry.test.ts +++ b/src/test/moves/freeze_dry.test.ts @@ -72,6 +72,31 @@ describe("Moves - Freeze-Dry", () => { expect(enemy.getMoveEffectiveness).toHaveReturnedWith(1); }); + /** + * Freeze drys forced super effectiveness should overwrite wonder guard + */ + it("should deal 2x dmg against soaked wonder guard target", async () => { + game.override + .enemySpecies(Species.SHEDINJA) + .enemyMoveset(Moves.SPLASH) + .starterSpecies(Species.MAGIKARP) + .moveset([ Moves.SOAK, Moves.FREEZE_DRY ]); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.SOAK); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.toNextTurn(); + + game.move.select(Moves.FREEZE_DRY); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); + expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); + }); + // enable if this is ever fixed (lol) it.todo("should deal 2x damage to water types under Normalize", async () => { game.override.ability(Abilities.NORMALIZE); diff --git a/src/test/moves/fusion_flare.test.ts b/src/test/moves/fusion_flare.test.ts index 162cefcfb1e..75641c04d02 100644 --- a/src/test/moves/fusion_flare.test.ts +++ b/src/test/moves/fusion_flare.test.ts @@ -1,7 +1,7 @@ -import { StatusEffect } from "#app/data/status-effect"; import { TurnStartPhase } from "#app/phases/turn-start-phase"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/moves/grudge.test.ts b/src/test/moves/grudge.test.ts new file mode 100644 index 00000000000..340808929ab --- /dev/null +++ b/src/test/moves/grudge.test.ts @@ -0,0 +1,90 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { BattlerIndex } from "#app/battle"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Grudge", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.EMBER, Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.SHEDINJA) + .enemyAbility(Abilities.WONDER_GUARD) + .enemyMoveset([ Moves.GRUDGE, Moves.SPLASH ]); + }); + + it("should reduce the PP of the Pokemon's move to 0 when the user has fainted", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const playerPokemon = game.scene.getPlayerPokemon(); + game.move.select(Moves.EMBER); + await game.forceEnemyMove(Moves.GRUDGE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + const playerMove = playerPokemon?.getMoveset().find(m => m?.moveId === Moves.EMBER); + + expect(playerMove?.getPpRatio()).toBe(0); + }); + + it("should remain in effect until the user's next move", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const playerPokemon = game.scene.getPlayerPokemon(); + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.GRUDGE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.toNextTurn(); + + game.move.select(Moves.EMBER); + await game.forceEnemyMove(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase"); + + const playerMove = playerPokemon?.getMoveset().find(m => m?.moveId === Moves.EMBER); + + expect(playerMove?.getPpRatio()).toBe(0); + }); + + it("should not reduce the opponent's PP if the user dies to weather/indirect damage", async () => { + // Opponent will be reduced to 1 HP by False Swipe, then faint to Sandstorm + game.override + .moveset([ Moves.FALSE_SWIPE ]) + .startingLevel(100) + .ability(Abilities.SAND_STREAM) + .enemySpecies(Species.RATTATA); + await game.classicMode.startBattle([ Species.GEODUDE ]); + + const enemyPokemon = game.scene.getEnemyPokemon(); + const playerPokemon = game.scene.getPlayerPokemon(); + + game.move.select(Moves.FALSE_SWIPE); + await game.forceEnemyMove(Moves.GRUDGE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemyPokemon?.isFainted()).toBe(true); + + const playerMove = playerPokemon?.getMoveset().find(m => m?.moveId === Moves.FALSE_SWIPE); + expect(playerMove?.getPpRatio()).toBeGreaterThan(0); + }); +}); diff --git a/src/test/moves/heal_block.test.ts b/src/test/moves/heal_block.test.ts index 252533215a8..25f2076ff3e 100644 --- a/src/test/moves/heal_block.test.ts +++ b/src/test/moves/heal_block.test.ts @@ -1,12 +1,12 @@ import { BattlerIndex } from "#app/battle"; import { ArenaTagSide } from "#app/data/arena-tag"; -import { WeatherType } from "#app/data/weather"; import GameManager from "#app/test/utils/gameManager"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { WeatherType } from "#enums/weather-type"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/moves/nightmare.test.ts b/src/test/moves/nightmare.test.ts index f4c485ff1b4..850b0793b1e 100644 --- a/src/test/moves/nightmare.test.ts +++ b/src/test/moves/nightmare.test.ts @@ -1,7 +1,7 @@ -import { StatusEffect } from "#app/data/status-effect"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/moves/order_up.test.ts b/src/test/moves/order_up.test.ts new file mode 100644 index 00000000000..d0b52dc1a9d --- /dev/null +++ b/src/test/moves/order_up.test.ts @@ -0,0 +1,85 @@ +import { BattlerIndex } from "#app/battle"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { PokemonAnimType } from "#enums/pokemon-anim-type"; +import { EffectiveStat, Stat } from "#enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Moves - Order Up", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset(Moves.ORDER_UP) + .ability(Abilities.COMMANDER) + .battleType("double") + .disableCrits() + .enemySpecies(Species.SNORLAX) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .startingLevel(100) + .enemyLevel(100); + + vi.spyOn(game.scene, "triggerPokemonBattleAnim").mockReturnValue(true); + }); + + it.each([ + { formIndex: 0, formName: "Curly", stat: Stat.ATK, statName: "Attack" }, + { formIndex: 1, formName: "Droopy", stat: Stat.DEF, statName: "Defense" }, + { formIndex: 2, formName: "Stretchy", stat: Stat.SPD, statName: "Speed" } + ])("should raise the user's $statName when the user is commanded by a $formName Tatsugiri", async ({ formIndex, stat }) => { + game.override.starterForms({ [Species.TATSUGIRI]: formIndex }); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + + game.move.select(Moves.ORDER_UP, 1, BattlerIndex.ENEMY); + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.phaseInterceptor.to("BerryPhase", false); + + const affectedStats: EffectiveStat[] = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ]; + affectedStats.forEach(st => expect(dondozo.getStatStage(st)).toBe(st === stat ? 3 : 2)); + }); + + it("should be boosted by Sheer Force while still applying a stat boost", async () => { + game.override + .passiveAbility(Abilities.SHEER_FORCE) + .starterForms({ [Species.TATSUGIRI]: 0 }); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); + + game.move.select(Moves.ORDER_UP, 1, BattlerIndex.ENEMY); + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(dondozo.battleData.abilitiesApplied.includes(Abilities.SHEER_FORCE)).toBeTruthy(); + expect(dondozo.getStatStage(Stat.ATK)).toBe(3); + }); +}); diff --git a/src/test/moves/plasma_fists.test.ts b/src/test/moves/plasma_fists.test.ts index 24210cd47fa..4075c1ab988 100644 --- a/src/test/moves/plasma_fists.test.ts +++ b/src/test/moves/plasma_fists.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; diff --git a/src/test/moves/pledge_moves.test.ts b/src/test/moves/pledge_moves.test.ts index 93fcf57cc60..64d586e7ba4 100644 --- a/src/test/moves/pledge_moves.test.ts +++ b/src/test/moves/pledge_moves.test.ts @@ -2,7 +2,7 @@ import { BattlerIndex } from "#app/battle"; import { allAbilities } from "#app/data/ability"; import { ArenaTagSide } from "#app/data/arena-tag"; import { allMoves, FlinchAttr } from "#app/data/move"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { ArenaTagType } from "#enums/arena-tag-type"; import { Stat } from "#enums/stat"; import { toDmgValue } from "#app/utils"; diff --git a/src/test/moves/psycho_shift.test.ts b/src/test/moves/psycho_shift.test.ts new file mode 100644 index 00000000000..448a8c99ef0 --- /dev/null +++ b/src/test/moves/psycho_shift.test.ts @@ -0,0 +1,49 @@ +import { StatusEffect } from "#app/enums/status-effect"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Psycho Shift", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.PSYCHO_SHIFT ]) + .ability(Abilities.BALL_FETCH) + .statusEffect(StatusEffect.POISON) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyLevel(20) + .enemyAbility(Abilities.SYNCHRONIZE) + .enemyMoveset(Moves.SPLASH); + }); + + it("If Psycho Shift is used on a Pokémon with Synchronize, the user of Psycho Shift will already be afflicted with a status condition when Synchronize activates", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const playerPokemon = game.scene.getPlayerPokemon(); + const enemyPokemon = game.scene.getEnemyPokemon(); + expect(enemyPokemon?.status).toBeUndefined(); + + game.move.select(Moves.PSYCHO_SHIFT); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon?.status).toBeNull(); + expect(enemyPokemon?.status).toBeDefined(); + }); +}); diff --git a/src/test/moves/purify.test.ts b/src/test/moves/purify.test.ts index 3b07eafd75a..171f94a611a 100644 --- a/src/test/moves/purify.test.ts +++ b/src/test/moves/purify.test.ts @@ -1,9 +1,10 @@ import { BattlerIndex } from "#app/battle"; -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import { MoveEndPhase } from "#app/phases/move-end-phase"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; diff --git a/src/test/moves/reflect_type.test.ts b/src/test/moves/reflect_type.test.ts index 0e47d4b00fc..50e0fc2fbe6 100644 --- a/src/test/moves/reflect_type.test.ts +++ b/src/test/moves/reflect_type.test.ts @@ -1,7 +1,7 @@ import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/moves/relic_song.test.ts b/src/test/moves/relic_song.test.ts index eb877b6054d..c09514850eb 100644 --- a/src/test/moves/relic_song.test.ts +++ b/src/test/moves/relic_song.test.ts @@ -1,4 +1,4 @@ -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Challenges } from "#app/enums/challenges"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; diff --git a/src/test/moves/roost.test.ts b/src/test/moves/roost.test.ts index e595f879844..69301dc86cf 100644 --- a/src/test/moves/roost.test.ts +++ b/src/test/moves/roost.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; diff --git a/src/test/moves/sketch.test.ts b/src/test/moves/sketch.test.ts index 2e3eb97a76c..f531f44ef0c 100644 --- a/src/test/moves/sketch.test.ts +++ b/src/test/moves/sketch.test.ts @@ -1,10 +1,13 @@ import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult, PokemonMove } from "#app/field/pokemon"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { StatusEffect } from "#app/enums/status-effect"; +import { BattlerIndex } from "#app/battle"; +import { allMoves, RandomMoveAttr } from "#app/data/move"; describe("Moves - Sketch", () => { let phaserGame: Phaser.Game; @@ -32,22 +35,64 @@ describe("Moves - Sketch", () => { }); it("Sketch should not fail even if a previous Sketch failed to retrieve a valid move and ran out of PP", async () => { - game.override.moveset([ Moves.SKETCH, Moves.SKETCH ]); - await game.classicMode.startBattle([ Species.REGIELEKI ]); - const playerPokemon = game.scene.getPlayerPokemon(); + const playerPokemon = game.scene.getPlayerPokemon()!; + // can't use normal moveset override because we need to check moveset changes + playerPokemon.moveset = [ new PokemonMove(Moves.SKETCH), new PokemonMove(Moves.SKETCH) ]; game.move.select(Moves.SKETCH); await game.phaseInterceptor.to("TurnEndPhase"); - expect(playerPokemon?.getLastXMoves()[0].result).toBe(MoveResult.FAIL); - const moveSlot0 = playerPokemon?.getMoveset()[0]; - expect(moveSlot0?.moveId).toBe(Moves.SKETCH); - expect(moveSlot0?.getPpRatio()).toBe(0); + expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + const moveSlot0 = playerPokemon.getMoveset()[0]!; + expect(moveSlot0.moveId).toBe(Moves.SKETCH); + expect(moveSlot0.getPpRatio()).toBe(0); await game.toNextTurn(); game.move.select(Moves.SKETCH); await game.phaseInterceptor.to("TurnEndPhase"); - expect(playerPokemon?.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); - // Can't verify if the player Pokemon's moveset was successfully changed because of overrides. + expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(playerPokemon.moveset[0]?.moveId).toBe(Moves.SPLASH); + expect(playerPokemon.moveset[1]?.moveId).toBe(Moves.SKETCH); + }); + + it("Sketch should retrieve the most recent valid move from its target history", async () => { + game.override.enemyStatusEffect(StatusEffect.PARALYSIS); + await game.classicMode.startBattle([ Species.REGIELEKI ]); + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + playerPokemon.moveset = [ new PokemonMove(Moves.SKETCH), new PokemonMove(Moves.GROWL) ]; + + game.move.select(Moves.GROWL); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.move.forceStatusActivation(false); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + + await game.toNextTurn(); + game.move.select(Moves.SKETCH); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.move.forceStatusActivation(true); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(playerPokemon.moveset[0]?.moveId).toBe(Moves.SPLASH); + expect(playerPokemon.moveset[1]?.moveId).toBe(Moves.GROWL); + }); + + it("should sketch moves that call other moves", async () => { + const randomMoveAttr = allMoves[Moves.METRONOME].findAttr(attr => attr instanceof RandomMoveAttr) as RandomMoveAttr; + vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.FALSE_SWIPE); + + game.override.enemyMoveset([ Moves.METRONOME ]); + await game.classicMode.startBattle([ Species.REGIELEKI ]); + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.moveset = [ new PokemonMove(Moves.SKETCH) ]; + + // Opponent uses Metronome -> False Swipe, then player uses Sketch, which should sketch Metronome + game.move.select(Moves.SKETCH); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(playerPokemon.moveset[0]?.moveId).toBe(Moves.METRONOME); + expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); // Make sure opponent actually used False Swipe }); }); diff --git a/src/test/moves/substitute.test.ts b/src/test/moves/substitute.test.ts index 92f66c967c4..14ab4ab5054 100644 --- a/src/test/moves/substitute.test.ts +++ b/src/test/moves/substitute.test.ts @@ -2,7 +2,11 @@ import { BattlerIndex } from "#app/battle"; import { ArenaTagSide } from "#app/data/arena-tag"; import { SubstituteTag, TrappedTag } from "#app/data/battler-tags"; import { allMoves, StealHeldItemChanceAttr } from "#app/data/move"; -import { StatusEffect } from "#app/data/status-effect"; +import { MoveResult } from "#app/field/pokemon"; +import { CommandPhase } from "#app/phases/command-phase"; +import GameManager from "#app/test/utils/gameManager"; +import { Command } from "#app/ui/command-ui-handler"; +import { Mode } from "#app/ui/ui"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type"; @@ -10,11 +14,7 @@ import { BerryType } from "#enums/berry-type"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { Stat } from "#enums/stat"; -import { MoveResult } from "#app/field/pokemon"; -import { CommandPhase } from "#app/phases/command-phase"; -import GameManager from "#app/test/utils/gameManager"; -import { Command } from "#app/ui/command-ui-handler"; -import { Mode } from "#app/ui/ui"; +import { StatusEffect } from "#enums/status-effect"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/src/test/moves/tar_shot.test.ts b/src/test/moves/tar_shot.test.ts index 4734da366e4..5fb70abc19c 100644 --- a/src/test/moves/tar_shot.test.ts +++ b/src/test/moves/tar_shot.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; import { Stat } from "#app/enums/stat"; diff --git a/src/test/moves/tera_blast.test.ts b/src/test/moves/tera_blast.test.ts index 0ce8a552105..311ac0f0d0e 100644 --- a/src/test/moves/tera_blast.test.ts +++ b/src/test/moves/tera_blast.test.ts @@ -1,7 +1,7 @@ import { BattlerIndex } from "#app/battle"; import { Stat } from "#enums/stat"; import { allMoves } from "#app/data/move"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Abilities } from "#app/enums/abilities"; import { HitResult } from "#app/field/pokemon"; import { Moves } from "#enums/moves"; diff --git a/src/test/moves/tera_starstorm.test.ts b/src/test/moves/tera_starstorm.test.ts index c7b6b66c1ce..22dd5b3c4d1 100644 --- a/src/test/moves/tera_starstorm.test.ts +++ b/src/test/moves/tera_starstorm.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; diff --git a/src/test/moves/thunder_wave.test.ts b/src/test/moves/thunder_wave.test.ts index 03e9ebb94f3..5551451e59b 100644 --- a/src/test/moves/thunder_wave.test.ts +++ b/src/test/moves/thunder_wave.test.ts @@ -1,8 +1,8 @@ -import { StatusEffect } from "#app/data/status-effect"; -import { Abilities } from "#app/enums/abilities"; import { EnemyPokemon } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/moves/toxic_spikes.test.ts b/src/test/moves/toxic_spikes.test.ts index a5c63a2652f..bdd59ed0ac8 100644 --- a/src/test/moves/toxic_spikes.test.ts +++ b/src/test/moves/toxic_spikes.test.ts @@ -1,10 +1,10 @@ import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag"; -import { StatusEffect } from "#app/data/status-effect"; import { decrypt, encrypt, GameData, SessionSaveData } from "#app/system/game-data"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/moves/trick_or_treat.test.ts b/src/test/moves/trick_or_treat.test.ts index 7ecd00ed076..5c85cac05e2 100644 --- a/src/test/moves/trick_or_treat.test.ts +++ b/src/test/moves/trick_or_treat.test.ts @@ -1,7 +1,7 @@ import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/src/test/moves/upper_hand.test.ts b/src/test/moves/upper_hand.test.ts new file mode 100644 index 00000000000..f94197d3fbd --- /dev/null +++ b/src/test/moves/upper_hand.test.ts @@ -0,0 +1,103 @@ +import { BattlerIndex } from "#app/battle"; +import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Upper Hand", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset(Moves.UPPER_HAND) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.QUICK_ATTACK) + .startingLevel(100) + .enemyLevel(100); + }); + + it("should flinch the opponent before they use a priority attack", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const feebas = game.scene.getPlayerPokemon()!; + const magikarp = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.UPPER_HAND); + await game.phaseInterceptor.to("BerryPhase"); + + expect(feebas.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(magikarp.isFullHp()).toBeFalsy(); + expect(feebas.isFullHp()).toBeTruthy(); + }); + + it.each([ + { descriptor: "non-priority attack", move: Moves.TACKLE }, + { descriptor: "status move", move: Moves.BABY_DOLL_EYES } + ])("should fail when the opponent selects a $descriptor", async ({ move }) => { + game.override.enemyMoveset(move); + + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const feebas = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.UPPER_HAND); + await game.phaseInterceptor.to("BerryPhase"); + + expect(feebas.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should flinch the opponent before they use an attack boosted by Gale Wings", async () => { + game.override + .enemyAbility(Abilities.GALE_WINGS) + .enemyMoveset(Moves.GUST); + + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const feebas = game.scene.getPlayerPokemon()!; + const magikarp = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.UPPER_HAND); + await game.phaseInterceptor.to("BerryPhase"); + + expect(feebas.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(magikarp.isFullHp()).toBeFalsy(); + expect(feebas.isFullHp()).toBeTruthy(); + }); + + it("should fail if the target has already moved", async () => { + game.override + .enemyMoveset(Moves.FAKE_OUT) + .enemyAbility(Abilities.SHEER_FORCE); + + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const feebas = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.UPPER_HAND); + + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(feebas.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(feebas.isFullHp()).toBeFalsy(); + }); +}); diff --git a/src/test/mystery-encounter/encounter-test-utils.ts b/src/test/mystery-encounter/encounter-test-utils.ts index f95a442d4c2..cd2fd2db042 100644 --- a/src/test/mystery-encounter/encounter-test-utils.ts +++ b/src/test/mystery-encounter/encounter-test-utils.ts @@ -1,18 +1,19 @@ -import { Button } from "#app/enums/buttons"; -import { MysteryEncounterBattlePhase, MysteryEncounterOptionSelectedPhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; -import MysteryEncounterUiHandler from "#app/ui/mystery-encounter-ui-handler"; -import { Mode } from "#app/ui/ui"; -import GameManager from "../utils/gameManager"; -import MessageUiHandler from "#app/ui/message-ui-handler"; -import { Status, StatusEffect } from "#app/data/status-effect"; -import { expect, vi } from "vitest"; import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { Status } from "#app/data/status-effect"; +import { CommandPhase } from "#app/phases/command-phase"; +import { MessagePhase } from "#app/phases/message-phase"; +import { MysteryEncounterBattlePhase, MysteryEncounterOptionSelectedPhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; +import { VictoryPhase } from "#app/phases/victory-phase"; +import MessageUiHandler from "#app/ui/message-ui-handler"; +import MysteryEncounterUiHandler from "#app/ui/mystery-encounter-ui-handler"; import PartyUiHandler from "#app/ui/party-ui-handler"; import OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler"; +import { Mode } from "#app/ui/ui"; import { isNullOrUndefined } from "#app/utils"; -import { CommandPhase } from "#app/phases/command-phase"; -import { VictoryPhase } from "#app/phases/victory-phase"; -import { MessagePhase } from "#app/phases/message-phase"; +import { Button } from "#enums/buttons"; +import { StatusEffect } from "#enums/status-effect"; +import GameManager from "#test/utils/gameManager"; +import { expect, vi } from "vitest"; /** * Runs a {@linkcode MysteryEncounter} to either the start of a battle, or to the {@linkcode MysteryEncounterRewardsPhase}, depending on the option selected diff --git a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index 7ea1f883bd1..a403a306b3d 100644 --- a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -27,7 +27,7 @@ import OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler"; import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { BerryType } from "#enums/berry-type"; import { PokemonHeldItemModifier } from "#app/modifier/modifier"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { CommandPhase } from "#app/phases/command-phase"; import { MovePhase } from "#app/phases/move-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; diff --git a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts index 92555e98679..215cab5c65a 100644 --- a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -1,8 +1,8 @@ import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; -import { Biome } from "#app/enums/biome"; -import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; -import { Species } from "#app/enums/species"; -import GameManager from "#app/test/utils/gameManager"; +import { Biome } from "#enums/biome"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { FieryFalloutEncounter } from "#app/data/mystery-encounters/encounters/fiery-fallout-encounter"; import { Gender } from "#app/data/gender"; @@ -13,8 +13,8 @@ import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRu import { Moves } from "#enums/moves"; import BattleScene from "#app/battle-scene"; import { AttackTypeBoosterModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; -import { Type } from "#app/data/type"; -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Type } from "#enums/type"; +import { Status } from "#app/data/status-effect"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -25,6 +25,7 @@ import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { BattlerTagType } from "#enums/battler-tag-type"; import { Abilities } from "#enums/abilities"; import i18next from "i18next"; +import { StatusEffect } from "#enums/status-effect"; const namespace = "mysteryEncounters/fieryFallout"; /** Arcanine and Ninetails for 2 Fire types. Lapras, Gengar, Abra for burnable mon. */ diff --git a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts index be0e6e68b5e..ae725f3480a 100644 --- a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -1,8 +1,8 @@ import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; -import { Biome } from "#app/enums/biome"; -import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; -import { Species } from "#app/enums/species"; -import GameManager from "#app/test/utils/gameManager"; +import { Biome } from "#enums/biome"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import * as BattleAnims from "#app/data/battle-anims"; @@ -11,7 +11,7 @@ import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } f import { Moves } from "#enums/moves"; import BattleScene from "#app/battle-scene"; import { TheStrongStuffEncounter } from "#app/data/mystery-encounters/encounters/the-strong-stuff-encounter"; -import { Nature } from "#app/data/nature"; +import { Nature } from "#enums/nature"; import { BerryType } from "#enums/berry-type"; import { BattlerTagType } from "#enums/battler-tag-type"; import { PokemonMove } from "#app/field/pokemon"; @@ -25,7 +25,7 @@ import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { CommandPhase } from "#app/phases/command-phase"; import { MovePhase } from "#app/phases/move-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; -import { Abilities } from "#app/enums/abilities"; +import { Abilities } from "#enums/abilities"; const namespace = "mysteryEncounters/theStrongStuff"; const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ]; diff --git a/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts b/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts index 2653b76ab7c..701a3c94add 100644 --- a/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts @@ -1,9 +1,9 @@ import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; -import { Biome } from "#app/enums/biome"; -import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; -import { Species } from "#app/enums/species"; -import GameManager from "#app/test/utils/gameManager"; +import { Biome } from "#enums/biome"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; import BattleScene from "#app/battle-scene"; @@ -19,12 +19,13 @@ import { Nature } from "#enums/nature"; import { Moves } from "#enums/moves"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { TheWinstrateChallengeEncounter } from "#app/data/mystery-encounters/encounters/the-winstrate-challenge-encounter"; -import { Status, StatusEffect } from "#app/data/status-effect"; +import { Status } from "#app/data/status-effect"; import { MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; import { CommandPhase } from "#app/phases/command-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { PartyHealPhase } from "#app/phases/party-heal-phase"; import { VictoryPhase } from "#app/phases/victory-phase"; +import { StatusEffect } from "#enums/status-effect"; const namespace = "mysteryEncounters/theWinstrateChallenge"; const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ]; diff --git a/src/test/mystery-encounter/mystery-encounter-utils.test.ts b/src/test/mystery-encounter/mystery-encounter-utils.test.ts index b57a88d5cad..ab2d5f052c0 100644 --- a/src/test/mystery-encounter/mystery-encounter-utils.test.ts +++ b/src/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -1,17 +1,17 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import GameManager from "#app/test/utils/gameManager"; -import Phaser from "phaser"; -import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; -import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; -import { StatusEffect } from "#app/data/status-effect"; -import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; import { speciesStarterCosts } from "#app/data/balance/starters"; -import { Type } from "#app/data/type"; -import { getHighestLevelPlayerPokemon, getLowestLevelPlayerPokemon, getRandomPlayerPokemon, getRandomSpeciesByStarterTier, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import { getEncounterText, queueEncounterMessage, showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { getHighestLevelPlayerPokemon, getLowestLevelPlayerPokemon, getRandomPlayerPokemon, getRandomSpeciesByStarterTier, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Type } from "#enums/type"; import { MessagePhase } from "#app/phases/message-phase"; +import GameManager from "#app/test/utils/gameManager"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Mystery Encounter Utils", () => { let phaserGame: Phaser.Game; diff --git a/src/test/phases/form-change-phase.test.ts b/src/test/phases/form-change-phase.test.ts index 70ca429672f..3c0016260a3 100644 --- a/src/test/phases/form-change-phase.test.ts +++ b/src/test/phases/form-change-phase.test.ts @@ -4,7 +4,7 @@ import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { modifierTypes } from "#app/modifier/modifier-type"; diff --git a/src/test/ui/starter-select.test.ts b/src/test/ui/starter-select.test.ts index 8f86d62c546..15af3619ed3 100644 --- a/src/test/ui/starter-select.test.ts +++ b/src/test/ui/starter-select.test.ts @@ -1,5 +1,5 @@ import { Gender } from "#app/data/gender"; -import { Nature } from "#app/data/nature"; +import { Nature } from "#enums/nature"; import { allSpecies } from "#app/data/pokemon-species"; import { GameModes } from "#app/game-mode"; import { EncounterPhase } from "#app/phases/encounter-phase"; diff --git a/src/test/utils/helpers/overridesHelper.ts b/src/test/utils/helpers/overridesHelper.ts index 404f5c34a26..02950d497ee 100644 --- a/src/test/utils/helpers/overridesHelper.ts +++ b/src/test/utils/helpers/overridesHelper.ts @@ -1,19 +1,20 @@ -import { StatusEffect } from "#app/data/status-effect"; -import { Weather, WeatherType } from "#app/data/weather"; +import { Variant } from "#app/data/variant"; +import { Weather } from "#app/data/weather"; import { Abilities } from "#app/enums/abilities"; -import { Biome } from "#app/enums/biome"; -import { Moves } from "#app/enums/moves"; -import { Species } from "#app/enums/species"; import * as GameMode from "#app/game-mode"; import { GameModes, getGameMode } from "#app/game-mode"; import { ModifierOverride } from "#app/modifier/modifier-type"; import Overrides from "#app/overrides"; +import { Unlockables } from "#app/system/unlockables"; +import { Biome } from "#enums/biome"; +import { Moves } from "#enums/moves"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import type { WeatherType } from "#enums/weather-type"; import { vi } from "vitest"; import { GameManagerHelper } from "./gameManagerHelper"; -import { Unlockables } from "#app/system/unlockables"; -import { Variant } from "#app/data/variant"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; /** * Helper to handle overrides in tests diff --git a/src/ui/arena-flyout.ts b/src/ui/arena-flyout.ts index a82f97244cd..573cb85db70 100644 --- a/src/ui/arena-flyout.ts +++ b/src/ui/arena-flyout.ts @@ -1,7 +1,7 @@ import { addTextObject, TextStyle } from "./text"; import BattleScene from "#app/battle-scene"; import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag"; -import { WeatherType } from "#app/data/weather"; +import { WeatherType } from "#enums/weather-type"; import { TerrainType } from "#app/data/terrain"; import { addWindow, WindowVariant } from "./ui-theme"; import { ArenaEvent, ArenaEventType, TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena"; diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index 1d97998f491..72447988bdd 100644 --- a/src/ui/battle-info.ts +++ b/src/ui/battle-info.ts @@ -3,9 +3,10 @@ import { getLevelTotalExp, getLevelRelExp } from "../data/exp"; import * as Utils from "../utils"; import { addTextObject, TextStyle } from "./text"; import { getGenderSymbol, getGenderColor, Gender } from "../data/gender"; -import { StatusEffect } from "../data/status-effect"; +import { StatusEffect } from "#enums/status-effect"; import BattleScene from "../battle-scene"; -import { Type, getTypeRgb } from "../data/type"; +import { getTypeRgb } from "#app/data/type"; +import { Type } from "#enums/type"; import { getVariantTint } from "#app/data/variant"; import { Stat } from "#enums/stat"; import BattleFlyout from "./battle-flyout"; diff --git a/src/ui/command-ui-handler.ts b/src/ui/command-ui-handler.ts index 0f5edc28675..0dacacc7b70 100644 --- a/src/ui/command-ui-handler.ts +++ b/src/ui/command-ui-handler.ts @@ -90,9 +90,6 @@ export default class CommandUiHandler extends UiHandler { switch (cursor) { // Fight case Command.FIGHT: - if ((this.scene.getCurrentPhase() as CommandPhase).checkFightOverride()) { - return true; - } ui.setMode(Mode.FIGHT, (this.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); success = true; break; diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index ee6641a1a27..eaf504495d5 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -1,6 +1,7 @@ import BattleScene, { InfoToggle } from "../battle-scene"; import { addTextObject, TextStyle } from "./text"; -import { getTypeDamageMultiplierColor, Type } from "../data/type"; +import { getTypeDamageMultiplierColor } from "#app/data/type"; +import { Type } from "#enums/type"; import { Command } from "./command-ui-handler"; import { Mode } from "./ui"; import UiHandler from "./ui-handler"; diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index 3f89ebe415f..a0358b5ca8c 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -1,6 +1,6 @@ import BattleScene from "../battle-scene"; import { getPlayerShopModifierTypeOptionsForWave, ModifierTypeOption, TmModifierType } from "../modifier/modifier-type"; -import { getPokeballAtlasKey, PokeballType } from "../data/pokeball"; +import { getPokeballAtlasKey } from "#app/data/pokeball"; import { addTextObject, getTextStyleOptions, getModifierTierTextTint, getTextColor, TextStyle } from "./text"; import AwaitableUiHandler from "./awaitable-ui-handler"; import { Mode } from "./ui"; @@ -15,6 +15,7 @@ import i18next from "i18next"; import { ShopCursorTarget } from "#app/enums/shop-cursor-target"; import { IntegerHolder } from "./../utils"; import Phaser from "phaser"; +import type { PokeballType } from "#enums/pokeball"; export const SHOP_OPTIONS_ROW_LIMIT = 7; const SINGLE_SHOP_ROW_YOFFSET = 12; diff --git a/src/ui/move-info-overlay.ts b/src/ui/move-info-overlay.ts index 6c58d32c515..d9c4200ea9b 100644 --- a/src/ui/move-info-overlay.ts +++ b/src/ui/move-info-overlay.ts @@ -3,7 +3,7 @@ import { TextStyle, addTextObject } from "./text"; import { addWindow } from "./ui-theme"; import * as Utils from "../utils"; import Move, { MoveCategory } from "../data/move"; -import { Type } from "../data/type"; +import { Type } from "#enums/type"; import i18next from "i18next"; export interface MoveInfoOverlaySettings { diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index a26aa572ef3..bd3561dd0b4 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -8,7 +8,7 @@ import * as Utils from "#app/utils"; import { PokemonFormChangeItemModifier, PokemonHeldItemModifier, SwitchEffectTransferModifier } from "#app/modifier/modifier"; import { allMoves, ForceSwitchOutAttr } from "#app/data/move"; import { getGenderColor, getGenderSymbol } from "#app/data/gender"; -import { StatusEffect } from "#app/data/status-effect"; +import { StatusEffect } from "#enums/status-effect"; import PokemonIconAnimHandler, { PokemonIconAnimMode } from "#app/ui/pokemon-icon-anim-handler"; import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; import { addWindow } from "#app/ui/ui-theme"; diff --git a/src/ui/pokemon-hatch-info-container.ts b/src/ui/pokemon-hatch-info-container.ts index 146d70522fd..494855d20fa 100644 --- a/src/ui/pokemon-hatch-info-container.ts +++ b/src/ui/pokemon-hatch-info-container.ts @@ -1,7 +1,7 @@ import PokemonInfoContainer from "#app/ui/pokemon-info-container"; import BattleScene from "#app/battle-scene"; import { Gender } from "#app/data/gender"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import * as Utils from "#app/utils"; import { TextStyle, addTextObject } from "#app/ui/text"; import { speciesEggMoves } from "#app/data/balance/egg-moves"; diff --git a/src/ui/pokemon-info-container.ts b/src/ui/pokemon-info-container.ts index 5b11aff43b1..ead24e6d92f 100644 --- a/src/ui/pokemon-info-container.ts +++ b/src/ui/pokemon-info-container.ts @@ -3,7 +3,7 @@ import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; import BattleScene from "../battle-scene"; import { Gender, getGenderColor, getGenderSymbol } from "../data/gender"; import { getNatureName } from "../data/nature"; -import { Type } from "../data/type"; +import { Type } from "#enums/type"; import Pokemon from "../field/pokemon"; import i18next from "i18next"; import { DexAttr, DexEntry, StarterDataEntry } from "../system/game-data"; diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index 4975f05b8a3..82d390016f7 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -15,7 +15,8 @@ import { TrainerVariant } from "../field/trainer"; import { Challenges } from "#enums/challenges"; import { getLuckString, getLuckTextTint } from "../modifier/modifier-type"; import RoundRectangle from "phaser3-rex-plugins/plugins/roundrectangle"; -import { Type, getTypeRgb } from "../data/type"; +import { getTypeRgb } from "#app/data/type"; +import { Type } from "#enums/type"; import { TypeColor, TypeShadow } from "#app/enums/color"; import { getNatureStatMultiplier, getNatureName } from "../data/nature"; import { getVariantTint } from "#app/data/variant"; diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index bb4dc74d44e..a08b7ceaec6 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -10,13 +10,13 @@ import { speciesEggMoves } from "#app/data/balance/egg-moves"; import { GrowthRate, getGrowthRateColor } from "#app/data/exp"; import { Gender, getGenderColor, getGenderSymbol } from "#app/data/gender"; import { allMoves } from "#app/data/move"; -import { Nature, getNatureName } from "#app/data/nature"; +import { getNatureName } from "#app/data/nature"; import { pokemonFormChanges } from "#app/data/pokemon-forms"; import { LevelMoves, pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves"; import PokemonSpecies, { allSpecies, getPokemonSpeciesForm, getPokerusStarters } from "#app/data/pokemon-species"; import { getStarterValueFriendshipCap, speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters"; import { starterPassiveAbilities } from "#app/data/balance/passives"; -import { Type } from "#app/data/type"; +import { Type } from "#enums/type"; import { GameModes } from "#app/game-mode"; import { AbilityAttr, DexAttr, DexAttrProps, DexEntry, StarterMoveset, StarterAttributes, StarterPreferences, StarterPrefs } from "#app/system/game-data"; import { Tutorial, handleTutorial } from "#app/tutorial"; @@ -50,6 +50,7 @@ import { TitlePhase } from "#app/phases/title-phase"; import { Abilities } from "#enums/abilities"; import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCandyCounts } from "#app/data/balance/starters"; import { BooleanHolder, capitalizeString, fixedInt, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, randIntRange, rgbHexToRgba, toReadableString } from "#app/utils"; +import type { Nature } from "#enums/nature"; export type StarterSelectCallback = (starters: Starter[]) => void; diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index 4b36867571e..2e05f3de4c1 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -5,16 +5,17 @@ import * as Utils from "#app/utils"; import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; import { argbFromRgba } from "@material/material-color-utilities"; -import { Type, getTypeRgb } from "#app/data/type"; +import { getTypeRgb } from "#app/data/type"; +import { Type } from "#enums/type"; import { TextStyle, addBBCodeTextObject, addTextObject, getBBCodeFrag } from "#app/ui/text"; import Move, { MoveCategory } from "#app/data/move"; import { getPokeballAtlasKey } from "#app/data/pokeball"; import { getGenderColor, getGenderSymbol } from "#app/data/gender"; import { getLevelRelExp, getLevelTotalExp } from "#app/data/exp"; import { PokemonHeldItemModifier } from "#app/modifier/modifier"; -import { StatusEffect } from "#app/data/status-effect"; +import { StatusEffect } from "#enums/status-effect"; import { getBiomeName } from "#app/data/balance/biomes"; -import { Nature, getNatureName, getNatureStatMultiplier } from "#app/data/nature"; +import { getNatureName, getNatureStatMultiplier } from "#app/data/nature"; import { loggedInUser } from "#app/account"; import { Variant, getVariantTint } from "#app/data/variant"; import { Button } from "#enums/buttons"; @@ -23,6 +24,7 @@ import i18next from "i18next"; import { modifierSortFunc } from "#app/modifier/modifier"; import { PlayerGender } from "#enums/player-gender"; import { Stat, PERMANENT_STATS, getStatKey } from "#enums/stat"; +import { Nature } from "#enums/nature"; enum Page { PROFILE,