diff --git a/docs/DESIGN.md b/docs/DESIGN.md index f13fa96b..6162ba33 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -662,7 +662,7 @@ Thirteen reports after playing v2.2. Where each is handled: #1 picking should si - **One rule**: the ground tile under the pointer decides (`render/projection.js pickTile`, raised tops first). Prep presses / clicks / right-click / long-press / drag starts / hover (`app.js pieceAt`) and the enemy preview pen (`penUnitAt`) take the unit standing on that tile (board, bench row 7, temp row 8; `pick.js pickOnTile`). An empty tile selects nothing; a press on a unit's head drawn over the tile behind it is a press on that tile; a raised tile in front hides the near 0.17–0.24 tile of the one behind it, and a press there is on the raised tile — what is drawn. Several units on one tile (the pen's clusters, a piece held for a moment): the nearest to the pointer's point on the tile, ties → the front-most. - **Battle** (`app.js battleUnitAt` → `pickBattle`): an ally on the tile under the pointer — a knocked-out operator lying on its tile (§18.3) included. An enemy within `ENEMY_REACH` 0.6 tile [ASSUMED], using whichever is nearer: its ground position from the pointer's point on the ground, or its drawn body on screen (the upright line from its feet up to its head, in its px per tile) — so a press on a tall enemy's or the boss's torso or head picks it; a flying enemy uses its drawn body only. When an ally tile and an enemy both qualify the nearer (in tiles) wins; ties → the ally, then the front-most. Dead and dying units are not picked (a knocked-out operator waiting on its tile is). - **Drag**: the dragged unit is held under the pointer, mouse and touch alike — its drawn (lifted) feet `DRAG_HOLD_TILES` = 0.45 tile (in its own px per tile) below the pointer [ASSUMED: the v2.1 offset], so the pointer rests on the chibi; an item plate is centred on the pointer. The drop target, its highlight, `tileHover` and the direction wheel's tile are the tile under the pointer. An item dropped on a tile equips the unit on that tile (`ui/gameLogic.js canPlace` / `dropIntent`; an empty tile says 请将装备拖拽至干员身上). A release over DOM covering the canvas (the shop bar) is `'outside'`: the piece goes back. -- **Touch**: the field canvas cancels `touchend` (`app.js onTouchEnd`, non-passive). A tap's compatibility mouse events and click came after `touchend`, hit-tested at the finger again — on the underframe the tap had just opened over the unit's tile (clamped under the top bar for rows 11–12 at 844×390), which pressed 撤退 / 出售. +- **Touch**: the field canvas cancels `touchend` (`app.js onTouchEnd`, non-passive). A tap's compatibility mouse events and click came after `touchend`, hit-tested at the finger again — on the underframe the tap had just opened over the unit's tile (clamped under the top bar for rows 11–12 at 844×390), which pressed 撤退 / 出售. The canvas is also a click target of its own (a no-op `click` listener, `app.js onTapTarget`; 0.1.4): the browser's touch adjustment moves a tap onto a nearby element that responds to clicks (pointer listeners do not count) when the finger's contact area reaches one, and once the 收起 toggle of PR #149 (§10) pushed the bond discs one button to the right, a tap on the row-12 unit at 844×390 opened the bond popup. Holding the finger's point, the canvas wins: a tap on the board stays on its tile. - **Notched phones**: the shop bar keeps its place at the bottom edge instead of rising by the bottom safe-area inset (`css/devices.css .gm__hud > .shopbar`): raised by it the bar covered ~3/4 of bench pads 2–9 at 844×390 with the iPhone insets, and bench pieces are pressed and dropped on by their tile. (The v2.3 figures — 23 % of those tiles free before, 71 % after — are superseded by §19.8: the prep camera now clears the HUD, every bench pad 100 % free on phones.) - Removed: the pixel probe (`drawnAt`, `hoverProbeAt`), body shapes (`pickShape`, `PROBE_MARGIN`, `BODY_H`), `TOUCH_LIFT_TILES`, the drag `dropPoint` hook, `pieceDragOver`, `ui/gameLogic.js pickPieceAt` / `equipRetarget`. `UnitView.bounds()` is a plain body rectangle again (tooltips and `pieceScreenRect` only). - Tests: `test/render/pick.test.js`, `drag.test.js`, `unitview.test.js`, `test/ui/gameLogic.test.js`, `test/ui/playtest4.test.js` (shop bar); browser: `test/render/models.browser.test.js` (every tile of every board, high ground, bench and temp; battle allies and enemies incl. torso and head; the model under the pointer at every drag step; the wheel on the pointer's tile; equipment by tile; phone touch incl. the ghost click and the shop-bar release), `test/render/downelem.browser.test.js` (a knocked-out operator picked on its tile), `test/ui/playtest2.e2e.test.js` (the top-row tile under the bond strip; the notched phone's bench). diff --git a/public/js/render/app.js b/public/js/render/app.js index 83d7b249..0e9aeaa2 100644 --- a/public/js/render/app.js +++ b/public/js/render/app.js @@ -1404,6 +1404,12 @@ export async function createFieldView(host, options = {}) { // selects it and its underframe opens over the tile (clamped under the top bar on a phone), and the click pressed // 撤退 / 出售 (user playtest #4 item 1 on a phone). Cancelling touchend drops them. const onTouchEnd = (e) => { if (e.cancelable) e.preventDefault(); }; + // The canvas is a click target too (a no-op listener). The browser's touch adjustment moves a tap onto a nearby + // element that responds to clicks (click / mousedown listeners, buttons, links; pointer listeners do not count) when the + // finger's contact area reaches one, so a tap on the back row right under the bond strip's discs (row 12 at 844×390 once + // the 收起 toggle of PR #149 moved the discs one button to the right) opened the bond popup instead of selecting the + // unit. As a click target that holds the finger's point the canvas wins: a tap on the board stays on the tile under it. + const onTapTarget = () => {}; canvas.addEventListener('pointerdown', onPointerDown); canvas.addEventListener('pointermove', onPointerMove); canvas.addEventListener('pointerup', onPointerUp); @@ -1411,6 +1417,7 @@ export async function createFieldView(host, options = {}) { canvas.addEventListener('pointerleave', onPointerLeave); canvas.addEventListener('contextmenu', onContext); canvas.addEventListener('touchend', onTouchEnd, { passive: false }); + canvas.addEventListener('click', onTapTarget); // ---- battle --------------------------------------------------------------------------------------------- @@ -2006,6 +2013,7 @@ export async function createFieldView(host, options = {}) { canvas.removeEventListener('pointerleave', onPointerLeave); canvas.removeEventListener('contextmenu', onContext); canvas.removeEventListener('touchend', onTouchEnd); + canvas.removeEventListener('click', onTapTarget); app.ticker.remove(frame); app.ticker.remove(preRender); app.ticker.remove(postRender); diff --git a/test/ui/playtest2.test.js b/test/ui/playtest2.test.js index ef9d8c2f..8b5e2f16 100644 --- a/test/ui/playtest2.test.js +++ b/test/ui/playtest2.test.js @@ -119,6 +119,17 @@ describe('8: the detail card never covers the selected unit\'s underframe', () = assert.match(panels, /\.uframe__label\s*\{[^}]*white-space:\s*nowrap\s*;/, 'shortcut labels stay on one line'); }); + // a finger's tap near a disc: the browser's touch adjustment moved it onto the nearest element that responds to clicks + // (the canvas's pointer listeners do not count), so with PR #149's 收起 toggle pushing the discs over the back row a tap + // on the row-12 unit at 844×390 opened the bond popup (test/render/models.browser.test.js #4.1 touch) + test('the field canvas is a click target of its own, so a tap on the board stays on the tile under the finger', async () => { + const { readFileSync } = await import('node:fs'); + const app = readFileSync(new URL('../../public/js/render/app.js', import.meta.url), 'utf8'); + assert.match(app, /\n {2}canvas\.addEventListener\('click', onTapTarget\);\n/, 'registered with the other canvas listeners'); + assert.match(app, /\n {6}canvas\.removeEventListener\('click', onTapTarget\);\n/, 'dropped on destroy'); + assert.match(app, /const onTapTarget = \(\) => \{\};/, 'a no-op: the press itself stays with the pointer events'); + }); + test('underframeRect covers the diamond and its buttons', () => { const r = underframeRect({ x: 500, y: 600, s: 100 }, 100); assert.ok(r.left <= 500 - 105 && r.right >= 500 + 105 && r.bottom === 600 + 105);