feedback7 R23-DRAGSNAP: a dragged operator stands on the tile under the pointer while that is a legal target (a board tile or a bench slot: render/drag.js dragStandTile), as in the official deploy drag (the owner's recording of 2026-10-09), and is held under the pointer elsewhere (an illegal tile, the shop bar); the pick, highlight, tileHover, wheel and drop stay the pointer's tile — the report in PR #403 by @bubububaoshe (not merged); §27.62; golden: unchanged
This commit is contained in:
1 parent
9bacd9ad45
commit
9405aae60c
11 files changed
+228
-53
No files matched your search
@@ -158,7 +158,7 @@ npm start # 启动服务器:http://localhost:3000
|
||||
| 操作 | 方法 |
|
||||
|---|---|
|
||||
| 购买 / 升级调度中心 / 机变选卡 | 点一次选中,再点一次确认(`D` 升级) |
|
||||
| 部署 / 移动干员 | 从整备区拖到棋盘格 → 出现方向轮盘 → 往上 / 右 / 下 / 左滑动选择朝向后松手;松在中心或点「✕ 点击取消」取消。拖动时模型在指针 / 手指下,指针所在的格子就是落点 |
|
||||
| 部署 / 移动干员 | 从整备区拖到棋盘格 → 出现方向轮盘 → 往上 / 右 / 下 / 左滑动选择朝向后松手;松在中心或点「✕ 点击取消」取消。拖动时指针 / 手指所在的格子就是落点,能放下时模型直接站在这一格上(不能放下时跟在指针下) |
|
||||
| 调整朝向 | 把干员拖回它自己的格子,再选方向 |
|
||||
| 出售 / 撤退 / 销毁装备 | 点击单位所在的格子 → 底部按钮「出售 +1」「撤退」;也可以把棋盘上的干员拖回整备区撤退。整备区里的装备与法术只能「销毁」,已配发的装备锁定在干员身上(干员出售或合成精锐时退回整备区) |
|
||||
| 装备 | 把装备拖到干员所在的格子上(每人 2 件;满了会弹出替换窗口,被替换的一件会被销毁);法术拖到地块上并选方向 |
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@
|
||||
- 从整备区把干员拖到棋盘:近战位 / 远程位高亮为可放置格。**特性写着「可以放置于远程位」的近战干员(钩索师、推击手:歌蕾蒂娅、崖心、见行者,以及自选编队的温蒂)也能放到高台上**,普通和精锐都可以,装什么模组或者不装模组都一样;其他近战干员只能放在地面。站在高台上时照常攻击,但不阻挡敌人。**水里不能部署**:战场#08(涨潮控制)中间的那条水池(深水区)和官方一样拒绝部署,任何干员和召唤物都放不上去;突袭盟约的干员再部署、战术家的援军自动选战术点时也只会落在岸上。松手后出现**方向轮盘**,向上 / 右 / 下 / 左滑动,棋盘上会用橙色条纹预览旋转后的攻击范围,松手确认;松在中心或点「✕ 点击取消」取消。
|
||||
- 想只改朝向:把干员拖回它自己所在的格子再选方向。朝向会一直保留到下一回合。
|
||||
- **点选按地上的方格**:点哪个方格就选中站在那个方格上的单位,点空方格不选任何单位(点到后一格干员画出来的头部,算作点了后面那一格)。作战中友方同样按方格;敌人在格子之间走动,点它的身体或脚下附近(0.6 格内)即可,飞行敌人点它的身体。
|
||||
- 拖动时模型一直在指针 / 手指下(脚在指针下方约半格,鼠标和触屏一样),**指针所在的格子**就是落点并高亮;装备拖到哪个格子,就配发给那个格子上的干员;在商店栏上松手则放回原位。
|
||||
- 拖动时**指针 / 手指所在的格子**就是落点并高亮。这一格可以放下(棋盘或整备区)时,干员模型直接站在这一格上,和官方部署一样:手指在格子里移动时模型不动,移到下一个可放下的格子就站到那一格;指针不在可放下的格子上时(例如在商店栏上),模型跟在指针 / 手指下(脚在指针下方约半格)。鼠标和触屏一样。装备拖到哪个格子,就配发给那个格子上的干员;在商店栏上松手则放回原位。
|
||||
- 点击干员显示攻击范围和底部按钮(「撤退」回到整备区、「出售 +1」);右键 / 长按查看详细信息。作战中、查看队友的阵地时,点开场上我方干员或召唤物(包括队友的)的详情卡,棋盘上也会画出它**当前的攻击范围**(按它现在的位置、朝向和技能,例如开着扩大攻击范围的技能时就是扩大后的范围),关掉详情卡、拖动或选方向时消失;敌人、整备区和临时整备区里的单位不画。详情卡里:头像旁是盟约,下面第一项就是干员的**特质**(如「休整期结束时 …」),之后是特性、属性、攻击范围、技能、模组、装备、天赋。属性是**实时数值**:作战中每秒刷新 4 次(当前生命、攻击、防御、法抗、攻击间隔、阻挡),休整期显示棋盘上的干员**开战时**的数值(已算入装备、盟约层数、特质、策略与机变效果);高于基础值为绿色、低于为红色并标出差值(攻击间隔变短算增益),鼠标悬停可看基础值。基础值是官方数据在该阶级的等级、技能等级(和精锐的模组)下、按你在「干员调配」里给这名干员设的**潜能和练度**(默认潜能 6、精英阶段2-60级)、0 信赖的数值:潜能加成包括部署费用、再部署时间、攻击力 / 生命 / 防御 / 法抗 / 攻速,以及「天赋效果增强」,召唤物的天赋按召唤者的潜能;练度把攻击、防御、生命上限单独乘上自持有加成(精英阶段2-60级 ×1.1;召唤物不乘)。补位的替补干员和原型干员没有潜能档,也没有自持有加成(0.2.2;0.2.1 是所有干员一律满潜能、没有自持有加成,GitHub #252)。
|
||||
- **召唤物手动摆放**:把带可部署召唤物的干员放上棋盘后,它的召唤物会进入整备区(赫默的医疗探机、巫恋的诅咒娃娃、凯瑟琳的 2 个支援装置、浊心斯卡蒂的海嗣、伺夜的狼群、缪尔赛思的流形;同一干员的召唤物叠在一格,不占部署位),像干员一样拖到棋盘上并选方向。凯瑟琳的支援装置给**它面朝的那一格**上的干员加屏障,所以要放在干员旁边、朝向干员。**战术家的援军**(伺夜的狼群、缪尔赛思的流形,也就是选战术点)和官方一样**只能放在召唤者的攻击范围内**:拖动时只有这些格子会亮起;之后在原地给战术家换朝向时,不在新攻击范围里的援军会退回整备区(还在范围里的保持不动;整备区和临时整备区都满、援军无处可退时,这次换朝向不生效并提示「整备区已满」)。撤退或出售干员时它的召唤物一起移除;**移动干员到别的格子时(包括把召唤物拖到干员身上交换位置),它已摆放的召唤物会退回整备区**,需要重新摆放。整备区满时退回的召唤物进入临时整备区,休整期结束还没处理的会在下一回合重新回到整备区。
|
||||
- 作战中:海嗣、狼群、流形、支援装置开战时就在摆放的位置部署;医疗探机、诅咒娃娃开战时也在摆放的位置免费部署一次(持续 10 / 15 秒,和官方一样不需要先开技能),之后所属干员每次发动技能时再次在原位出现;所属干员被击倒、再部署回场时,技能给的那一个如果还没出现,会立刻出现。巫恋退场时诅咒娃娃随之撤退,赫默退场时已出现的医疗探机不受影响。医疗探机、诅咒娃娃、海嗣(以及自选干员的工程蓄水炮、沙地兽、打字机、本能的召唤)这类不会受到攻击、到时间就离场的召唤物不受任何伤害(活性源石也一样)、也不会被治疗,它们的血条就是倒计时:随时间减少,空了就离场。琳琅诗怀雅二技能的香槟炸弹也不会掉血(活性源石和各种伤害都不会)、不会被治疗,不会因为生命值而消失,只在「见面礼」炸到碰到它的第一个地面敌人后消失(维护者 2026-10-08 的决定);隐匿(没有被反隐、也没有被阻挡)或沉睡的敌人选不中,不会触发它。医疗探机、诅咒娃娃、支援装置、海嗣没有摆放就不会出现;狼群、流形(战术家的援军)没有摆放时会在战术点出现。狼群最后一只狼影被击倒后进入战术点形态:暂时离开战斗(不阻挡、不攻击、不会被攻击,格子仍然占着),经过狼影的恢复时间(25 秒)后在原地满血、带着 1 只狼影回来,之后重新开始每 25 秒加 1 只狼影;伺夜退场时狼群随之撤退,伺夜再部署回场时狼群重新带着 2 只狼影出现。
|
||||
|
||||
@@ -22,7 +22,9 @@ view.on('pieceDragStart'|'pieceDrop'|'pieceDragEnd'|'pieceClick'|'pieceHover'|'t
|
||||
// area 'outside' lets the DOM UI decide: released off the grid or over DOM covering the canvas (the shop bar), mouse
|
||||
// and touch alike — there is no drag-to-sell, the piece goes back
|
||||
// the drop target, its highlight and tileHover while dragging = the tile under the POINTER; an item dropped on a
|
||||
// tile equips the unit standing on it (§18.1)
|
||||
// tile equips the unit standing on it (§18.1); a dragged unit is drawn standing on that tile while it is a legal
|
||||
// target — a board tile or a bench slot (render/drag.js dragStandTile), the official deploy drag — and held under
|
||||
// the pointer elsewhere (DRAG_HOLD_TILES), an item plate on the pointer (§27.62)
|
||||
view.pieceScreenRect(uid) // the drawn body's rect { left, top, right, bottom, width, height, x, y } (tooltips / overlays)
|
||||
view.tileScreen(row, col) // { x, y, s, poly } client px of a tile top (direction wheel geometry)
|
||||
view.holdPiece(uid, {row,col}|null) // keep a dropped piece on a tile while its direction is chosen
|
||||
@@ -46,7 +48,7 @@ Screens: **Title** (season art backdrop, 开始 → nickname), **Lobby** (独立
|
||||
|
||||
A bond the mode never activates (标准: official `inactiveBondIdList`) reads 本局禁用 on shop / reward cards (struck through), detail chips and the popup — no 在场 count, no 当前效果 (§21.7); a strategy built around one carries a 本局禁用 tag and note in the strategy draft and stays selectable (bands.json `bondIds`, §21.26).
|
||||
|
||||
Interactions (research 09 §1.2 / §5, which supersede the first draft): shop card, upgrade and 机变 card are two-tap (select → confirm, §18.2) — a tap anywhere on the card, its confirm strip included, is the card's tap (shop cards have no ⓘ corner; the armed bar sits above the detail card; a shop card that cannot be bought right now opens its detail on tap, §20.7); a press / drop goes by the tile under the pointer (§18.1); drag bench↔board — a legal board drop (also onto the piece's own tile) opens the 4-direction deploy wheel (swipe UP/RIGHT/DOWN/LEFT with the rotated range striped orange; release in the centre, ✕ 点击取消 or Esc cancels); tap a unit's tile ⇒ underframe with **出售 +N** (and **撤退** on the board; items/Arts in the hand / temp: **销毁** — equipped items are locked, §6.2) plus its rotated range — the range follows the open detail card (`screens/game/range.js inspectRange`, GitHub PR #281): an own board piece in prep, and in battle or on a scouted board any ally operator / summon on the field with its current range (the live stats' `range` and `dir`, the snapshot's tile), never an enemy or a bench unit (UnitInfo `area` hand / temp); drag, the wheel and the pen hide it; no drag-to-sell; drag an item onto a unit's tile ⇒ equip that unit (both slots used ⇒ the replace dialog `ui/equipReplace.js` shows the incoming and the two equipped items; the player picks the one to replace — it is destroyed — `g.equip replaceUid`; cancel changes nothing); Arts are dropped on a tile and get the direction step; right-click or long-press ⇒ detail (or a tap on a shop card while the bar is locked); keyboard shortcuts — the defaults, which each player rebinds in 设置 → 快捷键 (`ui/gameLogic/shortcuts.js`, saved with the settings; §25.15) — `R` refresh, `F` freeze, `D` level-up, `Q` retreat / `X` sell the selected operator, `Space` ready (in a solo battle: pause / resume, §14 Solo pause); `Esc` closes (fixed); none acts while a text field has the focus or a dialog is open. All actions optimistic-free: UI waits for server state (latency is small).
|
||||
Interactions (research 09 §1.2 / §5, which supersede the first draft): shop card, upgrade and 机变 card are two-tap (select → confirm, §18.2) — a tap anywhere on the card, its confirm strip included, is the card's tap (shop cards have no ⓘ corner; the armed bar sits above the detail card; a shop card that cannot be bought right now opens its detail on tap, §20.7); a press / drop goes by the tile under the pointer (§18.1); drag bench↔board — over a legal tile the dragged unit stands on it, as in the official deploy drag, held under the pointer elsewhere (§27.62); a legal board drop (also onto the piece's own tile) opens the 4-direction deploy wheel (swipe UP/RIGHT/DOWN/LEFT with the rotated range striped orange; release in the centre, ✕ 点击取消 or Esc cancels); tap a unit's tile ⇒ underframe with **出售 +N** (and **撤退** on the board; items/Arts in the hand / temp: **销毁** — equipped items are locked, §6.2) plus its rotated range — the range follows the open detail card (`screens/game/range.js inspectRange`, GitHub PR #281): an own board piece in prep, and in battle or on a scouted board any ally operator / summon on the field with its current range (the live stats' `range` and `dir`, the snapshot's tile), never an enemy or a bench unit (UnitInfo `area` hand / temp); drag, the wheel and the pen hide it; no drag-to-sell; drag an item onto a unit's tile ⇒ equip that unit (both slots used ⇒ the replace dialog `ui/equipReplace.js` shows the incoming and the two equipped items; the player picks the one to replace — it is destroyed — `g.equip replaceUid`; cancel changes nothing); Arts are dropped on a tile and get the direction step; right-click or long-press ⇒ detail (or a tap on a shop card while the bar is locked); keyboard shortcuts — the defaults, which each player rebinds in 设置 → 快捷键 (`ui/gameLogic/shortcuts.js`, saved with the settings; §25.15) — `R` refresh, `F` freeze, `D` level-up, `Q` retreat / `X` sell the selected operator, `Space` ready (in a solo battle: pause / resume, §14 Solo pause); `Esc` closes (fixed); none acts while a text field has the focus or a dialog is open. All actions optimistic-free: UI waits for server state (latency is small).
|
||||
|
||||
Audio: autochess BGM per phase, UI SFX (buy/sell/refresh/level/merge/ready/timer), per-unit attack/hit SFX (throttled; an impact plays for a hostile attack's phys / arts / true damage only, never an operator's skill-mode file for a normal attack, ≤ 2 copies of one battle sound — §18.4; a skill cue that reaches the client before its unit — the sim emits a deploy-tick cast ahead of the unit's `['spawn']` — is held and sounds once when the unit is tracked, `AudioManager.pendingSkill`, GitHub PR #292 by @LimitlessHPPK; entering a field late hands the sound the buffered `spawn` tuples only, `screens/game/early.js audioEarly`, so no old death, deploy or cast replays), boss music in R14/15.
|
||||
|
||||
|
||||
+42
-1
@@ -18,7 +18,7 @@ the boss bounty payout and the bots' guards → §27.33–§27.35; the battle vi
|
||||
pull slides, the champagne's bar, closed eyes, late attack clips, the HP-bar readouts, the renderer abort, held skill
|
||||
cues, atlas refreshes, 梓兰's frost) → §27.36–§27.46; the interface (device tips, the settings button, the shop fold,
|
||||
spectators, the top-bar capsule, the empty new shop slot) → §27.47–§27.52; voices → §27.53–§27.54; the statistics page
|
||||
→ §27.55; potential and training (潜能 / 练度) and the 干员调配 list → §27.56–§27.59; the 自选 picker's Esc → §27.60.
|
||||
→ §27.55; potential and training (潜能 / 练度) and the 干员调配 list → §27.56–§27.59; the 自选 picker's Esc → §27.60; the drag snap → §27.62.
|
||||
|
||||
Decisions of the owner, 2026-10-08 (round 22, from community reports, GitHub and research into the official rules):
|
||||
潜能 and the 自持有 练度 bonus are set per operator in 干员调配, defaults 潜能 6 and 精英2 Lv.60 (official: the account's
|
||||
@@ -1456,3 +1456,44 @@ Notes for the integrator
|
||||
- **Tests**: test/ui/diy.e2e.test.js adds the immediate Esc (it fails without the fix). The loadout-battle /
|
||||
teammate-loadout / loadout browser tests follow §27.57's 干员调配: the row's quick skill / module choices, the default
|
||||
练度 in the detail card's base ATK (×1.1), the detail back at the top before a click.
|
||||
|
||||
### 27.62 A dragged operator stands on the tile it will drop on, as in the official deploy drag (the report in PR #403 by @bubububaoshe — not merged: the pick stays the pointer's tile; the owner's recording of the official game, 2026-10-09) — `render/app.js moveDragVisual`, `render/drag.js dragStandTile`
|
||||
- **Seen**: while an operator was dragged, the model was held under the pointer — its drawn (lifted) feet
|
||||
`DRAG_HOLD_TILES` (0.45 tile, §18.1) of its own px per tile below the pointer, solved on the lifted plane (ground +
|
||||
the 0.3 drag lift) — while the drop target, its highlight, `tileHover` and the direction wheel are the tile under the
|
||||
pointer. On the prep camera the feet and the shadow landed about 0.7 tile toward the camera: on the tile below the
|
||||
highlighted one (PR #403's screenshot). Measured on the parent in the mock: the pointer on (9,4) held the model at y
|
||||
8.46–8.50 (mouse at 1920×1080, finger on the 844×390 phone), on (11,5) at y 9.98–10.50 (1600×900); in the Final
|
||||
Assault prep about 0.7 tile off its target. PR #403 moved the pick to the feet instead. That breaks the standing rule
|
||||
(§18.1: the tile under the pointer decides; restated by the owner on #138, §27.49), still misses by about 0.2 tile (it
|
||||
samples the ground ray, the model is lifted) and fails models.browser #4.1, so the pick stays and only the drawing
|
||||
changes.
|
||||
- **Official**: the owner's recording of the official game (2026-10-09; a deploy drag, the gesture 卫戍协议 shares, research
|
||||
09 §1.2): while an operator is dragged onto the field its model is drawn standing on the tile under the touch point.
|
||||
It does not float with the finger: the finger moving inside the tile leaves the model where it is, the touch point on
|
||||
that tile at the model's feet or lower body; that tile is lit (with the attack-range stripes), takes the drop, and the
|
||||
direction wheel opens on it. Over no deployable tile no model is shown on the field.
|
||||
- **Now**: `moveDragVisual` asks `dragStandTile(pieceDragMove)`, which returns the drop target while it is LEGAL and on
|
||||
the board or the bench (`{ row, col, bench }`, board space); the unit's world x / y / z are then set to that tile at
|
||||
once (no tween between tiles) through `boardWorld` — the point the drop tween goes to, with the Final Assault / Hidden
|
||||
Core prep transform — and it keeps its drag lift. Otherwise — no target, an illegal tile, a temp slot, its own bench
|
||||
slot, `'outside'` over the shop bar — it is held under the pointer as before, so the player still sees what they carry
|
||||
(our pieces start on the bench, unlike the official hand card, so "no model off the field" is not taken). Item plates
|
||||
are unchanged (centred on the pointer). The pick (`pickBoardTile`), the highlight, `tileHover`, the wheel and the drop
|
||||
are untouched, and mouse and touch share the path (the phone uses the same tile pick). A drop from the snapped place
|
||||
has nothing left to slide: the lift drops and the wheel opens on the tile. The DOM fallback field
|
||||
(`ui/fallbackField.js`: a flat grid, its ghost centred on the pointer, so already on its tile) is unchanged.
|
||||
- [ASSUMED]: a legal bench slot is stood on like a board tile (the recording shows the field only); the 0.3 drag lift
|
||||
stays while the unit stands (the recording does not settle a lift that small).
|
||||
- **Golden**: unchanged (presentation only).
|
||||
- **Tests**: test/render/drag.test.js (+2: `dragStandTile`'s decisions; a drag through the controller over its stubbed
|
||||
tile pick, mouse and touch — one tile while the pointer moves inside it, the next legal tile at once, none on an
|
||||
illegal tile, a temp slot, the own bench slot, the shop bar or off the grid, the drop on the tile stood on, a board
|
||||
piece on its own tile). test/render/models.browser.test.js #4.1 drag (mouse) and touch (phone): over a legal tile the
|
||||
unit stands on the pointer's tile (its x / y = the tile, on its top, lifted), at four spots of the tile without moving
|
||||
on screen, at every step of a second leg (standing on the legal tiles, held under the pointer elsewhere), held under
|
||||
the pointer / finger over the shop bar, standing on a free bench slot; the wheel on the pointer's tile as before.
|
||||
test/render/fieldview.browser.test.js: in the Final Assault prep (both halves) the dragged unit stands on its target's
|
||||
boss-field tile, mirrored on the right. Both browser extensions fail on the parent. test/docs-consistency.test.js (the
|
||||
code calls `dragStandTile`, DESIGN and PLAYING describe it). Docs: docs/PLAYING.md §4, README.md,
|
||||
docs/design/client.md §9 / §10, the app.js / drag.js / tune.js comments.
|
||||
+30
-19
@@ -44,9 +44,10 @@
|
||||
// clicks and hover (battleUnitAt), the pen (penUnitAt) — is render/pick.js over the tile under the pointer (groundTile:
|
||||
// raised tops first); battle enemies, which walk between tiles, by their ground position (flying ones by their drawn
|
||||
// body; a huge boss also anywhere on its hit area — data/enemies.json `hitArea`, user playtest #5). A dragged piece
|
||||
// is held under the pointer — a unit with its drawn feet DRAG_HOLD_TILES below it (the pointer on its body), an item
|
||||
// plate centred on it — and drops on the tile under the pointer (render/drag.js); an item dropped on a unit's tile
|
||||
// equips that unit.
|
||||
// drops on the tile under the pointer (render/drag.js). A dragged unit stands on that tile while it is a legal target
|
||||
// (a board tile or a bench slot: drag.js dragStandTile; the official deploy drag, the owner's recording of 2026-10-09,
|
||||
// PR #403's report) and is held under the pointer elsewhere — its drawn feet DRAG_HOLD_TILES below it, the pointer on
|
||||
// its body; an item plate stays centred on the pointer. An item dropped on a unit's tile equips that unit.
|
||||
// Direction step (ui/facingWheel.js, research 09 §1.2):
|
||||
// view.tileScreen(row, col) → { x, y, s, poly: [[x,y]×4] } (client px: tile-top centre, px per tile, corners) | null
|
||||
// view.holdPiece(uid, {row,col} | null) keep a dropped prep piece standing on a tile while its direction is
|
||||
@@ -119,7 +120,7 @@ import { SnapshotBuffer, frameTime } from './interp.js';
|
||||
import { TileField } from './tiles.js';
|
||||
import { UnitView, ItemView, DeviceView, FORMS, syncView } from './units.js';
|
||||
import { FxSystem, ensureDamageFonts } from './fx.js';
|
||||
import { createDragController, pieceTile } from './drag.js';
|
||||
import { createDragController, pieceTile, dragStandTile } from './drag.js';
|
||||
import { backdropTextures, shadowTexture, refreshTierChips, silhouetteTexture } from './textures.js';
|
||||
import { TILE_H, TIER_COLORS, COLORS } from './style.js';
|
||||
import { loadBoardArt } from './boardArt.js';
|
||||
@@ -1083,23 +1084,33 @@ export async function createFieldView(host, options = {}) {
|
||||
function moveDragVisual(p) {
|
||||
if (!dragState) return;
|
||||
const v = dragState.view;
|
||||
// held under the pointer: a unit with its drawn (lifted) feet DRAG_HOLD_TILES of its own px per tile below it (the
|
||||
// scale at the pointer's ground, refined once at the feet), an item plate centred on it; the drop target
|
||||
// (p.target, highlighted below) is the tile under the pointer
|
||||
const item = v instanceof ItemView;
|
||||
const g = cam.unproject(p.x, p.y, 0);
|
||||
const hold = item ? 0 : DRAG_HOLD_TILES;
|
||||
const s0 = g ? cam.scaleAt(g.x, g.y, 0) : cam.scale;
|
||||
const t = pickTile(cam, p.x, p.y + hold * s0, heightAt, tiles.levels);
|
||||
const z = t ? heightAt(t.row, t.col) : 0;
|
||||
const up = item ? z : z + (v.lift || 0) + (v.hover || 0);
|
||||
let w = cam.unproject(p.x, p.y + hold * s0, up);
|
||||
if (w && hold) w = cam.unproject(p.x, p.y + hold * cam.scaleAt(w.x, w.y, up), up) || w;
|
||||
if (w) {
|
||||
v.x = Math.max(-1, Math.min(21, w.x));
|
||||
v.y = Math.max(-1, Math.min(19, w.y));
|
||||
v.z = z;
|
||||
// a unit over a legal drop target — the tile under the pointer, on the board or the bench (render/drag.js
|
||||
// dragStandTile) — stands on it, lifted, as in the official deploy drag (the owner's recording of 2026-10-09): the
|
||||
// world point the drop tween goes to (boardWorld, the prep field transform applied), set at once, no tween
|
||||
const stand = item ? null : dragStandTile(p);
|
||||
if (stand) {
|
||||
const w = boardWorld(stand.row, stand.col, stand.bench);
|
||||
v.x = w.x; v.y = w.y; v.z = w.z;
|
||||
} else {
|
||||
// otherwise held under the pointer (the player still sees what they carry): a unit with its drawn (lifted) feet
|
||||
// DRAG_HOLD_TILES of its own px per tile below it (the scale at the pointer's ground, refined once at the feet), an
|
||||
// item plate centred on it
|
||||
const g = cam.unproject(p.x, p.y, 0);
|
||||
const hold = item ? 0 : DRAG_HOLD_TILES;
|
||||
const s0 = g ? cam.scaleAt(g.x, g.y, 0) : cam.scale;
|
||||
const t = pickTile(cam, p.x, p.y + hold * s0, heightAt, tiles.levels);
|
||||
const z = t ? heightAt(t.row, t.col) : 0;
|
||||
const up = item ? z : z + (v.lift || 0) + (v.hover || 0);
|
||||
let w = cam.unproject(p.x, p.y + hold * s0, up);
|
||||
if (w && hold) w = cam.unproject(p.x, p.y + hold * cam.scaleAt(w.x, w.y, up), up) || w;
|
||||
if (w) {
|
||||
v.x = Math.max(-1, Math.min(21, w.x));
|
||||
v.y = Math.max(-1, Math.min(19, w.y));
|
||||
v.z = z;
|
||||
}
|
||||
}
|
||||
// the drop target (p.target, highlighted) is the tile under the pointer
|
||||
if (p.target && p.target.area !== 'temp') {
|
||||
const r = p.target.area === 'board' ? p.target.row : GEO.HAND_ROW;
|
||||
const c = p.target.area === 'board' ? p.target.col : p.target.idx;
|
||||
|
||||
@@ -26,8 +26,9 @@ const CHAIN_KINDS = new Set(['chain', 'chainHeal']);
|
||||
const DROP_PENDING_MS = 1300;
|
||||
|
||||
/**
|
||||
* A dragged unit is held with its drawn feet this many tiles below the pointer — the pointer on its body, the model
|
||||
* under the finger / mouse (user playtest #4 item 1: as in v2.1; mouse and touch alike). An item plate is centred on it.
|
||||
* A dragged unit over no legal target is held with its drawn feet this many tiles below the pointer — the pointer on its
|
||||
* body, the model under the finger / mouse (user playtest #4 item 1: as in v2.1; mouse and touch alike); over a legal
|
||||
* target it stands on that tile (render/drag.js dragStandTile). An item plate is centred on the pointer.
|
||||
*/
|
||||
export const DRAG_HOLD_TILES = 0.45;
|
||||
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
// direction wheel to re-orient it in place (research 09 §1.2 — "drag the unit onto its own tile and swipe").
|
||||
// Everything is judged at the pointer (user playtest #4 item 1: the ground is drawn as tiles): a press / hover is on the
|
||||
// piece standing on the tile under it (`hitPiece`, render/pick.js), and while dragging the target, `tileHover` and the
|
||||
// legality are that tile — an item dropped on a unit's tile goes to that unit; app.js holds the dragged ghost under
|
||||
// the pointer. A release over DOM UI covering the canvas (the shop bar) is 'outside' (mouse and touch alike).
|
||||
// legality are that tile — an item dropped on a unit's tile goes to that unit. app.js draws a dragged unit standing on
|
||||
// that tile while it is a legal target (`dragStandTile`, the official deploy drag) and held under the pointer
|
||||
// otherwise; an item plate stays on the pointer. A release over DOM UI covering the canvas (the shop bar) is 'outside'
|
||||
// (mouse and touch alike).
|
||||
//
|
||||
// Events emitted through `emit(name, payload)`:
|
||||
// pieceHover { uid, piece, clientX, clientY } | { uid: null } (mouse/pen hover changes)
|
||||
@@ -80,6 +82,21 @@ export function resolveDrop(p) {
|
||||
return { kind: 'drop', target: slot };
|
||||
}
|
||||
|
||||
/**
|
||||
* The tile a dragged UNIT is drawn standing on, from a `pieceDragMove` payload: the drop target under the pointer while
|
||||
* it is legal — a board tile, or a bench slot (row HAND_ROW) — as `{ row, col, bench }` in board space; null otherwise
|
||||
* (no target, an illegal tile, a temp slot, the pointer over DOM UI such as the shop bar): the unit is then held under
|
||||
* the pointer. As in the official deploy drag (the owner's recording of 2026-10-09) the model stands on the tile the
|
||||
* finger is on, and moving within that tile leaves it where it is. app.js never snaps an item plate.
|
||||
*/
|
||||
export function dragStandTile(move, geo = GEO) {
|
||||
const t = move && move.legal === true ? move.target : null;
|
||||
if (!t) return null;
|
||||
if (t.area === 'board' && Number.isInteger(t.row) && Number.isInteger(t.col)) return { row: t.row, col: t.col, bench: false };
|
||||
if (t.area === 'hand' && Number.isInteger(t.idx)) return { row: geo.HAND_ROW, col: t.idx, bench: true };
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Legality via the UI callback (board → (piece,row,col); hand → (piece, HAND_ROW, idx)). Throwing ⇒ illegal. */
|
||||
export function isLegal(canPlace, piece, slot) {
|
||||
if (!slot || slot.area === 'temp' || slot.area === 'outside') return false;
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
// local art, 标准 = 战场#01 only; user playtest #3 (DESIGN §17): temp overflow kept until the first prep its player can
|
||||
// act in (never wiped at the round start), the 回环射手 boomerang and 蕾缪安's shells one by one, the live LP, the detail
|
||||
// card order and the static game data; user playtest #4 (DESIGN §18): picking by the tile under the pointer and the
|
||||
// dragged model held under it, a single human untimed, the strategy draft's one countdown, 机变 two taps, knocked-out
|
||||
// dragged model held under it (standing on that tile while it is a legal target, §27.62), a single human untimed, the
|
||||
// strategy draft's one countdown, 机变 two taps, knocked-out
|
||||
// operators and the official element gauges, live stats, the shop-only items, skill summons, 炎佑; user playtest #5
|
||||
// (DESIGN §19): blocking by contact radius, 联防 forced exit, huge-boss hit areas and 自缚, the element pipeline rules,
|
||||
// boss-field deployment, the phone prep camera — and the normative §3 / §5.1 / §5.5 / §6.1 / §7 lines that changed; user
|
||||
@@ -294,11 +295,15 @@ test('lost models, live LP, detail card order, static game data (user playtest #
|
||||
});
|
||||
|
||||
test('user playtest #4 (DESIGN §18): picking by tile, timers, 机变 two taps, down / element state, content — code and every doc agree', () => {
|
||||
// #1 the tile under the pointer; the dragged model held under the pointer (no touch lift, probe or body shapes)
|
||||
// #1 the tile under the pointer; the dragged model held under the pointer (no touch lift, probe or body shapes) —
|
||||
// standing on the pointer's tile while that is a legal target (§27.62, the official deploy drag)
|
||||
assert.equal(ENEMY_REACH, 0.6);
|
||||
const app = readFileSync(join(ROOT, 'public/js/render/app.js'), 'utf8');
|
||||
const tune = readFileSync(join(ROOT, 'public/js/render/app/tune.js'), 'utf8');
|
||||
assert.match(tune, /export const DRAG_HOLD_TILES = 0\.45;/);
|
||||
assert.match(app, /dragStandTile\(p\)/);
|
||||
assert.match(DESIGN, /`dragStandTile/);
|
||||
assert.match(PLAYING, /干员模型直接站在这一格上/);
|
||||
assert.ok(!/TOUCH_LIFT_TILES|drawnAt|pickShape|pieceDragOver/.test(app), 'no touch lift, pixel probe or body shapes (user playtest #4 item 1)');
|
||||
assert.match(DESIGN, /`DRAG_HOLD_TILES` = 0\.45 tile/);
|
||||
assert.match(DESIGN, /`ENEMY_REACH` 0\.6 tile/);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// test/render/drag.test.js — drop target resolution and the pointer state machine (no DOM, fake timers).
|
||||
// test/render/drag.test.js — drop target resolution, the pointer state machine (no DOM, fake timers) and the tile a
|
||||
// dragged unit is drawn standing on (dragStandTile; the harness's 100 px grid stands in for the camera's tile pick).
|
||||
|
||||
import { test, describe } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { GEO } from '../../shared/constants.js';
|
||||
import { tileSlot, pieceSlot, pieceTile, resolveDrop, isLegal, sameSlot, createDragController } from '../../public/js/render/drag.js';
|
||||
import { tileSlot, pieceSlot, pieceTile, resolveDrop, isLegal, sameSlot, createDragController, dragStandTile } from '../../public/js/render/drag.js';
|
||||
|
||||
describe('slots & targets', () => {
|
||||
test('tileSlot classifies hand, temp, board and everything else', () => {
|
||||
@@ -293,3 +294,54 @@ describe('drag controller', () => {
|
||||
} finally { console.error = orig; }
|
||||
});
|
||||
});
|
||||
|
||||
// The official deploy drag (the owner's recording of 2026-10-09; the report in PR #403): the dragged unit is drawn
|
||||
// standing on the tile under the pointer while that is a legal target, and held under the pointer elsewhere. The tile
|
||||
// is the drag target itself, so the pick, tileHover, the highlight and the drop stay the pointer's tile.
|
||||
describe('the tile a dragged unit stands on (dragStandTile)', () => {
|
||||
test('a legal board tile or bench slot is stood on; an illegal tile, a temp slot, no target hold it under the pointer', () => {
|
||||
assert.deepEqual(dragStandTile({ target: { area: 'board', row: 10, col: 3 }, legal: true }), { row: 10, col: 3, bench: false });
|
||||
assert.deepEqual(dragStandTile({ target: { area: 'hand', idx: 6 }, legal: true }), { row: GEO.HAND_ROW, col: 6, bench: true });
|
||||
assert.equal(dragStandTile({ target: { area: 'board', row: 10, col: 3 }, legal: false }), null, 'an illegal tile');
|
||||
assert.equal(dragStandTile({ target: { area: 'hand', idx: 6 }, legal: false }), null, 'the own bench slot / an illegal slot');
|
||||
assert.equal(dragStandTile({ target: { area: 'temp', idx: 1 }, legal: true }), null, 'a temp slot is never a target');
|
||||
assert.equal(dragStandTile({ target: { area: 'outside', clientX: 1, clientY: 2 }, legal: true }), null);
|
||||
assert.equal(dragStandTile({ target: null, legal: false }), null, 'off the grid, over the shop bar');
|
||||
assert.equal(dragStandTile({ target: { area: 'board', row: 10, col: 3 }, legal: 1 }), null, 'legal must be true');
|
||||
assert.equal(dragStandTile({ target: { area: 'board', row: 9.5, col: 3 }, legal: true }), null);
|
||||
assert.equal(dragStandTile({ target: { area: 'hand' }, legal: true }), null);
|
||||
assert.equal(dragStandTile(null), null);
|
||||
assert.equal(dragStandTile(undefined), null);
|
||||
assert.deepEqual(dragStandTile({ target: { area: 'hand', idx: 2 }, legal: true }, { HAND_ROW: 3 }), { row: 3, col: 2, bench: true }, 'geo is a parameter');
|
||||
});
|
||||
|
||||
test('over the drag path: one tile while the pointer moves inside it, the next legal tile at once, none where no drop lands (mouse and touch)', () => {
|
||||
for (const pointerType of ['mouse', 'touch']) {
|
||||
const h = harness({ domCover: true, canPlace: (p, r, c) => !(r === 11 && c === 4) });
|
||||
h.c.setEditable(true);
|
||||
const stand = () => dragStandTile(h.events.filter((e) => e[0] === 'pieceDragMove').pop()[1]);
|
||||
const at = (x, y) => { h.ev('pointerMove', x, y, { pointerType }); return stand(); };
|
||||
h.ev('pointerDown', 50, 550, { pointerType });
|
||||
assert.equal(at(50, 530), null, `${pointerType}: on its own bench slot (no target) it is held under the pointer`);
|
||||
assert.ok(h.c.dragging);
|
||||
// the pointer anywhere on (9,4) (x 400–499, y 300–399 in the harness grid): the unit stands on (9,4) the whole time
|
||||
const inside = [[410, 390], [450, 350], [495, 305], [402, 301]].map(([x, y]) => at(x, y));
|
||||
assert.deepEqual(inside, Array(4).fill({ row: 9, col: 4, bench: false }), `${pointerType}: moving inside the tile leaves it there`);
|
||||
assert.deepEqual(at(450, 299), { row: 10, col: 4, bench: false }, `${pointerType}: the next tile takes it at once`);
|
||||
assert.equal(at(450, 150), null, `${pointerType}: an illegal tile (11,4)`);
|
||||
assert.equal(at(550, 450), null, `${pointerType}: a temp slot`);
|
||||
assert.deepEqual(at(250, 550), { row: GEO.HAND_ROW, col: 2, bench: true }, `${pointerType}: another bench slot`);
|
||||
assert.equal(at(250, 950), null, `${pointerType}: over the shop bar (DOM over the canvas)`);
|
||||
assert.equal(at(-20, 300), null, `${pointerType}: off the grid`);
|
||||
assert.deepEqual(at(350, 250), { row: 10, col: 3, bench: false });
|
||||
h.ev('pointerUp', 350, 250, { pointerType });
|
||||
assert.deepEqual(h.events.find((e) => e[0] === 'pieceDrop')[1].target, { area: 'board', row: 10, col: 3 }, `${pointerType}: dropped on the tile it stood on`);
|
||||
// a board piece lifts on its own tile (a legal drop: the direction wheel re-orients it in place)
|
||||
h.events.length = 0;
|
||||
h.ev('pointerDown', 450, 250, { pointerType });
|
||||
assert.deepEqual(at(470, 230), { row: 10, col: 4, bench: false }, `${pointerType}: a board piece stands on its own tile`);
|
||||
h.ev('pointerUp', 470, 230, { pointerType });
|
||||
assert.deepEqual(h.events.find((e) => e[0] === 'pieceDrop')[1].target, { area: 'board', row: 10, col: 4 });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
// test/render/fieldview.browser.test.js — field-view features in headless Chrome through the dev demo
|
||||
// (public/dev/render-demo.html): the enemy preview pen (research 09 §2.2 / 08 §4.2) on the 3D and the 2D board and
|
||||
// the 0.25 s pan to it and back, the Final Assault prep on the player's half of the boss field (research 09 §1.2:
|
||||
// drop targets / directions stay in board space, the right half mirrored) with the round's leader on its spawn tile and
|
||||
// drop targets / directions stay in board space, the right half mirrored; a dragged unit stands on its target's
|
||||
// boss-field tile) with the round's leader on its spawn tile and
|
||||
// its red hit tiles beside a range preview (community report #12), damage numbers that never touch, the
|
||||
// automatic rebuild of a 3D board whose WebGL context was lost, and frame rates on a 4× throttled CPU.
|
||||
// Screenshots → test/e2e/out/pen-*.png, fa-prep-*.png, fa-leader-*.png, dmgnum-*.png; perf → test/e2e/out/fieldview-perf.json.
|
||||
@@ -194,6 +195,9 @@ describe('field view features in headless Chrome', { skip }, () => {
|
||||
await page.mouse.move(cx, cy);
|
||||
await page.mouse.down();
|
||||
for (let i = 1; i <= 12; i++) await page.mouse.move(cx + (target.x - cx) * i / 12, cy + (target.y - cy) * i / 12);
|
||||
// over the legal target the dragged unit stands on it (the official deploy drag): on that board tile's boss-field
|
||||
// tile, as the drop will place it — row − 7, the column mirrored on the right half
|
||||
const stood = await page.evaluate((uid) => { const v = window.__demo.view.debug.views.get('p:' + uid); return { x: v.x, y: v.y, lift: v.lift }; }, info.hand.uid);
|
||||
await page.mouse.up();
|
||||
await wait(400);
|
||||
const drops = await page.evaluate(() => window.__drops);
|
||||
@@ -222,6 +226,7 @@ describe('field view features in headless Chrome', { skip }, () => {
|
||||
await page.close();
|
||||
assert.deepEqual(problems, []);
|
||||
assert.deepEqual(drops[0], { area: 'board', row: target.row, col: target.col }, `drop target in board space: ${JSON.stringify(drops)}`);
|
||||
assert.deepEqual(stood, { x: side === 'R' ? 20 - target.col : target.col, y: target.row - 7, lift: 0.3 }, 'while dragged over it: standing on the target\'s boss-field tile');
|
||||
assert.deepEqual(dirs, { a: side === 'R' ? 'LEFT' : 'RIGHT', u: 'UP' });
|
||||
assert.deepEqual(past, { field: true, wallBelow: null, below: null, wallAbove: true, pen: null, pad: true });
|
||||
});
|
||||
|
||||
@@ -9,11 +9,13 @@
|
||||
// #4.1 picking by tile ("地上都画好了一个一个方格,点击对应方格就选中那个方格的人物就行"): a press anywhere on a tile — board
|
||||
// (adjacent rows and columns, high ground), bench, temporary bench — selects the unit on it and an empty tile
|
||||
// nothing (a press on a unit's head, drawn over the tile behind it, is a press on that tile), in prep and in battle
|
||||
// (allies by their tile, enemies by their ground position or drawn body); a dragged unit is held under the pointer — its drawn
|
||||
// feet DRAG_HOLD_TILES below it, the model around it — for a mouse and a finger (phone, 844×390); the drop, its
|
||||
// tileHover and the direction wheel are the tile under the pointer; equipment dropped on a tile equips the unit
|
||||
// on it (not the one in front whose head is drawn there); on a phone a release on a bench slot lands there and one
|
||||
// over the shop bar goes back.
|
||||
// (allies by their tile, enemies by their ground position or drawn body); a dragged unit stands on the tile under
|
||||
// the pointer while that is a legal target — wherever the pointer is on the tile, as in the official deploy drag
|
||||
// (the owner's recording of 2026-10-09; PR #403's report) — and is held under the pointer elsewhere (over the
|
||||
// shop bar: its drawn feet DRAG_HOLD_TILES below it, the model around it), for a mouse and a finger (phone,
|
||||
// 844×390); the drop, its tileHover and the direction wheel are the tile under the pointer; equipment dropped on
|
||||
// a tile equips the unit on it (not the one in front whose head is drawn there); on a phone a release on a bench
|
||||
// slot lands there and one over the shop bar goes back.
|
||||
//
|
||||
// Opt-in (starts Chrome): RENDER_E2E=1 node --test test/render/models.browser.test.js
|
||||
// Chrome path: $CHROME_PATH or the macOS default. Screenshots → test/e2e/out/models-*.png.
|
||||
@@ -315,13 +317,23 @@ describe('user playtest #3 items 1 and 7 (mock match, headless Chrome)', { skip
|
||||
assert.deepEqual(problems, []);
|
||||
});
|
||||
|
||||
/** The dragged view against the pointer (client px): its drawn feet, px per tile, and the Spine model's drawn bounds. */
|
||||
/** The dragged view against the pointer (client px): its drawn feet, px per tile, and the Spine model's drawn bounds;
|
||||
* its world point (wx = col, wy = row, wz) and the height of the board tile under it (hz). */
|
||||
const ghostAt = (page, uid) => page.evaluate((u) => {
|
||||
const R = globalThis.__SP_VIEW__.raw, cv = R.debug.app.view.getBoundingClientRect(), v = R.debug.views.get('p:' + u);
|
||||
const b = v.actor && v.spineReady && !v.imp ? v.actor.spine.getBounds() : null;
|
||||
return { lift: v.lift, x: cv.left + v.screen.x, y: cv.top + v.screen.y, s: v.screen.s, model: b && { x0: cv.left + b.x, y0: cv.top + b.y, x1: cv.left + b.x + b.width, y1: cv.top + b.y + b.height } };
|
||||
return { lift: v.lift, x: cv.left + v.screen.x, y: cv.top + v.screen.y, s: v.screen.s, model: b && { x0: cv.left + b.x, y0: cv.top + b.y, x1: cv.left + b.x + b.width, y1: cv.top + b.y + b.height },
|
||||
wx: v.x, wy: v.y, wz: v.z, hz: R.debug.tiles.heightAt(Math.round(v.y), Math.round(v.x)) };
|
||||
}, uid);
|
||||
/** The model is under the pointer: its feet DRAG_HOLD_TILES (0.45) below it, the pointer inside its drawn body. */
|
||||
/** Over a legal target the model stands on the pointer's tile (the official deploy drag): its world point is that
|
||||
* tile — the tile's top on the board, the bench pad on the bench — and it is lifted. */
|
||||
const assertStands = (g, [row, col], label) => {
|
||||
assert.ok(g.lift > 0, `${label}: lifted`);
|
||||
assert.ok(Math.abs(g.wx - col) < 1e-6 && Math.abs(g.wy - row) < 1e-6, `${label}: stands on (${row},${col}) (at ${g.wx.toFixed(3)},${g.wy.toFixed(3)})`);
|
||||
if (row !== 7) assert.ok(Math.abs(g.wz - g.hz) < 1e-6, `${label}: on the tile's top (z ${g.wz} vs ${g.hz})`);
|
||||
else assert.ok(g.wz > 0, `${label}: on the bench pad (z ${g.wz})`);
|
||||
};
|
||||
/** Elsewhere the model is under the pointer: its feet DRAG_HOLD_TILES (0.45) below it, the pointer inside its drawn body. */
|
||||
const assertHeld = (g, ptr, label) => {
|
||||
assert.ok(g.lift > 0, `${label}: lifted`);
|
||||
assert.ok(Math.abs(g.x - ptr.x) < 1.5 && Math.abs(g.y - ptr.y - 0.45 * g.s) < 1.5, `${label}: feet 0.45 tile below the pointer (feet ${g.x.toFixed(1)},${g.y.toFixed(1)}, pointer ${ptr.x.toFixed(1)},${ptr.y.toFixed(1)}, s ${g.s.toFixed(1)})`);
|
||||
@@ -331,7 +343,7 @@ describe('user playtest #3 items 1 and 7 (mock match, headless Chrome)', { skip
|
||||
}
|
||||
};
|
||||
|
||||
test('#4.1 drag (mouse): the unit is held under the pointer; target, tileHover and the direction wheel are the pointer\'s tile', async () => {
|
||||
test('#4.1 drag (mouse): the unit stands on the pointer\'s tile over a legal target, held under the pointer elsewhere; target, tileHover and the direction wheel are the pointer\'s tile', async () => {
|
||||
const { page, problems } = await open('phase=PREP');
|
||||
const hand = await page.evaluate(() => { const p = globalThis.__MOCK__.S().priv; const i = p.hand.findIndex((x) => x && x.kind === 'chess'); return { uid: p.hand[i].uid, idx: i }; });
|
||||
await page.evaluate(() => { window.__hover = []; globalThis.__SP_VIEW__.raw.on('tileHover', (t) => window.__hover.push(t && [t.row, t.col])); });
|
||||
@@ -340,17 +352,46 @@ describe('user playtest #3 items 1 and 7 (mock match, headless Chrome)', { skip
|
||||
await page.mouse.move(from.x, from.y); await page.mouse.down();
|
||||
for (let i = 1; i <= 12; i++) { await page.mouse.move(from.x + ((to.x - from.x) * i) / 12, from.y + ((to.y - from.y) * i) / 12); await sleep(16); }
|
||||
await sleep(150);
|
||||
assertHeld(await ghostAt(page, hand.uid), to, 'mouse');
|
||||
const first = await ghostAt(page, hand.uid);
|
||||
assertStands(first, [9, 4], 'mouse');
|
||||
assert.deepEqual(await page.evaluate(() => window.__hover.at(-1)), [9, 4], 'tileHover = the tile under the pointer');
|
||||
await page.screenshot({ path: path.join(OUT, 'models-drag-ghost.png') });
|
||||
// the whole way: the model stays under the pointer (sampled at every step of a second drag leg)
|
||||
// the pointer moving about inside the tile leaves the model where it is (the official recording: the finger moves,
|
||||
// the model stays on its tile)
|
||||
for (const [u, v] of [[-0.6, -0.3], [0.6, -0.3], [-0.5, 0.7], [0, 0]]) { // (the nearest strip is hidden by row 8: SPOTS)
|
||||
const pt = await tileSpot(page, 9, 4, u, v);
|
||||
await page.mouse.move(pt.x, pt.y);
|
||||
await sleep(40);
|
||||
const g = await ghostAt(page, hand.uid);
|
||||
assertStands(g, [9, 4], `mouse at (${u}, ${v}) of the tile`);
|
||||
assert.ok(Math.abs(g.x - first.x) < 0.5 && Math.abs(g.y - first.y) < 0.5, `mouse at (${u}, ${v}): the model did not move on screen`);
|
||||
}
|
||||
// the whole way: on a legal tile the model stands on the pointer's tile, elsewhere it is under the pointer (sampled
|
||||
// at every step of a second drag leg; the legal tiles are the view's own 'legal' highlight)
|
||||
const legal = new Set(await page.evaluate(() => globalThis.__SP_VIEW__.raw.debug.tiles.highlights.get('legal').tiles.map(([r, c]) => `${r},${c}`)));
|
||||
const leg = await tileSpot(page, 10, 5);
|
||||
const seen = { stands: 0, held: 0 };
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
const pt = { x: to.x + ((leg.x - to.x) * i) / 6, y: to.y + ((leg.y - to.y) * i) / 6 };
|
||||
await page.mouse.move(pt.x, pt.y);
|
||||
await sleep(40);
|
||||
assertHeld(await ghostAt(page, hand.uid), pt, `mouse step ${i}`);
|
||||
const tile = await page.evaluate(() => window.__hover.at(-1));
|
||||
if (legal.has(`${tile}`)) { seen.stands++; assertStands(await ghostAt(page, hand.uid), tile, `mouse step ${i}`); }
|
||||
else { seen.held++; assertHeld(await ghostAt(page, hand.uid), pt, `mouse step ${i}`); }
|
||||
}
|
||||
assert.ok(seen.stands > 0, `the leg crosses legal tiles (${JSON.stringify(seen)})`);
|
||||
// over the shop bar (DOM over the canvas: no target) the model is held under the pointer, so the player still sees
|
||||
// what they carry
|
||||
const bar = await page.evaluate(() => { const el = [...document.querySelectorAll('.scard')][2], r = el && el.getBoundingClientRect(); return r && { x: r.left + r.width / 2, y: r.top + r.height * 0.3 }; });
|
||||
assert.ok(bar, 'premise: a shop card');
|
||||
assert.ok(await page.evaluate((x, y) => !!document.elementFromPoint(x, y)?.closest('.shopbar'), bar.x, bar.y), 'premise: the point is on the shop bar');
|
||||
await page.mouse.move(bar.x, bar.y, { steps: 6 });
|
||||
await sleep(60);
|
||||
assert.equal(await page.evaluate(() => window.__hover.at(-1)), null, 'no tile under the shop bar');
|
||||
assertHeld(await ghostAt(page, hand.uid), bar, 'mouse over the shop bar');
|
||||
await page.mouse.move(to.x, to.y, { steps: 6 });
|
||||
await sleep(60);
|
||||
assertStands(await ghostAt(page, hand.uid), [9, 4], 'mouse back on (9,4)');
|
||||
await page.mouse.move(to.x, to.y);
|
||||
await sleep(60);
|
||||
await page.mouse.up();
|
||||
@@ -402,10 +443,10 @@ describe('user playtest #3 items 1 and 7 (mock match, headless Chrome)', { skip
|
||||
assert.deepEqual(problems, []);
|
||||
});
|
||||
|
||||
// a phone (touch, landscape 844×390): the finger holds the model like the mouse (no lift above the finger); on the
|
||||
// bench (the lowest canvas row, the shop bar right under it) a release on the slot lands there, one on the shop bar
|
||||
// goes back
|
||||
test('#4.1 touch (phone): held under the finger, dropped on the tile under it; a release over the shop bar goes back', async () => {
|
||||
// a phone (touch, landscape 844×390): the finger drags the model like the mouse — standing on the finger's tile over a
|
||||
// legal target, under the finger elsewhere (no lift above the finger); on the bench (the lowest canvas row, the shop
|
||||
// bar right under it) a release on the slot lands there, one on the shop bar goes back
|
||||
test('#4.1 touch (phone): stands on the finger\'s tile (under the finger over the shop bar), dropped on the tile under it; a release over the shop bar goes back', async () => {
|
||||
const page = await browser.newPage();
|
||||
const problems = [];
|
||||
page.on('pageerror', (e) => problems.push(`pageerror: ${e.message}`));
|
||||
@@ -436,10 +477,10 @@ describe('user playtest #3 items 1 and 7 (mock match, headless Chrome)', { skip
|
||||
await page.keyboard.press('Escape');
|
||||
await sleep(200);
|
||||
}
|
||||
// a bench operator onto the board: held under the finger, the wheel on the finger's tile
|
||||
// a bench operator onto the board: standing on the finger's tile, the wheel on that tile
|
||||
const hand = await page.evaluate(() => { const p = globalThis.__MOCK__.S().priv; const i = p.hand.findIndex((x) => x && x.kind === 'chess'); return { uid: p.hand[i].uid, idx: i }; });
|
||||
const to = await tileSpot(page, 9, 4, -0.3, 0.3);
|
||||
await touchDrag(await tileSpot(page, 7, hand.idx), to, { hold: async () => assertHeld(await ghostAt(page, hand.uid), to, 'touch') });
|
||||
await touchDrag(await tileSpot(page, 7, hand.idx), to, { hold: async () => assertStands(await ghostAt(page, hand.uid), [9, 4], 'touch') });
|
||||
await page.waitForSelector('.fwheel__dia', { timeout: 3000 });
|
||||
const w = await page.evaluate(() => { const r = document.querySelector('.fwheel__dia').getBoundingClientRect(); return { x: r.left + r.width / 2, y: r.top + r.height / 2 }; });
|
||||
const c = await tileSpot(page, 9, 4);
|
||||
@@ -457,9 +498,9 @@ describe('user playtest #3 items 1 and 7 (mock match, headless Chrome)', { skip
|
||||
return null;
|
||||
}, slot);
|
||||
assert.ok(bar, 'premise: the shop bar covers the canvas right below the bench slot');
|
||||
await touchDrag(await tileSpot(page, unit.row, unit.col), bar);
|
||||
await touchDrag(await tileSpot(page, unit.row, unit.col), bar, { hold: async () => assertHeld(await ghostAt(page, unit.uid), bar, 'touch over the shop bar') });
|
||||
assert.ok(await page.evaluate((u) => globalThis.__MOCK__.S().priv.board.some((b) => b.uid === u), unit.uid), 'released over the shop bar: back on the board');
|
||||
await touchDrag(await tileSpot(page, unit.row, unit.col), slot);
|
||||
await touchDrag(await tileSpot(page, unit.row, unit.col), slot, { hold: async () => assertStands(await ghostAt(page, unit.uid), [7, 5], 'touch on the free bench slot') });
|
||||
assert.equal(await page.evaluate(() => globalThis.__MOCK__.S().priv.hand[5]?.uid ?? null), unit.uid, 'released on the bench slot: it is there');
|
||||
await page.screenshot({ path: path.join(OUT, 'models-touch-phone.png') });
|
||||
await page.close();
|
||||
|
||||
Reference in new issue
Block a user