Add layered system viewer and local coordinates

This commit is contained in:
2026-03-13 00:48:08 -04:00
parent 22a4b18be8
commit a9c08124f5
7 changed files with 576 additions and 94 deletions

View File

@@ -33,7 +33,7 @@ public sealed record SimulationEventRecord(
public sealed record SystemSnapshot(
string Id,
string Label,
Vector3Dto Position,
Vector3Dto GalaxyPosition,
string StarKind,
int StarCount,
string StarColor,
@@ -59,7 +59,7 @@ public sealed record PlanetSnapshot(
public sealed record ResourceNodeSnapshot(
string Id,
string SystemId,
Vector3Dto Position,
Vector3Dto LocalPosition,
string SourceKind,
float OreRemaining,
float MaxOre,
@@ -68,7 +68,7 @@ public sealed record ResourceNodeSnapshot(
public sealed record ResourceNodeDelta(
string Id,
string SystemId,
Vector3Dto Position,
Vector3Dto LocalPosition,
string SourceKind,
float OreRemaining,
float MaxOre,
@@ -79,7 +79,7 @@ public sealed record StationSnapshot(
string Label,
string Category,
string SystemId,
Vector3Dto Position,
Vector3Dto LocalPosition,
string Color,
int DockedShips,
float OreStored,
@@ -91,7 +91,7 @@ public sealed record StationDelta(
string Label,
string Category,
string SystemId,
Vector3Dto Position,
Vector3Dto LocalPosition,
string Color,
int DockedShips,
float OreStored,
@@ -104,9 +104,9 @@ public sealed record ShipSnapshot(
string Role,
string ShipClass,
string SystemId,
Vector3Dto Position,
Vector3Dto Velocity,
Vector3Dto TargetPosition,
Vector3Dto LocalPosition,
Vector3Dto LocalVelocity,
Vector3Dto TargetLocalPosition,
string State,
string? OrderKind,
string DefaultBehaviorKind,
@@ -124,9 +124,9 @@ public sealed record ShipDelta(
string Role,
string ShipClass,
string SystemId,
Vector3Dto Position,
Vector3Dto Velocity,
Vector3Dto TargetPosition,
Vector3Dto LocalPosition,
Vector3Dto LocalVelocity,
Vector3Dto TargetLocalPosition,
string State,
string? OrderKind,
string DefaultBehaviorKind,

View File

@@ -116,6 +116,8 @@ public readonly record struct Vector3(float X, float Y, float Z)
{
public static Vector3 Zero => new(0f, 0f, 0f);
public float LengthSquared() => (X * X) + (Y * Y) + (Z * Z);
public float DistanceTo(Vector3 other)
{
var dx = X - other.X;

View File

@@ -111,9 +111,9 @@ public sealed class ScenarioLoader
Id = $"node-{++nodeIdCounter}",
SystemId = system.Definition.Id,
Position = new Vector3(
system.Position.X + (MathF.Cos(node.Angle) * node.RadiusOffset),
system.Position.Y + balance.YPlane,
system.Position.Z + (MathF.Sin(node.Angle) * node.RadiusOffset)),
MathF.Cos(node.Angle) * node.RadiusOffset,
balance.YPlane,
MathF.Sin(node.Angle) * node.RadiusOffset),
SourceKind = node.SourceKind,
ItemId = node.ItemId,
OreRemaining = node.OreAmount,
@@ -149,7 +149,7 @@ public sealed class ScenarioLoader
var patrolRoutes = scenario.PatrolRoutes.ToDictionary(
(route) => route.SystemId,
(route) => route.Points.Select(ToVector).ToList(),
(route) => route.Points.Select((point) => NormalizeScenarioPoint(systemsById[route.SystemId], point)).ToList(),
StringComparer.Ordinal);
var shipsRuntime = new List<ShipRuntime>();
@@ -164,7 +164,7 @@ public sealed class ScenarioLoader
for (var index = 0; index < formation.Count; index += 1)
{
var offset = new Vector3((index % 3) * 18f, balance.YPlane, (index / 3) * 18f);
var position = Add(ToVector(formation.Center), offset);
var position = Add(NormalizeScenarioPoint(systemsById[formation.SystemId], formation.Center), offset);
shipsRuntime.Add(new ShipRuntime
{
Id = $"ship-{++shipIdCounter}",
@@ -832,7 +832,7 @@ public sealed class ScenarioLoader
{
if (plan.Position is { Length: 3 })
{
return ToVector(plan.Position);
return NormalizeScenarioPoint(system, plan.Position);
}
if (plan.PlanetIndex is int planetIndex && planetIndex >= 0 && planetIndex < system.Definition.Planets.Count)
@@ -840,15 +840,28 @@ public sealed class ScenarioLoader
var planet = system.Definition.Planets[planetIndex];
var side = plan.LagrangeSide ?? 1;
return new Vector3(
system.Position.X + planet.OrbitRadius + (side * 72f),
system.Position.Y + balance.YPlane,
system.Position.Z + ((planetIndex + 1) * 42f * side));
planet.OrbitRadius + (side * 72f),
balance.YPlane,
(planetIndex + 1) * 42f * side);
}
return new Vector3(system.Position.X + 180f, system.Position.Y + balance.YPlane, system.Position.Z);
return new Vector3(180f, balance.YPlane, 0f);
}
private static Vector3 ToVector(float[] values) => new(values[0], values[1], values[2]);
private static Vector3 NormalizeScenarioPoint(SystemRuntime system, float[] values)
{
var raw = ToVector(values);
var relativeToSystem = new Vector3(
raw.X - system.Position.X,
raw.Y - system.Position.Y,
raw.Z - system.Position.Z);
return relativeToSystem.LengthSquared() < raw.LengthSquared()
? relativeToSystem
: raw;
}
private static Vector3 Add(Vector3 left, Vector3 right) => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
}

View File

@@ -77,7 +77,7 @@ public sealed class SimulationEngine
world.Nodes.Select(ToNodeDelta).Select((node) => new ResourceNodeSnapshot(
node.Id,
node.SystemId,
node.Position,
node.LocalPosition,
node.SourceKind,
node.OreRemaining,
node.MaxOre,
@@ -87,7 +87,7 @@ public sealed class SimulationEngine
station.Label,
station.Category,
station.SystemId,
station.Position,
station.LocalPosition,
station.Color,
station.DockedShips,
station.OreStored,
@@ -99,9 +99,9 @@ public sealed class SimulationEngine
ship.Role,
ship.ShipClass,
ship.SystemId,
ship.Position,
ship.Velocity,
ship.TargetPosition,
ship.LocalPosition,
ship.LocalVelocity,
ship.TargetLocalPosition,
ship.State,
ship.OrderKind,
ship.DefaultBehaviorKind,

View File

@@ -53,17 +53,43 @@ interface MoonVisual {
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 {
systemId: string;
mesh: THREE.Mesh;
icon: THREE.Sprite;
worldPosition: THREE.Vector3;
anchor: OrbitalAnchor;
orbitRadius: number;
orbitPhase: number;
orbitInclination: number;
localPosition: THREE.Vector3;
}
interface SystemVisual {
root: THREE.Group;
starCluster: THREE.Group;
icon: THREE.Sprite;
shellReticle: THREE.Sprite;
shellReticleBaseScale: number;
detailGroup: THREE.Group;
summary: SystemSummaryVisual;
galaxyPosition: THREE.Vector3;
}
interface WorldState {
@@ -128,8 +154,11 @@ const ZOOM_DISTANCE: Record<ZoomLevel, number> = {
system: 3200,
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 PROJECTED_GALAXY_RADIUS = 65000;
const STAR_RENDER_SCALE = 0.18;
const MIN_CAMERA_DISTANCE = 450;
const MAX_CAMERA_DISTANCE = 42000;
@@ -143,11 +172,12 @@ export class GameViewer {
private readonly container: HTMLElement;
private readonly renderer = new THREE.WebGLRenderer({ antialias: true });
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 raycaster = new THREE.Raycaster();
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 keyState = new Set<string>();
private readonly systemGroup = new THREE.Group();
@@ -157,7 +187,7 @@ export class GameViewer {
private readonly ambienceGroup = new THREE.Group();
private readonly selectableTargets = new Map<THREE.Object3D, Selectable>();
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 shipVisuals = new Map<string, ShipVisual>();
private readonly systemVisuals = new Map<string, SystemVisual>();
@@ -175,6 +205,7 @@ export class GameViewer {
private readonly performancePanelEl: HTMLDivElement;
private readonly errorEl: HTMLDivElement;
private readonly marqueeEl: HTMLDivElement;
private readonly hoverLabelEl: HTMLDivElement;
private world?: WorldState;
private worldTimeSyncMs = performance.now();
@@ -257,6 +288,7 @@ export class GameViewer {
</div>
<section class="faction-strip"></section>
<div class="marquee-box"></div>
<div class="hover-label" hidden></div>
`;
this.statusEl = hud.querySelector(".topbar-body") as HTMLDivElement;
@@ -270,6 +302,7 @@ export class GameViewer {
this.performancePanelEl = hud.querySelector(".performance-body") as HTMLDivElement;
this.errorEl = hud.querySelector(".error-strip") 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);
@@ -423,16 +456,27 @@ export class GameViewer {
for (const system of systems) {
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 renderedStarSize = Math.max(system.starSize * STAR_RENDER_SCALE, 8);
const starCluster = this.createStarCluster(system);
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));
summaryVisual.sprite.position.set(0, system.starSize + 110, 0);
root.add(starCluster, systemIcon, summaryVisual.sprite, detailGroup);
const shellReticle = this.createShellReticle("#ff3b30", 400);
const summaryVisual = this.createSystemSummaryVisual(new THREE.Vector3(system.galaxyPosition.x, system.galaxyPosition.y + renderedStarSize + 140, system.galaxyPosition.z));
summaryVisual.sprite.position.set(0, renderedStarSize + 110, 0);
root.add(starCluster, systemIcon, shellReticle, summaryVisual.sprite, detailGroup);
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);
starCluster.traverse((child) => {
if (child instanceof THREE.Mesh) {
@@ -440,6 +484,7 @@ export class GameViewer {
}
});
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()) {
const orbit = this.createPlanetOrbit(planet);
@@ -493,11 +538,19 @@ export class GameViewer {
const mesh = this.createNodeMesh(node);
const icon = this.createTacticalIcon(node.sourceKind === "gas-cloud" ? "#7fd6ff" : "#d2b07a", 20);
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, {
systemId: node.systemId,
mesh,
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.registerPresentation(mesh, icon, true, true, node.systemId);
@@ -514,11 +567,18 @@ export class GameViewer {
const mesh = this.createStationMesh(station);
const icon = this.createTacticalIcon(station.color, 26);
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, {
systemId: station.systemId,
mesh,
icon,
worldPosition: this.toThreeVector(station.position),
anchor,
orbitRadius: orbital.radius,
orbitPhase: orbital.phase,
orbitInclination: orbital.inclination,
localPosition,
});
this.stationGroup.add(mesh, icon);
this.registerPresentation(mesh, icon, true, true, station.systemId);
@@ -534,7 +594,7 @@ export class GameViewer {
for (const ship of ships) {
const mesh = this.createShipMesh(ship);
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);
this.shipGroup.add(mesh, icon);
this.selectableTargets.set(mesh, { kind: "ship", id: ship.id });
@@ -546,8 +606,8 @@ export class GameViewer {
icon,
startPosition: position.clone(),
authoritativePosition: position.clone(),
targetPosition: this.toThreeVector(ship.targetPosition),
velocity: this.toThreeVector(ship.velocity),
targetPosition: this.toThreeVector(ship.targetLocalPosition),
velocity: this.toThreeVector(ship.localVelocity),
receivedAtMs: performance.now(),
blendDurationMs: Math.max(tickIntervalMs, 80),
});
@@ -562,7 +622,13 @@ export class GameViewer {
}
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);
}
}
@@ -575,7 +641,12 @@ export class GameViewer {
}
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;
material.color.set(station.color);
material.emissive = new THREE.Color(station.color).multiplyScalar(0.1);
@@ -591,9 +662,9 @@ export class GameViewer {
visual.systemId = ship.systemId;
visual.startPosition.copy(visual.authoritativePosition);
visual.authoritativePosition.copy(this.toThreeVector(ship.position));
visual.targetPosition.copy(this.toThreeVector(ship.targetPosition));
visual.velocity.copy(this.toThreeVector(ship.velocity));
visual.authoritativePosition.copy(this.toThreeVector(ship.localPosition));
visual.targetPosition.copy(this.toThreeVector(ship.targetLocalPosition));
visual.velocity.copy(this.toThreeVector(ship.localVelocity));
visual.receivedAtMs = performance.now();
visual.blendDurationMs = Math.max(tickIntervalMs * 1.35, 100);
(visual.mesh.material as THREE.MeshStandardMaterial).color.set(this.shipColor(ship.role));
@@ -649,12 +720,14 @@ export class GameViewer {
if (!ship) {
return;
}
const parent = this.describeSelectionParent(selected);
this.detailTitleEl.textContent = ship.label;
this.detailBodyEl.innerHTML = `
<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>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 class="history">${ship.history.join("<br>")}</p>
`;
@@ -666,9 +739,11 @@ export class GameViewer {
if (!station) {
return;
}
const parent = this.describeSelectionParent(selected);
this.detailTitleEl.textContent = station.label;
this.detailBodyEl.innerHTML = `
<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 class="history">${this.renderRecentEvents("station", station.id)}</p>
`;
@@ -680,9 +755,11 @@ export class GameViewer {
if (!node) {
return;
}
const parent = this.describeSelectionParent(selected);
this.detailTitleEl.textContent = `Node ${node.id}`;
this.detailBodyEl.innerHTML = `
<p>${node.systemId}</p>
<p>Parent ${parent}</p>
<p>Source ${node.sourceKind}<br>Resource ${node.itemId}</p>
<p>Stock ${node.oreRemaining.toFixed(0)} / ${node.maxOre.toFixed(0)}</p>
`;
@@ -695,9 +772,11 @@ export class GameViewer {
if (!system || !planet) {
return;
}
const parent = this.describeSelectionParent(selected);
this.detailTitleEl.textContent = planet.label;
this.detailBodyEl.innerHTML = `
<p>${system.label}</p>
<p>Parent ${parent}</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>Phase ${planet.orbitPhaseAtEpoch.toFixed(1)}°</p>
@@ -710,7 +789,10 @@ export class GameViewer {
return;
}
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() {
@@ -742,13 +824,14 @@ export class GameViewer {
this.orbitPitch = THREE.MathUtils.clamp(this.orbitPitch, 0.18, 1.3);
const horizontalDistance = this.currentDistance * Math.cos(this.orbitPitch);
const focus = this.getCameraFocusWorldPosition();
this.cameraOffset.set(
Math.cos(this.orbitYaw) * horizontalDistance,
this.currentDistance * Math.sin(this.orbitPitch),
Math.sin(this.orbitYaw) * horizontalDistance,
);
this.camera.position.copy(this.focus).add(this.cameraOffset);
this.camera.lookAt(this.focus);
this.camera.position.copy(focus).add(this.cameraOffset);
this.camera.lookAt(focus);
}
private updatePanFromKeyboard(delta: number) {
@@ -778,7 +861,11 @@ export class GameViewer {
const right = new THREE.Vector3(-forward.z, 0, forward.x);
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);
this.focus.addScaledVector(pan, speed * delta);
if (this.activeSystemId) {
this.systemFocusLocal.addScaledVector(pan, speed * delta);
return;
}
this.galaxyFocus.addScaledVector(pan, speed * delta);
}
private applyZoomPresentation() {
@@ -787,10 +874,16 @@ export class GameViewer {
for (const entry of this.presentationEntries) {
const systemId = entry.systemId;
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
? Math.max(blend.localWeight, blend.systemWeight) * (isActiveDetail ? 1 : 0)
: 1;
const iconAlpha = entry.hideIconInUniverse
const iconAlpha = isProjectedSystemIcon
? 0
: entry.hideIconInUniverse
? blend.systemWeight * (isActiveDetail ? 1 : 0)
: Math.max(blend.systemWeight, blend.universeWeight);
@@ -909,6 +1002,7 @@ export class GameViewer {
private updateShipPresentation() {
const now = performance.now();
const worldTimeSeconds = this.currentWorldTimeSeconds();
for (const visual of this.shipVisuals.values()) {
const elapsedMs = now - visual.receivedAtMs;
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);
}
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);
const shipVisible = visual.systemId === this.activeSystemId;
visual.mesh.visible = shipVisible;
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) {
visual.mesh.lookAt(visual.mesh.position.clone().add(desiredHeading));
}
}
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.mesh.visible = visual.systemId === this.activeSystemId;
}
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.mesh.visible = visual.systemId === this.activeSystemId;
}
this.updateSystemStarPresentation();
this.updateSystemDetailVisibility();
this.updateSystemSummaryPresentation();
}
@@ -949,8 +1046,15 @@ export class GameViewer {
const nowSeconds = this.currentWorldTimeSeconds();
for (const visual of this.planetVisuals) {
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.position.copy(orbitOffset);
visual.mesh.position.copy(position);
visual.icon.position.copy(position);
if (visual.ring) {
@@ -976,25 +1080,26 @@ export class GameViewer {
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);
return mesh;
}
private createStarCluster(system: SystemSnapshot) {
const root = new THREE.Group();
const renderedStarSize = Math.max(system.starSize * STAR_RENDER_SCALE, 8);
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)];
for (const [index, offset] of offsets.entries()) {
const sizeScale = index === 0 ? 1 : 0.72;
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 }),
);
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({
color: system.starColor,
transparent: true,
@@ -1080,7 +1185,7 @@ export class GameViewer {
new THREE.MeshStandardMaterial({ color: station.color, emissive: new THREE.Color(station.color).multiplyScalar(0.1) }),
);
mesh.rotation.x = Math.PI / 2;
mesh.position.copy(this.toThreeVector(station.position));
mesh.position.copy(this.toThreeVector(station.localPosition));
return mesh;
}
@@ -1091,7 +1196,7 @@ export class GameViewer {
geometry,
new THREE.MeshStandardMaterial({ color: this.shipColor(ship.role) }),
);
mesh.position.copy(this.toThreeVector(ship.position));
mesh.position.copy(this.toThreeVector(ship.localPosition));
return mesh;
}
@@ -1262,6 +1367,50 @@ export class GameViewer {
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() {
if (!this.world) {
return;
@@ -1364,7 +1513,8 @@ export class GameViewer {
private updateSystemSummaryPresentation() {
const distanceScale = this.activeSystemId ? 0.05 : 0.085;
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 scale = Math.max(minimumScale, distance * distanceScale);
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);
}
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) {
let hash = this.world?.seed ?? 1;
for (let index = 0; index < value.length; index += 1) {
@@ -1616,6 +1864,8 @@ export class GameViewer {
};
private onPointerMove = (event: PointerEvent) => {
this.updateHoverLabel(event);
if (this.dragPointerId !== event.pointerId || !this.dragMode) {
return;
}
@@ -1669,23 +1919,57 @@ export class GameViewer {
return;
}
const bounds = this.renderer.domElement.getBoundingClientRect();
this.mouse.x = ((event.clientX - bounds.left) / bounds.width) * 2 - 1;
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)!] : [];
const picked = this.pickSelectableAtClientPosition(event.clientX, event.clientY);
this.selectedItems = picked ? [picked] : [];
this.syncFollowStateFromSelection();
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 = () => {
if (this.selectedItems.length !== 1) {
return;
}
const nextFocus = this.resolveSelectionPosition(this.selectedItems[0]);
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();
};
@@ -1728,15 +2012,21 @@ export class GameViewer {
if (selection.kind === "ship") {
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") {
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") {
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") {
const system = this.world.systems.get(selection.systemId);
@@ -1746,10 +2036,10 @@ export class GameViewer {
}
const visual = this.planetVisuals.find((candidate) =>
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);
return system ? this.toThreeVector(system.position) : undefined;
return system ? this.toThreeVector(system.galaxyPosition) : undefined;
}
private updateMarqueeBox() {
@@ -1850,6 +2140,9 @@ export class GameViewer {
return;
}
if (nextActiveSystemId) {
this.seedSystemFocusLocal(nextActiveSystemId);
}
this.activeSystemId = nextActiveSystemId;
this.updateSystemDetailVisibility();
this.updatePanels();
@@ -1882,8 +2175,8 @@ export class GameViewer {
let nearestSystemId: string | undefined;
let nearestDistance = Number.POSITIVE_INFINITY;
for (const system of this.world.systems.values()) {
const center = this.toThreeVector(system.position);
const distance = center.distanceTo(this.focus);
const center = this.toThreeVector(system.galaxyPosition);
const distance = center.distanceTo(this.galaxyFocus);
if (distance < nearestDistance) {
nearestDistance = distance;
nearestSystemId = system.id;
@@ -1906,8 +2199,8 @@ export class GameViewer {
return;
}
const target = this.toDisplayPosition(this.toThreeVector(ship.position), ship.systemId);
this.focus.lerp(target, 1 - Math.exp(-delta * 8));
const target = this.toThreeVector(ship.localPosition);
this.systemFocusLocal.lerp(target, 1 - Math.exp(-delta * 8));
}
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) {
if (!this.world) {
return undefined;
@@ -1946,18 +2291,122 @@ export class GameViewer {
return selection.id;
}
private toDisplayPosition(worldPosition: THREE.Vector3, systemId?: string) {
if (!this.world || !systemId || systemId !== this.activeSystemId) {
return worldPosition.clone();
private describeSelectionParent(selection: Selectable) {
if (!this.world) {
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);
if (!system) {
return worldPosition.clone();
return systemId;
}
const center = this.toThreeVector(system.position);
return worldPosition.clone().sub(center).multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE).add(center);
if (!anchor || anchor.kind === "star") {
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) {
@@ -1997,7 +2446,7 @@ export class GameViewer {
<p>${system.id}${activeContext ? " · active system" : ""}</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>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>
${followText}
`;
@@ -2019,6 +2468,6 @@ export class GameViewer {
}
this.systemTitleEl.textContent = activeSystem.label;
this.systemBodyEl.innerHTML = this.renderSystemDetails(activeSystem, true);
this.systemBodyEl.innerHTML = `<p>${activeSystem.starKind}</p>`;
}
}

View File

@@ -40,7 +40,7 @@ export interface Vector3Dto {
export interface SystemSnapshot {
id: string;
label: string;
position: Vector3Dto;
galaxyPosition: Vector3Dto;
starKind: string;
starCount: number;
starColor: string;
@@ -68,7 +68,7 @@ export interface PlanetSnapshot {
export interface ResourceNodeSnapshot {
id: string;
systemId: string;
position: Vector3Dto;
localPosition: Vector3Dto;
sourceKind: string;
oreRemaining: number;
maxOre: number;
@@ -82,7 +82,7 @@ export interface StationSnapshot {
label: string;
category: string;
systemId: string;
position: Vector3Dto;
localPosition: Vector3Dto;
color: string;
dockedShips: number;
oreStored: number;
@@ -98,9 +98,9 @@ export interface ShipSnapshot {
role: string;
shipClass: string;
systemId: string;
position: Vector3Dto;
velocity: Vector3Dto;
targetPosition: Vector3Dto;
localPosition: Vector3Dto;
localVelocity: Vector3Dto;
targetLocalPosition: Vector3Dto;
state: string;
orderKind: string | null;
defaultBehaviorKind: string;

View File

@@ -65,6 +65,24 @@ canvas {
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04);
}
.hover-label {
position: absolute;
padding: 8px 10px;
border-radius: 999px;
background: rgba(7, 15, 28, 0.88);
border: 1px solid rgba(255, 88, 72, 0.5);
color: #fff2ef;
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
font-size: 0.75rem;
line-height: 1;
white-space: nowrap;
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.32);
}
.hover-label[hidden] {
display: none;
}
.topbar,
.info-panel,
.network-panel,