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:
1 parent
ec3d1cd5c4
commit
7909302db1
16 files changed
+585
-63
No files matched your search
@@ -18,7 +18,7 @@ const NEW_BARK_TOWN = "24:4",
|
||||
DARK_CAVE_VIOLET_ENTRANCE = "3:78";
|
||||
const SPAWN_NEW_BARK = 14,
|
||||
SPAWN_CHERRYGROVE = 15,
|
||||
SPAWN_VIOLET = 16;
|
||||
SPAWN_AZALEA = 18;
|
||||
const ENGINE_STORMBADGE = 32,
|
||||
EVENT_TALKED_TO_MOM_AFTER_MYSTERY_EGG_QUEST = 64;
|
||||
const FLY = 0x13,
|
||||
@@ -203,10 +203,11 @@ describe.skipIf(!existsSync(romPath))("Fly, Dig, Teleport and the Escape Rope",
|
||||
it("refuses to fly to a flypoint that is not ported", () => {
|
||||
const { world } = fixture([FLY], ROUTE_29, 50, 10);
|
||||
world.state.flags.add(ENGINE_STORMBADGE);
|
||||
visit(world, SPAWN_NEW_BARK, SPAWN_CHERRYGROVE, SPAWN_VIOLET);
|
||||
visit(world, SPAWN_NEW_BARK, SPAWN_CHERRYGROVE, SPAWN_AZALEA);
|
||||
useMove(world, "FLY");
|
||||
world.moveCursor("down"); // wraps to Violet City
|
||||
expect(flyMap(world)?.name).toBe("VIOLET CITY");
|
||||
for (let press = 0; press < 4 && flyMap(world)?.name !== "AZALEA TOWN"; press++)
|
||||
world.moveCursor("down");
|
||||
expect(flyMap(world)?.name).toBe("AZALEA TOWN");
|
||||
world.choose("a");
|
||||
until(world);
|
||||
expect(world.notice).toBe("This destination is not ported yet.");
|
||||
|
||||
+3
-3
@@ -15,8 +15,7 @@ const romPath =
|
||||
"Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc";
|
||||
const POKEMON_CENTER = "26:5",
|
||||
CYNDAQUIL = 155,
|
||||
PIDGEY = 16,
|
||||
EGG = 0xfd;
|
||||
PIDGEY = 16;
|
||||
const POTION = 0x12,
|
||||
BERRY = 0xad,
|
||||
FLOWER_MAIL = 0x9e,
|
||||
@@ -282,7 +281,8 @@ describe.skipIf(!existsSync(romPath))("MAIL", () => {
|
||||
];
|
||||
world.state.party.push({
|
||||
...structuredClone(world.state.party[1]),
|
||||
species: EGG,
|
||||
egg: true,
|
||||
nickname: "EGG",
|
||||
});
|
||||
mailbox(world);
|
||||
expect(menuItems(world)).toEqual([
|
||||
|
||||
@@ -517,14 +517,16 @@ describe.skipIf(!existsSync(romPath))(
|
||||
load(),
|
||||
() => new Date("2026-09-22T12:00:00"),
|
||||
);
|
||||
const route = world.assets.map("26:1");
|
||||
const edge = Array.from({ length: route.width }, (_, x) => x).find(
|
||||
// Violet City's south edge leads to Route 32, which stays closed.
|
||||
const city = world.assets.map("10:5"),
|
||||
bottom = city.height - 1;
|
||||
const edge = Array.from({ length: city.width }, (_, x) => x).find(
|
||||
(x) =>
|
||||
world.assets.permission(world.assets.collision(route, x, 0)) === 0,
|
||||
world.assets.permission(world.assets.collision(city, x, bottom)) === 0,
|
||||
)!;
|
||||
world.restore({ ...world.save(), map: "26:1", x: edge, y: 0 });
|
||||
expect(world.move("up")).toBe(false);
|
||||
expect(world.notice).toBe("Route 31 is not ported yet.");
|
||||
world.restore({ ...world.save(), map: "10:5", x: edge, y: bottom });
|
||||
expect(world.move("down")).toBe(false);
|
||||
expect(world.notice).toBe("Route 32 is not ported yet.");
|
||||
const {
|
||||
scenes,
|
||||
phone,
|
||||
|
||||
+143
-1
@@ -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();
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -316,7 +316,7 @@ describe.skipIf(!existsSync(romPath))("the start menu", () => {
|
||||
it("shows the other party members' moves with left and right, skipping Eggs", () => {
|
||||
const world = fixture();
|
||||
const party = world.state.party;
|
||||
party.push({ ...structuredClone(party[1]), species: 0xfd }); // EGG
|
||||
party.push({ ...structuredClone(party[1]), egg: true, nickname: "EGG" });
|
||||
open(world);
|
||||
pick(world, "POKéMON");
|
||||
pick(world, "CYNDAQUIL");
|
||||
|
||||
Reference in new issue
Block a user