feat: world orchestrator with living NPC economy

This commit is contained in:
2026-06-12 14:22:42 +00:00
parent 29b1aef7d3
commit b2aef9e72c
4 changed files with 148 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
export function markSeen() {}
+6
View File
@@ -0,0 +1,6 @@
export function onPlayerArrive() {}
export function playerSystem(world) {
const loc = world.player.loc;
if (loc.kind === 'docked') return world.galaxy.planets[loc.planetId].systemId;
return loc.systemId ?? null;
}
+91
View File
@@ -0,0 +1,91 @@
import { OBSERVE_INTERVAL, PLAYER_START_CREDITS, PLAYER_CAPACITY } from './constants.js';
import { generateGalaxy, hopMatrix } from './galaxy.js';
import { NewsLedger } from './news.js';
import { EventQueue } from './events.js';
import { makeRng, hash32 } from './rng.js';
import { planNpc, onNpcArrive } from './npc.js';
import { onPlayerArrive } from './player.js';
import { markSeen } from './knowledge.js';
const NPC_FIRST_NAMES = ['Vask', 'Orla', 'Dex', 'Mira', 'Joss', 'Kade', 'Nyra', 'Tarn', 'Sela', 'Bram'];
export function createWorld(seed, { systemCount = 25, npcCount = 120 } = {}) {
const galaxy = generateGalaxy(seed, { systemCount });
const rng = makeRng(hash32('world:' + seed));
const news = new NewsLedger();
news.record(galaxy, 0);
const npcs = [];
for (let i = 0; i < npcCount; i++) {
const planet = galaxy.planets[Math.floor(rng() * galaxy.planets.length)];
npcs.push({
id: i,
seed: `w:${seed}:npc:${i}`,
name: `${NPC_FIRST_NAMES[i % NPC_FIRST_NAMES.length]}-${i}`,
alive: true,
credits: Math.floor(1500 + rng() * 2000),
capacity: 20 + Math.floor(rng() * 40),
cargo: null,
planetId: planet.id,
systemId: planet.systemId,
state: 'docked',
plan: null,
planCount: 0,
});
}
const startPlanet = galaxy.planets[0];
const world = {
seed, t: 0, galaxy, hops: hopMatrix(galaxy), news, npcs,
queue: new EventQueue(),
stats: { trades: 0, npcDestroyed: 0 },
log: [],
player: {
credits: PLAYER_START_CREDITS,
capacity: PLAYER_CAPACITY,
cargo: {},
loc: { kind: 'docked', planetId: startPlanet.id },
intent: null,
tripCount: 0,
alive: true,
won: false,
priceBook: {},
seenNpcs: new Set(),
},
};
// Stagger initial NPC planning across the first day.
for (const npc of npcs) world.queue.push(rng(), 'npc-replan', { npcId: npc.id });
world.queue.push(1, 'snapshot', {});
world.queue.push(OBSERVE_INTERVAL, 'observe', {});
return world;
}
export function pushLog(world, msg) {
world.log.push(`[day ${world.t.toFixed(1)}] ${msg}`);
if (world.log.length > 50) world.log.shift();
}
const HANDLERS = {
'npc-replan': (world, data) => planNpc(world, world.npcs[data.npcId]),
'npc-arrive': (world, data) => onNpcArrive(world, data),
'player-arrive': (world, data) => onPlayerArrive(world, data),
snapshot: (world) => {
world.news.record(world.galaxy, world.t);
world.queue.push(world.t + 1, 'snapshot', {});
},
observe: (world) => {
markSeen(world);
world.queue.push(world.t + OBSERVE_INTERVAL, 'observe', {});
},
};
// Advance the world to absolute time toTime, processing all due events in order.
export function advance(world, toTime) {
while (world.queue.size > 0 && world.queue.peek().time <= toTime) {
const ev = world.queue.pop();
world.t = ev.time;
HANDLERS[ev.type](world, ev.data);
}
world.t = toTime;
}