Open Route 31, Violet City and Sprout Tower (#31)

Enable Route 31 and its gate, Dark Cave's Violet entrance, Violet City with its buildings and Sprout Tower. Port the script commands they use: gettrainername, askforphonenumber (with AddPhoneNumber's room for Mom and Elm; addcellnum now reports failure as the original does), checkpokemail, loadmenu/_2dmenu/closewindow, giveegg and trade (NPCTrade, with wTradeFlags saved).

Eggs keep their species with an egg flag, as wPartySpecies does. The pack's TM/HM counts receive, check and toss TMs and HMs, which are named TM01-HM07.

The playthrough continues through Route 31 (with Mom's call), Violet City and Sprout Tower to Elder Li's Flash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-10-03 16:24:29 +02:00
1 parent ec3d1cd5c4
commit 7909302db1
16 files changed
+585 -63

No files matched your search

+50 -1
View File
@@ -11,6 +11,32 @@ import {
SYMBOLS,
} from "./profile";
import { defaultTextContext, readString } from "../scripts/text";
/** The TM and HM items (constants/item_constants.asm): TM01-TM50, then
* HM01-HM07, skipping the dummy ITEM_C3 and ITEM_DC. */
export const TM01 = 0xbf,
HM01 = 0xf3,
NUM_TMS = 50,
NUM_HMS = 7,
NUM_TMS_HMS = NUM_TMS + NUM_HMS;
const ITEM_C3 = 0xc3,
ITEM_DC = 0xdc;
/** GetTMHMNumber: TM01 is 1, HM01 is 51. */
export function tmhmNumber(item: number): number {
let value = item;
if (item >= ITEM_C3) value--;
if (item >= ITEM_DC) value--;
return value - TM01 + 1;
}
/** GetNumberedTMHM: the item for a TM/HM number. */
export function tmhmItem(number: number): number {
let item = number + TM01 - 1;
if (item >= ITEM_C3) item++;
if (item >= ITEM_DC) item++;
return item;
}
export const isTMHM = (item: number) =>
item >= TM01 && item < HM01 + NUM_HMS && item !== ITEM_C3 && item !== ITEM_DC;
import type { Palette } from "../world/types";
/** data/pokemon/base_stats: one 32-byte record per species. */
@@ -131,10 +157,33 @@ export class PokemonData {
}
itemName(item: number): string {
this.checkItem(item);
if (isTMHM(item)) {
// GetTMHMName: "TM" or "HM" and two digits; HMs count from 1.
const number = tmhmNumber(item),
hm = item >= HM01;
return `${hm ? "HM" : "TM"}${String(hm ? number - NUM_TMS : number).padStart(2, "0")}`;
}
return this.cachedName(`item:${item}`, () =>
this.listName(SYMBOLS.itemNames, item),
);
}
/** GetTMHMMove: the move a TM/HM number teaches. */
tmhmMove(number: number): number {
if (!Number.isInteger(number) || number < 1 || number > NUM_TMS_HMS)
throw new Error("Unknown TM or HM.");
return this.reader.byte(SYMBOLS.tmhmMoves + number - 1);
}
/** CanLearnTMHMMove: the move's place in TMHMMoves (with the tutor
* moves after the HMs) as a bit of the species' base TM/HM flags. */
canLearnTMHM(species: number, move: number): boolean {
this.checkSpecies(species);
const r = this.reader;
let index = 0;
for (let p = SYMBOLS.tmhmMoves; r.byte(p); p++, index++)
if (r.byte(p) === move)
return Boolean(r.byte(SYMBOLS.baseData + (species - 1) * 32 + 24 + (index >> 3)) & (1 << (index & 7)));
return false;
}
private cachedName(key: string, read: () => string) {
let name = this.names.get(key);
if (name === undefined) {
@@ -144,7 +193,7 @@ export class PokemonData {
return name;
}
private checkItem(item: number) {
if (!Number.isInteger(item) || item < 1 || item > NUM_ITEMS)
if (!Number.isInteger(item) || ((item < 1 || item > NUM_ITEMS) && !isTMHM(item)))
throw new Error("Unknown item.");
}
/** ItemAttributes: price, held effect, parameter, property, pocket, menus. */
+20
View File
@@ -57,6 +57,8 @@ export const SYMBOLS = {
pocketIsFullText: 0x97060,
findItemInBallScript: 0x122ce,
caughtAskNicknameText: 0x4db44,
stringEgg: 0xe035,
tmhmMoves: 0x1167a,
nicknameStrings: 0x11780,
rivalNameString: 0x117c3,
rivalDefaultName: 0xc2b2,
@@ -194,6 +196,11 @@ export const ENGINE_FLAGS = {
/** Overworld scripts, routines and tables, keyed by their PRET labels so
* that `npm run check:reference` verifies each address by name. */
export const OVERWORLD = {
NPCTrades: 0xfce58,
TradeTexts: 0xfcf53,
NPCTradeCableText: 0xfcf7b,
TradedForText: 0xfcf80,
"TradedForText.done": 0xfcf92,
BugCatchingContestBattleScript: 0x135eb,
ContestMons: 0x97d87,
"OverworldTownMap": 0xc2c0,
@@ -1028,5 +1035,18 @@ export const NATIVE_MAPS: Record<string, string> = {
"26:8": "Evolution speech house",
"26:9": "Berry house",
"26:10": "Mr. Pokémon’s house",
"26:2": "Route 31",
"26:11": "Route 31 Violet gate",
"3:78": "Dark Cave",
"10:5": "Violet City",
"10:6": "Violet Mart",
"10:7": "Violet Gym",
"10:8": "Earl’s Pokémon Academy",
"10:9": "Nickname speech house",
"10:10": "Pokémon Center",
"10:11": "Kyle’s house",
"3:1": "Sprout Tower 1F",
"3:2": "Sprout Tower 2F",
"3:3": "Sprout Tower 3F",
};
export const INITIAL_POSITION = { map: "24:7", x: 3, y: 3 } as const;