Files
Crystal/tests/rendering.test.ts
T
Sébastien QUEROLandClaude Opus 5.5 451464f7c6
Checks / web (push) Canceled after 0s
Complete the overworld foundation
Port forced movement (turn delays, bumps, ice, currents, pits), dynamic
return warps, elevators, Surf, Cut, Strength and cycling rules, working
map blocks with changeblock/changemapblocks, and the remaining NPC
movement types and movement commands.

Add tileset animation from the ROM's command tables, grass sprite
priority and special map palettes; room decorations with ownership, the
bedroom PC and save migration; and Key Items access to the Bicycle.

Fourteen original-engine traces compare positions, facing, actors, event
flags and scripted poses. Tile animation matches original VRAM for four
tilesets, and LoadMapPals matches 192 palette cases. skyfall_top now
keeps the $60 sprite offset its step function leaves behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 16:47:29 +02:00

88 lines
3.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { existsSync, readFileSync } from "node:fs";
import { drawTile } from "../src/game/rendering/pixels";
import { TilesetAnimator } from "../src/game/rendering/tileset-animation";
import { CrystalAssets } from "../src/game/rom/assets";
import { OW } from "../src/game/world/constants";
import type { Palette } from "../src/game/world/types";
it("grass priority uses background indices, preserving transparent sprite pixels", () => {
const palette: Palette = [
[255, 255, 255],
[80, 160, 80],
[32, 64, 32],
[0, 0, 0],
];
const target = new Uint8ClampedArray(8 * 8 * 4);
const indices = new Uint8Array(8 * 8);
const background = new Uint8Array(16);
background[0] = 0x80; // Colored grass at x=0; color zero elsewhere.
drawTile(target, 8, 8, background, 0, 0, 0, palette, { indices });
const sprite = new Uint8Array(16);
sprite[1] = 0xc0; // Sprite color 2 at x=0 and x=1, transparent at x=2.
drawTile(target, 8, 8, sprite, 0, 0, 0, palette, {
indices,
behind: true,
transparent: true,
});
expect([...target.slice(0, 12)]).toEqual([
80, 160, 80, 255, 32, 64, 32, 255, 255, 255, 255, 255,
]);
expect([...indices.slice(0, 3)]).toEqual([1, 0, 0]);
drawTile(target, 8, 8, sprite, 0, 0, 0, palette, {
indices,
transparent: true,
});
expect([...target.slice(0, 4)]).toEqual([32, 64, 32, 255]);
});
const romPath =
process.env.CRYSTAL_ROM_PATH ??
"Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc";
describe.skipIf(!existsSync(romPath))(
"overworld rendering with the local ROM",
() => {
it("runs every tileset animation without changing the cached ROM graphics", () => {
const assets = new CrystalAssets(new Uint8Array(readFileSync(romPath)));
const base = assets.map("24:4");
for (let tileset = 1; tileset <= OW.NUM_TILESETS; tileset++) {
const map = { ...base, tileset };
const graphics = assets.tileset(map).graphics;
const original = graphics.slice();
const animator = new TilesetAnimator(assets, map, graphics);
animator.advance(1024, false);
animator.advance(1024, true);
expect(graphics, `tileset ${tileset}`).toEqual(original);
expect(animator.graphics.length).toBe(original.length);
}
});
it("keeps special indoor palettes fixed and uses normal palettes for the Hall of Fame", () => {
const assets = new CrystalAssets(new Uint8Array(readFileSync(romPath)));
const base = assets.map("24:6");
for (const tileset of [
OW.TILESET_POKECOM_CENTER,
OW.TILESET_BATTLE_TOWER_INSIDE,
OW.TILESET_HOUSE,
OW.TILESET_RADIO_TOWER,
OW.TILESET_MANSION,
]) {
const map = { ...base, tileset };
expect(assets.palettes(map, 0)).toEqual(assets.palettes(map, 2));
expect(assets.palettes(map, 0)).toHaveLength(8);
}
const ice = {
...base,
tileset: OW.TILESET_ICE_PATH,
environment: OW.CAVE,
};
expect(assets.palettes(ice, 0)).toEqual(assets.palettes(ice, 2));
const hall = { ...ice, environment: OW.INDOOR };
expect(assets.palettes(hall, 1)).not.toEqual(assets.palettes(ice, 1));
expect(assets.palettes(hall, 1)).toEqual(
assets.palettes({ ...hall, tileset: OW.TILESET_POKECENTER }, 1),
);
});
},
);