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
+87
-15
@@ -6,11 +6,13 @@ import {
|
||||
NATIVE_MAPS,
|
||||
NUM_ENGINE_FLAGS,
|
||||
NUM_SPRITEMOVEDATA,
|
||||
OVERWORLD,
|
||||
RAM,
|
||||
SYMBOLS,
|
||||
} from "./profile";
|
||||
import { defaultTextContext, readString } from "../scripts/text";
|
||||
import { PokemonData } from "./pokemon";
|
||||
import { OW } from "../world/constants";
|
||||
import type {
|
||||
Direction,
|
||||
GameMap,
|
||||
@@ -308,6 +310,35 @@ export class CrystalAssets {
|
||||
];
|
||||
});
|
||||
}
|
||||
/** LoadSpecialMapPalette: tilesets with their own eight palettes, which
|
||||
* do not follow the time of day. */
|
||||
private specialPalettes(map: GameMap): Palette[] | undefined {
|
||||
const eight = (address: number) =>
|
||||
Array.from({ length: 8 }, (_, i) => this.palette(address + i * 8));
|
||||
switch (map.tileset) {
|
||||
case OW.TILESET_POKECOM_CENTER:
|
||||
return eight(OVERWORLD.PokeComPalette);
|
||||
case OW.TILESET_BATTLE_TOWER_INSIDE:
|
||||
return eight(OVERWORLD.BattleTowerInsidePalette);
|
||||
case OW.TILESET_ICE_PATH:
|
||||
// The Hall of Fame shares the tileset but is INDOOR.
|
||||
return (map.environment & 7) === OW.INDOOR ? undefined : eight(OVERWORLD.IcePathPalette);
|
||||
case OW.TILESET_HOUSE:
|
||||
return eight(SYMBOLS.housePalettes);
|
||||
case OW.TILESET_RADIO_TOWER:
|
||||
return eight(OVERWORLD.RadioTowerPalette);
|
||||
case OW.TILESET_MANSION: {
|
||||
// LoadMansionPalette: MansionPalette1, then yellow from
|
||||
// MansionPalette2, water from palette 6 and roof from palette 8.
|
||||
const palettes = eight(OVERWORLD.MansionPalette1);
|
||||
palettes[4] = this.palette(OVERWORLD.MansionPalette2);
|
||||
palettes[3] = this.palette(OVERWORLD.MansionPalette1 + 6 * 8);
|
||||
palettes[6] = this.palette(OVERWORLD.MansionPalette1 + 8 * 8);
|
||||
return palettes;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
palettes(map: GameMap, time: number): Palette[] {
|
||||
const r = this.reader;
|
||||
const group =
|
||||
@@ -316,13 +347,11 @@ export class CrystalAssets {
|
||||
r.bank(SYMBOLS.environmentColors),
|
||||
) +
|
||||
time * 8;
|
||||
const palettes = Array.from({ length: 8 }, (_, i) =>
|
||||
this.palette(
|
||||
map.tileset === 5
|
||||
? SYMBOLS.housePalettes + i * 8
|
||||
: SYMBOLS.backgroundPalettes + r.byte(group + i) * 8,
|
||||
),
|
||||
);
|
||||
const palettes =
|
||||
this.specialPalettes(map) ??
|
||||
Array.from({ length: 8 }, (_, i) =>
|
||||
this.palette(SYMBOLS.backgroundPalettes + r.byte(group + i) * 8),
|
||||
);
|
||||
if (map.environment === 1 || map.environment === 2) {
|
||||
const roof = SYMBOLS.roofPalettes + map.group * 8 + (time >= 2 ? 4 : 0);
|
||||
const colors = this.palette(roof);
|
||||
@@ -344,13 +373,15 @@ export class CrystalAssets {
|
||||
}
|
||||
throw new Error("Grass collision list is unterminated.");
|
||||
}
|
||||
/** JohtoGrassWildMons for a map: encounter rates and seven slots per
|
||||
* time of day (data/wild/johto_grass.asm). */
|
||||
/** JohtoGrassWildMons or KantoGrassWildMons for a map: encounter rates
|
||||
* and seven slots per time of day (data/wild/johto_grass.asm). */
|
||||
grassWildMons(
|
||||
id: string,
|
||||
kanto = false,
|
||||
): { rates: number[]; slots: { level: number; species: number }[][] } | undefined {
|
||||
const r = this.reader;
|
||||
for (let p = SYMBOLS.johtoGrassWildMons, i = 0; i < 256; i++, p += 47) {
|
||||
const table = kanto ? OVERWORLD.KantoGrassWildMons : SYMBOLS.johtoGrassWildMons;
|
||||
for (let p = table, i = 0; i < 256; i++, p += 47) {
|
||||
const group = r.byte(p);
|
||||
if (group === 0xff) return undefined;
|
||||
if (`${group}:${r.byte(p + 1)}` !== id) continue;
|
||||
@@ -365,6 +396,28 @@ export class CrystalAssets {
|
||||
}
|
||||
throw new Error("Wild encounter data is unterminated.");
|
||||
}
|
||||
/** JohtoWaterWildMons or KantoWaterWildMons: one rate and three slots
|
||||
* (data/wild/johto_water.asm). */
|
||||
waterWildMons(
|
||||
id: string,
|
||||
kanto = false,
|
||||
): { rate: number; slots: { level: number; species: number }[] } | undefined {
|
||||
const r = this.reader;
|
||||
const table = kanto ? OVERWORLD.KantoWaterWildMons : OVERWORLD.JohtoWaterWildMons;
|
||||
for (let p = table, i = 0; i < 256; i++, p += 9) {
|
||||
const group = r.byte(p);
|
||||
if (group === 0xff) return undefined;
|
||||
if (`${group}:${r.byte(p + 1)}` !== id) continue;
|
||||
return {
|
||||
rate: r.byte(p + 2),
|
||||
slots: Array.from({ length: 3 }, (_, slot) => ({
|
||||
level: r.byte(p + 3 + slot * 2),
|
||||
species: r.byte(p + 4 + slot * 2),
|
||||
})),
|
||||
};
|
||||
}
|
||||
throw new Error("Wild encounter data is unterminated.");
|
||||
}
|
||||
/** Engine flags kept in the four bytes CheckDailyResetTimer clears
|
||||
* (wDailyFlags1 through wUnusedDailyFlag), from the ROM's EngineFlags. */
|
||||
dailyEngineFlags(): number[] {
|
||||
@@ -381,13 +434,20 @@ export class CrystalAssets {
|
||||
* function, flags1 (FIXED_FACING, NOCLIP_*, ...) and palette flags. */
|
||||
spriteMovement(type: number): {
|
||||
fn: number;
|
||||
/** OBJECT_ACTION_* the object starts with. */
|
||||
action: number;
|
||||
flags: number;
|
||||
paletteFlags: number;
|
||||
} {
|
||||
if (type >= NUM_SPRITEMOVEDATA) throw new Error("Unknown movement type.");
|
||||
const r = this.reader,
|
||||
p = SYMBOLS.spriteMovementData + type * 6;
|
||||
return { fn: r.byte(p), flags: r.byte(p + 3), paletteFlags: r.byte(p + 5) };
|
||||
return {
|
||||
fn: r.byte(p),
|
||||
action: r.byte(p + 2),
|
||||
flags: r.byte(p + 3),
|
||||
paletteFlags: r.byte(p + 5),
|
||||
};
|
||||
}
|
||||
/** Emote bubble graphics (data/sprites/emotes.asm). */
|
||||
emote(index: number): Uint8Array {
|
||||
@@ -399,13 +459,25 @@ export class CrystalAssets {
|
||||
r.byte(entry + 2),
|
||||
);
|
||||
}
|
||||
/** GetSprite: overworld sprites, and Pokémon drawn from their party
|
||||
* menu icons (GetMonSprite). Variable sprites are resolved by the world
|
||||
* from wVariableSprites first; day-care Pokémon are not ported. */
|
||||
sprite(id: number): Sprite | undefined {
|
||||
// Variable decorations and Pokémon icons need separate decoders.
|
||||
if (id < 1 || id >= 0xa0) return undefined;
|
||||
const cached = this.sprites.get(id);
|
||||
if (cached) return cached;
|
||||
const r = this.reader,
|
||||
p = SYMBOLS.sprites + (id - 1) * 6;
|
||||
const r = this.reader;
|
||||
if (id >= OW.SPRITE_POKEMON && id < OW.SPRITE_POKEMON + OW.NUM_POKEMON_SPRITES) {
|
||||
// LoadOverworldMonIcon: eight tiles, two frames in the down and up
|
||||
// standing slots, with palette 0.
|
||||
const species = r.byte(OVERWORLD.SpriteMons + id - OW.SPRITE_POKEMON);
|
||||
const icon = r.byte(OVERWORLD.MonMenuIcons + species - 1);
|
||||
const pointer = r.near(OVERWORLD.IconPointers + icon * 2, r.bank(OVERWORLD.IconPointers));
|
||||
const sprite = { graphics: r.slice(pointer, 8 * 16), palette: 0, walking: true };
|
||||
this.sprites.set(id, sprite);
|
||||
return sprite;
|
||||
}
|
||||
if (id < 1 || id > OW.NUM_OVERWORLD_SPRITES) return undefined;
|
||||
const p = SYMBOLS.sprites + (id - 1) * 6;
|
||||
const length = r.byte(p + 2),
|
||||
walking = r.byte(p + 4) === 1;
|
||||
const sprite = {
|
||||
|
||||
@@ -155,6 +155,8 @@ export const RAM = {
|
||||
hiddenItemID: 0xd040,
|
||||
runningTrainerBattleScript: 0xd04d,
|
||||
callerContact: 0xd03f,
|
||||
surfingPlayerState: 0xd1eb,
|
||||
strengthSpecies: 0xd1ef,
|
||||
/** hMoneyTemp: HRAM, not WRAM like the other entries above. */
|
||||
moneyTemp: 0xffc3,
|
||||
} as const;
|
||||
@@ -174,6 +176,111 @@ export const ENGINE_FLAGS = {
|
||||
allFruitTrees: 84,
|
||||
} as const;
|
||||
|
||||
/** Overworld scripts, routines and tables, keyed by their PRET labels so
|
||||
* that `npm run check:reference` verifies each address by name. */
|
||||
export const OVERWORLD = {
|
||||
"OverworldTownMap": 0xc2c0,
|
||||
"TownMapGFX": 0xf8ba0,
|
||||
"JohtoMap": 0x91fff,
|
||||
"KantoMap": 0x92168,
|
||||
"TownMapPals.PalMap": 0x91f4b,
|
||||
"MalePokegearPals": 0xb729,
|
||||
"FemalePokegearPals": 0xb759,
|
||||
|
||||
"DecorationAttributes": 0x26a4f,
|
||||
"DecorationNames": 0x26b8d,
|
||||
"_PlayerDecorationMenu.category_pointers": 0x267aa,
|
||||
"PlayersHousePC": 0xc2e7,
|
||||
"PutAwayTheDecoText": 0x26ed1,
|
||||
"NothingToPutAwayText": 0x26ed6,
|
||||
"SetUpTheDecoText": 0x26edb,
|
||||
"PutAwayAndSetUpText": 0x26ee0,
|
||||
"AlreadySetUpText": 0x26ee5,
|
||||
"WhichSidePutOnText": 0x26e41,
|
||||
"WhichSidePutAwayText": 0x26e6b,
|
||||
"DecorationDesc_PosterPointers": 0x26f84,
|
||||
"DecorationDesc_OrnamentOrConsole.OrnamentConsoleScript": 0x26fd5,
|
||||
"DecorationDesc_GiantOrnament.BigDollScript": 0x26fe3,
|
||||
|
||||
Script_ForcedMovement: 0x1253d,
|
||||
FallIntoMapScript: 0x96c38,
|
||||
"_Sine.sinetable_u10": 0x850b,
|
||||
AskFloorElevatorText: 0x1350d,
|
||||
Elevator_CurrentFloorText: 0x13537,
|
||||
ElevatorFloorNames: 0x13583,
|
||||
JohtoWaterWildMons: 0x2b11d,
|
||||
KantoGrassWildMons: 0x2b274,
|
||||
KantoWaterWildMons: 0x2b7f7,
|
||||
WaterMonProbTable: 0x2a1d9,
|
||||
SpriteMons: 0x14495,
|
||||
MonMenuIcons: 0x8eac4,
|
||||
IconPointers: 0x8ebbf,
|
||||
AskSurfScript: 0xca2c,
|
||||
"UsedSurfScript.stubbed_fn": 0xc9a2,
|
||||
AskCutScript: 0xd1a9,
|
||||
"AskCutScript.CheckMap": 0xd1ba,
|
||||
CantCutScript: 0xd1cd,
|
||||
CutDownTreeOrGrass: 0xc810,
|
||||
GetPartyNickname: 0xc706,
|
||||
CutTreeBlockPointers: 0xc862,
|
||||
WhirlpoolBlockPointers: 0xc8a4,
|
||||
TryStrengthOW: 0xcd78,
|
||||
SetStrengthFlag: 0xcd12,
|
||||
Script_AskWaterfall: 0xcb86,
|
||||
Script_CantDoWaterfall: 0xcb7e,
|
||||
"Script_UsedWaterfall.CheckContinueWaterfall": 0xcb38,
|
||||
Script_AskWhirlpoolOW: 0xce6e,
|
||||
Script_MightyWhirlpool: 0xce66,
|
||||
DisappearWhirlpool: 0xce1d,
|
||||
ChrisStateSprites: 0x141b7,
|
||||
KrisStateSprites: 0x141c0,
|
||||
Script_GetOnBike: 0xd13e,
|
||||
Script_GetOffBike: 0xd158,
|
||||
Script_CantGetOffBike: 0xd171,
|
||||
UpdatePlayerSprite: 0xe4a,
|
||||
PlayMapMusic: 0x3cdf,
|
||||
SurfStartStep: 0x8379,
|
||||
UpdateTimePals: 0x485,
|
||||
WaitTileAnimation: 0xfc2fe,
|
||||
DoneTileAnimation: 0xfc2fb,
|
||||
StandingTileFrame8: 0xfc2ff,
|
||||
StandingTileFrame: 0xfc673,
|
||||
AnimateWaterTile: 0xfc402,
|
||||
"AnimateWaterTile.WaterTileFrames": 0xfc41c,
|
||||
AnimateWaterPalette: 0xfc6d7,
|
||||
AnimateFlowerTile: 0xfc56d,
|
||||
"AnimateFlowerTile.FlowerTileFrames": 0xfc58c,
|
||||
AnimateFountainTile: 0xfc387,
|
||||
"AnimateFountainTile.FountainTileFramePointers": 0xfc3a2,
|
||||
ForestTreeLeftAnimation: 0xfc45c,
|
||||
ForestTreeRightAnimation: 0xfc4c4,
|
||||
ForestTreeLeftAnimation2: 0xfc4f2,
|
||||
ForestTreeRightAnimation2: 0xfc51c,
|
||||
ForestTreeLeftFrames: 0xfc484,
|
||||
ForestTreeRightFrames: 0xfc4a4,
|
||||
AnimateLavaBubbleTile1: 0xfc5cc,
|
||||
AnimateLavaBubbleTile2: 0xfc5eb,
|
||||
LavaBubbleTileFrames: 0xfc605,
|
||||
AnimateTowerPillarTile: 0xfc645,
|
||||
"AnimateTowerPillarTile.TowerPillarTileFrameOffsets": 0xfc66b,
|
||||
AnimateWhirlpoolTile: 0xfc678,
|
||||
ReadTileToAnimBuffer: 0xfc6a2,
|
||||
WriteTileFromAnimBuffer: 0xfc696,
|
||||
ScrollTileRightLeft: 0xfc309,
|
||||
ScrollTileUpDown: 0xfc318,
|
||||
ScrollTileLeft: 0xfc327,
|
||||
ScrollTileRight: 0xfc33b,
|
||||
ScrollTileUp: 0xfc34f,
|
||||
ScrollTileDown: 0xfc36a,
|
||||
FlickeringCaveEntrancePalette: 0xfc71e,
|
||||
PokeComPalette: 0x49501,
|
||||
BattleTowerInsidePalette: 0x49550,
|
||||
IcePathPalette: 0x4959f,
|
||||
RadioTowerPalette: 0x4963d,
|
||||
MansionPalette1: 0x4967d,
|
||||
MansionPalette2: 0x496fe,
|
||||
} as const;
|
||||
|
||||
/** Battle engine tables and texts, keyed by their PRET labels so that
|
||||
* \`npm run check:reference\` can verify each address by name. */
|
||||
export const BATTLE = {
|
||||
|
||||
Reference in new issue
Block a user