clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

Align stage touch targets with animated menus

Dami committed 3ac3f9d
commit
3ac3f9d138536aabe3625be7a6c2050b78b504aa
parent
02f34bc5524a63c09f08a6b44196c853fd52941f
author
Dami <dami.etoile@gmail.com>
3 files changed, +86 -29
Msrc/render/menu-motion.js11++++
Msrc/stage/events.js94+++++++++++++++++++++---------
Mtests/menu-motion.test.js10+++-
M src/render/menu-motion.js +11 -0
M src/stage/events.js +66 -28
@@ -1,4 +1,6 @@
11 import { overlayInputForKey } from "./dispatch.js";2+import { winPointerTarget } from "../render/stage-win.js";3+import { pausePointerTarget } from "../render/menu-motion.js";24 35 /**46 * Installs the stage's DOM event boundary. Gameplay input is still routed by
@@ -25,6 +27,7 @@ export function installStageEvents({
2527 getVisualNow,2628 getPauseMotionLayout,2729 dispatchOverlayIntent,30+ noteInput,2831 handleIntent,2932 show,3033 closeGift,
@@ -43,6 +46,7 @@ export function installStageEvents({
4346 if (["Escape", "Enter", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(event.key))4447 event.stopPropagation();4548 if (event.key === "Escape" || (scene === "pause" && event.key.toLowerCase() === "z")) {49+ noteInput?.({ type: "menu", source: "keyboard" });4650 dispatchOverlayIntent({ type: "menu" });4751 event.preventDefault();4852 return;
@@ -56,48 +60,59 @@ export function installStageEvents({
5660 return;5761 }5862 if (scene === "gift" && getGift()) {59- dispatchOverlayIntent({ type: overlayInputForKey(event.key) });63+ const type = overlayInputForKey(event.key);64+ noteInput?.({ type, source: "keyboard" });65+ dispatchOverlayIntent({ type });6066 event.preventDefault();6167 return;6268 }6369 if (scene === "workshop" && getWorkshop()) {64- dispatchOverlayIntent({ type: overlayInputForKey(event.key) });70+ const type = overlayInputForKey(event.key);71+ noteInput?.({ type, source: "keyboard" });72+ dispatchOverlayIntent({ type });6573 event.preventDefault();6674 return;6775 }6876 if (scene === "pause") {6977 if (event.key === "ArrowUp") {78+ noteInput?.({ type: "move", dir: "up", source: "keyboard" });7079 dispatchOverlayIntent({ type: "move", dir: "up" });7180 event.preventDefault();7281 return;7382 }7483 if (event.key === "ArrowDown") {84+ noteInput?.({ type: "move", dir: "down", source: "keyboard" });7585 dispatchOverlayIntent({ type: "move", dir: "down" });7686 event.preventDefault();7787 return;7888 }7989 if (event.key === "Enter") {90+ noteInput?.({ type: "confirm", source: "keyboard" });8091 dispatchOverlayIntent({ type: "confirm" });8192 event.preventDefault();8293 return;8394 }8495 }8596 if (scene === "index" && event.key === "Enter") {97+ noteInput?.({ type: "confirm", source: "keyboard" });8698 dispatchOverlayIntent({ type: "confirm" });8799 event.preventDefault();88100 return;89101 }90102 if (scene === "index" && ["ArrowRight", "ArrowDown"].includes(event.key)) {103+ noteInput?.({ type: "move", dir: event.key === "ArrowRight" ? "right" : "down", source: "keyboard" });91104 dispatchOverlayIntent({ type: "move", dir: event.key === "ArrowRight" ? "right" : "down" });92105 event.preventDefault();93106 return;94107 }95108 if (scene === "index" && ["ArrowLeft", "ArrowUp"].includes(event.key)) {109+ noteInput?.({ type: "move", dir: event.key === "ArrowLeft" ? "left" : "up", source: "keyboard" });96110 dispatchOverlayIntent({ type: "move", dir: event.key === "ArrowLeft" ? "left" : "up" });97111 event.preventDefault();98112 return;99113 }100114 if (scene === "pause" && event.key.toLowerCase() === "w") {115+ noteInput?.({ type: "workshop", source: "keyboard" });101116 show("workshop");102117 event.preventDefault();103118 }
@@ -107,6 +122,7 @@ export function installStageEvents({
107122 const scene = getScene();108123 if (event.target?.matches?.("input,textarea,select,[contenteditable]")) return;109124 if (event.key === " " && (scene === "index" || scene === "title")) {125+ noteInput?.({ type: "workshop", source: "keyboard" });110126 event.preventDefault();111127 event.stopImmediatePropagation();112128 show("workshop", { origin: scene });
@@ -119,6 +135,7 @@ export function installStageEvents({
119135 if (!direction) return;120136 event.preventDefault();121137 event.stopImmediatePropagation();138+ noteInput?.({ type: "move", dir: direction, source: "keyboard" });122139 dispatchOverlayIntent({ type: "move", dir: direction });123140 }124141
@@ -130,8 +147,10 @@ export function installStageEvents({
130147 const y = event.clientY - rect.top;131148 if (scene === "title") {132149 const titleHit = getTitle()?.hit?.(x, y);133- if (titleHit?.kind === "workshop" || titleHit?.kind === "skip") return;150+ if (titleHit?.kind === "workshop") { noteInput?.({ type: "workshop", source: "touch" }); return; }151+ if (titleHit?.kind === "skip") { noteInput?.({ type: "confirm", source: "touch" }); return; }134152 if (titleHit?.kind === "menu") {153+ noteInput?.({ type: "menu", source: "touch" });135154 show("pause");136155 return;137156 }
@@ -141,6 +160,7 @@ export function installStageEvents({
141160 const workshopHit = geometry.footerActions?.workshop;142161 if (workshopHit && x >= workshopHit.x && x <= workshopHit.x + workshopHit.width &&143162 y >= workshopHit.y && y <= workshopHit.y + workshopHit.height) {163+ noteInput?.({ type: "workshop", source: "touch" });144164 show("workshop", { origin: "index" });145165 render();146166 return;
@@ -151,6 +171,7 @@ export function installStageEvents({
151171 if (hit && navigator) {152172 clearIndexLockedNode();153173 if (navigator.current?.node?.id === hit.id) {174+ noteInput?.({ type: "confirm", source: "touch" });154175 const choice = navigator.enter();155176 if (choice?.ok && onSelectLevel?.(choice.node.id) !== false) show("proof");156177 } else {
@@ -161,51 +182,68 @@ export function installStageEvents({
161182 }162183 if (scene === "proof" && width <= 760) {163184 const hasActiveAutomata = getBoardState()?.automata?.some(system => system.active);164- if (hasActiveAutomata && y >= height - 92 && y < height - 50 && x >= width * 0.32 && x <= width * 0.68)185+ if (hasActiveAutomata && y >= height - 92 && y < height - 50 && x >= width * 0.32 && x <= width * 0.68) {186+ noteInput?.({ type: "wait", source: "touch" });165187 onWaitAction?.();166- else if (y >= height - 50 && x < width * 0.32) documentTarget.querySelector("#undo-btn")?.click();167- else if (y >= height - 50 && x >= width * 0.32 && x <= width * 0.68)188+ } else if (y >= height - 50 && x < width * 0.32) {189+ noteInput?.({ type: "undo", source: "touch" });190+ documentTarget.querySelector("#undo-btn")?.click();191+ } else if (y >= height - 50 && x >= width * 0.32 && x <= width * 0.68) {192+ noteInput?.({ type: "restart", source: "touch" });168193 documentTarget.querySelector("#restart-btn")?.click();169- else if (y >= height - 50 && x > width * 0.68) show("pause");194+ } else if (y >= height - 50 && x > width * 0.68) {195+ noteInput?.({ type: "menu", source: "touch" });196+ show("pause");197+ }170198 }171199 if (scene === "win") {172200 const winFlow = getWinFlow();173201 if (winFlow.exiting) return;174- const cardWidth = Math.min(620, width - 48);175- const cardX = (width - cardWidth) / 2;176- const cardY = Math.max(120, height * 0.25);177- if (x >= cardX && x <= cardX + cardWidth && y >= cardY && y <= cardY + 290) {178- const actionRow = y >= cardY + 220;179- if (!winFlow.view.winSkip && getVisualNow() - winFlow.view.winStart < winEntranceEnd) winFlow.confirm();180- else if (actionRow && (width <= 760 || windowTarget.matchMedia?.("(pointer: coarse)").matches)) {181- const slot = Math.min(2, Math.floor((x - cardX) / (cardWidth / 3)));182- if (slot === 1) documentTarget.querySelector("#undo-btn")?.click();183- else if (slot === 2) documentTarget.querySelector("#restart-btn")?.click();184- else winFlow.confirm();185- } else winFlow.confirm();202+ const coarsePointer = windowTarget.matchMedia?.("(pointer: coarse)").matches ?? false;203+ const target = winPointerTarget({ width, height, x, y, coarsePointer });204+ if (!target) return;205+ if (!winFlow.view.winSkip && getVisualNow() - winFlow.view.winStart < winEntranceEnd) {206+ winFlow.confirm();207+ } else if (target === "continue" || (target === "card" && !coarsePointer && width > 760)) {208+ winFlow.confirm();209+ } else if (target === "undo") {210+ documentTarget.querySelector("#undo-btn")?.click();211+ } else if (target === "replay") {212+ documentTarget.querySelector("#restart-btn")?.click();186213 }187214 }188215 if (scene === "gift") {189216 const result = getGift()?.hit?.(x, y);217+ if (result?.action === "details") noteInput?.({ type: "details", source: "touch" });218+ if (result?.action === "copy") noteInput?.({ type: "copy", source: "touch" });219+ if (result?.action === "keep") noteInput?.({ type: "confirm", source: "touch" });220+ if (result?.action === "later") noteInput?.({ type: "menu", source: "touch" });190221 if (result?.action === "later") closeGift();191222 if (result?.action === "details") announce(getGift()?.accessibilityText?.());192223 }193224 if (scene === "workshop") {194225 const outcome = getWorkshop()?.hit?.(x, y);226+ Promise.resolve(outcome).then(result => {227+ const type = result?.event?.type;228+ if (type === "back") noteInput?.({ type: "menu", source: "touch" });229+ else if (["import", "play", "share", "export"].includes(type)) noteInput?.({ type: "confirm", source: "touch" });230+ });195231 Promise.resolve(outcome).then(result => {196232 if (result?.event?.type === "notes") announce(getWorkshop()?.accessibilityText?.());197233 });198234 }199- if (scene === "pause" && getPauseMotionPhase() !== "exiting" &&200- x >= width / 2 - 160 && x <= width / 2 + 160) {201- const relativeY = y - height * 0.24;202- const layout = getPauseMotionLayout();203- const items = getPauseItems();204- const index = Math.floor(relativeY / layout.pitch);205- const withinTile = relativeY - index * layout.pitch;206- if (index >= 0 && index < items.length && withinTile >= 0 && withinTile <= layout.height) {207- setSelected(index);235+ if (scene === "pause" && getPauseMotionPhase() !== "exiting") {236+ const target = pausePointerTarget({237+ x, y, width, height, itemCount: getPauseItems().length,238+ layout: getPauseMotionLayout(),239+ });240+ if (typeof target === "number") {241+ setSelected(target);242+ noteInput?.({ type: "confirm", source: "touch" });208243 activatePauseSelection();244+ } else if (target === "outside") {245+ noteInput?.({ type: "menu", source: "touch" });246+ dispatchOverlayIntent({ type: "menu" });209247 }210248 }211249 render();
M tests/menu-motion.test.js +9 -1