feat: tactical icons, follow-camera orbit, and ship info panel
This commit is contained in:
@@ -54,6 +54,7 @@ public sealed partial class SimulationEngine
|
||||
// Resolve live position each frame — entities like stations orbit celestials and move every tick
|
||||
var targetPosition = ResolveCurrentTargetPosition(world, task);
|
||||
var targetCelestial = ResolveTravelTargetCelestial(world, task, targetPosition);
|
||||
var distance = ship.Position.DistanceTo(targetPosition);
|
||||
ship.TargetPosition = targetPosition;
|
||||
|
||||
if (ship.SystemId != task.TargetSystemId)
|
||||
@@ -80,6 +81,13 @@ public sealed partial class SimulationEngine
|
||||
return UpdateWarpTransit(ship, world, deltaSeconds, targetPosition, targetCelestial);
|
||||
}
|
||||
|
||||
if (targetCelestial is not null
|
||||
&& distance > WarpEngageDistanceKilometers
|
||||
&& HasShipCapabilities(ship.Definition, "warp"))
|
||||
{
|
||||
return UpdateWarpTransit(ship, world, deltaSeconds, targetPosition, targetCelestial);
|
||||
}
|
||||
|
||||
return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetCelestial, task.Threshold);
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,8 @@ export class ViewerAppController {
|
||||
private readonly followCameraDirection = new THREE.Vector3(0, 0.16, 1);
|
||||
private readonly followCameraDesiredDirection = new THREE.Vector3(0, 0.16, 1);
|
||||
private readonly followCameraOffset = new THREE.Vector3();
|
||||
private followOrbitYaw = 0;
|
||||
private followOrbitPitch = 0.2;
|
||||
private readonly historyWindows: HistoryWindowState[] = [];
|
||||
private historyWindowCounter = 0;
|
||||
private historyWindowZCounter = 10;
|
||||
|
||||
@@ -9,6 +9,7 @@ export const NAV_DISTANCE: Record<PovLevel, number> = {
|
||||
// Close-orbit distance when double-clicking a planet (display units).
|
||||
// 0.005 units = ~333 km from planet center in system space.
|
||||
export const NAV_DISTANCE_PLANET_ORBIT = 0.005;
|
||||
export const NAV_DISTANCE_SHIP_HULL = 0.0004;
|
||||
|
||||
export const ACTIVE_SYSTEM_DETAIL_SCALE = 10;
|
||||
export const GALAXY_PARALLAX_FACTOR = 0.025;
|
||||
@@ -17,8 +18,8 @@ export const PROJECTED_GALAXY_RADIUS = 65000;
|
||||
export const STAR_RENDER_SCALE = 0.18;
|
||||
export const PLANET_RENDER_SCALE = 0.95;
|
||||
export const MOON_RENDER_SCALE = 1.1;
|
||||
// 0.002 units = ~133 km — allows scrolling into low orbit around planets.
|
||||
export const MIN_CAMERA_DISTANCE = 0.002;
|
||||
// 0.00005 units = ~3 km — allows scrolling very close to ships and structures.
|
||||
export const MIN_CAMERA_DISTANCE = 0.00005;
|
||||
export const MAX_CAMERA_DISTANCE = 150000;
|
||||
|
||||
export interface ZoomBlend {
|
||||
|
||||
@@ -57,6 +57,8 @@ export function createViewerControllers(host: any) {
|
||||
getPovLevel: () => host.povLevel,
|
||||
getSelectedItems: () => host.selectedItems,
|
||||
getOrbitYaw: () => host.orbitYaw,
|
||||
getFollowOrbitYaw: () => host.followOrbitYaw,
|
||||
getFollowOrbitPitch: () => host.followOrbitPitch,
|
||||
galaxyAnchor: host.galaxyAnchor,
|
||||
systemAnchor: host.systemAnchor,
|
||||
galaxyCamera: host.galaxyLayer.camera,
|
||||
@@ -240,8 +242,13 @@ export function createViewerControllers(host: any) {
|
||||
getFollowCameraFocus: () => host.followCameraFocus,
|
||||
screenPointFromClient: (x, y) => presentationController.screenPointFromClient(x, y),
|
||||
applyOrbitDelta: (delta: THREE.Vector2) => {
|
||||
host.orbitYaw += delta.x * 0.008;
|
||||
host.orbitPitch = THREE.MathUtils.clamp(host.orbitPitch + delta.y * 0.004, 0.18, 1.3);
|
||||
if (host.cameraMode === "follow") {
|
||||
host.followOrbitYaw += delta.x * 0.008;
|
||||
host.followOrbitPitch = THREE.MathUtils.clamp(host.followOrbitPitch + delta.y * 0.004, 0.02, 1.45);
|
||||
} else {
|
||||
host.orbitYaw += delta.x * 0.008;
|
||||
host.orbitPitch = THREE.MathUtils.clamp(host.orbitPitch + delta.y * 0.004, 0.18, 1.3);
|
||||
}
|
||||
},
|
||||
syncFollowStateFromSelection: () => navigationController.syncFollowStateFromSelection(),
|
||||
updatePanels: () => host.updatePanels(),
|
||||
|
||||
@@ -72,7 +72,7 @@ export function toggleCameraMode(params: {
|
||||
return {
|
||||
cameraMode: "follow" as const,
|
||||
cameraTargetShipId: nextTargetShipId,
|
||||
desiredDistance: Math.min(desiredDistance, 1800),
|
||||
desiredDistance: Math.min(desiredDistance, 0.0012),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,6 +90,8 @@ export function updateFollowCamera(params: {
|
||||
followCameraOffset: THREE.Vector3;
|
||||
systemAnchor: THREE.Vector3;
|
||||
delta: number;
|
||||
followOrbitYaw: number;
|
||||
followOrbitPitch: number;
|
||||
getAnimatedShipLocalPosition: (visual: ShipVisual) => THREE.Vector3;
|
||||
toDisplayLocalPosition: (localPosition: THREE.Vector3, systemId?: string) => THREE.Vector3;
|
||||
resolveShipHeading: (visual: ShipVisual, worldPosition: THREE.Vector3) => THREE.Vector3;
|
||||
@@ -107,6 +109,8 @@ export function updateFollowCamera(params: {
|
||||
followCameraOffset,
|
||||
systemAnchor,
|
||||
delta,
|
||||
followOrbitYaw,
|
||||
followOrbitPitch,
|
||||
getAnimatedShipLocalPosition,
|
||||
toDisplayLocalPosition,
|
||||
resolveShipHeading,
|
||||
@@ -160,14 +164,23 @@ export function updateFollowCamera(params: {
|
||||
followCameraDirection.lerp(followCameraDesiredDirection, 1 - Math.exp(-delta * 5));
|
||||
followCameraDirection.normalize();
|
||||
|
||||
const distance = THREE.MathUtils.clamp(currentDistance * 0.72, 320, 6800);
|
||||
const height = THREE.MathUtils.clamp(distance * 0.18, 70, 1100);
|
||||
const lookAhead = THREE.MathUtils.clamp(distance * 0.9, 220, 2400);
|
||||
followCameraOffset.copy(followCameraDirection).multiplyScalar(-distance);
|
||||
const distance = THREE.MathUtils.clamp(currentDistance * 0.72, 0.00018, 0.012);
|
||||
const height = THREE.MathUtils.clamp(distance * 0.14, 0.00002, 0.0012);
|
||||
const lookAhead = THREE.MathUtils.clamp(distance * 2.6, 0.0006, 0.028);
|
||||
|
||||
// Orbit the camera around the ship using followOrbitYaw/Pitch.
|
||||
// Base direction is "behind ship" (negate heading). Yaw rotates left/right, pitch elevates.
|
||||
const baseBack = followCameraDirection.clone().negate();
|
||||
const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), followOrbitYaw);
|
||||
const orbitBack = baseBack.clone().applyQuaternion(yawQuat);
|
||||
const cosP = Math.cos(followOrbitPitch), sinP = Math.sin(followOrbitPitch);
|
||||
followCameraOffset.set(orbitBack.x * cosP, sinP, orbitBack.z * cosP).normalize().multiplyScalar(distance);
|
||||
followCameraOffset.y += height;
|
||||
|
||||
const desiredPosition = shipWorldPosition.clone().add(followCameraOffset);
|
||||
const desiredFocus = shipWorldPosition.clone().addScaledVector(followCameraDirection, lookAhead);
|
||||
// Blend look-ahead based on how far off-axis the orbit is (full ahead when behind, ship center when in front)
|
||||
const lookBlend = Math.max(0, Math.cos(followOrbitYaw));
|
||||
const desiredFocus = shipWorldPosition.clone().addScaledVector(followCameraDirection, lookAhead * lookBlend);
|
||||
desiredFocus.y += height * 0.28;
|
||||
|
||||
const positionLerp = 1 - Math.exp(-delta * 6);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
toggleCameraMode,
|
||||
navigateFromWheel,
|
||||
} from "./viewerControls";
|
||||
import { NAV_DISTANCE, NAV_DISTANCE_PLANET_ORBIT } from "./viewerConstants";
|
||||
import { NAV_DISTANCE, NAV_DISTANCE_PLANET_ORBIT, NAV_DISTANCE_SHIP_HULL } from "./viewerConstants";
|
||||
import { ViewerHistoryWindowController } from "./viewerHistoryWindowController";
|
||||
import type {
|
||||
CameraMode,
|
||||
@@ -202,6 +202,7 @@ export class ViewerInteractionController {
|
||||
this.context.syncFollowStateFromSelection();
|
||||
this.context.focusOnSelection({ kind: "ship", id: shipId });
|
||||
this.toggleCameraMode("follow");
|
||||
this.context.setDesiredDistance(NAV_DISTANCE_SHIP_HULL);
|
||||
this.context.updatePanels();
|
||||
this.context.updateGamePanel("Live");
|
||||
return;
|
||||
@@ -238,6 +239,16 @@ export class ViewerInteractionController {
|
||||
this.context.syncFollowStateFromSelection();
|
||||
if (selection.kind === "planet") {
|
||||
this.context.setDesiredDistance(NAV_DISTANCE_PLANET_ORBIT);
|
||||
this.context.updateGamePanel("Live");
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.kind === "ship") {
|
||||
this.toggleCameraMode("follow");
|
||||
this.context.setDesiredDistance(NAV_DISTANCE_SHIP_HULL);
|
||||
this.context.updatePanels();
|
||||
this.context.updateGamePanel("Live");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ export interface ViewerNavigationContext {
|
||||
getPovLevel: () => PovLevel;
|
||||
getSelectedItems: () => Selectable[];
|
||||
getOrbitYaw: () => number;
|
||||
getFollowOrbitYaw: () => number;
|
||||
getFollowOrbitPitch: () => number;
|
||||
galaxyAnchor: THREE.Vector3;
|
||||
systemAnchor: THREE.Vector3;
|
||||
galaxyCamera: THREE.PerspectiveCamera;
|
||||
@@ -126,6 +128,8 @@ export class ViewerNavigationController {
|
||||
followCameraOffset: this.context.followCameraOffset,
|
||||
systemAnchor: this.context.systemAnchor,
|
||||
delta,
|
||||
followOrbitYaw: this.context.getFollowOrbitYaw(),
|
||||
followOrbitPitch: this.context.getFollowOrbitPitch(),
|
||||
getAnimatedShipLocalPosition,
|
||||
toDisplayLocalPosition: (localPosition) => toDisplayLocalPosition(localPosition),
|
||||
resolveShipHeading: (visual, worldPosition) => resolveShipHeading(visual, worldPosition, this.context.getOrbitYaw()),
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
formatSystemDistance,
|
||||
inventoryAmount,
|
||||
} from "./viewerMath";
|
||||
import { describeCelestialPathWithinSystem, describeOrbitalParent, describeSelectable, describeShipCurrentAction, describeShipObjective, describeShipState, getSelectionGroup, renderSystemDetails } from "./viewerSelection";
|
||||
import { describeCelestialPathWithinSystem, describeOrbitalParent, describeSelectable, describeShipBehavior, describeShipCurrentAction, describeShipOrder, describeShipState, getSelectionGroup, renderSystemDetails } from "./viewerSelection";
|
||||
import type {
|
||||
CameraMode,
|
||||
HistoryWindowState,
|
||||
@@ -197,13 +197,15 @@ export function updateDetailPanel(
|
||||
const parent = describeSelectionParent(selected);
|
||||
const cargoUsed = ship.inventory.reduce((sum, e) => sum + e.amount, 0);
|
||||
const shipState = describeShipState(world, ship);
|
||||
const shipBehavior = describeShipBehavior(ship);
|
||||
const shipOrder = describeShipOrder(ship);
|
||||
const shipAction = describeShipCurrentAction(ship);
|
||||
detailTitleEl.textContent = ship.label;
|
||||
detailBodyEl.innerHTML = `
|
||||
<p>Parent ${parent}</p>
|
||||
<p>Behavior ${shipBehavior}</p>
|
||||
<p>State ${shipState}</p>
|
||||
${ship.commanderObjective ? `<p>Objective ${describeShipObjective(ship.commanderObjective)}</p>` : ""}
|
||||
<p>Behavior ${ship.defaultBehaviorKind}${ship.behaviorPhase ? ` · ${ship.behaviorPhase}` : ""}</p>
|
||||
<p>Order ${shipOrder}</p>
|
||||
<p>Task ${ship.controllerTaskKind}</p>
|
||||
${shipAction ? `
|
||||
<div class="detail-progress">
|
||||
|
||||
@@ -4,8 +4,8 @@ import { computeMoonLocalPosition, computePlanetLocalPosition, currentWorldTimeS
|
||||
import type { PlanetVisual, ShipVisual, SystemVisual, WorldState } from "./viewerTypes";
|
||||
import { rawObject } from "./viewerScenePrimitives";
|
||||
|
||||
const MIN_ICON_PIXELS = 25;
|
||||
const MAX_ICON_PIXELS = 50;
|
||||
export const MIN_ICON_PIXELS = 25;
|
||||
export const MAX_ICON_PIXELS = 50;
|
||||
|
||||
export function iconWorldScale(distToCamera: number, camera: THREE.PerspectiveCamera, pixels: number): number {
|
||||
return pixels * distToCamera * 2 * Math.tan((camera.fov * Math.PI / 180) / 2) / window.innerHeight;
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { updatePlanetPresentation } from "./viewerPresentation";
|
||||
import { renderRecentEvents, updateGameStatus, updateSystemSummaries, updateWorldPresentation } from "./viewerWorldPresentation";
|
||||
import { updateSystemPanel } from "./viewerPanels";
|
||||
import { createBackdropStars, createMilkyWayBand, createNebulaClouds, createNebulaTexture } from "./viewerSceneFactory";
|
||||
import { createBackdropStars, createNebulaClouds, createNebulaTexture } from "./viewerSceneFactory";
|
||||
import type { Selectable } from "./viewerTypes";
|
||||
|
||||
export interface ViewerPresentationContext {
|
||||
@@ -45,13 +45,12 @@ export interface ViewerPresentationContext {
|
||||
}
|
||||
|
||||
export class ViewerPresentationController {
|
||||
constructor(private readonly context: ViewerPresentationContext) {}
|
||||
constructor(private readonly context: ViewerPresentationContext) { }
|
||||
|
||||
initializeAmbience() {
|
||||
this.context.ambienceGroup.renderOrder = -10;
|
||||
this.context.ambienceGroup.add(createBackdropStars(document));
|
||||
this.context.ambienceGroup.add(...createNebulaClouds(createNebulaTexture(document)));
|
||||
this.context.ambienceGroup.add(createMilkyWayBand(document));
|
||||
}
|
||||
|
||||
updateAmbience(_delta: number) {
|
||||
|
||||
@@ -4,15 +4,15 @@ import type { ShipSnapshot } from "./contracts";
|
||||
export function shipSize(ship: ShipSnapshot) {
|
||||
switch (ship.class) {
|
||||
case "capital":
|
||||
return 18;
|
||||
return 0.018;
|
||||
case "cruiser":
|
||||
return 13;
|
||||
return 0.012;
|
||||
case "destroyer":
|
||||
return 10;
|
||||
return 0.009;
|
||||
case "industrial":
|
||||
return 11;
|
||||
return 0.01;
|
||||
default:
|
||||
return 8;
|
||||
return 0.007;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -293,62 +293,6 @@ function createStarSparkleTexture(documentRef: Document): THREE.CanvasTexture {
|
||||
return texture;
|
||||
}
|
||||
|
||||
function createMilkyWayTexture(documentRef: Document): THREE.CanvasTexture {
|
||||
const canvas = documentRef.createElement("canvas");
|
||||
canvas.width = 1024;
|
||||
canvas.height = 256;
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
throw new Error("Unable to create milky way texture");
|
||||
}
|
||||
|
||||
const background = context.createLinearGradient(0, 0, 1024, 0);
|
||||
background.addColorStop(0, "rgba(0,0,0,0)");
|
||||
background.addColorStop(0.1, "rgba(150,110,255,0.08)");
|
||||
background.addColorStop(0.32, "rgba(120,210,255,0.14)");
|
||||
background.addColorStop(0.5, "rgba(255,240,220,0.28)");
|
||||
background.addColorStop(0.68, "rgba(255,165,210,0.16)");
|
||||
background.addColorStop(0.88, "rgba(115,155,255,0.08)");
|
||||
background.addColorStop(1, "rgba(0,0,0,0)");
|
||||
context.fillStyle = background;
|
||||
context.fillRect(0, 0, 1024, 256);
|
||||
|
||||
for (let index = 0; index < 220; index += 1) {
|
||||
const x = THREE.MathUtils.randFloat(0, 1024);
|
||||
const y = 128 + THREE.MathUtils.randFloatSpread(78);
|
||||
const radiusX = THREE.MathUtils.randFloat(40, 180);
|
||||
const radiusY = THREE.MathUtils.randFloat(8, 28);
|
||||
const alpha = THREE.MathUtils.randFloat(0.025, 0.09);
|
||||
const hue = THREE.MathUtils.randFloat(0.52, 0.76);
|
||||
const color = new THREE.Color().setHSL(hue, THREE.MathUtils.randFloat(0.25, 0.6), THREE.MathUtils.randFloat(0.72, 0.9));
|
||||
const puff = context.createRadialGradient(x, y, 0, x, y, radiusX);
|
||||
puff.addColorStop(0, `rgba(${Math.round(color.r * 255)},${Math.round(color.g * 255)},${Math.round(color.b * 255)},${alpha})`);
|
||||
puff.addColorStop(0.55, `rgba(${Math.round(color.r * 255)},${Math.round(color.g * 255)},${Math.round(color.b * 255)},${alpha * 0.45})`);
|
||||
puff.addColorStop(1, "rgba(0,0,0,0)");
|
||||
context.save();
|
||||
context.translate(x, y);
|
||||
context.scale(1, radiusY / radiusX);
|
||||
context.fillStyle = puff;
|
||||
context.beginPath();
|
||||
context.arc(0, 0, radiusX, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
context.restore();
|
||||
}
|
||||
|
||||
for (let index = 0; index < 540; index += 1) {
|
||||
const x = THREE.MathUtils.randFloat(0, 1024);
|
||||
const y = 128 + THREE.MathUtils.randFloatSpread(54);
|
||||
const alpha = THREE.MathUtils.randFloat(0.12, 0.65);
|
||||
const size = THREE.MathUtils.randFloat(0.8, 2.4);
|
||||
context.fillStyle = `rgba(255,255,255,${alpha})`;
|
||||
context.fillRect(x, y, size, size);
|
||||
}
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
texture.needsUpdate = true;
|
||||
return texture;
|
||||
}
|
||||
|
||||
function sampleBackdropStarColor(): THREE.Color {
|
||||
const roll = Math.random();
|
||||
if (roll < 0.1) {
|
||||
@@ -595,39 +539,6 @@ export function createNebulaClouds(texture: THREE.Texture): THREE.Sprite[] {
|
||||
});
|
||||
}
|
||||
|
||||
export function createMilkyWayBand(documentRef: Document): THREE.Group {
|
||||
const radius = 33800;
|
||||
const texture = createMilkyWayTexture(documentRef);
|
||||
const root = new THREE.Group();
|
||||
const planeNormal = new THREE.Vector3(0.24, 0.92, -0.3).normalize();
|
||||
const tangent = new THREE.Vector3().crossVectors(planeNormal, new THREE.Vector3(0, 0, 1));
|
||||
if (tangent.lengthSq() < 1e-6) {
|
||||
tangent.set(1, 0, 0);
|
||||
}
|
||||
tangent.normalize();
|
||||
const bitangent = new THREE.Vector3().crossVectors(planeNormal, tangent).normalize();
|
||||
|
||||
for (let index = 0; index < 8; index += 1) {
|
||||
const angle = (index / 8) * Math.PI * 2;
|
||||
const direction = tangent.clone().multiplyScalar(Math.cos(angle)).add(bitangent.clone().multiplyScalar(Math.sin(angle)));
|
||||
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
opacity: index % 2 === 0 ? 0.22 : 0.15,
|
||||
depthWrite: false,
|
||||
blending: THREE.AdditiveBlending,
|
||||
color: index % 3 === 0 ? "#ffd3f1" : index % 3 === 1 ? "#c8d8ff" : "#ffffff",
|
||||
fog: false,
|
||||
}));
|
||||
sprite.position.copy(direction.multiplyScalar(radius));
|
||||
sprite.scale.set(16500, 4300 + (index % 3) * 800, 1);
|
||||
sprite.material.rotation = angle + Math.PI / 2;
|
||||
root.add(sprite);
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
export function createTacticalIcon(documentRef: Document, color: string, size: number): SceneNode {
|
||||
const canvas = documentRef.createElement("canvas");
|
||||
canvas.width = 64;
|
||||
@@ -657,6 +568,38 @@ export function createTacticalIcon(documentRef: Document, color: string, size: n
|
||||
return createSceneNode(sprite);
|
||||
}
|
||||
|
||||
export function createShipTacticalIcon(documentRef: Document, color: string, size: number): SceneNode {
|
||||
const canvas = documentRef.createElement("canvas");
|
||||
canvas.width = 128;
|
||||
canvas.height = 96;
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
throw new Error("Unable to create ship tactical icon");
|
||||
}
|
||||
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
context.strokeStyle = color;
|
||||
context.fillStyle = "rgba(7, 16, 30, 0.7)";
|
||||
context.lineWidth = 5;
|
||||
|
||||
context.beginPath();
|
||||
context.arc(34, 48, 18, 0, Math.PI * 2);
|
||||
context.stroke();
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
depthTest: false,
|
||||
color: "#ffffff",
|
||||
}));
|
||||
sprite.center.set(0.28, 0.5);
|
||||
sprite.scale.set(size * 1.7, size * 1.275, 1);
|
||||
sprite.visible = false;
|
||||
return createSceneNode(sprite);
|
||||
}
|
||||
|
||||
|
||||
export function createStarDot(documentRef: Document, color: string): SceneNode {
|
||||
const canvas = documentRef.createElement("canvas");
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
createPlanetTexture,
|
||||
createShellReticle,
|
||||
createShipMesh,
|
||||
createShipTacticalIcon,
|
||||
createCelestialMesh,
|
||||
createStarCluster,
|
||||
createStarDot,
|
||||
@@ -370,7 +371,8 @@ export function syncShips(context: SceneSyncContext, ships: ShipSnapshot[], tick
|
||||
for (const ship of ships) {
|
||||
const mesh = createShipMesh(ship, context.shipSize(ship), context.shipLength(ship), context.shipPresentationColor(ship));
|
||||
const shipColor = context.shipPresentationColor(ship);
|
||||
const icon = createTacticalIcon(context.documentRef, shipColor, 90);
|
||||
const iconBaseScale = 78;
|
||||
const icon = createShipTacticalIcon(context.documentRef, shipColor, iconBaseScale);
|
||||
const localPosition = toThreeVector(ship.localPosition);
|
||||
const displayPos = toSystemPos(localPosition);
|
||||
mesh.setPosition(displayPos);
|
||||
@@ -386,6 +388,7 @@ export function syncShips(context: SceneSyncContext, ships: ShipSnapshot[], tick
|
||||
systemId: ship.systemId,
|
||||
mesh,
|
||||
icon,
|
||||
iconBaseScale,
|
||||
startPosition: localPosition.clone(),
|
||||
authoritativePosition: localPosition.clone(),
|
||||
targetPosition: toThreeVector(ship.targetLocalPosition),
|
||||
|
||||
@@ -46,7 +46,18 @@ export function describeHoverLabel(world: WorldState | undefined, item: Selectab
|
||||
}
|
||||
|
||||
if (item.kind === "ship") {
|
||||
return world.ships.get(item.id)?.label ?? item.id;
|
||||
const ship = world.ships.get(item.id);
|
||||
if (!ship) {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
const lines = [
|
||||
ship.label,
|
||||
`Behavior ${describeShipBehavior(ship)}`,
|
||||
`State ${describeShipState(world, ship)}`,
|
||||
`Order ${describeShipOrder(ship)}`,
|
||||
];
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
if (item.kind === "station") {
|
||||
@@ -373,6 +384,27 @@ export function describeShipObjective(objective: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
export function describeShipBehavior(ship: ShipSnapshot): string {
|
||||
return ship.behaviorPhase
|
||||
? `${ship.defaultBehaviorKind} · ${ship.behaviorPhase}`
|
||||
: ship.defaultBehaviorKind;
|
||||
}
|
||||
|
||||
export function describeShipOrder(ship: ShipSnapshot): string {
|
||||
const orderParts: string[] = [];
|
||||
if (ship.orderKind) {
|
||||
orderParts.push(ship.orderKind);
|
||||
}
|
||||
if (ship.commanderObjective) {
|
||||
orderParts.push(describeShipObjective(ship.commanderObjective));
|
||||
}
|
||||
if (orderParts.length > 0) {
|
||||
return orderParts.join(" · ");
|
||||
}
|
||||
|
||||
return describeControllerTask(ship.controllerTaskKind);
|
||||
}
|
||||
|
||||
export function describeShipCurrentAction(ship: ShipSnapshot): { label: string; progress: number } | undefined {
|
||||
if (!ship.currentAction) {
|
||||
return undefined;
|
||||
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
*/
|
||||
export class SystemLayer {
|
||||
readonly scene = new THREE.Scene();
|
||||
readonly camera = new THREE.PerspectiveCamera(50, 1, 0.0001, 300000);
|
||||
readonly camera = new THREE.PerspectiveCamera(50, 1, 0.000005, 300000);
|
||||
|
||||
readonly celestialGroup = new THREE.Group();
|
||||
readonly nodeGroup = new THREE.Group();
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface ShipVisual {
|
||||
systemId: string;
|
||||
mesh: SceneNode;
|
||||
icon: SceneNode;
|
||||
iconBaseScale: number;
|
||||
startPosition: THREE.Vector3;
|
||||
authoritativePosition: THREE.Vector3;
|
||||
targetPosition: THREE.Vector3;
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
updateSystemStarPresentation,
|
||||
getAnimatedShipLocalPosition,
|
||||
iconWorldScale,
|
||||
MIN_ICON_PIXELS,
|
||||
MAX_ICON_PIXELS,
|
||||
} from "./viewerPresentation";
|
||||
import { rawObject } from "./viewerScenePrimitives";
|
||||
import type {
|
||||
@@ -94,8 +96,16 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
|
||||
visual.mesh.setPosition(displayPosition);
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
const shipVisible = isShipVisible(renderMode, context.activeSystemId, ship);
|
||||
visual.mesh.setVisible(shipVisible);
|
||||
visual.icon.setVisible(shipVisible && rawObject(visual.icon).visible);
|
||||
const distToShip = context.camera.position.distanceTo(displayPosition);
|
||||
const useTacticalIcon = renderMode !== "local" || distToShip > 0.012;
|
||||
const iconScale = THREE.MathUtils.clamp(
|
||||
visual.iconBaseScale,
|
||||
iconWorldScale(distToShip, context.camera, MIN_ICON_PIXELS),
|
||||
iconWorldScale(distToShip, context.camera, MAX_ICON_PIXELS + 10),
|
||||
);
|
||||
visual.icon.setScaleScalar(iconScale);
|
||||
visual.mesh.setVisible(shipVisible && !useTacticalIcon);
|
||||
visual.icon.setVisible(shipVisible && useTacticalIcon);
|
||||
const desiredHeading = resolveShipHeading(visual, worldPosition, context.orbitYaw);
|
||||
if (desiredHeading.lengthSq() > 0.01) {
|
||||
visual.mesh.lookAt(rawObject(visual.mesh).position.clone().add(desiredHeading));
|
||||
|
||||
Reference in New Issue
Block a user