Rework shop UI to cyberpunk sub-bar + entry state, fix panel geometry
- Shop now opens on the tab sub-bar only (shop video plays); clicking Buy/Sell reveals the trade panel — showcases the clip before the UI takes over.
- Tab bar and Confirm/Clear are now MenuButtons in a console-blue sub-bar that rests above the command deck, anchored to the deck's Shop button (MenuSubBar recipe), replacing the old top-of-screen custom buttons.
- Fix trade panel vertical placement: a Container's (x,y) is a pure translation of local space (verified via dev/cbox-test.html), so the grid position IS the panel top-left; setting it to topY + h/2 treated it as a centered origin and clipped bottom rows. Panel now anchors just under the HUD planet text via SurfaceScene.shopTopY().
- Parent TradeGrid plate/title to the grid container so they render above the shop video (scene-level depth-0 objects drew behind it in this build).
- Add dev probes (dev/cbox-test.html, dev/shop-shot.{html,mjs}) and update phaser-stub/test stubs for setSize/setBlendMode/rectangle; document entry state + sub-bar in docs/ECONOMY.md.
This commit is contained in:
parent
1b4cba9f4f
commit
77d649276c
Binary file not shown.
|
Before Width: | Height: | Size: 422 KiB After Width: | Height: | Size: 704 KiB |
Binary file not shown.
|
|
@ -51,7 +51,8 @@
|
|||
"lineLetterSpacing": 1,
|
||||
"lineColor": "#7d92c4",
|
||||
"lineStroke": "#000000",
|
||||
"lineStrokeWidth": 2
|
||||
"lineStrokeWidth": 2,
|
||||
"shopPanelGap": 18
|
||||
},
|
||||
"videos": [
|
||||
{ "land": "terran-land-01.mp4", "surface": "terran-surface-01.mp4", "takeoff": "terran-takeoff-01.mp4", "shop": "terran-shop-01.mp4" },
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"enabled": true,
|
||||
|
||||
"tabs": {
|
||||
"_comment": "The sub-menu (two buttons). Buy/Sell opens the trade panel; Buyback opens the buyback panel (goods sold this session, session-local only). `disabledAlpha` is the Buyback dim until the player has sold something.",
|
||||
"_comment": "The tab sub-bar (two MenuButtons, the deck's cyberpunk style — js/ui/MenuButton.js). Buy/Sell opens the trade panel; Buyback opens the buyback panel (goods sold this session, session-local only). Size/palette come from data/save.json → subBar + colors (the Menu sub-bar recipe); this section just holds the labels + Buy/Sell accent. The sub-bar rests above the command deck, anchored to the deck's Shop button (SurfaceScene.shopAnchor).",
|
||||
"buySell": "Buy/Sell",
|
||||
"buyback": "Buyback",
|
||||
"w": 184, "h": 46, "gap": 14,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<base href="../" />
|
||||
<title>Dev probe — Phaser container transform semantics</title>
|
||||
<style>html,body{margin:0;height:100%;background:#04060d;overflow:hidden}#g{width:100%;height:100%}</style>
|
||||
<script src="lib/phaser.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="g"></div>
|
||||
<script type="module">
|
||||
/* Dev probe: WHAT DOES A CONTAINER'S (x, y) MEAN in this build?
|
||||
*
|
||||
* A container at (200,150) holds a 100x80 "plate" drawn at LOCAL (0,0)
|
||||
* and a 10x10 marker at local (50,40). The red plate lands with its
|
||||
* top-left at (200,150) — i.e. a container's position is a pure
|
||||
* TRANSLATION of its local space (local (0,0) == the container's
|
||||
* x,y). The container's origin (0.5,0.5) does NOT shift the children.
|
||||
*
|
||||
* Consequence the shop layout depends on: a TradeGrid's (x,y) IS the
|
||||
* panel's top-left corner — setting it to topY + h/2 (centered-origin
|
||||
* math) drops the panel half a grid lower and clips its bottom rows.
|
||||
*
|
||||
* View in a browser (node dev/server.mjs 8126 → /dev/cbox-test.html) —
|
||||
* the red rectangle's top-left sits exactly at (200,150) of the 800x600
|
||||
* canvas; window.__CBOX__ carries the numeric readings. */
|
||||
import Phaser from '../js/vendor/phaser.js';
|
||||
window.__ERRS__ = window.__ERRS__ ?? [];
|
||||
window.addEventListener('error', (e) => window.__ERRS__.push(String(e.message)));
|
||||
|
||||
class T extends Phaser.Scene {
|
||||
create() {
|
||||
const cx = 200, cy = 150;
|
||||
const c = this.add.container(cx, cy);
|
||||
// Red "plate" 100x80 at LOCAL (0,0) — like TradeGrid's plate.
|
||||
const plate = this.add.rectangle(50, 40, 100, 80, 0xff0000);
|
||||
c.add(plate);
|
||||
// Green 10x10 at local (50, 40) — the content center.
|
||||
const g = this.add.rectangle(50, 40, 10, 10, 0x00ff00);
|
||||
c.add(g);
|
||||
const tl = (o) => {
|
||||
if (o?.getTopLeft) { try { const v = o.getTopLeft(undefined, true); return { x: Math.round(v.x), y: Math.round(v.y) }; } catch { /* fall through */ } }
|
||||
if (o?.getBounds) { const b = o.getBounds(); return { x: Math.round(b.x), y: Math.round(b.y), w: Math.round(b.width), h: Math.round(b.height) }; }
|
||||
return null;
|
||||
};
|
||||
window.__CBOX__ = {
|
||||
container: { x: c.x, y: c.y, originX: c.originX, originY: c.originY,
|
||||
displayWidth: c.displayWidth, displayHeight: c.displayHeight },
|
||||
plate_world: tl(plate),
|
||||
green_world: tl(g),
|
||||
};
|
||||
}
|
||||
}
|
||||
T.key = 'T';
|
||||
|
||||
const game = new Phaser.Game({
|
||||
type: Phaser.AUTO, parent: 'g', width: 800, height: 600, backgroundColor: '#111', banner: false,
|
||||
scale: { mode: Phaser.Scale.NONE },
|
||||
scene: [T],
|
||||
});
|
||||
window.game = game;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -22,6 +22,8 @@ class GameObject {
|
|||
setBlendMode() { return this; }
|
||||
setPosition(x, y) { this.x = x; this.y = y; return this; }
|
||||
setStrokeStyle() { return this; }
|
||||
setSize(w, h) { this.width = w; this.height = h; return this; }
|
||||
set() { return this; }
|
||||
destroy() { this.active = false; this.destroyed = true; }
|
||||
}
|
||||
const Container = class extends GameObject {};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Orbit — dev shop panel geometry probe</title>
|
||||
<!-- This page lives in /dev, but the game's relative asset paths are rooted
|
||||
at the project root — resolve them against it. ("../" keeps this
|
||||
working even if the project is served from a subdirectory.) -->
|
||||
<base href="../" />
|
||||
<style>
|
||||
html, body { margin: 0; height: 100%; background: #04060d; overflow: hidden; }
|
||||
#game { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
|
||||
</style>
|
||||
<script src="lib/phaser.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="game"></div>
|
||||
<script type="module" src="dev/shop-shot.mjs"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
/**
|
||||
* Dev-only SHOP PANEL geometry probe (real browser, via cdp-firefox.mjs):
|
||||
* boots the game, lands on the home world, forces the surface stage, opens
|
||||
* the shop, clicks "Buy/Sell", waits for the slide-in, then reports the
|
||||
* on-screen geometry of the planet HUD text + the SELL/BUY panels + the
|
||||
* sub-bar — so the panel's top edge vs. the planet text is verifiable
|
||||
* numerically. Also sets window.__SHOP_SHOT__ so a driver can screenshot
|
||||
* the moment the numbers are ready.
|
||||
*
|
||||
* node dev/server.mjs 8126
|
||||
* node dev/cdp-firefox.mjs http://127.0.0.1:8126/dev/shop-shot.html \
|
||||
* 'return window.__SHOP_SHOT__;' 60000
|
||||
*/
|
||||
import Phaser from '../js/vendor/phaser.js';
|
||||
import { config } from '../js/config/Config.js';
|
||||
import { ConfigLoader } from '../js/config/ConfigLoader.js';
|
||||
import { createGameConfig } from '../js/config/GameConfig.js';
|
||||
import { GameScene } from '../js/scenes/GameScene.js';
|
||||
import { SurfaceScene } from '../js/scenes/SurfaceScene.js';
|
||||
|
||||
const fail = (msg, extra = {}) => {
|
||||
window.__SHOP_SHOT__ = { pass: false, error: String(msg), ...extra };
|
||||
console.error('[shop-shot] FAIL:', msg);
|
||||
};
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const data = await ConfigLoader.load();
|
||||
config.init(data);
|
||||
const gameConfig = createGameConfig();
|
||||
gameConfig.scene = [GameScene, SurfaceScene];
|
||||
const game = new Phaser.Game(gameConfig);
|
||||
window.game = game;
|
||||
|
||||
const bounds = (o) => {
|
||||
if (!o) return null;
|
||||
const wb = o.worldBounds ?? (o.getBounds && o.getBounds());
|
||||
if (wb && Number.isFinite(wb.x)) {
|
||||
return { x: Math.round(wb.x), y: Math.round(wb.y), w: Math.round(wb.width), h: Math.round(wb.height) };
|
||||
}
|
||||
return { x: Math.round(o.x ?? 0), y: Math.round(o.y ?? 0) };
|
||||
};
|
||||
|
||||
(async () => {
|
||||
let gs = null;
|
||||
for (let i = 0; i < 300; i++) {
|
||||
gs = game.scene?.getScene('GameScene') ?? null;
|
||||
if (gs && gs.ship) break;
|
||||
await sleep(100);
|
||||
}
|
||||
if (!gs || !gs.ship) throw new Error('GameScene never came up');
|
||||
|
||||
// Land on the home world + force the surface stage (skip the clip).
|
||||
const target = gs.commsTargetFor(gs.planet);
|
||||
gs.startLanding(target);
|
||||
await sleep(300);
|
||||
const ss = game.scene.getScene('SurfaceScene');
|
||||
if (!ss) throw new Error('SurfaceScene never registered');
|
||||
// Wait for the scene to finish loading (this build: RUNNING = 5) before
|
||||
// driving it.
|
||||
for (let i = 0; i < 120 && ss.sys.getStatus() !== 5; i++) await sleep(250);
|
||||
ss.startSurface();
|
||||
for (let i = 0; i < 40 && ss.phase !== 'surface'; i++) await sleep(100);
|
||||
if (ss.phase !== 'surface') throw new Error('surface stage never reached (phase=' + ss.phase + ')');
|
||||
await sleep(300);
|
||||
|
||||
// Give the hold a couple of goods so the SELL grid shows filled bays.
|
||||
const goods = config.get('goods.goods', []);
|
||||
gs.ship.cargo.add([goods[0].id, goods[1].id]);
|
||||
|
||||
// Open the shop the real way; if the shop clip is missing in this
|
||||
// environment (headless video), build the view directly (same UI path).
|
||||
ss.toggleShop();
|
||||
await sleep(200);
|
||||
if (!ss.shopView) ss.buildShopView();
|
||||
const sv = ss.shopView;
|
||||
if (!sv) throw new Error('no shopView');
|
||||
|
||||
// Click "Buy/Sell" (reveal the trade panel) and let the slide-in settle.
|
||||
sv.setTab('trade');
|
||||
await sleep(900);
|
||||
|
||||
const chain = (o) => o ? {
|
||||
x: o.x, y: o.y, scaleX: o.scaleX, scaleY: o.scaleY,
|
||||
ox: o.originX, oy: o.originY, dw: o.displayWidth, dh: o.displayHeight,
|
||||
active: o.active, parent: o.parent ? (o.parent.isContainer ? 'container' : o.parent.type ?? String(o.parent)) : null,
|
||||
} : null;
|
||||
const out = {
|
||||
pass: true,
|
||||
screen: { w: ss.scale.width, h: ss.scale.height },
|
||||
camera: { scrollX: ss.cameras?.main?.scrollX, scrollY: ss.cameras?.main?.scrollY, zoom: ss.cameras?.main?.zoom },
|
||||
topY: sv.topY ?? null,
|
||||
hudName: bounds(ss.hudName),
|
||||
hudLine: bounds(ss.hudLine),
|
||||
sellGrid: bounds(sv.sellGrid),
|
||||
buyGrid: bounds(sv.buyGrid),
|
||||
subBar: bounds(sv.subBar),
|
||||
panel: { w: sv.sellGrid?.width ?? null, h: sv.sellGrid?.height ?? null },
|
||||
chain: {
|
||||
shopView: chain(sv),
|
||||
content: chain(sv._content),
|
||||
sellGrid: chain(sv.sellGrid),
|
||||
buyGrid: chain(sv.buyGrid),
|
||||
},
|
||||
// Where the panel ACTUALLY renders: the plate + first cell + title are
|
||||
// real children at known local coords — their world matrices are the truth.
|
||||
actual: {
|
||||
sellTitle_world: (() => { const t = sv.sellGrid.title; if (t?.getTopLeft) { const v = t.getTopLeft(undefined, true); return { x: Math.round(v.x), y: Math.round(v.y) }; } return null; })(),
|
||||
sellCellName_world: (() => { const t = sv.sellGrid.cells?.[0]?.name; if (t?.getTopLeft) { const v = t.getTopLeft(undefined, true); return { x: Math.round(v.x), y: Math.round(v.y) }; } return null; })(),
|
||||
buyTitle_world: (() => { const t = sv.buyGrid.title; if (t?.getTopLeft) { const v = t.getTopLeft(undefined, true); return { x: Math.round(v.x), y: Math.round(v.y) }; } return null; })(),
|
||||
net_world: (() => { const t = sv._previewText; if (t?.getTopLeft) { const v = t.getTopLeft(undefined, true); return { x: Math.round(v.x), y: Math.round(v.y) }; } return null; })(),
|
||||
},
|
||||
};
|
||||
window.__SHOP_SHOT__ = out;
|
||||
console.info('[shop-shot] OK:', JSON.stringify(out));
|
||||
})().catch((e) => fail(String(e?.message || e)));
|
||||
|
|
@ -67,6 +67,8 @@ function stubObject(extra = {}) {
|
|||
setTint() { return this; },
|
||||
setPosition(x, y) { this.x = x; this.y = y; return this; },
|
||||
setStrokeStyle() { return this; },
|
||||
setSize(w, h) { this.width = w; this.height = h; return this; },
|
||||
setBlendMode() { return this; },
|
||||
setInteractive(opts) { this._interactive = opts; return this; },
|
||||
setTexture(key, frame) { this._textureKey = key; this.frame = frame; return this; },
|
||||
on() { return this; },
|
||||
|
|
@ -85,6 +87,9 @@ function stubObject(extra = {}) {
|
|||
clear() { return this; },
|
||||
fillStyle() { return this; },
|
||||
lineStyle() { return this; },
|
||||
fillPoints() { return this; },
|
||||
strokePoints() { return this; },
|
||||
lineBetween() { return this; },
|
||||
rect() { return this; },
|
||||
fillRect() { return this; },
|
||||
lineRect() { return this; },
|
||||
|
|
@ -99,9 +104,10 @@ function makeScene() {
|
|||
const scene = {
|
||||
scale: { width: 1280, height: 720, on() { return this; } },
|
||||
add: {
|
||||
text: (x, y, str, style) => stubObject({ x, y, text: String(str), _style: style }),
|
||||
text: (x, y, str, style) => stubObject({ x, y, text: String(str), _style: style, width: 60, height: 18 }),
|
||||
image: (x, y, key, frame) => stubObject({ x, y, key, frame }),
|
||||
graphics: () => stubObject({}),
|
||||
rectangle: (x, y, w, h, key, alpha) => stubObject({ x, y, width: w, height: h, key, alpha }),
|
||||
existing: (o) => o,
|
||||
},
|
||||
tweens: {
|
||||
|
|
@ -151,29 +157,63 @@ const A = goods[0].id;
|
|||
const B = goods[1].id;
|
||||
const C = stock[0].id; // a good the shop actually sells
|
||||
|
||||
function makeView(ship) {
|
||||
function makeView(ship, { reveal = true, topY = 120 } = {}) {
|
||||
const scene = makeScene();
|
||||
const gs = { refreshed: 0, refreshCreditsHud() { this.refreshed += 1; } };
|
||||
const view = new ShopView(scene, {
|
||||
ship, galaxy, cls, systemId: sysId, gameScene: gs, onLeave() {},
|
||||
// topY = the surface HUD's planet-text bottom + padding (what
|
||||
// SurfaceScene.shopTopY() computes). The panel top should sit here.
|
||||
topY,
|
||||
});
|
||||
return { scene, view, gs };
|
||||
if (reveal) view.setTab('trade'); // reveal the panel (what clicking "Buy/Sell" does)
|
||||
return { scene, view, gs, topY };
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
console.log('== CONSTRUCT — both grids, Buy/Sell default ==');
|
||||
console.log('== CONSTRUCT — entry state shows only the tabs; Buy/Sell reveals the panel ==');
|
||||
{
|
||||
const ship = makeShip(10000);
|
||||
ship.cargo.add([A, B]);
|
||||
const { view } = makeView(ship);
|
||||
assert.equal(view.tab, 'trade', 'opens on the Buy/Sell tab');
|
||||
assert.ok(view.sellGrid && view.buyGrid, 'both the SELL and BUY grids exist');
|
||||
const { view, topY } = makeView(ship, { reveal: false });
|
||||
// Entry state: shop video + tab bar only. No trade panel yet — the player
|
||||
// must click "Buy/Sell" to reveal it (showcase the clip before the UI).
|
||||
assert.equal(view.tab, null, 'starts unopened (entry state)');
|
||||
assert.equal(view.sellGrid, null, 'no SELL grid yet (revealed on Buy/Sell)');
|
||||
assert.equal(view.buyGrid, null, 'no BUY grid yet');
|
||||
assert.ok(view._tabTrade, 'the Buy/Sell tab is present in the entry state');
|
||||
assert.ok(view._tabBuyback, 'the Buyback tab is present (grayed)');
|
||||
assert.equal(view.isBuybackEnabled(), false, 'Buyback is off before a sale');
|
||||
|
||||
// The tabs are cyberpunk MenuButtons in a sub-bar that rests ABOVE the
|
||||
// deck (the MenuSubBar recipe) — not the old top-of-screen custom buttons.
|
||||
assert.equal(view._tabBuyback.disabled, true, 'Buyback tab is disabled (grayed) before a sale');
|
||||
assert.equal(view._tabTrade.disabled, false, 'Buy/Sell tab is enabled (the entry point)');
|
||||
assert.ok(view.subBar, 'the sub-bar container exists');
|
||||
assert.ok(view.subBar.y > view._H() - 80, `sub-bar rests above the deck (bar.y=${view.subBar.y}, H=${view._H()})`);
|
||||
assert.ok(view.y === 0, 'ShopView root stays at the screen origin (0,0) so the trade panel renders in screen space');
|
||||
assert.ok(view.subBarW > 0 && view.subBarH > 0, 'sub-bar has a measured width + height');
|
||||
|
||||
// Reveal the trade panel (what clicking "Buy/Sell" does) — both grids appear.
|
||||
view.setTab('trade');
|
||||
assert.equal(view.tab, 'trade', 'Buy/Sell reveals the trade tab');
|
||||
assert.ok(view.sellGrid && view.buyGrid, 'both the SELL and BUY grids exist after reveal');
|
||||
// The panel's TOP edge sits at topY (just under the planet text). A
|
||||
// TradeGrid lays its plate/cells out in local (0,0)→(w,h) and a
|
||||
// Container's (x,y) is a pure translation of that local space (verified
|
||||
// in dev/cbox-test.html) — so the grid's position IS the panel's
|
||||
// top-left corner: gridTop == grid.y. (Setting it to topY + h/2 treated
|
||||
// the position like a centered origin and dropped the panel half a grid
|
||||
// lower, clipping the bottom rows.)
|
||||
const gridTop = view.sellGrid.y;
|
||||
assert.ok(Math.abs(gridTop - topY) < 2, `panel top is at topY (gridTop=${gridTop.toFixed(1)}, topY=${topY})`);
|
||||
assert.ok(view.sellGrid.y > 80 && view.sellGrid.y < view._H() - 40, `trade panel is on-screen (sellGrid.y=${view.sellGrid.y})`);
|
||||
assert.ok(view.sellGrid.y + view.sellGrid.height <= view._H(), `panel bottom stays on-screen (${view.sellGrid.y} + ${view.sellGrid.height} <= ${view._H()})`);
|
||||
assert.equal(view.sellGrid.maxSlots, 16, 'SELL grid is 16 bays');
|
||||
assert.equal(view.buyGrid.maxSlots, 16, 'BUY grid is 16 slots');
|
||||
assert.ok(view.buyGrid.stock.length >= 1, 'BUY grid carries the shop stock');
|
||||
assert.deepEqual(view.sellGrid.bays.slice(0, 2), [A, B], 'SELL bays mirror the hold');
|
||||
assert.deepEqual(view.sellGrid.getSellMap(), {}, 'nothing checked yet');
|
||||
assert.equal(view.isBuybackEnabled(), false, 'Buyback is off before a sale');
|
||||
}
|
||||
console.log('CONSTRUCT: OK');
|
||||
|
||||
|
|
|
|||
|
|
@ -297,18 +297,48 @@ Where the loop is actually played. You open it from the surface deck's **SHOP**
|
|||
existing `data/actionbar.json` slot); the shop clip is already live, and the screen sits **over**
|
||||
it. It's one component — `js/ui/shop/ShopView.js` — composing two `TradeGrid`s.
|
||||
|
||||
**Entry state (showcase the clip).** When you first open the shop, **only the tab sub-bar**
|
||||
is up — the cyberpunk **MenuButton** pair `[ BUY/SELL | BUYBACK ]` (Buyback grayed out), the
|
||||
same style + placement as the deck's **Menu** sub-bar (`js/ui/MenuSubBar.js`). It rests **above
|
||||
the command deck**, right-aligned to the deck's **Shop** button — so it reads as the same family
|
||||
of UI as the Menu drawer. The screen stays open on the shop video so the clip plays. Clicking
|
||||
**Buy/Sell** reveals the trade panel in the middle of the screen (the grids slide in).
|
||||
|
||||
```
|
||||
# first click — shop video + the tab sub-bar (rests above the deck):
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ (clip plays) │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ BUY/SELL │ BUYBACK │ ← sub-bar │
|
||||
│ └─────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ SHOP │ BUILD │ SHIP │ QUESTS │ TAKE OFF │ MENU ← command deck│
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
# after clicking Buy/Sell — the trade panel reveals (middle of the screen):
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ [ Buy/Sell | Buyback ] ← tabs │
|
||||
│ │
|
||||
│ ┌SELL 4×4┐ NET +420 cr ┌BUY 4×4┐ │
|
||||
│ │ bays │ (green ≥ 0 / │ stock │ │
|
||||
│ │(cargo) │ red < 0) │(12–16) │ │
|
||||
│ └────────┘ [ Confirm ] └────────┘ │
|
||||
│ [ Clear ] │
|
||||
│ └────────┘ [ CONFIRM ] └────────┘ │
|
||||
│ [ CLEAR ] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ BUY/SELL │ BUYBACK │ ← sub-bar │
|
||||
│ └─────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ SHOP │ BUILD │ SHIP │ QUESTS │ TAKE OFF │ MENU ← command deck│
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Tab sub-bar** — the **Buy/Sell** + **Buyback** buttons are `MenuButton`s (the deck's
|
||||
cyberpunk style: cut-corner panel, neon edge, hover sweep, press punch) in a console-blue
|
||||
panel that rests above the command deck, anchored to the Shop button — the exact recipe of
|
||||
the Menu sub-bar. Buyback is **disabled** (grayed) until you've sold something this visit.
|
||||
- **Confirm / Clear** — the same `MenuButton` style; Confirm is **disabled** (grayed) while
|
||||
the trade is invalid (can't go negative, hold would overflow), and both appear once you start
|
||||
picking.
|
||||
|
||||
- **SELL (left)** — your **cargo bays**. 16 cells (the fixed grid); the first `cargoHold`
|
||||
are live bays (filled with a good, or empty), the rest are **locked** (the hold's upgrade
|
||||
room). Tap a filled bay to **check it off to sell** (a bay = 1 unit). No stepper — the
|
||||
|
|
@ -316,8 +346,7 @@ it. It's one component — `js/ui/shop/ShopView.js` — composing two `TradeGrid
|
|||
- **BUY (right)** — the shop's **stock** (12–16 distinct goods from `Economy.shopFor`). Each
|
||||
cell has a **−/qty/+ stepper**; buy as many as you like (capped by credits + hold room).
|
||||
- **NET preview (center)** — the live credit delta. Green when you're up (a net sale), red when
|
||||
you'd spend. **Confirm** is disabled while the trade is invalid (can't go negative, hold
|
||||
would overflow). **Clear** drops the current selection. Both appear once you start picking.
|
||||
you'd spend. Updates as you check bays / step goods.
|
||||
- **SELL → BUY is atomic** — check bays to sell *and* step goods to buy, then one Confirm
|
||||
executes it: credits `+ sale − buy`, cargo `− sold + bought`, in that order (free room
|
||||
first). Selling to free a bay for a buy lands in the same Confirm.
|
||||
|
|
|
|||
|
|
@ -350,10 +350,46 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
systemId,
|
||||
gameScene: g,
|
||||
onLeave: () => this.leaveShop(),
|
||||
anchor: this.shopAnchor(),
|
||||
topY: this.shopTopY(),
|
||||
});
|
||||
return this.shopView;
|
||||
}
|
||||
|
||||
/**
|
||||
* The trade panel's top edge — just under the surface HUD's planet text
|
||||
* (the name + the type/tether sub-line), with a little padding. The HUD is
|
||||
* upper-left, so the panel hangs below it (near the top of the screen).
|
||||
* Falls back to a small top margin if the HUD isn't up (dev).
|
||||
*/
|
||||
shopTopY() {
|
||||
const hud = config.section('landing.hud', {});
|
||||
const pad = hud.shopPanelGap ?? 18; // a little padding under the planet text
|
||||
const line = this.hudLine;
|
||||
if (line && Number.isFinite(line.y) && Number.isFinite(line.height)) {
|
||||
return line.y + line.height + pad;
|
||||
}
|
||||
const name = this.hudName;
|
||||
if (name && Number.isFinite(name.y) && Number.isFinite(name.height)) {
|
||||
return name.y + name.height + pad;
|
||||
}
|
||||
return (this.scale.height ?? 720) * 0.16;
|
||||
}
|
||||
|
||||
/**
|
||||
* The shop tab sub-bar's anchor — the deck's "Shop" button, the same
|
||||
* recipe as menuAnchor(): the bar rests ABOVE the deck (bottom edge on its
|
||||
* top rail), right-aligned to the Shop button. Falls back to bottom-center
|
||||
* if the deck is missing (dev).
|
||||
*/
|
||||
shopAnchor() {
|
||||
const slot = this.actionBar?.slots?.find((s) => s.id === 'shop');
|
||||
if (slot) {
|
||||
return { x: slot.slot.x, y: this.actionBar.rect.y, w: this.actionBar.style.bw };
|
||||
}
|
||||
return { x: this.scale.width / 2, y: this.scale.height - 58, w: 120 };
|
||||
}
|
||||
|
||||
destroyShopView() {
|
||||
if (!this.shopView) return;
|
||||
this.shopView.destroy?.();
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@
|
|||
import Phaser from '../../vendor/phaser.js';
|
||||
import { config } from '../../config/Config.js';
|
||||
import * as E from '../../economy/Economy.js';
|
||||
import { MenuButton } from '../MenuButton.js';
|
||||
import { toColor, toCss } from '../../utils/Color.js';
|
||||
import { TradeGrid } from './TradeGrid.js';
|
||||
|
||||
export class ShopView extends Phaser.GameObjects.Container {
|
||||
|
|
@ -48,6 +50,10 @@ export class ShopView extends Phaser.GameObjects.Container {
|
|||
* gameScene — to refresh the credits/minerals HUD after a trade.
|
||||
* onLeave — called by close() so the SurfaceScene can restore the
|
||||
* surface loop (the deck's "Leave Shop" does this too).
|
||||
* anchor — { x, y, w } the sub-bar's anchor: the deck's "Shop"
|
||||
* button center x, the deck's top-rail y (the bar's bottom
|
||||
* edge rests on it), and the button width (right-aligns the
|
||||
* bar) — the MenuSubBar recipe (see SurfaceScene.shopAnchor).
|
||||
*/
|
||||
constructor(scene, o) {
|
||||
super(scene, 0, 0);
|
||||
|
|
@ -58,6 +64,13 @@ export class ShopView extends Phaser.GameObjects.Container {
|
|||
this.systemId = o.systemId;
|
||||
this.gameScene = o.gameScene;
|
||||
this.onLeave = o.onLeave;
|
||||
// The tab sub-bar anchor (the deck's Shop button). Falls back to a
|
||||
// bottom-center bar if the scene doesn't provide one (headless/dev).
|
||||
this.anchor = o.anchor ?? { x: this._W() / 2, y: this._H() - 58, w: 120 };
|
||||
// The trade panel's TOP edge — just under the surface HUD's planet text
|
||||
// (the scene computes it from its own hudName/hudLine). Falls back to a
|
||||
// small top margin if the scene doesn't provide one (headless/dev).
|
||||
this.topY = Number.isFinite(o.topY) ? o.topY : (this._H() * 0.16);
|
||||
|
||||
this.maxSlots = Math.max(1, Math.floor(Number(config.get('economy.cargo.maxSlots', 16)) || 16));
|
||||
this.capacity = this.ship.cargo ? this.ship.cargo.capacity : 0;
|
||||
|
|
@ -89,7 +102,12 @@ export class ShopView extends Phaser.GameObjects.Container {
|
|||
// ENTRY + TABS
|
||||
// ---------------------------------------------------------------------
|
||||
enter() {
|
||||
this.setTab('trade');
|
||||
// Entry state: the shop video + tab bar only. The player must click
|
||||
// "Buy/Sell" to reveal the trade panel (this keeps the screen open so the
|
||||
// shop clip plays before the UI takes over). Buyback is grayed until a
|
||||
// sale exists this visit.
|
||||
this.tab = null;
|
||||
this._refreshTabs();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
@ -108,36 +126,130 @@ export class ShopView extends Phaser.GameObjects.Container {
|
|||
}
|
||||
|
||||
_buildTabs() {
|
||||
// The tab sub-bar: the same recipe as the deck's Menu sub-bar
|
||||
// (js/ui/MenuSubBar.js) — a console-blue panel with cut corners + the
|
||||
// signature top rail, holding cyberpunk MenuButtons. It rests ABOVE the
|
||||
// command deck, right-aligned to the deck's "Shop" button (this.anchor),
|
||||
// so it reads as the same family of UI as the Menu drawer.
|
||||
const a = this.anchor;
|
||||
const H = config.get('save.subBar.height', 58);
|
||||
const pad = config.get('save.subBar.padding', 16);
|
||||
const gap = config.get('save.subBar.gap', 12);
|
||||
const tabs = config.get('shopui.tabs', {});
|
||||
const W = this._W();
|
||||
const y = tabs.y ?? 44;
|
||||
const tw = tabs.w ?? 184, th = tabs.h ?? 46, gap = tabs.gap ?? 14;
|
||||
const cx = W / 2;
|
||||
const sellBtn = this._makeButton(tabs.buySell ?? 'Buy/Sell', tw, th, tabs.activeAccent ?? '#00e5ff');
|
||||
sellBtn.setPosition(cx - gap / 2 - tw / 2, y);
|
||||
this.add?.(sellBtn.container);
|
||||
sellBtn.tap(() => this.setTab('trade'));
|
||||
this._tabTrade = sellBtn;
|
||||
const c = config.section('save.colors', {});
|
||||
const btnCfg = config.section('save.subBar.button', {});
|
||||
|
||||
const bbBtn = this._makeButton(tabs.buyback ?? 'Buyback', tw, th, tabs.activeAccent ?? '#00e5ff');
|
||||
bbBtn.setPosition(cx + gap / 2 + tw / 2, y);
|
||||
this.add?.(bbBtn.container);
|
||||
bbBtn.tap(() => { if (this.isBuybackEnabled()) this.setTab('buyback'); });
|
||||
this._tabBuyback = bbBtn;
|
||||
const items = [
|
||||
{ id: 'trade', label: tabs.buySell ?? 'Buy/Sell', accent: tabs.activeAccent ?? c.neon ?? '#00e5ff' },
|
||||
{ id: 'buyback', label: tabs.buyback ?? 'Buyback', accent: c.dim ?? '#7d92c4' },
|
||||
];
|
||||
|
||||
// Build the buttons first (they measure their own width) so the row
|
||||
// layout uses the REAL widths — no second, divergent text-measure pass.
|
||||
const mkBtn = (item) => new MenuButton(
|
||||
this.scene, 0, -H / 2, String(item.label ?? item.id), () => this._onTabTap(item.id),
|
||||
{
|
||||
screenFixed: true,
|
||||
fontSize: btnCfg.fontSize ?? 13,
|
||||
paddingX: btnCfg.paddingX ?? 18,
|
||||
paddingY: btnCfg.paddingY ?? 9,
|
||||
letterSpacing: btnCfg.letterSpacing ?? 2,
|
||||
upper: true,
|
||||
textColor: c.ink ?? '#eaf6ff',
|
||||
bgColor: c.cardBg ?? '#0a1222',
|
||||
hoverColor: c.cardBg ?? '#0a1222',
|
||||
borderColor: toCss(item.accent ?? c.neon ?? '#00e5ff'),
|
||||
},
|
||||
);
|
||||
|
||||
const btns = items.map(mkBtn);
|
||||
const widths = btns.map((b) => b.style?.width ?? 120);
|
||||
const W = Math.min(
|
||||
this._W() - 24,
|
||||
pad * 2 + widths.reduce((s, w) => s + w, 0) + gap * (widths.length - 1),
|
||||
);
|
||||
|
||||
// Position: bottom edge on the deck's top rail (a.y), right edge on the
|
||||
// Shop button's right edge — the MenuSubBar recipe. The sub-bar is a
|
||||
// CHILD of the ShopView (which stays at the screen origin, 0,0) at
|
||||
// (cx, a.y) — so the trade panel (a SIBLING at 0,0) still renders in
|
||||
// screen space, not offset down to the deck.
|
||||
const sceneW = this._W();
|
||||
let cx = a.x + a.w / 2 - W / 2;
|
||||
cx = Phaser.Math.Clamp(cx, W / 2 + 12, sceneW - W / 2 - 12);
|
||||
|
||||
const bar = new Phaser.GameObjects.Container(this.scene, cx, a.y);
|
||||
bar.setScrollFactor?.(0);
|
||||
this.subBar = bar;
|
||||
this.subBarW = W;
|
||||
this.subBarH = H;
|
||||
this.add?.(bar);
|
||||
|
||||
// The body FIRST (v4 paints children in list order — the opaque panel
|
||||
// fill must come before the buttons or it would cover them).
|
||||
const bodyG = this.scene.add?.graphics?.();
|
||||
if (bodyG) {
|
||||
bodyG.setScrollFactor?.(0);
|
||||
bar.add?.(bodyG);
|
||||
this._drawSubBarBody(bodyG, W, H, c);
|
||||
}
|
||||
|
||||
// The buttons LAST (above the body), centered in the row.
|
||||
let bx = -W / 2 + pad + widths[0] / 2;
|
||||
items.forEach((it, i) => {
|
||||
const btn = btns[i];
|
||||
btn.setX?.(bx);
|
||||
bar.add?.(btn);
|
||||
if (it.id === 'trade') this._tabTrade = btn;
|
||||
else this._tabBuyback = btn;
|
||||
bx += widths[i] / 2 + gap + (i < items.length - 1 ? widths[i + 1] / 2 : 0);
|
||||
});
|
||||
|
||||
// Buyback starts grayed (disabled) until a sale exists this visit.
|
||||
this._refreshTabs();
|
||||
}
|
||||
|
||||
/** The sub-bar panel body (the MenuSubBar recipe at full height). */
|
||||
_drawSubBarBody(g, W, H, c) {
|
||||
if (!g || typeof g.clear !== 'function') return;
|
||||
const panelTop = toColor(c.panelTop ?? '#0e1930');
|
||||
const panelBottom = toColor(c.panelBottom ?? '#050912');
|
||||
const neon = toColor(c.neon ?? '#00e5ff');
|
||||
const magenta = toColor(c.magenta ?? '#ff2d6f');
|
||||
const ink = toColor(c.ink ?? '#eaf6ff');
|
||||
const cut = Math.max(3, Math.min(12, H * 0.18));
|
||||
g.clear();
|
||||
const pts = [
|
||||
{ x: -W / 2 + cut, y: -H }, { x: W / 2 - cut, y: -H },
|
||||
{ x: W / 2, y: -H + cut }, { x: W / 2, y: 0 },
|
||||
{ x: -W / 2, y: 0 }, { x: -W / 2, y: -H + cut },
|
||||
];
|
||||
const has = (fn) => typeof g[fn] === 'function';
|
||||
if (has('fillPoints')) {
|
||||
g.fillStyle(panelBottom, 1); g.fillPoints(pts, true);
|
||||
g.fillStyle(panelTop, 0.55); g.fillPoints(pts, true);
|
||||
}
|
||||
if (has('fillRect')) {
|
||||
g.fillStyle(neon, 0.05); g.fillRect(-W / 2, -H, 26, H); // cyan left
|
||||
g.fillStyle(magenta, 0.06); g.fillRect(W / 2 - 26, -H, 26, H); // magenta right
|
||||
const rail = 2;
|
||||
g.fillStyle(neon, 0.9); g.fillRect(-W / 2 + cut, -H, W - cut * 2, rail);
|
||||
g.fillStyle(ink, 0.8); g.fillRect(-W / 2 + cut, -H + 0.5, W - cut * 2, 0.8);
|
||||
g.fillStyle(magenta, 0.5); g.fillRect(-W / 2 + cut, -H + rail, W - cut * 2, 1);
|
||||
g.fillStyle(magenta, 0.35); g.fillRect(-W / 2, -1.5, W, 1.5); // seam
|
||||
}
|
||||
}
|
||||
|
||||
/** A tab button press (MenuButton → _onTabTap). */
|
||||
_onTabTap(id) {
|
||||
if (id === 'trade') this.setTab('trade');
|
||||
else if (id === 'buyback' && this.isBuybackEnabled()) this.setTab('buyback');
|
||||
}
|
||||
|
||||
_refreshTabs() {
|
||||
const tabs = config.get('shopui.tabs', {});
|
||||
const dim = tabs.disabledAlpha ?? 0.34;
|
||||
const accent = tabs.activeAccent ?? '#00e5ff';
|
||||
// active tab = bright, inactive = dimmer; buyback disabled = dim + no-op.
|
||||
const tradeOn = this.tab === 'trade';
|
||||
const bbOn = this.tab === 'buyback';
|
||||
const bbEnabled = this.isBuybackEnabled();
|
||||
this._tabTrade?.setAccent(accent);
|
||||
this._tabTrade?.setAlpha(tradeOn ? 1 : 0.55);
|
||||
this._tabBuyback?.setAccent(bbOn ? accent : '#7d92c4');
|
||||
this._tabBuyback?.setAlpha(bbOn ? 1 : (bbEnabled ? 0.55 : dim));
|
||||
// MenuButtons carry their own state (base/hover/disabled) — the only thing
|
||||
// that changes here is Buyback's locked state (grayed until a sale exists).
|
||||
this._tabBuyback?.setDisabled?.(!this.isBuybackEnabled());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
|
@ -163,44 +275,72 @@ export class ShopView extends Phaser.GameObjects.Container {
|
|||
this.sellGrid = sellGrid;
|
||||
this.buyGrid = buyGrid;
|
||||
|
||||
// Layout: [SELL grid] [center] [BUY grid], centered in the viewport.
|
||||
// Layout: [SELL grid] [center] [BUY grid]. The panel's TOP edge sits at
|
||||
// this.topY — just under the surface HUD's planet text (upper-left) — so
|
||||
// the whole panel reads as one unit near the top of the screen (not
|
||||
// hanging down to the deck/sub-bar).
|
||||
//
|
||||
// IMPORTANT: a TradeGrid lays its plate/cells out in local (0,0)→(w,h)
|
||||
// and a Container's (x,y) is a pure TRANSLATION of that local space
|
||||
// (verified with dev/cbox-test.html) — so the grid's position IS the
|
||||
// panel's top-left corner. Treating it as a centered origin (topY + h/2)
|
||||
// drops the panel half a grid lower and clips the bottom rows.
|
||||
const totalW = sellGrid.width + centerW + buyGrid.width;
|
||||
const leftX = Math.max(8, (W - totalW) / 2);
|
||||
const contentY = (config.get('shopui.tabs.y', 44) + (config.get('shopui.tabs.h', 46))) + 26;
|
||||
sellGrid.setPosition(leftX, contentY);
|
||||
const gridH = sellGrid.height;
|
||||
sellGrid.setPosition(leftX, this.topY);
|
||||
const rightX = leftX + sellGrid.width + centerW;
|
||||
buyGrid.setPosition(rightX, contentY);
|
||||
buyGrid.setPosition(rightX, this.topY);
|
||||
|
||||
const content = new Phaser.GameObjects.Container(this.scene, 0, 0);
|
||||
content.setScrollFactor?.(0);
|
||||
content.add?.(sellGrid);
|
||||
content.add?.(buyGrid);
|
||||
|
||||
// center column: NET preview + Confirm + Clear
|
||||
// center column: NET preview + Confirm + Clear — vertically centered with
|
||||
// the grids (the NET above, the buttons below), so the whole panel is one
|
||||
// cohesive block near the top of the screen.
|
||||
const cx = leftX + sellGrid.width + centerW / 2;
|
||||
const cy = contentY + sellGrid.height / 2;
|
||||
this._previewText = this._makeText(cx, cy - 30, 'NET 0 cr', {
|
||||
fontSize: `${config.get('shopui.preview.valueFontSize', 34)}px`,
|
||||
color: config.get('shopui.preview.zeroColor' , '#7d92c4'), align: 'center',
|
||||
});
|
||||
this._previewLabel = this._makeText(cx, cy - 62, config.get('shopui.preview.label', 'NET'), {
|
||||
const cy = this.topY + gridH / 2;
|
||||
this._previewLabel = this._makeText(cx, cy - 52, config.get('shopui.preview.label', 'NET'), {
|
||||
fontSize: `${config.get('shopui.preview.labelFontSize', 13)}px`,
|
||||
color: config.get('shopui.preview.labelColor', '#7d92c4'), align: 'center',
|
||||
});
|
||||
this._previewText = this._makeText(cx, cy - 14, 'NET 0 cr', {
|
||||
fontSize: `${config.get('shopui.preview.valueFontSize', 34)}px`,
|
||||
color: config.get('shopui.preview.zeroColor', '#7d92c4'), align: 'center',
|
||||
});
|
||||
content.add?.(this._previewLabel);
|
||||
content.add?.(this._previewText);
|
||||
|
||||
const bw = config.get('shopui.buttons.w', 158), bh = config.get('shopui.buttons.h', 54);
|
||||
this._confirmBtn = this._makeButton(config.get('shopui.buttons.confirmLabel', 'Confirm'), bw, bh, config.get('shopui.buttons.confirmAccent', '#7ce8a4'));
|
||||
this._confirmBtn.setPosition(cx, cy + 34);
|
||||
this._confirmBtn.tap(() => this.confirm());
|
||||
this._confirmBtn.setAlpha(0);
|
||||
content.add?.(this._confirmBtn.container);
|
||||
// Confirm / Clear — the same cyberpunk MenuButton as the tab bar.
|
||||
const c = config.section('save.colors', {});
|
||||
const btnCfg = config.section('save.subBar.button', {});
|
||||
this._confirmBtn = new MenuButton(
|
||||
this.scene, cx, cy + 34, config.get('shopui.buttons.confirmLabel', 'Confirm'), () => this.confirm(),
|
||||
{
|
||||
screenFixed: true,
|
||||
fontSize: btnCfg.fontSize ?? 13, paddingX: 26, paddingY: 12,
|
||||
letterSpacing: btnCfg.letterSpacing ?? 2, upper: true,
|
||||
textColor: c.ink ?? '#eaf6ff', bgColor: c.cardBg ?? '#0a1222',
|
||||
hoverColor: c.cardBg ?? '#0a1222', borderColor: toCss(config.get('shopui.buttons.confirmAccent', '#7ce8a4')),
|
||||
},
|
||||
);
|
||||
this._confirmBtn.setAlpha?.(0);
|
||||
content.add?.(this._confirmBtn);
|
||||
|
||||
this._clearBtn = this._makeButton(config.get('shopui.buttons.clearLabel', 'Clear'), bw, bh, config.get('shopui.buttons.clearAccent', '#7d92c4'));
|
||||
this._clearBtn.setPosition(cx, cy + 104);
|
||||
this._clearBtn.tap(() => this.clear());
|
||||
this._clearBtn.setAlpha(0);
|
||||
content.add?.(this._clearBtn.container);
|
||||
this._clearBtn = new MenuButton(
|
||||
this.scene, cx, cy + 104, config.get('shopui.buttons.clearLabel', 'Clear'), () => this.clear(),
|
||||
{
|
||||
screenFixed: true,
|
||||
fontSize: btnCfg.fontSize ?? 13, paddingX: 26, paddingY: 12,
|
||||
letterSpacing: btnCfg.letterSpacing ?? 2, upper: true,
|
||||
textColor: c.ink ?? '#eaf6ff', bgColor: c.cardBg ?? '#0a1222',
|
||||
hoverColor: c.cardBg ?? '#0a1222', borderColor: toCss(config.get('shopui.buttons.clearAccent', '#7d92c4')),
|
||||
},
|
||||
);
|
||||
this._clearBtn.setAlpha?.(0);
|
||||
content.add?.(this._clearBtn);
|
||||
|
||||
this._content = content;
|
||||
this.add?.(content);
|
||||
|
|
@ -223,8 +363,9 @@ export class ShopView extends Phaser.GameObjects.Container {
|
|||
_showBuyback() {
|
||||
const W = this._W();
|
||||
const content = new Phaser.GameObjects.Container(this.scene, 0, 0);
|
||||
content.setScrollFactor?.(0);
|
||||
const rows = this._bbRows();
|
||||
const contentY = (config.get('shopui.tabs.y', 44) + (config.get('shopui.tabs.h', 46))) + 26;
|
||||
const contentY = this._H() / 2;
|
||||
const cx = W / 2;
|
||||
|
||||
const title = this._makeText(cx, contentY + 10, 'BUY BACK', {
|
||||
|
|
@ -399,11 +540,13 @@ export class ShopView extends Phaser.GameObjects.Container {
|
|||
}
|
||||
const confirmOn = anySel;
|
||||
const confirmEnabled = anySel && pv.valid;
|
||||
this._confirmBtn?.setAlpha(confirmOn ? 1 : 0);
|
||||
this._confirmBtn?.setAccent(confirmEnabled
|
||||
? config.get('shopui.buttons.confirmAccent', '#7ce8a4')
|
||||
: config.get('shopui.buttons.disabled', '#7d92c4'));
|
||||
this._clearBtn?.setAlpha(confirmOn ? 1 : 0);
|
||||
// MenuButton: hide (alpha) when no selection, and GRAY (setDisabled) while
|
||||
// the trade is invalid — a disabled button swallows its own clicks.
|
||||
if (this._confirmBtn) {
|
||||
this._confirmBtn.setAlpha?.(confirmOn ? 1 : 0);
|
||||
this._confirmBtn.setDisabled?.(!confirmEnabled);
|
||||
}
|
||||
this._clearBtn?.setAlpha?.(confirmOn ? 1 : 0);
|
||||
}
|
||||
|
||||
_hasAnySelection() {
|
||||
|
|
@ -461,6 +604,10 @@ export class ShopView extends Phaser.GameObjects.Container {
|
|||
return this.scene.scale?.width ?? 1280;
|
||||
}
|
||||
|
||||
_H() {
|
||||
return this.scene.scale?.height ?? 720;
|
||||
}
|
||||
|
||||
_makeText(x, y, str, style = {}) {
|
||||
// Owned by whichever container the caller adds it to (or this) — real
|
||||
// Phaser tears the whole tree down on destroy(); the headless stub doesn't
|
||||
|
|
|
|||
|
|
@ -97,6 +97,13 @@ export class TradeGrid extends Phaser.GameObjects.Container {
|
|||
this.plate = gr;
|
||||
this._objs.push(gr);
|
||||
this._drawPlate();
|
||||
// Parent it to the grid so it renders with the panel: in this Phaser
|
||||
// build a scene-level object at depth 0 draws BEHIND the shop video
|
||||
// (verified in dev/cbox-test.html + screenshots) — container children
|
||||
// inherit the grid's depth (ShopView = 100) and its translation, so the
|
||||
// plate stays glued to the panel and above the video. The cells are
|
||||
// added later, so the plate sits behind them (added first = drawn first).
|
||||
this.add?.(gr);
|
||||
}
|
||||
this.plateStyle = p;
|
||||
}
|
||||
|
|
@ -124,6 +131,10 @@ export class TradeGrid extends Phaser.GameObjects.Container {
|
|||
t.setOrigin?.(0.5, 0);
|
||||
this.title = t;
|
||||
this._objs.push(t);
|
||||
// Parent to the grid (same reason as the plate — scene-level depth 0
|
||||
// renders behind the shop video in this build, so the SELL/BUY header
|
||||
// was invisible before).
|
||||
this.add?.(t);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue