From b70bf0f4aaed679bd1ae0f1e42b5a1ed2586a381 Mon Sep 17 00:00:00 2001 From: PrabbyDD <147005742+PrabbyDD@users.noreply.github.com> Date: Sat, 30 Nov 2024 02:06:09 -0800 Subject: [PATCH] [Bug] Fix for Octolock bypasses Ghost Invulnerability to lower Stats (#4923) Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> --- src/data/battler-tags.ts | 4 -- src/data/move.ts | 6 +++ src/test/battlerTags/octolock.test.ts | 29 +------------ src/test/moves/octolock.test.ts | 62 +++++++++++++++++++++------ 4 files changed, 55 insertions(+), 46 deletions(-) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index ce25b56157c..75b7cddb904 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1085,10 +1085,6 @@ export class OctolockTag extends TrappedTag { super(BattlerTagType.OCTOLOCK, BattlerTagLapseType.TURN_END, 1, Moves.OCTOLOCK, sourceId); } - canAdd(pokemon: Pokemon): boolean { - return !pokemon.getTag(BattlerTagType.OCTOLOCK); - } - lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { const shouldLapse = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType); diff --git a/src/data/move.ts b/src/data/move.ts index 64f8e43cb7b..091301990f3 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -7541,6 +7541,8 @@ const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Po return party.some(pokemon => pokemon.isActive() && !pokemon.isOnField()); }; +const failIfGhostTypeCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => !target.isOfType(Type.GHOST); + export type MoveAttrFilter = (attr: MoveAttr) => boolean; function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise { @@ -8287,6 +8289,7 @@ export function initMoves() { new AttackMove(Moves.THIEF, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 25, -1, 0, 2) .attr(StealHeldItemChanceAttr, 0.3), new StatusMove(Moves.SPIDER_WEB, Type.BUG, -1, 10, -1, 0, 2) + .condition(failIfGhostTypeCondition) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), new StatusMove(Moves.MIND_READER, Type.NORMAL, -1, 5, -1, 0, 2) .attr(IgnoreAccuracyAttr), @@ -8423,6 +8426,7 @@ export function initMoves() { new AttackMove(Moves.STEEL_WING, Type.STEEL, MoveCategory.PHYSICAL, 70, 90, 25, 10, 0, 2) .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true), new StatusMove(Moves.MEAN_LOOK, Type.NORMAL, -1, 5, -1, 0, 2) + .condition(failIfGhostTypeCondition) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), new StatusMove(Moves.ATTRACT, Type.NORMAL, 100, 15, -1, 0, 2) .attr(AddBattlerTagAttr, BattlerTagType.INFATUATED) @@ -8802,6 +8806,7 @@ export function initMoves() { new SelfStatusMove(Moves.IRON_DEFENSE, Type.STEEL, -1, 15, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true), new StatusMove(Moves.BLOCK, Type.NORMAL, -1, 5, -1, 0, 3) + .condition(failIfGhostTypeCondition) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), new StatusMove(Moves.HOWL, Type.NORMAL, -1, 40, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.ATK ], 1) @@ -10096,6 +10101,7 @@ export function initMoves() { .attr(EatBerryAttr) .target(MoveTarget.ALL), new StatusMove(Moves.OCTOLOCK, Type.FIGHTING, 100, 15, -1, 0, 8) + .condition(failIfGhostTypeCondition) .attr(AddBattlerTagAttr, BattlerTagType.OCTOLOCK, false, true, 1), new AttackMove(Moves.BOLT_BEAK, Type.ELECTRIC, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 8) .attr(FirstAttackDoublePowerAttr), diff --git a/src/test/battlerTags/octolock.test.ts b/src/test/battlerTags/octolock.test.ts index ebd92dc6401..9efce220fe8 100644 --- a/src/test/battlerTags/octolock.test.ts +++ b/src/test/battlerTags/octolock.test.ts @@ -1,9 +1,8 @@ import BattleScene from "#app/battle-scene"; import { describe, expect, it, vi } from "vitest"; import Pokemon from "#app/field/pokemon"; -import { BattlerTag, BattlerTagLapseType, OctolockTag, TrappedTag } from "#app/data/battler-tags"; +import { BattlerTagLapseType, OctolockTag, TrappedTag } from "#app/data/battler-tags"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; -import { BattlerTagType } from "#app/enums/battler-tag-type"; import { Stat } from "#enums/stat"; vi.mock("#app/battle-scene.js"); @@ -33,30 +32,4 @@ describe("BattlerTag - OctolockTag", () => { it ("traps its target (extends TrappedTag)", async () => { expect(new OctolockTag(1)).toBeInstanceOf(TrappedTag); }); - - it("can be added to pokemon who are not octolocked", async => { - const mockPokemon = { - getTag: vi.fn().mockReturnValue(undefined) as Pokemon["getTag"], - } as Pokemon; - - const subject = new OctolockTag(1); - - expect(subject.canAdd(mockPokemon)).toBeTruthy(); - - expect(mockPokemon.getTag).toHaveBeenCalledTimes(1); - expect(mockPokemon.getTag).toHaveBeenCalledWith(BattlerTagType.OCTOLOCK); - }); - - it("cannot be added to pokemon who are octolocked", async => { - const mockPokemon = { - getTag: vi.fn().mockReturnValue(new BattlerTag(null!, null!, null!, null!)) as Pokemon["getTag"], - } as Pokemon; - - const subject = new OctolockTag(1); - - expect(subject.canAdd(mockPokemon)).toBeFalsy(); - - expect(mockPokemon.getTag).toHaveBeenCalledTimes(1); - expect(mockPokemon.getTag).toHaveBeenCalledWith(BattlerTagType.OCTOLOCK); - }); }); diff --git a/src/test/moves/octolock.test.ts b/src/test/moves/octolock.test.ts index d80b71a51e1..6ca96eeb464 100644 --- a/src/test/moves/octolock.test.ts +++ b/src/test/moves/octolock.test.ts @@ -1,11 +1,8 @@ -import { Stat } from "#enums/stat"; import { TrappedTag } from "#app/data/battler-tags"; -import { CommandPhase } from "#app/phases/command-phase"; -import { MoveEndPhase } from "#app/phases/move-end-phase"; -import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -27,12 +24,13 @@ describe("Moves - Octolock", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleType("single") - .enemySpecies(Species.RATTATA) + game.override + .battleType("single") + .enemySpecies(Species.MAGIKARP) .enemyMoveset(Moves.SPLASH) .enemyAbility(Abilities.BALL_FETCH) .startingLevel(2000) - .moveset([ Moves.OCTOLOCK, Moves.SPLASH ]) + .moveset([ Moves.OCTOLOCK, Moves.SPLASH, Moves.TRICK_OR_TREAT ]) .ability(Abilities.BALL_FETCH); }); @@ -43,16 +41,15 @@ describe("Moves - Octolock", () => { // use Octolock and advance to init phase of next turn to check for stat changes game.move.select(Moves.OCTOLOCK); - await game.phaseInterceptor.to(TurnInitPhase); + await game.toNextTurn(); expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1); expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1); // take a second turn to make sure stat changes occur again - await game.phaseInterceptor.to(CommandPhase); game.move.select(Moves.SPLASH); + await game.toNextTurn(); - await game.phaseInterceptor.to(TurnInitPhase); expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-2); expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-2); }); @@ -65,7 +62,7 @@ describe("Moves - Octolock", () => { // use Octolock and advance to init phase of next turn to check for stat changes game.move.select(Moves.OCTOLOCK); - await game.phaseInterceptor.to(TurnInitPhase); + await game.toNextTurn(); expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1); @@ -79,7 +76,7 @@ describe("Moves - Octolock", () => { // use Octolock and advance to init phase of next turn to check for stat changes game.move.select(Moves.OCTOLOCK); - await game.phaseInterceptor.to(TurnInitPhase); + await game.toNextTurn(); expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0); @@ -93,7 +90,7 @@ describe("Moves - Octolock", () => { // use Octolock and advance to init phase of next turn to check for stat changes game.move.select(Moves.OCTOLOCK); - await game.phaseInterceptor.to(TurnInitPhase); + await game.toNextTurn(); expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0); @@ -110,7 +107,44 @@ describe("Moves - Octolock", () => { game.move.select(Moves.OCTOLOCK); // after Octolock - enemy should be trapped - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(enemyPokemon.findTag(t => t instanceof TrappedTag)).toBeDefined(); }); + + it("does not work on ghost type pokemon", async () => { + game.override.enemyMoveset(Moves.OCTOLOCK); + await game.classicMode.startBattle([ Species.GASTLY ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + // before Octolock - player should not be trapped + expect(playerPokemon.findTag(t => t instanceof TrappedTag)).toBeUndefined(); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + // after Octolock - player should still not be trapped, and no stat loss + expect(playerPokemon.findTag(t => t instanceof TrappedTag)).toBeUndefined(); + expect(playerPokemon.getStatStage(Stat.DEF)).toBe(0); + expect(playerPokemon.getStatStage(Stat.SPDEF)).toBe(0); + }); + + it("does not work on pokemon with added ghost type via Trick-or-Treat", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const enemy = game.scene.getEnemyPokemon()!; + + // before Octolock - pokemon should not be trapped + expect(enemy.findTag(t => t instanceof TrappedTag)).toBeUndefined(); + + game.move.select(Moves.TRICK_OR_TREAT); + await game.toNextTurn(); + game.move.select(Moves.OCTOLOCK); + await game.toNextTurn(); + + // after Octolock - pokemon should still not be trapped, and no stat loss + expect(enemy.findTag(t => t instanceof TrappedTag)).toBeUndefined(); + expect(enemy.getStatStage(Stat.DEF)).toBe(0); + expect(enemy.getStatStage(Stat.SPDEF)).toBe(0); + }); });