feat: player knowledge layer - aging price intel and seen ships
This commit is contained in:
+27
-1
@@ -1 +1,27 @@
|
|||||||
export function markSeen() {}
|
import { playerSystem } from './player.js';
|
||||||
|
import { shipsIn } from './npc.js';
|
||||||
|
|
||||||
|
// Best price intel the player has for a planet: personal dock visit
|
||||||
|
// vs. news that has reached the player's current location. Caches the
|
||||||
|
// winner in the price book so intel never gets worse.
|
||||||
|
export function playerKnownPrices(world, planetId) {
|
||||||
|
const p = world.player;
|
||||||
|
const sys = playerSystem(world);
|
||||||
|
if (sys == null) return p.priceBook[planetId] ?? null;
|
||||||
|
const planet = world.galaxy.planets[planetId];
|
||||||
|
const news = world.news.knownPrices(planetId, world.hops[sys][planet.systemId], world.t);
|
||||||
|
const book = p.priceBook[planetId] ?? null;
|
||||||
|
let best = book;
|
||||||
|
if (news && (!best || news.time > best.time)) best = news;
|
||||||
|
if (best && (!book || best.time > book.time)) p.priceBook[planetId] = best;
|
||||||
|
return best ? { time: best.time, prices: best.prices, ageDays: world.t - best.time } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by the world's observe heartbeat: every concrete ship sharing
|
||||||
|
// the player's system becomes "seen" — its cloud is now player-visible forever.
|
||||||
|
export function markSeen(world) {
|
||||||
|
if (!world.player.alive) return;
|
||||||
|
const sys = playerSystem(world);
|
||||||
|
if (sys == null) return;
|
||||||
|
for (const { npc } of shipsIn(world, sys)) world.player.seenNpcs.add(npc.id);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { createWorld, advance } from '../src/sim/world.js';
|
||||||
|
import { playerKnownPrices } from '../src/sim/knowledge.js';
|
||||||
|
import { playerSystem } from '../src/sim/player.js';
|
||||||
|
|
||||||
|
describe('knowledge', () => {
|
||||||
|
it('knows every planet at game start via the day-0 snapshot', () => {
|
||||||
|
const w = createWorld('know-test', { systemCount: 10, npcCount: 5 });
|
||||||
|
for (const planet of w.galaxy.planets) {
|
||||||
|
const known = playerKnownPrices(w, planet.id);
|
||||||
|
expect(known).not.toBeNull();
|
||||||
|
expect(known.time).toBe(0);
|
||||||
|
expect(known.ageDays).toBe(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('intel ages and refreshes as news travels', () => {
|
||||||
|
const w = createWorld('know-test', { systemCount: 10, npcCount: 5 });
|
||||||
|
const sys = playerSystem(w);
|
||||||
|
const far = w.galaxy.planets.find((p) => w.hops[sys][p.systemId] >= 2);
|
||||||
|
advance(w, 5);
|
||||||
|
const known = playerKnownPrices(w, far.id);
|
||||||
|
const hops = w.hops[sys][far.systemId];
|
||||||
|
// freshest possible news is `hops` days old
|
||||||
|
expect(known.ageDays).toBeGreaterThanOrEqual(hops - 1e-9);
|
||||||
|
expect(known.ageDays).toBeLessThanOrEqual(hops + 1 + 1e-9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('local planet intel is near-live', () => {
|
||||||
|
const w = createWorld('know-test', { systemCount: 10, npcCount: 5 });
|
||||||
|
advance(w, 5);
|
||||||
|
const local = w.galaxy.planets.find((p) => p.systemId === playerSystem(w));
|
||||||
|
expect(playerKnownPrices(w, local.id).ageDays).toBeLessThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('the observe heartbeat marks co-located NPCs as seen', () => {
|
||||||
|
const w = createWorld('know-test', { systemCount: 10, npcCount: 40 });
|
||||||
|
advance(w, 10);
|
||||||
|
// with 40 NPCs over 10 days, some must have passed through the player's system
|
||||||
|
expect(w.player.seenNpcs.size).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user