diff --git a/src/data/ability.ts b/src/data/ability.ts index 5e261f46316..2b24fc5d09e 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4296,6 +4296,10 @@ export class ReduceBerryUseThresholdAbAttr extends AbAttr { } } +/** + * Ability attribute used for abilites that change the ability owner's weight + * Used for Heavy Metal (doubling weight) and Light Metal (halving weight) + */ export class WeightMultiplierAbAttr extends AbAttr { private multiplier: integer; diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 3be6562307b..e92446ef5a2 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2281,6 +2281,36 @@ export class TarShotTag extends BattlerTag { } } +/** + * Battler Tag that keeps track of how many times the user has Autotomized + * Each count of Autotomization reduces the weight by 100kg + */ +export class AutotomizedTag extends BattlerTag { + public autotomizeCount: number = 0; + constructor(sourceMove: Moves = Moves.AUTOTOMIZE) { + super(BattlerTagType.AUTOTOMIZED, BattlerTagLapseType.CUSTOM, 1, sourceMove); + } + + /** + * Adds an autotomize count to the Pokemon. Each stack reduces weight by 100kg + * If the Pokemon is over 0.1kg it also displays a message. + * @param pokemon The Pokemon that is being autotomized + */ + onAdd(pokemon: Pokemon): void { + const minWeight = 0.1; + if (pokemon.getWeight() > minWeight) { + pokemon.scene.queueMessage(i18next.t("battlerTags:autotomizeOnAdd", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) + })); + } + this.autotomizeCount += 1; + } + + onOverlap(pokemon: Pokemon): void { + this.onAdd(pokemon); + } +} + export class SubstituteTag extends BattlerTag { /** The substitute's remaining HP. If HP is depleted, the Substitute fades. */ public hp: number; @@ -2568,6 +2598,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new GorillaTacticsTag(); case BattlerTagType.SUBSTITUTE: return new SubstituteTag(sourceMove, sourceId); + case BattlerTagType.AUTOTOMIZED: + return new AutotomizedTag(); case BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON: return new MysteryEncounterPostSummonTag(); case BattlerTagType.HEAL_BLOCK: diff --git a/src/data/move.ts b/src/data/move.ts index 44d01c71055..8866e86f708 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -5174,31 +5174,29 @@ export class RevivalBlessingAttr extends MoveEffectAttr { } export class ForceSwitchOutAttr extends MoveEffectAttr { - private user: boolean; - private batonPass: boolean; - - constructor(user?: boolean, batonPass?: boolean) { + constructor( + private selfSwitch: boolean = false, + private batonPass: boolean = false + ) { super(false, MoveEffectTrigger.POST_APPLY, false, true); - this.user = !!user; - this.batonPass = !!batonPass; } isBatonPass() { return this.batonPass; } + // TODO: Why is this a Promise? apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { return new Promise(resolve => { - // Check if the move category is not STATUS or if the switch out condition is not met if (!this.getSwitchOutCondition()(user, target, move)) { return resolve(false); } - // Move the switch out logic inside the conditional block - // This ensures that the switch out only happens when the conditions are met - const switchOutTarget = this.user ? user : target; - if (switchOutTarget instanceof PlayerPokemon) { + // Move the switch out logic inside the conditional block + // This ensures that the switch out only happens when the conditions are met + const switchOutTarget = this.selfSwitch ? user : target; + if (switchOutTarget instanceof PlayerPokemon) { switchOutTarget.leaveField(!this.batonPass); if (switchOutTarget.hp > 0) { @@ -5207,41 +5205,43 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } else { resolve(false); } - return; - } else if (user.scene.currentBattle.battleType !== BattleType.WILD) { - // Switch out logic for trainer battles + return; + } else if (user.scene.currentBattle.battleType !== BattleType.WILD) { + // Switch out logic for trainer battles switchOutTarget.leaveField(!this.batonPass); - if (switchOutTarget.hp > 0) { - // for opponent switching out - user.scene.prependToPhase(new SwitchSummonPhase(user.scene, switchOutTarget.getFieldIndex(), (user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0), false, this.batonPass, false), MoveEndPhase); + if (switchOutTarget.hp > 0) { + // for opponent switching out + user.scene.prependToPhase(new SwitchSummonPhase(user.scene, switchOutTarget.getFieldIndex(), + (user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0), + false, this.batonPass, false), MoveEndPhase); } - } else { - // Switch out logic for everything else (eg: WILD battles) - switchOutTarget.leaveField(false); + } else { + // Switch out logic for everything else (eg: WILD battles) + switchOutTarget.leaveField(false); - if (switchOutTarget.hp) { - user.scene.queueMessage(i18next.t("moveTriggers:fled", {pokemonName: getPokemonNameWithAffix(switchOutTarget)}), null, true, 500); + if (switchOutTarget.hp) { + user.scene.queueMessage(i18next.t("moveTriggers:fled", {pokemonName: getPokemonNameWithAffix(switchOutTarget)}), null, true, 500); // in double battles redirect potential moves off fled pokemon if (switchOutTarget.scene.currentBattle.double) { const allyPokemon = switchOutTarget.getAlly(); switchOutTarget.scene.redirectPokemonMoves(switchOutTarget, allyPokemon); } - } + } - if (!switchOutTarget.getAlly()?.isActive(true)) { - user.scene.clearEnemyHeldItemModifiers(); + if (!switchOutTarget.getAlly()?.isActive(true)) { + user.scene.clearEnemyHeldItemModifiers(); - if (switchOutTarget.hp) { - user.scene.pushPhase(new BattleEndPhase(user.scene)); - user.scene.pushPhase(new NewBattlePhase(user.scene)); - } - } - } + if (switchOutTarget.hp) { + user.scene.pushPhase(new BattleEndPhase(user.scene)); + user.scene.pushPhase(new NewBattlePhase(user.scene)); + } + } + } - resolve(true); - }); + resolve(true); + }); } getCondition(): MoveConditionFunc { @@ -5256,29 +5256,33 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { getSwitchOutCondition(): MoveConditionFunc { return (user, target, move) => { - const switchOutTarget = (this.user ? user : target); + const switchOutTarget = (this.selfSwitch ? user : target); const player = switchOutTarget instanceof PlayerPokemon; - if (!this.user && move.hitsSubstitute(user, target)) { - return false; + if (!this.selfSwitch) { + if (move.hitsSubstitute(user, target)) { + return false; + } + + const blockedByAbility = new Utils.BooleanHolder(false); + applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility); + return !blockedByAbility.value; } - if (!this.user && move.category === MoveCategory.STATUS && (target.hasAbilityWithAttr(ForceSwitchOutImmunityAbAttr))) { - return false; - } - - if (!player && !user.scene.currentBattle.battleType) { + if (!player && user.scene.currentBattle.battleType === BattleType.WILD) { if (this.batonPass) { return false; } // Don't allow wild opponents to flee on the boss stage since it can ruin a run early on - if (!(user.scene.currentBattle.waveIndex % 10)) { + if (user.scene.currentBattle.waveIndex % 10 === 0) { return false; } } const party = player ? user.scene.getParty() : user.scene.getEnemyParty(); - return (!player && !user.scene.currentBattle.battleType) || party.filter(p => p.isAllowedInBattle() && (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > user.scene.currentBattle.getBattlerCount(); + return (!player && !user.scene.currentBattle.battleType) + || party.filter(p => p.isAllowedInBattle() + && (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > user.scene.currentBattle.getBattlerCount(); }; } @@ -5286,8 +5290,8 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { if (!user.scene.getEnemyParty().find(p => p.isActive() && !p.isOnField())) { return -20; } - let ret = this.user ? Math.floor((1 - user.getHpRatio()) * 20) : super.getUserBenefitScore(user, target, move); - if (this.user && this.batonPass) { + let ret = this.selfSwitch ? Math.floor((1 - user.getHpRatio()) * 20) : super.getUserBenefitScore(user, target, move); + if (this.selfSwitch && this.batonPass) { const statStageTotal = user.getStatStages().reduce((s: integer, total: integer) => total += s, 0); ret = ret / 2 + (Phaser.Tweens.Builders.GetEaseFunction("Sine.easeOut")(Math.min(Math.abs(statStageTotal), 10) / 10) * (statStageTotal >= 0 ? 10 : -10)); } @@ -8105,7 +8109,7 @@ export function initMoves() { .attr(MovePowerMultiplierAttr, (user, target, move) => target.status && (target.status.effect === StatusEffect.POISON || target.status.effect === StatusEffect.TOXIC) ? 2 : 1), new SelfStatusMove(Moves.AUTOTOMIZE, Type.STEEL, -1, 15, -1, 0, 5) .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true) - .partial(), + .attr(AddBattlerTagAttr, BattlerTagType.AUTOTOMIZED, true), new SelfStatusMove(Moves.RAGE_POWDER, Type.BUG, -1, 20, -1, 2, 5) .powderMove() .attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, true), diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index 21ba3a9890f..82593a4e08b 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -2067,7 +2067,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ULTRA_BALL; - p.formIndex = Utils.randSeedInt(5, 1); // Shock, Burn, Chill, or Douse Drive + p.formIndex = Utils.randSeedInt(4, 1); // Shock, Burn, Chill, or Douse Drive })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BASCULEGION, Species.JELLICENT ], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 6cf2d260dcb..d606ae319f7 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -79,6 +79,7 @@ export enum BattlerTagType { TAR_SHOT = "TAR_SHOT", BURNED_UP = "BURNED_UP", DOUBLE_SHOCKED = "DOUBLE_SHOCKED", + AUTOTOMIZED = "AUTOTOMIZED", MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON", HEAL_BLOCK = "HEAL_BLOCK", } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index a8d82003ca5..7734f8adec2 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -17,7 +17,7 @@ import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims"; import { Status, StatusEffect, getRandomStatus } from "../data/status-effect"; import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions"; import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms"; -import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag } from "../data/battler-tags"; +import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag } from "../data/battler-tags"; import { WeatherType } from "../data/weather"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag"; import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs } from "../data/ability"; @@ -1427,11 +1427,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return false; } + /** + * Gets the weight of the Pokemon with subtractive modifiers (Autotomize) happening first + * and then multiplicative modifiers happening after (Heavy Metal and Light Metal) + * @returns the kg of the Pokemon (minimum of 0.1) + */ getWeight(): number { - const weight = new Utils.NumberHolder(this.species.weight); + const autotomizedTag = this.getTag(AutotomizedTag); + let weightRemoved = 0; + if (!Utils.isNullOrUndefined(autotomizedTag)) { + weightRemoved = 100 * autotomizedTag!.autotomizeCount; + } + const minWeight = 0.1; + const weight = new Utils.NumberHolder(this.species.weight - weightRemoved); + // This will trigger the ability overlay so only call this function when necessary applyAbAttrs(WeightMultiplierAbAttr, this, null, false, weight); - return weight.value; + return Math.max(minWeight, weight.value); } /** diff --git a/src/locales/en/battler-tags.json b/src/locales/en/battler-tags.json index b31826b0244..6b513f3a832 100644 --- a/src/locales/en/battler-tags.json +++ b/src/locales/en/battler-tags.json @@ -73,5 +73,6 @@ "tarShotOnAdd": "{{pokemonNameWithAffix}} became weaker to fire!", "substituteOnAdd": "{{pokemonNameWithAffix}} put in a substitute!", "substituteOnHit": "The substitute took damage for {{pokemonNameWithAffix}}!", - "substituteOnRemove": "{{pokemonNameWithAffix}}'s substitute faded!" + "substituteOnRemove": "{{pokemonNameWithAffix}}'s substitute faded!", + "autotomizeOnAdd": "{{pokemonNameWIthAffix}} became nimble!" } diff --git a/src/locales/es/move.json b/src/locales/es/move.json index 21f73bbf1d3..f7e563a6684 100644 --- a/src/locales/es/move.json +++ b/src/locales/es/move.json @@ -2913,7 +2913,7 @@ }, "zippyZap": { "name": "Pikaturbo", - "effect": "The user attacks the target with bursts of electricity at high speed. This move always goes first and raises the user's evasiveness." + "effect": "Ataque eléctrico a la velocidad del rayo. Este movimiento tiene prioridad alta y aumenta la Evasión del usuario." }, "splishySplash": { "name": "Salpikasurf", diff --git a/src/phases/form-change-phase.ts b/src/phases/form-change-phase.ts index 33c1f8e8cef..1f18457146d 100644 --- a/src/phases/form-change-phase.ts +++ b/src/phases/form-change-phase.ts @@ -9,6 +9,7 @@ import PartyUiHandler from "../ui/party-ui-handler"; import { getPokemonNameWithAffix } from "../messages"; import { EndEvolutionPhase } from "./end-evolution-phase"; import { EvolutionPhase } from "./evolution-phase"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; export class FormChangePhase extends EvolutionPhase { private formChange: SpeciesFormChange; @@ -157,6 +158,7 @@ export class FormChangePhase extends EvolutionPhase { } end(): void { + this.pokemon.findAndRemoveTags(t => t.tagType === BattlerTagType.AUTOTOMIZED); if (this.modal) { this.scene.ui.revertMode().then(() => { if (this.scene.ui.getMode() === Mode.PARTY) { diff --git a/src/phases/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts index dde500e156a..c28cc28b592 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -3,6 +3,7 @@ import { SemiInvulnerableTag } from "#app/data/battler-tags"; import { SpeciesFormChange, getSpeciesFormChangeMessage } from "#app/data/pokemon-forms"; import { getTypeRgb } from "#app/data/type"; import { BattleSpec } from "#app/enums/battle-spec"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; import Pokemon, { EnemyPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { BattlePhase } from "./battle-phase"; @@ -113,6 +114,7 @@ export class QuietFormChangePhase extends BattlePhase { } end(): void { + this.pokemon.findAndRemoveTags(t => t.tagType === BattlerTagType.AUTOTOMIZED); if (this.pokemon.scene?.currentBattle.battleSpec === BattleSpec.FINAL_BOSS && this.pokemon instanceof EnemyPokemon) { this.scene.playBgm(); this.scene.unshiftPhase(new PokemonHealPhase(this.scene, this.pokemon.getBattlerIndex(), this.pokemon.getMaxHp(), null, false, false, false, true)); diff --git a/src/test/moves/autotomize.test.ts b/src/test/moves/autotomize.test.ts new file mode 100644 index 00000000000..329b92b38fe --- /dev/null +++ b/src/test/moves/autotomize.test.ts @@ -0,0 +1,98 @@ +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, it, expect } from "vitest"; + +describe("Moves - Autotomize", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const TIMEOUT = 20 * 1000; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([Moves.AUTOTOMIZE, Moves.KINGS_SHIELD, Moves.FALSE_SWIPE]) + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("Autotomize should reduce weight", async () => { + const baseDracozoltWeight = 190; + const oneAutotomizeDracozoltWeight = 90; + const twoAutotomizeDracozoltWeight = 0.1; + const threeAutotomizeDracozoltWeight = 0.1; + + await game.classicMode.startBattle([Species.DRACOZOLT]); + const playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getWeight()).toBe(baseDracozoltWeight); + game.move.select(Moves.AUTOTOMIZE); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(oneAutotomizeDracozoltWeight); + + game.move.select(Moves.AUTOTOMIZE); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(twoAutotomizeDracozoltWeight); + + + game.move.select(Moves.AUTOTOMIZE); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(threeAutotomizeDracozoltWeight); + }, TIMEOUT); + + it("Changing forms should revert weight", async () => { + const baseAegislashWeight = 53; + const autotomizeAegislashWeight = 0.1; + + await game.classicMode.startBattle([Species.AEGISLASH]); + const playerPokemon = game.scene.getPlayerPokemon()!; + + expect(playerPokemon.getWeight()).toBe(baseAegislashWeight); + + game.move.select(Moves.AUTOTOMIZE); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(autotomizeAegislashWeight); + + // Transform to sword form + game.move.select(Moves.FALSE_SWIPE); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(baseAegislashWeight); + + game.move.select(Moves.AUTOTOMIZE); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(autotomizeAegislashWeight); + + // Transform to shield form + game.move.select(Moves.KINGS_SHIELD); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(baseAegislashWeight); + + game.move.select(Moves.AUTOTOMIZE); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(autotomizeAegislashWeight); + }, TIMEOUT); + + it("Autotomize should interact with light metal correctly", async () => { + const baseLightGroudonWeight = 475; + const autotomizeLightGroudonWeight = 425; + game.override.ability(Abilities.LIGHT_METAL); + await game.classicMode.startBattle([Species.GROUDON]); + const playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getWeight()).toBe(baseLightGroudonWeight); + game.move.select(Moves.AUTOTOMIZE); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(autotomizeLightGroudonWeight); + }, TIMEOUT); +}); diff --git a/src/test/moves/dragon_tail.test.ts b/src/test/moves/dragon_tail.test.ts index 4b222a0c477..dd7193dc97f 100644 --- a/src/test/moves/dragon_tail.test.ts +++ b/src/test/moves/dragon_tail.test.ts @@ -1,16 +1,11 @@ import { BattlerIndex } from "#app/battle"; import { allMoves } from "#app/data/move"; -import { BattleEndPhase } from "#app/phases/battle-end-phase"; -import { BerryPhase } from "#app/phases/berry-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 GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; -import GameManager from "../utils/gameManager"; - - +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Moves - Dragon Tail", () => { let phaserGame: Phaser.Game; @@ -29,7 +24,7 @@ describe("Moves - Dragon Tail", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override.battleType("single") - .moveset([Moves.DRAGON_TAIL, Moves.SPLASH]) + .moveset([Moves.DRAGON_TAIL, Moves.SPLASH, Moves.FLAMETHROWER]) .enemySpecies(Species.WAILORD) .enemyMoveset(Moves.SPLASH) .startingLevel(5) @@ -38,109 +33,110 @@ describe("Moves - Dragon Tail", () => { vi.spyOn(allMoves[Moves.DRAGON_TAIL], "accuracy", "get").mockReturnValue(100); }); - test( - "Single battle should cause opponent to flee, and not crash", - async () => { - await game.startBattle([Species.DRATINI]); + it("should cause opponent to flee, and not crash", async () => { + await game.classicMode.startBattle([Species.DRATINI]); - const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; - game.move.select(Moves.DRAGON_TAIL); + game.move.select(Moves.DRAGON_TAIL); - await game.phaseInterceptor.to(BerryPhase); + await game.phaseInterceptor.to("BerryPhase"); - const isVisible = enemyPokemon.visible; - const hasFled = enemyPokemon.switchOutStatus; - expect(!isVisible && hasFled).toBe(true); + const isVisible = enemyPokemon.visible; + const hasFled = enemyPokemon.switchOutStatus; + expect(!isVisible && hasFled).toBe(true); - // simply want to test that the game makes it this far without crashing - await game.phaseInterceptor.to(BattleEndPhase); - } - ); + // simply want to test that the game makes it this far without crashing + await game.phaseInterceptor.to("BattleEndPhase"); + }); - test( - "Single battle should cause opponent to flee, display ability, and not crash", - async () => { - game.override.enemyAbility(Abilities.ROUGH_SKIN); - await game.startBattle([Species.DRATINI]); + it("should cause opponent to flee, display ability, and not crash", async () => { + game.override.enemyAbility(Abilities.ROUGH_SKIN); + await game.classicMode.startBattle([Species.DRATINI]); - const leadPokemon = game.scene.getPlayerPokemon()!; - const enemyPokemon = game.scene.getEnemyPokemon()!; + const leadPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; - game.move.select(Moves.DRAGON_TAIL); + game.move.select(Moves.DRAGON_TAIL); - await game.phaseInterceptor.to(BerryPhase); + await game.phaseInterceptor.to("BerryPhase"); - const isVisible = enemyPokemon.visible; - const hasFled = enemyPokemon.switchOutStatus; - expect(!isVisible && hasFled).toBe(true); - expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); - } - ); + const isVisible = enemyPokemon.visible; + const hasFled = enemyPokemon.switchOutStatus; + expect(!isVisible && hasFled).toBe(true); + expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); + }); - test( - "Double battles should proceed without crashing", - async () => { - game.override.battleType("double").enemyMoveset(Moves.SPLASH); - game.override.moveset([Moves.DRAGON_TAIL, Moves.SPLASH, Moves.FLAMETHROWER]) - .enemyAbility(Abilities.ROUGH_SKIN); - await game.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]); + it("should proceed without crashing in a double battle", async () => { + game.override + .battleType("double").enemyMoveset(Moves.SPLASH) + .enemyAbility(Abilities.ROUGH_SKIN); + await game.classicMode.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]); - const leadPokemon = game.scene.getParty()[0]!; + const leadPokemon = game.scene.getParty()[0]!; - const enemyLeadPokemon = game.scene.getEnemyParty()[0]!; - const enemySecPokemon = game.scene.getEnemyParty()[1]!; + const enemyLeadPokemon = game.scene.getEnemyParty()[0]!; + const enemySecPokemon = game.scene.getEnemyParty()[1]!; - game.move.select(Moves.DRAGON_TAIL, 0, BattlerIndex.ENEMY); - game.move.select(Moves.SPLASH, 1); + game.move.select(Moves.DRAGON_TAIL, 0, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, 1); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); - const isVisibleLead = enemyLeadPokemon.visible; - const hasFledLead = enemyLeadPokemon.switchOutStatus; - const isVisibleSec = enemySecPokemon.visible; - const hasFledSec = enemySecPokemon.switchOutStatus; - expect(!isVisibleLead && hasFledLead && isVisibleSec && !hasFledSec).toBe(true); - expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); + const isVisibleLead = enemyLeadPokemon.visible; + const hasFledLead = enemyLeadPokemon.switchOutStatus; + const isVisibleSec = enemySecPokemon.visible; + const hasFledSec = enemySecPokemon.switchOutStatus; + expect(!isVisibleLead && hasFledLead && isVisibleSec && !hasFledSec).toBe(true); + expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); - // second turn - game.move.select(Moves.FLAMETHROWER, 0, BattlerIndex.ENEMY_2); - game.move.select(Moves.SPLASH, 1); + // second turn + game.move.select(Moves.FLAMETHROWER, 0, BattlerIndex.ENEMY_2); + game.move.select(Moves.SPLASH, 1); - await game.phaseInterceptor.to(BerryPhase); - expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp()); - } - ); + await game.phaseInterceptor.to("BerryPhase"); + expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp()); + }); - test( - "Flee move redirection works", - async () => { - game.override.battleType("double").enemyMoveset(Moves.SPLASH); - game.override.moveset([Moves.DRAGON_TAIL, Moves.SPLASH, Moves.FLAMETHROWER]); - game.override.enemyAbility(Abilities.ROUGH_SKIN); - await game.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]); + it("should redirect targets upon opponent flee", async () => { + game.override + .battleType("double") + .enemyMoveset(Moves.SPLASH) + .enemyAbility(Abilities.ROUGH_SKIN); + await game.classicMode.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]); - const leadPokemon = game.scene.getParty()[0]!; - const secPokemon = game.scene.getParty()[1]!; + const leadPokemon = game.scene.getParty()[0]!; + const secPokemon = game.scene.getParty()[1]!; - const enemyLeadPokemon = game.scene.getEnemyParty()[0]!; - const enemySecPokemon = game.scene.getEnemyParty()[1]!; + const enemyLeadPokemon = game.scene.getEnemyParty()[0]!; + const enemySecPokemon = game.scene.getEnemyParty()[1]!; - game.move.select(Moves.DRAGON_TAIL, 0, BattlerIndex.ENEMY); - // target the same pokemon, second move should be redirected after first flees - game.move.select(Moves.DRAGON_TAIL, 1, BattlerIndex.ENEMY); + game.move.select(Moves.DRAGON_TAIL, 0, BattlerIndex.ENEMY); + // target the same pokemon, second move should be redirected after first flees + game.move.select(Moves.DRAGON_TAIL, 1, BattlerIndex.ENEMY); - await game.phaseInterceptor.to(BerryPhase); + await game.phaseInterceptor.to("BerryPhase"); - const isVisibleLead = enemyLeadPokemon.visible; - const hasFledLead = enemyLeadPokemon.switchOutStatus; - const isVisibleSec = enemySecPokemon.visible; - const hasFledSec = enemySecPokemon.switchOutStatus; - expect(!isVisibleLead && hasFledLead && !isVisibleSec && hasFledSec).toBe(true); - expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); - expect(secPokemon.hp).toBeLessThan(secPokemon.getMaxHp()); - expect(enemyLeadPokemon.hp).toBeLessThan(enemyLeadPokemon.getMaxHp()); - expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp()); - } - ); + const isVisibleLead = enemyLeadPokemon.visible; + const hasFledLead = enemyLeadPokemon.switchOutStatus; + const isVisibleSec = enemySecPokemon.visible; + const hasFledSec = enemySecPokemon.switchOutStatus; + expect(!isVisibleLead && hasFledLead && !isVisibleSec && hasFledSec).toBe(true); + expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); + expect(secPokemon.hp).toBeLessThan(secPokemon.getMaxHp()); + expect(enemyLeadPokemon.hp).toBeLessThan(enemyLeadPokemon.getMaxHp()); + expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp()); + }); + + it("doesn't switch out if the target has suction cups", async () => { + game.override.enemyAbility(Abilities.SUCTION_CUPS); + await game.classicMode.startBattle([Species.REGIELEKI]); + + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DRAGON_TAIL); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(enemy.isFullHp()).toBe(false); + }); }); diff --git a/src/test/moves/shell_side_arm.test.ts b/src/test/moves/shell_side_arm.test.ts index ded7ed82fd1..643313f1eae 100644 --- a/src/test/moves/shell_side_arm.test.ts +++ b/src/test/moves/shell_side_arm.test.ts @@ -5,11 +5,14 @@ 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, it, expect, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Moves - Shell Side Arm", () => { let phaserGame: Phaser.Game; let game: GameManager; + const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM]; + const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0]; + beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -34,14 +37,11 @@ describe("Moves - Shell Side Arm", () => { it("becomes a physical attack if forecasted to deal more damage as physical", async () => { game.override.enemySpecies(Species.SNORLAX); - await game.classicMode.startBattle([Species.MANAPHY]); + await game.classicMode.startBattle([Species.RAMPARDOS]); - const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM]; - const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0]; vi.spyOn(shellSideArmAttr, "apply"); game.move.select(Moves.SHELL_SIDE_ARM); - await game.phaseInterceptor.to("MoveEffectPhase"); expect(shellSideArmAttr.apply).toHaveLastReturnedWith(true); @@ -50,14 +50,11 @@ describe("Moves - Shell Side Arm", () => { it("remains a special attack if forecasted to deal more damage as special", async () => { game.override.enemySpecies(Species.SLOWBRO); - await game.classicMode.startBattle([Species.MANAPHY]); + await game.classicMode.startBattle([Species.XURKITREE]); - const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM]; - const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0]; vi.spyOn(shellSideArmAttr, "apply"); game.move.select(Moves.SHELL_SIDE_ARM); - await game.phaseInterceptor.to("MoveEffectPhase"); expect(shellSideArmAttr.apply).toHaveLastReturnedWith(false); @@ -70,14 +67,10 @@ describe("Moves - Shell Side Arm", () => { await game.classicMode.startBattle([Species.MANAPHY]); - const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM]; - const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0]; vi.spyOn(shellSideArmAttr, "apply"); game.move.select(Moves.SHELL_SIDE_ARM); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); - await game.phaseInterceptor.to("BerryPhase", false); expect(shellSideArmAttr.apply).toHaveLastReturnedWith(false); diff --git a/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts b/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts index 44a5197a39e..1045f9420b9 100644 --- a/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts @@ -42,7 +42,8 @@ describe("Teleporting Hijinks - Mystery Encounter", () => { .startingWave(defaultWave) .startingBiome(defaultBiome) .disableTrainerWaves() - .enemyPassiveAbility(Abilities.BALL_FETCH); + .enemyPassiveAbility(Abilities.BALL_FETCH) + .enemyAbility(Abilities.BALL_FETCH); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([