Port Fly, Dig, Teleport and the Escape Rope

FLY opens _FlyMap in the party menu: the region's Town Map with "Where?",
the visited flypoints in turn and the Fly user's icon, then FlyScript flies
the icon off along SpriteAnimFunc_FlyFrom's path, enters the spawn point and
lands it with SpriteAnimFunc_FlyTo. Teleport returns to the last Pokémon
Center's town through IsSpawnPoint. Dig and the Escape Rope lead out of a
cave to the warp EnterMapWarp's SaveDigWarp recorded, which saves now keep.

newloadmap stops its script as StopScript does and the world resumes it on
the new map, which the landing and return movements need. A destination
that is not ported yet gives a notice.

Closes #25

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-10-02 11:18:57 +02:00
1 parent c4cc0e756c
commit a7aceda089
21 files changed
+1166 -97

No files matched your search

+51 -3
View File
@@ -30,6 +30,7 @@ export class CrystalAssets {
private maps = new Map<string, GameMap>();
private tilesets = new Map<string, Tileset>();
private sprites = new Map<number, Sprite>();
private spawnFlags?: Map<number, number>;
private sceneOwners?: Map<string, string>;
constructor(bytes: Uint8Array) {
this.reader = new RomReader(bytes);
@@ -432,6 +433,44 @@ export class CrystalAssets {
}
return flags;
}
/** The engine flag (ENGINE_FLYPOINT_*) of a wVisitedSpawns bit, from the
* ROM's EngineFlags; undefined for spawn points without one. */
spawnEngineFlag(spawn: number): number | undefined {
if (!this.spawnFlags) {
const r = this.reader;
this.spawnFlags = new Map();
for (let index = 0; index < NUM_ENGINE_FLAGS; index++) {
const entry = SYMBOLS.engineFlags + index * 3,
offset = r.word(entry) - RAM.visitedSpawns,
mask = r.byte(entry + 2);
if (offset >= 0 && offset < 4)
this.spawnFlags.set(offset * 8 + Math.log2(mask), index);
}
}
return this.spawnFlags.get(spawn);
}
/** SpawnPoints: a SPAWN_* point's map and position. */
spawnPoint(spawn: number): { map: string; x: number; y: number } {
const r = this.reader,
entry = SYMBOLS.spawnPoints + spawn * 4;
return {
map: `${r.byte(entry)}:${r.byte(entry + 1)}`,
x: r.byte(entry + 2),
y: r.byte(entry + 3),
};
}
/** IsSpawnPoint: the SPAWN_* index of a map, or -1. */
spawnIndex(map: string | null): number {
const r = this.reader;
if (map)
for (
let entry = SYMBOLS.spawnPoints, index = 0;
r.byte(entry) !== 0xff;
entry += 4, index++
)
if (`${r.byte(entry)}:${r.byte(entry + 1)}` === map) return index;
return -1;
}
/** SpriteMovementData for a SPRITEMOVEDATA_* type: the movement
* function, flags1 (FIXED_FACING, NOCLIP_*, ...) and palette flags. */
spriteMovement(type: number): {
@@ -461,6 +500,17 @@ export class CrystalAssets {
r.byte(entry + 2),
);
}
/** GetIcon: a species' two-frame party menu icon (ReadMonMenuIcon and
* IconPointers), eight tiles. */
monIcon(species: number): Uint8Array {
const r = this.reader;
const icon = r.byte(OVERWORLD.MonMenuIcons + species - 1);
const pointer = r.near(
OVERWORLD.IconPointers + icon * 2,
r.bank(OVERWORLD.IconPointers),
);
return r.slice(pointer, 8 * 16);
}
/** 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. */
@@ -472,9 +522,7 @@ export class CrystalAssets {
// 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 };
const sprite = { graphics: this.monIcon(species), palette: 0, walking: true };
this.sprites.set(id, sprite);
return sprite;
}
+16
View File
@@ -158,6 +158,7 @@ export const RAM = {
surfingPlayerState: 0xd1eb,
strengthSpecies: 0xd1ef,
curHPAnimDeltaHP: 0xd1f3,
visitedSpawns: 0xdca5,
/** hMoneyTemp: HRAM, not WRAM like the other entries above. */
moneyTemp: 0xffc3,
} as const;
@@ -170,6 +171,7 @@ export const EVENTS = {
playersHouseDoll2: 1859,
playersHouseBigDoll: 1860,
wallOpenedInAerodactylChamber: 809,
wallOpenedInKabutoChamber: 807,
} as const;
/** Engine flag indices named directly by translated routines. */
@@ -238,6 +240,19 @@ export const OVERWORLD = {
AlreadySurfingText: 0xc9b3,
"EscapeRopeOrDig.CantUseDigText": 0xcc26,
"TeleportFunction.CantUseTeleportText": 0xccb6,
"FlyFunction.FlyScript": 0xcaa3,
"FlyFunction.ReturnFromFly": 0xcacb,
"EscapeRopeOrDig.UsedEscapeRopeScript": 0xcc2b,
"EscapeRopeOrDig.UsedDigScript": 0xcc35,
"TeleportFunction.TeleportScript": 0xccbb,
FlyFromAnim: 0x8caed,
FlyToAnim: 0x8cb33,
HideSprites: 0x3016,
SkipUpdateMapSprites: 0x154f1,
WaitSFX: 0x3c55,
Flypoints: 0x91c5e,
"TownMapBubble.Where": 0x91bf4,
RuinsOfAlphKabutoChamber_MapAttributes: 0x958d0,
"Softboiled_MilkDrinkFunction.ItemCantUseOnMonText": 0xf44a,
RuinsOfAlphAerodactylChamber_MapAttributes: 0x958e8,
AskCutScript: 0xd1a9,
@@ -474,6 +489,7 @@ export const MENU = {
PPsIncreasedText: 0xf734,
BicycleEffect: 0xee08,
EvoStoneEffect: 0xee0f,
EscapeRopeEffect: 0xf44f,
VitaminEffect: 0xee3d,
RareCandyEffect: 0xef14,
ItemStatRoseText: 0xeea6,