fix: prod-bundle boot, cloud-existence leak, observation gating (final review)

- Wrap main.js body in async main() to eliminate top-level await deadlock in Rollup
- Add npc.lastPlan so clouds persist after hidden arrival/destruction until analytic envelope expires
- Add maxEnvelopeArrival() helper; rewrite playerClouds() to use lastPlan for envelope-gated clouds
- Block markSeen() while player is mid-lane (no observation from hyperspace)
- Pass obsSys=null to playerClouds when player is in a lane (don't exclude visible band from hyperspace)
- Galaxy map clicks now select-only (intel); jumps via Nav buttons / gate diamonds only
- Add preview script, favicon no-op, .playwright-mcp/ and *.png to .gitignore, update README controls text
This commit is contained in:
2026-06-12 15:35:20 +00:00
parent 395c0e851d
commit 051a111855
11 changed files with 116 additions and 78 deletions
+18 -3
View File
@@ -86,13 +86,28 @@ function normalize(segs) {
return segs.map((s) => ({ ...s, w: s.w / m }));
}
// All player-visible clouds: seen, alive, in-transit NPCs on multi-system runs.
// Latest possible arrival under the analytic envelope (slowest legal journey).
export function maxEnvelopeArrival(galaxy, plan) {
const laneIds = plan.routeLanes ?? plan.legs.map((l) => l.laneId);
let t = plan.startTime;
for (const laneId of laneIds) t += galaxy.lanes[laneId].days * (JIT_MIN + JIT_SPAN) * DELAY_MULT;
return t;
}
// All player-visible clouds: seen NPCs whose cloud envelope hasn't yet expired.
export function playerClouds(world, observedSystemId) {
const out = [];
for (const npcId of world.player.seenNpcs) {
const npc = world.npcs[npcId];
if (!npc.alive || npc.state !== 'transit' || !npc.plan) continue;
let segs = cloudFor(world.galaxy, npc.plan, world.t);
// Active journey, or a finished/doomed one whose envelope hasn't expired:
// cloud EXISTENCE must not reveal the hidden outcome either.
let plan = null;
if (npc.alive && npc.state === 'transit' && npc.plan) plan = npc.plan;
else if (npc.lastPlan && world.t < maxEnvelopeArrival(world.galaxy, npc.lastPlan)) plan = npc.lastPlan;
if (!plan) continue;
// A ship the player can concretely see right now has no cloud.
if (npc.alive && npc.state === 'docked' && npc.systemId === observedSystemId) continue;
let segs = cloudFor(world.galaxy, plan, world.t);
if (observedSystemId != null) segs = excludeVisibleBand(world.galaxy, segs, observedSystemId);
if (segs.length > 0) out.push({ npcId, name: npc.name, segs });
}
+1
View File
@@ -22,6 +22,7 @@ export function playerKnownPrices(world, planetId) {
// the player's system becomes "seen" — its cloud is now player-visible forever.
export function markSeen(world) {
if (!world.player.alive) return;
if (world.player.loc.kind === 'lane') return; // no observation from hyperspace
const sys = playerSystem(world);
if (sys == null) return;
for (const { npc } of shipsIn(world, sys)) world.player.seenNpcs.add(npc.id);
+3
View File
@@ -102,6 +102,7 @@ export function planNpc(world, npc) {
routeLanes: best.route.lanes, startSys: origin.systemId,
startTime: t, ...itin,
};
npc.lastPlan = null; // superseded; v1 simplification: a fresh cloud implies the previous run ended
npc.state = 'transit';
world.queue.push(itin.arriveTime, 'npc-arrive', { npcId: npc.id, planId });
}
@@ -113,6 +114,7 @@ export function onNpcArrive(world, { npcId, planId }) {
const plan = npc.plan;
if (plan.finalOutcome === 'destroyed') {
npc.alive = false;
npc.lastPlan = plan; // cloud persists from the envelope until observed/expired
npc.plan = null;
npc.cargo = null;
npc.state = 'destroyed';
@@ -128,6 +130,7 @@ export function onNpcArrive(world, { npcId, planId }) {
npc.credits = Math.round((npc.credits + price * npc.cargo.qty) * 100) / 100;
world.stats.trades++;
}
npc.lastPlan = plan; // unobserved arrival: the player's cloud persists a while
npc.cargo = null;
npc.plan = null;
world.queue.push(world.t + DOCK_DELAY, 'npc-replan', { npcId });