Complete the overworld foundation
Checks / web (push) Canceled after 0s

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>
This commit is contained in:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-09-25 16:47:29 +02:00
1 parent 9ac111f262
commit 451464f7c6
38 files changed
+5600 -490

No files matched your search

+111
View File
@@ -0,0 +1,111 @@
/** Overworld constants keyed by their PRET names, so that
* `npm run check:reference` verifies every value against the pinned
* disassembly (constants/*.asm). */
export const OW = {
// Collision types (constants/collision_constants.asm).
COLL_CUT_TREE: 0x12,
COLL_CUT_TREE_1A: 0x1a,
COLL_TALL_GRASS_10: 0x10,
COLL_TALL_GRASS: 0x18,
COLL_LONG_GRASS: 0x14,
COLL_LONG_GRASS_1C: 0x1c,
COLL_HEADBUTT_TREE: 0x15,
COLL_HEADBUTT_TREE_1D: 0x1d,
COLL_ICE: 0x23,
COLL_ICE_2B: 0x2b,
COLL_WHIRLPOOL: 0x24,
COLL_WHIRLPOOL_2C: 0x2c,
COLL_WATERFALL: 0x33,
COLL_CURRENT_DOWN: 0x3b,
COLL_PIT: 0x60,
COLL_PIT_68: 0x68,
COLL_WARP_CARPET_DOWN: 0x70,
COLL_DOOR: 0x71,
COLL_WARP_CARPET_LEFT: 0x76,
COLL_WARP_CARPET_UP: 0x78,
COLL_DOOR_79: 0x79,
COLL_STAIRCASE: 0x7a,
COLL_CAVE: 0x7b,
COLL_WARP_CARPET_RIGHT: 0x7e,
COLL_COUNTER: 0x90,
HI_NYBBLE_CURRENT: 0x30,
HI_NYBBLE_WALK: 0x40,
HI_NYBBLE_WALK_ALT: 0x50,
HI_NYBBLE_WARPS: 0x70,
HI_NYBBLE_LEDGES: 0xa0,
HI_NYBBLE_SIDE_WALLS: 0xb0,
LAND_TILE: 0x00,
WATER_TILE: 0x01,
// wPlayerState (constants/ram_constants.asm).
PLAYER_NORMAL: 0,
PLAYER_BIKE: 1,
PLAYER_SKATE: 2,
PLAYER_SURF: 4,
PLAYER_SURF_PIKA: 8,
// Engine flags backed by wBikeFlags (constants/engine_flags.asm).
ENGINE_FOREST_IS_RESTLESS: 100,
ENGINE_STRENGTH_ACTIVE: 24,
ENGINE_ALWAYS_ON_BIKE: 25,
ENGINE_DOWNHILL: 26,
ENGINE_ZEPHYRBADGE: 27,
ENGINE_HIVEBADGE: 28,
ENGINE_PLAINBADGE: 29,
ENGINE_FOGBADGE: 30,
ENGINE_GLACIERBADGE: 33,
ENGINE_RISINGBADGE: 34,
// Map environments (constants/map_data_constants.asm).
TOWN: 1,
ROUTE: 2,
INDOOR: 3,
CAVE: 4,
ENVIRONMENT_5: 5,
GATE: 6,
DUNGEON: 7,
// Tilesets (constants/tileset_constants.asm).
NUM_TILESETS: 36,
TILESET_JOHTO: 1,
TILESET_JOHTO_MODERN: 2,
TILESET_KANTO: 3,
TILESET_HOUSE: 5,
TILESET_POKECENTER: 7,
TILESET_PORT: 9,
TILESET_FOREST: 31,
TILESET_PARK: 25,
TILESET_POKECOM_CENTER: 21,
TILESET_BATTLE_TOWER_INSIDE: 22,
TILESET_RADIO_TOWER: 27,
TILESET_MANSION: 13,
TILESET_ICE_PATH: 29,
TILESET_ELITE_FOUR_ROOM: 15,
TILESET_TOWER: 23,
TILESET_CAVE: 24,
TILESET_DARK_CAVE: 30,
// Map palettes (constants/map_data_constants.asm).
PALETTE_DARK: 4,
// Regions (constants/landmark_constants.asm).
LANDMARK_SPECIAL: 0,
LANDMARK_FAST_SHIP: 95,
KANTO_LANDMARK: 47,
COLL_WATER: 0x29,
// Sprite ranges (constants/sprite_constants.asm).
NUM_OVERWORLD_SPRITES: 102,
SPRITE_POKEMON: 0x80,
NUM_POKEMON_SPRITES: 35,
SPRITE_VARS: 0xf0,
// Moves used as field moves (constants/move_constants.asm).
CUT: 0x0f,
SURF: 0x39,
STRENGTH: 0x46,
WHIRLPOOL: 250,
WATERFALL: 127,
HEADBUTT: 29,
BICYCLE: 7,
PIKACHU: 25,
EGG: 0xfd,
} as const;
/** Directions in the original's order (DOWN, UP, LEFT, RIGHT). */
export const DOWN = 0,
UP = 1,
LEFT = 2,
RIGHT = 3;
+100
View File
@@ -0,0 +1,100 @@
import { RomReader } from "../rom/reader";
import { OVERWORLD } from "../rom/profile";
import { PokemonData } from "../rom/pokemon";
import { defaultTextContext, readString } from "../scripts/text";
export const DECORATION_SLOTS = [
"bed",
"carpet",
"plant",
"poster",
"console",
"bigDoll",
"left",
"right",
] as const;
export type DecorationSlot = (typeof DECORATION_SLOTS)[number];
export type Decorations = Record<DecorationSlot, number>;
export const emptyDecorations = (): Decorations => ({
bed: 0,
carpet: 0,
plant: 0,
poster: 0,
console: 0,
bigDoll: 0,
left: 0,
right: 0,
});
export const initialDecorations = (): Decorations => ({
...emptyDecorations(),
bed: 2,
poster: 16,
});
/** DecorationAttributes and GetDecoName, read from the user's ROM. */
export class DecorationData {
constructor(
private reader: RomReader,
private pokemon: PokemonData,
) {}
attribute(id: number) {
if (!Number.isInteger(id) || id < 0 || id > 52)
throw new Error("Invalid decoration.");
const p = OVERWORLD.DecorationAttributes + id * 6,
r = this.reader;
return {
type: r.byte(p),
name: r.byte(p + 1),
action: r.byte(p + 2),
event: r.word(p + 3),
sprite: r.byte(p + 5),
};
}
private word(index: number) {
let p = OVERWORLD.DecorationNames;
for (let i = 0; i < index; i++) {
while (this.reader.byte(p++) !== 0x50) {}
}
return readString(this.reader, p, defaultTextContext("PLAYER"));
}
name(id: number) {
const a = this.attribute(id);
if (a.type === 1) return this.word(a.name);
if (a.type === 2 || a.type === 3)
return this.word(a.name) + this.word(a.type === 2 ? 13 : 14);
const mon = this.pokemon.speciesName(a.name);
return a.type === 6
? this.word(17) + mon
: mon + this.word(a.type === 4 ? 15 : 16);
}
owned(events: Set<number>, action: number) {
return Array.from({ length: 52 }, (_, i) => i + 1).filter((id) => {
const a = this.attribute(id);
return a.action === action && events.has(a.event);
});
}
valid(value: unknown, events: Set<number>): value is Decorations {
if (
!value ||
typeof value !== "object" ||
Array.isArray(value) ||
Object.keys(value).length !== 8
)
return false;
const slots = value as Decorations;
return (
DECORATION_SLOTS.every((slot, i) => {
const id = slots[slot];
return (
Number.isInteger(id) &&
id >= 0 &&
id <= 52 &&
(id === 0 ||
(this.attribute(id).action === (i < 6 ? i * 2 + 1 : 13) &&
events.has(this.attribute(id).event)))
);
}) &&
(!slots.left || slots.left !== slots.right)
);
}
}
+1546 -251
View File
File diff suppressed because it is too large. Load diff