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:
@@ -1,2 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
|
.playwright-mcp/
|
||||||
|
*.png
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ from per-ship seeds) — the probability is *yours*, not the universe's.
|
|||||||
|
|
||||||
- You start docked. Buy what a planet produces cheap, sell where it's consumed.
|
- You start docked. Buy what a planet produces cheap, sell where it's consumed.
|
||||||
- Sidebar: navigation, market (when docked), cargo, intel, log. Space = pause,
|
- Sidebar: navigation, market (when docked), cargo, intel, log. Space = pause,
|
||||||
Tab = map/system view. Click planets/gates/systems to move; click systems on
|
Tab = map/system view. Move by clicking planets and gate diamonds in the
|
||||||
the map for price intel — note the age tag, old news lies.
|
system view, or use the Navigation buttons. Click systems on the galaxy map
|
||||||
|
for price intel — note the age tag, old news lies.
|
||||||
- Lanes are tinted by pirate danger. Dangerous shortcuts are profitable until
|
- Lanes are tinted by pirate danger. Dangerous shortcuts are profitable until
|
||||||
they aren't.
|
they aren't.
|
||||||
- Orange smears on the map are ships you've met: where they probably are now.
|
- Orange smears on the map are ships you've met: where they probably are now.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Probability</title>
|
<title>Probability</title>
|
||||||
|
<link rel="icon" href="data:," />
|
||||||
<link rel="stylesheet" href="/src/styles.css" />
|
<link rel="stylesheet" href="/src/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
"test": "vitest run"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { SystemView } from './render/systemView.js';
|
|||||||
import { Panel } from './ui/panel.js';
|
import { Panel } from './ui/panel.js';
|
||||||
import { goTo, playerSystem } from './sim/player.js';
|
import { goTo, playerSystem } from './sim/player.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
const world = createWorld(new URLSearchParams(location.search).get('seed') ?? 'neon-1');
|
const world = createWorld(new URLSearchParams(location.search).get('seed') ?? 'neon-1');
|
||||||
const renderer = await createRenderer(document.getElementById('stage'));
|
const renderer = await createRenderer(document.getElementById('stage'));
|
||||||
|
|
||||||
@@ -72,3 +73,5 @@ renderer.app.ticker.add(() => {
|
|||||||
document.getElementById('stage').appendChild(banner);
|
document.getElementById('stage').appendChild(banner);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
main();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { playerSystem } from '../sim/player.js';
|
|||||||
import { otherEnd } from '../sim/galaxy.js';
|
import { otherEnd } from '../sim/galaxy.js';
|
||||||
|
|
||||||
export class GalaxyView {
|
export class GalaxyView {
|
||||||
// onSelectSystem(systemId) -> UI intel panel; onJump(laneId) -> goTo lane
|
// onSelectSystem(systemId) -> UI intel panel
|
||||||
constructor(renderer, world, { onSelectSystem, onJump }) {
|
constructor(renderer, world, { onSelectSystem, onJump }) {
|
||||||
this.renderer = renderer;
|
this.renderer = renderer;
|
||||||
this.world = world;
|
this.world = world;
|
||||||
@@ -56,13 +56,6 @@ export class GalaxyView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSystemTap(systemId) {
|
handleSystemTap(systemId) {
|
||||||
const sim = this.world;
|
|
||||||
const cur = playerSystem(sim);
|
|
||||||
// Clicking a neighbor = jump intent; clicking anything = intel selection.
|
|
||||||
const lane = sim.galaxy.lanes.find(
|
|
||||||
(l) => (l.a === cur && l.b === systemId) || (l.b === cur && l.a === systemId)
|
|
||||||
);
|
|
||||||
if (lane) this.onJump(lane.id);
|
|
||||||
this.onSelectSystem(systemId);
|
this.onSelectSystem(systemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,11 +70,13 @@ export class GalaxyView {
|
|||||||
const sim = this.world;
|
const sim = this.world;
|
||||||
const v = this.v;
|
const v = this.v;
|
||||||
const cur = playerSystem(sim);
|
const cur = playerSystem(sim);
|
||||||
|
// Mid-lane the player observes nothing; don't exclude any visible band.
|
||||||
|
const obsSys = sim.player.loc.kind === 'lane' ? null : cur;
|
||||||
|
|
||||||
// Probability clouds: glowing strokes along lane segments, alpha ~ weight.
|
// Probability clouds: glowing strokes along lane segments, alpha ~ weight.
|
||||||
const cg = this.cloudG;
|
const cg = this.cloudG;
|
||||||
cg.clear();
|
cg.clear();
|
||||||
for (const cloud of playerClouds(sim, cur)) {
|
for (const cloud of playerClouds(sim, obsSys)) {
|
||||||
for (const seg of cloud.segs) {
|
for (const seg of cloud.segs) {
|
||||||
const lane = sim.galaxy.lanes[seg.laneId];
|
const lane = sim.galaxy.lanes[seg.laneId];
|
||||||
const p0 = this.lanePoint(lane, seg.fromSys, seg.f0);
|
const p0 = this.lanePoint(lane, seg.fromSys, seg.f0);
|
||||||
|
|||||||
+18
-3
@@ -86,13 +86,28 @@ function normalize(segs) {
|
|||||||
return segs.map((s) => ({ ...s, w: s.w / m }));
|
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) {
|
export function playerClouds(world, observedSystemId) {
|
||||||
const out = [];
|
const out = [];
|
||||||
for (const npcId of world.player.seenNpcs) {
|
for (const npcId of world.player.seenNpcs) {
|
||||||
const npc = world.npcs[npcId];
|
const npc = world.npcs[npcId];
|
||||||
if (!npc.alive || npc.state !== 'transit' || !npc.plan) continue;
|
// Active journey, or a finished/doomed one whose envelope hasn't expired:
|
||||||
let segs = cloudFor(world.galaxy, npc.plan, world.t);
|
// 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 (observedSystemId != null) segs = excludeVisibleBand(world.galaxy, segs, observedSystemId);
|
||||||
if (segs.length > 0) out.push({ npcId, name: npc.name, segs });
|
if (segs.length > 0) out.push({ npcId, name: npc.name, segs });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function playerKnownPrices(world, planetId) {
|
|||||||
// the player's system becomes "seen" — its cloud is now player-visible forever.
|
// the player's system becomes "seen" — its cloud is now player-visible forever.
|
||||||
export function markSeen(world) {
|
export function markSeen(world) {
|
||||||
if (!world.player.alive) return;
|
if (!world.player.alive) return;
|
||||||
|
if (world.player.loc.kind === 'lane') return; // no observation from hyperspace
|
||||||
const sys = playerSystem(world);
|
const sys = playerSystem(world);
|
||||||
if (sys == null) return;
|
if (sys == null) return;
|
||||||
for (const { npc } of shipsIn(world, sys)) world.player.seenNpcs.add(npc.id);
|
for (const { npc } of shipsIn(world, sys)) world.player.seenNpcs.add(npc.id);
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export function planNpc(world, npc) {
|
|||||||
routeLanes: best.route.lanes, startSys: origin.systemId,
|
routeLanes: best.route.lanes, startSys: origin.systemId,
|
||||||
startTime: t, ...itin,
|
startTime: t, ...itin,
|
||||||
};
|
};
|
||||||
|
npc.lastPlan = null; // superseded; v1 simplification: a fresh cloud implies the previous run ended
|
||||||
npc.state = 'transit';
|
npc.state = 'transit';
|
||||||
world.queue.push(itin.arriveTime, 'npc-arrive', { npcId: npc.id, planId });
|
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;
|
const plan = npc.plan;
|
||||||
if (plan.finalOutcome === 'destroyed') {
|
if (plan.finalOutcome === 'destroyed') {
|
||||||
npc.alive = false;
|
npc.alive = false;
|
||||||
|
npc.lastPlan = plan; // cloud persists from the envelope until observed/expired
|
||||||
npc.plan = null;
|
npc.plan = null;
|
||||||
npc.cargo = null;
|
npc.cargo = null;
|
||||||
npc.state = 'destroyed';
|
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;
|
npc.credits = Math.round((npc.credits + price * npc.cargo.qty) * 100) / 100;
|
||||||
world.stats.trades++;
|
world.stats.trades++;
|
||||||
}
|
}
|
||||||
|
npc.lastPlan = plan; // unobserved arrival: the player's cloud persists a while
|
||||||
npc.cargo = null;
|
npc.cargo = null;
|
||||||
npc.plan = null;
|
npc.plan = null;
|
||||||
world.queue.push(world.t + DOCK_DELAY, 'npc-replan', { npcId });
|
world.queue.push(world.t + DOCK_DELAY, 'npc-replan', { npcId });
|
||||||
|
|||||||
+17
-1
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { cloudFor, excludeVisibleBand, totalMass } from '../src/sim/clouds.js';
|
import { cloudFor, excludeVisibleBand, totalMass, playerClouds } from '../src/sim/clouds.js';
|
||||||
|
|
||||||
// Synthetic 3-lane chain: systems 0-1-2-3, each lane 2 days.
|
// Synthetic 3-lane chain: systems 0-1-2-3, each lane 2 days.
|
||||||
function chainGalaxy() {
|
function chainGalaxy() {
|
||||||
@@ -88,4 +88,20 @@ describe('clouds', () => {
|
|||||||
expect(JSON.stringify(cloudFor(g, doomed, t))).toBe(JSON.stringify(cloudFor(g, intact, t)));
|
expect(JSON.stringify(cloudFor(g, doomed, t))).toBe(JSON.stringify(cloudFor(g, intact, t)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('a cloud persists after a hidden early arrival or destruction until the envelope expires', () => {
|
||||||
|
const g = chainGalaxy();
|
||||||
|
const lastPlan = chainPlan();
|
||||||
|
lastPlan.routeLanes = [0, 1, 2];
|
||||||
|
lastPlan.startSys = 0;
|
||||||
|
const world = {
|
||||||
|
t: 4.5, // past a truncated arrival, inside the envelope (max = 6 * 1.15 * 1.6 = 11.04)
|
||||||
|
galaxy: g,
|
||||||
|
npcs: [{ id: 0, name: 'Ghost', alive: false, state: 'destroyed', plan: null, lastPlan }],
|
||||||
|
player: { seenNpcs: new Set([0]) },
|
||||||
|
};
|
||||||
|
expect(playerClouds(world, null).length).toBe(1);
|
||||||
|
world.t = 12; // envelope exhausted: cloud finally gone
|
||||||
|
expect(playerClouds(world, null)).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: { target: 'esnext' }, // top-level await in src/main.js
|
build: { target: 'esnext' }, // modern output target
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user