orbit/js/economy/CargoHold.js

149 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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());
}
}