- 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:
1 parent
11d1153802
commit
796fe8f6d5
24 files changed
+1549
-59
No files matched your search
+113
-5
@@ -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"],
|
||||
|
||||
Reference in new issue
Block a user