[Feature] Fully implement Octolock (#2985)

* implement octolock

* Add tests
This commit is contained in:
mcmontag 2024-07-12 22:19:53 -04:00 committed by GitHub
parent 985c24e7bd
commit 0aa5e0d49d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 170 additions and 2 deletions

View File

@ -634,6 +634,32 @@ export class IngrainTag extends TrappedTag {
}
}
/**
* Octolock traps the target pokemon and reduces its DEF and SPDEF by one stage at the
* end of each turn.
*/
export class OctolockTag extends TrappedTag {
constructor(sourceId: number) {
super(BattlerTagType.OCTOLOCK, BattlerTagLapseType.TURN_END, 1, Moves.OCTOLOCK, sourceId);
}
canAdd(pokemon: Pokemon): boolean {
const isOctolocked = pokemon.getTag(BattlerTagType.OCTOLOCK);
return !isOctolocked;
}
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
const shouldLapse = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType);
if (shouldLapse) {
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.DEF, BattleStat.SPDEF], -1));
return true;
}
return false;
}
}
export class AquaRingTag extends BattlerTag {
constructor() {
super(BattlerTagType.AQUA_RING, BattlerTagLapseType.TURN_END, 1, Moves.AQUA_RING, undefined);
@ -1662,6 +1688,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: integer, sourc
return new DestinyBondTag(sourceMove, sourceId);
case BattlerTagType.ICE_FACE:
return new IceFaceTag(sourceMove);
case BattlerTagType.OCTOLOCK:
return new OctolockTag(sourceId);
case BattlerTagType.NONE:
default:
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);

View File

@ -7796,8 +7796,7 @@ export function initMoves() {
.attr(EatBerryAttr)
.target(MoveTarget.ALL),
new StatusMove(Moves.OCTOLOCK, Type.FIGHTING, 100, 15, -1, 0, 8)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1)
.partial(),
.attr(AddBattlerTagAttr, BattlerTagType.OCTOLOCK, false, true, 1),
new AttackMove(Moves.BOLT_BEAK, Type.ELECTRIC, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 8)
.attr(FirstAttackDoublePowerAttr),
new AttackMove(Moves.FISHIOUS_REND, Type.WATER, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 8)

View File

@ -13,6 +13,7 @@ export enum BattlerTagType {
ENCORE = "ENCORE",
HELPING_HAND = "HELPING_HAND",
INGRAIN = "INGRAIN",
OCTOLOCK = "OCTOLOCK",
AQUA_RING = "AQUA_RING",
DROWSY = "DROWSY",
TRAPPED = "TRAPPED",

View File

@ -0,0 +1,62 @@
import { describe, expect, it, vi } from "vitest";
import Pokemon from "#app/field/pokemon.js";
import BattleScene from "#app/battle-scene.js";
import { BattlerTag, BattlerTagLapseType, OctolockTag, TrappedTag } from "#app/data/battler-tags.js";
import { StatChangePhase } from "#app/phases.js";
import { BattleStat } from "#app/data/battle-stat.js";
import { BattlerTagType } from "#app/enums/battler-tag-type.js";
jest.mock("#app/battle-scene.js");
describe("BattlerTag - OctolockTag", () => {
describe("lapse behavior", () => {
it("unshifts a StatChangePhase with expected stat changes", { timeout: 10000 }, async () => {
const mockPokemon = {
scene: new BattleScene(),
getBattlerIndex: () => 0,
} as Pokemon;
const subject = new OctolockTag(1);
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => {
expect(phase).toBeInstanceOf(StatChangePhase);
expect((phase as StatChangePhase)["levels"]).toEqual(-1);
expect((phase as StatChangePhase)["stats"]).toEqual([BattleStat.DEF, BattleStat.SPDEF]);
});
subject.lapse(mockPokemon, BattlerTagLapseType.TURN_END);
expect(mockPokemon.scene.unshiftPhase).toBeCalledTimes(1);
});
});
it ("traps its target (extends TrappedTag)", { timeout: 2000 }, async () => {
expect(new OctolockTag(1)).toBeInstanceOf(TrappedTag);
});
it("can be added to pokemon who are not octolocked", { timeout: 2000 }, 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", { timeout: 2000 }, 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

@ -0,0 +1,78 @@
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import * as overrides from "#app/overrides";
import { CommandPhase, MoveEndPhase, TurnInitPhase } from "#app/phases";
import {getMovePosition} from "#app/test/utils/gameManagerUtils";
import {BattleStat} from "#app/data/battle-stat";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { TrappedTag } from "#app/data/battler-tags.js";
describe("Moves - Octolock", () => {
describe("integration tests", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA);
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE);
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(2000);
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.OCTOLOCK, Moves.SPLASH]);
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE);
});
it("Reduces DEf and SPDEF by 1 each turn", { timeout: 10000 }, async () => {
await game.startBattle([Species.GRAPPLOCT]);
const enemyPokemon = game.scene.getEnemyField();
// use Octolock and advance to init phase of next turn to check for stat changes
game.doAttack(getMovePosition(game.scene, 0, Moves.OCTOLOCK));
await game.phaseInterceptor.to(TurnInitPhase);
expect(enemyPokemon[0].summonData.battleStats[BattleStat.DEF]).toBe(-1);
expect(enemyPokemon[0].summonData.battleStats[BattleStat.SPDEF]).toBe(-1);
// take a second turn to make sure stat changes occur again
await game.phaseInterceptor.to(CommandPhase);
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
await game.phaseInterceptor.to(TurnInitPhase);
expect(enemyPokemon[0].summonData.battleStats[BattleStat.DEF]).toBe(-2);
expect(enemyPokemon[0].summonData.battleStats[BattleStat.SPDEF]).toBe(-2);
});
it("Traps the target pokemon", { timeout: 10000 }, async () => {
await game.startBattle([Species.GRAPPLOCT]);
const enemyPokemon = game.scene.getEnemyField();
// before Octolock - enemy should not be trapped
expect(enemyPokemon[0].findTag(t => t instanceof TrappedTag)).toBeUndefined();
game.doAttack(getMovePosition(game.scene, 0, Moves.OCTOLOCK));
// after Octolock - enemy should be trapped
await game.phaseInterceptor.to(MoveEndPhase);
expect(enemyPokemon[0].findTag(t => t instanceof TrappedTag)).toBeDefined();
});
});
});