diff --git a/assets/images/goods.png b/assets/images/goods.png index 1cf854b..c22043f 100644 Binary files a/assets/images/goods.png and b/assets/images/goods.png differ diff --git a/assets/images/goods.psd b/assets/images/goods.psd index b6a3a86..05d8afa 100644 Binary files a/assets/images/goods.psd and b/assets/images/goods.psd differ diff --git a/data/shopui.json b/data/shopui.json index 6661afb..50ac73f 100644 --- a/data/shopui.json +++ b/data/shopui.json @@ -154,10 +154,6 @@ "fontSize": 12, "color": "#ffc94d" }, - "priceSell": { - "fontSize": 12, - "color": "#7ce8a4" - }, "flavor": { "fontSize": 12, "color": "#7d92c4" diff --git a/dev/shop-shot.mjs b/dev/shop-shot.mjs index c350d1b..1642f11 100644 --- a/dev/shop-shot.mjs +++ b/dev/shop-shot.mjs @@ -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; diff --git a/dev/shop-ui.test.mjs b/dev/shop-ui.test.mjs index f69c93e..1233503 100644 --- a/dev/shop-ui.test.mjs +++ b/dev/shop-ui.test.mjs @@ -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"). diff --git a/js/ui/shop/BuyList.js b/js/ui/shop/BuyList.js index 7cc8adf..f4fc93f 100644 --- a/js/ui/shop/BuyList.js +++ b/js/ui/shop/BuyList.js @@ -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; } diff --git a/js/ui/shop/ScoutPanel.js b/js/ui/shop/ScoutPanel.js index 5d58f6b..e2e6607 100644 --- a/js/ui/shop/ScoutPanel.js +++ b/js/ui/shop/ScoutPanel.js @@ -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 ?? {}; diff --git a/js/ui/shop/ShopView.js b/js/ui/shop/ShopView.js index d90bda2..d3729f6 100644 --- a/js/ui/shop/ShopView.js +++ b/js/ui/shop/ShopView.js @@ -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; } // --------------------------------------------------------------------- diff --git a/js/ui/shop/TradeGrid.js b/js/ui/shop/TradeGrid.js index 0bde0bb..4d3c24a 100644 --- a/js/ui/shop/TradeGrid.js +++ b/js/ui/shop/TradeGrid.js @@ -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; }