Make BUY/SELL a toggle and save pending trade across panel close

- Pressing the open tab now closes the panel (back to entry state) instead of
  rebuilding it; pressing again re-opens with the selection restored, so hiding
  the panel no longer loses the player's pending trade.
- Save the SELL/BUY maps onto ShopView (_pendingSell/_pendingBuy) on toggle-close
  and tab switch; _restorePendingSelection() re-applies them when the panel is
  rebuilt. The sub-bar stays live while closed: NET shows the delta, Clear
  cancels the saved trade, Confirm executes it.
- preview()/clear()/_afterTrade()/_hasAnySelection() now treat a null tab with a
  pending selection as an active trade, so the buyback path is no longer shadowed.
- Rename ScoutPanel.data to report: Phaser's GameObject reserves .data (its Data
  manager), and a plain object there crashed GameObject.destroy(). Update all
  callers in ShopView, tests, and dev/shop-shot.mjs.
- Simplify the scout price line to credits only ("9 CR") and drop the now-unused
  priceSell style from shopui.json; update goods.png/psd.
- Add setSellMap()/setBuyMap() to TradeGrid/BuyList to restore a saved selection
  onto freshly rebuilt panels, and cover the toggle flow in shop-ui.test.mjs plus
  a toggle probe in dev/shop-shot.mjs.
This commit is contained in:
Brian Fertig 2026-09-12 11:19:39 -06:00
parent 564b086c6a
commit 54989d3a3a
9 changed files with 287 additions and 50 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 KiB

After

Width:  |  Height:  |  Size: 706 KiB

Binary file not shown.

View File

