From 1df530f2b98ea806fe0c9303f56ca1e879edbff2 Mon Sep 17 00:00:00 2001 From: sganggs Date: Tue, 13 Oct 2026 15:54:40 +0800 Subject: [PATCH] =?UTF-8?q?0.2.6-dev:=20a=20piece=20has=20no=20screen=20re?= =?UTF-8?q?ct=20until=20it=20is=20first=20drawn=20=E2=80=94=20pieceScreenR?= =?UTF-8?q?ect=20returned=20a=20just-built=20view's=20unprojected=20(0,=20?= =?UTF-8?q?0)=20record,=20so=20a=20browser=20test=20grabbed=20a=20piece=20?= =?UTF-8?q?bought=20that=20frame=20at=20the=20canvas=20corner=20(the=20int?= =?UTF-8?q?ermittent=20.fwheel=5F=5Fdia=20timeout=20in=20coop.e2e=20/=20re?= =?UTF-8?q?al.e2e);=20the=20tests'=20piecePoint=20waits=20for=20a=20drawn?= =?UTF-8?q?=20piece=20that=20holds=20still;=20golden:=20unchanged?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/js/render/app.js | 4 +++- public/js/render/units.js | 6 ++++++ test/e2e/client.mjs | 19 +++++++++++++++---- test/ui/real.e2e.test.js | 18 ++++++++++++++---- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/public/js/render/app.js b/public/js/render/app.js index d496a514..44d97930 100644 --- a/public/js/render/app.js +++ b/public/js/render/app.js @@ -47,6 +47,7 @@ // a special terrain tile's own tip; the screen resolves it with gameLogic.terrainInfo) // tileHover { row, col, area, idx } | null (while dragging: the drop target — the tile under the pointer) // view.pieceScreenRect(uid) → { left, top, right, bottom, width, height, x, y } (client px: the drawn body) | null +// (null also until the piece is first drawn) // Picking (user playtest #4 item 1: the ground is drawn as tiles — a press on a tile is a press on the unit standing // there): every "which unit is under the pointer" — prep press / click / detail / drag start / hover (pieceAt), battle // clicks and hover (battleUnitAt), the pen (penUnitAt) — is render/pick.js over the tile under the pointer (groundTile: @@ -1939,7 +1940,8 @@ export async function createFieldView(host, options = {}) { pieceScreenRect(uid) { let v = views.get('p:' + uid); if (!v) for (const x of views.values()) if (x.uid === uid || x.info?.uid === uid || x.id === uid) { v = x; break; } - if (!v || v.destroyed) return null; + // a view built this frame is placed by its first update: before that its screen record is still (0, 0) + if (!v || v.destroyed || !v.placed) return null; const b = v.bounds(); const r = canvas.getBoundingClientRect(); const left = r.left + b.x, top = r.top + b.y; diff --git a/public/js/render/units.js b/public/js/render/units.js index 29956416..a375e045 100644 --- a/public/js/render/units.js +++ b/public/js/render/units.js @@ -632,6 +632,7 @@ export class UnitView { this.lastAtk = -1; this.atkInterval = defaultInterval(ctx, info); this.shake = 0; this.screen = { x: 0, y: 0, s: 1, top: 0 }; + this.placed = false; // set by the first update's projection: until then `screen` is not on screen (bounds) this.destroyed = false; this.form = typeof info.form === 'string' ? info.form : null; // the unit's model form: an enemy's mode, a 傀儡师's 替身 (setForm, FORMS) this._dieForm = null; // the form it died in (die): a model built while it lies down shows that form's death @@ -1337,6 +1338,7 @@ export class UnitView { const hoverTo = this.flying && (this.alive || (this.dying > DIE_FADE_TAIL && !this.down)) ? FLY_HOVER : 0; if (this.hover !== hoverTo) this.hover = Math.abs(hoverTo - this.hover) < 1e-3 ? hoverTo : this.hover + (hoverTo - this.hover) * Math.min(1, dt * 6); const p = cam.project(this.x, this.y, this.z + this.hover + this.lift, this.screen); + this.placed = true; const s = p.s; // fades if (this.fadeIn < 1) this.fadeIn = Math.min(1, this.fadeIn + dt * 4); @@ -2143,6 +2145,7 @@ export class ItemView { this.alive = true; this.remove = false; this.lift = 0; this.dimmed = false; this.bob = Math.random() * 6; this.screen = { x: 0, y: 0, s: 1, top: 0 }; + this.placed = false; this.shadow = new P.Sprite(ctx.shadowTex || shadowTexture()); this.shadow.anchor.set(0.5); ctx.layers.shadow.addChild(this.shadow); @@ -2169,6 +2172,7 @@ export class ItemView { update(dt, cam, t) { const lifted = this.lift > 0; const p = cam.project(this.x, this.y, lifted ? this.z : this.z + this.lift + 0.12 + Math.sin(t * 2 + this.bob) * 0.03, this.screen); + this.placed = true; this.root.position.set(p.x, p.y); this.root.zIndex = unitDepthKey(cam, this.x, this.y, this.lift); const size = p.s * 0.62; @@ -2214,6 +2218,7 @@ export class DeviceView { this.facing = info.facing === -1 ? -1 : 1; this.aim = this.facing > 0 ? 0 : Math.PI; this.aimTo = this.aim; this.fire = 0; this.screen = { x: 0, y: 0, s: 1, top: 0 }; + this.placed = false; this._headTiles = this.spec.height + 0.2; this.box = ctx.createBox ? ctx.createBox() : null; this.gfx = new P.Graphics(); // fallback body (no tile field) and the turret head @@ -2270,6 +2275,7 @@ export class DeviceView { g.zIndex = zKey + 0.001; g.alpha = k; const p = cam.project(this.x, this.y, this.z + this.spec.height + 0.35, this.screen); + this.placed = true; const show = this.alive && this.hp < this.maxHp - 0.5; this.hud.visible = show; if (show) { diff --git a/test/e2e/client.mjs b/test/e2e/client.mjs index dc9126a4..5e336ba0 100644 --- a/test/e2e/client.mjs +++ b/test/e2e/client.mjs @@ -286,12 +286,23 @@ export class Client { viewKind() { return this.page.evaluate(() => globalThis.__SP_VIEW__?.kind ?? null); } viewStats() { return this.page.evaluate(() => { try { return globalThis.__SP_VIEW__?.raw?.stats?.() ?? null; } catch { return null; } }); } - /** Client point to grab a prep piece (`at` = fraction of the piece rect's height, from the top). */ - piecePoint(uid, at = 0.72) { - return this.page.evaluate((uid, at) => { + /** Client point to grab a prep piece (`at` = fraction of the piece rect's height, from the top). Waits (up to 2.5 s) + * until the piece is drawn and holds still: a piece bought this frame has no rect yet, and one still sliding to its + * tile (0.22 s) would be grabbed over a neighbouring tile — picking is by the tile under the pointer. */ + async piecePoint(uid, at = 0.72) { + const read = () => this.page.evaluate((uid, at) => { const r = globalThis.__SP_VIEW__?.pieceScreenRect(uid); - return r && r.width > 0 ? { x: r.left + r.width / 2, y: r.top + r.height * at } : null; + if (!r || !(r.width >= 4)) return null; + const x = r.left + r.width / 2, y = r.top + r.height * at; + return x >= 0 && y >= 0 && x <= innerWidth && y <= innerHeight ? { x, y } : null; }, uid, at); + let last = null; + for (const end = Date.now() + 2500; Date.now() < end; await sleep(80)) { + const p = await read(); + if (p && last && Math.abs(p.x - last.x) < 1.5 && Math.abs(p.y - last.y) < 1.5) return p; + last = p; + } + return last; } /** Record the view's pieceDrop events (for diagnostics) and return + clear them. */ diff --git a/test/ui/real.e2e.test.js b/test/ui/real.e2e.test.js index 54e5367d..0af1fc71 100644 --- a/test/ui/real.e2e.test.js +++ b/test/ui/real.e2e.test.js @@ -145,12 +145,22 @@ class Client { viewKind() { return this.page.evaluate(() => globalThis.__SP_VIEW__?.kind ?? null); } viewStats() { return this.page.evaluate(() => { try { return globalThis.__SP_VIEW__?.raw?.stats?.() ?? null; } catch { return null; } }); } - /** Client point to grab a prep piece. */ - piecePoint(uid) { - return this.page.evaluate((uid) => { + /** Client point to grab a prep piece. Waits (up to 2.5 s) until the piece is drawn and holds still: a piece bought + * this frame has no rect yet, and one still sliding to its tile would be grabbed over a neighbouring tile. */ + async piecePoint(uid) { + const read = () => this.page.evaluate((uid) => { const r = globalThis.__SP_VIEW__?.pieceScreenRect(uid); - return r && r.width > 0 ? { x: r.left + r.width / 2, y: r.top + r.height * 0.72 } : null; + if (!r || !(r.width >= 4)) return null; + const x = r.left + r.width / 2, y = r.top + r.height * 0.72; + return x >= 0 && y >= 0 && x <= innerWidth && y <= innerHeight ? { x, y } : null; }, uid); + let last = null; + for (const end = Date.now() + 2500; Date.now() < end; await sleep(80)) { + const p = await read(); + if (p && last && Math.abs(p.x - last.x) < 1.5 && Math.abs(p.y - last.y) < 1.5) return p; + last = p; + } + return last; } /** Client point of a board tile centre (top face). */