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,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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user