Move the live NET readout from the panel center onto the tab sub-bar

The pending trade's credit delta now sits at the right end of the
Buy/Sell + Buyback sub-bar, past a hairline divider — tabs AND the
credit math on one console, one glance apart (entry state dims it to
0 cr). The center column keeps Confirm/Clear (re-centered); the
buyback panel's Confirm moves up to sit just below its rows.

Knobs: data/shopui.json → preview (bar sizing, zeroColor, divider).
Docs: docs/ECONOMY.md §12 mockups + bullets follow the new layout.
Tests: dev/shop-ui.test.mjs asserts the bar's NET readout mirrors the
pending trade (+N cr while picking, 0 cr after Confirm).
This commit is contained in:
Brian Fertig 2026-09-11 22:09:49 -06:00
parent 05b6f573c3
commit 2b860cc899
4 changed files with 126 additions and 74 deletions

View File

@ -1,9 +1,9 @@
{ {
"_comment": "SHOP UI — the buy/sell (and buyback) screen over the shop clip (data/shopui.json). LAYOUT: the two 4×4 grids sit side by side (SELL = the player's cargo bays on the LEFT, BUY = the shop's stock on the RIGHT), a center column carries the live NET preview + Confirm/Clear, and a tab bar up top toggles Buy/Sell ↔ Buyback (Buyback is disabled until the player has SOLD something this session). The panels are semi-transparent so the shop clip shows through; items + text are full alpha. CELLS show the good's ICON only — the name + price appear in a hover TOOLTIP (→ tooltip) instead of over the icon. GEOMETRY (slot/gap/gridCols/gridRows/centerW) is primitives the code (js/ui/shop/ShopView.js) lays out against the live scene size — not pixel positions. The ECONOMY itself (prices, stock) is Economy.js + data/goods.json + data/economy.json; this file is only presentation.", "_comment": "SHOP UI — the buy/sell (and buyback) screen over the shop clip (data/shopui.json). LAYOUT: the two 4×4 grids sit side by side (SELL = the player's cargo bays on the LEFT, BUY = the shop's stock on the RIGHT), Confirm/Clear sit in the center column, the live NET-credits readout rides on the TAB SUB-BAR (right of the Buy/Sell + Buyback buttons, past a hairline divider), and the tab sub-bar toggles Buy/Sell ↔ Buyback (Buyback is disabled until the player has SOLD something this session). The panels are semi-transparent so the shop clip shows through; items + text are full alpha. CELLS show the good's ICON only — the name + price appear in a hover TOOLTIP (→ tooltip) instead of over the icon. GEOMETRY (slot/gap/gridCols/gridRows/centerW) is primitives the code (js/ui/shop/ShopView.js) lays out against the live scene size — not pixel positions. The ECONOMY itself (prices, stock) is Economy.js + data/goods.json + data/economy.json; this file is only presentation.",
"enabled": true, "enabled": true,
"tabs": { "tabs": {
"_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).", "_comment": "The tab sub-bar (two MenuButtons, the deck's cyberpunk style — js/ui/MenuButton.js), EXTENDED at its right end by the live NET readout (→ preview) past a hairline divider. 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", "buySell": "Buy/Sell",
"buyback": "Buyback", "buyback": "Buyback",
"w": 184, "h": 46, "gap": 14, "w": 184, "h": 46, "gap": 14,
@ -61,13 +61,16 @@
}, },
"preview": { "preview": {
"_comment": "The live NET-credits readout in the center column. Positive (you're up) = green, negative (you'd spend past your credits / it's a net spend) = the value shown; the Confirm button is disabled while the trade is invalid (can't go negative, hold overflows).", "_comment": "The live NET-credits readout — on the TAB SUB-BAR, right of the Buy/Sell + Buyback buttons, past a hairline divider (→ divider): tabs AND the pending trade's credit delta on one console, one glance apart. Positive (you're up) = green, negative (a net spend) = the value shown in magenta, zero = dim (entry state / nothing selected). The value is right-aligned at the bar's end so longer numbers (+1,240 cr) never resize the bar. Confirm is disabled while the trade is invalid (can't go negative, hold overflows).",
"label": "NET", "label": "NET",
"labelColor": "#7d92c4", "labelColor": "#7d92c4",
"labelFontSize": 13, "labelFontSize": 12,
"valueFontSize": 34, "valueFontSize": 22,
"zeroColor": "#7d92c4",
"posColor": "#7ce8a4", "posColor": "#7ce8a4",
"negColor": "#ff2d6f" "negColor": "#ff2d6f",
"gap": 12,
"divider": { "width": 2, "height": 26, "color": "#22405f", "alpha": 0.9 }
}, },
"buttons": { "buttons": {

View File

@ -333,12 +333,19 @@ console.log('\n== SELL — check a bay, Confirm earns credits + empties the hold
assert.equal(pv.net, sellA, '… net = the sale (nothing bought)'); assert.equal(pv.net, sellA, '… net = the sale (nothing bought)');
assert.equal(pv.valid, true, '… and it\u2019s legal'); assert.equal(pv.valid, true, '… and it\u2019s legal');
// The live NET readout rides on the TAB SUB-BAR (not the center column) —
// it mirrors the pending trade's credit delta, one glance from the tabs.
assert.ok(view._netValue, 'the sub-bar carries the NET value readout');
assert.ok(view._netLabel, '… with the NET label');
assert.equal(view._netValue.text, `+${sellA} cr`, '… it shows the pending +cr');
const before = ship.credits; const before = ship.credits;
assert.equal(view.confirm(), true, 'Confirm goes through'); assert.equal(view.confirm(), true, 'Confirm goes through');
assert.equal(ship.credits, before + sellA, 'credits rise by the sale'); assert.equal(ship.credits, before + sellA, 'credits rise by the sale');
assert.deepEqual(ship.cargo.snapshot(), [B], 'A leaves the hold, B stays'); assert.deepEqual(ship.cargo.snapshot(), [B], 'A leaves the hold, B stays');
assert.equal(view.isBuybackEnabled(), true, '… and A is now buyback-eligible'); assert.equal(view.isBuybackEnabled(), true, '… and A is now buyback-eligible');
assert.equal(gs.refreshed, 1, 'the credits HUD was refreshed'); assert.equal(gs.refreshed, 1, 'the credits HUD was refreshed');
assert.equal(view._netValue.text, '0 cr', '… and the bar’s NET settles back to 0 cr');
} }
console.log('SELL: OK'); console.log('SELL: OK');

View File

@ -298,7 +298,8 @@ existing `data/actionbar.json` slot); the shop clip is already live, and the scr
it. It's one component — `js/ui/shop/ShopView.js` — composing two `TradeGrid`s. 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** **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 is up — the cyberpunk **MenuButton** pair `[ BUY/SELL | BUYBACK ]` (Buyback grayed out),
extended at its right end by the live **NET** readout (dim `0 cr` until you start picking), the
same style + placement as the deck's **Menu** sub-bar (`js/ui/MenuSubBar.js`). It rests **above 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 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 of UI as the Menu drawer. The screen stays open on the shop video so the clip plays. Clicking
@ -308,24 +309,23 @@ of UI as the Menu drawer. The screen stays open on the shop video so the clip pl
# first click — shop video + the tab sub-bar (rests above the deck): # first click — shop video + the tab sub-bar (rests above the deck):
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
│ (clip plays) │ │ (clip plays) │
│ ┌─────────────────────────┐ │ │ ┌──────────────────────────────────┐ │
│ │ BUY/SELL │ BUYBACK │ ← sub-bar │ │ │ BUY/SELL │ BUYBACK │ NET 0 cr │ ← sub-bar │
│ └─────────────────────────┘ │ │ └──────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────────┤
│ SHOP │ BUILD │ SHIP │ QUESTS │ TAKE OFF │ MENU ← command deck│ │ SHOP │ BUILD │ SHIP │ QUESTS │ TAKE OFF │ MENU ← command deck│
└─────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────┘
# after clicking Buy/Sell — the trade panel reveals (middle of the screen): # after clicking Buy/Sell — the trade panel reveals (middle of the screen):
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
│ ┌SELL 4×4┐ NET +420 cr ┌BUY 4×4┐ │ │ ┌SELL 4×4┐ [ CONFIRM ] ┌BUY 4×4┐ │
│ │ bays │ (green ≥ 0 / │ stock │ │ │ │ bays │ [ CLEAR ] │ stock │ │
│ │(cargo) │ red < 0) │(12–16) │ │ │ │(cargo) │ │(12–16) │ │
│ └────────┘ [ CONFIRM ] └────────┘ │ │ └────────┘ └────────┘ │
│ [ CLEAR ] │
├─────────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────┐ │ │ ┌──────────────────────────────────┐ │
│ │ BUY/SELL │ BUYBACK │ ← sub-bar │ │ │ BUY/SELL │ BUYBACK │ NET +420 cr │ ← sub-bar │
│ └─────────────────────────┘ │ │ └──────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────────┤
│ SHOP │ BUILD │ SHIP │ QUESTS │ TAKE OFF │ MENU ← command deck│ │ SHOP │ BUILD │ SHIP │ QUESTS │ TAKE OFF │ MENU ← command deck│
└─────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────┘
@ -335,9 +335,11 @@ of UI as the Menu drawer. The screen stays open on the shop video so the clip pl
cyberpunk style: cut-corner panel, neon edge, hover sweep, press punch) in a console-blue 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 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. 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 bar is **extended** past a hairline divider to carry the live **NET** readout —
the trade is invalid (can't go negative, hold would overflow), and both appear once you start one glance sees the tabs *and* the pending trade's credit delta.
picking. - **Confirm / Clear** — the same `MenuButton` style, in the center column between the grids;
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` - **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 are live bays (filled with a good, or empty), the rest are **locked** (the hold's upgrade
@ -345,8 +347,9 @@ of UI as the Menu drawer. The screen stays open on the shop video so the clip pl
quantity is simply the number of its bays that are checked. quantity is simply the number of its bays that are checked.
- **BUY (right)** — the shop's **stock** (12–16 distinct goods from `Economy.shopFor`). Each - **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). 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 - **NET readout (tab sub-bar)** — the live credit delta, right of the tabs past the divider.
you'd spend. Updates as you check bays / step goods. Green when you're up (a net sale), red when you'd spend, dim at zero. Updates as you check
bays / step goods (it's the same `preview()` the Confirm button is gated on).
- **SELL → BUY is atomic** — check bays to sell *and* step goods to buy, then one Confirm - **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 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. first). Selling to free a bay for a buy lands in the same Confirm.

View File

@ -1,22 +1,24 @@
/** /**
* ShopView — the buy/sell (and buyback) screen over the shop clip video. * ShopView — the buy/sell (and buyback) screen over the shop clip video.
* *
* ┌─────────────────────────────────────────────────────────────┐ * ┌────────────────────────────────────────────────────────────┐
* │ [ Buy/Sell | Buyback ] ← tabs │ * │ ┌SELL 4×4┐ [ Confirm ] ┌BUY 4×4┐ │
* │ │ * │ │ bays │ [ Clear ] │ stock │ │
* │ ┌SELL 4×4┐ ┌ NET ┐ ┌BUY 4×4┐ │ * │ │ (cargo)│ │ (12–16)│ │
* │ │ bays │ │ +420 │ │ stock │ │ * │ └────────┘ └────────┘ │
* │ │ (cargo)│ │ cr │ │ (12–16)│ │ * └────────────────────────────────────────────────────────────┘
* │ └────────┘ │[Conf]│ └────────┘ │ * ┌────────────────────────────────────────────────────────────┐
* │ │[Clear]│ │ * │ BUY/SELL BUYBACK │ NET +420 cr ← tab sub-bar │
* └─────────────────────────────────────────────────────────────┘ * └────────────────────────────────────────────────────────────┘
* *
* The two sides are TradeGrids (js/ui/shop/TradeGrid.js): * The two sides are TradeGrids (js/ui/shop/TradeGrid.js):
* · SELL (left) — the player's cargo bays. Tap a filled bay to check it * · SELL (left) — the player's cargo bays. Tap a filled bay to check it
* off (a bay = 1 unit). Locked bays are the hold's upgrade room. * off (a bay = 1 unit). Locked bays are the hold's upgrade room.
* · BUY (right) — the shop's stock, each with a −/qty/+ stepper. * · BUY (right) — the shop's stock, each with a −/qty/+ stepper.
* The live NET preview (green ≥ 0, red < 0) and Confirm/Clear live in the * The live NET preview (green ≥ 0, red < 0) sits on the TAB SUB-BAR, right
* center; Confirm is disabled while the trade is invalid (can't go negative, * of the Buy/Sell + Buyback buttons past a hairline divider (tabs AND the
* pending trade's credit math on one console); Confirm/Clear stay in the
* center. Confirm is disabled while the trade is invalid (can't go negative,
* hold would overflow). SELL→BUY happens atomically (sell to free room for a * hold would overflow). SELL→BUY happens atomically (sell to free room for a
* buy). * buy).
* *
@ -83,7 +85,8 @@ export class ShopView extends Phaser.GameObjects.Container {
this.sellGrid = null; this.sellGrid = null;
this.buyGrid = null; this.buyGrid = null;
this._content = null; // the active content container this._content = null; // the active content container
this._previewText = null; this._netLabel = null; // the sub-bar NET label (persistent)
this._netValue = null; // the sub-bar NET value (live)
this._confirmBtn = null; this._confirmBtn = null;
this._clearBtn = null; this._clearBtn = null;
this._tabBuyback = null; this._tabBuyback = null;
@ -132,6 +135,12 @@ export class ShopView extends Phaser.GameObjects.Container {
// signature top rail, holding cyberpunk MenuButtons. It rests ABOVE the // signature top rail, holding cyberpunk MenuButtons. It rests ABOVE the
// command deck, right-aligned to the deck's "Shop" button (this.anchor), // 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. // so it reads as the same family of UI as the Menu drawer.
//
// EXTENDED (data/shopui.json → preview): the live NET credit readout —
// the pending trade's credit delta — sits at the bar's right end past a
// hairline divider. Tabs AND the credit math on ONE console: the readout
// is one glance away from the tabs at every moment (entry state included,
// where it dims to "0 cr").
const a = this.anchor; const a = this.anchor;
const H = config.get('save.subBar.height', 58); const H = config.get('save.subBar.height', 58);
const pad = config.get('save.subBar.padding', 16); const pad = config.get('save.subBar.padding', 16);
@ -139,6 +148,7 @@ export class ShopView extends Phaser.GameObjects.Container {
const tabs = config.get('shopui.tabs', {}); const tabs = config.get('shopui.tabs', {});
const c = config.section('save.colors', {}); const c = config.section('save.colors', {});
const btnCfg = config.section('save.subBar.button', {}); const btnCfg = config.section('save.subBar.button', {});
const pv = config.section('shopui.preview', {});
const items = [ const items = [
{ id: 'trade', label: tabs.buySell ?? 'Buy/Sell', accent: tabs.activeAccent ?? c.neon ?? '#00e5ff' }, { id: 'trade', label: tabs.buySell ?? 'Buy/Sell', accent: tabs.activeAccent ?? c.neon ?? '#00e5ff' },
@ -165,10 +175,31 @@ export class ShopView extends Phaser.GameObjects.Container {
const btns = items.map(mkBtn); const btns = items.map(mkBtn);
const widths = btns.map((b) => b.style?.width ?? 120); const widths = btns.map((b) => b.style?.width ?? 120);
const W = Math.min( const tabsW = widths.reduce((s, w) => s + w, 0) + gap * (widths.length - 1);
this._W() - 24,
pad * 2 + widths.reduce((s, w) => s + w, 0) + gap * (widths.length - 1), // The NET readout — created BEFORE the panel body so its REAL width feeds
); // the bar width (a headless fake without text metrics falls back to an
// estimate). The value is RIGHT-aligned at the bar's end, so longer
// numbers ("+1,240 cr") never need the bar to grow.
const labelStr = String(pv.label ?? 'NET');
const valueFont = Math.max(8, Number(pv.valueFontSize) || 22);
this._netLabel = this._makeText(0, -H / 2, labelStr, {
fontSize: `${Math.max(8, Number(pv.labelFontSize) || 12)}px`,
color: toCss(pv.labelColor ?? c.dim ?? '#7d92c4'),
align: 'left',
});
this._netValue = this._makeText(0, -H / 2, '0 cr', {
fontSize: `${valueFont}px`,
color: toCss(pv.zeroColor ?? pv.labelColor ?? '#7d92c4'),
align: 'left',
});
const labelW = (Number(this._netLabel?.width) > 0 ? this._netLabel.width : labelStr.length * 8) || 30;
const valueW = (Number(this._netValue?.width) > 0 ? this._netValue.width : 4 * valueFont * 0.6) || 40;
const divW = Math.max(1, Number(pv.divider?.width) || 2);
const divH = Math.max(10, Number(pv.divider?.height) || Math.round(H * 0.45));
// Row: [ BUY/SELL ][ BUYBACK ] │ NET value
const W = Math.min(this._W() - 24, pad * 2 + tabsW + gap + divW + gap + labelW + gap + valueW);
// Position: bottom edge on the deck's top rail (a.y), right edge on the // 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 // Shop button's right edge — the MenuSubBar recipe. The sub-bar is a
@ -187,15 +218,18 @@ export class ShopView extends Phaser.GameObjects.Container {
this.add?.(bar); this.add?.(bar);
// The body FIRST (v4 paints children in list order — the opaque panel // The body FIRST (v4 paints children in list order — the opaque panel
// fill must come before the buttons or it would cover them). // fill must come before the buttons or it would cover them). The hairline
// divider between the tabs and the NET block is part of the body, so it
// paints under both.
const divX = -W / 2 + pad + tabsW + gap + divW / 2;
const bodyG = this.scene.add?.graphics?.(); const bodyG = this.scene.add?.graphics?.();
if (bodyG) { if (bodyG) {
bodyG.setScrollFactor?.(0); bodyG.setScrollFactor?.(0);
bar.add?.(bodyG); bar.add?.(bodyG);
this._drawSubBarBody(bodyG, W, H, c); this._drawSubBarBody(bodyG, W, H, c, divX, divH);
} }
// The buttons LAST (above the body), centered in the row. // The buttons LAST (above the body), left-anchored in the row.
let bx = -W / 2 + pad + widths[0] / 2; let bx = -W / 2 + pad + widths[0] / 2;
items.forEach((it, i) => { items.forEach((it, i) => {
const btn = btns[i]; const btn = btns[i];
@ -206,12 +240,23 @@ export class ShopView extends Phaser.GameObjects.Container {
bx += widths[i] / 2 + gap + (i < items.length - 1 ? widths[i + 1] / 2 : 0); bx += widths[i] / 2 + gap + (i < items.length - 1 ? widths[i + 1] / 2 : 0);
}); });
// The NET block: the label right after the divider, the value right-
// aligned to the bar's end (so a growing number never shifts the label).
this._netLabel.setPosition?.(divX + divW / 2 + gap + labelW / 2, -H / 2);
this._netValue.setPosition?.(W / 2 - pad - valueW / 2, -H / 2);
bar.add?.(this._netLabel);
bar.add?.(this._netValue);
// Buyback starts grayed (disabled) until a sale exists this visit. // Buyback starts grayed (disabled) until a sale exists this visit.
this._refreshTabs(); this._refreshTabs();
} }
/** The sub-bar panel body (the MenuSubBar recipe at full height). */ /**
_drawSubBarBody(g, W, H, c) { * The sub-bar panel body (the MenuSubBar recipe at full height).
* `divX`/`divH` (optional) add the hairline divider between the tabs and
* the NET readout — drawn as body so it paints under the buttons + text.
*/
_drawSubBarBody(g, W, H, c, divX, divH) {
if (!g || typeof g.clear !== 'function') return; if (!g || typeof g.clear !== 'function') return;
const panelTop = toColor(c.panelTop ?? '#0e1930'); const panelTop = toColor(c.panelTop ?? '#0e1930');
const panelBottom = toColor(c.panelBottom ?? '#050912'); const panelBottom = toColor(c.panelBottom ?? '#050912');
@ -238,6 +283,10 @@ export class ShopView extends Phaser.GameObjects.Container {
g.fillStyle(ink, 0.8); g.fillRect(-W / 2 + cut, -H + 0.5, W - cut * 2, 0.8); 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.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 g.fillStyle(magenta, 0.35); g.fillRect(-W / 2, -1.5, W, 1.5); // seam
if (typeof divX === 'number' && divH > 0) {
g.fillStyle(toColor(c.border ?? '#22405f'), 0.9);
g.fillRect(divX - 1, -H / 2 - divH / 2, 2, divH); // tabs │ NET
}
} }
} }
@ -298,27 +347,17 @@ export class ShopView extends Phaser.GameObjects.Container {
content.add?.(sellGrid); content.add?.(sellGrid);
content.add?.(buyGrid); content.add?.(buyGrid);
// center column: NET preview + Confirm + Clear — vertically centered with // center column: Confirm + Clear — vertically centered with the grids,
// the grids (the NET above, the buttons below), so the whole panel is one // so the whole panel is one cohesive block near the top of the screen.
// cohesive block near the top of the screen. // (The live NET preview now rides on the tab sub-bar — see _buildTabs.)
const cx = leftX + sellGrid.width + centerW / 2; const cx = leftX + sellGrid.width + centerW / 2;
const cy = this.topY + gridH / 2; 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);
// Confirm / Clear — the same cyberpunk MenuButton as the tab bar. // Confirm / Clear — the same cyberpunk MenuButton as the tab bar.
const c = config.section('save.colors', {}); const c = config.section('save.colors', {});
const btnCfg = config.section('save.subBar.button', {}); const btnCfg = config.section('save.subBar.button', {});
this._confirmBtn = new MenuButton( this._confirmBtn = new MenuButton(
this.scene, cx, cy + 34, config.get('shopui.buttons.confirmLabel', 'Confirm'), () => this.confirm(), this.scene, cx, cy + 16, config.get('shopui.buttons.confirmLabel', 'Confirm'), () => this.confirm(),
{ {
screenFixed: true, screenFixed: true,
fontSize: btnCfg.fontSize ?? 13, paddingX: 26, paddingY: 12, fontSize: btnCfg.fontSize ?? 13, paddingX: 26, paddingY: 12,
@ -331,7 +370,7 @@ export class ShopView extends Phaser.GameObjects.Container {
content.add?.(this._confirmBtn); content.add?.(this._confirmBtn);
this._clearBtn = new MenuButton( this._clearBtn = new MenuButton(
this.scene, cx, cy + 104, config.get('shopui.buttons.clearLabel', 'Clear'), () => this.clear(), this.scene, cx, cy + 86, config.get('shopui.buttons.clearLabel', 'Clear'), () => this.clear(),
{ {
screenFixed: true, screenFixed: true,
fontSize: btnCfg.fontSize ?? 13, paddingX: 26, paddingY: 12, fontSize: btnCfg.fontSize ?? 13, paddingX: 26, paddingY: 12,
@ -390,16 +429,11 @@ export class ShopView extends Phaser.GameObjects.Container {
} }
const cy = y + 24; const cy = y + 24;
this._previewLabel = this._makeText(cx, cy - 8, config.get('shopui.preview.label', 'NET'), { // (The live NET readout rides on the tab sub-bar — see _buildTabs.)
fontSize: `${config.get('shopui.preview.labelFontSize', 13)}px`, color: config.get('shopui.preview.labelColor', '#7d92c4'), align: 'center' });
content.add?.(this._previewLabel);
this._previewText = this._makeText(cx, cy + 22, 'NET 0 cr', {
fontSize: `${config.get('shopui.preview.valueFontSize', 34)}px`, color: config.get('shopui.preview.zeroColor', '#7d92c4'), align: 'center' });
content.add?.(this._previewText);
const bw = config.get('shopui.buttons.w', 158), bh = config.get('shopui.buttons.h', 54); 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 = this._makeButton(config.get('shopui.buttons.confirmLabel', 'Confirm'), bw, bh, config.get('shopui.buttons.confirmAccent', '#7ce8a4'));
this._confirmBtn.setPosition(cx, cy + 92); this._confirmBtn.setPosition(cx, cy + 40);
this._confirmBtn.tap(() => this.confirm()); this._confirmBtn.tap(() => this.confirm());
this._confirmBtn.setAlpha(0); this._confirmBtn.setAlpha(0);
content.add?.(this._confirmBtn.container); content.add?.(this._confirmBtn.container);
@ -531,13 +565,16 @@ export class ShopView extends Phaser.GameObjects.Container {
_syncPreview() { _syncPreview() {
const pv = this.preview(); const pv = this.preview();
const anySel = this._hasAnySelection(); const anySel = this._hasAnySelection();
if (this._previewText && typeof this._previewText.setText === 'function') { // The live NET readout lives on the tab sub-bar (this._netValue) — one
const sign = pv.net >= 0 ? '+' : ''; // glance sees the tabs AND the pending trade's credit delta. Right-
this._previewText.setText(`NET ${sign}${Math.round(pv.net)} cr`); // aligned, so a growing number never shifts the "NET" label.
const col = pv.net === 0 if (this._netValue && typeof this._netValue.setText === 'function') {
const n = Math.round(pv.net);
this._netValue.setText(n === 0 ? '0 cr' : `${n > 0 ? '+' : ''}${n} cr`);
const col = n === 0
? config.get('shopui.preview.zeroColor', '#7d92c4') ? config.get('shopui.preview.zeroColor', '#7d92c4')
: (pv.net > 0 ? config.get('shopui.preview.posColor', '#7ce8a4') : config.get('shopui.preview.negColor', '#ff2d6f')); : (n > 0 ? config.get('shopui.preview.posColor', '#7ce8a4') : config.get('shopui.preview.negColor', '#ff2d6f'));
if (typeof this._previewText.setColor === 'function') this._previewText.setColor(col); if (typeof this._netValue.setColor === 'function') this._netValue.setColor(col);
} }
const confirmOn = anySel; const confirmOn = anySel;
const confirmEnabled = anySel && pv.valid; const confirmEnabled = anySel && pv.valid;
@ -584,8 +621,6 @@ export class ShopView extends Phaser.GameObjects.Container {
this.buyGrid?.destroy?.(); this.buyGrid?.destroy?.();
this.sellGrid = null; this.sellGrid = null;
this.buyGrid = null; this.buyGrid = null;
this._previewText = null;
this._previewLabel = null;
this._confirmBtn = null; this._confirmBtn = null;
this._clearBtn = null; this._clearBtn = null;
} }
@ -594,6 +629,10 @@ export class ShopView extends Phaser.GameObjects.Container {
this._destroyContent(); this._destroyContent();
this._tabTrade?.destroy?.(); this._tabTrade?.destroy?.();
this._tabBuyback?.destroy?.(); this._tabBuyback?.destroy?.();
this._netLabel?.destroy?.();
this._netValue?.destroy?.();
this._netLabel = null;
this._netValue = null;
if (typeof super.destroy === 'function') super.destroy(); if (typeof super.destroy === 'function') super.destroy();
} }