@ -154,10 +154,6 @@
"fontSize": 12,
"color": "#ffc94d"
},
"priceSell": {
"fontSize": 12,
"color": "#7ce8a4"
},
"flavor": {
"fontSize": 12,
"color": "#7d92c4"

View File

@ -451,12 +451,12 @@ const bounds = (o) => {
scoutAlpha: sv._scout?.alpha ?? null,
sellAlpha: sv.sellGrid?.alpha ?? null,
focusId: bl._focusId,
data: sv._scout?.data ? {
id: sv._scout.data.id,
localBuy: sv._scout.data.localBuy,
demand: (sv._scout.data.demand ?? []).map((c) => c.label),
producer: (sv._scout.data.producer ?? []).map((c) => c.label),
exits: (sv._scout.data.exits ?? []).map((e) => ({
data: sv._scout?.report ? {
id: sv._scout.report.id,
localBuy: sv._scout.report.localBuy,
demand: (sv._scout.report.demand ?? []).map((c) => c.label),
producer: (sv._scout.report.producer ?? []).map((c) => c.label),
exits: (sv._scout.report.exits ?? []).map((e) => ({
sys: e.systemName, cls: e.clsLabel, price: e.price, profit: e.profit, hops: e.hops,
})),
} : null,
@ -483,7 +483,7 @@ const bounds = (o) => {
await sleep(300); // the 140ms fade-in settles
scoutChecks.diag = diag.slice();
scoutChecks.open = scoutOpenState();
const d0 = sv._scout?.data;
const d0 = sv._scout?.report;
scoutChecks.openOk = !!d0
&& sv._scoutOpen === scouted.id
&& (sv._scout.alpha ?? 0) > 0.99
@ -507,7 +507,7 @@ const bounds = (o) => {
clickCanvas(round(bl.x + bl.vpX + 40), bl.y + round(otherRow.y0 - bl._scroll));
await sleep(300);
scoutChecks.swapOk = sv._scoutOpen === otherRow.id
&& sv._scout?.data?.id === otherRow.id
&& sv._scout?.report?.id === otherRow.id
&& (sv.sellGrid.alpha ?? 1) < 0.01;
// (d) TAP THE SCOUT ITSELF → closes ("anywhere else").
clickCanvas(round(sv._scout.x + sv._scout.width / 2), round(sv._scout.y + sv._scout.height / 2));
@ -517,21 +517,87 @@ const bounds = (o) => {
box: { x: boxX, y: boxY },
diag,
open: scoutChecks.open,
swap: { row: otherRow.id, id: sv._scout?.data?.id ?? sv._scoutOpen },
swap: { row: otherRow.id, id: sv._scout?.report?.id ?? sv._scoutOpen },
checks: { openOk: scoutChecks.openOk, closeOk: scoutChecks.closeOk, swapOk: scoutChecks.swapOk, selfCloseOk: scoutChecks.selfCloseOk },
ok: scoutChecks.openOk && scoutChecks.closeOk && scoutChecks.swapOk && scoutChecks.selfCloseOk,
};
out.pass = out.pass && out.tradeScout.ok;
// ---- TOGGLE PROBE (BUY/SELL press = open; AGAIN = close the panel) ----
// Real button-handler path (_onTabTap — the MenuButton onClick). The sub-
// bar probe left 3 BUY rows selected (1,2,1): closing the panel must SAVE
// that selection (NET + Confirm stay live), re-opening must RESTORE it,
// Clear (closed) must cancel it, and Confirm must work with the panel
// closed. (The SELL side is exercised by the Confirm step below.)
const tog = {};
const savedB = {};
for (const [id, q] of bl.buyQty) savedB[id] = q;
tog.startTab = sv.tab;
// (1) BUY/SELL again → the panel CLOSES; the selection is saved.
sv._onTabTap('trade');
await sleep(120);
tog.closedTab = sv.tab;
tog.closedGrid = sv.sellGrid === null;
tog.closedList = sv.buyList === null;
tog.saved = { sell: sv._pendingSell, buy: sv._pendingBuy };
tog.netClosed = sv._netValue?.text ?? null;
tog.confirmAlphaClosed = sv._confirmBtn?.alpha ?? null;
tog.clearAlphaClosed = sv._clearBtn?.alpha ?? null;
// (2) BUY/SELL → the panel re-OPENS with the selection RESTORED.
sv._onTabTap('trade');
await sleep(200);
const bl2 = sv.buyList;
tog.reopenTab = sv.tab;
const restored = {};
for (const [id, q] of Object.entries(savedB)) restored[id] = bl2?.buyQty?.get(id) ?? 0;
tog.restored = restored;
// (3) Close again, then Clear (closed) → the saved selection is CANCELLED.
sv._onTabTap('trade');
await sleep(120);
sv.clear();
tog.afterClearNet = sv._netValue?.text ?? null;
tog.afterClearConfirmAlpha = sv._confirmBtn?.alpha ?? null;
// (4) A SELL, closed panel, Confirm from the sub-bar (trade lands).
sv._onTabTap('trade');
await sleep(200);
sv.sellGrid?.toggleBay?.(0);
sv._onTabTap('trade');
await sleep(120);
const creditsBefore = gs.ship.credits;
tog.confirmClosed = sv.confirm();
tog.creditsDelta = gs.ship.credits - creditsBefore;
tog.netAfterConfirm = sv._netValue?.text ?? null;
tog.buybackEnabled = sv.isBuybackEnabled();
// (5) Re-open → FRESH selection (the confirmed trade is gone).
sv._onTabTap('trade');
await sleep(200);
tog.freshSell = sv.sellGrid?.getSellMap?.() ?? {};
tog.freshBuy = sv.buyList?.getBuyMap?.() ?? {};
const savedBuyOk = tog.saved?.buy && Object.entries(savedB).every(([id, q]) => tog.saved.buy[id] === q);
const restoredOk = bl2 && Object.entries(savedB).every(([id, q]) => restored[id] === q);
tog.ok = tog.startTab === 'trade'
&& tog.closedTab === null && tog.closedGrid && tog.closedList
&& savedBuyOk
&& tog.netClosed === netText && tog.netClosed !== '0 cr' // NET kept the delta
&& (tog.confirmAlphaClosed ?? 0) > 0.9 && (tog.clearAlphaClosed ?? 0) > 0.9
&& tog.reopenTab === 'trade' && restoredOk
&& tog.afterClearNet === '0 cr' && (tog.afterClearConfirmAlpha ?? 1) < 0.1
&& tog.confirmClosed === true && tog.creditsDelta > 0
&& tog.netAfterConfirm === '0 cr' && tog.buybackEnabled === true
&& Object.keys(tog.freshSell).length === 0
&& Object.keys(tog.freshBuy).length === 0;
out.toggle = { ...tog, savedB };
out.pass = out.pass && out.toggle.ok;
// Leave the scout OPEN on a good with charted exits (for the screenshot).
for (const r of bl.rows) {
clickCanvas(round(bl.x + bl.vpX + 40), bl.y + round(r.y0 - bl._scroll));
await sleep(280);
if (sv._scoutOpen === r.id && (sv._scout.data?.exits?.length ?? 0) > 0) break;
if (sv._scoutOpen === r.id && (sv._scout.report?.exits?.length ?? 0) > 0) break;
clickCanvas(round(bl.x + bl.vpX + 40), bl.y + round(r.y0 - bl._scroll)); // close again
await sleep(280);
}
out.scoutShot = { openId: sv._scoutOpen, exits: sv._scout?.data?.exits?.length ?? 0 };
out.scoutShot = { openId: sv._scoutOpen, exits: sv._scout?.report?.exits?.length ?? 0 };
out.screen = { w: ss.scale.width, h: ss.scale.height };
out.topY = sv.topY ?? null;

View File

@ -637,6 +637,65 @@ console.log('\n== BUYBACK is locked until a sale + Clear resets the selection ==
}
console.log('LOCK + CLEAR: OK');
// ===========================================================================
console.log('\n== TOGGLE — BUY/SELL presses open the panel; again closes it ==');
{
const ship = makeShip(10000);
ship.cargo.add([A]);
const { view } = makeView(ship, { reveal: false });
// Entry state: panel closed, sub-bar idle.
assert.equal(view.tab, null, 'entry state: the panel is closed');
assert.equal(view._netValue.text, '0 cr', '… and the NET readout is zero');
assert.equal(view._confirmBtn.alpha, 0, '… and Confirm is hidden');
// Press BUY/SELL (the button handler is _onTabTap) → the panel opens.
view._onTabTap('trade');
assert.equal(view.tab, 'trade', 'pressing BUY/SELL opens the trade panel');
assert.ok(view.sellGrid && view.buyList, '… with both panels built');
// Build a pending trade: sell A (bay 0) + buy 2 of the stocked good.
view.sellGrid.toggleBay(0);
view.buyList.bumpBuy(C, 2);
const netBefore = view.preview().net;
// Press BUY/SELL AGAIN → the panel CLOSES (toggle off)…
view._onTabTap('trade');
assert.equal(view.tab, null, 'pressing BUY/SELL again closes the panel');
assert.equal(view.sellGrid, null, '… the SELL grid is torn down');
assert.equal(view.buyList, null, '… and the BUY list');
// … but the pending trade is SAVED, not lost — the sub-bar stays live.
assert.deepEqual(view._pendingSell, { [A]: 1 }, '… the SELL selection is saved');
assert.deepEqual(view._pendingBuy, { [C]: 2 }, '… and the BUY selection');
assert.equal(view.preview().net, netBefore, '… the preview still sees the same trade');
assert.equal(view._confirmBtn.alpha, 1, '… Confirm is live on the closed sub-bar');
assert.equal(view._clearBtn.alpha, 1, '… and Clear');
// Clear (with the panel closed) cancels the saved trade.
view.clear();
assert.equal(view.preview().net, 0, 'Clear (closed) cancels the saved trade');
assert.equal(view._confirmBtn.alpha, 0, '… Confirm hides again');
// Rebuild a sell, close again, and CONFIRM from the closed state.
view._onTabTap('trade'); // open again (toggle on)
assert.equal(view.tab, 'trade', 'pressing BUY/SELL re-opens the panel');
view.sellGrid.toggleBay(0);
view._onTabTap('trade'); // close again (toggle off)
const creditsBefore = ship.credits;
assert.equal(view.confirm(), true, 'Confirm works with the panel closed');
assert.ok(ship.credits > creditsBefore, '… and the sale landed (credits up)');
assert.equal(view.preview().net, 0, '… the trade is spent');
assert.equal(view.isBuybackEnabled(), true, '… and the sale registered for Buyback');
// Re-open: a FRESH selection (the confirmed trade is gone, not restored).
view._onTabTap('trade');
assert.equal(view.tab, 'trade', '… the panel re-opens');
assert.deepEqual(view.sellGrid.getSellMap(), {}, '… with no carry-over sell selection');
assert.deepEqual(view.buyList.getBuyMap(), {}, '… and no carry-over buy selection');
}
console.log('TOGGLE: OK');
// ===========================================================================
console.log('\n== TRADE SCOUT — tap a BUY row: the cargo grid swaps for the report ==');
{
@ -658,10 +717,10 @@ console.log('\n== TRADE SCOUT — tap a BUY row: the cargo grid swaps for the re
assert.equal(view._scout.alpha, 1, 'the report is visible (the stub tween lands on alpha 1)');
assert.equal(view.sellGrid.alpha, 0, '… the cargo grid steps aside (alpha 0)');
assert.equal(view.buyList._focusId, row.id, '… the row keeps its focus stroke');
assert.equal(view._scout.data.id, row.id, '… and it shows THAT good');
assert.equal(view._scout.report.id, row.id, '… and it shows THAT good');
// The report: identity + baseline + charted exits + profit math.
const d = view._scout.data;
const d = view._scout.report;
assert.equal(d.localBuy, row.price, 'the profit baseline is this shop\'s buy price for the row');
assert.ok(Array.isArray(d.demand) && d.demand.every((c) => c.label), 'demand ("sought on") classes carry labels');
assert.ok(Array.isArray(d.producer) && d.producer.every((c) => c.label), 'producer ("grown on") classes carry labels');
@ -682,7 +741,7 @@ console.log('\n== TRADE SCOUT — tap a BUY row: the cargo grid swaps for the re
const other = view.buyList.rows[1] ?? view.buyList.rows[0];
view._onRowTap(other);
assert.equal(view._scoutOpen, other.id, 'a different box swaps the report');
assert.equal(view._scout.data.id, other.id, '… to that good');
assert.equal(view._scout.report.id, other.id, '… to that good');
assert.equal(view.sellGrid.alpha, 0, '… the cargo grid stays aside');
// TAP THE LIST'S DEAD SPACE (null) → closes too ("anywhere else").

View File

@ -704,6 +704,23 @@ export class BuyList extends Phaser.GameObjects.Container {
return m;
}
/**
* Restore { goodId: qty } stepper quantities onto the CURRENT stock (the
* ShopView re-applies the player's pending trade after the BUY/SELL toggle
* rebuilds the panel). Ids no longer in stock are dropped. Call after
* refresh() (which resets the quantities).
*/
setBuyMap(map = {}) {
this.buyQty = new Map();
for (const [id, q] of Object.entries(map ?? {})) {
const n = Math.max(0, Math.floor(Number(q) || 0));
if (n > 0 && this.stock.some((s) => s.id === id)) this.buyQty.set(id, n);
}
this._renderRows();
this._emitSelChange();
return this;
}
hasSelection() {
return this.buyQty.size > 0;
}

View File

@ -6,7 +6,7 @@
* │ TRADE SCOUT ✕ │
* │ ┌────┐ Refined Fuel │
* │ │icon│ ENERGY & FUEL · T2 │
* │ └────┘ BUY HERE 9 cr │
* │ └────┘ 9 CR │
* │ The lifeblood… │
* │ ────────────────────────── │
* │ GROWN ON Gas Giant │
@ -56,7 +56,11 @@ export class ScoutPanel extends Phaser.GameObjects.Container {
this.setAlpha?.(0); // hidden until show() + reveal()
}
get data() { return this._data; }
// NOTE: this accessor is NOT named `data` — Phaser's GameObject reserves
// `.data` (its Data manager), and GameObject.destroy() calls
// `this.data.destroy()`. A plain report object there would crash the
// destroy pass ("this.data.destroy is not a function").
get report() { return this._data; }
// ------------------------------------------------------------------ shell
_buildShell() {
@ -150,17 +154,11 @@ export class ScoutPanel extends Phaser.GameObjects.Container {
const mt = s.meta ?? {};
add(text(tx, 84, meta, { fontSize: `${mt.fontSize ?? 11}px`, color: mt.color ?? '#7d92c4' })).setOrigin?.(0, 0);
// local prices (the baseline the exits' profits are measured against)
// the buy price — the baseline the exits' profits are measured against.
// Credits only; no "bought/sells here" wording.
const pb = s.priceBuy ?? {};
const priceStr = d.localSell != null
? `BUY HERE ${d.localBuy} CR · SELLS HERE ${d.localSell} CR`
: `BUY HERE ${d.localBuy} CR · NOT BOUGHT HERE`;
// ONE wrapped line: the "not bought here" variant is wider than the
// panel at 446px, so wrap instead of running into the buy list.
const priceT = add(text(tx, 108, priceStr, { fontSize: `${pb.fontSize ?? 12}px`, color: pb.color ?? '#ffc94d' }));
const priceT = add(text(tx, 108, `${d.localBuy} CR`, { fontSize: `${pb.fontSize ?? 12}px`, color: pb.color ?? '#ffc94d' }));
priceT?.setOrigin?.(0, 0);
priceT?.setWordWrap?.(true);
priceT?.setWordWrapWidth?.(W - tx - pad);
// flavor (full width, under the identity block)
const fl = s.flavor ?? {};

View File

@ -92,9 +92,13 @@ export class ShopView extends Phaser.GameObjects.Container {
this.capacity = this.ship.cargo ? this.ship.cargo.capacity : 0;
// --- session state ---------------------------------------------------
this.tab = 'trade'; // 'trade' | 'buyback'
this.tab = 'trade'; // 'trade' | 'buyback' | null (closed)
this.soldThisSession = []; // [{ id, name, qty, price }] (buyback source)
this.buybackSel = new Map(); // goodId -> qty to buy back
this._pendingSell = null; // { goodId: qty } — the pending trade's
this._pendingBuy = null; // SELL/BUY side, saved when the
// BUY/SELL toggle closes the panel
// (it survives until Confirm/Clear)
this.sellGrid = null;
this.buyList = null;
this._content = null; // the active content container
@ -136,6 +140,15 @@ export class ShopView extends Phaser.GameObjects.Container {
setTab(t) {
if (t === 'buyback' && !this.isBuybackEnabled()) return this; // locked
// Leaving an OPEN trade panel with a pending trade? Save the selection
// (same as the toggle-close) so a tab switch never loses it; _showTrade
// re-applies it when the panel comes back.
if (this.tab === 'trade' && t !== 'trade') {
const s = this.sellGrid?.getSellMap?.() ?? {};
const b = this.buyList?.getBuyMap?.() ?? {};
this._pendingSell = Object.keys(s).length > 0 ? s : null;
this._pendingBuy = Object.keys(b).length > 0 ? b : null;
}
this.tab = t;
this._closeScout(); // the content (and the scout) is rebuilt
this._destroyContent();
@ -360,10 +373,43 @@ export class ShopView extends Phaser.GameObjects.Container {
}
}
/** A tab button press (MenuButton → _onTabTap). */
/**
* A tab button press (MenuButton → _onTabTap). The tabs are TOGGLES:
* pressing the one that's already open closes the panel (back to the
* entry state — shop clip + sub-bar); pressing it again re-opens it.
* The pending trade selection is SAVED across the close (see
* _closePanel) — hiding the panel never loses the trade.
*/
_onTabTap(id) {
if (id === 'trade') this.setTab('trade');
else if (id === 'buyback' && this.isBuybackEnabled()) this.setTab('buyback');
if (id === 'trade') {
if (this.tab === 'trade') this._closePanel();
else this.setTab('trade');
} else if (id === 'buyback' && this.isBuybackEnabled()) {
if (this.tab === 'buyback') this._closePanel();
else this.setTab('buyback');
}
}
/**
* The toggle-close: the panel above the sub-bar hides (back to the
* entry state — tab null, content gone, shop clip playing). The player's
* pending trade is NOT lost — its SELL/BUY maps are saved onto the view
* (_pendingSell/_pendingBuy) and re-applied when the panel re-opens
* (_restorePendingSelection). While closed, the sub-bar stays live over
* the saved selection: NET shows the delta, Clear cancels it, Confirm
* executes it. (The buyback selection is already session-persistent.)
*/
_closePanel() {
if (this.tab === 'trade') {
const s = this.sellGrid?.getSellMap?.() ?? {};
const b = this.buyList?.getBuyMap?.() ?? {};
this._pendingSell = Object.keys(s).length > 0 ? s : null;
this._pendingBuy = Object.keys(b).length > 0 ? b : null;
}
this.tab = null;
this._destroyContent(); // kills the panel + scout + focus
this._syncPreview(); // NET + Clear/Confirm over the saved sel
return this;
}
_refreshTabs() {
@ -437,6 +483,9 @@ export class ShopView extends Phaser.GameObjects.Container {
buyList.refresh({ stock: E.shopFor(this.cls, this.systemId, this.galaxy).buy });
sellGrid.onSelectionChange = () => this._syncPreview();
buyList.onSelectionChange = () => this._syncPreview();
// The BUY/SELL toggle closed the panel WITH a pending trade? Re-apply
// it onto these fresh panels (hiding the panel never loses the trade).
this._restorePendingSelection();
// Tap a BUY row's box (outside the stepper) → the TRADE SCOUT: the cargo
// grid is swapped for the row's strategy report (what wants it, where
// it's grown, the top charted exits + profit). Tap again / anywhere →
@ -531,24 +580,28 @@ export class ShopView extends Phaser.GameObjects.Container {
const cargo = this.ship.cargo ? this.ship.cargo.snapshot() : [];
const capacity = this.capacity;
const credits = this.ship.credits;
if (this.tab === 'trade') {
if (this.tab === 'buyback') {
// buyback — priced at the sale price (buyOverrides), a neutral undo.
const buyMap = {};
const buyOverrides = {};
for (const r of this._bbRows()) {
if (r.sel > 0) { buyMap[r.id] = r.sel; buyOverrides[r.id] = r.price; }
}
return E.tradePreview({
cls: this.cls, systemId: this.systemId, galaxy: this.galaxy,
cargo, capacity, credits,
sellMap: this.sellGrid ? this.sellGrid.getSellMap() : {},
buyMap: this.buyList ? this.buyList.getBuyMap() : {},
sellMap: {}, buyMap, buyOverrides,
});
}
// buyback — priced at the sale price (buyOverrides), a neutral undo.
const buyMap = {};
const buyOverrides = {};
for (const r of this._bbRows()) {
if (r.sel > 0) { buyMap[r.id] = r.sel; buyOverrides[r.id] = r.price; }
}
// The trade — the LIVE panel selection when it's open, the SAVED
// selection when the BUY/SELL toggle closed the panel (tab === null;
// the trade is still pending, just not visible).
const sellMap = this.tab === 'trade' ? (this.sellGrid ? this.sellGrid.getSellMap() : {}) : (this._pendingSell ?? {});
const buyMap = this.tab === 'trade' ? (this.buyList ? this.buyList.getBuyMap() : {}) : (this._pendingBuy ?? {});
return E.tradePreview({
cls: this.cls, systemId: this.systemId, galaxy: this.galaxy,
cargo, capacity, credits,
sellMap: {}, buyMap, buyOverrides,
sellMap, buyMap,
});
}
@ -564,11 +617,15 @@ export class ShopView extends Phaser.GameObjects.Container {
}
clear() {
if (this.tab === 'trade') {
if (this.tab === 'buyback') {
this.buybackSel = new Map();
} else if (this.tab === 'trade') {
this.sellGrid?.clearSelection();
this.buyList?.clearSelection();
} else {
this.buybackSel = new Map();
// Closed panel — cancel the SAVED pending trade (the toggle-close kept it).
this._pendingSell = null;
this._pendingBuy = null;
}
this._syncPreview();
return this;
@ -583,13 +640,17 @@ export class ShopView extends Phaser.GameObjects.Container {
}
_afterTrade(pv) {
if (this.tab === 'trade') {
if (this.tab === 'buyback') {
for (const b of pv.buy) this._reduceSold(b.id, b.qty);
this.buybackSel = new Map();
} else {
// The trade — open tab OR the closed panel (tab === null, selection in
// the _pending* store). Record the sale for buyback, reset both.
for (const s of pv.sell) this._addSold(s.id, s.qty, s.unit, s.name);
this.sellGrid?.clearSelection();
this.buyList?.clearSelection();
} else {
for (const b of pv.buy) this._reduceSold(b.id, b.qty);
this.buybackSel = new Map();
this._pendingSell = null;
this._pendingBuy = null;
}
this.sellGrid?.refresh({ cargo: this.ship.cargo ? this.ship.cargo.snapshot() : [] });
this.refreshHud();
@ -645,8 +706,28 @@ export class ShopView extends Phaser.GameObjects.Container {
}
_hasAnySelection() {
if (this.tab === 'buyback') return [...this.buybackSel.values()].some((q) => q > 0);
if (this.tab === 'trade') return (this.sellGrid?.hasSelection() ?? false) || (this.buyList?.hasSelection() ?? false);
return [...this.buybackSel.values()].some((q) => q > 0);
// Closed panel: the saved pending trade is the selection.
return Object.keys(this._pendingSell ?? {}).length > 0 || Object.keys(this._pendingBuy ?? {}).length > 0;
}
/**
* Re-apply the selection _closePanel() saved (the player closed the PANEL
* via the BUY/SELL toggle, not the trade). Only called from _showTrade,
* right after the fresh panels' refresh() + handler wiring. Clears the
* store so a later confirm/clear sees the live panels as the source.
*/
_restorePendingSelection() {
const s = this._pendingSell;
const b = this._pendingBuy;
this._pendingSell = null;
this._pendingBuy = null;
if (!s && !b) return this;
if (s && Object.keys(s).length > 0) this.sellGrid?.setSellMap?.(s);
if (b && Object.keys(b).length > 0) this.buyList?.setBuyMap?.(b);
this._syncPreview();
return this;
}
// ---------------------------------------------------------------------

View File

@ -276,6 +276,26 @@ export class TradeGrid extends Phaser.GameObjects.Container {
return m;
}
/**
* Restore a { goodId: qty } selection onto the CURRENT bays (the ShopView
* re-applies the player's pending trade after the BUY/SELL toggle rebuilds
* the panel). Checks the first N filled bays of each good; ids no longer
* in the hold are dropped. Call after refresh() (which resets selection).
*/
setSellMap(map = {}) {
this.sellSel = new Set();
for (const [id, want] of Object.entries(map ?? {})) {
let need = Math.max(0, Math.floor(Number(want) || 0));
if (!need) continue;
for (let i = 0; i < this.capacity && need > 0; i++) {
if (this._bayIdAt(i) === id) { this.sellSel.add(i); need--; }
}
}
this._renderAll();
this._emitSelChange();
return this;
}
hasSelection() {
return this.sellSel.size > 0;
}