Files
Crystal/tests/blackthorn.test.ts
T
kawaandClaude Opus 5.5 9c3579937c Open the Radio Tower takeover, Ice Path, Blackthorn and Dragon's Den (#32)
The Rocket-held Radio Tower, the Underground's switch room and warehouse,
Ice Path, Blackthorn City and its gym, Routes 45 and 46, Dark Cave's
Blackthorn entrance, Dragon's Den and the Dragon Shrine are open. The story
plays through the Basement Key, the switches and the director, the Clear
Bell, Ice Path's falling boulder, Clair's gym and the shrine's test for the
RISINGBADGE, TM24 and the elder's Dratini.

New specials: GiveDratini and MoveDeletion. A save keeps an object that a
script made disappear (or appear against its flag) as Continue does, so the
Radio Tower director no longer blocks the 5F stairs after a reload. A
whiteout from a battle started in an scall no longer refuses.

The playthrough driver pushes boulders into their holes (planned breadth
first), clears whirlpools, chooses the move a TM replaces and heals with
the strongest potion; the run gains six stages to the Rising Badge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:19:02 +02:00

149 lines
5.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { existsSync, readFileSync } from "node:fs";
import { CrystalAssets } from "../src/game/rom/assets";
import { GameWorld } from "../src/game/world/world";
import { generatePartyMon } from "../src/game/party/pokemon";
import { settle } from "./world-driver";
const romPath =
process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc";
describe.skipIf(!existsSync(romPath))("Blackthorn City with the local ROM", () => {
const bytes = existsSync(romPath) ? new Uint8Array(readFileSync(romPath)) : new Uint8Array();
const MOVE_DELETERS_HOUSE = "5:7",
DRAGON_SHRINE = "3:82";
const TYPHLOSION = 157,
DRATINI = 147,
SCENE_DRAGONSHRINE_NOOP = 1;
const WRAP = 35,
LEER = 43,
THUNDER_WAVE = 86,
TWISTER = 239,
EXTREMESPEED = 245;
const EVENT_GOT_DRATINI = 0xbd,
EVENT_ANSWERED_DRAGON_MASTER_QUIZ_WRONG = 0xc1;
const world = () => {
let seed = 13;
return new GameWorld(
new CrystalAssets(bytes),
() => new Date("2026-09-22T12:00:00"),
() => {
seed = (seed * 1103515245 + 12345) >>> 0;
return (seed >>> 16) & 255;
},
);
};
const mon = (game: GameWorld, species: number, level = 30) =>
generatePartyMon(game.assets.pokemon, {
species,
level,
item: 0,
otId: game.playerId,
ot: "KAWA",
dvs: 0x8888,
caughtData: 0,
});
/** Enter `map`, stand at (x, y) facing `face`, press A and answer. */
const talk = (
game: GameWorld,
map: string,
x: number,
y: number,
face: "up" | "down" | "left" | "right",
answers: (number | string)[] = [],
) => {
game.warpTo(map, x, y);
settle(game);
game.restore({ ...game.save(), x, y, direction: face });
game.interact();
const shown = settle(game, answers).join(" ");
expect(game.notice, game.diagnostic).toBe("");
return shown;
};
/** The Move Deleter stands at (2, 3). */
const deleter = (game: GameWorld, answers: number[]) =>
talk(game, MOVE_DELETERS_HOUSE, 2, 4, "up", answers);
describe("the Move Deleter (MoveDeletion)", () => {
it("deletes the chosen move and moves the later moves and PP up", () => {
const game = world();
const typhlosion = mon(game, TYPHLOSION);
typhlosion.moves = [33, 43, 108, 52];
typhlosion.pp = [35, 30 | 0xc0, 20, 25 | 0x40];
game.state.party = [typhlosion];
// YES, the first Pokémon, its second move (LEER), YES.
const shown = deleter(game, [1, 0, 1, 1]);
expect(shown).toContain("the MOVE DELETER.");
expect(shown).toContain("Oh, make it forget\nLEER?");
expect(shown).toContain("forgot the move.");
// The PP Up bits move with each move's PP.
expect(game.state.party[0].moves).toEqual([33, 108, 52, 0]);
expect(game.state.party[0].pp).toEqual([35, 20, 25 | 0x40, 0]);
});
it("refuses a Pokémon with one move and an Egg, and says goodbye to every no", () => {
const game = world();
const one = mon(game, TYPHLOSION);
one.moves = [33, 0, 0, 0];
one.pp = [35, 0, 0, 0];
const egg = { ...mon(game, TYPHLOSION, 5), egg: true };
game.state.party = [one, egg, mon(game, TYPHLOSION)];
expect(deleter(game, [1, 0])).toContain("knows\nonly one move.");
expect(deleter(game, [1, 1])).toContain("An EGG doesn't");
const before = structuredClone(game.state.party[2]);
expect(deleter(game, [0])).toContain("Come visit me");
expect(deleter(game, [1, -1])).toContain("Come visit me");
expect(deleter(game, [1, 2, -1])).toContain("Come visit me");
expect(deleter(game, [1, 2, 0, 0])).toContain("Come visit me");
expect(game.state.party[2]).toEqual(before);
});
});
describe("the Dragon Shrine elder's Dratini (GiveDratini)", () => {
/** After the test (the shrine's scene done), talk to the elder. */
const elder = (game: GameWorld) => {
game.state.scenes[DRAGON_SHRINE] = SCENE_DRAGONSHRINE_NOOP;
return talk(game, DRAGON_SHRINE, 5, 2, "up", [0]);
};
it("gives a Dratini with Extremespeed after a test without mistakes", () => {
const game = world();
game.state.party = [mon(game, TYPHLOSION)];
const shown = elder(game);
expect(shown).toContain("Take this DRATINI");
expect(game.state.events.has(EVENT_GOT_DRATINI)).toBe(true);
const dratini = game.state.party[1];
expect(dratini).toMatchObject({ species: DRATINI, level: 15 });
expect(dratini.moves).toEqual([WRAP, THUNDER_WAVE, TWISTER, EXTREMESPEED]);
// Each move's base PP, as .GiveMoves reads it from Moves.
expect(dratini.pp).toEqual([20, 20, 20, 5]);
// Re-entering clears EVENT_TEMPORARY_UNTIL_MAP_RELOAD_7.
expect(elder(game)).toContain("CLAIR appears to");
});
it("gives the normal moves after a mistake, to the last Dratini only", () => {
const game = world();
game.state.events.add(EVENT_ANSWERED_DRAGON_MASTER_QUIZ_WRONG);
const earlier = mon(game, DRATINI, 20);
game.state.party = [earlier, mon(game, TYPHLOSION)];
const before = structuredClone(earlier);
elder(game);
expect(game.state.party[2].moves).toEqual([WRAP, LEER, THUNDER_WAVE, TWISTER]);
expect(game.state.party[2].pp).toEqual([20, 30, 20, 20]);
expect(game.state.party[0]).toEqual(before);
});
it("keeps the Dratini for later when the party is full", () => {
const game = world();
game.state.party = Array.from({ length: 6 }, () => mon(game, TYPHLOSION));
expect(elder(game)).toContain("party is full.");
expect(game.state.events.has(EVENT_GOT_DRATINI)).toBe(false);
game.state.party.pop();
elder(game);
expect(game.state.party[5].species).toBe(DRATINI);
});
});
});