/** * CargoHold — the ship's trade-goods hold, as a PURE, framework-free object. * * The player carries trade goods between shops (buy cheap in one region, * sell high in another — the whole game loop). The hold is a set of fixed * BAYS: one good occupies one bay (data/economy.json → cargo.unitPerGood = 1). * The number of UNLOCKED bays is the capacity (ship.stats.cargoHold — the * player starts with 4, upgrades raise it toward the 16-slot grid); the * locked bays are just capacity the ship doesn't have yet. * * It is deliberately NOT a Phaser object: the rules live here so they can be * unit-tested in plain Node (dev/cargo.test.mjs) and so Ship.js stays a thin * wrapper. A good is identified by its catalog id (data/goods.json); the hold * stores ids, one per filled bay, in fill order. Duplicates are fine (two * frost-diamonds = two bays). * * The hold is the carrier, not a price ledger — pricing lives in * js/economy/Economy.js (buyPrice/sellPrice, per good + shop). */ export class CargoHold { /** * @param {number} capacity — the number of unlocked bays (0 when the ship * has no cargo capacity yet). Clamped to a non-negative integer. */ constructor(capacity = 0) { this._capacity = Math.max(0, Math.floor(Number(capacity) || 0)); // Filled bays, in fill order. Each entry is a good id (string). // Invariant: items.length ≤ _capacity (enforced by add/set). this.items = []; } /** The number of unlocked bays (the hold's capacity). */ get capacity() { return this._capacity; } /** * Raise or lower the capacity. Raising frees more bays (the cargo upgrades * do this). Lowering DROPS the oldest (tail) goods first so the invariant * holds — a ship that shrinks its hold sheds what no longer fits. */ set capacity(n) { this._capacity = Math.max(0, Math.floor(Number(n) || 0)); while (this.items.length > this._capacity) this.items.pop(); } /** How many bays are filled (0 … capacity). */ get used() { return this.items.length; } /** Empty bays left (capacity − used). */ get room() { return Math.max(0, this._capacity - this.items.length); } isFull() { return this.room <= 0; } /** How many copies of a good are aboard. */ count(goodId) { let n = 0; for (const id of this.items) if (id === goodId) n++; return n; } has(goodId) { return this.count(goodId) > 0; } /** * Load goods into the hold (buying, or a hand-off). `goodIds` is a good id * or an array of them (duplicates allowed — one per bay). Fills until the * hold is full or the list is exhausted. Returns the number actually added. */ add(goodIds) { const ids = Array.isArray(goodIds) ? goodIds : [goodIds]; let added = 0; for (const id of ids) { if (this.room <= 0) break; if (typeof id !== 'string' || !id) continue; this.items.push(id); added++; } return added; } /** * Remove up to `n` copies of a good from the hold (selling). Returns the * number actually removed (0 when the good isn't aboard). */ remove(goodId, n = 1) { const k = Math.max(1, Math.floor(Number(n) || 1)); let removed = 0; for (let i = 0; i < this.items.length && removed < k; i++) { if (this.items[i] === goodId) { this.items.splice(i, 1); i--; removed++; } } return removed; } /** Empty the hold entirely. */ clear() { this.items.length = 0; } /** * Replace the hold's contents wholesale (the SAVE/LOAD path). `items` is an * array of good ids; it is filtered to real ids and truncated to capacity * (a record can never overfill the hold). Returns the number kept. */ set(items) { const clean = (Array.isArray(items) ? items : []).filter( (id) => typeof id === 'string' && id, ); this.items = clean.slice(0, this._capacity); return this.items.length; } /** A copy of the filled-bay id list (safe to iterate / spread). */ snapshot() { return [...this.items]; } /** Serialize (SaveData.js → captureState → record.ship.cargo). */ toJSON() { return { capacity: this._capacity, items: [...this.items] }; } /** * Deserialize (GameScene.applyRestore). Missing fields degrade to an empty * hold (a save predating cargo loads as an empty one). */ static fromJSON(o) { const h = new CargoHold(o?.capacity ?? 0); h.set(o?.items ?? []); return h; } /** A deep copy (tests, previews). */ clone() { return CargoHold.fromJSON(this.toJSON()); } }