Open Route 32 to Route 34, the Ruins of Alph and Azalea Town (#32)
Enable Route 32 and its Pokémon Center, the Ruins of Alph with the four chambers, item and word rooms, inner chamber and research center, Union Cave, Route 33, Azalea Town with Kurt's house, the kiln and Bugsy's gym, the Slowpoke Well, Ilex Forest and Route 34. Port what their scripts need: saved WRAM bytes by label (readmem, writemem, loadmem: wFarfetchdPosition, wKurtApricornQuantity), VAR_UNOWNCOUNT and VAR_KURT_APRICORNS, SelectApricornForKurt and verbosegiveitemvar, Unown letters (GetUnownLetter, per-letter pictures, CheckUnownLetter's redraw, UpdateUnownDex, wFirstUnownSeen), the chamber puzzles (_UnownPuzzle), DisplayUnownWords, HoOhChamber and OmanyteChamber. Minigames and specials that draw a whole screen in tiles now share a tile screen effect with ROM graphics, palettes and sprites, answered with buttons; the standard font and textbox frame load for them. The playthrough continues through Kurt's Slowpoke Well, Bugsy, the rival and the Farfetch'd to the Route 34 gate; its helpers move to tests/playthrough-driver.ts. New tests solve the Kabuto puzzle through the world, catch Unown and make Kurt's balls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
15a31f95e4
commit
f4ef244fc7
31 files changed
+2334
-528
No files matched your search
@@ -0,0 +1,74 @@
|
||||
import type { RomReader } from "./reader";
|
||||
import { JOHTO } from "./profile";
|
||||
|
||||
/** TEXTBOX_FRAME_TILES and NUM_FRAMES (constants/text_constants.asm). */
|
||||
const TEXTBOX_FRAME_TILES = 6,
|
||||
NUM_FRAMES = 8;
|
||||
/** The tiles LoadFrame and _LoadStandardFont fill: '┌' at $79, ' ' at
|
||||
* $7f and the font from $80. */
|
||||
export const FRAME_TILE = 0x79,
|
||||
SPACE_TILE = 0x7f,
|
||||
FONT_TILE = 0x80;
|
||||
|
||||
/** Get1bpp: both bit planes copy the 1bpp row, so set pixels are color 3. */
|
||||
function oneBpp(target: Uint8Array, tile: number, source: Uint8Array) {
|
||||
for (let row = 0; row < 8; row++) {
|
||||
target[tile * 16 + row * 2] = source[row];
|
||||
target[tile * 16 + row * 2 + 1] = source[row];
|
||||
}
|
||||
}
|
||||
|
||||
/** vTiles2 $79-$7f and vTiles1 as the text engine leaves them: the
|
||||
* textbox frame chosen in the options (LoadFrame), the space, and the
|
||||
* standard font (_LoadStandardFont). Indices are tile numbers. */
|
||||
export function fontTiles(reader: RomReader, frame: number) {
|
||||
const tiles = new Uint8Array(0x100 * 16);
|
||||
const frames = JOHTO.Frames + (frame & (NUM_FRAMES - 1)) * TEXTBOX_FRAME_TILES * 8;
|
||||
for (let i = 0; i < TEXTBOX_FRAME_TILES; i++)
|
||||
oneBpp(tiles, FRAME_TILE + i, reader.slice(frames + i * 8, 8));
|
||||
oneBpp(tiles, SPACE_TILE, reader.slice(JOHTO.TextboxSpaceGFX, 8));
|
||||
for (let i = 0; i < 128; i++)
|
||||
oneBpp(tiles, FONT_TILE + i, reader.slice(JOHTO.Font + i * 8, 8));
|
||||
return tiles;
|
||||
}
|
||||
|
||||
/** The characters these screens print, to their charmap codes
|
||||
* (constants/charmap.asm). */
|
||||
const CODES: Record<string, number> = {
|
||||
" ": 0x7f,
|
||||
"(": 0x9a,
|
||||
")": 0x9b,
|
||||
":": 0x9c,
|
||||
";": 0x9d,
|
||||
"[": 0x9e,
|
||||
"]": 0x9f,
|
||||
"'": 0xe0,
|
||||
"-": 0xe3,
|
||||
"?": 0xe6,
|
||||
"!": 0xe7,
|
||||
".": 0xe8,
|
||||
"&": 0xe9,
|
||||
"é": 0xea,
|
||||
"→": 0xeb,
|
||||
"▷": 0xec,
|
||||
"▶": 0xed,
|
||||
"▼": 0xee,
|
||||
"♂": 0xef,
|
||||
"¥": 0xf0,
|
||||
"×": 0xf1,
|
||||
"/": 0xf3,
|
||||
",": 0xf4,
|
||||
"♀": 0xf5,
|
||||
};
|
||||
/** Text as tile numbers, one character per tile. */
|
||||
export function textTiles(text: string): number[] {
|
||||
return [...text].map((character) => {
|
||||
const code = character.charCodeAt(0);
|
||||
if (character >= "A" && character <= "Z") return 0x80 + code - 65;
|
||||
if (character >= "a" && character <= "z") return 0xa0 + code - 97;
|
||||
if (character >= "0" && character <= "9") return 0xf6 + code - 48;
|
||||
const mapped = CODES[character];
|
||||
if (mapped === undefined) throw new Error(`No tile for "${character}".`);
|
||||
return mapped;
|
||||
});
|
||||
}
|
||||
+34
-8
@@ -3,6 +3,7 @@ import { decompress } from "./lz";
|
||||
import {
|
||||
BATTLE,
|
||||
INTRO,
|
||||
JOHTO,
|
||||
MENU,
|
||||
NUM_ITEMS,
|
||||
NUM_POKEMON,
|
||||
@@ -89,6 +90,10 @@ const STAT_TRIGGERS = ["gt", "lt", "eq"] as const;
|
||||
|
||||
/** Decoders for species, moves and items. All data is read from the
|
||||
* user's ROM on demand and cached in memory only. */
|
||||
/** UNOWN and NUM_UNOWN (constants/pokemon_constants.asm). */
|
||||
const UNOWN = 201;
|
||||
export const NUM_UNOWN = 26;
|
||||
|
||||
export class PokemonData {
|
||||
private base = new Map<number, BaseData>();
|
||||
private names = new Map<string, string>();
|
||||
@@ -389,11 +394,33 @@ export class PokemonData {
|
||||
return ((quadratic & 0xffffff) + cubic) & 0xffffff;
|
||||
}
|
||||
|
||||
/** The decompressed front picture: base frame, then animation tiles. */
|
||||
frontPicData(species: number): Uint8Array {
|
||||
/** GetFrontpicPointer's entry: PokemonPicPointers, or for Unown
|
||||
* UnownPicPointers by letter (1-26), at the same address in the next
|
||||
* bank. */
|
||||
private picEntry(species: number, letter: number) {
|
||||
this.checkSpecies(species);
|
||||
if (species !== UNOWN) return SYMBOLS.pokemonPics + (species - 1) * 6;
|
||||
if (!Number.isInteger(letter) || letter < 1 || letter > NUM_UNOWN)
|
||||
throw new Error("Unknown Unown letter.");
|
||||
return JOHTO.UnownPicPointers + (letter - 1) * 6;
|
||||
}
|
||||
/** GetUnownLetter: the middle two bits of each DV (Attack, Defense,
|
||||
* Speed, Special) make a byte, divided by $ff / NUM_UNOWN + 1, plus one. */
|
||||
static unownLetter(dvs: number): number {
|
||||
const high = dvs >> 8,
|
||||
low = dvs & 0xff;
|
||||
const value =
|
||||
((high & 0x60) << 1) |
|
||||
((high & 0x06) << 3) |
|
||||
((low & 0x60) >> 3) |
|
||||
((low & 0x06) >> 1);
|
||||
return Math.floor(value / (Math.floor(0xff / NUM_UNOWN) + 1)) + 1;
|
||||
}
|
||||
/** The decompressed front picture: base frame, then animation tiles.
|
||||
* Unown's depends on its letter. */
|
||||
frontPicData(species: number, letter = 1): Uint8Array {
|
||||
const r = this.reader,
|
||||
entry = SYMBOLS.pokemonPics + (species - 1) * 6;
|
||||
entry = this.picEntry(species, letter);
|
||||
// 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));
|
||||
@@ -414,10 +441,10 @@ export class PokemonData {
|
||||
}
|
||||
/** 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 {
|
||||
frontPic(species: number, letter = 1): Uint8Array {
|
||||
const size = this.baseData(species).picSize;
|
||||
if (size < 5 || size > 7) throw new Error("Invalid picture size.");
|
||||
const tiles = this.frontPicData(species);
|
||||
const tiles = this.frontPicData(species, letter);
|
||||
if (tiles.length < size * size * 16)
|
||||
throw new Error("Picture data is truncated.");
|
||||
const padded = new Uint8Array(49 * 16);
|
||||
@@ -430,10 +457,9 @@ export class PokemonData {
|
||||
return padded;
|
||||
}
|
||||
/** The back picture: 6x6 tiles, column by column (GetMonBackpic). */
|
||||
backPic(species: number): Uint8Array {
|
||||
this.checkSpecies(species);
|
||||
backPic(species: number, letter = 1): Uint8Array {
|
||||
const r = this.reader,
|
||||
entry = SYMBOLS.pokemonPics + (species - 1) * 6 + 3;
|
||||
entry = this.picEntry(species, letter) + 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);
|
||||
|
||||
@@ -186,6 +186,8 @@ export const EVENTS = {
|
||||
playersHouseBigDoll: 1860,
|
||||
wallOpenedInAerodactylChamber: 809,
|
||||
wallOpenedInKabutoChamber: 807,
|
||||
wallOpenedInHoOhChamber: 806,
|
||||
wallOpenedInOmanyteChamber: 808,
|
||||
} as const;
|
||||
|
||||
/** Engine flag indices named directly by translated routines. */
|
||||
@@ -1039,6 +1041,53 @@ export const OPTIONS = {
|
||||
"Options_MenuAccount.On": 0xe44f6,
|
||||
} as const;
|
||||
|
||||
/** Johto's scripts, routines and tables from Route 32 on, keyed by their
|
||||
* PRET labels for `npm run check:reference`. */
|
||||
export const JOHTO = {
|
||||
UnlockedUnownLetterSets: 0x3eba1,
|
||||
UnownPicPointers: 0x124000,
|
||||
"InitUnownPuzzlePiecePositions.PuzzlePieceInitialPositions": 0xe126d,
|
||||
"GetCurrentPuzzlePieceVTileCorner.Corners": 0xe148f,
|
||||
"CheckSolvedUnownPuzzle.SolvedPuzzleConfiguration": 0xe14b5,
|
||||
"RedrawUnownPuzzlePieces.OAM_HoldingPiece": 0xe150f,
|
||||
"RedrawUnownPuzzlePieces.OAM_NotHoldingPiece": 0xe1534,
|
||||
UnownPuzzleCoordData: 0xe1559,
|
||||
"PuzzlePieceBorderData.TileBordersGFX": 0xe1723,
|
||||
UnownPuzzleCursorGFX: 0xe17c5,
|
||||
UnownPuzzleStartCancelLZ: 0xe1805,
|
||||
HoOhPuzzleLZ: 0xe18ab,
|
||||
AerodactylPuzzleLZ: 0xe19fb,
|
||||
KabutoPuzzleLZ: 0xe1bab,
|
||||
OmanytePuzzleLZ: 0xe1c9b,
|
||||
Font: 0xf8200,
|
||||
Frames: 0xf8800,
|
||||
TextboxSpaceGFX: 0xf9204,
|
||||
UnownWalls: 0x8aebc,
|
||||
MenuHeaders_UnownWalls: 0x8aed5,
|
||||
UnownPuzzle: 0xc360,
|
||||
DisplayUnownWords: 0x8ae68,
|
||||
HoOhChamber: 0x8addb,
|
||||
OmanyteChamber: 0x8adef,
|
||||
ApricornBalls: 0x24ca0,
|
||||
SelectApricornForKurt: 0x88018,
|
||||
"Kurt_PrintTextWhichApricorn.WhichApricornText": 0x88007,
|
||||
"Kurt_PrintTextHowMany.HowManyShouldIMakeText": 0x88013,
|
||||
} as const;
|
||||
|
||||
/** Saved WRAM bytes that map scripts read and write directly (readmem,
|
||||
* writemem, loadmem and readvar), keyed by their PRET labels; the native
|
||||
* save keeps them by label in `memory`. */
|
||||
export const SAVED_WRAM = {
|
||||
wFarfetchdPosition: 0xd964,
|
||||
wKurtApricornQuantity: 0xdca4,
|
||||
} as const;
|
||||
export type SavedByte = keyof typeof SAVED_WRAM;
|
||||
/** Work RAM the native port reads by address but does not save, keyed by
|
||||
* PRET labels. */
|
||||
export const WRAM = {
|
||||
wTilemap: 0xc4a0,
|
||||
} as const;
|
||||
|
||||
export const MAP_GROUP_COUNT = 26;
|
||||
export const NUM_POKEMON = 251;
|
||||
export const NUM_TRAINER_CLASSES = 67;
|
||||
@@ -1083,5 +1132,41 @@ export const NATIVE_MAPS: Record<string, string> = {
|
||||
"3:1": "Sprout Tower 1F",
|
||||
"3:2": "Sprout Tower 2F",
|
||||
"3:3": "Sprout Tower 3F",
|
||||
"10:1": "Route 32",
|
||||
"10:12": "Ruins of Alph gate",
|
||||
"10:13": "Pokémon Center",
|
||||
"10:16": "Ruins of Alph gate",
|
||||
"3:22": "Ruins of Alph",
|
||||
"3:23": "Ho-Oh chamber",
|
||||
"3:24": "Kabuto chamber",
|
||||
"3:25": "Omanyte chamber",
|
||||
"3:26": "Aerodactyl chamber",
|
||||
"3:27": "Inner chamber",
|
||||
"3:28": "Research center",
|
||||
"3:29": "Ho-Oh item room",
|
||||
"3:30": "Kabuto item room",
|
||||
"3:31": "Omanyte item room",
|
||||
"3:32": "Aerodactyl item room",
|
||||
"3:33": "Ho-Oh word room",
|
||||
"3:34": "Kabuto word room",
|
||||
"3:35": "Omanyte word room",
|
||||
"3:36": "Aerodactyl word room",
|
||||
"3:37": "Union Cave 1F",
|
||||
"3:38": "Union Cave B1F",
|
||||
"3:39": "Union Cave B2F",
|
||||
"8:6": "Route 33",
|
||||
"8:7": "Azalea Town",
|
||||
"8:1": "Pokémon Center",
|
||||
"8:2": "Charcoal kiln",
|
||||
"8:3": "Azalea Mart",
|
||||
"8:4": "Kurt’s house",
|
||||
"8:5": "Azalea Gym",
|
||||
"3:40": "Slowpoke Well B1F",
|
||||
"3:41": "Slowpoke Well B2F",
|
||||
"11:22": "Ilex Forest gate",
|
||||
"3:52": "Ilex Forest",
|
||||
"11:23": "Route 34 gate",
|
||||
"11:1": "Route 34",
|
||||
"11:24": "Day-Care",
|
||||
};
|
||||
export const INITIAL_POSITION = { map: "24:7", x: 3, y: 3 } as const;
|
||||
Reference in new issue
Block a user