100 lines
4.4 KiB
JavaScript
100 lines
4.4 KiB
JavaScript
/**
|
||
* Stock — the origin world's time-gated daily batch.
|
||
*
|
||
* MODEL (model v2 — the one stateful piece of the economy, see
|
||
* docs/ECONOMY.md §5 + data/economy.json → stock): every ORIGIN system's
|
||
* signature good comes to market as a DAILY BATCH of `stock.dailyBatch`
|
||
* units. A DAY is `stock.dayMs` of wall-clock time past `stock.epoch` —
|
||
* the batch regenerates while the player is away, so the local buy→sell
|
||
* loop (buy at the origin world, sell to the same-system station) is a
|
||
* BOUNDED pocket change, never infinite money — while long-haul SELLs stay
|
||
* unlimited (the market is a sink; the buy side is the only limit).
|
||
*
|
||
* The SELL side is NEVER stock-limited: a shop always buys back at its
|
||
* fixed price. The state is the player's REMAINING batch per origin
|
||
* system — a tiny save-safe map:
|
||
*
|
||
* economyStock = { [systemId]: { day: <int>, left: <int> } }
|
||
*
|
||
* (absent key = a full batch for today; a stale `day` = today's full
|
||
* batch — the batch is per-DAY, so yesterday's remainder is worthless).
|
||
*
|
||
* Pure, framework-free, save-clone-safe (plain objects, integers). The
|
||
* scene owns the map (GameScene.economyStock — save/restore in
|
||
* js/save/SaveData.js); ShopView reads remaining() to render the row
|
||
* ("N left" / "SOLD OUT") and to cap the stepper; confirm() decrements
|
||
* via spend(). The day clock uses Date.now() (wall time — the batch is a
|
||
* real-day game, not a session one); tests pass an explicit `now`.
|
||
*/
|
||
import { config } from '../config/Config.js';
|
||
|
||
const stockCfg = () => (config.section('economy.stock', {}) ?? {});
|
||
|
||
/** Units per origin per day (data/economy.json → stock.dailyBatch). */
|
||
export function dailyBatch() {
|
||
return Math.max(1, Math.floor(Number(stockCfg().dailyBatch) || 6));
|
||
}
|
||
|
||
/** Milliseconds per DAY (the batch's clock period). */
|
||
export function dayMs() {
|
||
return Math.max(1000, Math.floor(Number(stockCfg().dayMs) || 3600000));
|
||
}
|
||
|
||
/** The batch epoch (ms) — the day counter starts here (0 = the Unix epoch). */
|
||
export function epochMs() {
|
||
const e = stockCfg().epoch;
|
||
const t = typeof e === 'string' ? Date.parse(e) : (Number.isFinite(e) ? Number(e) : NaN);
|
||
return Number.isFinite(t) ? t : 0;
|
||
}
|
||
|
||
/**
|
||
* The current DAY (integer) for a wall-clock instant: floor((now − epoch)
|
||
* / dayMs). The batch's identity — a purchase belongs to the day it was
|
||
* made, and the batch regenerates when the day rolls over (wall clock,
|
||
* so an away player finds a fresh batch on return).
|
||
*/
|
||
export function economyDay(now = Date.now()) {
|
||
const t = Number.isFinite(now) ? Number(now) : Date.now();
|
||
return Math.floor(Math.max(0, t - epochMs()) / dayMs());
|
||
}
|
||
|
||
/**
|
||
* Remaining batch for an origin system on a given day.
|
||
* map — the player's stock map { [systemId]: { day, left } } (save).
|
||
* systemId — the ORIGIN system (the batch is per origin, per day).
|
||
* day — the day to check (default: the live economyDay()).
|
||
* @returns {number} 0..dailyBatch() — a full batch when the system has no
|
||
* entry for that day (fresh) or its entry is from another day (stale).
|
||
*/
|
||
export function batchRemaining(map, systemId, day = economyDay()) {
|
||
const rec = map && typeof map === 'object' ? map[systemId] : null;
|
||
if (rec && Number(rec.day) === Number(day) && Number.isFinite(rec.left)) {
|
||
return Math.max(0, Math.min(dailyBatch(), Math.floor(Number(rec.left))));
|
||
}
|
||
return dailyBatch();
|
||
}
|
||
|
||
/**
|
||
* The stock map AFTER a purchase (pure — returns a new map; the scene
|
||
* swaps the reference in, the save picks it up). A dry day is recorded as
|
||
* `{ day, left: 0 }` (NOT dropped — an absent entry reads as a FULL batch
|
||
* for the day, so a dry system must keep its zero entry). qty over the
|
||
* remaining batch is clamped (the UI + tradePreview both enforce the cap;
|
||
* this is the last line of defense). Stale-day entries for OTHER systems
|
||
* are pruned (worthless after the day rolls).
|
||
*/
|
||
export function spend(map, systemId, qty, day = economyDay()) {
|
||
const left = batchRemaining(map, systemId, day) - Math.max(0, Math.floor(Number(qty) || 0));
|
||
const out = {};
|
||
if (map && typeof map === 'object') {
|
||
for (const [k, v] of Object.entries(map)) {
|
||
if (k === systemId) continue;
|
||
if (v && typeof v === 'object' && Number(v.day) === Number(day) && Number.isFinite(v.left)) {
|
||
out[k] = { day: Number(v.day), left: Math.max(0, Math.floor(Number(v.left))) };
|
||
}
|
||
}
|
||
}
|
||
out[systemId] = { day: Number(day), left: Math.max(0, Math.min(dailyBatch(), left)) };
|
||
return out;
|
||
}
|