Open Route 29 and Cherrygrove City
Checks / web (push) Canceled after 0s

- Enable Route 29, its Route 46 gate, Cherrygrove City and its buildings.
- Run item balls through FindItemInBallScript and fruit trees through
  FruitTreeScript, with a native daily reset of the daily engine flags.
- Port the Pokémon Center nurse (HealParty with the PP Up clamp,
  CheckPokerus, phone-call checks) and record the whiteout spawn on
  entering a Pokémon Center, as EnterMapWarp does.
- Port standard Poké Marts with keyboard, D-pad and pointer menus.
- Add the recoil battle command (Struggle) and VAR_BOXSPACE.
- Save fruit trees, the daily reset day and the last spawn map; older
  version-3 backups migrate by absence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-09-23 17:25:02 +02:00
1 parent 11d1153802
commit 796fe8f6d5
24 files changed
+1549 -59

No files matched your search

+113 -5
View File
@@ -6,7 +6,7 @@ import { LocalMapView } from "./game/rendering/minimap";
import type { Choice, GameWorld } from "./game/world/world";
import { drawTile } from "./game/rendering/pixels";
import { PokemonData } from "./game/rom/pokemon";
import type { Palette } from "./game/world/types";
import type { Direction, Palette } from "./game/world/types";
import type { Button } from "./input/types";
import { NICKNAME_PATTERN } from "./game/party/pokemon";
import { GameSession } from "./app/session";
@@ -52,7 +52,8 @@ function showWorld(world: GameWorld) {
const pictureWait = Boolean(
dialogue && !dialogue.choice && dialogue.pages[dialogue.index] === "",
);
get("native-dialogue").hidden = !dialogue || pictureWait;
const mart = dialogue?.choice && martChoice(dialogue.choice);
get("native-dialogue").hidden = !dialogue || pictureWait || Boolean(mart);
get("pokepic-next").hidden = !pictureWait;
get("dialogue-text").textContent = dialogue?.pages[dialogue.index] ?? "";
get("dialogue-page").textContent =
@@ -60,12 +61,13 @@ function showWorld(world: GameWorld) {
? `${dialogue.index + 1} / ${dialogue.pages.length}`
: "";
const last = dialogue && dialogue.index === dialogue.pages.length - 1;
const choice = last ? dialogue.choice : undefined;
const choice = last && !mart ? dialogue.choice : undefined;
// Timed pages advance themselves; choices replace Continue.
get("dialogue-next").hidden = Boolean(
choice || (last && dialogue.holdTicks !== undefined),
choice || mart || (last && dialogue.holdTicks !== undefined),
);
showChoice(choice);
showMart(world, mart);
// Coordinates provide a stable accessible description and a test observation
// point, without exposing mutable game state or a production debug API.
canvas.dataset.map = world.mapId;
@@ -125,7 +127,7 @@ function showChoice(choice: Choice | undefined) {
);
container.firstElementChild?.setAttribute("aria-label", "Previous day");
container.children[2]?.setAttribute("aria-label", "Next day");
} else {
} else if (choice.kind === "name") {
// The naming screen: an empty name keeps the current one.
const input = document.createElement("input");
input.maxLength = choice.max;
@@ -159,6 +161,112 @@ function showChoice(choice: Choice | undefined) {
queueMicrotask(() => input.focus({ preventScroll: true }));
}
}
type MartChoice = Extract<
Choice,
{ kind: "martmenu" | "martlist" | "martquantity" }
>;
function martChoice(choice: Choice): MartChoice | undefined {
return choice.kind === "martmenu" ||
choice.kind === "martlist" ||
choice.kind === "martquantity"
? choice
: undefined;
}
let martMenuKey = "";
/** The mart's BUY/SELL/QUIT box, item list or quantity stepper, plus a
* money display. Follows the battle menu's button-grid style. */
function showMart(world: GameWorld, mart: MartChoice | undefined) {
get("mart").hidden = !mart;
if (!mart) {
martMenuKey = "";
return;
}
get("mart-money").textContent = `MONEY ¥${world.liveState.money}`;
get("mart-prompt").textContent = mart.kind === "martlist" ? "" : mart.prompt;
const key = JSON.stringify(mart);
if (key === martMenuKey) return;
martMenuKey = key;
const menu = get("mart-menu");
const button = (
label: string,
action: () => void,
options: { detail?: string; selected?: boolean; wide?: boolean } = {},
) => {
const element = document.createElement("button");
element.type = "button";
element.textContent = label;
if (options.detail) {
const small = document.createElement("small");
small.textContent = options.detail;
element.append(small);
}
if (options.selected) element.classList.add("battle-selected");
if (options.wide) element.classList.add("wide");
element.addEventListener("click", () => {
if (session.state !== "playing") return;
action();
canvas.focus({ preventScroll: true });
});
return element;
};
if (mart.kind === "martmenu") {
menu.replaceChildren(
...(["BUY", "SELL", "QUIT"] as const).map((label, index) =>
button(label, () => runtime.choose(index), {
selected: mart.selected === index,
}),
),
);
return;
}
if (mart.kind === "martlist") {
const buttons = mart.entries.map((entry, index) =>
button(entry.name, () => runtime.choose(index), {
detail:
mart.mode === "sell" ? `have ${entry.owned}` : `¥${entry.price}`,
selected: mart.selected === index,
}),
);
buttons.push(button("BACK", () => runtime.choose(-1), { wide: true }));
menu.replaceChildren(...buttons);
return;
}
// martquantity: a 1-99 stepper (±1 Up/Down, ±10 Left/Right, matching
// BuySellToss_InterpretJoypad), then BUY/SELL and CANCEL.
const total =
mart.mode === "sell"
? Math.floor((mart.price * mart.selected) / 2)
: mart.price * mart.selected;
const stepper = document.createElement("div");
stepper.className = "mart-quantity";
const step = (label: string, aria: string, direction: Direction) => {
const element = document.createElement("button");
element.type = "button";
element.textContent = label;
element.setAttribute("aria-label", aria);
element.addEventListener("click", () => {
if (session.state !== "playing") return;
runtime.world?.moveCursor(direction);
if (runtime.world) showWorld(runtime.world);
canvas.focus({ preventScroll: true });
});
return element;
};
const output = document.createElement("output");
output.textContent = `${mart.name} ×${mart.selected} ¥${total}`;
stepper.append(
step("◀◀", "Minus 10", "left"),
step("▼", "Minus 1", "down"),
output,
step("▲", "Plus 1", "up"),
step("▶▶", "Plus 10", "right"),
);
menu.replaceChildren(
stepper,
button(mart.mode === "sell" ? "SELL" : "BUY", () => runtime.choose(mart.selected), { wide: true }),
button("CANCEL", () => runtime.choose(-1), { wide: true }),
);
}
const STATUS_NAMES: [number, string][] = [
[0x08, "PSN"],
[0x10, "BRN"],