0.2.6-dev: a piece has no screen rect until it is first drawn — pieceScreenRect returned a just-built view's unprojected (0, 0) record, so a browser test grabbed a piece bought that frame at the canvas corner (the intermittent .fwheel__dia timeout in coop.e2e / real.e2e); the tests' piecePoint waits for a drawn piece that holds still; golden: unchanged

This commit is contained in:
sganggs committed 2026-10-13 15:54:40 +08:00
1 parent 2b685011e9
commit 1df530f2b9
4 files changed
+38 -9

No files matched your search

+3 -1
View File
@@ -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;
+6
View File
@@ -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) {
+15 -4
View File
@@ -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. */
+14 -4
View File
@@ -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). */