feat: world orchestrator with living NPC economy
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export function markSeen() {}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user