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:
kawaandClaude Opus 5.5 committed 2026-10-06 15:35:27 +02:00
1 parent 15a31f95e4
commit f4ef244fc7
31 files changed
+2334 -528

No files matched your search

+44 -4
View File
@@ -1,6 +1,6 @@
import { RomReader } from "../rom/reader";
import { BATTLE, RAM, SYMBOLS } from "../rom/profile";
import type { PokemonData } from "../rom/pokemon";
import { BATTLE, JOHTO, RAM, SYMBOLS } from "../rom/profile";
import { PokemonData } from "../rom/pokemon";
import type { Bag } from "../party/bag";
import {
MONS_PER_BOX,
@@ -15,7 +15,9 @@ import {
calcStats,
levelForExp,
maxPP,
updateUnownDex,
type PartyMon,
type UnownRecord,
} from "../party/pokemon";
import { readString, readTextEnd, type TextContext } from "../scripts/text";
import { BattleData } from "./data";
@@ -256,8 +258,33 @@ export interface BattleOptions {
/** wOptions' BATTLE_SCENE clear: battle animations on (the default).
* Some texts only show with them off (_CheckBattleScene). */
battleScene?: boolean;
/** wUnownDex and wFirstUnownSeen, which meeting and catching Unown
* update, and wUnlockedUnowns, the letter sets wild Unown may have. */
unown?: UnownRecord;
unlockedUnowns?: number;
}
/** CheckUnownLetter: whether an Unown with these DVs has a letter in one
* of the sets wUnlockedUnowns has unlocked (UnlockedUnownLetterSets, each
* a list of UNOWN_* letters ending with -1). */
export function unownUnlocked(
data: PokemonData,
unlocked: number,
dvs: number,
): boolean {
const r = data.reader,
letter = PokemonData.unownLetter(dvs),
table = JOHTO.UnlockedUnownLetterSets;
for (let set = 0; set < NUM_UNLOCKED_UNOWN_SETS; set++) {
if (!(unlocked & (1 << set))) continue;
for (let p = r.near(table + set * 2, r.bank(table)); r.byte(p) !== 0xff; p++)
if (r.byte(p) === letter) return true;
}
return false;
}
/** NUM_UNLOCKED_UNOWN_SETS (constants/ram_constants.asm). */
const NUM_UNLOCKED_UNOWN_SETS = 4;
/** BATTLERESULT_BOX_FULL: a catch filled the current box. */
const BATTLERESULT_BOX_FULL = 0x80;
const SUB1 = 1,
@@ -780,12 +807,17 @@ export class Battle {
this.enemy.stats = [...this.enemy.mon.stats];
this.enemySpecies = mon.species;
this.options.pokedex.seen.add(mon.species);
// LoadEnemyMonToSwitchTo: a trainer's Unown can be the first seen.
const unown = this.options.unown;
if (mon.species === C.UNOWN && unown && !unown.firstUnownSeen)
unown.firstUnownSeen = PokemonData.unownLetter(mon.dvs);
return;
}
if (!this.options.wild) throw new UnsupportedBattle("Missing encounter.");
const { species, level } = this.options.wild;
if (species === C.UNOWN)
throw new UnsupportedBattle("Unown encounters are not ported yet.");
const unown = this.options.unown;
if (species === C.UNOWN && !unown)
throw new UnsupportedBattle("Missing the Unown Dex.");
const data = this.options.data,
base = data.baseData(species);
// 25% chance of an item: 23% the first, 2% the second. Ho-Oh, Lugia
@@ -805,7 +837,13 @@ export class Battle {
dvs = roamer.dvs;
} else if (this.battleType === C.BATTLETYPE_FORCESHINY) dvs = 0xeaaa;
else dvs = (this.random() << 8) | this.random();
// CheckUnownLetter: a letter outside the unlocked sets draws again.
while (species === C.UNOWN && !unownUnlocked(data, this.options.unlockedUnowns ?? 0, dvs))
dvs = (this.random() << 8) | this.random();
while (species === C.MAGIKARP && this.rerollMagikarp(dvs)) dvs = (this.random() << 8) | this.random();
// InitEnemyWildmon: the first Unown met names wFirstUnownSeen.
if (species === C.UNOWN && !unown!.firstUnownSeen)
unown!.firstUnownSeen = PokemonData.unownLetter(dvs);
const stats = calcStats(data, species, dvs, level);
const moves = this.levelMoves(species, level);
this.enemySpecies = species;
@@ -1782,6 +1820,7 @@ export class Battle {
stats: calcStats(data, species, enemy.dvs, level),
};
this.options.party.push(mon);
if (this.options.unown) updateUnownDex(this.options.unown, mon);
mon.nickname = yield* this.askNickname(name);
this.tossItem("balls", item, slot);
return true;
@@ -1978,6 +2017,7 @@ export class Battle {
};
restorePP(data, mon);
box.unshift(mon);
if (this.options.unown) updateUnownDex(this.options.unown, mon, false);
if (box.length === MONS_PER_BOX) this.battleResult |= BATTLERESULT_BOX_FULL;
mon.nickname = yield* this.askNickname(name);
this.strings[RAM.monOrItemNameBuffer] = mon.nickname;