Playtest #4 fixes: tile picking, knocked-down state, element gauges, draft timer, live stats, shop items, drones, 赫默, 炎佑, UI flash
- #1 picking by the tile under the pointer (prep, battle, pen, equipment drops); the dragged model stays under the pointer (mouse and touch); pixel probe removed; phone ghost-click on the underframe fixed - #2 two-tap confirm on 机变 cards (like the shop) - #3 a 同盟 room with one human is untimed like 独立模拟 (no countdown outside battles) - #4 strategy draft: one synced countdown, 30 s per human turn, timeout takes the highlighted band - #5 special 维式重锤 (4) and 突变细胞 no longer sold; item card shows where they come from - #6 operator skill-mode sounds no longer play on normal attacks (纯烬艾雅法拉 and 55 others) - #7 detail card shows live battle stats and the prep-time battle-start stats (g.unitStats) - #8 official element gauges on units; burst cooldown (阻回) per PRTS - #9 knocked-down operators stay on their tile with a redeploy countdown ring - #10 drone behaviour per PRTS (暴鸰, 先兆者 shells, 黑云, 萨科塔之眼) - #11 赫默's drone only from her skill; placeable summons limited to talent tokens - #12 炎佑 stays where it is with no enemies; 祛恶之焰 as a continuous channel - #13 the occasional giant bond icon (loading texture scaled ×46) fixed - docs (DESIGN §18), recordings, data and tests updated (2467 unit tests, all browser suites green)
This commit is contained in:
1 parent
0aba602ecf
commit
d4f868cd19
107 files changed
+4510
-1653
No files matched your search
+42
-64
@@ -77,7 +77,6 @@ function harness(opts = {}) {
|
||||
const c = createDragController({
|
||||
hitPiece: (x, y) => pieces.find((p) => Math.abs(p.x - x) < 40 && Math.abs(p.y - y) < 40) || null,
|
||||
pickTile: (x, y) => (x < 0 || y < 0 || x >= 2100 ? null : { row: 12 - Math.floor(y / 100), col: Math.floor(x / 100) }),
|
||||
dropPoint: opts.dropPoint,
|
||||
isOverCanvas: (cx, cy) => !(opts.domCover && cy > 900),
|
||||
canPlace: opts.canPlace,
|
||||
emit: (name, p) => events.push([name, p]),
|
||||
@@ -218,84 +217,63 @@ describe('drag controller', () => {
|
||||
assert.equal(h.events.filter((e) => e[0] === 'pieceDrop').length, 0);
|
||||
});
|
||||
|
||||
// user playtest #3 item 7: the drop target is the tile under the dragged ghost's feet (render/app.js dropPoint: the
|
||||
// pointer for a mouse, a little above the finger for touch) — the hover tile, legality, the ghost and the drop agree
|
||||
test('dropPoint: target, tileHover, legality and the drop follow the ghost\'s feet; default = the pointer', () => {
|
||||
const calls = [];
|
||||
const h = harness({
|
||||
dropPoint: (e, piece) => { calls.push([e.pointerType, piece.uid]); return e.pointerType === 'touch' ? { x: e.x, y: e.y - 100 } : null; },
|
||||
canPlace: (p, r) => r !== 11,
|
||||
});
|
||||
h.c.setEditable(true);
|
||||
// mouse: the hook returns nothing → the pointer itself
|
||||
h.ev('pointerDown', 50, 550);
|
||||
h.ev('pointerMove', 350, 250);
|
||||
let mv = h.events.filter((e) => e[0] === 'pieceDragMove').pop()[1];
|
||||
assert.deepEqual([mv.hx, mv.hy, mv.target], [350, 250, { area: 'board', row: 10, col: 3 }]);
|
||||
h.ev('pointerUp', 350, 250);
|
||||
assert.deepEqual(h.events.find((e) => e[0] === 'pieceDrop')[1].target, { area: 'board', row: 10, col: 3 });
|
||||
// touch: the ghost (and the target) one row above the finger
|
||||
h.events.length = 0;
|
||||
h.ev('pointerDown', 50, 550, { pointerType: 'touch' });
|
||||
h.ev('pointerMove', 450, 350, { pointerType: 'touch' });
|
||||
mv = h.events.filter((e) => e[0] === 'pieceDragMove').pop()[1];
|
||||
assert.deepEqual([mv.x, mv.y, mv.hx, mv.hy], [450, 350, 450, 250], 'pointer and drop point both reported');
|
||||
assert.deepEqual(mv.target, { area: 'board', row: 10, col: 4 });
|
||||
assert.equal(mv.legal, true);
|
||||
assert.ok(h.events.some((e) => e[0] === 'tileHover' && e[1] && e[1].row === 10 && e[1].col === 4), 'tileHover = the target');
|
||||
h.ev('pointerMove', 450, 250, { pointerType: 'touch' });
|
||||
assert.equal(h.events.filter((e) => e[0] === 'pieceDragMove').pop()[1].legal, false, 'row 11 under the ghost: illegal');
|
||||
h.ev('pointerUp', 450, 350, { pointerType: 'touch' });
|
||||
assert.deepEqual(h.events.find((e) => e[0] === 'pieceDrop')[1].target, { area: 'board', row: 10, col: 4 });
|
||||
assert.ok(calls.every(([, uid]) => uid === 1) && calls.some(([t]) => t === 'touch'));
|
||||
// user playtest #4 item 1: everything is judged at the pointer — the tile under it is the target (tileHover, legality,
|
||||
// the drop) for a mouse and a finger alike (v2.2 lifted a touch drag's target 0.6 tile above the finger)
|
||||
test('the target, tileHover, legality and the drop are the tile under the pointer (mouse and touch)', () => {
|
||||
for (const pointerType of ['mouse', 'touch']) {
|
||||
const h = harness({ canPlace: (p, r) => r !== 11 });
|
||||
h.c.setEditable(true);
|
||||
h.ev('pointerDown', 50, 550, { pointerType });
|
||||
h.ev('pointerMove', 450, 350, { pointerType });
|
||||
let mv = h.events.filter((e) => e[0] === 'pieceDragMove').pop()[1];
|
||||
assert.deepEqual([mv.x, mv.y, mv.target, mv.legal], [450, 350, { area: 'board', row: 9, col: 4 }, true], pointerType);
|
||||
assert.ok(h.events.some((e) => e[0] === 'tileHover' && e[1] && e[1].row === 9 && e[1].col === 4), `${pointerType}: tileHover = the target`);
|
||||
h.ev('pointerMove', 450, 150, { pointerType });
|
||||
mv = h.events.filter((e) => e[0] === 'pieceDragMove').pop()[1];
|
||||
assert.deepEqual([mv.target, mv.legal], [{ area: 'board', row: 11, col: 4 }, false], `${pointerType}: row 11 under the pointer is illegal`);
|
||||
h.ev('pointerMove', 450, 350, { pointerType });
|
||||
h.ev('pointerUp', 450, 350, { pointerType });
|
||||
assert.deepEqual(h.events.find((e) => e[0] === 'pieceDrop')[1].target, { area: 'board', row: 9, col: 4 }, pointerType);
|
||||
}
|
||||
});
|
||||
|
||||
// the view may skip its pixel probe on hover picks (a readback per mouse move stalls on the GPU): the controller says
|
||||
// which calls are hovers — mouse moves without a press, drag moves — and which must be exact: presses and releases
|
||||
test('hitPiece / dropPoint tell hovers from presses and releases', () => {
|
||||
const hits = [];
|
||||
const drops = [];
|
||||
test('presses and hover pick the piece at the pointer (the view\'s hitPiece: the piece on the tile there)', () => {
|
||||
const asked = [];
|
||||
const pieces = [{ uid: 1, area: 'hand', idx: 0, x: 50, y: 550 }];
|
||||
const c = createDragController({
|
||||
hitPiece: (x, y, hover) => { hits.push(hover === true); return pieces.find((p) => Math.abs(p.x - x) < 40 && Math.abs(p.y - y) < 40) || null; },
|
||||
hitPiece: (x, y) => { asked.push([x, y]); return pieces.find((p) => Math.abs(p.x - x) < 40 && Math.abs(p.y - y) < 40) || null; },
|
||||
pickTile: (x, y) => ({ row: 12 - Math.floor(y / 100), col: Math.floor(x / 100) }),
|
||||
dropPoint: (e, piece, final) => { drops.push(final === true); return null; },
|
||||
emit: () => {},
|
||||
});
|
||||
const ev = (type, x, y) => c[type]({ pointerId: 1, pointerType: 'mouse', button: 0, x, y, clientX: x, clientY: y });
|
||||
const ev = (type, x, y, pointerType = 'mouse') => c[type]({ pointerId: 1, pointerType, button: 0, x, y, clientX: x, clientY: y });
|
||||
c.setEditable(true);
|
||||
ev('pointerMove', 50, 550);
|
||||
assert.deepEqual(hits, [true], 'a mouse move without a press is a hover');
|
||||
ev('pointerDown', 50, 550);
|
||||
assert.deepEqual(hits, [true, false], 'a press probes');
|
||||
ev('pointerMove', 350, 250);
|
||||
ev('pointerMove', 360, 250);
|
||||
ev('pointerUp', 360, 250);
|
||||
assert.ok(drops.length >= 3);
|
||||
assert.ok(drops.slice(0, -1).every((f) => f === false), 'drag moves are hovers');
|
||||
assert.equal(drops[drops.length - 1], true, 'the release is final');
|
||||
ev('pointerMove', 60, 560);
|
||||
ev('pointerDown', 55, 545, 'touch');
|
||||
assert.deepEqual(asked, [[60, 560], [55, 545]], 'hover and press ask at the pointer itself');
|
||||
ev('pointerMove', 355, 255, 'touch');
|
||||
ev('pointerUp', 355, 255, 'touch');
|
||||
assert.equal(asked.length, 2, 'a drag asks no hit-test');
|
||||
});
|
||||
|
||||
// review of item 7: on a phone the bench is the lowest canvas row and the shop bar (DOM) sits right under it — a
|
||||
// touch ghost lifted onto the bench has the finger on the shop bar. DOM cover is judged at the drop point.
|
||||
test('dropPoint: DOM cover is tested at the ghost\'s feet, not at the finger (touch onto the bench above the shop bar)', () => {
|
||||
const h = harness({ domCover: true, dropPoint: (e) => (e.pointerType === 'touch' ? { x: e.x, y: e.y - 400 } : null) });
|
||||
// on a phone the bench is the lowest canvas row and the shop bar (DOM) sits right under it: a release with the finger
|
||||
// on the shop bar is 'outside' (the piece goes back); on the bench slot above it, it lands there
|
||||
test('DOM cover is judged at the pointer: a touch release over the shop bar is outside, on the bench it lands', () => {
|
||||
const h = harness({ domCover: true });
|
||||
h.c.setEditable(true);
|
||||
// board unit (10,4) dragged by touch; the finger ends on the covered strip (y > 900), its ghost on bench slot 2
|
||||
h.ev('pointerDown', 450, 250, { pointerType: 'touch' });
|
||||
h.ev('pointerMove', 250, 950, { pointerType: 'touch' });
|
||||
const mv = h.events.filter((e) => e[0] === 'pieceDragMove').pop()[1];
|
||||
assert.deepEqual(mv.target, { area: 'hand', idx: 2 });
|
||||
assert.equal(mv.legal, true);
|
||||
h.ev('pointerUp', 250, 950, { pointerType: 'touch' });
|
||||
h.ev('pointerMove', 250, 550, { pointerType: 'touch' });
|
||||
let mv = h.events.filter((e) => e[0] === 'pieceDragMove').pop()[1];
|
||||
assert.deepEqual([mv.target, mv.legal], [{ area: 'hand', idx: 2 }, true]);
|
||||
h.ev('pointerUp', 250, 550, { pointerType: 'touch' });
|
||||
assert.deepEqual(h.events.find((e) => e[0] === 'pieceDrop')[1].target, { area: 'hand', idx: 2 });
|
||||
// the ghost itself under the DOM: outside (the piece goes back), as for a mouse released on the DOM
|
||||
h.events.length = 0;
|
||||
h.ev('pointerDown', 450, 250, { pointerType: 'touch' });
|
||||
h.ev('pointerMove', 250, 1350, { pointerType: 'touch' });
|
||||
assert.equal(h.events.filter((e) => e[0] === 'pieceDragMove').pop()[1].target, null);
|
||||
h.ev('pointerUp', 250, 1350, { pointerType: 'touch' });
|
||||
assert.equal(h.events.find((e) => e[0] === 'pieceDrop')[1].target.area, 'outside');
|
||||
h.ev('pointerMove', 250, 950, { pointerType: 'touch' });
|
||||
mv = h.events.filter((e) => e[0] === 'pieceDragMove').pop()[1];
|
||||
assert.equal(mv.target, null, 'the finger on the DOM: no target');
|
||||
assert.ok(h.events.some((e) => e[0] === 'tileHover' && e[1] === null), 'no tile lit');
|
||||
h.ev('pointerUp', 250, 950, { pointerType: 'touch' });
|
||||
assert.deepEqual(h.events.find((e) => e[0] === 'pieceDrop')[1].target, { area: 'outside', clientX: 250, clientY: 950 });
|
||||
});
|
||||
|
||||
test('a throwing listener or hit-test never breaks the controller', () => {
|
||||
|
||||
Reference in new issue
Block a user