|
|
|
@@ -53,17 +53,43 @@ interface MoonVisual {
|
|
|
|
orbit: THREE.LineLoop;
|
|
|
|
orbit: THREE.LineLoop;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type OrbitalAnchor =
|
|
|
|
|
|
|
|
| { kind: "star" }
|
|
|
|
|
|
|
|
| { kind: "planet"; planetIndex: number }
|
|
|
|
|
|
|
|
| { kind: "moon"; planetIndex: number; moonIndex: number };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
interface NodeVisual {
|
|
|
|
|
|
|
|
systemId: string;
|
|
|
|
|
|
|
|
mesh: THREE.Mesh;
|
|
|
|
|
|
|
|
icon: THREE.Sprite;
|
|
|
|
|
|
|
|
sourceKind: string;
|
|
|
|
|
|
|
|
anchor: OrbitalAnchor;
|
|
|
|
|
|
|
|
localPosition: THREE.Vector3;
|
|
|
|
|
|
|
|
orbitRadius: number;
|
|
|
|
|
|
|
|
orbitPhase: number;
|
|
|
|
|
|
|
|
orbitInclination: number;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface StructureVisual {
|
|
|
|
interface StructureVisual {
|
|
|
|
systemId: string;
|
|
|
|
systemId: string;
|
|
|
|
mesh: THREE.Mesh;
|
|
|
|
mesh: THREE.Mesh;
|
|
|
|
icon: THREE.Sprite;
|
|
|
|
icon: THREE.Sprite;
|
|
|
|
worldPosition: THREE.Vector3;
|
|
|
|
anchor: OrbitalAnchor;
|
|
|
|
|
|
|
|
orbitRadius: number;
|
|
|
|
|
|
|
|
orbitPhase: number;
|
|
|
|
|
|
|
|
orbitInclination: number;
|
|
|
|
|
|
|
|
localPosition: THREE.Vector3;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface SystemVisual {
|
|
|
|
interface SystemVisual {
|
|
|
|
root: THREE.Group;
|
|
|
|
root: THREE.Group;
|
|
|
|
|
|
|
|
starCluster: THREE.Group;
|
|
|
|
|
|
|
|
icon: THREE.Sprite;
|
|
|
|
|
|
|
|
shellReticle: THREE.Sprite;
|
|
|
|
|
|
|
|
shellReticleBaseScale: number;
|
|
|
|
detailGroup: THREE.Group;
|
|
|
|
detailGroup: THREE.Group;
|
|
|
|
summary: SystemSummaryVisual;
|
|
|
|
summary: SystemSummaryVisual;
|
|
|
|
|
|
|
|
galaxyPosition: THREE.Vector3;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface WorldState {
|
|
|
|
interface WorldState {
|
|
|
|
@@ -128,8 +154,11 @@ const ZOOM_DISTANCE: Record<ZoomLevel, number> = {
|
|
|
|
system: 3200,
|
|
|
|
system: 3200,
|
|
|
|
universe: 26000,
|
|
|
|
universe: 26000,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
const ACTIVE_SYSTEM_DETAIL_SCALE = 2.2;
|
|
|
|
const ACTIVE_SYSTEM_DETAIL_SCALE = 10;
|
|
|
|
|
|
|
|
const GALAXY_PARALLAX_FACTOR = 0.025;
|
|
|
|
const ACTIVE_SYSTEM_CAPTURE_RADIUS = 9000;
|
|
|
|
const ACTIVE_SYSTEM_CAPTURE_RADIUS = 9000;
|
|
|
|
|
|
|
|
const PROJECTED_GALAXY_RADIUS = 65000;
|
|
|
|
|
|
|
|
const STAR_RENDER_SCALE = 0.18;
|
|
|
|
const MIN_CAMERA_DISTANCE = 450;
|
|
|
|
const MIN_CAMERA_DISTANCE = 450;
|
|
|
|
const MAX_CAMERA_DISTANCE = 42000;
|
|
|
|
const MAX_CAMERA_DISTANCE = 42000;
|
|
|
|
|
|
|
|
|
|
|
|
@@ -143,11 +172,12 @@ export class GameViewer {
|
|
|
|
private readonly container: HTMLElement;
|
|
|
|
private readonly container: HTMLElement;
|
|
|
|
private readonly renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
|
|
private readonly renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
|
|
private readonly scene = new THREE.Scene();
|
|
|
|
private readonly scene = new THREE.Scene();
|
|
|
|
private readonly camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100000);
|
|
|
|
private readonly camera = new THREE.PerspectiveCamera(50, 1, 0.1, 160000);
|
|
|
|
private readonly clock = new THREE.Clock();
|
|
|
|
private readonly clock = new THREE.Clock();
|
|
|
|
private readonly raycaster = new THREE.Raycaster();
|
|
|
|
private readonly raycaster = new THREE.Raycaster();
|
|
|
|
private readonly mouse = new THREE.Vector2();
|
|
|
|
private readonly mouse = new THREE.Vector2();
|
|
|
|
private readonly focus = new THREE.Vector3(2200, 0, 300);
|
|
|
|
private readonly galaxyFocus = new THREE.Vector3(2200, 0, 300);
|
|
|
|
|
|
|
|
private readonly systemFocusLocal = new THREE.Vector3();
|
|
|
|
private readonly cameraOffset = new THREE.Vector3();
|
|
|
|
private readonly cameraOffset = new THREE.Vector3();
|
|
|
|
private readonly keyState = new Set<string>();
|
|
|
|
private readonly keyState = new Set<string>();
|
|
|
|
private readonly systemGroup = new THREE.Group();
|
|
|
|
private readonly systemGroup = new THREE.Group();
|
|
|
|
@@ -157,7 +187,7 @@ export class GameViewer {
|
|
|
|
private readonly ambienceGroup = new THREE.Group();
|
|
|
|
private readonly ambienceGroup = new THREE.Group();
|
|
|
|
private readonly selectableTargets = new Map<THREE.Object3D, Selectable>();
|
|
|
|
private readonly selectableTargets = new Map<THREE.Object3D, Selectable>();
|
|
|
|
private readonly presentationEntries: PresentationEntry[] = [];
|
|
|
|
private readonly presentationEntries: PresentationEntry[] = [];
|
|
|
|
private readonly nodeVisuals = new Map<string, StructureVisual>();
|
|
|
|
private readonly nodeVisuals = new Map<string, NodeVisual>();
|
|
|
|
private readonly stationVisuals = new Map<string, StructureVisual>();
|
|
|
|
private readonly stationVisuals = new Map<string, StructureVisual>();
|
|
|
|
private readonly shipVisuals = new Map<string, ShipVisual>();
|
|
|
|
private readonly shipVisuals = new Map<string, ShipVisual>();
|
|
|
|
private readonly systemVisuals = new Map<string, SystemVisual>();
|
|
|
|
private readonly systemVisuals = new Map<string, SystemVisual>();
|
|
|
|
@@ -175,6 +205,7 @@ export class GameViewer {
|
|
|
|
private readonly performancePanelEl: HTMLDivElement;
|
|
|
|
private readonly performancePanelEl: HTMLDivElement;
|
|
|
|
private readonly errorEl: HTMLDivElement;
|
|
|
|
private readonly errorEl: HTMLDivElement;
|
|
|
|
private readonly marqueeEl: HTMLDivElement;
|
|
|
|
private readonly marqueeEl: HTMLDivElement;
|
|
|
|
|
|
|
|
private readonly hoverLabelEl: HTMLDivElement;
|
|
|
|
|
|
|
|
|
|
|
|
private world?: WorldState;
|
|
|
|
private world?: WorldState;
|
|
|
|
private worldTimeSyncMs = performance.now();
|
|
|
|
private worldTimeSyncMs = performance.now();
|
|
|
|
@@ -257,6 +288,7 @@ export class GameViewer {
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<section class="faction-strip"></section>
|
|
|
|
<section class="faction-strip"></section>
|
|
|
|
<div class="marquee-box"></div>
|
|
|
|
<div class="marquee-box"></div>
|
|
|
|
|
|
|
|
<div class="hover-label" hidden></div>
|
|
|
|
`;
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
this.statusEl = hud.querySelector(".topbar-body") as HTMLDivElement;
|
|
|
|
this.statusEl = hud.querySelector(".topbar-body") as HTMLDivElement;
|
|
|
|
@@ -270,6 +302,7 @@ export class GameViewer {
|
|
|
|
this.performancePanelEl = hud.querySelector(".performance-body") as HTMLDivElement;
|
|
|
|
this.performancePanelEl = hud.querySelector(".performance-body") as HTMLDivElement;
|
|
|
|
this.errorEl = hud.querySelector(".error-strip") as HTMLDivElement;
|
|
|
|
this.errorEl = hud.querySelector(".error-strip") as HTMLDivElement;
|
|
|
|
this.marqueeEl = hud.querySelector(".marquee-box") as HTMLDivElement;
|
|
|
|
this.marqueeEl = hud.querySelector(".marquee-box") as HTMLDivElement;
|
|
|
|
|
|
|
|
this.hoverLabelEl = hud.querySelector(".hover-label") as HTMLDivElement;
|
|
|
|
|
|
|
|
|
|
|
|
this.container.append(this.renderer.domElement, hud);
|
|
|
|
this.container.append(this.renderer.domElement, hud);
|
|
|
|
|
|
|
|
|
|
|
|
@@ -423,16 +456,27 @@ export class GameViewer {
|
|
|
|
|
|
|
|
|
|
|
|
for (const system of systems) {
|
|
|
|
for (const system of systems) {
|
|
|
|
const root = new THREE.Group();
|
|
|
|
const root = new THREE.Group();
|
|
|
|
root.position.set(system.position.x, system.position.y, system.position.z);
|
|
|
|
root.position.set(system.galaxyPosition.x, system.galaxyPosition.y, system.galaxyPosition.z);
|
|
|
|
const detailGroup = new THREE.Group();
|
|
|
|
const detailGroup = new THREE.Group();
|
|
|
|
|
|
|
|
const renderedStarSize = Math.max(system.starSize * STAR_RENDER_SCALE, 8);
|
|
|
|
|
|
|
|
|
|
|
|
const starCluster = this.createStarCluster(system);
|
|
|
|
const starCluster = this.createStarCluster(system);
|
|
|
|
const systemIcon = this.createTacticalIcon(system.starColor, 96);
|
|
|
|
const systemIcon = this.createTacticalIcon(system.starColor, 96);
|
|
|
|
const summaryVisual = this.createSystemSummaryVisual(new THREE.Vector3(system.position.x, system.position.y + system.starSize + 140, system.position.z));
|
|
|
|
const shellReticle = this.createShellReticle("#ff3b30", 400);
|
|
|
|
summaryVisual.sprite.position.set(0, system.starSize + 110, 0);
|
|
|
|
const summaryVisual = this.createSystemSummaryVisual(new THREE.Vector3(system.galaxyPosition.x, system.galaxyPosition.y + renderedStarSize + 140, system.galaxyPosition.z));
|
|
|
|
root.add(starCluster, systemIcon, summaryVisual.sprite, detailGroup);
|
|
|
|
summaryVisual.sprite.position.set(0, renderedStarSize + 110, 0);
|
|
|
|
|
|
|
|
root.add(starCluster, systemIcon, shellReticle, summaryVisual.sprite, detailGroup);
|
|
|
|
this.registerPresentation(starCluster, systemIcon, true);
|
|
|
|
this.registerPresentation(starCluster, systemIcon, true);
|
|
|
|
this.systemVisuals.set(system.id, { root, detailGroup, summary: summaryVisual });
|
|
|
|
this.systemVisuals.set(system.id, {
|
|
|
|
|
|
|
|
root,
|
|
|
|
|
|
|
|
starCluster,
|
|
|
|
|
|
|
|
icon: systemIcon,
|
|
|
|
|
|
|
|
shellReticle,
|
|
|
|
|
|
|
|
shellReticleBaseScale: 400,
|
|
|
|
|
|
|
|
detailGroup,
|
|
|
|
|
|
|
|
summary: summaryVisual,
|
|
|
|
|
|
|
|
galaxyPosition: this.toThreeVector(system.galaxyPosition),
|
|
|
|
|
|
|
|
});
|
|
|
|
this.systemSummaryVisuals.set(system.id, summaryVisual);
|
|
|
|
this.systemSummaryVisuals.set(system.id, summaryVisual);
|
|
|
|
starCluster.traverse((child) => {
|
|
|
|
starCluster.traverse((child) => {
|
|
|
|
if (child instanceof THREE.Mesh) {
|
|
|
|
if (child instanceof THREE.Mesh) {
|
|
|
|
@@ -440,6 +484,7 @@ export class GameViewer {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
this.selectableTargets.set(systemIcon, { kind: "system", id: system.id });
|
|
|
|
this.selectableTargets.set(systemIcon, { kind: "system", id: system.id });
|
|
|
|
|
|
|
|
this.selectableTargets.set(shellReticle, { kind: "system", id: system.id });
|
|
|
|
|
|
|
|
|
|
|
|
for (const [planetIndex, planet] of system.planets.entries()) {
|
|
|
|
for (const [planetIndex, planet] of system.planets.entries()) {
|
|
|
|
const orbit = this.createPlanetOrbit(planet);
|
|
|
|
const orbit = this.createPlanetOrbit(planet);
|
|
|
|
@@ -493,11 +538,19 @@ export class GameViewer {
|
|
|
|
const mesh = this.createNodeMesh(node);
|
|
|
|
const mesh = this.createNodeMesh(node);
|
|
|
|
const icon = this.createTacticalIcon(node.sourceKind === "gas-cloud" ? "#7fd6ff" : "#d2b07a", 20);
|
|
|
|
const icon = this.createTacticalIcon(node.sourceKind === "gas-cloud" ? "#7fd6ff" : "#d2b07a", 20);
|
|
|
|
icon.position.copy(mesh.position);
|
|
|
|
icon.position.copy(mesh.position);
|
|
|
|
|
|
|
|
const localPosition = this.toThreeVector(node.localPosition);
|
|
|
|
|
|
|
|
const anchor = this.resolveOrbitalAnchor(node.systemId, localPosition);
|
|
|
|
|
|
|
|
const orbital = this.deriveNodeOrbital(node, anchor);
|
|
|
|
this.nodeVisuals.set(node.id, {
|
|
|
|
this.nodeVisuals.set(node.id, {
|
|
|
|
systemId: node.systemId,
|
|
|
|
systemId: node.systemId,
|
|
|
|
mesh,
|
|
|
|
mesh,
|
|
|
|
icon,
|
|
|
|
icon,
|
|
|
|
worldPosition: this.toThreeVector(node.position),
|
|
|
|
sourceKind: node.sourceKind,
|
|
|
|
|
|
|
|
anchor,
|
|
|
|
|
|
|
|
localPosition,
|
|
|
|
|
|
|
|
orbitRadius: orbital.radius,
|
|
|
|
|
|
|
|
orbitPhase: orbital.phase,
|
|
|
|
|
|
|
|
orbitInclination: orbital.inclination,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
this.nodeGroup.add(mesh, icon);
|
|
|
|
this.nodeGroup.add(mesh, icon);
|
|
|
|
this.registerPresentation(mesh, icon, true, true, node.systemId);
|
|
|
|
this.registerPresentation(mesh, icon, true, true, node.systemId);
|
|
|
|
@@ -514,11 +567,18 @@ export class GameViewer {
|
|
|
|
const mesh = this.createStationMesh(station);
|
|
|
|
const mesh = this.createStationMesh(station);
|
|
|
|
const icon = this.createTacticalIcon(station.color, 26);
|
|
|
|
const icon = this.createTacticalIcon(station.color, 26);
|
|
|
|
icon.position.copy(mesh.position);
|
|
|
|
icon.position.copy(mesh.position);
|
|
|
|
|
|
|
|
const localPosition = this.toThreeVector(station.localPosition);
|
|
|
|
|
|
|
|
const anchor = this.resolveOrbitalAnchor(station.systemId, localPosition);
|
|
|
|
|
|
|
|
const orbital = this.deriveOrbitalFromLocalPosition(localPosition, station.systemId, anchor);
|
|
|
|
this.stationVisuals.set(station.id, {
|
|
|
|
this.stationVisuals.set(station.id, {
|
|
|
|
systemId: station.systemId,
|
|
|
|
systemId: station.systemId,
|
|
|
|
mesh,
|
|
|
|
mesh,
|
|
|
|
icon,
|
|
|
|
icon,
|
|
|
|
worldPosition: this.toThreeVector(station.position),
|
|
|
|
anchor,
|
|
|
|
|
|
|
|
orbitRadius: orbital.radius,
|
|
|
|
|
|
|
|
orbitPhase: orbital.phase,
|
|
|
|
|
|
|
|
orbitInclination: orbital.inclination,
|
|
|
|
|
|
|
|
localPosition,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
this.stationGroup.add(mesh, icon);
|
|
|
|
this.stationGroup.add(mesh, icon);
|
|
|
|
this.registerPresentation(mesh, icon, true, true, station.systemId);
|
|
|
|
this.registerPresentation(mesh, icon, true, true, station.systemId);
|
|
|
|
@@ -534,7 +594,7 @@ export class GameViewer {
|
|
|
|
for (const ship of ships) {
|
|
|
|
for (const ship of ships) {
|
|
|
|
const mesh = this.createShipMesh(ship);
|
|
|
|
const mesh = this.createShipMesh(ship);
|
|
|
|
const icon = this.createTacticalIcon(this.shipColor(ship.role), 18);
|
|
|
|
const icon = this.createTacticalIcon(this.shipColor(ship.role), 18);
|
|
|
|
const position = this.toThreeVector(ship.position);
|
|
|
|
const position = this.toThreeVector(ship.localPosition);
|
|
|
|
icon.position.copy(position);
|
|
|
|
icon.position.copy(position);
|
|
|
|
this.shipGroup.add(mesh, icon);
|
|
|
|
this.shipGroup.add(mesh, icon);
|
|
|
|
this.selectableTargets.set(mesh, { kind: "ship", id: ship.id });
|
|
|
|
this.selectableTargets.set(mesh, { kind: "ship", id: ship.id });
|
|
|
|
@@ -546,8 +606,8 @@ export class GameViewer {
|
|
|
|
icon,
|
|
|
|
icon,
|
|
|
|
startPosition: position.clone(),
|
|
|
|
startPosition: position.clone(),
|
|
|
|
authoritativePosition: position.clone(),
|
|
|
|
authoritativePosition: position.clone(),
|
|
|
|
targetPosition: this.toThreeVector(ship.targetPosition),
|
|
|
|
targetPosition: this.toThreeVector(ship.targetLocalPosition),
|
|
|
|
velocity: this.toThreeVector(ship.velocity),
|
|
|
|
velocity: this.toThreeVector(ship.localVelocity),
|
|
|
|
receivedAtMs: performance.now(),
|
|
|
|
receivedAtMs: performance.now(),
|
|
|
|
blendDurationMs: Math.max(tickIntervalMs, 80),
|
|
|
|
blendDurationMs: Math.max(tickIntervalMs, 80),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
@@ -562,7 +622,13 @@ export class GameViewer {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
visual.systemId = node.systemId;
|
|
|
|
visual.systemId = node.systemId;
|
|
|
|
visual.worldPosition.copy(this.toThreeVector(node.position));
|
|
|
|
visual.sourceKind = node.sourceKind;
|
|
|
|
|
|
|
|
visual.localPosition.copy(this.toThreeVector(node.localPosition));
|
|
|
|
|
|
|
|
visual.anchor = this.resolveOrbitalAnchor(node.systemId, visual.localPosition);
|
|
|
|
|
|
|
|
const orbital = this.deriveNodeOrbital(node, visual.anchor);
|
|
|
|
|
|
|
|
visual.orbitRadius = orbital.radius;
|
|
|
|
|
|
|
|
visual.orbitPhase = orbital.phase;
|
|
|
|
|
|
|
|
visual.orbitInclination = orbital.inclination;
|
|
|
|
visual.mesh.scale.setScalar(0.8 + (node.oreRemaining / Math.max(node.maxOre, 1)) * 0.6);
|
|
|
|
visual.mesh.scale.setScalar(0.8 + (node.oreRemaining / Math.max(node.maxOre, 1)) * 0.6);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -575,7 +641,12 @@ export class GameViewer {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
visual.systemId = station.systemId;
|
|
|
|
visual.systemId = station.systemId;
|
|
|
|
visual.worldPosition.copy(this.toThreeVector(station.position));
|
|
|
|
visual.localPosition.copy(this.toThreeVector(station.localPosition));
|
|
|
|
|
|
|
|
visual.anchor = this.resolveOrbitalAnchor(station.systemId, visual.localPosition);
|
|
|
|
|
|
|
|
const orbital = this.deriveOrbitalFromLocalPosition(visual.localPosition, station.systemId, visual.anchor);
|
|
|
|
|
|
|
|
visual.orbitRadius = orbital.radius;
|
|
|
|
|
|
|
|
visual.orbitPhase = orbital.phase;
|
|
|
|
|
|
|
|
visual.orbitInclination = orbital.inclination;
|
|
|
|
const material = visual.mesh.material as THREE.MeshStandardMaterial;
|
|
|
|
const material = visual.mesh.material as THREE.MeshStandardMaterial;
|
|
|
|
material.color.set(station.color);
|
|
|
|
material.color.set(station.color);
|
|
|
|
material.emissive = new THREE.Color(station.color).multiplyScalar(0.1);
|
|
|
|
material.emissive = new THREE.Color(station.color).multiplyScalar(0.1);
|
|
|
|
@@ -591,9 +662,9 @@ export class GameViewer {
|
|
|
|
|
|
|
|
|
|
|
|
visual.systemId = ship.systemId;
|
|
|
|
visual.systemId = ship.systemId;
|
|
|
|
visual.startPosition.copy(visual.authoritativePosition);
|
|
|
|
visual.startPosition.copy(visual.authoritativePosition);
|
|
|
|
visual.authoritativePosition.copy(this.toThreeVector(ship.position));
|
|
|
|
visual.authoritativePosition.copy(this.toThreeVector(ship.localPosition));
|
|
|
|
visual.targetPosition.copy(this.toThreeVector(ship.targetPosition));
|
|
|
|
visual.targetPosition.copy(this.toThreeVector(ship.targetLocalPosition));
|
|
|
|
visual.velocity.copy(this.toThreeVector(ship.velocity));
|
|
|
|
visual.velocity.copy(this.toThreeVector(ship.localVelocity));
|
|
|
|
visual.receivedAtMs = performance.now();
|
|
|
|
visual.receivedAtMs = performance.now();
|
|
|
|
visual.blendDurationMs = Math.max(tickIntervalMs * 1.35, 100);
|
|
|
|
visual.blendDurationMs = Math.max(tickIntervalMs * 1.35, 100);
|
|
|
|
(visual.mesh.material as THREE.MeshStandardMaterial).color.set(this.shipColor(ship.role));
|
|
|
|
(visual.mesh.material as THREE.MeshStandardMaterial).color.set(this.shipColor(ship.role));
|
|
|
|
@@ -649,12 +720,14 @@ export class GameViewer {
|
|
|
|
if (!ship) {
|
|
|
|
if (!ship) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const parent = this.describeSelectionParent(selected);
|
|
|
|
this.detailTitleEl.textContent = ship.label;
|
|
|
|
this.detailTitleEl.textContent = ship.label;
|
|
|
|
this.detailBodyEl.innerHTML = `
|
|
|
|
this.detailBodyEl.innerHTML = `
|
|
|
|
<p>${ship.shipClass} · ${ship.role} · ${ship.systemId}</p>
|
|
|
|
<p>${ship.shipClass} · ${ship.role} · ${ship.systemId}</p>
|
|
|
|
|
|
|
|
<p>Parent ${parent}</p>
|
|
|
|
<p>State ${ship.state}<br>Behavior ${ship.defaultBehaviorKind}<br>Task ${ship.controllerTaskKind}</p>
|
|
|
|
<p>State ${ship.state}<br>Behavior ${ship.defaultBehaviorKind}<br>Task ${ship.controllerTaskKind}</p>
|
|
|
|
<p>Cargo ${ship.cargo.toFixed(0)} / ${ship.cargoCapacity.toFixed(0)} ${ship.cargoItemId ?? ""}</p>
|
|
|
|
<p>Cargo ${ship.cargo.toFixed(0)} / ${ship.cargoCapacity.toFixed(0)} ${ship.cargoItemId ?? ""}</p>
|
|
|
|
<p>Velocity ${this.formatVector(ship.velocity)}</p>
|
|
|
|
<p>Velocity ${this.formatVector(ship.localVelocity)}</p>
|
|
|
|
<p>${this.followedShipId === ship.id ? "Camera follow engaged" : "Camera follow idle"}</p>
|
|
|
|
<p>${this.followedShipId === ship.id ? "Camera follow engaged" : "Camera follow idle"}</p>
|
|
|
|
<p class="history">${ship.history.join("<br>")}</p>
|
|
|
|
<p class="history">${ship.history.join("<br>")}</p>
|
|
|
|
`;
|
|
|
|
`;
|
|
|
|
@@ -666,9 +739,11 @@ export class GameViewer {
|
|
|
|
if (!station) {
|
|
|
|
if (!station) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const parent = this.describeSelectionParent(selected);
|
|
|
|
this.detailTitleEl.textContent = station.label;
|
|
|
|
this.detailTitleEl.textContent = station.label;
|
|
|
|
this.detailBodyEl.innerHTML = `
|
|
|
|
this.detailBodyEl.innerHTML = `
|
|
|
|
<p>${station.category} · ${station.systemId}</p>
|
|
|
|
<p>${station.category} · ${station.systemId}</p>
|
|
|
|
|
|
|
|
<p>Parent ${parent}</p>
|
|
|
|
<p>Ore ${station.oreStored.toFixed(0)}<br>Refined ${station.refinedStock.toFixed(0)}<br>Docked ${station.dockedShips}</p>
|
|
|
|
<p>Ore ${station.oreStored.toFixed(0)}<br>Refined ${station.refinedStock.toFixed(0)}<br>Docked ${station.dockedShips}</p>
|
|
|
|
<p class="history">${this.renderRecentEvents("station", station.id)}</p>
|
|
|
|
<p class="history">${this.renderRecentEvents("station", station.id)}</p>
|
|
|
|
`;
|
|
|
|
`;
|
|
|
|
@@ -680,9 +755,11 @@ export class GameViewer {
|
|
|
|
if (!node) {
|
|
|
|
if (!node) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const parent = this.describeSelectionParent(selected);
|
|
|
|
this.detailTitleEl.textContent = `Node ${node.id}`;
|
|
|
|
this.detailTitleEl.textContent = `Node ${node.id}`;
|
|
|
|
this.detailBodyEl.innerHTML = `
|
|
|
|
this.detailBodyEl.innerHTML = `
|
|
|
|
<p>${node.systemId}</p>
|
|
|
|
<p>${node.systemId}</p>
|
|
|
|
|
|
|
|
<p>Parent ${parent}</p>
|
|
|
|
<p>Source ${node.sourceKind}<br>Resource ${node.itemId}</p>
|
|
|
|
<p>Source ${node.sourceKind}<br>Resource ${node.itemId}</p>
|
|
|
|
<p>Stock ${node.oreRemaining.toFixed(0)} / ${node.maxOre.toFixed(0)}</p>
|
|
|
|
<p>Stock ${node.oreRemaining.toFixed(0)} / ${node.maxOre.toFixed(0)}</p>
|
|
|
|
`;
|
|
|
|
`;
|
|
|
|
@@ -695,9 +772,11 @@ export class GameViewer {
|
|
|
|
if (!system || !planet) {
|
|
|
|
if (!system || !planet) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const parent = this.describeSelectionParent(selected);
|
|
|
|
this.detailTitleEl.textContent = planet.label;
|
|
|
|
this.detailTitleEl.textContent = planet.label;
|
|
|
|
this.detailBodyEl.innerHTML = `
|
|
|
|
this.detailBodyEl.innerHTML = `
|
|
|
|
<p>${system.label}</p>
|
|
|
|
<p>${system.label}</p>
|
|
|
|
|
|
|
|
<p>Parent ${parent}</p>
|
|
|
|
<p>${planet.planetType} · ${planet.shape} · Moons ${planet.moonCount}</p>
|
|
|
|
<p>${planet.planetType} · ${planet.shape} · Moons ${planet.moonCount}</p>
|
|
|
|
<p>Orbit ${planet.orbitRadius.toFixed(0)}<br>Speed ${planet.orbitSpeed.toFixed(3)}<br>Ecc ${planet.orbitEccentricity.toFixed(3)}<br>Inc ${planet.orbitInclination.toFixed(1)}°</p>
|
|
|
|
<p>Orbit ${planet.orbitRadius.toFixed(0)}<br>Speed ${planet.orbitSpeed.toFixed(3)}<br>Ecc ${planet.orbitEccentricity.toFixed(3)}<br>Inc ${planet.orbitInclination.toFixed(1)}°</p>
|
|
|
|
<p>Phase ${planet.orbitPhaseAtEpoch.toFixed(1)}°</p>
|
|
|
|
<p>Phase ${planet.orbitPhaseAtEpoch.toFixed(1)}°</p>
|
|
|
|
@@ -710,7 +789,10 @@ export class GameViewer {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.detailTitleEl.textContent = system.label;
|
|
|
|
this.detailTitleEl.textContent = system.label;
|
|
|
|
this.detailBodyEl.innerHTML = this.renderSystemDetails(system, false);
|
|
|
|
this.detailBodyEl.innerHTML = `
|
|
|
|
|
|
|
|
<p>Parent galaxy</p>
|
|
|
|
|
|
|
|
${this.renderSystemDetails(system, false)}
|
|
|
|
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private render() {
|
|
|
|
private render() {
|
|
|
|
@@ -742,13 +824,14 @@ export class GameViewer {
|
|
|
|
this.orbitPitch = THREE.MathUtils.clamp(this.orbitPitch, 0.18, 1.3);
|
|
|
|
this.orbitPitch = THREE.MathUtils.clamp(this.orbitPitch, 0.18, 1.3);
|
|
|
|
|
|
|
|
|
|
|
|
const horizontalDistance = this.currentDistance * Math.cos(this.orbitPitch);
|
|
|
|
const horizontalDistance = this.currentDistance * Math.cos(this.orbitPitch);
|
|
|
|
|
|
|
|
const focus = this.getCameraFocusWorldPosition();
|
|
|
|
this.cameraOffset.set(
|
|
|
|
this.cameraOffset.set(
|
|
|
|
Math.cos(this.orbitYaw) * horizontalDistance,
|
|
|
|
Math.cos(this.orbitYaw) * horizontalDistance,
|
|
|
|
this.currentDistance * Math.sin(this.orbitPitch),
|
|
|
|
this.currentDistance * Math.sin(this.orbitPitch),
|
|
|
|
Math.sin(this.orbitYaw) * horizontalDistance,
|
|
|
|
Math.sin(this.orbitYaw) * horizontalDistance,
|
|
|
|
);
|
|
|
|
);
|
|
|
|
this.camera.position.copy(this.focus).add(this.cameraOffset);
|
|
|
|
this.camera.position.copy(focus).add(this.cameraOffset);
|
|
|
|
this.camera.lookAt(this.focus);
|
|
|
|
this.camera.lookAt(focus);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private updatePanFromKeyboard(delta: number) {
|
|
|
|
private updatePanFromKeyboard(delta: number) {
|
|
|
|
@@ -778,7 +861,11 @@ export class GameViewer {
|
|
|
|
const right = new THREE.Vector3(-forward.z, 0, forward.x);
|
|
|
|
const right = new THREE.Vector3(-forward.z, 0, forward.x);
|
|
|
|
const pan = right.multiplyScalar(move.x).add(forward.multiplyScalar(move.z));
|
|
|
|
const pan = right.multiplyScalar(move.x).add(forward.multiplyScalar(move.z));
|
|
|
|
const speed = THREE.MathUtils.mapLinear(this.currentDistance, MIN_CAMERA_DISTANCE, MAX_CAMERA_DISTANCE, 320, 6800);
|
|
|
|
const speed = THREE.MathUtils.mapLinear(this.currentDistance, MIN_CAMERA_DISTANCE, MAX_CAMERA_DISTANCE, 320, 6800);
|
|
|
|
this.focus.addScaledVector(pan, speed * delta);
|
|
|
|
if (this.activeSystemId) {
|
|
|
|
|
|
|
|
this.systemFocusLocal.addScaledVector(pan, speed * delta);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
this.galaxyFocus.addScaledVector(pan, speed * delta);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private applyZoomPresentation() {
|
|
|
|
private applyZoomPresentation() {
|
|
|
|
@@ -787,10 +874,16 @@ export class GameViewer {
|
|
|
|
for (const entry of this.presentationEntries) {
|
|
|
|
for (const entry of this.presentationEntries) {
|
|
|
|
const systemId = entry.systemId;
|
|
|
|
const systemId = entry.systemId;
|
|
|
|
const isActiveDetail = !systemId || systemId === this.activeSystemId;
|
|
|
|
const isActiveDetail = !systemId || systemId === this.activeSystemId;
|
|
|
|
|
|
|
|
const isProjectedSystemIcon = !!this.activeSystemId
|
|
|
|
|
|
|
|
&& !!systemId
|
|
|
|
|
|
|
|
&& systemId !== this.activeSystemId
|
|
|
|
|
|
|
|
&& this.systemVisuals.get(systemId)?.icon === entry.icon;
|
|
|
|
const detailAlpha = entry.hideDetailInUniverse
|
|
|
|
const detailAlpha = entry.hideDetailInUniverse
|
|
|
|
? Math.max(blend.localWeight, blend.systemWeight) * (isActiveDetail ? 1 : 0)
|
|
|
|
? Math.max(blend.localWeight, blend.systemWeight) * (isActiveDetail ? 1 : 0)
|
|
|
|
: 1;
|
|
|
|
: 1;
|
|
|
|
const iconAlpha = entry.hideIconInUniverse
|
|
|
|
const iconAlpha = isProjectedSystemIcon
|
|
|
|
|
|
|
|
? 0
|
|
|
|
|
|
|
|
: entry.hideIconInUniverse
|
|
|
|
? blend.systemWeight * (isActiveDetail ? 1 : 0)
|
|
|
|
? blend.systemWeight * (isActiveDetail ? 1 : 0)
|
|
|
|
: Math.max(blend.systemWeight, blend.universeWeight);
|
|
|
|
: Math.max(blend.systemWeight, blend.universeWeight);
|
|
|
|
|
|
|
|
|
|
|
|
@@ -909,6 +1002,7 @@ export class GameViewer {
|
|
|
|
|
|
|
|
|
|
|
|
private updateShipPresentation() {
|
|
|
|
private updateShipPresentation() {
|
|
|
|
const now = performance.now();
|
|
|
|
const now = performance.now();
|
|
|
|
|
|
|
|
const worldTimeSeconds = this.currentWorldTimeSeconds();
|
|
|
|
for (const visual of this.shipVisuals.values()) {
|
|
|
|
for (const visual of this.shipVisuals.values()) {
|
|
|
|
const elapsedMs = now - visual.receivedAtMs;
|
|
|
|
const elapsedMs = now - visual.receivedAtMs;
|
|
|
|
const blendT = THREE.MathUtils.clamp(elapsedMs / visual.blendDurationMs, 0, 1);
|
|
|
|
const blendT = THREE.MathUtils.clamp(elapsedMs / visual.blendDurationMs, 0, 1);
|
|
|
|
@@ -919,28 +1013,31 @@ export class GameViewer {
|
|
|
|
worldPosition.copy(visual.authoritativePosition).addScaledVector(visual.velocity, extrapolationSeconds);
|
|
|
|
worldPosition.copy(visual.authoritativePosition).addScaledVector(visual.velocity, extrapolationSeconds);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
visual.mesh.position.copy(this.toDisplayPosition(worldPosition, visual.systemId));
|
|
|
|
visual.mesh.position.copy(this.toDisplayLocalPosition(worldPosition, visual.systemId));
|
|
|
|
visual.icon.position.copy(visual.mesh.position);
|
|
|
|
visual.icon.position.copy(visual.mesh.position);
|
|
|
|
const shipVisible = visual.systemId === this.activeSystemId;
|
|
|
|
const shipVisible = visual.systemId === this.activeSystemId;
|
|
|
|
visual.mesh.visible = shipVisible;
|
|
|
|
visual.mesh.visible = shipVisible;
|
|
|
|
visual.icon.visible = shipVisible && visual.icon.visible;
|
|
|
|
visual.icon.visible = shipVisible && visual.icon.visible;
|
|
|
|
const desiredHeading = visual.targetPosition.clone().sub(visual.mesh.position);
|
|
|
|
const desiredHeading = visual.targetPosition.clone().sub(worldPosition);
|
|
|
|
if (desiredHeading.lengthSq() > 0.01) {
|
|
|
|
if (desiredHeading.lengthSq() > 0.01) {
|
|
|
|
visual.mesh.lookAt(visual.mesh.position.clone().add(desiredHeading));
|
|
|
|
visual.mesh.lookAt(visual.mesh.position.clone().add(desiredHeading));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (const visual of this.nodeVisuals.values()) {
|
|
|
|
for (const visual of this.nodeVisuals.values()) {
|
|
|
|
visual.mesh.position.copy(this.toDisplayPosition(visual.worldPosition, visual.systemId));
|
|
|
|
const animatedLocalPosition = this.computeNodeLocalPosition(visual, worldTimeSeconds);
|
|
|
|
|
|
|
|
visual.mesh.position.copy(this.toDisplayLocalPosition(animatedLocalPosition, visual.systemId));
|
|
|
|
visual.icon.position.copy(visual.mesh.position);
|
|
|
|
visual.icon.position.copy(visual.mesh.position);
|
|
|
|
visual.mesh.visible = visual.systemId === this.activeSystemId;
|
|
|
|
visual.mesh.visible = visual.systemId === this.activeSystemId;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (const visual of this.stationVisuals.values()) {
|
|
|
|
for (const visual of this.stationVisuals.values()) {
|
|
|
|
visual.mesh.position.copy(this.toDisplayPosition(visual.worldPosition, visual.systemId));
|
|
|
|
const animatedLocalPosition = this.computeStructureLocalPosition(visual, this.currentWorldTimeSeconds(), 0.09);
|
|
|
|
|
|
|
|
visual.mesh.position.copy(this.toDisplayLocalPosition(animatedLocalPosition, visual.systemId));
|
|
|
|
visual.icon.position.copy(visual.mesh.position);
|
|
|
|
visual.icon.position.copy(visual.mesh.position);
|
|
|
|
visual.mesh.visible = visual.systemId === this.activeSystemId;
|
|
|
|
visual.mesh.visible = visual.systemId === this.activeSystemId;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.updateSystemStarPresentation();
|
|
|
|
this.updateSystemDetailVisibility();
|
|
|
|
this.updateSystemDetailVisibility();
|
|
|
|
this.updateSystemSummaryPresentation();
|
|
|
|
this.updateSystemSummaryPresentation();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -949,8 +1046,15 @@ export class GameViewer {
|
|
|
|
const nowSeconds = this.currentWorldTimeSeconds();
|
|
|
|
const nowSeconds = this.currentWorldTimeSeconds();
|
|
|
|
for (const visual of this.planetVisuals) {
|
|
|
|
for (const visual of this.planetVisuals) {
|
|
|
|
const scale = visual.systemId === this.activeSystemId ? ACTIVE_SYSTEM_DETAIL_SCALE : 1;
|
|
|
|
const scale = visual.systemId === this.activeSystemId ? ACTIVE_SYSTEM_DETAIL_SCALE : 1;
|
|
|
|
const position = this.computePlanetLocalPosition(visual.planet, nowSeconds).multiplyScalar(scale);
|
|
|
|
const localPosition = this.computePlanetLocalPosition(visual.planet, nowSeconds);
|
|
|
|
|
|
|
|
const orbitOffset = visual.systemId === this.activeSystemId
|
|
|
|
|
|
|
|
? this.systemFocusLocal.clone().multiplyScalar(-scale)
|
|
|
|
|
|
|
|
: new THREE.Vector3();
|
|
|
|
|
|
|
|
const position = visual.systemId === this.activeSystemId
|
|
|
|
|
|
|
|
? localPosition.clone().sub(this.systemFocusLocal).multiplyScalar(scale)
|
|
|
|
|
|
|
|
: localPosition.multiplyScalar(scale);
|
|
|
|
visual.orbit.scale.setScalar(scale);
|
|
|
|
visual.orbit.scale.setScalar(scale);
|
|
|
|
|
|
|
|
visual.orbit.position.copy(orbitOffset);
|
|
|
|
visual.mesh.position.copy(position);
|
|
|
|
visual.mesh.position.copy(position);
|
|
|
|
visual.icon.position.copy(position);
|
|
|
|
visual.icon.position.copy(position);
|
|
|
|
if (visual.ring) {
|
|
|
|
if (visual.ring) {
|
|
|
|
@@ -976,25 +1080,26 @@ export class GameViewer {
|
|
|
|
emissive: new THREE.Color(isGas ? 0x7fd6ff : 0xd2b07a).multiplyScalar(isGas ? 0.22 : 0.05),
|
|
|
|
emissive: new THREE.Color(isGas ? 0x7fd6ff : 0xd2b07a).multiplyScalar(isGas ? 0.22 : 0.05),
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
mesh.position.copy(this.toThreeVector(node.position));
|
|
|
|
mesh.position.copy(this.toThreeVector(node.localPosition));
|
|
|
|
mesh.scale.setScalar(0.8 + (node.oreRemaining / Math.max(node.maxOre, 1)) * 0.6);
|
|
|
|
mesh.scale.setScalar(0.8 + (node.oreRemaining / Math.max(node.maxOre, 1)) * 0.6);
|
|
|
|
return mesh;
|
|
|
|
return mesh;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private createStarCluster(system: SystemSnapshot) {
|
|
|
|
private createStarCluster(system: SystemSnapshot) {
|
|
|
|
const root = new THREE.Group();
|
|
|
|
const root = new THREE.Group();
|
|
|
|
|
|
|
|
const renderedStarSize = Math.max(system.starSize * STAR_RENDER_SCALE, 8);
|
|
|
|
const offsets = system.starCount > 1
|
|
|
|
const offsets = system.starCount > 1
|
|
|
|
? [new THREE.Vector3(-system.starSize * 0.55, 0, 0), new THREE.Vector3(system.starSize * 0.75, system.starSize * 0.08, 0)]
|
|
|
|
? [new THREE.Vector3(-renderedStarSize * 0.55, 0, 0), new THREE.Vector3(renderedStarSize * 0.75, renderedStarSize * 0.08, 0)]
|
|
|
|
: [new THREE.Vector3(0, 0, 0)];
|
|
|
|
: [new THREE.Vector3(0, 0, 0)];
|
|
|
|
|
|
|
|
|
|
|
|
for (const [index, offset] of offsets.entries()) {
|
|
|
|
for (const [index, offset] of offsets.entries()) {
|
|
|
|
const sizeScale = index === 0 ? 1 : 0.72;
|
|
|
|
const sizeScale = index === 0 ? 1 : 0.72;
|
|
|
|
const star = new THREE.Mesh(
|
|
|
|
const star = new THREE.Mesh(
|
|
|
|
new THREE.SphereGeometry(system.starSize * sizeScale, 28, 28),
|
|
|
|
new THREE.SphereGeometry(renderedStarSize * sizeScale, 24, 24),
|
|
|
|
new THREE.MeshBasicMaterial({ color: system.starColor }),
|
|
|
|
new THREE.MeshBasicMaterial({ color: system.starColor }),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
const halo = new THREE.Mesh(
|
|
|
|
const halo = new THREE.Mesh(
|
|
|
|
new THREE.SphereGeometry(system.starSize * sizeScale * 1.72, 24, 24),
|
|
|
|
new THREE.SphereGeometry(renderedStarSize * sizeScale * 1.45, 20, 20),
|
|
|
|
new THREE.MeshBasicMaterial({
|
|
|
|
new THREE.MeshBasicMaterial({
|
|
|
|
color: system.starColor,
|
|
|
|
color: system.starColor,
|
|
|
|
transparent: true,
|
|
|
|
transparent: true,
|
|
|
|
@@ -1080,7 +1185,7 @@ export class GameViewer {
|
|
|
|
new THREE.MeshStandardMaterial({ color: station.color, emissive: new THREE.Color(station.color).multiplyScalar(0.1) }),
|
|
|
|
new THREE.MeshStandardMaterial({ color: station.color, emissive: new THREE.Color(station.color).multiplyScalar(0.1) }),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
mesh.rotation.x = Math.PI / 2;
|
|
|
|
mesh.rotation.x = Math.PI / 2;
|
|
|
|
mesh.position.copy(this.toThreeVector(station.position));
|
|
|
|
mesh.position.copy(this.toThreeVector(station.localPosition));
|
|
|
|
return mesh;
|
|
|
|
return mesh;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1091,7 +1196,7 @@ export class GameViewer {
|
|
|
|
geometry,
|
|
|
|
geometry,
|
|
|
|
new THREE.MeshStandardMaterial({ color: this.shipColor(ship.role) }),
|
|
|
|
new THREE.MeshStandardMaterial({ color: this.shipColor(ship.role) }),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
mesh.position.copy(this.toThreeVector(ship.position));
|
|
|
|
mesh.position.copy(this.toThreeVector(ship.localPosition));
|
|
|
|
return mesh;
|
|
|
|
return mesh;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1262,6 +1367,50 @@ export class GameViewer {
|
|
|
|
return { sprite, texture, anchor };
|
|
|
|
return { sprite, texture, anchor };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private createShellReticle(color: string, size: number) {
|
|
|
|
|
|
|
|
const canvas = document.createElement("canvas");
|
|
|
|
|
|
|
|
canvas.width = 128;
|
|
|
|
|
|
|
|
canvas.height = 128;
|
|
|
|
|
|
|
|
const context = canvas.getContext("2d");
|
|
|
|
|
|
|
|
if (!context) {
|
|
|
|
|
|
|
|
throw new Error("Unable to create shell reticle");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
context.clearRect(0, 0, 128, 128);
|
|
|
|
|
|
|
|
context.strokeStyle = color;
|
|
|
|
|
|
|
|
context.lineWidth = 6;
|
|
|
|
|
|
|
|
context.globalAlpha = 0.58;
|
|
|
|
|
|
|
|
context.beginPath();
|
|
|
|
|
|
|
|
context.arc(64, 64, 48, 0.12 * Math.PI, 0.34 * Math.PI);
|
|
|
|
|
|
|
|
context.stroke();
|
|
|
|
|
|
|
|
context.beginPath();
|
|
|
|
|
|
|
|
context.arc(64, 64, 48, 0.62 * Math.PI, 0.84 * Math.PI);
|
|
|
|
|
|
|
|
context.stroke();
|
|
|
|
|
|
|
|
context.beginPath();
|
|
|
|
|
|
|
|
context.arc(64, 64, 48, 1.12 * Math.PI, 1.34 * Math.PI);
|
|
|
|
|
|
|
|
context.stroke();
|
|
|
|
|
|
|
|
context.beginPath();
|
|
|
|
|
|
|
|
context.arc(64, 64, 48, 1.62 * Math.PI, 1.84 * Math.PI);
|
|
|
|
|
|
|
|
context.stroke();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const texture = new THREE.CanvasTexture(canvas);
|
|
|
|
|
|
|
|
const material = new THREE.SpriteMaterial({
|
|
|
|
|
|
|
|
map: texture,
|
|
|
|
|
|
|
|
transparent: true,
|
|
|
|
|
|
|
|
depthWrite: false,
|
|
|
|
|
|
|
|
depthTest: false,
|
|
|
|
|
|
|
|
color,
|
|
|
|
|
|
|
|
opacity: 1,
|
|
|
|
|
|
|
|
blending: THREE.AdditiveBlending,
|
|
|
|
|
|
|
|
fog: false,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const sprite = new THREE.Sprite(material);
|
|
|
|
|
|
|
|
sprite.scale.setScalar(size);
|
|
|
|
|
|
|
|
sprite.visible = false;
|
|
|
|
|
|
|
|
sprite.renderOrder = 1000;
|
|
|
|
|
|
|
|
return sprite;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private updateSystemSummaries() {
|
|
|
|
private updateSystemSummaries() {
|
|
|
|
if (!this.world) {
|
|
|
|
if (!this.world) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
@@ -1364,7 +1513,8 @@ export class GameViewer {
|
|
|
|
private updateSystemSummaryPresentation() {
|
|
|
|
private updateSystemSummaryPresentation() {
|
|
|
|
const distanceScale = this.activeSystemId ? 0.05 : 0.085;
|
|
|
|
const distanceScale = this.activeSystemId ? 0.05 : 0.085;
|
|
|
|
for (const [systemId, visual] of this.systemSummaryVisuals.entries()) {
|
|
|
|
for (const [systemId, visual] of this.systemSummaryVisuals.entries()) {
|
|
|
|
const distance = this.camera.position.distanceTo(visual.anchor);
|
|
|
|
const worldPosition = visual.sprite.getWorldPosition(new THREE.Vector3());
|
|
|
|
|
|
|
|
const distance = this.camera.position.distanceTo(worldPosition);
|
|
|
|
const minimumScale = this.activeSystemId && systemId !== this.activeSystemId ? 1200 : 1400;
|
|
|
|
const minimumScale = this.activeSystemId && systemId !== this.activeSystemId ? 1200 : 1400;
|
|
|
|
const scale = Math.max(minimumScale, distance * distanceScale);
|
|
|
|
const scale = Math.max(minimumScale, distance * distanceScale);
|
|
|
|
visual.sprite.scale.set(scale, scale * 0.3125, 1);
|
|
|
|
visual.sprite.scale.set(scale, scale * 0.3125, 1);
|
|
|
|
@@ -1566,6 +1716,104 @@ export class GameViewer {
|
|
|
|
return Math.min(base + variance, planet.size * 0.42);
|
|
|
|
return Math.min(base + variance, planet.size * 0.42);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private deriveNodeOrbital(node: ResourceNodeSnapshot | ResourceNodeDelta, anchor: OrbitalAnchor) {
|
|
|
|
|
|
|
|
return this.deriveOrbitalFromLocalPosition(this.toThreeVector(node.localPosition), node.systemId, anchor);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private deriveOrbitalFromLocalPosition(localPosition: THREE.Vector3, systemId: string, anchor: OrbitalAnchor) {
|
|
|
|
|
|
|
|
const anchorPosition = this.resolveOrbitalAnchorPosition(systemId, anchor, this.currentWorldTimeSeconds());
|
|
|
|
|
|
|
|
const relativePosition = localPosition.clone().sub(anchorPosition);
|
|
|
|
|
|
|
|
const radius = Math.max(Math.sqrt((relativePosition.x * relativePosition.x) + (relativePosition.z * relativePosition.z)), 24);
|
|
|
|
|
|
|
|
const phase = Math.atan2(relativePosition.z, relativePosition.x);
|
|
|
|
|
|
|
|
const inclination = Math.atan2(relativePosition.y, radius);
|
|
|
|
|
|
|
|
return { radius, phase, inclination };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private computeNodeLocalPosition(node: NodeVisual, timeSeconds: number) {
|
|
|
|
|
|
|
|
const speed = this.computeNodeOrbitSpeed(node);
|
|
|
|
|
|
|
|
const angle = node.orbitPhase + (timeSeconds * speed);
|
|
|
|
|
|
|
|
const orbit = new THREE.Vector3(
|
|
|
|
|
|
|
|
Math.cos(angle) * node.orbitRadius,
|
|
|
|
|
|
|
|
0,
|
|
|
|
|
|
|
|
Math.sin(angle) * node.orbitRadius,
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
orbit.applyAxisAngle(new THREE.Vector3(1, 0, 0), node.orbitInclination);
|
|
|
|
|
|
|
|
return orbit.add(this.resolveOrbitalAnchorPosition(node.systemId, node.anchor, timeSeconds));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private computeNodeOrbitSpeed(node: NodeVisual) {
|
|
|
|
|
|
|
|
const base = node.sourceKind === "gas-cloud" ? 0.16 : 0.24;
|
|
|
|
|
|
|
|
return base / Math.sqrt(Math.max(node.orbitRadius / 140, 0.4));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private computeStructureLocalPosition(structure: StructureVisual, timeSeconds: number, baseSpeed: number) {
|
|
|
|
|
|
|
|
const angle = structure.orbitPhase + (timeSeconds * (baseSpeed / Math.sqrt(Math.max(structure.orbitRadius / 180, 0.45))));
|
|
|
|
|
|
|
|
const orbit = new THREE.Vector3(
|
|
|
|
|
|
|
|
Math.cos(angle) * structure.orbitRadius,
|
|
|
|
|
|
|
|
0,
|
|
|
|
|
|
|
|
Math.sin(angle) * structure.orbitRadius,
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
orbit.applyAxisAngle(new THREE.Vector3(1, 0, 0), structure.orbitInclination);
|
|
|
|
|
|
|
|
return orbit.add(this.resolveOrbitalAnchorPosition(structure.systemId, structure.anchor, timeSeconds));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private resolveOrbitalAnchor(systemId: string, localPosition: THREE.Vector3): OrbitalAnchor {
|
|
|
|
|
|
|
|
if (!this.world) {
|
|
|
|
|
|
|
|
return { kind: "star" };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const system = this.world.systems.get(systemId);
|
|
|
|
|
|
|
|
if (!system) {
|
|
|
|
|
|
|
|
return { kind: "star" };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const nowSeconds = this.currentWorldTimeSeconds();
|
|
|
|
|
|
|
|
let bestAnchor: OrbitalAnchor = { kind: "star" };
|
|
|
|
|
|
|
|
let bestDistance = Number.POSITIVE_INFINITY;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const [planetIndex, planet] of system.planets.entries()) {
|
|
|
|
|
|
|
|
const planetPosition = this.computePlanetLocalPosition(planet, nowSeconds);
|
|
|
|
|
|
|
|
const planetDistance = localPosition.distanceTo(planetPosition);
|
|
|
|
|
|
|
|
const planetThreshold = Math.max(planet.size * 10, 180);
|
|
|
|
|
|
|
|
if (planetDistance < planetThreshold && planetDistance < bestDistance) {
|
|
|
|
|
|
|
|
bestDistance = planetDistance;
|
|
|
|
|
|
|
|
bestAnchor = { kind: "planet", planetIndex };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const moonCount = Math.min(planet.moonCount, 12);
|
|
|
|
|
|
|
|
for (let moonIndex = 0; moonIndex < moonCount; moonIndex += 1) {
|
|
|
|
|
|
|
|
const moonPosition = planetPosition.clone().add(this.computeMoonLocalPosition(planet, moonIndex, nowSeconds));
|
|
|
|
|
|
|
|
const moonDistance = localPosition.distanceTo(moonPosition);
|
|
|
|
|
|
|
|
const moonThreshold = Math.max(this.computeMoonSize(planet, moonIndex) * 14, 80);
|
|
|
|
|
|
|
|
if (moonDistance < moonThreshold && moonDistance < bestDistance) {
|
|
|
|
|
|
|
|
bestDistance = moonDistance;
|
|
|
|
|
|
|
|
bestAnchor = { kind: "moon", planetIndex, moonIndex };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return bestAnchor;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private resolveOrbitalAnchorPosition(systemId: string, anchor: OrbitalAnchor, timeSeconds: number) {
|
|
|
|
|
|
|
|
if (!this.world || anchor.kind === "star") {
|
|
|
|
|
|
|
|
return new THREE.Vector3();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const system = this.world.systems.get(systemId);
|
|
|
|
|
|
|
|
const planet = system?.planets[anchor.planetIndex];
|
|
|
|
|
|
|
|
if (!system || !planet) {
|
|
|
|
|
|
|
|
return new THREE.Vector3();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const planetPosition = this.computePlanetLocalPosition(planet, timeSeconds);
|
|
|
|
|
|
|
|
if (anchor.kind === "planet") {
|
|
|
|
|
|
|
|
return planetPosition;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return planetPosition.add(this.computeMoonLocalPosition(planet, anchor.moonIndex, timeSeconds));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private hashUnit(value: string) {
|
|
|
|
private hashUnit(value: string) {
|
|
|
|
let hash = this.world?.seed ?? 1;
|
|
|
|
let hash = this.world?.seed ?? 1;
|
|
|
|
for (let index = 0; index < value.length; index += 1) {
|
|
|
|
for (let index = 0; index < value.length; index += 1) {
|
|
|
|
@@ -1616,6 +1864,8 @@ export class GameViewer {
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
private onPointerMove = (event: PointerEvent) => {
|
|
|
|
private onPointerMove = (event: PointerEvent) => {
|
|
|
|
|
|
|
|
this.updateHoverLabel(event);
|
|
|
|
|
|
|
|
|
|
|
|
if (this.dragPointerId !== event.pointerId || !this.dragMode) {
|
|
|
|
if (this.dragPointerId !== event.pointerId || !this.dragMode) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -1669,23 +1919,57 @@ export class GameViewer {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const bounds = this.renderer.domElement.getBoundingClientRect();
|
|
|
|
const picked = this.pickSelectableAtClientPosition(event.clientX, event.clientY);
|
|
|
|
this.mouse.x = ((event.clientX - bounds.left) / bounds.width) * 2 - 1;
|
|
|
|
this.selectedItems = picked ? [picked] : [];
|
|
|
|
this.mouse.y = -(((event.clientY - bounds.top) / bounds.height) * 2 - 1);
|
|
|
|
|
|
|
|
this.raycaster.setFromCamera(this.mouse, this.camera);
|
|
|
|
|
|
|
|
const hit = this.raycaster.intersectObjects([...this.selectableTargets.keys()], false)[0];
|
|
|
|
|
|
|
|
this.selectedItems = hit ? [this.selectableTargets.get(hit.object)!] : [];
|
|
|
|
|
|
|
|
this.syncFollowStateFromSelection();
|
|
|
|
this.syncFollowStateFromSelection();
|
|
|
|
this.updatePanels();
|
|
|
|
this.updatePanels();
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private updateHoverLabel(event: PointerEvent) {
|
|
|
|
|
|
|
|
if (this.dragMode) {
|
|
|
|
|
|
|
|
this.hoverLabelEl.hidden = true;
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const selection = this.pickSelectableAtClientPosition(event.clientX, event.clientY);
|
|
|
|
|
|
|
|
if (!selection || selection.kind !== "system" || selection.id === this.activeSystemId) {
|
|
|
|
|
|
|
|
this.hoverLabelEl.hidden = true;
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const system = this.world?.systems.get(selection.id);
|
|
|
|
|
|
|
|
if (!system) {
|
|
|
|
|
|
|
|
this.hoverLabelEl.hidden = true;
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.hoverLabelEl.hidden = false;
|
|
|
|
|
|
|
|
this.hoverLabelEl.textContent = system.label;
|
|
|
|
|
|
|
|
const point = this.screenPointFromClient(event.clientX, event.clientY);
|
|
|
|
|
|
|
|
this.hoverLabelEl.style.left = `${point.x + 14}px`;
|
|
|
|
|
|
|
|
this.hoverLabelEl.style.top = `${point.y + 14}px`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private pickSelectableAtClientPosition(clientX: number, clientY: number) {
|
|
|
|
|
|
|
|
const bounds = this.renderer.domElement.getBoundingClientRect();
|
|
|
|
|
|
|
|
this.mouse.x = ((clientX - bounds.left) / bounds.width) * 2 - 1;
|
|
|
|
|
|
|
|
this.mouse.y = -(((clientY - bounds.top) / bounds.height) * 2 - 1);
|
|
|
|
|
|
|
|
this.raycaster.setFromCamera(this.mouse, this.camera);
|
|
|
|
|
|
|
|
const hit = this.raycaster.intersectObjects([...this.selectableTargets.keys()], false)[0];
|
|
|
|
|
|
|
|
return hit ? this.selectableTargets.get(hit.object) : undefined;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private onDoubleClick = () => {
|
|
|
|
private onDoubleClick = () => {
|
|
|
|
if (this.selectedItems.length !== 1) {
|
|
|
|
if (this.selectedItems.length !== 1) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const nextFocus = this.resolveSelectionPosition(this.selectedItems[0]);
|
|
|
|
const nextFocus = this.resolveSelectionPosition(this.selectedItems[0]);
|
|
|
|
if (nextFocus) {
|
|
|
|
if (nextFocus) {
|
|
|
|
this.focus.copy(nextFocus);
|
|
|
|
if (this.activeSystemId && this.isSelectionInActiveSystem(this.selectedItems[0])) {
|
|
|
|
|
|
|
|
this.systemFocusLocal.copy(nextFocus);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
this.galaxyFocus.copy(nextFocus);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.syncFollowStateFromSelection();
|
|
|
|
this.syncFollowStateFromSelection();
|
|
|
|
};
|
|
|
|
};
|
|
|
|
@@ -1728,15 +2012,21 @@ export class GameViewer {
|
|
|
|
|
|
|
|
|
|
|
|
if (selection.kind === "ship") {
|
|
|
|
if (selection.kind === "ship") {
|
|
|
|
const ship = this.world.ships.get(selection.id);
|
|
|
|
const ship = this.world.ships.get(selection.id);
|
|
|
|
return ship ? this.toDisplayPosition(this.toThreeVector(ship.position), ship.systemId) : undefined;
|
|
|
|
return ship ? this.toThreeVector(ship.localPosition) : undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (selection.kind === "station") {
|
|
|
|
if (selection.kind === "station") {
|
|
|
|
const station = this.world.stations.get(selection.id);
|
|
|
|
const station = this.world.stations.get(selection.id);
|
|
|
|
return station ? this.toDisplayPosition(this.toThreeVector(station.position), station.systemId) : undefined;
|
|
|
|
const visual = station ? this.stationVisuals.get(station.id) : undefined;
|
|
|
|
|
|
|
|
return visual
|
|
|
|
|
|
|
|
? this.computeStructureLocalPosition(visual, this.currentWorldTimeSeconds(), 0.09)
|
|
|
|
|
|
|
|
: (station ? this.toThreeVector(station.localPosition) : undefined);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (selection.kind === "node") {
|
|
|
|
if (selection.kind === "node") {
|
|
|
|
const node = this.world.nodes.get(selection.id);
|
|
|
|
const node = this.world.nodes.get(selection.id);
|
|
|
|
return node ? this.toDisplayPosition(this.toThreeVector(node.position), node.systemId) : undefined;
|
|
|
|
const visual = node ? this.nodeVisuals.get(node.id) : undefined;
|
|
|
|
|
|
|
|
return visual
|
|
|
|
|
|
|
|
? this.computeNodeLocalPosition(visual, this.currentWorldTimeSeconds())
|
|
|
|
|
|
|
|
: (node ? this.toThreeVector(node.localPosition) : undefined);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (selection.kind === "planet") {
|
|
|
|
if (selection.kind === "planet") {
|
|
|
|
const system = this.world.systems.get(selection.systemId);
|
|
|
|
const system = this.world.systems.get(selection.systemId);
|
|
|
|
@@ -1746,10 +2036,10 @@ export class GameViewer {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const visual = this.planetVisuals.find((candidate) =>
|
|
|
|
const visual = this.planetVisuals.find((candidate) =>
|
|
|
|
candidate.systemId === selection.systemId && candidate.planet === planet);
|
|
|
|
candidate.systemId === selection.systemId && candidate.planet === planet);
|
|
|
|
return visual?.mesh.getWorldPosition(new THREE.Vector3());
|
|
|
|
return visual?.mesh.position.clone() ?? this.computePlanetLocalPosition(planet, this.currentWorldTimeSeconds());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const system = this.world.systems.get(selection.id);
|
|
|
|
const system = this.world.systems.get(selection.id);
|
|
|
|
return system ? this.toThreeVector(system.position) : undefined;
|
|
|
|
return system ? this.toThreeVector(system.galaxyPosition) : undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private updateMarqueeBox() {
|
|
|
|
private updateMarqueeBox() {
|
|
|
|
@@ -1850,6 +2140,9 @@ export class GameViewer {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (nextActiveSystemId) {
|
|
|
|
|
|
|
|
this.seedSystemFocusLocal(nextActiveSystemId);
|
|
|
|
|
|
|
|
}
|
|
|
|
this.activeSystemId = nextActiveSystemId;
|
|
|
|
this.activeSystemId = nextActiveSystemId;
|
|
|
|
this.updateSystemDetailVisibility();
|
|
|
|
this.updateSystemDetailVisibility();
|
|
|
|
this.updatePanels();
|
|
|
|
this.updatePanels();
|
|
|
|
@@ -1882,8 +2175,8 @@ export class GameViewer {
|
|
|
|
let nearestSystemId: string | undefined;
|
|
|
|
let nearestSystemId: string | undefined;
|
|
|
|
let nearestDistance = Number.POSITIVE_INFINITY;
|
|
|
|
let nearestDistance = Number.POSITIVE_INFINITY;
|
|
|
|
for (const system of this.world.systems.values()) {
|
|
|
|
for (const system of this.world.systems.values()) {
|
|
|
|
const center = this.toThreeVector(system.position);
|
|
|
|
const center = this.toThreeVector(system.galaxyPosition);
|
|
|
|
const distance = center.distanceTo(this.focus);
|
|
|
|
const distance = center.distanceTo(this.galaxyFocus);
|
|
|
|
if (distance < nearestDistance) {
|
|
|
|
if (distance < nearestDistance) {
|
|
|
|
nearestDistance = distance;
|
|
|
|
nearestDistance = distance;
|
|
|
|
nearestSystemId = system.id;
|
|
|
|
nearestSystemId = system.id;
|
|
|
|
@@ -1906,8 +2199,8 @@ export class GameViewer {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const target = this.toDisplayPosition(this.toThreeVector(ship.position), ship.systemId);
|
|
|
|
const target = this.toThreeVector(ship.localPosition);
|
|
|
|
this.focus.lerp(target, 1 - Math.exp(-delta * 8));
|
|
|
|
this.systemFocusLocal.lerp(target, 1 - Math.exp(-delta * 8));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private syncFollowStateFromSelection() {
|
|
|
|
private syncFollowStateFromSelection() {
|
|
|
|
@@ -1926,6 +2219,58 @@ export class GameViewer {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private updateSystemStarPresentation() {
|
|
|
|
|
|
|
|
const activeSystem = this.activeSystemId ? this.systemVisuals.get(this.activeSystemId) : undefined;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const [systemId, visual] of this.systemVisuals.entries()) {
|
|
|
|
|
|
|
|
visual.root.position.copy(visual.galaxyPosition);
|
|
|
|
|
|
|
|
visual.shellReticle.scale.setScalar(visual.shellReticleBaseScale);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!activeSystem) {
|
|
|
|
|
|
|
|
visual.starCluster.position.set(0, 0, 0);
|
|
|
|
|
|
|
|
visual.icon.position.set(0, 0, 0);
|
|
|
|
|
|
|
|
visual.icon.visible = true;
|
|
|
|
|
|
|
|
visual.shellReticle.position.set(0, 0, 0);
|
|
|
|
|
|
|
|
visual.shellReticle.visible = false;
|
|
|
|
|
|
|
|
this.setShellReticleOpacity(visual.shellReticle, 0);
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (systemId !== this.activeSystemId) {
|
|
|
|
|
|
|
|
visual.starCluster.position.set(0, 0, 0);
|
|
|
|
|
|
|
|
visual.icon.position.set(0, 0, 0);
|
|
|
|
|
|
|
|
visual.icon.visible = false;
|
|
|
|
|
|
|
|
visual.shellReticle.position.set(0, 0, 0);
|
|
|
|
|
|
|
|
visual.shellReticle.visible = true;
|
|
|
|
|
|
|
|
this.setShellReticleOpacity(visual.shellReticle, 1);
|
|
|
|
|
|
|
|
const direction = visual.galaxyPosition.clone().sub(activeSystem.galaxyPosition);
|
|
|
|
|
|
|
|
if (direction.lengthSq() > 0.0001) {
|
|
|
|
|
|
|
|
visual.root.position.copy(
|
|
|
|
|
|
|
|
activeSystem.galaxyPosition.clone().add(direction.normalize().multiplyScalar(PROJECTED_GALAXY_RADIUS)),
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const reticleWorldPosition = visual.root.getWorldPosition(new THREE.Vector3());
|
|
|
|
|
|
|
|
const reticleDistance = this.camera.position.distanceTo(reticleWorldPosition);
|
|
|
|
|
|
|
|
const reticleScale = Math.max(900, reticleDistance * 0.032);
|
|
|
|
|
|
|
|
visual.shellReticle.scale.setScalar(reticleScale);
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const offset = this.systemFocusLocal.clone().multiplyScalar(-ACTIVE_SYSTEM_DETAIL_SCALE);
|
|
|
|
|
|
|
|
visual.starCluster.position.copy(offset);
|
|
|
|
|
|
|
|
visual.icon.position.copy(offset);
|
|
|
|
|
|
|
|
visual.icon.visible = true;
|
|
|
|
|
|
|
|
visual.shellReticle.visible = false;
|
|
|
|
|
|
|
|
this.setShellReticleOpacity(visual.shellReticle, 0);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private setShellReticleOpacity(sprite: THREE.Sprite, opacity: number) {
|
|
|
|
|
|
|
|
sprite.visible = opacity > 0.02;
|
|
|
|
|
|
|
|
sprite.material.opacity = opacity;
|
|
|
|
|
|
|
|
sprite.material.needsUpdate = true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private resolveSelectableSystemId(selection: Selectable) {
|
|
|
|
private resolveSelectableSystemId(selection: Selectable) {
|
|
|
|
if (!this.world) {
|
|
|
|
if (!this.world) {
|
|
|
|
return undefined;
|
|
|
|
return undefined;
|
|
|
|
@@ -1946,18 +2291,122 @@ export class GameViewer {
|
|
|
|
return selection.id;
|
|
|
|
return selection.id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private toDisplayPosition(worldPosition: THREE.Vector3, systemId?: string) {
|
|
|
|
private describeSelectionParent(selection: Selectable) {
|
|
|
|
if (!this.world || !systemId || systemId !== this.activeSystemId) {
|
|
|
|
if (!this.world) {
|
|
|
|
return worldPosition.clone();
|
|
|
|
return "unknown";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (selection.kind === "system") {
|
|
|
|
|
|
|
|
return "galaxy";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (selection.kind === "planet") {
|
|
|
|
|
|
|
|
const system = this.world.systems.get(selection.systemId);
|
|
|
|
|
|
|
|
return system ? `${system.label} star` : selection.systemId;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (selection.kind === "ship") {
|
|
|
|
|
|
|
|
const ship = this.world.ships.get(selection.id);
|
|
|
|
|
|
|
|
if (!ship) {
|
|
|
|
|
|
|
|
return "unknown";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const system = this.world.systems.get(ship.systemId);
|
|
|
|
|
|
|
|
return system ? `${system.label} system` : ship.systemId;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (selection.kind === "station") {
|
|
|
|
|
|
|
|
const station = this.world.stations.get(selection.id);
|
|
|
|
|
|
|
|
const visual = station ? this.stationVisuals.get(selection.id) : undefined;
|
|
|
|
|
|
|
|
return this.describeOrbitalParent(station?.systemId, visual?.anchor);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const node = this.world.nodes.get(selection.id);
|
|
|
|
|
|
|
|
const visual = node ? this.nodeVisuals.get(selection.id) : undefined;
|
|
|
|
|
|
|
|
return this.describeOrbitalParent(node?.systemId, visual?.anchor);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private describeOrbitalParent(systemId?: string, anchor?: OrbitalAnchor) {
|
|
|
|
|
|
|
|
if (!this.world || !systemId) {
|
|
|
|
|
|
|
|
return "unknown";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const system = this.world.systems.get(systemId);
|
|
|
|
const system = this.world.systems.get(systemId);
|
|
|
|
if (!system) {
|
|
|
|
if (!system) {
|
|
|
|
return worldPosition.clone();
|
|
|
|
return systemId;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const center = this.toThreeVector(system.position);
|
|
|
|
if (!anchor || anchor.kind === "star") {
|
|
|
|
return worldPosition.clone().sub(center).multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE).add(center);
|
|
|
|
return `${system.label} star`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const planet = system.planets[anchor.planetIndex];
|
|
|
|
|
|
|
|
if (!planet) {
|
|
|
|
|
|
|
|
return `${system.label} star`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (anchor.kind === "planet") {
|
|
|
|
|
|
|
|
return planet.label;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `${planet.label} moon ${anchor.moonIndex + 1}`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private isSelectionInActiveSystem(selection: Selectable) {
|
|
|
|
|
|
|
|
return !!this.activeSystemId && this.resolveSelectableSystemId(selection) === this.activeSystemId;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private getCameraFocusWorldPosition() {
|
|
|
|
|
|
|
|
if (!this.activeSystemId || !this.world) {
|
|
|
|
|
|
|
|
return this.galaxyFocus;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const system = this.world.systems.get(this.activeSystemId);
|
|
|
|
|
|
|
|
return system
|
|
|
|
|
|
|
|
? this.toThreeVector(system.galaxyPosition).add(this.systemFocusLocal.clone().multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE * GALAXY_PARALLAX_FACTOR))
|
|
|
|
|
|
|
|
: this.galaxyFocus;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private seedSystemFocusLocal(systemId: string) {
|
|
|
|
|
|
|
|
if (!this.world) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (this.followedShipId) {
|
|
|
|
|
|
|
|
const followedShip = this.world.ships.get(this.followedShipId);
|
|
|
|
|
|
|
|
if (followedShip?.systemId === systemId) {
|
|
|
|
|
|
|
|
this.systemFocusLocal.copy(this.toThreeVector(followedShip.localPosition));
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const selected = this.selectedItems[0];
|
|
|
|
|
|
|
|
if (selected && this.resolveSelectableSystemId(selected) === systemId) {
|
|
|
|
|
|
|
|
const selectedPosition = this.resolveSelectionPosition(selected);
|
|
|
|
|
|
|
|
if (selectedPosition) {
|
|
|
|
|
|
|
|
this.systemFocusLocal.copy(selectedPosition);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.systemFocusLocal.set(0, 0, 0);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private toDisplayLocalPosition(localPosition: THREE.Vector3, systemId?: string) {
|
|
|
|
|
|
|
|
if (!this.world || !systemId) {
|
|
|
|
|
|
|
|
return localPosition.clone();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const system = this.world.systems.get(systemId);
|
|
|
|
|
|
|
|
if (!system) {
|
|
|
|
|
|
|
|
return localPosition.clone();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const center = this.toThreeVector(system.galaxyPosition);
|
|
|
|
|
|
|
|
if (systemId !== this.activeSystemId) {
|
|
|
|
|
|
|
|
return center.clone().add(localPosition);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return center.clone().add(localPosition.clone().sub(this.systemFocusLocal).multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private renderSystemDetails(system: SystemSnapshot, activeContext: boolean) {
|
|
|
|
private renderSystemDetails(system: SystemSnapshot, activeContext: boolean) {
|
|
|
|
@@ -1997,7 +2446,7 @@ export class GameViewer {
|
|
|
|
<p>${system.id}${activeContext ? " · active system" : ""}</p>
|
|
|
|
<p>${system.id}${activeContext ? " · active system" : ""}</p>
|
|
|
|
<p>${system.starKind} · ${system.starCount} star${system.starCount > 1 ? "s" : ""}</p>
|
|
|
|
<p>${system.starKind} · ${system.starCount} star${system.starCount > 1 ? "s" : ""}</p>
|
|
|
|
<p>Planets ${system.planets.length}<br>Moons ${moonCount}<br>Ships ${shipCount}<br>Stations ${stationCount}<br>Nodes ${nodeCount}</p>
|
|
|
|
<p>Planets ${system.planets.length}<br>Moons ${moonCount}<br>Ships ${shipCount}<br>Stations ${stationCount}<br>Nodes ${nodeCount}</p>
|
|
|
|
<p>Height ${system.position.y.toFixed(0)}</p>
|
|
|
|
<p>Height ${system.galaxyPosition.y.toFixed(0)}</p>
|
|
|
|
<p>${system.planets.slice(0, 8).map((planet) => `${planet.label} (${planet.planetType})`).join("<br>")}</p>
|
|
|
|
<p>${system.planets.slice(0, 8).map((planet) => `${planet.label} (${planet.planetType})`).join("<br>")}</p>
|
|
|
|
${followText}
|
|
|
|
${followText}
|
|
|
|
`;
|
|
|
|
`;
|
|
|
|
@@ -2019,6 +2468,6 @@ export class GameViewer {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.systemTitleEl.textContent = activeSystem.label;
|
|
|
|
this.systemTitleEl.textContent = activeSystem.label;
|
|
|
|
this.systemBodyEl.innerHTML = this.renderSystemDetails(activeSystem, true);
|
|
|
|
this.systemBodyEl.innerHTML = `<p>${activeSystem.starKind}</p>`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|