Match the original's choice layouts: vertical YES/NO and lists, full PC menu [no-card]
YES/NO and battle lists are single columns moved with Up/Down, the battle menu cursor follows the 2x2 grid, and the dialogue menu shows every entry. Not pushed yet: awaiting manual testing (#64, #65). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ce8129703f
commit
5546e575f4
3 files changed
+79
-7
No files matched your search
+56
-3
@@ -143,6 +143,7 @@ function showChoice(choice: Choice | undefined) {
|
|||||||
const key = choice ? JSON.stringify(choice) : "";
|
const key = choice ? JSON.stringify(choice) : "";
|
||||||
if (container.dataset.choice === key) return;
|
if (container.dataset.choice === key) return;
|
||||||
container.dataset.choice = key;
|
container.dataset.choice = key;
|
||||||
|
container.dataset.kind = choice?.kind ?? "";
|
||||||
const button = (
|
const button = (
|
||||||
label: string,
|
label: string,
|
||||||
pressed: boolean | undefined,
|
pressed: boolean | undefined,
|
||||||
@@ -294,6 +295,10 @@ function showChoice(choice: Choice | undefined) {
|
|||||||
list,
|
list,
|
||||||
button("CANCEL", undefined, () => runtime.choose(-1)),
|
button("CANCEL", undefined, () => runtime.choose(-1)),
|
||||||
);
|
);
|
||||||
|
// On a small screen the box scrolls; keep the highlighted entry in view.
|
||||||
|
list
|
||||||
|
.querySelector('[aria-selected="true"]')
|
||||||
|
?.scrollIntoView({ block: "nearest" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** The naming screen as a text field and OK: an empty name keeps the
|
/** The naming screen as a text field and OK: an empty name keeps the
|
||||||
@@ -627,6 +632,42 @@ function battleButtons() {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
/** The enabled button next to `from` in the arrow's direction as drawn, so
|
||||||
|
* the cursor always follows the layout: a column moves with up and down, a
|
||||||
|
* row with left and right, and a grid with all four. */
|
||||||
|
function adjacentButton(
|
||||||
|
buttons: HTMLButtonElement[],
|
||||||
|
from: number,
|
||||||
|
arrow: "up" | "down" | "left" | "right",
|
||||||
|
): number | undefined {
|
||||||
|
const origin = buttons[from]?.getBoundingClientRect();
|
||||||
|
if (!origin) return undefined;
|
||||||
|
const vertical = arrow === "up" || arrow === "down";
|
||||||
|
let best: number | undefined;
|
||||||
|
let bestScore = Infinity;
|
||||||
|
buttons.forEach((candidate, index) => {
|
||||||
|
if (index === from) return;
|
||||||
|
const box = candidate.getBoundingClientRect();
|
||||||
|
const gap =
|
||||||
|
arrow === "up"
|
||||||
|
? origin.top - box.bottom
|
||||||
|
: arrow === "down"
|
||||||
|
? box.top - origin.bottom
|
||||||
|
: arrow === "left"
|
||||||
|
? origin.left - box.right
|
||||||
|
: box.left - origin.right;
|
||||||
|
if (gap < -1) return;
|
||||||
|
const offset = vertical
|
||||||
|
? Math.abs(box.left + box.width / 2 - (origin.left + origin.width / 2))
|
||||||
|
: Math.abs(box.top + box.height / 2 - (origin.top + origin.height / 2));
|
||||||
|
const score = Math.max(gap, 0) + offset * 2;
|
||||||
|
if (score < bestScore) {
|
||||||
|
bestScore = score;
|
||||||
|
best = index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return best;
|
||||||
|
}
|
||||||
function highlightBattleChoice() {
|
function highlightBattleChoice() {
|
||||||
battleButtons().forEach((button, index) => {
|
battleButtons().forEach((button, index) => {
|
||||||
button.classList.toggle("battle-selected", index === battleCursor);
|
button.classList.toggle("battle-selected", index === battleCursor);
|
||||||
@@ -664,9 +705,14 @@ function handleBattleInput(button: Button): boolean {
|
|||||||
buttons[battleCursor]?.click();
|
buttons[battleCursor]?.click();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (["up", "down", "left", "right"].includes(button)) {
|
if (
|
||||||
const delta = button === "up" || button === "left" ? -1 : 1;
|
button === "up" ||
|
||||||
battleCursor = (battleCursor + delta + buttons.length) % buttons.length;
|
button === "down" ||
|
||||||
|
button === "left" ||
|
||||||
|
button === "right"
|
||||||
|
) {
|
||||||
|
battleCursor =
|
||||||
|
adjacentButton(buttons, battleCursor, button) ?? battleCursor;
|
||||||
if (yesNoPrompt(world.battle.prompt))
|
if (yesNoPrompt(world.battle.prompt))
|
||||||
world.battle.selected = battleCursor === 0 ? 1 : 0;
|
world.battle.selected = battleCursor === 0 ? 1 : 0;
|
||||||
highlightBattleChoice();
|
highlightBattleChoice();
|
||||||
@@ -1012,6 +1058,13 @@ function showBattle(world: GameWorld) {
|
|||||||
button("Continue ▶", () => runtime.advanceDialogue(), { wide: true }),
|
button("Continue ▶", () => runtime.advanceDialogue(), { wide: true }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// FIGHT/PKMN/PACK/RUN is the original's 2x2 grid; every other choice is a
|
||||||
|
// vertical list, moved with Up and Down as it is drawn.
|
||||||
|
const grid =
|
||||||
|
(prompt?.kind === "action" && !showMoves && !showPack) ||
|
||||||
|
prompt?.kind === "stats";
|
||||||
|
menu.dataset.layout = grid ? "grid" : "list";
|
||||||
|
menu.dataset.kind = prompt?.kind ?? "";
|
||||||
menu.replaceChildren(...buttons);
|
menu.replaceChildren(...buttons);
|
||||||
battleCursor = Math.min(
|
battleCursor = Math.min(
|
||||||
battleCursor,
|
battleCursor,
|
||||||
|
|||||||
+18
-2
@@ -338,12 +338,18 @@ dialog h2 {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
/* The original's YES/NO box is vertical, YES above NO, moved with Up/Down. */
|
||||||
|
.native-dialogue > .dialogue-choice[data-kind="yesno"] {
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
/* Every entry shows (the bedroom PC has six). The dialogue box grows upward
|
||||||
|
and scrolls only when the screen is too small. */
|
||||||
.dialogue-menu {
|
.dialogue-menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
max-height: 9.5em;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
.dialogue-choice .mail-lines {
|
.dialogue-choice .mail-lines {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -452,6 +458,16 @@ dialog h2 {
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
/* Everything but the 2x2 battle menu is a vertical list, as in the original:
|
||||||
|
YES above NO, one move or item per row. */
|
||||||
|
.battle-menu[data-layout="list"] {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
.battle-menu[data-kind="yesno"] button,
|
||||||
|
.battle-menu[data-kind="contestswitch"] button:not(.wide) {
|
||||||
|
justify-self: end;
|
||||||
|
min-width: 6em;
|
||||||
|
}
|
||||||
.battle-menu button {
|
.battle-menu button {
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|||||||
@@ -126,10 +126,13 @@ test("keyboard and touch D-pad navigate battle moves, cancel and run", async ({
|
|||||||
await page.keyboard.press("KeyZ");
|
await page.keyboard.press("KeyZ");
|
||||||
await expect(menu.getByRole("button", { name: "FIGHT", exact: true })).toBeVisible();
|
await expect(menu.getByRole("button", { name: "FIGHT", exact: true })).toBeVisible();
|
||||||
// A real pointer press on the on-screen D-pad (synthetic events cannot be captured).
|
// A real pointer press on the on-screen D-pad (synthetic events cannot be captured).
|
||||||
|
// The menu is the original's 2x2 grid: FIGHT PKMN / PACK RUN.
|
||||||
await page.locator('[data-game-button="down"]').click();
|
await page.locator('[data-game-button="down"]').click();
|
||||||
await expect(menu.getByRole("button", { name: "PKMN", exact: true })).toHaveClass(/battle-selected/);
|
|
||||||
await page.keyboard.press("ArrowDown");
|
|
||||||
await expect(menu.getByRole("button", { name: "PACK", exact: true })).toHaveClass(/battle-selected/);
|
await expect(menu.getByRole("button", { name: "PACK", exact: true })).toHaveClass(/battle-selected/);
|
||||||
|
await page.keyboard.press("ArrowRight");
|
||||||
|
await expect(menu.getByRole("button", { name: "RUN", exact: true })).toHaveClass(/battle-selected/);
|
||||||
|
await page.keyboard.press("ArrowUp");
|
||||||
|
await expect(menu.getByRole("button", { name: "PKMN", exact: true })).toHaveClass(/battle-selected/);
|
||||||
await page.keyboard.press("ArrowDown");
|
await page.keyboard.press("ArrowDown");
|
||||||
await expect(menu.getByRole("button", { name: "RUN", exact: true })).toHaveClass(/battle-selected/);
|
await expect(menu.getByRole("button", { name: "RUN", exact: true })).toHaveClass(/battle-selected/);
|
||||||
await page.keyboard.press("KeyX");
|
await page.keyboard.press("KeyX");
|
||||||
|
|||||||
Reference in new issue
Block a user