[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>
This commit is contained in:
PrabbyDD 2024-11-30 02:06:09 -08:00 committed by GitHub
parent 4d75d902d8
commit b70bf0f4aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 55 additions and 46 deletions

View File

@ -1085,10 +1085,6 @@ export class OctolockTag extends TrappedTag {
super(BattlerTagType.OCTOLOCK, BattlerTagLapseType.TURN_END, 1, Moves.OCTOLOCK, sourceId); 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 { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
const shouldLapse = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType); const shouldLapse = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType);

View File

@ -7541,6 +7541,8 @@ const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Po
return party.some(pokemon => pokemon.isActive() && !pokemon.isOnField()); 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; export type MoveAttrFilter = (attr: MoveAttr) => boolean;
function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise<void> { function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise<void> {
@ -8287,6 +8289,7 @@ export function initMoves() {
new AttackMove(Moves.THIEF, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 25, -1, 0, 2) new AttackMove(Moves.THIEF, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 25, -1, 0, 2)
.attr(StealHeldItemChanceAttr, 0.3), .attr(StealHeldItemChanceAttr, 0.3),
new StatusMove(Moves.SPIDER_WEB, Type.BUG, -1, 10, -1, 0, 2) new StatusMove(Moves.SPIDER_WEB, Type.BUG, -1, 10, -1, 0, 2)
.condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1),
new StatusMove(Moves.MIND_READER, Type.NORMAL, -1, 5, -1, 0, 2) new StatusMove(Moves.MIND_READER, Type.NORMAL, -1, 5, -1, 0, 2)
.attr(IgnoreAccuracyAttr), .attr(IgnoreAccuracyAttr),
@ -8423,6 +8426,7 @@ export function initMoves() {
new AttackMove(Moves.STEEL_WING, Type.STEEL, MoveCategory.PHYSICAL, 70, 90, 25, 10, 0, 2) new AttackMove(Moves.STEEL_WING, Type.STEEL, MoveCategory.PHYSICAL, 70, 90, 25, 10, 0, 2)
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, true), .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true),
new StatusMove(Moves.MEAN_LOOK, Type.NORMAL, -1, 5, -1, 0, 2) new StatusMove(Moves.MEAN_LOOK, Type.NORMAL, -1, 5, -1, 0, 2)
.condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1),
new StatusMove(Moves.ATTRACT, Type.NORMAL, 100, 15, -1, 0, 2) new StatusMove(Moves.ATTRACT, Type.NORMAL, 100, 15, -1, 0, 2)
.attr(AddBattlerTagAttr, BattlerTagType.INFATUATED) .attr(AddBattlerTagAttr, BattlerTagType.INFATUATED)
@ -8802,6 +8806,7 @@ export function initMoves() {
new SelfStatusMove(Moves.IRON_DEFENSE, Type.STEEL, -1, 15, -1, 0, 3) new SelfStatusMove(Moves.IRON_DEFENSE, Type.STEEL, -1, 15, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true), .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
new StatusMove(Moves.BLOCK, Type.NORMAL, -1, 5, -1, 0, 3) new StatusMove(Moves.BLOCK, Type.NORMAL, -1, 5, -1, 0, 3)
.condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1),
new StatusMove(Moves.HOWL, Type.NORMAL, -1, 40, -1, 0, 3) new StatusMove(Moves.HOWL, Type.NORMAL, -1, 40, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.ATK ], 1) .attr(StatStageChangeAttr, [ Stat.ATK ], 1)
@ -10096,6 +10101,7 @@ export function initMoves() {
.attr(EatBerryAttr) .attr(EatBerryAttr)
.target(MoveTarget.ALL), .target(MoveTarget.ALL),
new StatusMove(Moves.OCTOLOCK, Type.FIGHTING, 100, 15, -1, 0, 8) new StatusMove(Moves.OCTOLOCK, Type.FIGHTING, 100, 15, -1, 0, 8)
.condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.OCTOLOCK, false, true, 1), .attr(AddBattlerTagAttr, BattlerTagType.OCTOLOCK, false, true, 1),
new AttackMove(Moves.BOLT_BEAK, Type.ELECTRIC, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 8) new AttackMove(Moves.BOLT_BEAK, Type.ELECTRIC, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 8)
.attr(FirstAttackDoublePowerAttr), .attr(FirstAttackDoublePowerAttr),

View File

@ -1,9 +1,8 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import Pokemon from "#app/field/pokemon"; 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 { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
vi.mock("#app/battle-scene.js"); vi.mock("#app/battle-scene.js");
@ -33,30 +32,4 @@ describe("BattlerTag - OctolockTag", () => {
it ("traps its target (extends TrappedTag)", async () => { it ("traps its target (extends TrappedTag)", async () => {
expect(new OctolockTag(1)).toBeInstanceOf(TrappedTag); 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);
});
}); });

View File

@ -1,11 +1,8 @@
import { Stat } from "#enums/stat";
import { TrappedTag } from "#app/data/battler-tags"; 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 { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
@ -27,12 +24,13 @@ describe("Moves - Octolock", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override.battleType("single") game.override
.enemySpecies(Species.RATTATA) .battleType("single")
.enemySpecies(Species.MAGIKARP)
.enemyMoveset(Moves.SPLASH) .enemyMoveset(Moves.SPLASH)
.enemyAbility(Abilities.BALL_FETCH) .enemyAbility(Abilities.BALL_FETCH)
.startingLevel(2000) .startingLevel(2000)
.moveset([ Moves.OCTOLOCK, Moves.SPLASH ]) .moveset([ Moves.OCTOLOCK, Moves.SPLASH, Moves.TRICK_OR_TREAT ])
.ability(Abilities.BALL_FETCH); .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 // use Octolock and advance to init phase of next turn to check for stat changes
game.move.select(Moves.OCTOLOCK); game.move.select(Moves.OCTOLOCK);
await game.phaseInterceptor.to(TurnInitPhase); await game.toNextTurn();
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1); expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1);
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1); expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1);
// take a second turn to make sure stat changes occur again // take a second turn to make sure stat changes occur again
await game.phaseInterceptor.to(CommandPhase);
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
await game.toNextTurn();
await game.phaseInterceptor.to(TurnInitPhase);
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-2); expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-2);
expect(enemyPokemon.getStatStage(Stat.SPDEF)).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 // use Octolock and advance to init phase of next turn to check for stat changes
game.move.select(Moves.OCTOLOCK); game.move.select(Moves.OCTOLOCK);
await game.phaseInterceptor.to(TurnInitPhase); await game.toNextTurn();
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1); 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 // use Octolock and advance to init phase of next turn to check for stat changes
game.move.select(Moves.OCTOLOCK); game.move.select(Moves.OCTOLOCK);
await game.phaseInterceptor.to(TurnInitPhase); await game.toNextTurn();
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.SPDEF)).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 // use Octolock and advance to init phase of next turn to check for stat changes
game.move.select(Moves.OCTOLOCK); game.move.select(Moves.OCTOLOCK);
await game.phaseInterceptor.to(TurnInitPhase); await game.toNextTurn();
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0); expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0);
@ -110,7 +107,44 @@ describe("Moves - Octolock", () => {
game.move.select(Moves.OCTOLOCK); game.move.select(Moves.OCTOLOCK);
// after Octolock - enemy should be trapped // after Octolock - enemy should be trapped
await game.phaseInterceptor.to(MoveEndPhase); await game.phaseInterceptor.to("MoveEndPhase");
expect(enemyPokemon.findTag(t => t instanceof TrappedTag)).toBeDefined(); 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);
});
}); });