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

+143 -1
View File
@@ -14,7 +14,23 @@ const NEW_BARK = "24:4",
ROUTE_29 = "24:3",
CHERRYGROVE = "26:3",
ROUTE_30 = "26:1",
MR_POKEMONS_HOUSE = "26:10";
MR_POKEMONS_HOUSE = "26:10",
CHERRYGROVE_CENTER = "26:5",
ROUTE_31 = "26:2",
ROUTE_31_GATE = "26:11",
VIOLET = "10:5",
VIOLET_CENTER = "10:10",
VIOLET_GYM = "10:7",
SPROUT_1F = "3:1",
SPROUT_2F = "3:2",
SPROUT_3F = "3:3";
const EVENT_TALKED_TO_MOM_AFTER_MYSTERY_EGG_QUEST = 0x40,
EVENT_GOT_HM05_FLASH = 0x14,
EVENT_BEAT_FALKNER = 0x2bd,
EVENT_GOT_TM31_MUD_SLAP = 0x8,
EVENT_GOT_TOGEPI_EGG_FROM_ELMS_AIDE = 0x2d,
EVENT_ELMS_AIDE_IN_VIOLET_POKEMON_CENTER = 0x48c,
ENGINE_ZEPHYRBADGE = 0x1b;
/** The acceptance run for PLAN section 2: a new game played from the
* introduction to the catching tutorial through the world's own inputs,
@@ -196,6 +212,89 @@ describe.skipIf(!existsSync(romPath))(
expect(world.save()).toEqual(saved);
};
const lead = () => world.state.party[0];
/** PokecenterNurseScript from in front of the counter, then back out. */
const heal = (center: string, city: string) => {
enter(center, "up");
walk(3, 3);
world.move("up"); // the counter: this only turns the player
world.interact();
play();
expect(world.state.party.every((mon) => mon.egg || mon.hp === mon.stats[0])).toBe(true);
exit(city);
};
/** Pace between two neighbouring grass tiles (the nearest pair) until
* the lead reaches `level`. `rest` heals and comes back here. */
const train = (level: number, rest: () => void) => {
const map = world.mapId,
grass = world.assets.grassCollisions();
const isGrass = (x: number, y: number) =>
x >= 0 && y >= 0 && x < world.map.width && y < world.map.height &&
grass.includes(world.assets.collision(world.map, x, y));
for (let round = 0; round < 2000 && lead().level < level; round++) {
if (lead().hp * 2 < lead().stats[0]) {
rest();
expect(world.mapId).toBe(map);
}
const pairs: { x: number; y: number; length: number }[] = [];
for (let y = 0; y < world.map.height; y++)
for (let x = 0; x < world.map.width; x++)
if (isGrass(x, y) && isGrass(x + 1, y))
try {
pairs.push({ x, y, length: findPath(world, x, y).length });
} catch {
/* unreachable */
}
pairs.sort((a, b) => a.length - b.length);
const { x, y } = pairs[0];
walk(x, y);
walk(x + 1, y);
}
expect(lead().level).toBeGreaterThanOrEqual(level);
};
/** Take the stairs (or another warp) to `target`, trying each. */
const stairs = (target: string) => {
for (const warp of world.map.warps.filter((warp) => warp.targetMap === target)) {
try {
findPath(world, warp.x, warp.y);
} catch {
continue;
}
walk(warp.x, warp.y);
if (world.mapId === target) return;
}
throw new Error(`No way from ${world.mapId} to ${target}`);
};
/** Find a way through warps among `maps` to `target`, as a player
* trying staircases would: depth first, backing out of dead ends. */
const route = (target: string, maps: string[], depth = 6): boolean => {
if (world.mapId === target) return true;
if (!depth) return false;
const here = world.save();
for (const warp of world.map.warps) {
if (!maps.includes(warp.targetMap) && warp.targetMap !== target) continue;
try {
findPath(world, warp.x, warp.y);
} catch {
continue;
}
const from = world.mapId;
walk(warp.x, warp.y);
if (world.mapId !== from && route(target, maps, depth - 1)) return true;
world.restore(here);
}
return false;
};
/** From Route 31: through the gate to Violet's Pokémon Center and back. */
const restInViolet = () => {
exit(ROUTE_31_GATE, "left");
exit(VIOLET, "left");
heal(VIOLET_CENTER, VIOLET);
exit(ROUTE_31_GATE, "right");
exit(ROUTE_31, "right");
};
beforeEach(() => {
log.length = 0;
});
@@ -317,5 +416,48 @@ describe.skipIf(!existsSync(romPath))(
expect(save.rivalName).toBe("SILVER");
expect(save.intro).toBe(false);
});
it("trains and walks Route 30 and Route 31 to Violet City", () => {
traceFailures();
const restInCherrygrove = () => {
cross("left", CHERRYGROVE);
heal(CHERRYGROVE_CENTER, CHERRYGROVE);
cross("right", ROUTE_29);
};
heal(CHERRYGROVE_CENTER, CHERRYGROVE);
cross("right", ROUTE_29);
train(10, restInCherrygrove);
cross("left", CHERRYGROVE);
heal(CHERRYGROVE_CENTER, CHERRYGROVE);
cross("up", ROUTE_30);
cross("up", ROUTE_31);
exit(ROUTE_31_GATE, "left");
// Mom worries when the player has not come home since the Egg; the
// call rings on a step after Route 31's callback queued it.
expect(world.state.events.has(EVENT_TALKED_TO_MOM_AFTER_MYSTERY_EGG_QUEST), log.join("|")).toBe(true);
exit(VIOLET, "left");
expect(world.state.flags.size).toBeGreaterThan(0);
reload();
});
it("trains on Route 31 and earns Flash from Elder Li in Sprout Tower", () => {
traceFailures();
exit(ROUTE_31_GATE, "right");
exit(ROUTE_31, "right");
train(14, restInViolet);
restInViolet();
exit(ROUTE_31_GATE, "left");
exit(VIOLET, "left");
enter(SPROUT_1F, "up");
expect(route(SPROUT_3F, [SPROUT_1F, SPROUT_2F])).toBe(true);
walk(10, 3);
world.move("up"); // Elder Li: this only turns the player
world.interact();
play();
const pages = log.join("\n").replace(/\n/g, " ");
expect(pages).toContain("FLASH");
expect(world.state.events.has(EVENT_GOT_HM05_FLASH)).toBe(true);
reload();
});
},
);