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:
1 parent
9ac111f262
commit
451464f7c6
38 files changed
+5600
-490
No files matched your search
@@ -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;
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user