149 lines
4.5 KiB
JavaScript
149 lines
4.5 KiB
JavaScript
/**
|
||
* 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());
|
||
}
|
||
}
|