import { describe, expect, it } from "vitest"; import { existsSync, readFileSync } from "node:fs"; import { CrystalAssets } from "../src/game/rom/assets"; import { JOHTO, MENU } from "../src/game/rom/profile"; import { revisionAddresses, romRevision } from "../src/game/rom/revision"; import { RELEASES_IN_PROGRESS, validateRom } from "../src/rom/validate"; import { readString } from "../src/game/scripts/text"; import { textTiles } from "../src/game/rom/font"; import { Playthrough } from "./playthrough-driver"; /** The European releases at the repository root, each skipped when absent. */ const releases = [ { language: "fr", rom: "Pokemon - Version Cristal (France).gbc", release: RELEASES_IN_PROGRESS[0] }, { language: "de", rom: "Pokemon - Kristall-Edition (Germany).gbc", release: RELEASES_IN_PROGRESS[1] }, { language: "it", rom: "Pokemon - Versione Cristallo (Italy).gbc", release: RELEASES_IN_PROGRESS[2] }, { language: "es", rom: "Pokemon - Edicion Cristal (Spain).gbc", release: RELEASES_IN_PROGRESS[3] }, ] as const; const english = "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc"; for (const { language, rom, release } of releases) describe.skipIf(!existsSync(rom))(`the ${language} release`, () => { const bytes = existsSync(rom) ? new Uint8Array(readFileSync(rom)) : new Uint8Array(); it("is recognized from its header, then refused until its logic is ported", async () => { const assets = new CrystalAssets(bytes); expect(romRevision(assets.reader)).toBe(language); await expect(validateRom(new File([bytes], "crystal.gbc"))).rejects.toThrow( `This is the ${release.name.replace("Pokémon Crystal · ", "")} release`, ); }); it("points the profile at its addresses and back to English for another cartridge", () => { const overrides = revisionAddresses(language).JOHTO; const [key, value] = Object.entries(overrides).find(([, address]) => address > 0)!; new CrystalAssets(bytes); expect((JOHTO as Record)[key]).toBe(value); if (existsSync(english)) { new CrystalAssets(new Uint8Array(readFileSync(english))); expect((JOHTO as Record)[key]).not.toBe(value); new CrystalAssets(bytes); } }); it("decodes its location names and menu strings, and writes them back as tiles", () => { const assets = new CrystalAssets(bytes); const context = { player: "", rival: "", mom: "", strings: {}, weekday: "" }; for (let landmark = 1; landmark < 0x60; landmark++) { const name = assets.landmark(landmark); expect(name.length).toBeGreaterThan(0); // One tile per character, except the release's own word breaks. expect(textTiles(name.replace(/\n/g, " ")).length).toBeGreaterThan(0); } expect(readString(assets.reader, MENU["PlacePartyMonTMHMCompatibility.string_able"], context).length).toBeGreaterThan(0); }); it("plays the introduction and Mom's scene in its language, then steps outside", () => { const p = new Playthrough(new CrystalAssets(bytes)); p.start(); p.world.beginIntro(); p.play([0, "", 1, "", 1, 0, "KAWA"]); expect(p.world.intro).toBe(false); expect(p.world.name).toBe("KAWA"); // The release's own words: none of the English introduction's. const log = p.log.join(" "); expect(log.length).toBeGreaterThan(200); expect(log).not.toContain("Welcome to the world of POKéMON!"); p.reload(); p.walk(7, 0); expect(p.world.mapId).toBe("24:6"); p.walk(9, 3); expect(p.world.move("down")).toBe(true); p.play([1, 1, 1, 1, 1]); p.reload(); p.walk(6, 7); expect(p.world.move("down")).toBe(true); p.play(); expect(p.world.mapId).toBe("24:4"); expect(p.world.notice).toBe(""); }); });