From 06957c65e19694b62daa192f5e9d00016d0ad2c4 Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Fri, 12 Jun 2026 14:34:22 +0000 Subject: [PATCH] feat: player knowledge layer - aging price intel and seen ships --- src/sim/knowledge.js | 28 ++++++++++++++++++++++++++- tests/knowledge.test.js | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 tests/knowledge.test.js diff --git a/src/sim/knowledge.js b/src/sim/knowledge.js index 272ecbd..8f62e0c 100644 --- a/src/sim/knowledge.js +++ b/src/sim/knowledge.js @@ -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); +} diff --git a/tests/knowledge.test.js b/tests/knowledge.test.js new file mode 100644 index 0000000..494b942 --- /dev/null +++ b/tests/knowledge.test.js @@ -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); + }); +});