diff --git a/data/landing.json b/data/landing.json index 4c5f560..e198fe0 100644 --- a/data/landing.json +++ b/data/landing.json @@ -69,7 +69,14 @@ { "land": "ice-land-03.mp4", "surface": "ice-surface-03.mp4", "takeoff": "ice-takeoff-03.mp4", "shop": "ice-shop-03.mp4" }, { "land": "ice-land-01.mp4", "surface": "ice-surface-01.mp4", "takeoff": "ice-takeoff-01.mp4", "shop": "ice-shop-01.mp4" }, { "land": "lava-land-02.mp4", "surface": "lava-surface-02.mp4", "takeoff": "lava-takeoff-02.mp4", "shop": "ice-shop-02.mp4" }, - { "land": "ice-land-03.mp4", "surface": "ice-surface-03.mp4", "takeoff": "ice-takeoff-03.mp4", "shop": "ice-shop-03.mp4" } + { "land": "ice-land-03.mp4", "surface": "ice-surface-03.mp4", "takeoff": "ice-takeoff-03.mp4", "shop": "ice-shop-03.mp4" }, + { "land": "ice-land-01.mp4", "surface": "ice-surface-01.mp4", "takeoff": "ice-takeoff-01.mp4", "shop": "ice-shop-01.mp4" }, + { "land": "lava-land-02.mp4", "surface": "lava-surface-02.mp4", "takeoff": "lava-takeoff-02.mp4", "shop": "ice-shop-02.mp4" }, + { "land": "ice-land-03.mp4", "surface": "ice-surface-03.mp4", "takeoff": "ice-takeoff-03.mp4", "shop": "ice-shop-03.mp4" }, + { "land": "capital-land-neutral.mp4", "surface": "ice-surface-01.mp4", "takeoff": "ice-takeoff-01.mp4", "shop": "ice-shop-01.mp4" }, + { "land": "lava-land-02.mp4", "surface": "lava-surface-02.mp4", "takeoff": "lava-takeoff-02.mp4", "shop": "ice-shop-02.mp4" }, + { "land": "ice-land-03.mp4", "surface": "ice-surface-03.mp4", "takeoff": "ice-takeoff-03.mp4", "shop": "ice-shop-03.mp4" }, + { "land": "ice-land-01.mp4", "surface": "ice-surface-01.mp4", "takeoff": "ice-takeoff-01.mp4", "shop": "ice-shop-01.mp4" } ], "stationVideos": [ diff --git a/dev/shop-shot.mjs b/dev/shop-shot.mjs index c609208..c93907f 100644 --- a/dev/shop-shot.mjs +++ b/dev/shop-shot.mjs @@ -164,6 +164,115 @@ const bounds = (o) => { sv.sellGrid.hoverCell(0); out.tooltip.finalSell = tipState(sv.sellGrid); + // ---- BUY ROW BAR PROBE (the semi-transparent plates behind each row) --- + // The bar must sit BEHIND its row: the bg object is positioned at the row + // CENTER, and its fillRect is drawn around the object's OWN local origin. + // (A double offset — bg moved to the row center AND filled at the row's + // absolute coords — used to fling the bars right + down with stretched + // spacing. Capture the real fillRect call to verify both halves.) + const bl = sv.buyList; + const barRows = []; + for (const r of bl.rows.slice(0, 3)) { + const calls = []; + const orig = r.objs.bg?.fillRect; + if (orig) r.objs.bg.fillRect = (...a) => { calls.push(a.map(Math.round)); return orig.apply(r.objs.bg, a); }; + bl._renderRows(); + if (orig) r.objs.bg.fillRect = orig; + barRows.push({ + id: r.id, + bgCenter: { x: round(r.objs.bg?.x ?? -1), y: round(r.objs.bg?.y ?? -1) }, + rowCenter: { x: round(bl.vpX + bl.vpW / 2), y: round(r.y0 - bl._scroll) }, + fillRect: calls[0] ?? null, + }); + } + out.buyRowBars = { + vpW: bl.vpW, rowH: bl.rowH, + rows: barRows, + ok: barRows.every((b) => + b.bgCenter.x === b.rowCenter.x && b.bgCenter.y === b.rowCenter.y && + b.fillRect?.length === 4 && + b.fillRect[0] === -Math.round(bl.vpW / 2) && b.fillRect[1] === -Math.round(bl.rowH / 2) && + b.fillRect[2] === bl.vpW && b.fillRect[3] === bl.rowH), + }; + out.pass = out.pass && out.buyRowBars.ok; + + // ---- BUY STEPPER HITBOX PROBE (− / qty / + taps) ----------------------- + // This build calls hitAreaCallback(hitArea, localX, localY, obj) with + // localX/Y in the object's TOP-LEFT-anchored frame (the input adds + // displayOrigin — see pointWithinHitArea/getLocalPoint in the phaser + // build), so the glyph spans (0,0)-(w,h) there regardless of origin and + // the tap square must be centered on the glyph (w/2, h/2). A square + // centered on (0,0) sits upper-left of the visible button. Verify + // end-to-end with REAL canvas mouse events: + // - a click ON a visible glyph center must register (plus +1, minus −1); + // - a click just outside the new square, still inside the span of the + // old (mis-anchored) square, must NOT register. + const r0 = bl.rows[0]; + const stepProbe = (el) => ({ + origin: { x: el?.originX ?? null, y: el?.originY ?? null }, + size: { w: round(el?.width ?? 0), h: round(el?.height ?? 0) }, + }); + const stepNames = ['minus', 'qty', 'plus']; + const stepChecks = {}; + for (const k of stepNames) stepChecks[k] = stepProbe(r0.objs[k]); + + const clickCanvas = (wx, wy) => { + const c = game.canvas; + const r = c.getBoundingClientRect(); + const cx = r.left + (wx / 1280) * r.width; + const cy = r.top + (wy / 720) * r.height; + const opts = { clientX: cx, clientY: cy, bubbles: true, view: window, button: 0 }; + c.dispatchEvent(new MouseEvent('mousedown', opts)); + c.dispatchEvent(new MouseEvent('mouseup', opts)); + }; + const plusEl = r0.objs.plus; + const minusEl = r0.objs.minus; + const qtyEl = r0.objs.qty; + const pCx = bl.x + plusEl.x; // glyph center in world/canvas coords (origin 0.5) + const pCy = bl.y + plusEl.y; + const mCx = bl.x + minusEl.x; + const PAD = 6; // must match _bindStep's tap padding + bl.buyQty.delete(r0.id); bl._renderRows(); + await sleep(80); + const qStart = bl.buyQty.get(r0.id) ?? 0; + clickCanvas(pCx, pCy); // PLUS: on the visible glyph + await sleep(120); + const qAfterPlus = bl.buyQty.get(r0.id) ?? 0; + // Discriminator for the OLD (mis-anchored) box: the old plus box spanned + // [pCx-16, pCx+4] (anchored on the glyph top-left corner). The NEW boxes + // (glyph-centered) leave a small GAP between qty and plus — intersect it + // with the old box and pick the middle: hits under the old code, misses + // under the new. + const spacing = Math.round(plusEl.x - qtyEl.x); + const qtyWNow = qtyEl.width; + const gapL = pCx - spacing + qtyWNow / 2 + PAD; // qty box right edge + const gapR = pCx - plusEl.width / 2 - PAD; // plus box left edge + const L = Math.max(gapL, pCx - 16); + const R = Math.min(gapR, pCx + 4); + const missX = Math.round((L + R) / 2); + const miss = { missX, window: [Math.round(gapL), Math.round(gapR)], gapPx: Math.round((gapR - gapL) * 10) / 10, insideOldPlusBox: missX >= pCx - 16 && missX <= pCx + 4 }; + clickCanvas(missX, pCy); + await sleep(120); + const qAfterMiss = bl.buyQty.get(r0.id) ?? 0; + clickCanvas(mCx, pCy); // MINUS: on the visible glyph + await sleep(120); + const qAfterMinus = bl.buyQty.get(r0.id) ?? 0; + out.buySteppers = { + rows: stepChecks, + clicks: { + plusOnGlyph: qAfterPlus - qStart, + outsideBox: qAfterMiss - qAfterPlus, + minusOnGlyph: qAfterMinus - qAfterMiss, + }, + miss, + ok: stepNames.every((k) => stepChecks[k].origin?.x === 0.5 && stepChecks[k].origin?.y === 0.5) + && gapR > gapL + && (qAfterPlus - qStart) === 1 + && (qAfterMiss - qAfterPlus) === 0 + && (qAfterMinus - qAfterMiss) === -1, + }; + out.pass = out.pass && out.buySteppers.ok; + out.screen = { w: ss.scale.width, h: ss.scale.height }; out.topY = sv.topY ?? null; out.hudName = (() => { const t = ss.hudName; if (t?.getTopLeft) { const v = t.getTopLeft(undefined, true); return { x: round(v.x), y: round(v.y) }; } return null; })(); diff --git a/js/ui/shop/BuyList.js b/js/ui/shop/BuyList.js index 8e7142d..910dc11 100644 --- a/js/ui/shop/BuyList.js +++ b/js/ui/shop/BuyList.js @@ -249,39 +249,62 @@ export class BuyList extends Phaser.GameObjects.Container { ); if (priceEl) { priceEl.setOrigin?.(1, 0.5); row.objs.price = priceEl; this._objs.push(priceEl); this.add?.(priceEl); } - // stepper: − qty + (the qty tap = +1, the old grid's behavior) - const mkStep = (str) => this._makeText(0, 0, str, { - fontSize: `${this.st.fontSize ?? 19}px`, - color: this.st.btn ?? '#7d92c4', align: 'center', - }); + // stepper: − qty + (the qty tap = +1, the old grid's behavior). + // Centered origin: _btnXs() positions each stepper at its intended + // CENTER, so render the glyph centered on that point (name/desc/price + // above set their origins the same way). The tap square is centered on + // the glyph in _bindStep. + const mkStep = (str) => { + const t = this._makeText(0, 0, str, { + fontSize: `${this.st.fontSize ?? 19}px`, + color: this.st.btn ?? '#7d92c4', align: 'center', + }); + t?.setOrigin?.(0.5, 0.5); + return t; + }; const minus = mkStep('−'); const qty = mkStep('0'); const plus = mkStep('+'); - const hit = (el) => (el && el.width > 0 ? el.width + 8 : 22); - const bx = this._btnXs(); - const stepHit = Math.max(hit(minus), hit(plus), 22) / 2; if (minus) { row.objs.minus = minus; this._objs.push(minus); this.add?.(minus); } if (qty) { row.objs.qty = qty; this._objs.push(qty); this.add?.(qty); } if (plus) { row.objs.plus = plus; this._objs.push(plus); this.add?.(plus); } // The steppers sit on the scroll surface; stopPropagation keeps a // stepper tap from starting a list drag (TradeGrid's recipe). - this._bindStep(minus, () => this.bumpBuy(id, -1), bx.minus, stepHit, row); - this._bindStep(qty, () => this.bumpBuy(id, +1), bx.qty, stepHit, row); - this._bindStep(plus, () => this.bumpBuy(id, +1), bx.plus, stepHit, row); + this._bindStep(minus, () => this.bumpBuy(id, -1), row); + this._bindStep(qty, () => this.bumpBuy(id, +1), row); + this._bindStep(plus, () => this.bumpBuy(id, +1), row); this.rows.push(row); } - _bindStep(el, fn, x, half, row) { + _bindStep(el, fn, row) { if (!el || typeof el.setInteractive !== 'function' || typeof el.on !== 'function') return; const Rect = Phaser.Geom?.Rectangle; - const area = Rect ? new Rect(-half, -half, half * 2, half * 2) : null; + // THIS BUILD's input convention (pointWithinHitArea / getLocalPoint in + // the phaser build): hitAreaCallback(hitArea, localX, localY, obj), with + // localX/Y in the object's TOP-LEFT-anchored frame (the input adds + // displayOrigin) — so a Text glyph always spans (0,0)-(w,h) there, + // regardless of origin. (A fixed rect centered on (0,0) sits upper-left + // of the visible button, anchored on its top-left corner.) + // + // Test against the LIVE glyph size inside the callback: the qty text + // re-measures on every bump ("0"=25px wide, "1"=17px ...), the + // top-left anchor moves with it, and a rect baked at bind time slides + // (the narrower "1" made the old box drift right and swallow the gap + // toward "+"). + const PAD = 6; + const live = (px, py) => { + const w = el.width > 0 ? el.width : 12; + const h = el.height > 0 ? el.height : 18; + return px >= -PAD && px <= w + PAD && py >= -PAD && py <= h + PAD; + }; + const area = Rect ? new Rect(-PAD, -PAD, 1, 1) : null; // placeholder — the callback does the real test el.setInteractive({ useHandCursor: true, hitArea: area, // Dead when the row is scrolled out of the viewport (alpha-0 buttons // must not be tappable — gate the hit test on row visibility too). - hitAreaCallback: (p, px, py) => (row.shown ? (area ? Rect.Contains(p, px, py) : true) : false), + hitAreaCallback: (hit, px, py) => (row.shown && live(px, py)), }); el.on('pointerdown', (p) => { p?.stopPropagation?.(); @@ -444,13 +467,16 @@ export class BuyList extends Phaser.GameObjects.Container { // selection cyan), the qty text lit. const sel = q > 0; const bl = this.bl; - const cx = this.vpX + this.vpW / 2; - const y = row.y0 - this._scroll; if (o.bg && typeof o.bg.clear === 'function' && typeof o.bg.fillStyle === 'function') { o.bg.clear?.(); o.bg.fillStyle(_hex(sel ? (bl.rowSelBg ?? '#12283a') : (bl.rowBg ?? '#0e1728')), sel ? (bl.rowSelBgAlpha ?? 0.85) : (bl.rowBgAlpha ?? 0.55)); o.bg.lineStyle?.(sel ? 2 : 1, _hex(sel ? (bl.selected ?? '#00e5ff') : (bl.rowBorder ?? '#1d2e4c')), 255); - o.bg.fillRect?.(cx - this.vpW / 2, y - this.rowH / 2, this.vpW, this.rowH); + // Draw centered on the bg object's OWN local origin — _clipRows() + // already moves the object to the row center (cx, y), and Graphics + // content renders in the object's local space. Filling at the row's + // ABSOLUTE list coords here used to double the offset (bars drifted + // right by cx and down by y, with stretched vertical spacing). + o.bg.fillRect?.(-this.vpW / 2, -this.rowH / 2, this.vpW, this.rowH); } if (o.qty && typeof o.qty.setColor === 'function') { o.qty.setColor(toCss(sel ? (this.st.activeColor ?? '#00e5ff') : (this.st.color ?? '#eaf6ff')));