Integrate battles with the overworld UI and validate browser flows
Checks / web (push) Canceled after 0s

- Drive wild battles through the real UI in Chromium and WebKit using a
  test-only encounter fixture: victory with level-up, escape, whiteout,
  mid-battle backup restore and unsupported-effect rollback.
- Add keyboard and on-screen D-pad navigation for battle menus.
- Accept pending stat experience in saves (awarded without CalcMonStats).
- Commit whiteout healing, money loss and destination callbacks together;
  aborted scripts restore state and map.
- Give long notices their own status-bar row on narrow screens.

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 16:37:07 +02:00
1 parent c08d250cf6
commit 11d1153802
11 files changed
+462 -13

No files matched your search

+47
View File
@@ -7,6 +7,7 @@ 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 { Button } from "./input/types";
import { NICKNAME_PATTERN } from "./game/party/pokemon";
import { GameSession } from "./app/session";
import { InputController } from "./input/controller";
@@ -179,6 +180,41 @@ function drawPic(target: HTMLCanvasElement, tiles: Uint8Array, size: number, pal
const battlePics = { foe: "", player: "" };
let battleMenuKey = "";
let showMoves = false;
let battleCursor = 0;
let battleSelectionKey = "";
function battleButtons() {
return [...get("battle-menu").querySelectorAll<HTMLButtonElement>("button:not(:disabled)")];
}
function highlightBattleChoice() {
battleButtons().forEach((button, index) => {
button.classList.toggle("battle-selected", index === battleCursor);
});
}
/** Keyboard and on-screen D-pad share the same menu buttons as pointer input. */
function handleBattleInput(button: Button): boolean {
const world = runtime.world;
if (!world?.battle) return false;
if (button === "b" && showMoves) {
showMoves = false;
return true;
}
const buttons = battleButtons();
if (!buttons.length) return false;
if (button === "a") {
buttons[battleCursor]?.click();
return true;
}
if (["up", "down", "left", "right"].includes(button)) {
const delta = button === "up" || button === "left" ? -1 : 1;
battleCursor = (battleCursor + delta + buttons.length) % buttons.length;
if (world.battle.prompt?.kind === "yesno")
world.battle.selected = battleCursor === 0 ? 1 : 0;
highlightBattleChoice();
return true;
}
return false;
}
/** The battle screen: pictures, HUDs, messages and the current choice. */
function showBattle(world: GameWorld) {
const view = world.battle;
@@ -187,6 +223,8 @@ function showBattle(world: GameWorld) {
if (!view) {
battlePics.foe = battlePics.player = battleMenuKey = "";
showMoves = false;
battleCursor = 0;
battleSelectionKey = "";
return;
}
const data = world.assets.pokemon;
@@ -221,6 +259,12 @@ function showBattle(world: GameWorld) {
}
get("battle-text").textContent = view.message?.text ?? "";
const prompt = view.prompt;
const selectionKey = `${prompt?.kind ?? "text"}:${showMoves}`;
if (selectionKey !== battleSelectionKey) {
battleCursor = 0;
battleSelectionKey = selectionKey;
}
if (prompt?.kind === "yesno") battleCursor = view.selected === 1 ? 0 : 1;
const key = JSON.stringify([prompt, view.selected, showMoves, own?.pp, view.message?.prompt]);
if (key === battleMenuKey) return;
battleMenuKey = key;
@@ -293,6 +337,8 @@ function showBattle(world: GameWorld) {
buttons.push(button("Continue ▶", () => runtime.advanceDialogue(), { wide: true }));
}
menu.replaceChildren(...buttons);
battleCursor = Math.min(battleCursor, Math.max(0, battleButtons().length - 1));
highlightBattleChoice();
}
function setHp(bar: HTMLElement, hp: number, max: number) {
const fraction = max ? hp / max : 0;
@@ -348,6 +394,7 @@ const runtime = new NativeRuntime(
? "kris"
: "chris",
}),
handleBattleInput,
);
const session = new GameSession(
runtime,