Implement native opening scenes, battle foundation and overworld movement
Checks / web (push) Canceled after 0s
Checks / web (push) Canceled after 0s
This commit is contained in:
1 parent
8b80d05ca2
commit
c08d250cf6
39 files changed
+10727
-1524
No files matched your search
+173
-9
@@ -1,6 +1,13 @@
|
||||
import { RomReader } from "./reader";
|
||||
import { decompress } from "./lz";
|
||||
import { NATIVE_MAPS, SYMBOLS } from "./profile";
|
||||
import {
|
||||
LAST_MAP_GROUP_SIZE,
|
||||
MAP_GROUP_COUNT,
|
||||
NATIVE_MAPS,
|
||||
SYMBOLS,
|
||||
} from "./profile";
|
||||
import { defaultTextContext, readString } from "../scripts/text";
|
||||
import { PokemonData } from "./pokemon";
|
||||
import type {
|
||||
Direction,
|
||||
GameMap,
|
||||
@@ -14,23 +21,83 @@ import type {
|
||||
* decoded from the supplied ROM, on demand, and cached only in memory. */
|
||||
export class CrystalAssets {
|
||||
readonly reader: RomReader;
|
||||
readonly pokemon: PokemonData;
|
||||
private maps = new Map<string, GameMap>();
|
||||
private tilesets = new Map<string, Tileset>();
|
||||
private sprites = new Map<number, Sprite>();
|
||||
private sceneOwners?: Map<string, string>;
|
||||
constructor(bytes: Uint8Array) {
|
||||
this.reader = new RomReader(bytes);
|
||||
this.pokemon = new PokemonData(this.reader);
|
||||
}
|
||||
|
||||
/** Areas whose rules and scripts are ported. Other maps can be decoded,
|
||||
* for example to draw a connected border, but cannot be entered. */
|
||||
isPorted(id: string): boolean {
|
||||
return Object.hasOwn(NATIVE_MAPS, id);
|
||||
}
|
||||
|
||||
/** Header offset for a map ID, bounded by the ROM's own group table. */
|
||||
private header(id: string): number {
|
||||
const match = /^(\d{1,2}):(\d{1,3})$/.exec(id);
|
||||
const group = Number(match?.[1]),
|
||||
number = Number(match?.[2]);
|
||||
if (!match || group < 1 || group > MAP_GROUP_COUNT || number < 1)
|
||||
throw new Error("Unknown map.");
|
||||
const r = this.reader,
|
||||
bank = r.bank(SYMBOLS.mapGroups);
|
||||
const first = r.near(SYMBOLS.mapGroups + (group - 1) * 2, bank);
|
||||
const count =
|
||||
group === MAP_GROUP_COUNT
|
||||
? LAST_MAP_GROUP_SIZE
|
||||
: (r.near(SYMBOLS.mapGroups + group * 2, bank) - first) / 9;
|
||||
if (number > count) throw new Error("Unknown map.");
|
||||
return first + (number - 1) * 9;
|
||||
}
|
||||
|
||||
/** The location name the game shows for a landmark, from the ROM. */
|
||||
landmark(index: number): string {
|
||||
const r = this.reader,
|
||||
entry = SYMBOLS.landmarks + index * 4;
|
||||
const name = readString(
|
||||
r,
|
||||
r.near(entry + 2, r.bank(SYMBOLS.landmarks)),
|
||||
defaultTextContext(""),
|
||||
);
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(
|
||||
/(^|[\s.-])(\p{L})/gu,
|
||||
(_, gap, letter) => gap + letter.toUpperCase(),
|
||||
);
|
||||
}
|
||||
|
||||
/** The map that owns a map's scene variable (data/maps/scenes.asm). */
|
||||
sceneOwner(id: string): string | undefined {
|
||||
if (!this.sceneOwners) {
|
||||
const owners = new Map<string, string>(),
|
||||
byAddress = new Map<number, string>();
|
||||
const r = this.reader;
|
||||
for (let p = SYMBOLS.mapScenes, i = 0; i < 256; i++, p += 4) {
|
||||
const group = r.byte(p);
|
||||
if (group === 0xff) break;
|
||||
const map = `${group}:${r.byte(p + 1)}`,
|
||||
address = r.word(p + 2);
|
||||
const owner = byAddress.get(address) ?? map;
|
||||
byAddress.set(address, owner);
|
||||
owners.set(map, owner);
|
||||
}
|
||||
this.sceneOwners = owners;
|
||||
}
|
||||
return this.sceneOwners.get(id);
|
||||
}
|
||||
|
||||
map(id: string): GameMap {
|
||||
const cached = this.maps.get(id);
|
||||
if (cached) return cached;
|
||||
if (!Object.hasOwn(NATIVE_MAPS, id))
|
||||
throw new Error("This area has not been ported yet.");
|
||||
const header = this.header(id);
|
||||
const [group, number] = id.split(":").map(Number);
|
||||
const r = this.reader;
|
||||
const header =
|
||||
r.near(SYMBOLS.mapGroups + (group - 1) * 2, r.bank(SYMBOLS.mapGroups)) +
|
||||
(number - 1) * 9;
|
||||
const attributes = r.near(header + 3, r.byte(header));
|
||||
const blockWidth = r.byte(attributes + 2),
|
||||
blockHeight = r.byte(attributes + 1);
|
||||
@@ -38,11 +105,14 @@ export class CrystalAssets {
|
||||
throw new Error("Invalid map dimensions.");
|
||||
const scriptBank = r.byte(attributes + 6);
|
||||
let p = r.near(attributes + 9, scriptBank) + 2;
|
||||
const location = this.landmark(r.byte(header + 5));
|
||||
const map: GameMap = {
|
||||
id,
|
||||
group,
|
||||
number,
|
||||
name: NATIVE_MAPS[id],
|
||||
name: NATIVE_MAPS[id] ?? location,
|
||||
location,
|
||||
landmark: r.byte(header + 5),
|
||||
width: blockWidth * 2,
|
||||
height: blockHeight * 2,
|
||||
border: r.byte(attributes),
|
||||
@@ -165,6 +235,54 @@ export class CrystalAssets {
|
||||
if (x < 0 || y < 0 || x >= map.width || y >= map.height) return map.border;
|
||||
return map.blocks[Math.floor(y / 2) * (map.width / 2) + Math.floor(x / 2)];
|
||||
}
|
||||
/** Where a step coordinate outside `map` lands in a connected map. */
|
||||
connected(
|
||||
map: GameMap,
|
||||
x: number,
|
||||
y: number,
|
||||
): { map: GameMap; x: number; y: number } | undefined {
|
||||
const direction =
|
||||
y < 0
|
||||
? "up"
|
||||
: y >= map.height
|
||||
? "down"
|
||||
: x < 0
|
||||
? "left"
|
||||
: x >= map.width
|
||||
? "right"
|
||||
: undefined;
|
||||
// Corners are outside every connection strip and show border blocks.
|
||||
if (!direction || ((x < 0 || x >= map.width) && (y < 0 || y >= map.height)))
|
||||
return undefined;
|
||||
const connection = map.connections.find((c) => c.direction === direction);
|
||||
if (!connection) return undefined;
|
||||
const target = this.map(connection.targetMap);
|
||||
// Offsets are the coordinates EnterMapConnection assigns on crossing.
|
||||
const position =
|
||||
direction === "up"
|
||||
? { x: x + connection.x, y: y + 1 + connection.y }
|
||||
: direction === "down"
|
||||
? { x: x + connection.x, y: y - map.height + connection.y }
|
||||
: direction === "left"
|
||||
? { x: x + 1 + connection.x, y: y + connection.y }
|
||||
: { x: x - map.width + connection.x, y: y + connection.y };
|
||||
if (
|
||||
position.x < 0 ||
|
||||
position.y < 0 ||
|
||||
position.x >= target.width ||
|
||||
position.y >= target.height
|
||||
)
|
||||
return undefined;
|
||||
return { map: target, ...position };
|
||||
}
|
||||
/** A visible block, drawing connected maps beyond the edges as the
|
||||
* original connection strips do, with the current map's tileset. */
|
||||
visibleBlock(map: GameMap, x: number, y: number): number {
|
||||
if (x >= 0 && y >= 0 && x < map.width && y < map.height)
|
||||
return this.block(map, x, y);
|
||||
const target = this.connected(map, x, y);
|
||||
return target ? this.block(target.map, target.x, target.y) : map.border;
|
||||
}
|
||||
collision(map: GameMap, x: number, y: number): number {
|
||||
if (x < 0 || y < 0 || x >= map.width || y >= map.height) return 7;
|
||||
const block = this.block(map, x, y);
|
||||
@@ -210,6 +328,48 @@ export class CrystalAssets {
|
||||
objectPalette(index: number, time: number): Palette {
|
||||
return this.palette(SYMBOLS.objectPalettes + time * 64 + (index & 7) * 8);
|
||||
}
|
||||
/** Collisions that allow wild encounters (CheckGrassCollision). */
|
||||
grassCollisions(): number[] {
|
||||
const r = this.reader,
|
||||
values: number[] = [];
|
||||
for (let p = SYMBOLS.grassCollisions; values.length < 32; p++) {
|
||||
const value = r.byte(p);
|
||||
if (value === 0xff) return values;
|
||||
values.push(value);
|
||||
}
|
||||
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). */
|
||||
grassWildMons(
|
||||
id: string,
|
||||
): { 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 group = r.byte(p);
|
||||
if (group === 0xff) return undefined;
|
||||
if (`${group}:${r.byte(p + 1)}` !== id) continue;
|
||||
const rates = [r.byte(p + 2), r.byte(p + 3), r.byte(p + 4)];
|
||||
const slots = [0, 1, 2].map((time) =>
|
||||
Array.from({ length: 7 }, (_, slot) => {
|
||||
const entry = p + 5 + time * 14 + slot * 2;
|
||||
return { level: r.byte(entry), species: r.byte(entry + 1) };
|
||||
}),
|
||||
);
|
||||
return { rates, slots };
|
||||
}
|
||||
throw new Error("Wild encounter data is unterminated.");
|
||||
}
|
||||
/** Emote bubble graphics (data/sprites/emotes.asm). */
|
||||
emote(index: number): Uint8Array {
|
||||
const r = this.reader,
|
||||
entry = SYMBOLS.emotes + index * 6;
|
||||
if (index < 0 || index > 11) throw new Error("Unknown emote.");
|
||||
return r.slice(
|
||||
r.address(r.byte(entry + 3), r.word(entry)),
|
||||
r.byte(entry + 2),
|
||||
);
|
||||
}
|
||||
sprite(id: number): Sprite | undefined {
|
||||
// Variable decorations and Pokémon icons need separate decoders.
|
||||
if (id < 1 || id >= 0xa0) return undefined;
|
||||
@@ -228,8 +388,12 @@ export class CrystalAssets {
|
||||
return sprite;
|
||||
}
|
||||
facing(direction: Direction, frame: number): FacingTile[] {
|
||||
const facing =
|
||||
["down", "up", "left", "right"].indexOf(direction) * 4 + (frame % 4);
|
||||
return this.facingTiles(
|
||||
["down", "up", "left", "right"].indexOf(direction) * 4 + (frame % 4),
|
||||
);
|
||||
}
|
||||
/** FACING_* layouts; 0x14 is the emote bubble. */
|
||||
facingTiles(facing: number): FacingTile[] {
|
||||
const r = this.reader,
|
||||
pointer = r.near(SYMBOLS.facings + facing * 2, r.bank(SYMBOLS.facings));
|
||||
return Array.from({ length: r.byte(pointer) }, (_, i) => {
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
import { RomReader } from "./reader";
|
||||
import { decompress } from "./lz";
|
||||
import { NUM_ITEMS, NUM_POKEMON, SYMBOLS } from "./profile";
|
||||
import { defaultTextContext, readString } from "../scripts/text";
|
||||
import type { Palette } from "../world/types";
|
||||
|
||||
/** data/pokemon/base_stats: one 32-byte record per species. */
|
||||
export interface BaseData {
|
||||
species: number;
|
||||
/** HP, Attack, Defense, Speed, Sp. Atk, Sp. Def. */
|
||||
stats: readonly [number, number, number, number, number, number];
|
||||
types: readonly [number, number];
|
||||
catchRate: number;
|
||||
baseExp: number;
|
||||
items: readonly [number, number];
|
||||
genderRatio: number;
|
||||
eggCycles: number;
|
||||
/** Front picture width and height in tiles (5, 6 or 7). */
|
||||
picSize: number;
|
||||
growthRate: number;
|
||||
eggGroups: number;
|
||||
}
|
||||
export interface MoveData {
|
||||
id: number;
|
||||
effect: number;
|
||||
power: number;
|
||||
type: number;
|
||||
accuracy: number;
|
||||
pp: number;
|
||||
chance: number;
|
||||
}
|
||||
/** Item types, which select a bag pocket (constants/item_data_constants.asm). */
|
||||
export type Pocket = "item" | "key" | "ball" | "tm";
|
||||
const POCKETS: readonly Pocket[] = ["item", "key", "ball", "tm"];
|
||||
|
||||
/** Decoders for species, moves and items. All data is read from the
|
||||
* user's ROM on demand and cached in memory only. */
|
||||
export class PokemonData {
|
||||
private base = new Map<number, BaseData>();
|
||||
private names = new Map<string, string>();
|
||||
constructor(readonly reader: RomReader) {}
|
||||
|
||||
private checkSpecies(species: number) {
|
||||
if (!Number.isInteger(species) || species < 1 || species > NUM_POKEMON)
|
||||
throw new Error("Unknown Pokémon species.");
|
||||
}
|
||||
baseData(species: number): BaseData {
|
||||
this.checkSpecies(species);
|
||||
const cached = this.base.get(species);
|
||||
if (cached) return cached;
|
||||
const r = this.reader,
|
||||
p = SYMBOLS.baseData + (species - 1) * 32;
|
||||
const data: BaseData = {
|
||||
species: r.byte(p),
|
||||
stats: [1, 2, 3, 4, 5, 6].map((i) =>
|
||||
r.byte(p + i),
|
||||
) as unknown as BaseData["stats"],
|
||||
types: [r.byte(p + 7), r.byte(p + 8)],
|
||||
catchRate: r.byte(p + 9),
|
||||
baseExp: r.byte(p + 10),
|
||||
items: [r.byte(p + 11), r.byte(p + 12)],
|
||||
genderRatio: r.byte(p + 13),
|
||||
eggCycles: r.byte(p + 15),
|
||||
picSize: r.byte(p + 17) & 15,
|
||||
growthRate: r.byte(p + 22),
|
||||
eggGroups: r.byte(p + 23),
|
||||
};
|
||||
this.base.set(species, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Fixed-length names are padded with "@"; a full-length name has none. */
|
||||
speciesName(species: number): string {
|
||||
this.checkSpecies(species);
|
||||
return this.cachedName(`mon:${species}`, () => {
|
||||
const r = this.reader,
|
||||
p = SYMBOLS.pokemonNames + (species - 1) * 10;
|
||||
let bytes = 0;
|
||||
while (bytes < 10 && r.byte(p + bytes) !== 0x50) bytes++;
|
||||
const copy = new Uint8Array(bytes + 1);
|
||||
copy.set(r.slice(p, bytes));
|
||||
copy[bytes] = 0x50;
|
||||
return readString(new RomReader(copy), 0, defaultTextContext(""));
|
||||
});
|
||||
}
|
||||
/** Variable-length name lists are read like GetName: skip n - 1 names. */
|
||||
private listName(table: number, index: number): string {
|
||||
const r = this.reader;
|
||||
let p = table;
|
||||
for (let skipped = 1; skipped < index; skipped++) {
|
||||
let length = 0;
|
||||
while (r.byte(p++) !== 0x50)
|
||||
if (++length > 32) throw new Error("Invalid name list.");
|
||||
}
|
||||
return readString(r, p, defaultTextContext(""));
|
||||
}
|
||||
moveName(move: number): string {
|
||||
if (!Number.isInteger(move) || move < 1 || move > 251)
|
||||
throw new Error("Unknown move.");
|
||||
return this.cachedName(`move:${move}`, () =>
|
||||
this.listName(SYMBOLS.moveNames, move),
|
||||
);
|
||||
}
|
||||
itemName(item: number): string {
|
||||
this.checkItem(item);
|
||||
return this.cachedName(`item:${item}`, () =>
|
||||
this.listName(SYMBOLS.itemNames, item),
|
||||
);
|
||||
}
|
||||
private cachedName(key: string, read: () => string) {
|
||||
let name = this.names.get(key);
|
||||
if (name === undefined) {
|
||||
name = read();
|
||||
this.names.set(key, name);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
private checkItem(item: number) {
|
||||
if (!Number.isInteger(item) || item < 1 || item > NUM_ITEMS)
|
||||
throw new Error("Unknown item.");
|
||||
}
|
||||
/** ItemAttributes: price, held effect, parameter, property, pocket, menus. */
|
||||
itemPocket(item: number): Pocket {
|
||||
this.checkItem(item);
|
||||
const pocket =
|
||||
POCKETS[
|
||||
this.reader.byte(SYMBOLS.itemAttributes + (item - 1) * 7 + 5) - 1
|
||||
];
|
||||
if (!pocket) throw new Error("Item has no pocket.");
|
||||
return pocket;
|
||||
}
|
||||
pocketName(pocket: Pocket): string {
|
||||
const r = this.reader,
|
||||
bank = r.bank(SYMBOLS.itemPocketNames);
|
||||
return readString(
|
||||
r,
|
||||
r.near(SYMBOLS.itemPocketNames + POCKETS.indexOf(pocket) * 2, bank),
|
||||
defaultTextContext(""),
|
||||
);
|
||||
}
|
||||
|
||||
move(move: number): MoveData {
|
||||
if (!Number.isInteger(move) || move < 1 || move > 251)
|
||||
throw new Error("Unknown move.");
|
||||
const r = this.reader,
|
||||
p = SYMBOLS.moves + (move - 1) * 7;
|
||||
return {
|
||||
id: r.byte(p),
|
||||
effect: r.byte(p + 1),
|
||||
power: r.byte(p + 2),
|
||||
type: r.byte(p + 3),
|
||||
accuracy: r.byte(p + 4),
|
||||
pp: r.byte(p + 5),
|
||||
chance: r.byte(p + 6),
|
||||
};
|
||||
}
|
||||
|
||||
/** Level-up moves (data/pokemon/evos_attacks.asm). Like FillMoves, the
|
||||
* evolution list is skipped up to its terminating zero byte. */
|
||||
learnset(species: number): { level: number; move: number }[] {
|
||||
this.checkSpecies(species);
|
||||
const r = this.reader;
|
||||
let p = r.near(
|
||||
SYMBOLS.evosAttacks + (species - 1) * 2,
|
||||
r.bank(SYMBOLS.evosAttacks),
|
||||
);
|
||||
for (let guard = 0; r.byte(p++) !== 0; guard++)
|
||||
if (guard > 64) throw new Error("Invalid evolution data.");
|
||||
const moves: { level: number; move: number }[] = [];
|
||||
for (let level = r.byte(p); level !== 0; level = r.byte(p)) {
|
||||
moves.push({ level, move: r.byte(p + 1) });
|
||||
p += 2;
|
||||
if (moves.length > 64) throw new Error("Invalid learnset.");
|
||||
}
|
||||
return moves;
|
||||
}
|
||||
|
||||
/** CalcExpAtLevel: (a/b)n^3 + c n^2 + d n - e in 24-bit arithmetic,
|
||||
* including the original underflow for Medium Slow at level 1. */
|
||||
expAtLevel(growthRate: number, level: number): number {
|
||||
if (!Number.isInteger(growthRate) || growthRate < 0 || growthRate > 5)
|
||||
throw new Error("Unknown growth rate.");
|
||||
const r = this.reader,
|
||||
p = SYMBOLS.growthRates + growthRate * 4;
|
||||
const ab = r.byte(p),
|
||||
c = r.byte(p + 1),
|
||||
d = r.byte(p + 2),
|
||||
e = r.byte(p + 3);
|
||||
const cubic = Math.floor(((ab >> 4) * level ** 3) / (ab & 15)) & 0xffffff;
|
||||
const square = ((c & 0x7f) * level ** 2) & 0xffffff;
|
||||
const linear = (d * level - e) & 0xffffff;
|
||||
const quadratic = c & 0x80 ? linear - square : linear + square;
|
||||
return ((quadratic & 0xffffff) + cubic) & 0xffffff;
|
||||
}
|
||||
|
||||
/** The decompressed front picture: base frame, then animation tiles. */
|
||||
frontPicData(species: number): Uint8Array {
|
||||
this.checkSpecies(species);
|
||||
const r = this.reader,
|
||||
entry = SYMBOLS.pokemonPics + (species - 1) * 6;
|
||||
// FixPicBank: stored banks index a table whose first entry is "Pics 1".
|
||||
const pics1 = r.byte(SYMBOLS.picsBanks);
|
||||
const bank = r.byte(SYMBOLS.picsBanks + r.byte(entry) - (pics1 - 0x36));
|
||||
return decompress(r, r.address(bank, r.word(entry + 1)), 0x2000);
|
||||
}
|
||||
/** Front picture tiles, padded into the 7x7 box as PadFrontpic does:
|
||||
* column-major, bottom-aligned, with a blank left column for 5 and 6. */
|
||||
frontPic(species: number): Uint8Array {
|
||||
const size = this.baseData(species).picSize;
|
||||
if (size < 5 || size > 7) throw new Error("Invalid picture size.");
|
||||
const tiles = this.frontPicData(species);
|
||||
if (tiles.length < size * size * 16)
|
||||
throw new Error("Picture data is truncated.");
|
||||
const padded = new Uint8Array(49 * 16);
|
||||
const left = size === 7 ? 0 : 1;
|
||||
for (let column = 0; column < size; column++)
|
||||
padded.set(
|
||||
tiles.slice(column * size * 16, (column + 1) * size * 16),
|
||||
((column + left) * 7 + (7 - size)) * 16,
|
||||
);
|
||||
return padded;
|
||||
}
|
||||
/** The back picture: 6x6 tiles, column by column (GetMonBackpic). */
|
||||
backPic(species: number): Uint8Array {
|
||||
this.checkSpecies(species);
|
||||
const r = this.reader,
|
||||
entry = SYMBOLS.pokemonPics + (species - 1) * 6 + 3;
|
||||
const pics1 = r.byte(SYMBOLS.picsBanks);
|
||||
const bank = r.byte(SYMBOLS.picsBanks + r.byte(entry) - (pics1 - 0x36));
|
||||
const tiles = decompress(r, r.address(bank, r.word(entry + 1)), 0x1000);
|
||||
if (tiles.length < 36 * 16) throw new Error("Picture data is truncated.");
|
||||
return tiles.slice(0, 36 * 16);
|
||||
}
|
||||
/** Shininess from DVs (CheckShininess): Defense, Speed and Special 10,
|
||||
* and an Attack DV with bit 1 set. */
|
||||
static shiny(dvs: number): boolean {
|
||||
const attack = (dvs >> 12) & 15;
|
||||
return (dvs & 0x0fff) === 0x0aaa && Boolean(attack & 2);
|
||||
}
|
||||
/** Normal colors from PokemonPalettes, between white and black. */
|
||||
palette(species: number, shiny = false): Palette {
|
||||
this.checkSpecies(species);
|
||||
const r = this.reader,
|
||||
p = SYMBOLS.pokemonPalettes + species * 8 + (shiny ? 4 : 0);
|
||||
const color = (value: number) =>
|
||||
[
|
||||
Math.round(((value & 31) * 255) / 31),
|
||||
Math.round((((value >> 5) & 31) * 255) / 31),
|
||||
Math.round((((value >> 10) & 31) * 255) / 31),
|
||||
] as const;
|
||||
return [color(0x7fff), color(r.word(p)), color(r.word(p + 2)), color(0)];
|
||||
}
|
||||
}
|
||||
+205
-1
@@ -16,9 +16,213 @@ export const SYMBOLS = {
|
||||
sprites: 0x14736,
|
||||
facings: 0x4049,
|
||||
standardScripts: 0xbc000,
|
||||
mapScenes: 0x4d01e,
|
||||
specials: 0xc029,
|
||||
emotes: 0x1444d,
|
||||
landmarks: 0x1ca8c3,
|
||||
weekdayNames: 0x909f2,
|
||||
whatDayText: 0x90a3f,
|
||||
isItDayText: 0x90a4f,
|
||||
dstConfirmText: 0x90a83,
|
||||
timeConfirmText: 0x90ab7,
|
||||
baseData: 0x51424,
|
||||
pokemonNames: 0x53384,
|
||||
pokemonPics: 0x120000,
|
||||
picsBanks: 0x511d4,
|
||||
pokemonPalettes: 0xa8ce,
|
||||
growthRates: 0x50efa,
|
||||
evosAttacks: 0x425b1,
|
||||
moves: 0x41afb,
|
||||
moveNames: 0x1c9f29,
|
||||
itemNames: 0x1c8000,
|
||||
itemAttributes: 0x67c1,
|
||||
itemPocketNames: 0x9701c,
|
||||
giveItemScript: 0x96f77,
|
||||
putItemInPocketText: 0x9705b,
|
||||
pocketIsFullText: 0x97060,
|
||||
caughtAskNicknameText: 0x4db44,
|
||||
nicknameStrings: 0x11780,
|
||||
wildBattleScript: 0x97cf9,
|
||||
battleWhiteoutScript: 0x124c1,
|
||||
spawnPoints: 0x152ab,
|
||||
johtoGrassWildMons: 0x2a5e9,
|
||||
grassMonProbTable: 0x2a1cb,
|
||||
grassCollisions: 0x149ea,
|
||||
} as const;
|
||||
|
||||
/** The currently ported area. Story callbacks are not executed yet. */
|
||||
/** Assembly routines that scripts `callasm` but that do nothing. */
|
||||
export const EMPTY_ROUTINES = {
|
||||
giveItemDummy: 0x96f76,
|
||||
battleBGMap: 0x1250a,
|
||||
} as const;
|
||||
|
||||
/** Assembly routines that scripts `callasm`, translated natively. */
|
||||
export const NATIVE_ROUTINES = {
|
||||
halveMoney: 0x12513,
|
||||
getWhiteoutSpawn: 0x12527,
|
||||
} as const;
|
||||
|
||||
/** Native handlers are selected by the routine a special index points to,
|
||||
* so the ROM's own special table remains the source of truth. */
|
||||
export const SPECIAL_ROUTINES = {
|
||||
restartMapMusic: 0x3d47,
|
||||
setDayOfWeek: 0x90913,
|
||||
initialSetDst: 0x90a54,
|
||||
initialClearDst: 0x90a88,
|
||||
toggleDecorationsVisibility: 0x27043,
|
||||
toggleMaptileDecorations: 0x26feb,
|
||||
fadeOutToWhite: 0x8c084,
|
||||
healParty: 0xc658,
|
||||
warpToSpawnPoint: 0x97c28,
|
||||
} as const;
|
||||
|
||||
/** Work RAM addresses that the event and text formats name as operands. */
|
||||
export const RAM = {
|
||||
stringBuffer1: 0xd073,
|
||||
stringBuffer2: 0xd086,
|
||||
stringBuffer3: 0xd099,
|
||||
stringBuffer4: 0xd0ac,
|
||||
stringBuffer5: 0xd0bf,
|
||||
playerName: 0xd47d,
|
||||
momsName: 0xd488,
|
||||
rivalName: 0xd493,
|
||||
battleMonNickname: 0xc621,
|
||||
enemyMonNickname: 0xc616,
|
||||
curPartyLevel: 0xd143,
|
||||
monOrItemNameBuffer: 0xd050,
|
||||
} as const;
|
||||
|
||||
/** Event constants named directly by translated special routines. */
|
||||
export const EVENTS = {
|
||||
playersRoomPoster: 716,
|
||||
playersHouseConsole: 1857,
|
||||
playersHouseDoll1: 1858,
|
||||
playersHouseDoll2: 1859,
|
||||
playersHouseBigDoll: 1860,
|
||||
} 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 = {
|
||||
AccuracyLevelMultipliers: 0x34eb2,
|
||||
AlreadyAsleepText: 0x811c1,
|
||||
AlreadyConfusedText: 0x80dcc,
|
||||
AlreadyParalyzedText: 0x81592,
|
||||
AlreadyPoisonedText: 0x811fa,
|
||||
AlwaysFleeMons: 0x3c5b1,
|
||||
AskForgetMoveText: 0x667f,
|
||||
AttackMissed2Text: 0x81033,
|
||||
AttackMissedText: 0x81020,
|
||||
"BattleCommand_StatDownMessage.BattleStatFellText": 0x36415,
|
||||
"BattleCommand_StatDownMessage.BattleStatSharplyFellText": 0x36410,
|
||||
"BattleCommand_StatDownMessage.stat": 0x363fe,
|
||||
"BattleCommand_StatUpMessage.BattleStatWentUpText": 0x363e4,
|
||||
"BattleCommand_StatUpMessage.BattleStatWentWayUpText": 0x363df,
|
||||
"BattleCommand_StatUpMessage.stat": 0x363cd,
|
||||
BattleMonNicknameText: 0x3f2ef,
|
||||
BattleText_CantEscape: 0x80ba0,
|
||||
BattleText_CantEscape2: 0x80b3b,
|
||||
BattleText_EnemyMonFainted: 0x809a8,
|
||||
BattleText_GotAwaySafely: 0x80b77,
|
||||
BattleText_ItemHealedConfusion: 0x80dab,
|
||||
BattleText_MonFainted: 0x80a75,
|
||||
BattleText_MonHasNoMovesLeft: 0x80c72,
|
||||
BattleText_StringBuffer1GrewToLevel: 0x80c9c,
|
||||
BattleText_TargetRecoveredWithItem: 0x80880,
|
||||
BattleText_TheMoveIsDisabled: 0x80c5b,
|
||||
BattleText_TheresNoPPLeftForThisMove: 0x80c39,
|
||||
BattleText_WildFled: 0x807bd,
|
||||
BecameConfusedText: 0x80d97,
|
||||
BoostedExpPointsText: 0x3f12c,
|
||||
ButItFailedText: 0x814f0,
|
||||
ConfusedNoMoreText: 0x80d81,
|
||||
ConsumableEffects: 0x271de,
|
||||
CriticalHitChances: 0x346ab,
|
||||
CriticalHitMoves: 0x346a3,
|
||||
CriticalHitText: 0x81086,
|
||||
DefrostedOpponentText: 0x8124b,
|
||||
DidNotLearnMoveText: 0x667a,
|
||||
DidntAffect1Text: 0x8150c,
|
||||
DidntAffect2Text: 0x81520,
|
||||
DisabledMoveText: 0x80f02,
|
||||
DisabledNoMoreText: 0x80d39,
|
||||
DoesntAffectText: 0x81071,
|
||||
EnemyText: 0x1295,
|
||||
ExpPointsText: 0x3f131,
|
||||
FastAsleepText: 0x80ce7,
|
||||
FellAsleepText: 0x811b1,
|
||||
FlinchedText: 0x80d1a,
|
||||
FrozenSolidText: 0x80d06,
|
||||
FullyParalyzedText: 0x8157d,
|
||||
GoMonText: 0x3f2d1,
|
||||
HasNoPPLeftText: 0x80fba,
|
||||
HeldStatusHealingEffects: 0x3de44,
|
||||
HPIsFullText: 0x81534,
|
||||
HungOnText: 0x80e99,
|
||||
HurtByBurnText: 0x807f8,
|
||||
HurtByPoisonText: 0x807e2,
|
||||
HurtItselfText: 0x80d5f,
|
||||
IdentifiedText: 0x816b1,
|
||||
IsConfusedText: 0x80d4f,
|
||||
"IsHMMove.HMMoves": 0x34f0,
|
||||
ItemAttributes: 0x67c1,
|
||||
ItFailedText: 0x81500,
|
||||
LearnedMoveText: 0x666b,
|
||||
MoveAskForgetText: 0x6670,
|
||||
MoveCantForgetHMText: 0x669a,
|
||||
MoveEffectPriorities: 0x3c5df,
|
||||
MoveEffectsPointers: 0x271f4,
|
||||
MoveGrammar: 0x105e7a,
|
||||
MoveNameText: 0x105e23,
|
||||
"MoveNameText.endusedmovetexts": 0x105e39,
|
||||
MustRechargeText: 0x80d27,
|
||||
NoPPLeftText: 0x80f99,
|
||||
NothingHappenedText: 0x814d9,
|
||||
NotVeryEffectiveText: 0x810c1,
|
||||
OftenFleeMons: 0x3c5a8,
|
||||
OneHitKOText: 0x81097,
|
||||
ParalyzedText: 0x81558,
|
||||
"PrintTempMonStats.StatNames": 0x50bb5,
|
||||
ProtectedByMistText: 0x81321,
|
||||
ProtectedByText: 0x815a9,
|
||||
ProtectingItselfText: 0x8167a,
|
||||
RageBuildingText: 0x810f3,
|
||||
RecoveredUsingText: 0x80bc2,
|
||||
RegainedHealthText: 0x8100c,
|
||||
SafeguardProtectText: 0x81733,
|
||||
SometimesFleeMons: 0x3c59a,
|
||||
StatLevelMultipliers: 0x364e6,
|
||||
StatNames: 0x364a6,
|
||||
StopLearningMoveText: 0x6675,
|
||||
SuckedHealthText: 0x81210,
|
||||
SuperEffectiveText: 0x810aa,
|
||||
Text_1_2_and_Poof: 0x6684,
|
||||
"Text_1_2_and_Poof.MoveForgotText": 0x6695,
|
||||
Text_MonGainedExpPoint: 0x3f11b,
|
||||
TypeBoostItems: 0x35703,
|
||||
TypeMatchups: 0x34bb1,
|
||||
UnaffectedText: 0x81061,
|
||||
UsedMove1Text: 0x105e04,
|
||||
UsedMove2Text: 0x105e0b,
|
||||
UsedMoveText: 0x105db9,
|
||||
"UsedMoveText_CheckObedience.UsedInsteadText": 0x105e1a,
|
||||
WasBurnedText: 0x8123c,
|
||||
WasPoisonedText: 0x811d5,
|
||||
WildPokemonAppearedText: 0x80746,
|
||||
WokeUpText: 0x80cfa,
|
||||
WontDropAnymoreText: 0x8128f,
|
||||
WontRiseAnymoreText: 0x81272,
|
||||
} as const;
|
||||
|
||||
export const MAP_GROUP_COUNT = 26;
|
||||
export const NUM_POKEMON = 251;
|
||||
export const NUM_ITEMS = 190;
|
||||
/** The last group has no following pointer to bound it. */
|
||||
export const LAST_MAP_GROUP_SIZE = 11;
|
||||
|
||||
/** Areas whose rules, scripts and presentation are ported. Other decoded
|
||||
* maps remain unavailable; the label only distinguishes interiors that share
|
||||
* a ROM landmark name. */
|
||||
export const NATIVE_MAPS: Record<string, string> = {
|
||||
"24:4": "New Bark Town",
|
||||
"24:5": "Elm’s lab",
|
||||
|
||||
Reference in new issue
Block a user