Files
Crystal/tests/movement-fixture.ts
T
Sébastien QUEROLandClaude Opus 5.5 397dcef44c Port the fishing rods and wild Magikarp
The Old, Good and Super Rods run FishFunction facing water: the map's fish
group and Fish pick no fish, no bite, or a Pokémon from FishGroups and the
time of day's TimeFishGroups. The player casts and jiggles with the
fishing frames drawn from LoadFishingGFX's tiles, shows "!" on a bite, and
the hooked Pokémon attacks as BATTLETYPE_FISH. Facing pieces now use
signed offsets, which the rod above and left of the player needs.

Wild Magikarp no longer stop a battle: LoadEnemyMon redraws their DVs
through CalcMagikarpLength and its filters, as the original does. The
length routine matches the original in the reference emulator.

Part of #27

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:49:35 +02:00

93 lines
2.8 KiB
TypeScript

import { emptyDecorations } from "../src/game/world/decorations";
import { CrystalAssets } from "../src/game/rom/assets";
import { GameWorld } from "../src/game/world/world";
import type { GameMap } from "../src/game/world/types";
/** Synthetic maps and script bytes: these tests run without a private ROM. */
export class MovementAssets extends CrystalAssets {
readonly fixtures = new Map<string, GameMap>();
readonly tiles = new Map<string, number>();
readonly bytes: Uint8Array;
constructor() {
const bytes = new Uint8Array(0xa0000);
bytes[0] = 0x50; // Empty weekday name for synthetic script hosts.
super(bytes);
this.bytes = bytes;
for (const id of ["24:7", "24:6"]) {
this.fixtures.set(id, {
id,
group: 24,
number: Number(id.split(":")[1]),
name: id,
location: id,
landmark: 1,
width: 8,
height: 8,
border: 0,
tileset: 1,
environment: 1,
paletteMode: 1,
phoneService: 0,
fishGroup: 0,
blocks: new Uint8Array(16),
warps: [],
backgroundEvents: [],
objects: [],
connections: [],
sceneScripts: [],
callbacks: [],
coordinateEvents: [],
});
}
}
override map(id: string) {
return this.fixtures.get(id)!;
}
override isPorted(id: string) {
return this.fixtures.has(id);
}
override sceneOwner() {
return undefined;
}
override collision(map: GameMap, x: number, y: number) {
if (x < 0 || y < 0 || x >= map.width || y >= map.height) return 7;
return this.tiles.get(`${map.id}:${x},${y}`) ?? 0;
}
/** CollisionPermissionTable in brief: walls, then the water rows
* ($2x, $3x, buoys $cx) apart from ice and the wall-like buoy tiles. */
override permission(collision: number) {
if (collision === 7 || collision === 0x27 || collision === 0x2f) return 15;
if (collision === 0x23 || collision === 0x2b) return 0;
return [0x20, 0x30, 0xc0].includes(collision & 0xf0) ? 1 : 0;
}
override grassCollisions() {
return [];
}
override sprite(id: number) {
return id ? { graphics: new Uint8Array(0), palette: 0, walking: false } : undefined;
}
tile(x: number, y: number, collision: number, map = "24:7") {
this.tiles.set(`${map}:${x},${y}`, collision);
}
}
export function fixture() {
const assets = new MovementAssets();
const world = new GameWorld(
assets,
() => new Date("2026-09-22T12:00:00"),
() => 0,
);
world.state.decorations = emptyDecorations();
return { assets, world };
}
export function connectWarp(assets: MovementAssets, x = 3, y = 4) {
assets.map("24:7").warps.push({ x, y, targetMap: "24:6", targetWarp: 1 });
assets
.map("24:6")
.warps.push({ x: 2, y: 2, targetMap: "24:7", targetWarp: 1 });
assets.tile(2, 2, 0x70, "24:6");
}