diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 565e5e9aeb34..1523f50985c4 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1225,6 +1225,14 @@ export default class BattleScene extends SceneBase { pokemon.resetBattleData(); applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon); } + // Only trigger form change when Mimikyu is in Busted form + // Hardcoded Mimikyu for now in case it is fused with another pokemon + if (pokemon.species.speciesId === Species.MIMIKYU && pokemon.hasAbility(Abilities.DISGUISE) && pokemon.formIndex === 1) { + this.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger); + } + + pokemon.resetBattleData(); + applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon); } this.unshiftPhase(new ShowTrainerPhase(this)); diff --git a/src/data/ability.ts b/src/data/ability.ts index ff8aeaa74146..49ec524ecf8a 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -245,25 +245,6 @@ export class PreDefendAbAttr extends AbAttr { } } -export class PreDefendFormChangeAbAttr extends PreDefendAbAttr { - private formFunc: (p: Pokemon) => integer; - - constructor(formFunc: ((p: Pokemon) => integer)) { - super(true); - - this.formFunc = formFunc; - } - - applyPreDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { - const formIndex = this.formFunc(pokemon); - if (formIndex !== pokemon.formIndex) { - pokemon.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger, false); - return true; - } - - return false; - } -} export class PreDefendFullHpEndureAbAttr extends PreDefendAbAttr { applyPreDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { if (pokemon.hp === pokemon.getMaxHp() && @@ -327,21 +308,6 @@ export class ReceivedTypeDamageMultiplierAbAttr extends ReceivedMoveDamageMultip } } -export class PreDefendMoveDamageToOneAbAttr extends ReceivedMoveDamageMultiplierAbAttr { - constructor(condition: PokemonDefendCondition) { - super(condition, 1); - } - - applyPreDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { - if (this.condition(pokemon, attacker, move)) { - (args[0] as Utils.NumberHolder).value = Math.floor(pokemon.getMaxHp() / 8); - return true; - } - - return false; - } -} - /** * Determines whether a Pokemon is immune to a move because of an ability. * @extends PreDefendAbAttr @@ -490,45 +456,6 @@ export class PostDefendAbAttr extends AbAttr { } } -export class PostDefendDisguiseAbAttr extends PostDefendAbAttr { - - applyPostDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (pokemon.formIndex === 0 && pokemon.battleData.hitCount !== 0 && (move.category === MoveCategory.SPECIAL || move.category === MoveCategory.PHYSICAL)) { - - const recoilDamage = Math.ceil((pokemon.getMaxHp() / 8) - attacker.turnData.damageDealt); - if (!recoilDamage) { - return false; - } - pokemon.damageAndUpdate(recoilDamage, HitResult.OTHER); - pokemon.turnData.damageTaken += recoilDamage; - pokemon.scene.queueMessage(getPokemonMessage(pokemon, "'s disguise was busted!")); - return true; - } - - return false; - } -} - -export class PostDefendFormChangeAbAttr extends PostDefendAbAttr { - private formFunc: (p: Pokemon) => integer; - - constructor(formFunc: ((p: Pokemon) => integer)) { - super(true); - - this.formFunc = formFunc; - } - - applyPostDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - const formIndex = this.formFunc(pokemon); - if (formIndex !== pokemon.formIndex) { - pokemon.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger, false); - return true; - } - - return false; - } -} - export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr { applyPreDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { const attackPriority = new Utils.IntegerHolder(move.priority); @@ -3874,6 +3801,54 @@ export class IceFaceBlockPhysicalAbAttr extends ReceivedMoveDamageMultiplierAbAt } } +/** + * Takes no damage from the first hit of a damaging move. + * This is used in the Disguise ability. + */ +export class DisguiseBlockDamageAbAttr extends ReceivedMoveDamageMultiplierAbAttr { + private multiplier: number; + + constructor(condition: PokemonDefendCondition, multiplier: number) { + super(condition, multiplier); + + this.multiplier = multiplier; + } + + /** + * Applies the Disguise pre-defense ability to the Pokémon. + * Removes BattlerTagType.DISGUISE when hit by an attack and is in its Disguised form. + * + * @param {Pokemon} pokemon - The Pokémon with the Disguise ability. + * @param {boolean} passive - Whether the ability is passive. + * @param {Pokemon} attacker - The attacking Pokémon. + * @param {PokemonMove} move - The move being used. + * @param {Utils.BooleanHolder} cancelled - A holder for whether the move was cancelled. + * @param {any[]} args - Additional arguments. + * @returns {boolean} - Whether the immunity was applied. + */ + applyPreDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { + if (this.condition(pokemon, attacker, move)) { + (args[0] as Utils.NumberHolder).value = this.multiplier; + pokemon.removeTag(BattlerTagType.DISGUISE); + pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 8), HitResult.OTHER); + return true; + } + + return false; + } + + /** + * Gets the message triggered when the Pokémon avoids damage using the Disguise ability. + * @param {Pokemon} pokemon - The Pokémon with the Disguise ability. + * @param {string} abilityName - The name of the ability. + * @param {...any} args - Additional arguments. + * @returns {string} - The trigger message. + */ + getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string { + return i18next.t("abilityTriggers:disguiseAvoidedDamage", { pokemonName: pokemon.name, abilityName: abilityName }); + } +} + /** * If a Pokémon with this Ability selects a damaging move, it has a 30% chance of going first in its priority bracket. If the Ability activates, this is announced at the start of the turn (after move selection). * @@ -4783,20 +4758,15 @@ export function initAbilities() { .attr(NoFusionAbilityAbAttr) .bypassFaint(), new Ability(Abilities.DISGUISE, 7) - .attr(PreDefendMoveDamageToOneAbAttr, (target, user, move) => target.formIndex === 0 && target.getAttackTypeEffectiveness(move.type, user) > 0) - .attr(PostSummonFormChangeAbAttr, p => p.battleData.hitCount === 0 ? 0 : 1) - .attr(PostBattleInitFormChangeAbAttr, () => 0) - .attr(PostDefendFormChangeAbAttr, p => p.battleData.hitCount === 0 ? 0 : 1) - .attr(PreDefendFormChangeAbAttr, p => p.battleData.hitCount === 0 ? 0 : 1) - .attr(PostDefendDisguiseAbAttr) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(UnsuppressableAbilityAbAttr) .attr(NoTransformAbilityAbAttr) .attr(NoFusionAbilityAbAttr) - .bypassFaint() - .ignorable() - .partial(), + // Add BattlerTagType.DISGUISE if the pokemon is in its disguised form + .conditionalAttr(pokemon => pokemon.formIndex === 0, PostSummonAddBattlerTagAbAttr, BattlerTagType.DISGUISE, 0, false) + .attr(DisguiseBlockDamageAbAttr, (target, user, move) => !!target.getTag(BattlerTagType.DISGUISE) && target.getAttackTypeEffectiveness(move.type, user) > 0, 0) + .ignorable(), new Ability(Abilities.BATTLE_BOND, 7) .attr(PostVictoryFormChangeAbAttr, () => 2) .attr(PostBattleInitFormChangeAbAttr, () => 1) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 139360736791..9c5c680da556 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1549,6 +1549,54 @@ export class IceFaceTag extends BattlerTag { } } +/** + * Provides the Disguise ability's effects. + */ +export class DisguiseTag extends BattlerTag { + constructor(sourceMove: Moves) { + super(BattlerTagType.DISGUISE, BattlerTagLapseType.CUSTOM, 1, sourceMove); + } + + /** + * Determines if the Disguise tag can be added to the Pokémon. + * @param {Pokemon} pokemon - The Pokémon to which the tag might be added. + * @returns {boolean} - True if the tag can be added, false otherwise. + */ + canAdd(pokemon: Pokemon): boolean { + const isFormDisguised = pokemon.formIndex === 0; + + // Hard code Mimikyu for now, this is to prevent the game from crashing if fused pokemon has Disguise + if (pokemon.species.speciesId === Species.MIMIKYU && isFormDisguised) { + return true; + } + return false; + } + + /** + * Applies the Disguise tag to the Pokémon. + * Triggers a form change to Disguised if the Pokémon is not in its Disguised form. + * @param {Pokemon} pokemon - The Pokémon to which the tag is added. + */ + onAdd(pokemon: Pokemon): void { + super.onAdd(pokemon); + + if (pokemon.formIndex !== 0) { + pokemon.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger); + } + } + + /** + * Removes the Disguise tag from the Pokémon. + * Triggers a form change to Busted when the tag is removed. + * @param {Pokemon} pokemon - The Pokémon from which the tag is removed. + */ + onRemove(pokemon: Pokemon): void { + super.onRemove(pokemon); + + pokemon.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger); + } +} + export class MysteryEncounterPostSummonTag extends BattlerTag { constructor(sourceMove: Moves) { super(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON, BattlerTagLapseType.CUSTOM, 1, sourceMove); @@ -1702,6 +1750,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: integer, sourc return new DestinyBondTag(sourceMove, sourceId); case BattlerTagType.ICE_FACE: return new IceFaceTag(sourceMove); + case BattlerTagType.DISGUISE: + return new DisguiseTag(sourceMove); case BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON: return new MysteryEncounterPostSummonTag(sourceMove); case BattlerTagType.NONE: diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index ae1532f0be01..b734abe190d9 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -371,6 +371,9 @@ export function getSpeciesFormChangeMessage(pokemon: Pokemon, formChange: Specie if (isRevert) { return `${prefix}${pokemon.name} reverted\nto its original form!`; } + if (pokemon.species.speciesId === Species.MIMIKYU) { + return "Its disguise served it as a decoy!"; + } return `${prefix}${preName} changed form!`; } diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index c458d7863f00..46faf1d1656b 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -62,5 +62,6 @@ export enum BattlerTagType { ICE_FACE = "ICE_FACE", RECEIVE_DOUBLE_DAMAGE = "TAKE_DOUBLE_DAMAGE", ALWAYS_GET_HIT = "ALWAYS_GET_HIT", + DISGUISE = "DISGUISE", MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON" // Provides effects on post-summon for MEs } diff --git a/src/locales/de/ability-trigger.ts b/src/locales/de/ability-trigger.ts index 4e69db202314..9c572302c095 100644 --- a/src/locales/de/ability-trigger.ts +++ b/src/locales/de/ability-trigger.ts @@ -7,5 +7,6 @@ export const abilityTriggers: SimpleTranslationEntries = { "iceFaceAvoidedDamage": "{{pokemonName}} wehrt Schaden mit {{abilityName}} ab!", "trace": "{{pokemonName}} kopiert {{abilityName}} von {{targetName}}!", "windPowerCharged": "Der Treffer durch {{moveName}} läd die Stärke von {{pokemonName}} auf!", + "disguiseAvoidedDamage" : "{{pokemonName}}'s disguise was busted!", "quickDraw": "Durch Schnellschuss kann {{pokemonName}} schneller handeln als sonst!", } as const; diff --git a/src/locales/en/ability-trigger.ts b/src/locales/en/ability-trigger.ts index b516bc8dde03..df4589695150 100644 --- a/src/locales/en/ability-trigger.ts +++ b/src/locales/en/ability-trigger.ts @@ -10,4 +10,5 @@ export const abilityTriggers: SimpleTranslationEntries = { "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", "windPowerCharged": "Being hit by {{moveName}} charged {{pokemonName}} with power!", "quickDraw": "{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", + "disguiseAvoidedDamage" : "{{pokemonName}}'s disguise was busted!", } as const; diff --git a/src/locales/es/ability-trigger.ts b/src/locales/es/ability-trigger.ts index 5c09c3832c04..fe0d9aadc7fe 100644 --- a/src/locales/es/ability-trigger.ts +++ b/src/locales/es/ability-trigger.ts @@ -8,4 +8,5 @@ export const abilityTriggers: SimpleTranslationEntries = { "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", "windPowerCharged": "¡{{pokemonName}} se ha cargado de electricidad gracias a {{moveName}}!", "quickDraw": "{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", + "disguiseAvoidedDamage" : "{{pokemonName}}'s disguise was busted!", } as const; diff --git a/src/locales/fr/ability-trigger.ts b/src/locales/fr/ability-trigger.ts index f99ff15c26fb..e595a501cb62 100644 --- a/src/locales/fr/ability-trigger.ts +++ b/src/locales/fr/ability-trigger.ts @@ -10,4 +10,5 @@ export const abilityTriggers: SimpleTranslationEntries = { "trace": "{{pokemonName}} copie le talent {{abilityName}}\nde {{targetName}} !", "windPowerCharged": "{{pokemonName}} a été touché par la capacité {{moveName}} et se charge en électricité !", "quickDraw": "{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", + "disguiseAvoidedDamage" : "{{pokemonName}}'s disguise was busted!", } as const; diff --git a/src/locales/it/ability-trigger.ts b/src/locales/it/ability-trigger.ts index 1f6fcfb12580..46839a32778f 100644 --- a/src/locales/it/ability-trigger.ts +++ b/src/locales/it/ability-trigger.ts @@ -8,4 +8,5 @@ export const abilityTriggers: SimpleTranslationEntries = { "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", "windPowerCharged": "Venire colpito da {{moveName}} ha caricato {{pokemonName}}!", "quickDraw":"{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", + "disguiseAvoidedDamage" : "{{pokemonName}}'s disguise was busted!", } as const; diff --git a/src/locales/ko/ability-trigger.ts b/src/locales/ko/ability-trigger.ts index 58ba7bf9aa6e..e49dafe6bb82 100644 --- a/src/locales/ko/ability-trigger.ts +++ b/src/locales/ko/ability-trigger.ts @@ -10,4 +10,5 @@ export const abilityTriggers: SimpleTranslationEntries = { "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", "windPowerCharged": "{{pokemonName}}[[는]]\n{{moveName}}에 맞아 충전되었다!", "quickDraw": "{{pokemonName}}[[는]]\n퀵드로에 의해 행동이 빨라졌다!", + "disguiseAvoidedDamage" : "{{pokemonName}}'s disguise was busted!", } as const; diff --git a/src/locales/pt_BR/ability-trigger.ts b/src/locales/pt_BR/ability-trigger.ts index 526c6def80d6..eb7e6e615cb6 100644 --- a/src/locales/pt_BR/ability-trigger.ts +++ b/src/locales/pt_BR/ability-trigger.ts @@ -9,5 +9,6 @@ export const abilityTriggers: SimpleTranslationEntries = { "poisonHeal": "{{abilityName}} de {{pokemonName}}\nrestaurou seus PS um pouco!", "trace": "{{pokemonName}} copiou {{abilityName}}\nde {{targetName}}!", "windPowerCharged": "Ser atingido por {{moveName}} carregou {{pokemonName}} com poder!", + "disguiseAvoidedDamage" : "{{pokemonName}}'s disguise was busted!", "quickDraw":"{{pokemonName}} pode agir mais rápido que o normal\ngraças ao seu Quick Draw!", } as const; diff --git a/src/locales/zh_CN/ability-trigger.ts b/src/locales/zh_CN/ability-trigger.ts index a9d7fa5b2023..9f02975afadf 100644 --- a/src/locales/zh_CN/ability-trigger.ts +++ b/src/locales/zh_CN/ability-trigger.ts @@ -8,4 +8,5 @@ export const abilityTriggers: SimpleTranslationEntries = { "trace": "{{pokemonName}}复制了{{targetName}}的\n{{abilityName}}!", "windPowerCharged": "受{{moveName}}的影响,{{pokemonName}}提升了能力!", "quickDraw":"因为速击效果发动,\n{{pokemonName}}比平常出招更快了!", + "disguiseAvoidedDamage" : "{{pokemonName}}'s disguise was busted!", } as const; diff --git a/src/locales/zh_TW/ability-trigger.ts b/src/locales/zh_TW/ability-trigger.ts index c436e5021f7b..888212c306f8 100644 --- a/src/locales/zh_TW/ability-trigger.ts +++ b/src/locales/zh_TW/ability-trigger.ts @@ -8,4 +8,5 @@ export const abilityTriggers: SimpleTranslationEntries = { "trace": "{{pokemonName}} 複製了 {{targetName}} 的\n{{abilityName}}!", "windPowerCharged": "受 {{moveName}} 的影響, {{pokemonName}} 提升了能力!", "quickDraw":"{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", + "disguiseAvoidedDamage" : "{{pokemonName}}'s disguise was busted!", } as const; diff --git a/src/test/abilities/disguise.test.ts b/src/test/abilities/disguise.test.ts index 297aa33e06c9..6fc46d55597a 100644 --- a/src/test/abilities/disguise.test.ts +++ b/src/test/abilities/disguise.test.ts @@ -1,19 +1,22 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import GameManager from "#test/utils/gameManager"; import { getMovePosition } from "#test/utils/gameManagerUtils"; -import * as Overrides from "#app/overrides"; +import * as overrides from "#app/overrides"; import { Moves } from "#enums/moves"; import { Abilities } from "#enums/abilities"; import { Species } from "#enums/species"; -import { Status, StatusEffect } from "#app/data/status-effect.js"; -import { TurnEndPhase } from "#app/phases.js"; -import { QuietFormChangePhase } from "#app/form-change-phase.js"; +import { StatusEffect } from "#app/data/status-effect.js"; +import { MoveEffectPhase, MoveEndPhase, TurnEndPhase, TurnInitPhase } from "#app/phases.js"; +import { BattlerTagType } from "#app/enums/battler-tag-type.js"; +import { BattleStat } from "#app/data/battle-stat.js"; const TIMEOUT = 20 * 1000; -describe("Abilities - DISGUISE", () => { +describe("Abilities - Disguise", () => { let phaserGame: Phaser.Game; let game: GameManager; + const bustedForm = 1; + const disguisedForm = 0; beforeAll(() => { phaserGame = new Phaser.Game({ @@ -27,72 +30,198 @@ describe("Abilities - DISGUISE", () => { beforeEach(() => { game = new GameManager(phaserGame); - const moveToUse = Moves.SPLASH; - vi.spyOn(Overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); - vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.DISGUISE); - vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); - vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); - }); + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MIMIKYU); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.DISGUISE); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); + + vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.REGIELEKI); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SHADOW_SNEAK, Moves.VACUUM_WAVE, Moves.TOXIC_THREAD, Moves.SPLASH]); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.UNNERVE); + }, TIMEOUT); + + it("takes no damage from attacking move and transforms to Busted form, taking 1/8 max HP damage from the disguise breaking", async () => { + await game.startBattle(); + + const mimikyu = game.scene.getEnemyPokemon(); + const maxHp = mimikyu.getMaxHp(); + const disguiseDamage = Math.floor(maxHp / 8); + + expect(mimikyu.getTag(BattlerTagType.DISGUISE)).toBeDefined(); + expect(mimikyu.formIndex).toBe(disguisedForm); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SHADOW_SNEAK)); + + await game.phaseInterceptor.to(MoveEndPhase); + + expect(mimikyu.hp).equals(maxHp - disguiseDamage); + expect(mimikyu.formIndex).toBe(bustedForm); + expect(mimikyu.getTag(BattlerTagType.DISGUISE)).toBeUndefined(); + }, TIMEOUT); + + it("doesn't break disguise when attacked with ineffective move", async () => { + await game.startBattle(); + + const mimikyu = game.scene.getEnemyPokemon(); + + expect(mimikyu.getTag(BattlerTagType.DISGUISE)).toBeDefined(); + expect(mimikyu.formIndex).toBe(disguisedForm); + + game.doAttack(getMovePosition(game.scene, 0, Moves.VACUUM_WAVE)); + + await game.phaseInterceptor.to(MoveEndPhase); + + expect(mimikyu.getTag(BattlerTagType.DISGUISE)).toBeDefined(); + expect(mimikyu.formIndex).toBe(disguisedForm); + }, TIMEOUT); + + it("takes no damage from the first hit of a multihit move and transforms to Busted form, then takes damage from the second hit", async () => { + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SURGING_STRIKES]); + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(5); + await game.startBattle(); + + const mimikyu = game.scene.getEnemyPokemon(); + const maxHp = mimikyu.getMaxHp(); + const disguiseDamage = Math.floor(maxHp / 8); + + expect(mimikyu.getTag(BattlerTagType.DISGUISE)).toBeDefined(); + expect(mimikyu.formIndex).toBe(disguisedForm); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SURGING_STRIKES)); + + // First hit + await game.phaseInterceptor.to(MoveEffectPhase); + expect(mimikyu.hp).equals(maxHp - disguiseDamage); + expect(mimikyu.formIndex).toBe(disguisedForm); + expect(mimikyu.getTag(BattlerTagType.ICE_FACE)).toBeUndefined(); + + // Second hit + await game.phaseInterceptor.to(MoveEffectPhase); + expect(mimikyu.hp).lessThan(maxHp - disguiseDamage); + expect(mimikyu.formIndex).toBe(bustedForm); + expect(mimikyu.getTag(BattlerTagType.DISGUISE)).toBeUndefined(); + }, TIMEOUT); + + it("takes effects from status moves and damage from status effects", async () => { + await game.startBattle(); + + const mimikyu = game.scene.getEnemyPokemon(); + expect(mimikyu.hp).toBe(mimikyu.getMaxHp()); + + game.doAttack(getMovePosition(game.scene, 0, Moves.TOXIC_THREAD)); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(mimikyu.getTag(BattlerTagType.DISGUISE)).toBeDefined(); + expect(mimikyu.formIndex).toBe(disguisedForm); + expect(mimikyu.status.effect).toBe(StatusEffect.POISON); + expect(mimikyu.summonData.battleStats[BattleStat.SPD]).toBe(-1); + expect(mimikyu.hp).toBeLessThan(mimikyu.getMaxHp()); + }, TIMEOUT); + + it("persists form change when switched out", async () => { + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SHADOW_SNEAK, Moves.SHADOW_SNEAK, Moves.SHADOW_SNEAK, Moves.SHADOW_SNEAK]); + vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(0); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.DISGUISE); + + await game.startBattle([Species.MIMIKYU, Species.FURRET]); + + let mimikyu = game.scene.getPlayerPokemon(); + const maxHp = mimikyu.getMaxHp(); + const disguiseDamage = Math.floor(maxHp / 8); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(mimikyu.getTag(BattlerTagType.DISGUISE)).toBeUndefined(); + expect(mimikyu.formIndex).toBe(bustedForm); + expect(mimikyu.hp).equals(maxHp - disguiseDamage); + + await game.toNextTurn(); + game.doSwitchPokemon(1); + + await game.phaseInterceptor.to(TurnEndPhase); + mimikyu = game.scene.getParty()[1]; + + expect(mimikyu.formIndex).toBe(bustedForm); + expect(mimikyu.getTag(BattlerTagType.DISGUISE)).toBeUndefined(); + }, TIMEOUT); + + it("reverts to Disguised on arena reset", async () => { + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(4); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(4); + vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(0); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.DISGUISE); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH); + vi.spyOn(overrides, "STARTER_FORM_OVERRIDES", "get").mockReturnValue({ + [Species.MIMIKYU]: bustedForm, + }); + + await game.startBattle([Species.MIMIKYU]); + + const mimikyu = game.scene.getPlayerPokemon(); + + expect(mimikyu.formIndex).toBe(bustedForm); + expect(mimikyu.getTag(BattlerTagType.DISGUISE)).toBeUndefined(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.ICE_BEAM)); + await game.doKillOpponents(); + await game.phaseInterceptor.to(TurnEndPhase); + game.doSelectModifier(); + await game.phaseInterceptor.to(TurnInitPhase); + + expect(mimikyu.formIndex).toBe(disguisedForm); + expect(mimikyu.getTag(BattlerTagType.DISGUISE)).toBeDefined(); + }, TIMEOUT); + + it("cannot be suppressed", async () => { + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.GASTRO_ACID]); + + await game.startBattle(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.GASTRO_ACID)); + + await game.phaseInterceptor.to(TurnEndPhase); + + const mimikyu = game.scene.getEnemyPokemon(); + + expect(mimikyu.getTag(BattlerTagType.DISGUISE)).toBeDefined(); + expect(mimikyu.formIndex).toBe(disguisedForm); + expect(mimikyu.summonData.abilitySuppressed).toBe(false); + }, TIMEOUT); + + it("cannot be swapped with another ability", async () => { + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SKILL_SWAP]); + + await game.startBattle(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SKILL_SWAP)); + + await game.phaseInterceptor.to(TurnEndPhase); + + const mimikyu = game.scene.getEnemyPokemon(); + + expect(mimikyu.getTag(BattlerTagType.DISGUISE)).toBeDefined(); + expect(mimikyu.formIndex).toBe(disguisedForm); + expect(mimikyu.hasAbility(Abilities.DISGUISE)).toBe(true); + }, TIMEOUT); + + it("cannot be copied", async () => { + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.TRACE); + + await game.startBattle(); + + const mimikyu = game.scene.getEnemyPokemon(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SIMPLE_BEAM)); + + await game.phaseInterceptor.to(TurnInitPhase); - test( - "check if fainted pokemon switched to base form on arena reset", - async () => { - const baseForm = 0, - bustedForm = 1; - vi.spyOn(Overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(4); - vi.spyOn(Overrides, "STARTER_FORM_OVERRIDES", "get").mockReturnValue({ - [Species.MIMIKYU]: bustedForm, - }); - - await game.startBattle([Species.MAGIKARP, Species.MIMIKYU]); - - const mimikyu = game.scene.getParty().find((p) => p.species.speciesId === Species.MIMIKYU); - expect(mimikyu).not.toBe(undefined); - expect(mimikyu.formIndex).toBe(bustedForm); - - mimikyu.hp = 0; - mimikyu.status = new Status(StatusEffect.FAINT); - expect(mimikyu.isFainted()).toBe(true); - - game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); - await game.doKillOpponents(); - await game.phaseInterceptor.to(TurnEndPhase); - game.doSelectModifier(); - await game.phaseInterceptor.to(QuietFormChangePhase); - - expect(mimikyu.formIndex).toBe(baseForm); - }, - TIMEOUT - ); - - test( - "damage taken should be equal to 1/8 of its maximum HP, rounded down", - async () => { - const baseForm = 0, - bustedForm = 1; - - vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.DARK_PULSE, Moves.DARK_PULSE, Moves.DARK_PULSE, Moves.DARK_PULSE]); - vi.spyOn(Overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(20); - vi.spyOn(Overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(20); - vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP); - vi.spyOn(Overrides, "STARTER_FORM_OVERRIDES", "get").mockReturnValue({ - [Species.MIMIKYU]: baseForm, - }); - - await game.startBattle([Species.MIMIKYU]); - - const mimikyu = game.scene.getPlayerPokemon(); - const damage = (Math.floor(mimikyu.getMaxHp()/8)); - - expect(mimikyu).not.toBe(undefined); - expect(mimikyu.formIndex).toBe(baseForm); - - game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); - await game.phaseInterceptor.to(TurnEndPhase); - - expect(mimikyu.formIndex).toBe(bustedForm); - expect(game.scene.getEnemyPokemon().turnData.currDamageDealt).toBe(damage); - }, - TIMEOUT - ); + expect(mimikyu.getTag(BattlerTagType.DISGUISE)).toBeDefined(); + expect(mimikyu.formIndex).toBe(disguisedForm); + expect(game.scene.getPlayerPokemon().hasAbility(Abilities.TRACE)).toBe(true); + }, TIMEOUT); });