Simplify simulation and restore ship browser
This commit is contained in:
60
SESSION.md
60
SESSION.md
@@ -8,7 +8,7 @@ The codebase is still TypeScript + Three.js on Vite, with authored catalogs unde
|
|||||||
|
|
||||||
- procedural universe generation
|
- procedural universe generation
|
||||||
- autonomous faction behavior
|
- autonomous faction behavior
|
||||||
- fleet / wing hierarchy
|
- direct per-ship faction control
|
||||||
- economic production loops
|
- economic production loops
|
||||||
- pirate harassment
|
- pirate harassment
|
||||||
- strategic system control
|
- strategic system control
|
||||||
@@ -24,12 +24,6 @@ The current build includes:
|
|||||||
- rich central systems that factions contest for control
|
- rich central systems that factions contest for control
|
||||||
- faction-owned stations, ships, inventories, and combat stats
|
- faction-owned stations, ships, inventories, and combat stats
|
||||||
- autonomous shipbuilding and limited outpost growth
|
- autonomous shipbuilding and limited outpost growth
|
||||||
- fleet and wing structure with behaviors such as:
|
|
||||||
- `command`
|
|
||||||
- `screen`
|
|
||||||
- `mining`
|
|
||||||
- `logistics`
|
|
||||||
- `escort`
|
|
||||||
- observer controls for camera orbit, pan, focus, and inspection
|
- observer controls for camera orbit, pan, focus, and inspection
|
||||||
|
|
||||||
## Major Gameplay / Sim Systems
|
## Major Gameplay / Sim Systems
|
||||||
@@ -67,23 +61,14 @@ The current build includes:
|
|||||||
|
|
||||||
### High-Level AI / Delegation
|
### High-Level AI / Delegation
|
||||||
|
|
||||||
- Faction AI now acts at a strategic level instead of directly micromanaging every ship.
|
- Faction AI now acts at a strategic level and issues direct orders to ships.
|
||||||
- Empire AI chooses high-level goals such as:
|
- Empire AI chooses high-level goals such as:
|
||||||
- secure home and mining space
|
- secure home and mining space
|
||||||
- contest central systems
|
- contest central systems
|
||||||
- assign industrial fleets to mining loops
|
- send miners to resource systems
|
||||||
- Pirate AI chooses raid targets and dispatches fleets into hostile space.
|
- send military ships to rally and patrol targets
|
||||||
- Fleet-level orders are now the intended command boundary between:
|
- Pirate AI chooses raid targets and moves military ships into hostile space.
|
||||||
- faction strategy
|
- The fleet / wing layer has been removed from both simulation and UI.
|
||||||
- fleet / wing execution
|
|
||||||
- This work was specifically done to stop faction AI from stomping `screen` behavior with raw ship move orders.
|
|
||||||
|
|
||||||
### Fleets / Wings
|
|
||||||
|
|
||||||
- Fleet creation now groups ships per faction and role in `src/game/fleet/runtime.ts`.
|
|
||||||
- War fleets and industry fleets are generated from faction-owned ships.
|
|
||||||
- Wing behaviors remain meaningful at the tactical layer.
|
|
||||||
- `screen` is intended to remain subordinate to fleet command rather than independent faction micromanagement.
|
|
||||||
|
|
||||||
### Economy / Production
|
### Economy / Production
|
||||||
|
|
||||||
@@ -106,11 +91,14 @@ The current build includes:
|
|||||||
## Starting State
|
## Starting State
|
||||||
|
|
||||||
- Empires now start very small for easier debugging and growth observation.
|
- Empires now start very small for easier debugging and growth observation.
|
||||||
- Each empire currently starts with only 3 ships:
|
- Each empire currently starts with:
|
||||||
- 1 frigate
|
|
||||||
- 1 hauler
|
|
||||||
- 1 miner
|
- 1 miner
|
||||||
- Pirates still start with small raiding groups.
|
- 1 manufactory
|
||||||
|
- 1 refinery
|
||||||
|
- Each pirate faction currently starts with:
|
||||||
|
- 1 frigate
|
||||||
|
- 1 trade hub
|
||||||
|
- This is a bootstrap-oriented setup: factions mine first, then try to grow from minimal infrastructure.
|
||||||
|
|
||||||
## UI / UX State
|
## UI / UX State
|
||||||
|
|
||||||
@@ -123,7 +111,6 @@ The current build includes:
|
|||||||
- status line
|
- status line
|
||||||
- horizontally scrolling cards for selected entities
|
- horizontally scrolling cards for selected entities
|
||||||
- fallback observer details when nothing specific is selected
|
- fallback observer details when nothing specific is selected
|
||||||
- Fleet launch controls were removed from the main HUD.
|
|
||||||
- A dedicated `Debug` window now contains the `New Universe` button.
|
- A dedicated `Debug` window now contains the `New Universe` button.
|
||||||
|
|
||||||
### Selection / Inspection
|
### Selection / Inspection
|
||||||
@@ -136,19 +123,24 @@ The current build includes:
|
|||||||
- stations
|
- stations
|
||||||
- Double-click centers / focuses the clicked target.
|
- Double-click centers / focuses the clicked target.
|
||||||
- Multiple ship selections render as horizontal cards in the bottom dock.
|
- Multiple ship selections render as horizontal cards in the bottom dock.
|
||||||
- Fleet window tree selection still works.
|
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
- Generic draggable / resizable app windows still exist.
|
- Generic draggable / resizable app windows still exist.
|
||||||
- Main windows currently in use:
|
- Main windows currently in use:
|
||||||
- `Fleet Command`
|
- `Ships`
|
||||||
- `Debug`
|
- `Debug`
|
||||||
|
- The `Ships` window:
|
||||||
|
- lists ships grouped by faction
|
||||||
|
- selects a ship on click
|
||||||
|
- focuses a ship on double click
|
||||||
|
- focuses a faction home system when clicking that faction header
|
||||||
|
|
||||||
### Strategic Rendering
|
### Strategic Rendering
|
||||||
|
|
||||||
- Strategic overlay and minimap infrastructure still exist.
|
- Strategic overlay and minimap infrastructure still exist.
|
||||||
- The minimap canvas is still created for renderer use, but it is no longer shown in the visible HUD.
|
- The minimap canvas is still created for renderer use, but it is no longer shown in the visible HUD.
|
||||||
|
- Fleet link overlays and fleet counters were removed along with the fleet system.
|
||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
|
|
||||||
@@ -160,10 +152,10 @@ The current build includes:
|
|||||||
- `Middle Drag`: orbit camera
|
- `Middle Drag`: orbit camera
|
||||||
- `Shift + Middle Drag`: pan camera
|
- `Shift + Middle Drag`: pan camera
|
||||||
- `Mouse Wheel` or `-` / `=`: zoom
|
- `Mouse Wheel` or `-` / `=`: zoom
|
||||||
- `W A S D`: pan camera
|
- `W A S D`: pan camera using the same motion as `Shift + Middle Drag`
|
||||||
- `Q / E`: rotate camera
|
- `Q / E`: rotate camera
|
||||||
- `F`: focus current selection
|
- `F`: focus current selection
|
||||||
- `G`: toggle fleet command window
|
- `G`: toggle ships window
|
||||||
- `Tab`: jump camera between systems
|
- `Tab`: jump camera between systems
|
||||||
|
|
||||||
## Technical Notes
|
## Technical Notes
|
||||||
@@ -173,8 +165,6 @@ The current build includes:
|
|||||||
- `src/game/world/worldFactory.ts`
|
- `src/game/world/worldFactory.ts`
|
||||||
- Procedural universe generation:
|
- Procedural universe generation:
|
||||||
- `src/game/world/universeGenerator.ts`
|
- `src/game/world/universeGenerator.ts`
|
||||||
- Fleet composition helpers:
|
|
||||||
- `src/game/fleet/runtime.ts`
|
|
||||||
- Selection state:
|
- Selection state:
|
||||||
- `src/game/state/selectionManager.ts`
|
- `src/game/state/selectionManager.ts`
|
||||||
- HUD / presentation:
|
- HUD / presentation:
|
||||||
@@ -191,16 +181,16 @@ The current build includes:
|
|||||||
- Economic logistics are still abstracted heavily.
|
- Economic logistics are still abstracted heavily.
|
||||||
- Ship construction is recipe-gated but still simplified.
|
- Ship construction is recipe-gated but still simplified.
|
||||||
- Stations consume pooled faction stock rather than explicit transport delivery chains.
|
- Stations consume pooled faction stock rather than explicit transport delivery chains.
|
||||||
- Fleet window remains useful, but the overall UI is now only partially refit for observer mode.
|
- Bootstrap progression is still constrained by the current station recipe / stock model.
|
||||||
|
- The ships window is useful for inspection, but the overall UI is still only partially refit for observer mode.
|
||||||
- There is still no persistence layer for window layouts, saves, or generated universe seeds.
|
- There is still no persistence layer for window layouts, saves, or generated universe seeds.
|
||||||
|
|
||||||
## Suggested Next Steps
|
## Suggested Next Steps
|
||||||
|
|
||||||
- Extract faction strategy into a dedicated AI / planning module
|
- Extract faction strategy into a dedicated AI / planning module
|
||||||
- Extract fleet order execution into its own gameplay system
|
|
||||||
- Separate economic simulation from UI and rendering concerns
|
- Separate economic simulation from UI and rendering concerns
|
||||||
- Improve transport logistics so goods physically move through faction supply chains
|
- Improve transport logistics so goods physically move through faction supply chains
|
||||||
- Add explicit shipyard construction queues and faction production priorities
|
- Add explicit shipyard construction queues and faction production priorities
|
||||||
- Improve combat behavior so `screen`, `escort`, and `command` have stronger distinct tactical roles
|
- Rework bootstrap progression so factions can genuinely grow from near-zero infrastructure
|
||||||
- Add system-level threat, ownership, and economy views for game-master inspection
|
- Add system-level threat, ownership, and economy views for game-master inspection
|
||||||
- Add save/load support for generated universes and long-running simulations
|
- Add save/load support for generated universes and long-running simulations
|
||||||
|
|||||||
@@ -6,20 +6,10 @@ import {
|
|||||||
recipeDefinitions,
|
recipeDefinitions,
|
||||||
shipDefinitionsById,
|
shipDefinitionsById,
|
||||||
} from "./data/catalog";
|
} from "./data/catalog";
|
||||||
import {
|
|
||||||
createDefaultFleets,
|
|
||||||
describeFleetOrder,
|
|
||||||
getFleetCommander,
|
|
||||||
getFleetShipIds,
|
|
||||||
getWingLeader,
|
|
||||||
getWingMembers,
|
|
||||||
} from "./fleet/runtime";
|
|
||||||
import { addShipCargo, getShipCargoAmount, removeShipCargo } from "./state/inventory";
|
import { addShipCargo, getShipCargoAmount, removeShipCargo } from "./state/inventory";
|
||||||
import { SelectionManager } from "./state/selectionManager";
|
import { SelectionManager } from "./state/selectionManager";
|
||||||
import type {
|
import type {
|
||||||
FactionInstance,
|
FactionInstance,
|
||||||
FleetInstance,
|
|
||||||
FleetWingInstance,
|
|
||||||
GameWindowId,
|
GameWindowId,
|
||||||
ResourceNode,
|
ResourceNode,
|
||||||
SelectableTarget,
|
SelectableTarget,
|
||||||
@@ -32,7 +22,7 @@ import type {
|
|||||||
ViewLevel,
|
ViewLevel,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { createHud } from "./ui/hud";
|
import { createHud } from "./ui/hud";
|
||||||
import { getFleetWindowMarkup, getSelectionCardsMarkup, getSelectionDetails, getSelectionTitle } from "./ui/presenters";
|
import { getSelectionCardsMarkup, getSelectionDetails, getSelectionTitle, getShipWindowMarkup } from "./ui/presenters";
|
||||||
import { drawMinimap, drawStrategicOverlay } from "./ui/strategicRenderer";
|
import { drawMinimap, drawStrategicOverlay } from "./ui/strategicRenderer";
|
||||||
import { buildInitialWorld, createShipInstance, createStationInstance } from "./world/worldFactory";
|
import { buildInitialWorld, createShipInstance, createStationInstance } from "./world/worldFactory";
|
||||||
import { generateUniverse } from "./world/universeGenerator";
|
import { generateUniverse } from "./world/universeGenerator";
|
||||||
@@ -70,8 +60,6 @@ export class GameApp {
|
|||||||
private readonly stations: StationInstance[] = [];
|
private readonly stations: StationInstance[] = [];
|
||||||
private readonly nodes: ResourceNode[] = [];
|
private readonly nodes: ResourceNode[] = [];
|
||||||
private readonly systems: SolarSystemInstance[] = [];
|
private readonly systems: SolarSystemInstance[] = [];
|
||||||
private readonly fleets: FleetInstance[] = [];
|
|
||||||
private readonly fleetsById = new Map<string, FleetInstance>();
|
|
||||||
private readonly factions: FactionInstance[] = [];
|
private readonly factions: FactionInstance[] = [];
|
||||||
private readonly factionsById = new Map<string, FactionInstance>();
|
private readonly factionsById = new Map<string, FactionInstance>();
|
||||||
|
|
||||||
@@ -92,7 +80,6 @@ export class GameApp {
|
|||||||
private cameraDragPointerId?: number;
|
private cameraDragPointerId?: number;
|
||||||
private cameraDragLast?: THREE.Vector2;
|
private cameraDragLast?: THREE.Vector2;
|
||||||
private stationIdCounter = 0;
|
private stationIdCounter = 0;
|
||||||
private activeFleetId?: string;
|
|
||||||
private readonly windowState: Record<GameWindowId, boolean> = {
|
private readonly windowState: Record<GameWindowId, boolean> = {
|
||||||
"fleet-command": true,
|
"fleet-command": true,
|
||||||
"ship-designer": false,
|
"ship-designer": false,
|
||||||
@@ -113,11 +100,9 @@ export class GameApp {
|
|||||||
private readonly fleetWindowEl: HTMLDivElement;
|
private readonly fleetWindowEl: HTMLDivElement;
|
||||||
private readonly fleetWindowBodyEl: HTMLDivElement;
|
private readonly fleetWindowBodyEl: HTMLDivElement;
|
||||||
private readonly fleetWindowTitleEl: HTMLHeadingElement;
|
private readonly fleetWindowTitleEl: HTMLHeadingElement;
|
||||||
private readonly fleetWindowSubtitleEl: HTMLParagraphElement;
|
|
||||||
private readonly debugWindowEl: HTMLDivElement;
|
private readonly debugWindowEl: HTMLDivElement;
|
||||||
private readonly sessionActionsEl: HTMLDivElement;
|
private readonly sessionActionsEl: HTMLDivElement;
|
||||||
private universe: UniverseDefinition;
|
private universe: UniverseDefinition;
|
||||||
private fleetRefreshNeeded = false;
|
|
||||||
|
|
||||||
constructor(container: HTMLElement) {
|
constructor(container: HTMLElement) {
|
||||||
this.container = container;
|
this.container = container;
|
||||||
@@ -139,7 +124,6 @@ export class GameApp {
|
|||||||
this.container.append(this.renderer.domElement);
|
this.container.append(this.renderer.domElement);
|
||||||
const hud = createHud(this.container, {
|
const hud = createHud(this.container, {
|
||||||
onWindowAction: (action) => this.handleWindowAction(action),
|
onWindowAction: (action) => this.handleWindowAction(action),
|
||||||
onFleetAction: (action, fleetId) => this.handleFleetAction(action, fleetId),
|
|
||||||
onSelectionAction: (kind, id) => this.handleWindowSelection(kind, id),
|
onSelectionAction: (kind, id) => this.handleWindowSelection(kind, id),
|
||||||
});
|
});
|
||||||
this.detailsEl = hud.details;
|
this.detailsEl = hud.details;
|
||||||
@@ -155,7 +139,6 @@ export class GameApp {
|
|||||||
this.fleetWindowEl = hud.fleetWindow;
|
this.fleetWindowEl = hud.fleetWindow;
|
||||||
this.fleetWindowBodyEl = hud.fleetWindowBody;
|
this.fleetWindowBodyEl = hud.fleetWindowBody;
|
||||||
this.fleetWindowTitleEl = hud.fleetWindowTitle;
|
this.fleetWindowTitleEl = hud.fleetWindowTitle;
|
||||||
this.fleetWindowSubtitleEl = hud.fleetWindowSubtitle;
|
|
||||||
this.debugWindowEl = hud.debugWindow;
|
this.debugWindowEl = hud.debugWindow;
|
||||||
this.sessionActionsEl = hud.sessionActions;
|
this.sessionActionsEl = hud.sessionActions;
|
||||||
|
|
||||||
@@ -197,7 +180,6 @@ export class GameApp {
|
|||||||
this.stationIdCounter = this.stations.length;
|
this.stationIdCounter = this.stations.length;
|
||||||
|
|
||||||
this.initializeFactions();
|
this.initializeFactions();
|
||||||
this.initializeFleets();
|
|
||||||
this.applyViewLevel();
|
this.applyViewLevel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,14 +191,10 @@ export class GameApp {
|
|||||||
this.stations.length = 0;
|
this.stations.length = 0;
|
||||||
this.nodes.length = 0;
|
this.nodes.length = 0;
|
||||||
this.systems.length = 0;
|
this.systems.length = 0;
|
||||||
this.fleets.length = 0;
|
|
||||||
this.fleetsById.clear();
|
|
||||||
this.factions.length = 0;
|
this.factions.length = 0;
|
||||||
this.factionsById.clear();
|
this.factionsById.clear();
|
||||||
this.followShipId = undefined;
|
this.followShipId = undefined;
|
||||||
this.activeFleetId = undefined;
|
|
||||||
this.buildMode = false;
|
this.buildMode = false;
|
||||||
this.fleetRefreshNeeded = false;
|
|
||||||
this.selectedSystemIndex = 0;
|
this.selectedSystemIndex = 0;
|
||||||
this.stationIdCounter = 0;
|
this.stationIdCounter = 0;
|
||||||
this.marqueeStart = undefined;
|
this.marqueeStart = undefined;
|
||||||
@@ -460,7 +438,6 @@ export class GameApp {
|
|||||||
} else if (!this.selectionManager.hasShip(target.ship)) {
|
} else if (!this.selectionManager.hasShip(target.ship)) {
|
||||||
this.selectionManager.addShip(target.ship);
|
this.selectionManager.addShip(target.ship);
|
||||||
}
|
}
|
||||||
this.syncActiveFleetFromSelection();
|
|
||||||
}
|
}
|
||||||
if (target?.kind === "station") {
|
if (target?.kind === "station") {
|
||||||
this.selectionManager.setStation(target.station);
|
this.selectionManager.setStation(target.station);
|
||||||
@@ -592,17 +569,6 @@ export class GameApp {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeFleets() {
|
|
||||||
const previousActiveFleetId = this.activeFleetId;
|
|
||||||
this.fleets.length = 0;
|
|
||||||
this.fleetsById.clear();
|
|
||||||
createDefaultFleets(this.ships).forEach((fleet) => {
|
|
||||||
this.fleets.push(fleet);
|
|
||||||
this.fleetsById.set(fleet.id, fleet);
|
|
||||||
});
|
|
||||||
this.activeFleetId = this.fleetsById.has(previousActiveFleetId ?? "") ? previousActiveFleetId : this.fleets[0]?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private makePatrolPoints(systemId: string) {
|
private makePatrolPoints(systemId: string) {
|
||||||
const route = this.universe.scenario.patrolRoutes.find((candidate) => candidate.systemId === systemId);
|
const route = this.universe.scenario.patrolRoutes.find((candidate) => candidate.systemId === systemId);
|
||||||
if (!route) {
|
if (!route) {
|
||||||
@@ -617,180 +583,25 @@ export class GameApp {
|
|||||||
return route.points.map((point) => new THREE.Vector3(...point).setY(gameBalance.yPlane));
|
return route.points.map((point) => new THREE.Vector3(...point).setY(gameBalance.yPlane));
|
||||||
}
|
}
|
||||||
|
|
||||||
private setFleetMineOrder(fleet: FleetInstance, nodeSystemId: string, refinerySystemId: string) {
|
private getFactionShips(factionId: string) {
|
||||||
fleet.order = { kind: "mine", nodeSystemId, refinerySystemId };
|
return this.ships.filter((ship) => ship.factionId === factionId);
|
||||||
this.applyFleetOrder(fleet);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private setFleetPatrolOrder(fleet: FleetInstance, systemId: string) {
|
private getFactionMilitaryShips(factionId: string) {
|
||||||
fleet.order = {
|
return this.getFactionShips(factionId).filter((ship) => ship.definition.role === "military");
|
||||||
kind: "patrol",
|
|
||||||
systemId,
|
|
||||||
points: this.makePatrolPoints(systemId),
|
|
||||||
index: 0,
|
|
||||||
};
|
|
||||||
this.applyFleetOrder(fleet);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private setFleetIdleOrder(fleet: FleetInstance) {
|
private getFactionIndustryShips(factionId: string) {
|
||||||
fleet.order = { kind: "idle" };
|
return this.getFactionShips(factionId).filter((ship) => ship.definition.role !== "military");
|
||||||
this.applyFleetOrder(fleet);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private issueFleetMoveOrder(fleet: FleetInstance, destination: THREE.Vector3) {
|
private makeEscortOffset(index: number, spacing = 26) {
|
||||||
const system = this.findNearestSystem(destination);
|
if (index === 0) {
|
||||||
fleet.order = {
|
return new THREE.Vector3(0, 0, 22);
|
||||||
kind: "move",
|
|
||||||
destination: destination.clone().setY(gameBalance.yPlane),
|
|
||||||
systemId: system.definition.id,
|
|
||||||
};
|
|
||||||
this.applyFleetOrder(fleet);
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyFleetOrder(fleet: FleetInstance) {
|
|
||||||
const commander = getFleetCommander(fleet, this.shipsById);
|
|
||||||
if (!commander) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fleet.wings.forEach((wing) => {
|
|
||||||
switch (fleet.order.kind) {
|
|
||||||
case "idle":
|
|
||||||
this.applyFleetIdleDirective(fleet, wing, commander);
|
|
||||||
break;
|
|
||||||
case "move":
|
|
||||||
this.applyFleetMoveDirective(fleet, wing, commander);
|
|
||||||
break;
|
|
||||||
case "patrol":
|
|
||||||
this.applyFleetPatrolDirective(fleet, wing, commander);
|
|
||||||
break;
|
|
||||||
case "mine":
|
|
||||||
this.applyFleetMiningDirective(fleet, wing, commander);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fleet.systemId = commander.systemId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyFleetIdleDirective(fleet: FleetInstance, wing: FleetWingInstance, commander: ShipInstance) {
|
|
||||||
if (wing.behavior === "command") {
|
|
||||||
const members = getWingMembers(wing, this.shipsById);
|
|
||||||
members.forEach((ship) => {
|
|
||||||
if (ship.id === commander.id) {
|
|
||||||
ship.order = { kind: "idle" };
|
|
||||||
ship.state = "idle";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setEscortOrder(ship, commander, ship.formationOffset.clone());
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.assignWingFollowOrder(fleet, wing, this.resolveWingAnchor(fleet, wing) ?? commander);
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyFleetMoveDirective(fleet: FleetInstance, wing: FleetWingInstance, commander: ShipInstance) {
|
|
||||||
const order = fleet.order;
|
|
||||||
if (order.kind !== "move") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (wing.behavior === "command") {
|
|
||||||
const members = getWingMembers(wing, this.shipsById);
|
|
||||||
members.forEach((ship) => {
|
|
||||||
if (ship.id === commander.id) {
|
|
||||||
this.issueMoveOrder(ship, order.destination.clone());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setEscortOrder(ship, commander, ship.formationOffset.clone());
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.assignWingFollowOrder(fleet, wing, this.resolveWingAnchor(fleet, wing) ?? commander);
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyFleetPatrolDirective(fleet: FleetInstance, wing: FleetWingInstance, commander: ShipInstance) {
|
|
||||||
const order = fleet.order;
|
|
||||||
if (order.kind !== "patrol") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wing.behavior === "command") {
|
|
||||||
this.setPatrolOrder(commander, order.points, order.index, order.systemId);
|
|
||||||
getWingMembers(wing, this.shipsById)
|
|
||||||
.filter((ship) => ship.id !== commander.id)
|
|
||||||
.forEach((ship) => this.setEscortOrder(ship, commander, ship.formationOffset.clone()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wing.behavior === "screen") {
|
|
||||||
getWingMembers(wing, this.shipsById).forEach((ship, index) =>
|
|
||||||
this.setPatrolOrder(ship, order.points, (order.index + index) % order.points.length, order.systemId),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.assignWingFollowOrder(fleet, wing, this.resolveWingAnchor(fleet, wing) ?? commander);
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyFleetMiningDirective(fleet: FleetInstance, wing: FleetWingInstance, commander: ShipInstance) {
|
|
||||||
const order = fleet.order;
|
|
||||||
if (order.kind !== "mine") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wing.behavior === "command") {
|
|
||||||
const targetSystem = this.getSystem(order.nodeSystemId);
|
|
||||||
const rally = targetSystem.center.clone().add(new THREE.Vector3(-140, 0, 120));
|
|
||||||
this.issueMoveOrder(commander, rally);
|
|
||||||
getWingMembers(wing, this.shipsById)
|
|
||||||
.filter((ship) => ship.id !== commander.id)
|
|
||||||
.forEach((ship) => this.setEscortOrder(ship, commander, ship.formationOffset.clone()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wing.behavior === "mining") {
|
|
||||||
const refinery = this.findRefinery(order.refinerySystemId);
|
|
||||||
getWingMembers(wing, this.shipsById).forEach((ship) =>
|
|
||||||
this.assignMineOrder(ship, this.findBestMiningNode(order.nodeSystemId), refinery),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const anchor = this.resolveWingAnchor(fleet, wing) ?? commander;
|
|
||||||
const multiplier = wing.behavior === "logistics" ? 1.8 : 1.2;
|
|
||||||
this.assignWingFollowOrder(fleet, wing, anchor, multiplier);
|
|
||||||
}
|
|
||||||
|
|
||||||
private resolveWingAnchor(fleet: FleetInstance, wing: FleetWingInstance) {
|
|
||||||
if (wing.parentWingId) {
|
|
||||||
const parentWing = fleet.wings.find((candidate) => candidate.id === wing.parentWingId);
|
|
||||||
if (parentWing) {
|
|
||||||
return getWingLeader(parentWing, this.shipsById);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return getFleetCommander(fleet, this.shipsById);
|
|
||||||
}
|
|
||||||
|
|
||||||
private assignWingFollowOrder(fleet: FleetInstance, wing: FleetWingInstance, anchor: ShipInstance, offsetScale = 1) {
|
|
||||||
getWingMembers(wing, this.shipsById).forEach((ship) => {
|
|
||||||
if (ship.id === anchor.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const localOffset = ship.formationOffset.clone().multiplyScalar(offsetScale);
|
|
||||||
const twist = ship.behavior === "escort" ? new THREE.Vector3(0, 0, -18) : new THREE.Vector3(0, 0, 18);
|
|
||||||
this.setEscortOrder(ship, anchor, localOffset.add(twist));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (anchor.fleetId !== fleet.id && wing.behavior !== "logistics") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const leader = getWingLeader(wing, this.shipsById);
|
|
||||||
if (leader && leader.id !== anchor.id && wing.behavior !== "escort" && wing.behavior !== "logistics") {
|
|
||||||
this.setEscortOrder(leader, anchor, leader.formationOffset.clone().multiplyScalar(offsetScale));
|
|
||||||
}
|
}
|
||||||
|
const side = index % 2 === 0 ? 1 : -1;
|
||||||
|
const rank = Math.ceil(index / 2);
|
||||||
|
return new THREE.Vector3(side * rank * spacing, 0, 22 + rank * 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateFactionSimulation(delta: number) {
|
private updateFactionSimulation(delta: number) {
|
||||||
@@ -832,43 +643,45 @@ export class GameApp {
|
|||||||
const threatenedSystemId = this.findThreatenedSystem(faction.definition.id);
|
const threatenedSystemId = this.findThreatenedSystem(faction.definition.id);
|
||||||
const centralTarget = this.pickCentralTargetSystem(faction);
|
const centralTarget = this.pickCentralTargetSystem(faction);
|
||||||
const militaryTargetSystemId = threatenedSystemId ?? centralTarget ?? faction.definition.homeSystemId;
|
const militaryTargetSystemId = threatenedSystemId ?? centralTarget ?? faction.definition.homeSystemId;
|
||||||
const industryFleet = this.getFactionIndustryFleet(faction.definition.id);
|
const miningSystemId = miningSystems[0] ?? faction.definition.miningSystemId ?? faction.definition.homeSystemId;
|
||||||
if (industryFleet) {
|
const refinery = this.findRefinery(faction.definition.miningSystemId ?? faction.definition.homeSystemId, faction.definition.id);
|
||||||
const miningSystemId = miningSystems[0] ?? faction.definition.miningSystemId ?? faction.definition.homeSystemId;
|
const miners = this.getFactionIndustryShips(faction.definition.id).filter((ship) => ship.definition.role === "mining");
|
||||||
this.setFleetMineOrder(
|
const transports = this.getFactionIndustryShips(faction.definition.id).filter((ship) => ship.definition.role === "transport");
|
||||||
industryFleet,
|
miners.forEach((ship) => this.assignMineOrder(ship, this.findBestMiningNode(miningSystemId), refinery));
|
||||||
miningSystemId,
|
transports.forEach((ship, index) => {
|
||||||
faction.definition.miningSystemId ?? faction.definition.homeSystemId,
|
const anchor = miners[index % Math.max(1, miners.length)];
|
||||||
);
|
if (anchor) {
|
||||||
}
|
this.setEscortOrder(ship, anchor, this.makeEscortOffset(index));
|
||||||
|
} else {
|
||||||
|
this.setPatrolOrder(ship, this.makePatrolPoints(faction.definition.homeSystemId), index, faction.definition.homeSystemId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.getFactionWarFleets(faction.definition.id).forEach((fleet) => {
|
const targetSystem = this.getSystem(militaryTargetSystemId);
|
||||||
if (fleet.systemId !== militaryTargetSystemId) {
|
const rally = targetSystem.center.clone().add(new THREE.Vector3(-140, 0, 120));
|
||||||
const targetSystem = this.getSystem(militaryTargetSystemId);
|
this.getFactionMilitaryShips(faction.definition.id).forEach((ship, index) => {
|
||||||
const rally = targetSystem.center.clone().add(new THREE.Vector3(-140, 0, 120));
|
if (ship.systemId !== militaryTargetSystemId) {
|
||||||
this.issueFleetMoveOrder(fleet, rally);
|
this.issueMoveOrder(ship, rally.clone().add(this.makeEscortOffset(index, 18)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setFleetPatrolOrder(fleet, militaryTargetSystemId);
|
this.setPatrolOrder(ship, this.makePatrolPoints(militaryTargetSystemId), index, militaryTargetSystemId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private commandPirateFaction(faction: FactionInstance) {
|
private commandPirateFaction(faction: FactionInstance) {
|
||||||
const targetSystemId = faction.definition.targetSystemIds[0] ?? faction.definition.homeSystemId;
|
const targetSystemId = faction.definition.targetSystemIds[0] ?? faction.definition.homeSystemId;
|
||||||
const targetSystem = this.getSystem(targetSystemId);
|
const targetSystem = this.getSystem(targetSystemId);
|
||||||
this.getFactionWarFleets(faction.definition.id).forEach((fleet) => {
|
const raidPoint = targetSystem.center.clone().add(new THREE.Vector3(120, 0, 160));
|
||||||
const raidPoint = targetSystem.center.clone().add(new THREE.Vector3(120, 0, 160));
|
this.getFactionMilitaryShips(faction.definition.id).forEach((ship, index) => {
|
||||||
if (fleet.systemId !== targetSystemId) {
|
if (ship.systemId !== targetSystemId) {
|
||||||
this.issueFleetMoveOrder(fleet, raidPoint);
|
this.issueMoveOrder(ship, raidPoint.clone().add(this.makeEscortOffset(index, 20)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setFleetPatrolOrder(fleet, targetSystemId);
|
this.setPatrolOrder(ship, this.makePatrolPoints(targetSystemId), index, targetSystemId);
|
||||||
|
});
|
||||||
|
this.getFactionIndustryShips(faction.definition.id).forEach((ship, index) => {
|
||||||
|
this.setPatrolOrder(ship, this.makePatrolPoints(faction.definition.homeSystemId), index, faction.definition.homeSystemId);
|
||||||
});
|
});
|
||||||
|
|
||||||
const industryFleet = this.getFactionIndustryFleet(faction.definition.id);
|
|
||||||
if (industryFleet) {
|
|
||||||
this.setFleetPatrolOrder(industryFleet, faction.definition.homeSystemId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateSystemControl(delta: number) {
|
private updateSystemControl(delta: number) {
|
||||||
@@ -981,17 +794,6 @@ export class GameApp {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private refreshFleets() {
|
|
||||||
if (!this.fleetRefreshNeeded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.fleetRefreshNeeded = false;
|
|
||||||
this.initializeFleets();
|
|
||||||
this.factions.forEach((faction) => {
|
|
||||||
faction.commandTick = 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private tick() {
|
private tick() {
|
||||||
const delta = Math.min(this.clock.getDelta(), 0.033);
|
const delta = Math.min(this.clock.getDelta(), 0.033);
|
||||||
const elapsed = this.clock.elapsedTime;
|
const elapsed = this.clock.elapsedTime;
|
||||||
@@ -1000,14 +802,7 @@ export class GameApp {
|
|||||||
this.updateFactionSimulation(delta);
|
this.updateFactionSimulation(delta);
|
||||||
this.updateShips(delta, elapsed);
|
this.updateShips(delta, elapsed);
|
||||||
this.updateCombat(delta);
|
this.updateCombat(delta);
|
||||||
this.fleets.forEach((fleet) => {
|
|
||||||
const commander = getFleetCommander(fleet, this.shipsById);
|
|
||||||
if (commander) {
|
|
||||||
fleet.systemId = commander.systemId;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.updateSystems(delta);
|
this.updateSystems(delta);
|
||||||
this.refreshFleets();
|
|
||||||
this.applyViewLevel();
|
this.applyViewLevel();
|
||||||
if (this.selection.length > 0 || this.selectedStation || this.selectedSystem || this.selectedPlanet || this.followShipId) {
|
if (this.selection.length > 0 || this.selectedStation || this.selectedSystem || this.selectedPlanet || this.followShipId) {
|
||||||
this.updateHud();
|
this.updateHud();
|
||||||
@@ -1026,28 +821,22 @@ export class GameApp {
|
|||||||
this.selectedSystemIndex = this.systems.findIndex((system) => system.definition.id === followedShip.systemId);
|
this.selectedSystemIndex = this.systems.findIndex((system) => system.definition.id === followedShip.systemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const forward = new THREE.Vector3();
|
|
||||||
this.camera.getWorldDirection(forward);
|
|
||||||
forward.y = 0;
|
|
||||||
forward.normalize();
|
|
||||||
const right = new THREE.Vector3().crossVectors(forward, new THREE.Vector3(0, 1, 0)).normalize();
|
|
||||||
|
|
||||||
const panSpeed = Math.max(80, this.camera.position.distanceTo(focus) * 0.7) * delta;
|
const panSpeed = Math.max(80, this.camera.position.distanceTo(focus) * 0.7) * delta;
|
||||||
let manualCameraInput = false;
|
let manualCameraInput = false;
|
||||||
if (this.keyState.has("w")) {
|
if (this.keyState.has("w")) {
|
||||||
focus.add(forward.clone().multiplyScalar(panSpeed));
|
this.panCamera(0, panSpeed / Math.max(0.2, this.camera.position.distanceTo(focus) * 0.0014));
|
||||||
manualCameraInput = true;
|
manualCameraInput = true;
|
||||||
}
|
}
|
||||||
if (this.keyState.has("s")) {
|
if (this.keyState.has("s")) {
|
||||||
focus.add(forward.clone().multiplyScalar(-panSpeed));
|
this.panCamera(0, -panSpeed / Math.max(0.2, this.camera.position.distanceTo(focus) * 0.0014));
|
||||||
manualCameraInput = true;
|
manualCameraInput = true;
|
||||||
}
|
}
|
||||||
if (this.keyState.has("a")) {
|
if (this.keyState.has("a")) {
|
||||||
focus.add(right.clone().multiplyScalar(panSpeed));
|
this.panCamera(panSpeed / Math.max(0.2, this.camera.position.distanceTo(focus) * 0.0014), 0);
|
||||||
manualCameraInput = true;
|
manualCameraInput = true;
|
||||||
}
|
}
|
||||||
if (this.keyState.has("d")) {
|
if (this.keyState.has("d")) {
|
||||||
focus.add(right.clone().multiplyScalar(-panSpeed));
|
this.panCamera(-panSpeed / Math.max(0.2, this.camera.position.distanceTo(focus) * 0.0014), 0);
|
||||||
manualCameraInput = true;
|
manualCameraInput = true;
|
||||||
}
|
}
|
||||||
if (this.keyState.has("q") || this.keyState.has("e")) {
|
if (this.keyState.has("q") || this.keyState.has("e")) {
|
||||||
@@ -1896,7 +1685,7 @@ export class GameApp {
|
|||||||
ship.state = "patrolling";
|
ship.state = "patrolling";
|
||||||
}
|
}
|
||||||
|
|
||||||
private setEscortOrder(ship: ShipInstance, target: ShipInstance, offset = ship.formationOffset.clone()) {
|
private setEscortOrder(ship: ShipInstance, target: ShipInstance, offset = new THREE.Vector3()) {
|
||||||
const angle = (this.ships.indexOf(ship) % 6) * (Math.PI / 3);
|
const angle = (this.ships.indexOf(ship) % 6) * (Math.PI / 3);
|
||||||
const formationOffset =
|
const formationOffset =
|
||||||
offset.lengthSq() > 0 ? offset : new THREE.Vector3(Math.cos(angle) * 32, 0, Math.sin(angle) * 32);
|
offset.lengthSq() > 0 ? offset : new THREE.Vector3(Math.cos(angle) * 32, 0, Math.sin(angle) * 32);
|
||||||
@@ -1912,18 +1701,6 @@ export class GameApp {
|
|||||||
return this.stations.filter((station) => station.factionId === factionId);
|
return this.stations.filter((station) => station.factionId === factionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getFactionFleets(factionId: string) {
|
|
||||||
return this.fleets.filter((fleet) => fleet.factionId === factionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getFactionIndustryFleet(factionId: string) {
|
|
||||||
return this.getFactionFleets(factionId).find((fleet) => fleet.wings.some((wing) => wing.behavior === "mining"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private getFactionWarFleets(factionId: string) {
|
|
||||||
return this.getFactionFleets(factionId).filter((fleet) => !fleet.wings.some((wing) => wing.behavior === "mining"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private getFactionItemAmount(factionId: string, itemId: string) {
|
private getFactionItemAmount(factionId: string, itemId: string) {
|
||||||
return this.findFactionStations(factionId).reduce((total, station) => total + (station.itemStocks[itemId] ?? 0), 0);
|
return this.findFactionStations(factionId).reduce((total, station) => total + (station.itemStocks[itemId] ?? 0), 0);
|
||||||
}
|
}
|
||||||
@@ -2031,7 +1808,6 @@ export class GameApp {
|
|||||||
[...this.selectableTargets.entries()]
|
[...this.selectableTargets.entries()]
|
||||||
.filter(([, target]) => target.kind === "ship" && target.ship.id === ship.id)
|
.filter(([, target]) => target.kind === "ship" && target.ship.id === ship.id)
|
||||||
.forEach(([object]) => this.selectableTargets.delete(object));
|
.forEach(([object]) => this.selectableTargets.delete(object));
|
||||||
this.fleetRefreshNeeded = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private tryBuildShipForFaction(faction: FactionInstance) {
|
private tryBuildShipForFaction(faction: FactionInstance) {
|
||||||
@@ -2075,7 +1851,6 @@ export class GameApp {
|
|||||||
this.shipsById.set(ship.id, ship);
|
this.shipsById.set(ship.id, ship);
|
||||||
faction.shipsBuilt += 1;
|
faction.shipsBuilt += 1;
|
||||||
faction.credits -= 60;
|
faction.credits -= 60;
|
||||||
this.fleetRefreshNeeded = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private tryBuildOutpostForFaction(faction: FactionInstance) {
|
private tryBuildOutpostForFaction(faction: FactionInstance) {
|
||||||
@@ -2149,7 +1924,7 @@ export class GameApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private focusSelection() {
|
private focusSelection() {
|
||||||
if (this.selection.length === 0 && !this.selectedStation && !this.selectedSystem && !this.selectedPlanet && !this.activeFleetId) {
|
if (this.selection.length === 0 && !this.selectedStation && !this.selectedSystem && !this.selectedPlanet) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.selectedPlanet) {
|
if (this.selectedPlanet) {
|
||||||
@@ -2171,10 +1946,6 @@ export class GameApp {
|
|||||||
this.updateHud();
|
this.updateHud();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.selection.length === 0 && this.activeFleetId) {
|
|
||||||
this.focusFleet(this.activeFleetId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.selection.length === 1) {
|
if (this.selection.length === 1) {
|
||||||
const ship = this.selection[0];
|
const ship = this.selection[0];
|
||||||
this.followShipId = ship.id;
|
this.followShipId = ship.id;
|
||||||
@@ -2220,10 +1991,6 @@ export class GameApp {
|
|||||||
this.generateNewUniverse();
|
this.generateNewUniverse();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action === "toggle-fleet-command") {
|
|
||||||
this.toggleWindow("fleet-command");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (action === "toggle-debug") {
|
if (action === "toggle-debug") {
|
||||||
this.toggleWindow("debug");
|
this.toggleWindow("debug");
|
||||||
return;
|
return;
|
||||||
@@ -2237,54 +2004,28 @@ export class GameApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleFleetAction(action: string, fleetId = this.activeFleetId) {
|
|
||||||
if (!fleetId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === "select") {
|
|
||||||
this.activeFleetId = fleetId;
|
|
||||||
this.selectFleetShips(fleetId);
|
|
||||||
this.updateHud();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fleet = this.fleetsById.get(fleetId);
|
|
||||||
if (!fleet) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.activeFleetId = fleet.id;
|
|
||||||
if (action === "focus") {
|
|
||||||
this.focusFleet(fleet.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (action === "patrol") {
|
|
||||||
this.setFleetPatrolOrder(fleet, fleet.systemId);
|
|
||||||
}
|
|
||||||
if (action === "mine") {
|
|
||||||
this.setFleetMineOrder(
|
|
||||||
fleet,
|
|
||||||
this.universe.scenario.miningDefaults.nodeSystemId,
|
|
||||||
this.universe.scenario.miningDefaults.refinerySystemId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (action === "hold") {
|
|
||||||
this.setFleetIdleOrder(fleet);
|
|
||||||
}
|
|
||||||
this.updateHud();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleWindowSelection(kind: string, id: string) {
|
private handleWindowSelection(kind: string, id: string) {
|
||||||
if (kind === "fleet") {
|
if (kind === "faction" || kind === "focus-faction") {
|
||||||
this.selectFleetShips(id);
|
const faction = this.factionsById.get(id);
|
||||||
this.activeFleetId = id;
|
if (!faction) {
|
||||||
this.updateHud();
|
return;
|
||||||
|
}
|
||||||
|
const system = this.getSystem(faction.definition.homeSystemId);
|
||||||
|
this.selectionManager.setSystem(system);
|
||||||
|
this.selectedSystemIndex = this.systems.findIndex((candidate) => candidate.definition.id === system.definition.id);
|
||||||
|
this.focusSystem(system.definition.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kind === "wing") {
|
if (kind === "focus-ship") {
|
||||||
this.selectWingShips(id);
|
const ship = this.shipsById.get(id);
|
||||||
|
if (!ship) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.selectionManager.replaceShips([ship]);
|
||||||
|
this.followShipId = ship.id;
|
||||||
|
this.selectedSystemIndex = this.systems.findIndex((system) => system.definition.id === ship.systemId);
|
||||||
|
this.focusPoint(ship.group.position, 520);
|
||||||
this.updateHud();
|
this.updateHud();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2295,7 +2036,6 @@ export class GameApp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.selectionManager.replaceShips([ship]);
|
this.selectionManager.replaceShips([ship]);
|
||||||
this.syncActiveFleetFromSelection();
|
|
||||||
this.updateHud();
|
this.updateHud();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2305,70 +2045,6 @@ export class GameApp {
|
|||||||
this.updateHud();
|
this.updateHud();
|
||||||
}
|
}
|
||||||
|
|
||||||
private focusFleet(fleetId: string) {
|
|
||||||
const fleet = this.fleetsById.get(fleetId);
|
|
||||||
if (!fleet) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const center = new THREE.Vector3();
|
|
||||||
const ships = getFleetShipIds(fleet)
|
|
||||||
.map((shipId) => this.shipsById.get(shipId))
|
|
||||||
.filter((ship): ship is ShipInstance => Boolean(ship));
|
|
||||||
if (ships.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ships.forEach((ship) => center.add(ship.group.position));
|
|
||||||
center.multiplyScalar(1 / ships.length);
|
|
||||||
this.followShipId = undefined;
|
|
||||||
this.getCameraFocus().copy(center);
|
|
||||||
this.selectedSystemIndex = this.systems.findIndex((system) => system.definition.id === fleet.systemId);
|
|
||||||
this.updateHud();
|
|
||||||
}
|
|
||||||
|
|
||||||
private selectFleetShips(fleetId: string) {
|
|
||||||
const fleet = this.fleetsById.get(fleetId);
|
|
||||||
if (!fleet) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.selectionManager.replaceShips(
|
|
||||||
getFleetShipIds(fleet)
|
|
||||||
.map((shipId) => this.shipsById.get(shipId))
|
|
||||||
.filter((ship): ship is ShipInstance => Boolean(ship)),
|
|
||||||
);
|
|
||||||
this.syncActiveFleetFromSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
private selectWingShips(wingId: string) {
|
|
||||||
const fleet = this.fleets.find((candidate) => candidate.wings.some((wing) => wing.id === wingId));
|
|
||||||
if (!fleet) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const wingIds = this.collectWingTreeIds(fleet, wingId);
|
|
||||||
const ships = fleet.wings
|
|
||||||
.filter((wing) => wingIds.has(wing.id))
|
|
||||||
.flatMap((wing) => wing.shipIds)
|
|
||||||
.map((shipId) => this.shipsById.get(shipId))
|
|
||||||
.filter((ship): ship is ShipInstance => Boolean(ship));
|
|
||||||
this.selectionManager.replaceShips(ships);
|
|
||||||
this.activeFleetId = fleet.id;
|
|
||||||
this.syncActiveFleetFromSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
private collectWingTreeIds(fleet: FleetInstance, rootWingId: string) {
|
|
||||||
const ids = new Set<string>([rootWingId]);
|
|
||||||
let changed = true;
|
|
||||||
while (changed) {
|
|
||||||
changed = false;
|
|
||||||
fleet.wings.forEach((wing) => {
|
|
||||||
if (wing.parentWingId && ids.has(wing.parentWingId) && !ids.has(wing.id)) {
|
|
||||||
ids.add(wing.id);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
private adjustZoom(multiplier: number) {
|
private adjustZoom(multiplier: number) {
|
||||||
const focus = this.getCameraFocus();
|
const focus = this.getCameraFocus();
|
||||||
const direction = this.camera.position.clone().sub(focus).normalize();
|
const direction = this.camera.position.clone().sub(focus).normalize();
|
||||||
@@ -2389,8 +2065,6 @@ export class GameApp {
|
|||||||
selection: this.selection,
|
selection: this.selection,
|
||||||
selectedStation: this.selectedStation,
|
selectedStation: this.selectedStation,
|
||||||
cameraFocus: this.getCameraFocus(),
|
cameraFocus: this.getCameraFocus(),
|
||||||
fleets: this.fleets,
|
|
||||||
activeFleetId: this.activeFleetId,
|
|
||||||
});
|
});
|
||||||
drawStrategicOverlay({
|
drawStrategicOverlay({
|
||||||
context: this.strategicOverlayContext,
|
context: this.strategicOverlayContext,
|
||||||
@@ -2404,8 +2078,6 @@ export class GameApp {
|
|||||||
selectedStation: this.selectedStation,
|
selectedStation: this.selectedStation,
|
||||||
selectedSystemIndex: this.selectedSystemIndex,
|
selectedSystemIndex: this.selectedSystemIndex,
|
||||||
viewLevel: this.viewLevel,
|
viewLevel: this.viewLevel,
|
||||||
fleets: this.fleets,
|
|
||||||
activeFleetId: this.activeFleetId,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2414,7 +2086,6 @@ export class GameApp {
|
|||||||
const system = this.systems[this.selectedSystemIndex] ?? this.systems[0];
|
const system = this.systems[this.selectedSystemIndex] ?? this.systems[0];
|
||||||
const selectedCount =
|
const selectedCount =
|
||||||
this.selection.length + (this.selectedStation ? 1 : 0) + (this.selectedSystem ? 1 : 0) + (this.selectedPlanet ? 1 : 0);
|
this.selection.length + (this.selectedStation ? 1 : 0) + (this.selectedSystem ? 1 : 0) + (this.selectedPlanet ? 1 : 0);
|
||||||
const activeFleet = this.activeFleetId ? this.fleetsById.get(this.activeFleetId) : undefined;
|
|
||||||
this.selectionTitleEl.textContent = getSelectionTitle(
|
this.selectionTitleEl.textContent = getSelectionTitle(
|
||||||
this.selection,
|
this.selection,
|
||||||
this.selectedStation,
|
this.selectedStation,
|
||||||
@@ -2438,7 +2109,6 @@ export class GameApp {
|
|||||||
this.systems,
|
this.systems,
|
||||||
this.viewLevel,
|
this.viewLevel,
|
||||||
this.ships,
|
this.ships,
|
||||||
this.fleets,
|
|
||||||
this.factions,
|
this.factions,
|
||||||
);
|
);
|
||||||
this.detailsEl.style.display = hasExplicitSelection ? "none" : "block";
|
this.detailsEl.style.display = hasExplicitSelection ? "none" : "block";
|
||||||
@@ -2449,16 +2119,12 @@ export class GameApp {
|
|||||||
this.sessionActionsEl.title = `${this.universe.label} • ${this.universe.systems.length} systems`;
|
this.sessionActionsEl.title = `${this.universe.label} • ${this.universe.systems.length} systems`;
|
||||||
this.statusEl.textContent = this.buildMode
|
this.statusEl.textContent = this.buildMode
|
||||||
? `Observer Mode: ${selectedDefinition.label} preview in ${system.definition.label} • ${this.viewLevel} view • ${this.universe.systems.length} systems`
|
? `Observer Mode: ${selectedDefinition.label} preview in ${system.definition.label} • ${this.viewLevel} view • ${this.universe.systems.length} systems`
|
||||||
: `Game Master Mode: ${selectedCount} inspected • Camera ${system.definition.label} • ${this.viewLevel} view • ${this.universe.systems.length} systems${this.followShipId ? " • following ship" : ""}${activeFleet ? ` • Fleet ${activeFleet.label}` : ""}`;
|
: `Game Master Mode: ${selectedCount} inspected • Camera ${system.definition.label} • ${this.viewLevel} view • ${this.universe.systems.length} systems${this.followShipId ? " • following ship" : ""}`;
|
||||||
this.ordersEl.dataset.mode =
|
this.ordersEl.dataset.mode = this.selectedStation ? "station" : this.selection.length > 0 ? "ships" : "none";
|
||||||
this.selectedStation ? "station" : this.selection.length > 0 ? "ships" : this.activeFleetId ? "fleet" : "none";
|
|
||||||
this.fleetWindowEl.dataset.open = this.windowState["fleet-command"] ? "true" : "false";
|
this.fleetWindowEl.dataset.open = this.windowState["fleet-command"] ? "true" : "false";
|
||||||
|
this.fleetWindowTitleEl.textContent = "Ships";
|
||||||
|
this.fleetWindowBodyEl.innerHTML = getShipWindowMarkup(this.ships, this.selection);
|
||||||
this.debugWindowEl.dataset.open = this.windowState.debug ? "true" : "false";
|
this.debugWindowEl.dataset.open = this.windowState.debug ? "true" : "false";
|
||||||
this.fleetWindowTitleEl.textContent = "Fleet Command";
|
|
||||||
this.fleetWindowSubtitleEl.textContent = activeFleet
|
|
||||||
? `${activeFleet.label} • ${describeFleetOrder(activeFleet)}`
|
|
||||||
: "No fleet selected";
|
|
||||||
this.fleetWindowBodyEl.innerHTML = getFleetWindowMarkup(this.fleets, this.shipsById, this.activeFleetId, this.selection);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private placeStation(definition: StationInstance["definition"], position: THREE.Vector3, systemId: string) {
|
private placeStation(definition: StationInstance["definition"], position: THREE.Vector3, systemId: string) {
|
||||||
@@ -2487,17 +2153,14 @@ export class GameApp {
|
|||||||
|
|
||||||
private clearSelection() {
|
private clearSelection() {
|
||||||
this.selectionManager.clear();
|
this.selectionManager.clear();
|
||||||
this.syncActiveFleetFromSelection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private addShipToSelection(ship: ShipInstance) {
|
private addShipToSelection(ship: ShipInstance) {
|
||||||
this.selectionManager.addShip(ship);
|
this.selectionManager.addShip(ship);
|
||||||
this.syncActiveFleetFromSelection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeShipFromSelection(ship: ShipInstance) {
|
private removeShipFromSelection(ship: ShipInstance) {
|
||||||
this.selectionManager.removeShip(ship);
|
this.selectionManager.removeShip(ship);
|
||||||
this.syncActiveFleetFromSelection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateMarqueeBox(clientX: number, clientY: number) {
|
private updateMarqueeBox(clientX: number, clientY: number) {
|
||||||
@@ -2555,21 +2218,10 @@ export class GameApp {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.syncActiveFleetFromSelection();
|
|
||||||
this.updateHud();
|
this.updateHud();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCameraFocus() {
|
private getCameraFocus() {
|
||||||
return this.cameraFocus;
|
return this.cameraFocus;
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncActiveFleetFromSelection() {
|
|
||||||
if (this.selection.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const firstFleetId = this.selection[0]?.fleetId;
|
|
||||||
if (firstFleetId && this.selection.every((ship) => ship.fleetId === firstFleetId)) {
|
|
||||||
this.activeFleetId = firstFleetId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,212 +0,0 @@
|
|||||||
import * as THREE from "three";
|
|
||||||
import type { FleetBehavior, FleetInstance, FleetWingInstance, ShipInstance } from "../types";
|
|
||||||
|
|
||||||
interface FleetBuildSpec {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
stance: FleetInstance["stance"];
|
|
||||||
systemId: string;
|
|
||||||
factionId?: string;
|
|
||||||
commander: ShipInstance;
|
|
||||||
wings: Array<{
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
behavior: FleetBehavior;
|
|
||||||
parentWingId?: string;
|
|
||||||
ships: ShipInstance[];
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDefaultFleets(ships: ShipInstance[]) {
|
|
||||||
clearFleetAssignments(ships);
|
|
||||||
const specs: FleetBuildSpec[] = [];
|
|
||||||
const factionIds = [...new Set(ships.map((ship) => ship.factionId))];
|
|
||||||
|
|
||||||
factionIds.forEach((factionId) => {
|
|
||||||
const factionShips = ships.filter((ship) => ship.factionId === factionId);
|
|
||||||
const military = factionShips.filter((ship) => ship.definition.role === "military");
|
|
||||||
const industrial = factionShips.filter((ship) => ship.definition.role !== "military");
|
|
||||||
const systems = [...new Set(factionShips.map((ship) => ship.systemId))];
|
|
||||||
|
|
||||||
systems.forEach((systemId) => {
|
|
||||||
const localMilitary = military.filter((ship) => ship.systemId === systemId);
|
|
||||||
if (localMilitary.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const commander =
|
|
||||||
localMilitary.find((ship) => ship.definition.shipClass === "capital") ??
|
|
||||||
localMilitary.find((ship) => ship.definition.shipClass === "cruiser") ??
|
|
||||||
localMilitary[0];
|
|
||||||
const lineShips = localMilitary.filter((ship) => ship.id !== commander.id);
|
|
||||||
specs.push({
|
|
||||||
id: `${factionId}:${systemId}:warfleet`,
|
|
||||||
label: `${commander.factionId} War Fleet`,
|
|
||||||
stance: factionId.includes("pirate") || factionId.includes("flag") || factionId.includes("rats") ? "balanced" : "defensive",
|
|
||||||
systemId,
|
|
||||||
factionId,
|
|
||||||
commander,
|
|
||||||
wings: [
|
|
||||||
{ id: "command", label: "Command Wing", behavior: "command", ships: [commander] },
|
|
||||||
{ id: "screen", label: "Screen Wing", behavior: "screen", parentWingId: "command", ships: lineShips },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const miners = industrial.filter((ship) => ship.definition.role === "mining");
|
|
||||||
const haulers = industrial.filter((ship) => ship.definition.role === "transport");
|
|
||||||
const logisticsCommander = haulers[0] ?? miners[0];
|
|
||||||
if (logisticsCommander) {
|
|
||||||
specs.push({
|
|
||||||
id: `${factionId}:industry`,
|
|
||||||
label: `${logisticsCommander.factionId} Industry Group`,
|
|
||||||
stance: "industrial",
|
|
||||||
systemId: logisticsCommander.systemId,
|
|
||||||
factionId,
|
|
||||||
commander: logisticsCommander,
|
|
||||||
wings: [
|
|
||||||
{ id: "command", label: "Command Wing", behavior: "command", ships: [logisticsCommander] },
|
|
||||||
{
|
|
||||||
id: "miners",
|
|
||||||
label: "Mining Wing",
|
|
||||||
behavior: "mining",
|
|
||||||
parentWingId: "command",
|
|
||||||
ships: miners.filter((ship) => ship.id !== logisticsCommander.id),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "transport",
|
|
||||||
label: "Transport Wing",
|
|
||||||
behavior: "logistics",
|
|
||||||
parentWingId: "command",
|
|
||||||
ships: haulers.filter((ship) => ship.id !== logisticsCommander.id),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return specs.map((spec) => materializeFleet(spec));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getFleetShipIds(fleet: FleetInstance) {
|
|
||||||
return fleet.shipIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getFleetCommander(fleet: FleetInstance, shipsById: Map<string, ShipInstance>) {
|
|
||||||
return shipsById.get(fleet.commanderShipId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getWingLeader(wing: FleetWingInstance, shipsById: Map<string, ShipInstance>) {
|
|
||||||
return shipsById.get(wing.leaderShipId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getWingMembers(wing: FleetWingInstance, shipsById: Map<string, ShipInstance>) {
|
|
||||||
return wing.shipIds.map((shipId) => shipsById.get(shipId)).filter((ship): ship is ShipInstance => Boolean(ship));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function describeFleetOrder(fleet: FleetInstance) {
|
|
||||||
switch (fleet.order.kind) {
|
|
||||||
case "idle":
|
|
||||||
return "Holding formation";
|
|
||||||
case "move":
|
|
||||||
return `Moving to ${fleet.order.systemId}`;
|
|
||||||
case "patrol":
|
|
||||||
return `Patrolling ${fleet.order.systemId}`;
|
|
||||||
case "mine":
|
|
||||||
return `Mining ${fleet.order.nodeSystemId} -> ${fleet.order.refinerySystemId}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function materializeFleet(spec: FleetBuildSpec): FleetInstance {
|
|
||||||
const wings = spec.wings.filter((wing) => wing.ships.length > 0).map((wing) => buildWing(spec.id, wing));
|
|
||||||
const shipIds = wings.flatMap((wing) => wing.shipIds);
|
|
||||||
|
|
||||||
shipIds.forEach((shipId, index) => {
|
|
||||||
const ship = spec.wings.flatMap((wing) => wing.ships).find((candidate) => candidate.id === shipId);
|
|
||||||
if (!ship) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const fleetWing = wings.find((wing) => wing.shipIds.includes(shipId));
|
|
||||||
if (!fleetWing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ship.fleetId = spec.id;
|
|
||||||
ship.wingId = fleetWing.id;
|
|
||||||
ship.behavior = fleetWing.behavior;
|
|
||||||
ship.isFleetCommander = ship.id === spec.commander.id;
|
|
||||||
ship.isWingLeader = ship.id === fleetWing.leaderShipId;
|
|
||||||
ship.formationOffset.copy(makeFormationOffset(index));
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: spec.id,
|
|
||||||
label: spec.label,
|
|
||||||
stance: spec.stance,
|
|
||||||
commanderShipId: spec.commander.id,
|
|
||||||
systemId: spec.systemId,
|
|
||||||
factionId: spec.factionId,
|
|
||||||
shipIds,
|
|
||||||
wings,
|
|
||||||
order: { kind: "idle" },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildWing(
|
|
||||||
fleetId: string,
|
|
||||||
wing: {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
behavior: FleetBehavior;
|
|
||||||
parentWingId?: string;
|
|
||||||
ships: ShipInstance[];
|
|
||||||
},
|
|
||||||
): FleetWingInstance {
|
|
||||||
const orderedShips = [...wing.ships];
|
|
||||||
const leader = orderedShips[0];
|
|
||||||
|
|
||||||
orderedShips.forEach((ship, index) => {
|
|
||||||
ship.formationOffset.copy(makeFormationOffset(index));
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: `${fleetId}:${wing.id}`,
|
|
||||||
fleetId,
|
|
||||||
label: wing.label,
|
|
||||||
behavior: wing.behavior,
|
|
||||||
parentWingId: wing.parentWingId ? `${fleetId}:${wing.parentWingId}` : undefined,
|
|
||||||
leaderShipId: leader.id,
|
|
||||||
shipIds: orderedShips.map((ship) => ship.id),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearFleetAssignments(ships: ShipInstance[]) {
|
|
||||||
ships.forEach((ship) => {
|
|
||||||
ship.fleetId = undefined;
|
|
||||||
ship.wingId = undefined;
|
|
||||||
ship.behavior = "independent";
|
|
||||||
ship.isFleetCommander = false;
|
|
||||||
ship.isWingLeader = false;
|
|
||||||
ship.formationOffset.set(0, 0, 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeFormationOffset(index: number) {
|
|
||||||
if (index === 0) {
|
|
||||||
return new THREE.Vector3();
|
|
||||||
}
|
|
||||||
const ring = Math.ceil((Math.sqrt(index + 1) - 1) / 2);
|
|
||||||
const slot = index - (2 * ring - 1) ** 2;
|
|
||||||
const side = Math.floor(slot / Math.max(1, ring * 2));
|
|
||||||
const local = slot % Math.max(1, ring * 2);
|
|
||||||
const spacing = 26;
|
|
||||||
|
|
||||||
switch (side) {
|
|
||||||
case 0:
|
|
||||||
return new THREE.Vector3((local - ring) * spacing, 0, ring * spacing);
|
|
||||||
case 1:
|
|
||||||
return new THREE.Vector3(ring * spacing, 0, (ring - local) * spacing);
|
|
||||||
case 2:
|
|
||||||
return new THREE.Vector3((ring - local) * spacing, 0, -ring * spacing);
|
|
||||||
default:
|
|
||||||
return new THREE.Vector3(-ring * spacing, 0, (local - ring) * spacing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,6 @@ import * as THREE from "three";
|
|||||||
|
|
||||||
export type ShipRole = "military" | "transport" | "mining";
|
export type ShipRole = "military" | "transport" | "mining";
|
||||||
export type ShipClass = "frigate" | "destroyer" | "cruiser" | "industrial" | "capital";
|
export type ShipClass = "frigate" | "destroyer" | "cruiser" | "industrial" | "capital";
|
||||||
export type FleetBehavior = "command" | "screen" | "escort" | "mining" | "logistics" | "reserve";
|
|
||||||
export type FleetStance = "balanced" | "defensive" | "industrial";
|
|
||||||
export type GameWindowId = "fleet-command" | "ship-designer" | "station-manager" | "debug";
|
export type GameWindowId = "fleet-command" | "ship-designer" | "station-manager" | "debug";
|
||||||
export type FactionKind = "empire" | "pirate";
|
export type FactionKind = "empire" | "pirate";
|
||||||
export type ConstructibleCategory =
|
export type ConstructibleCategory =
|
||||||
@@ -168,12 +166,6 @@ export interface PatrolRouteDefinition {
|
|||||||
points: [number, number, number][];
|
points: [number, number, number][];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FleetOrder =
|
|
||||||
| { kind: "idle" }
|
|
||||||
| { kind: "move"; destination: THREE.Vector3; systemId: string }
|
|
||||||
| { kind: "patrol"; points: THREE.Vector3[]; systemId: string; index: number }
|
|
||||||
| { kind: "mine"; nodeSystemId: string; refinerySystemId: string };
|
|
||||||
|
|
||||||
export interface ScenarioDefinition {
|
export interface ScenarioDefinition {
|
||||||
initialStations: InitialStationDefinition[];
|
initialStations: InitialStationDefinition[];
|
||||||
shipFormations: ShipFormationDefinition[];
|
shipFormations: ShipFormationDefinition[];
|
||||||
@@ -271,28 +263,6 @@ export interface TravelPlan {
|
|||||||
arrivalPoint: THREE.Vector3;
|
arrivalPoint: THREE.Vector3;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FleetWingInstance {
|
|
||||||
id: string;
|
|
||||||
fleetId: string;
|
|
||||||
label: string;
|
|
||||||
behavior: FleetBehavior;
|
|
||||||
parentWingId?: string;
|
|
||||||
leaderShipId: string;
|
|
||||||
shipIds: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FleetInstance {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
stance: FleetStance;
|
|
||||||
commanderShipId: string;
|
|
||||||
systemId: string;
|
|
||||||
factionId?: string;
|
|
||||||
shipIds: string[];
|
|
||||||
wings: FleetWingInstance[];
|
|
||||||
order: FleetOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ShipInstance {
|
export interface ShipInstance {
|
||||||
id: string;
|
id: string;
|
||||||
definition: ShipDefinition;
|
definition: ShipDefinition;
|
||||||
@@ -326,12 +296,6 @@ export interface ShipInstance {
|
|||||||
idleOrbitRadius: number;
|
idleOrbitRadius: number;
|
||||||
idleOrbitAngle: number;
|
idleOrbitAngle: number;
|
||||||
warpFx: THREE.Group;
|
warpFx: THREE.Group;
|
||||||
fleetId?: string;
|
|
||||||
wingId?: string;
|
|
||||||
behavior: FleetBehavior | "independent";
|
|
||||||
isFleetCommander: boolean;
|
|
||||||
isWingLeader: boolean;
|
|
||||||
formationOffset: THREE.Vector3;
|
|
||||||
dockedShipIds: Set<string>;
|
dockedShipIds: Set<string>;
|
||||||
dockingPorts: THREE.Vector3[];
|
dockingPorts: THREE.Vector3[];
|
||||||
}
|
}
|
||||||
@@ -425,6 +389,5 @@ export interface HudElements {
|
|||||||
fleetWindow: HTMLDivElement;
|
fleetWindow: HTMLDivElement;
|
||||||
fleetWindowBody: HTMLDivElement;
|
fleetWindowBody: HTMLDivElement;
|
||||||
fleetWindowTitle: HTMLHeadingElement;
|
fleetWindowTitle: HTMLHeadingElement;
|
||||||
fleetWindowSubtitle: HTMLParagraphElement;
|
|
||||||
debugWindow: HTMLDivElement;
|
debugWindow: HTMLDivElement;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { HudElements } from "../types";
|
import type { HudElements } from "../types";
|
||||||
interface HudHandlers {
|
interface HudHandlers {
|
||||||
onWindowAction: (action: string) => void;
|
onWindowAction: (action: string) => void;
|
||||||
onFleetAction: (action: string, fleetId?: string) => void;
|
|
||||||
onSelectionAction: (kind: string, id: string) => void;
|
onSelectionAction: (kind: string, id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,16 +21,8 @@ export function createHud(container: HTMLElement, handlers: HudHandlers): HudEle
|
|||||||
<section class="app-window fleet-window" data-window-id="fleet-command">
|
<section class="app-window fleet-window" data-window-id="fleet-command">
|
||||||
<div class="window-header">
|
<div class="window-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>Fleet Command</h2>
|
<h2>Ships</h2>
|
||||||
<p class="window-subtitle">No fleet selected</p>
|
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="window-close" data-window-action="toggle-fleet-command">Close</button>
|
|
||||||
</div>
|
|
||||||
<div class="fleet-actions">
|
|
||||||
<button type="button" data-fleet-action="focus">Focus</button>
|
|
||||||
<button type="button" data-fleet-action="patrol">Patrol</button>
|
|
||||||
<button type="button" data-fleet-action="mine">Mine</button>
|
|
||||||
<button type="button" data-fleet-action="hold">Hold</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="window-body fleet-window-body"></div>
|
<div class="window-body fleet-window-body"></div>
|
||||||
<div class="window-resize-handle" aria-hidden="true"></div>
|
<div class="window-resize-handle" aria-hidden="true"></div>
|
||||||
@@ -59,24 +50,17 @@ export function createHud(container: HTMLElement, handlers: HudHandlers): HudEle
|
|||||||
root.querySelectorAll<HTMLButtonElement>("[data-window-action]").forEach((button) => {
|
root.querySelectorAll<HTMLButtonElement>("[data-window-action]").forEach((button) => {
|
||||||
button.addEventListener("click", () => handlers.onWindowAction(button.dataset.windowAction ?? ""));
|
button.addEventListener("click", () => handlers.onWindowAction(button.dataset.windowAction ?? ""));
|
||||||
});
|
});
|
||||||
root.querySelectorAll<HTMLButtonElement>("[data-fleet-action]").forEach((button) => {
|
|
||||||
button.addEventListener("click", () => handlers.onFleetAction(button.dataset.fleetAction ?? ""));
|
|
||||||
});
|
|
||||||
|
|
||||||
const fleetWindowBody = root.querySelector<HTMLDivElement>(".fleet-window-body");
|
const fleetWindowBody = root.querySelector<HTMLDivElement>(".fleet-window-body");
|
||||||
fleetWindowBody?.addEventListener("click", (event) => {
|
fleetWindowBody?.addEventListener("click", (event) => {
|
||||||
|
const mouseEvent = event as MouseEvent;
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
const selectionNode = target.closest<HTMLElement>("[data-select-kind][data-select-id]");
|
const selectionNode = target.closest<HTMLElement>("[data-select-kind][data-select-id]");
|
||||||
if (selectionNode) {
|
if (selectionNode) {
|
||||||
handlers.onSelectionAction(selectionNode.dataset.selectKind ?? "", selectionNode.dataset.selectId ?? "");
|
const kind = selectionNode.dataset.selectKind ?? "";
|
||||||
return;
|
const id = selectionNode.dataset.selectId ?? "";
|
||||||
}
|
handlers.onSelectionAction(mouseEvent.detail >= 2 ? `focus-${kind}` : kind, id);
|
||||||
const fleetButton = target.closest<HTMLButtonElement>("[data-fleet-id]");
|
|
||||||
if (fleetButton?.dataset.fleetId) {
|
|
||||||
handlers.onFleetAction("select", fleetButton.dataset.fleetId);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const minimap = root.querySelector<HTMLCanvasElement>(".minimap");
|
const minimap = root.querySelector<HTMLCanvasElement>(".minimap");
|
||||||
const minimapContext = minimap?.getContext("2d");
|
const minimapContext = minimap?.getContext("2d");
|
||||||
if (!minimap || !minimapContext) {
|
if (!minimap || !minimapContext) {
|
||||||
@@ -104,7 +88,6 @@ export function createHud(container: HTMLElement, handlers: HudHandlers): HudEle
|
|||||||
fleetWindow: root.querySelector(".fleet-window") as HTMLDivElement,
|
fleetWindow: root.querySelector(".fleet-window") as HTMLDivElement,
|
||||||
fleetWindowBody: fleetWindowBody as HTMLDivElement,
|
fleetWindowBody: fleetWindowBody as HTMLDivElement,
|
||||||
fleetWindowTitle: root.querySelector(".fleet-window h2") as HTMLHeadingElement,
|
fleetWindowTitle: root.querySelector(".fleet-window h2") as HTMLHeadingElement,
|
||||||
fleetWindowSubtitle: root.querySelector(".window-subtitle") as HTMLParagraphElement,
|
|
||||||
debugWindow: root.querySelector(".debug-window") as HTMLDivElement,
|
debugWindow: root.querySelector(".debug-window") as HTMLDivElement,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ import {
|
|||||||
moduleDefinitionsById,
|
moduleDefinitionsById,
|
||||||
recipeDefinitions,
|
recipeDefinitions,
|
||||||
} from "../data/catalog";
|
} from "../data/catalog";
|
||||||
import { describeFleetOrder } from "../fleet/runtime";
|
|
||||||
import { getShipCargoAmount } from "../state/inventory";
|
import { getShipCargoAmount } from "../state/inventory";
|
||||||
import type {
|
import type {
|
||||||
FactionInstance,
|
FactionInstance,
|
||||||
FleetInstance,
|
|
||||||
PlanetInstance,
|
PlanetInstance,
|
||||||
ShipInstance,
|
ShipInstance,
|
||||||
SolarSystemInstance,
|
SolarSystemInstance,
|
||||||
@@ -122,7 +120,6 @@ export function getSelectionDetails(
|
|||||||
systems: SolarSystemInstance[],
|
systems: SolarSystemInstance[],
|
||||||
viewLevel: ViewLevel,
|
viewLevel: ViewLevel,
|
||||||
ships: ShipInstance[],
|
ships: ShipInstance[],
|
||||||
fleets: FleetInstance[],
|
|
||||||
factions: FactionInstance[],
|
factions: FactionInstance[],
|
||||||
) {
|
) {
|
||||||
if (selectedPlanet) {
|
if (selectedPlanet) {
|
||||||
@@ -132,7 +129,7 @@ export function getSelectionDetails(
|
|||||||
return `${selectedSystem.definition.label}\nType: ${selectedSystem.strategicValue}\nControl: ${selectedSystem.controllingFactionId ?? "Contested"} ${Math.round(selectedSystem.controlProgress)}%\nPlanets: ${selectedSystem.planets.length}\nResource Nodes: ${selectedSystem.definition.resourceNodes.length}\nGravity Well: ${Math.round(selectedSystem.gravityWellRadius)}`;
|
return `${selectedSystem.definition.label}\nType: ${selectedSystem.strategicValue}\nControl: ${selectedSystem.controllingFactionId ?? "Contested"} ${Math.round(selectedSystem.controlProgress)}%\nPlanets: ${selectedSystem.planets.length}\nResource Nodes: ${selectedSystem.definition.resourceNodes.length}\nGravity Well: ${Math.round(selectedSystem.gravityWellRadius)}`;
|
||||||
}
|
}
|
||||||
if (selectedStation) {
|
if (selectedStation) {
|
||||||
return describeStation(selectedStation, ships, fleets);
|
return describeStation(selectedStation, ships);
|
||||||
}
|
}
|
||||||
if (selection.length === 0) {
|
if (selection.length === 0) {
|
||||||
const central = systems
|
const central = systems
|
||||||
@@ -146,7 +143,7 @@ export function getSelectionDetails(
|
|||||||
`${faction.definition.label}: systems ${faction.ownedSystemIds.size} • mined ${Math.round(faction.oreMined)} • built ${faction.shipsBuilt} ships • losses ${faction.shipsLost}`,
|
`${faction.definition.label}: systems ${faction.ownedSystemIds.size} • mined ${Math.round(faction.oreMined)} • built ${faction.shipsBuilt} ships • losses ${faction.shipsLost}`,
|
||||||
)
|
)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
return `Observer Mode\nSystems online: ${systems.length}\nFleets tracked: ${fleets.length}\nView: ${viewLevel}\n\nCentral systems:\n${central}\n\nEmpires:\n${factionLines}`;
|
return `Systems online: ${systems.length}\nShips tracked: ${ships.length}\nView: ${viewLevel}\n\nCentral systems:\n${central}\n\nEmpires:\n${factionLines}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return selection
|
return selection
|
||||||
@@ -157,17 +154,17 @@ export function getSelectionDetails(
|
|||||||
ship.definition.dockingCapacity && ship.definition.dockingCapacity > 0
|
ship.definition.dockingCapacity && ship.definition.dockingCapacity > 0
|
||||||
? `\nHangar: ${ship.dockedShipIds.size}/${ship.definition.dockingCapacity} for ${(ship.definition.dockingClasses ?? []).join(", ")}`
|
? `\nHangar: ${ship.dockedShipIds.size}/${ship.definition.dockingCapacity} for ${(ship.definition.dockingClasses ?? []).join(", ")}`
|
||||||
: "";
|
: "";
|
||||||
return `${ship.definition.label} • ${ship.systemId}\nFaction: ${ship.factionId}\nClass: ${ship.definition.shipClass}\nState: ${ship.state}${dockedAt ? ` @ ${dockedAt}` : ""}\nOrder: ${ship.order.kind}\nFleet: ${ship.fleetId ?? "Independent"}${ship.isFleetCommander ? " • Commander" : ship.isWingLeader ? " • Wing Leader" : ""}\nBehavior: ${ship.behavior}\nHealth: ${Math.round(ship.health)}/${ship.maxHealth}\nCargo: ${Math.round(getShipCargoAmount(ship))}/${ship.definition.cargoCapacity || 0} ${getItemLabel(ship.cargoItemId)}\nFuel: ${ship.fuel.toFixed(0)}/${ship.maxFuel}\nEnergy: ${ship.energy.toFixed(0)}/${ship.maxEnergy}\nHold Type: ${ship.definition.cargoKind ?? "none"}${hangarStatus}\nModules: ${ship.definition.modules.map(getModuleLabel).join(", ")}`;
|
return `${ship.definition.label} • ${ship.systemId}\nFaction: ${ship.factionId}\nClass: ${ship.definition.shipClass}\nState: ${ship.state}${dockedAt ? ` @ ${dockedAt}` : ""}\nOrder: ${ship.order.kind}\nHealth: ${Math.round(ship.health)}/${ship.maxHealth}\nCargo: ${Math.round(getShipCargoAmount(ship))}/${ship.definition.cargoCapacity || 0} ${getItemLabel(ship.cargoItemId)}\nFuel: ${ship.fuel.toFixed(0)}/${ship.maxFuel}\nEnergy: ${ship.energy.toFixed(0)}/${ship.maxEnergy}\nHold Type: ${ship.definition.cargoKind ?? "none"}${hangarStatus}\nModules: ${ship.definition.modules.map(getModuleLabel).join(", ")}`;
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function describeStation(station: StationInstance, ships: ShipInstance[], fleets: FleetInstance[]) {
|
export function describeStation(station: StationInstance, ships: ShipInstance[]) {
|
||||||
const miners = ships.filter((ship) => ship.systemId === station.systemId && ship.order.kind === "mine").length;
|
const miners = ships.filter((ship) => ship.systemId === station.systemId && ship.order.kind === "mine").length;
|
||||||
const escorts = ships.filter((ship) => ship.systemId === station.systemId && ship.order.kind === "escort").length;
|
const escorts = ships.filter((ship) => ship.systemId === station.systemId && ship.order.kind === "escort").length;
|
||||||
const patrols = ships.filter((ship) => ship.systemId === station.systemId && ship.order.kind === "patrol").length;
|
const patrols = ships.filter((ship) => ship.systemId === station.systemId && ship.order.kind === "patrol").length;
|
||||||
const localFleets = fleets.filter((fleet) => fleet.systemId === station.systemId).length;
|
const localShips = ships.filter((ship) => ship.systemId === station.systemId).length;
|
||||||
const activeRecipe = station.activeRecipeId
|
const activeRecipe = station.activeRecipeId
|
||||||
? recipeDefinitions.find((recipe) => recipe.id === station.activeRecipeId)
|
? recipeDefinitions.find((recipe) => recipe.id === station.activeRecipeId)
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -189,101 +186,60 @@ export function describeStation(station: StationInstance, ships: ShipInstance[],
|
|||||||
: station.definition.category === "farm"
|
: station.definition.category === "farm"
|
||||||
? "Supplying agricultural goods and industrial consumables"
|
? "Supplying agricultural goods and industrial consumables"
|
||||||
: station.definition.category === "defense"
|
: station.definition.category === "defense"
|
||||||
? `Coordinating ${escorts} escort wings`
|
? `Coordinating ${escorts} escort ships`
|
||||||
: station.definition.category === "gate"
|
: station.definition.category === "gate"
|
||||||
? "Assembling transit infrastructure and gate components"
|
? "Assembling transit infrastructure and gate components"
|
||||||
: station.modules.includes("fabricator-array")
|
: station.modules.includes("fabricator-array")
|
||||||
? "Fabricating industrial parts and equipment"
|
? "Fabricating industrial parts and equipment"
|
||||||
: "Managing local trade traffic";
|
: "Managing local trade traffic";
|
||||||
|
|
||||||
return `${station.definition.label} • ${station.systemId}\nFaction: ${station.factionId}\nRole: ${station.definition.category}\nActivity: ${activity}\nLocal Fleets: ${localFleets}\nDocking: ${station.dockedShipIds.size}/${station.definition.dockingCapacity}\nHealth: ${Math.round(station.health)}/${station.maxHealth}\nFuel: ${station.fuel.toFixed(0)}/${station.maxFuel}\nEnergy: ${station.energy.toFixed(0)}/${station.maxEnergy}\nBulk Solid: ${Math.round(station.inventory["bulk-solid"])}\nContainer: ${Math.round(station.inventory.container)}\nManufactured: ${Math.round(station.inventory.manufactured)}\nModules: ${station.modules.map(getModuleLabel).join(", ")}\n${productionStatus}Radius: ${station.definition.radius}`;
|
return `${station.definition.label} • ${station.systemId}\nFaction: ${station.factionId}\nRole: ${station.definition.category}\nActivity: ${activity}\nLocal Ships: ${localShips}\nDocking: ${station.dockedShipIds.size}/${station.definition.dockingCapacity}\nHealth: ${Math.round(station.health)}/${station.maxHealth}\nFuel: ${station.fuel.toFixed(0)}/${station.maxFuel}\nEnergy: ${station.energy.toFixed(0)}/${station.maxEnergy}\nBulk Solid: ${Math.round(station.inventory["bulk-solid"])}\nContainer: ${Math.round(station.inventory.container)}\nManufactured: ${Math.round(station.inventory.manufactured)}\nModules: ${station.modules.map(getModuleLabel).join(", ")}\n${productionStatus}Radius: ${station.definition.radius}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFleetWindowMarkup(
|
export function getShipWindowMarkup(ships: ShipInstance[], selection: ShipInstance[]) {
|
||||||
fleets: FleetInstance[],
|
if (ships.length === 0) {
|
||||||
shipsById: Map<string, ShipInstance>,
|
return `<div class="ship-window-empty">No ships online.</div>`;
|
||||||
activeFleetId: string | undefined,
|
|
||||||
selection: ShipInstance[],
|
|
||||||
) {
|
|
||||||
if (fleets.length === 0) {
|
|
||||||
return `<div class="fleet-card"><span class="fleet-card-line">No fleets initialized.</span></div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedShipIds = new Set(selection.map((ship) => ship.id));
|
const selectedShipIds = new Set(selection.map((ship) => ship.id));
|
||||||
|
const sortedShips = [...ships]
|
||||||
|
.sort((left, right) =>
|
||||||
|
left.factionId.localeCompare(right.factionId) ||
|
||||||
|
left.systemId.localeCompare(right.systemId) ||
|
||||||
|
left.definition.label.localeCompare(right.definition.label) ||
|
||||||
|
left.id.localeCompare(right.id),
|
||||||
|
);
|
||||||
|
|
||||||
return fleets
|
const shipsByFaction = new Map<string, ShipInstance[]>();
|
||||||
.map((fleet) => {
|
sortedShips.forEach((ship) => {
|
||||||
const commander = shipsById.get(fleet.commanderShipId);
|
const bucket = shipsByFaction.get(ship.factionId) ?? [];
|
||||||
const rootWings = fleet.wings.filter((wing) => !wing.parentWingId);
|
bucket.push(ship);
|
||||||
const tree = rootWings.map((wing) => renderWingNode(fleet, wing.id, shipsById, selectedShipIds)).join("");
|
shipsByFaction.set(ship.factionId, bucket);
|
||||||
const fleetSelected = fleet.shipIds.length > 0 && fleet.shipIds.every((shipId) => selectedShipIds.has(shipId));
|
});
|
||||||
|
|
||||||
return `
|
return [...shipsByFaction.entries()]
|
||||||
<article class="fleet-card" data-active="${fleet.id === activeFleetId}">
|
.map(
|
||||||
<button type="button" class="fleet-select" data-select-kind="fleet" data-select-id="${fleet.id}">${fleet.label}</button>
|
([factionId, factionShips]) => `
|
||||||
<div class="fleet-tree">
|
<section class="ship-window-group">
|
||||||
<div class="fleet-tree-root" data-select-kind="fleet" data-select-id="${fleet.id}" data-selected="${fleetSelected}">
|
<h3 class="ship-window-group-title" data-select-kind="faction" data-select-id="${factionId}">${factionId}</h3>
|
||||||
<span class="fleet-card-title">${commander?.definition.label ?? fleet.commanderShipId}</span>
|
${factionShips
|
||||||
<span class="fleet-card-line">Commander • ${fleet.shipIds.length} ships • ${fleet.systemId}</span>
|
.map(
|
||||||
<span class="fleet-card-line">Fleet Order: ${describeFleetOrder(fleet)}</span>
|
(ship) => `
|
||||||
<span class="fleet-card-line">Stance: ${fleet.stance}</span>
|
<div class="ship-window-row" data-select-kind="ship" data-select-id="${ship.id}" data-selected="${selectedShipIds.has(ship.id)}">
|
||||||
</div>
|
<span class="ship-window-name">${ship.definition.label}</span>
|
||||||
<div class="fleet-tree-children">${tree}</div>
|
<span class="ship-window-meta">${describeShipNode(ship)}</span>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
`,
|
||||||
`;
|
)
|
||||||
})
|
.join("")}
|
||||||
|
</section>
|
||||||
|
`,
|
||||||
|
)
|
||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderWingNode(
|
|
||||||
fleet: FleetInstance,
|
|
||||||
wingId: string,
|
|
||||||
shipsById: Map<string, ShipInstance>,
|
|
||||||
selectedShipIds: Set<string>,
|
|
||||||
): string {
|
|
||||||
const wing = fleet.wings.find((candidate) => candidate.id === wingId);
|
|
||||||
if (!wing) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const leader = shipsById.get(wing.leaderShipId);
|
|
||||||
const childWings = fleet.wings.filter((candidate) => candidate.parentWingId === wing.id);
|
|
||||||
const wingTreeShipIds = collectWingShipIds(fleet, wing.id);
|
|
||||||
const wingSelected = wingTreeShipIds.length > 0 && wingTreeShipIds.every((shipId) => selectedShipIds.has(shipId));
|
|
||||||
const nonLeaderShips = wing.shipIds
|
|
||||||
.filter((shipId) => shipId !== wing.leaderShipId)
|
|
||||||
.map((shipId) => shipsById.get(shipId))
|
|
||||||
.filter((ship): ship is ShipInstance => Boolean(ship));
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="fleet-tree-node wing-node">
|
|
||||||
<div class="fleet-node-card" data-select-kind="wing" data-select-id="${wing.id}" data-selected="${wingSelected}">
|
|
||||||
<span class="fleet-node-title">${wing.label}</span>
|
|
||||||
<span class="fleet-node-meta">${wing.behavior} • ${wing.shipIds.length} ships</span>
|
|
||||||
<span class="fleet-node-meta">Wing Lead: ${leader ? describeShipNode(leader) : wing.leaderShipId}</span>
|
|
||||||
</div>
|
|
||||||
<div class="fleet-tree-children">
|
|
||||||
${childWings.map((childWing) => renderWingNode(fleet, childWing.id, shipsById, selectedShipIds)).join("")}
|
|
||||||
${nonLeaderShips
|
|
||||||
.map(
|
|
||||||
(ship) => `
|
|
||||||
<div class="fleet-tree-node ship-node">
|
|
||||||
<div class="fleet-node-card" data-select-kind="ship" data-select-id="${ship.id}" data-selected="${selectedShipIds.has(ship.id)}">
|
|
||||||
<span class="fleet-node-title">${ship.definition.label}</span>
|
|
||||||
<span class="fleet-node-meta">${describeShipNode(ship)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function describeShipNode(ship: ShipInstance): string {
|
function describeShipNode(ship: ShipInstance): string {
|
||||||
return `${ship.definition.shipClass} • ${ship.state} • ${ship.order.kind} • ${ship.behavior}`;
|
return `${ship.state} - ${ship.systemId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCard(title: string, lines: string[]) {
|
function renderCard(title: string, lines: string[]) {
|
||||||
@@ -295,23 +251,6 @@ function renderCard(title: string, lines: string[]) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectWingShipIds(fleet: FleetInstance, rootWingId: string): string[] {
|
|
||||||
const wingIds = new Set<string>([rootWingId]);
|
|
||||||
let changed = true;
|
|
||||||
|
|
||||||
while (changed) {
|
|
||||||
changed = false;
|
|
||||||
fleet.wings.forEach((wing) => {
|
|
||||||
if (wing.parentWingId && wingIds.has(wing.parentWingId) && !wingIds.has(wing.id)) {
|
|
||||||
wingIds.add(wing.id);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return fleet.wings.filter((wing) => wingIds.has(wing.id)).flatMap((wing) => wing.shipIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getItemLabel(itemId?: string) {
|
export function getItemLabel(itemId?: string) {
|
||||||
return itemId ? itemDefinitionsById.get(itemId)?.label ?? itemId : "None";
|
return itemId ? itemDefinitionsById.get(itemId)?.label ?? itemId : "None";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { getFleetCommander, getWingLeader } from "../fleet/runtime";
|
import type { ShipRole, ShipInstance, SolarSystemInstance, StationInstance, ViewLevel } from "../types";
|
||||||
import type { FleetInstance, ShipRole, ShipInstance, SolarSystemInstance, StationInstance, ViewLevel } from "../types";
|
|
||||||
|
|
||||||
interface RenderMinimapOptions {
|
interface RenderMinimapOptions {
|
||||||
context: CanvasRenderingContext2D;
|
context: CanvasRenderingContext2D;
|
||||||
@@ -12,8 +11,6 @@ interface RenderMinimapOptions {
|
|||||||
selection: ShipInstance[];
|
selection: ShipInstance[];
|
||||||
selectedStation?: StationInstance;
|
selectedStation?: StationInstance;
|
||||||
cameraFocus: THREE.Vector3;
|
cameraFocus: THREE.Vector3;
|
||||||
fleets: FleetInstance[];
|
|
||||||
activeFleetId?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RenderOverlayOptions {
|
interface RenderOverlayOptions {
|
||||||
@@ -28,8 +25,6 @@ interface RenderOverlayOptions {
|
|||||||
selectedStation?: StationInstance;
|
selectedStation?: StationInstance;
|
||||||
selectedSystemIndex: number;
|
selectedSystemIndex: number;
|
||||||
viewLevel: ViewLevel;
|
viewLevel: ViewLevel;
|
||||||
fleets: FleetInstance[];
|
|
||||||
activeFleetId?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function drawMinimap({
|
export function drawMinimap({
|
||||||
@@ -42,8 +37,6 @@ export function drawMinimap({
|
|||||||
selection,
|
selection,
|
||||||
selectedStation,
|
selectedStation,
|
||||||
cameraFocus,
|
cameraFocus,
|
||||||
fleets,
|
|
||||||
activeFleetId,
|
|
||||||
}: RenderMinimapOptions) {
|
}: RenderMinimapOptions) {
|
||||||
context.clearRect(0, 0, width, height);
|
context.clearRect(0, 0, width, height);
|
||||||
context.fillStyle = "rgba(4, 9, 20, 0.92)";
|
context.fillStyle = "rgba(4, 9, 20, 0.92)";
|
||||||
@@ -89,16 +82,6 @@ export function drawMinimap({
|
|||||||
context.fill();
|
context.fill();
|
||||||
});
|
});
|
||||||
|
|
||||||
fleets.forEach((fleet) => {
|
|
||||||
const commander = getFleetCommander(fleet, new Map(ships.map((ship) => [ship.id, ship])));
|
|
||||||
if (!commander) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const point = mapPoint(commander.group.position);
|
|
||||||
context.strokeStyle = fleet.id === activeFleetId ? "#ffbf69" : "rgba(126, 212, 255, 0.42)";
|
|
||||||
context.strokeRect(point.x - 6, point.y - 6, 12, 12);
|
|
||||||
});
|
|
||||||
|
|
||||||
const focus = mapPoint(cameraFocus);
|
const focus = mapPoint(cameraFocus);
|
||||||
context.strokeStyle = "rgba(255,255,255,0.7)";
|
context.strokeStyle = "rgba(255,255,255,0.7)";
|
||||||
context.strokeRect(focus.x - 9, focus.y - 9, 18, 18);
|
context.strokeRect(focus.x - 9, focus.y - 9, 18, 18);
|
||||||
@@ -116,8 +99,6 @@ export function drawStrategicOverlay({
|
|||||||
selectedStation,
|
selectedStation,
|
||||||
selectedSystemIndex,
|
selectedSystemIndex,
|
||||||
viewLevel,
|
viewLevel,
|
||||||
fleets,
|
|
||||||
activeFleetId,
|
|
||||||
}: RenderOverlayOptions) {
|
}: RenderOverlayOptions) {
|
||||||
context.clearRect(0, 0, width, height);
|
context.clearRect(0, 0, width, height);
|
||||||
if (viewLevel === "local") {
|
if (viewLevel === "local") {
|
||||||
@@ -132,8 +113,6 @@ export function drawStrategicOverlay({
|
|||||||
context.textBaseline = "middle";
|
context.textBaseline = "middle";
|
||||||
|
|
||||||
if (viewLevel === "solar") {
|
if (viewLevel === "solar") {
|
||||||
drawFleetLinks(context, camera, fleets, ships, systems[selectedSystemIndex]?.definition.id, activeFleetId);
|
|
||||||
|
|
||||||
selection.forEach((ship) => {
|
selection.forEach((ship) => {
|
||||||
const screen = projectWorldToScreen(ship.group.position, camera);
|
const screen = projectWorldToScreen(ship.group.position, camera);
|
||||||
if (screen) {
|
if (screen) {
|
||||||
@@ -172,7 +151,7 @@ export function drawStrategicOverlay({
|
|||||||
if (!bucket || bucket.length === 0) {
|
if (!bucket || bucket.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
drawFleetSymbol(context, screen.x - 52 + index * 52, screen.y + 32, role, bucket.length, bucket.some((ship) => selection.includes(ship)));
|
drawRoleSymbol(context, screen.x - 52 + index * 52, screen.y + 32, role, bucket.length, bucket.some((ship) => selection.includes(ship)));
|
||||||
});
|
});
|
||||||
|
|
||||||
const stationCount = stations.filter((station) => station.systemId === system.definition.id).length;
|
const stationCount = stations.filter((station) => station.systemId === system.definition.id).length;
|
||||||
@@ -183,12 +162,6 @@ export function drawStrategicOverlay({
|
|||||||
drawStrategicStationGroup(context, screen.x, screen.y - 38, stationCount, stationSelected);
|
drawStrategicStationGroup(context, screen.x, screen.y - 38, stationCount, stationSelected);
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeFleets = fleets.filter((fleet) => fleet.systemId === system.definition.id).length;
|
|
||||||
if (activeFleets > 0) {
|
|
||||||
context.fillStyle = "rgba(255, 191, 105, 0.92)";
|
|
||||||
context.font = "600 10px Space Grotesk, sans-serif";
|
|
||||||
context.fillText(`${activeFleets} FLEET${activeFleets > 1 ? "S" : ""}`, screen.x, screen.y + 58);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +194,7 @@ function drawSystemFrame(context: CanvasRenderingContext2D, x: number, y: number
|
|||||||
context.fillText(label.toUpperCase(), x, y - 28);
|
context.fillText(label.toUpperCase(), x, y - 28);
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawFleetSymbol(
|
function drawRoleSymbol(
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
@@ -284,54 +257,6 @@ function drawFleetSymbol(
|
|||||||
context.restore();
|
context.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawFleetLinks(
|
|
||||||
context: CanvasRenderingContext2D,
|
|
||||||
camera: THREE.PerspectiveCamera,
|
|
||||||
fleets: FleetInstance[],
|
|
||||||
ships: ShipInstance[],
|
|
||||||
systemId: string | undefined,
|
|
||||||
activeFleetId: string | undefined,
|
|
||||||
) {
|
|
||||||
const shipsById = new Map(ships.map((ship) => [ship.id, ship]));
|
|
||||||
|
|
||||||
fleets
|
|
||||||
.filter((fleet) => !systemId || fleet.systemId === systemId)
|
|
||||||
.forEach((fleet) => {
|
|
||||||
const commander = getFleetCommander(fleet, shipsById);
|
|
||||||
if (!commander) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const commanderScreen = projectWorldToScreen(commander.group.position, camera);
|
|
||||||
if (!commanderScreen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const highlighted = fleet.id === activeFleetId;
|
|
||||||
context.strokeStyle = highlighted ? "rgba(255, 191, 105, 0.85)" : "rgba(126, 212, 255, 0.24)";
|
|
||||||
context.fillStyle = highlighted ? "#ffbf69" : "rgba(126, 212, 255, 0.72)";
|
|
||||||
context.lineWidth = highlighted ? 1.8 : 1.1;
|
|
||||||
|
|
||||||
fleet.wings.forEach((wing) => {
|
|
||||||
const leader = getWingLeader(wing, shipsById);
|
|
||||||
if (!leader || leader.id === commander.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const leaderScreen = projectWorldToScreen(leader.group.position, camera);
|
|
||||||
if (!leaderScreen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
context.beginPath();
|
|
||||||
context.moveTo(commanderScreen.x, commanderScreen.y);
|
|
||||||
context.lineTo(leaderScreen.x, leaderScreen.y);
|
|
||||||
context.stroke();
|
|
||||||
context.beginPath();
|
|
||||||
context.arc(leaderScreen.x, leaderScreen.y, highlighted ? 4.5 : 3, 0, Math.PI * 2);
|
|
||||||
context.fill();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawStrategicStationGroup(
|
function drawStrategicStationGroup(
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
x: number,
|
x: number,
|
||||||
|
|||||||
@@ -118,48 +118,31 @@ function createScenario(systems: SolarSystemDefinition[], factions: FactionDefin
|
|||||||
}
|
}
|
||||||
|
|
||||||
initialStations.push(
|
initialStations.push(
|
||||||
{ constructibleId: "trade-hub", systemId: capital.id, factionId: faction.id, planetIndex: 1, lagrangeSide: 1 },
|
|
||||||
{
|
|
||||||
constructibleId: "farm-ring",
|
|
||||||
systemId: capital.id,
|
|
||||||
factionId: faction.id,
|
|
||||||
planetIndex: 0,
|
|
||||||
lagrangeSide: -1,
|
|
||||||
seedStock: { gas: 120, water: 160 },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
constructibleId: "manufactory",
|
constructibleId: "manufactory",
|
||||||
systemId: capital.id,
|
systemId: capital.id,
|
||||||
factionId: faction.id,
|
factionId: faction.id,
|
||||||
planetIndex: Math.min(2, capital.planets.length - 1),
|
planetIndex: Math.min(1, capital.planets.length - 1),
|
||||||
lagrangeSide: 1,
|
lagrangeSide: 1,
|
||||||
seedStock: { "refined-metals": 200, water: 100, "ship-equipment": 40, "naval-guns": 24 },
|
seedStock: { water: 40 },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
constructibleId: "shipyard",
|
|
||||||
systemId: capital.id,
|
|
||||||
factionId: faction.id,
|
|
||||||
planetIndex: Math.min(3, capital.planets.length - 1),
|
|
||||||
lagrangeSide: -1,
|
|
||||||
seedStock: { "ship-parts": 80, "ammo-crates": 70, "hull-sections": 100, "ship-equipment": 40 },
|
|
||||||
},
|
|
||||||
{ constructibleId: "defense-grid", systemId: capital.id, factionId: faction.id, planetIndex: 1, lagrangeSide: -1 },
|
|
||||||
{
|
{
|
||||||
constructibleId: "refinery",
|
constructibleId: "refinery",
|
||||||
systemId: mining.id,
|
systemId: mining.id,
|
||||||
factionId: faction.id,
|
factionId: faction.id,
|
||||||
planetIndex: 0,
|
planetIndex: 0,
|
||||||
lagrangeSide: 1,
|
lagrangeSide: 1,
|
||||||
seedStock: { ore: 240, "refined-metals": 80 },
|
seedStock: { ore: 60 },
|
||||||
},
|
},
|
||||||
{ constructibleId: "defense-grid", systemId: mining.id, factionId: faction.id, planetIndex: 1, lagrangeSide: -1 },
|
|
||||||
);
|
);
|
||||||
|
|
||||||
shipFormations.push(
|
shipFormations.push({
|
||||||
{ shipId: "frigate", count: 1, center: localPoint(capital, 180, 120), systemId: capital.id, factionId: faction.id },
|
shipId: "miner",
|
||||||
{ shipId: "hauler", count: 1, center: localPoint(capital, 280, -120), systemId: capital.id, factionId: faction.id },
|
count: 1,
|
||||||
{ shipId: "miner", count: 1, center: localPoint(mining, 180, 100), systemId: mining.id, factionId: faction.id },
|
center: localPoint(mining, 180, 100),
|
||||||
);
|
systemId: mining.id,
|
||||||
|
factionId: faction.id,
|
||||||
|
});
|
||||||
|
|
||||||
patrolRoutes.push(createPatrolRoute(capital), createPatrolRoute(mining));
|
patrolRoutes.push(createPatrolRoute(capital), createPatrolRoute(mining));
|
||||||
});
|
});
|
||||||
@@ -176,15 +159,16 @@ function createScenario(systems: SolarSystemDefinition[], factions: FactionDefin
|
|||||||
factionId: faction.id,
|
factionId: faction.id,
|
||||||
planetIndex: 0,
|
planetIndex: 0,
|
||||||
lagrangeSide: 1,
|
lagrangeSide: 1,
|
||||||
seedStock: { "refined-metals": 100, "ship-parts": 30, "ammo-crates": 30 },
|
seedStock: { "refined-metals": 60, "ammo-crates": 12 },
|
||||||
},
|
},
|
||||||
{ constructibleId: "defense-grid", systemId: base.id, factionId: faction.id, planetIndex: 1, lagrangeSide: -1 },
|
|
||||||
);
|
|
||||||
shipFormations.push(
|
|
||||||
{ shipId: "frigate", count: 4, center: localPoint(base, 180, 60), systemId: base.id, factionId: faction.id },
|
|
||||||
{ shipId: "destroyer", count: 2, center: localPoint(base, 250, 120), systemId: base.id, factionId: faction.id },
|
|
||||||
{ shipId: "hauler", count: 1, center: localPoint(base, 320, -90), systemId: base.id, factionId: faction.id },
|
|
||||||
);
|
);
|
||||||
|
shipFormations.push({
|
||||||
|
shipId: "frigate",
|
||||||
|
count: 1,
|
||||||
|
center: localPoint(base, 180, 60),
|
||||||
|
systemId: base.id,
|
||||||
|
factionId: faction.id,
|
||||||
|
});
|
||||||
patrolRoutes.push(createPatrolRoute(base));
|
patrolRoutes.push(createPatrolRoute(base));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -714,10 +714,6 @@ export function createShipInstance({
|
|||||||
idleOrbitRadius: Math.max(120, group.position.length()),
|
idleOrbitRadius: Math.max(120, group.position.length()),
|
||||||
idleOrbitAngle: 0,
|
idleOrbitAngle: 0,
|
||||||
warpFx,
|
warpFx,
|
||||||
behavior: "independent",
|
|
||||||
isFleetCommander: false,
|
|
||||||
isWingLeader: false,
|
|
||||||
formationOffset: new THREE.Vector3(),
|
|
||||||
dockedShipIds: new Set(),
|
dockedShipIds: new Set(),
|
||||||
dockingPorts,
|
dockingPorts,
|
||||||
};
|
};
|
||||||
|
|||||||
200
src/style.css
200
src/style.css
@@ -161,8 +161,7 @@ canvas {
|
|||||||
letter-spacing: 0.14em;
|
letter-spacing: 0.14em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.window-launchers,
|
.window-launchers {
|
||||||
.fleet-actions {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -174,7 +173,6 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.window-launchers button,
|
.window-launchers button,
|
||||||
.fleet-actions button,
|
|
||||||
.session-actions button,
|
.session-actions button,
|
||||||
.window-close {
|
.window-close {
|
||||||
border: 1px solid rgba(126, 212, 255, 0.16);
|
border: 1px solid rgba(126, 212, 255, 0.16);
|
||||||
@@ -191,7 +189,6 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.window-launchers button:hover,
|
.window-launchers button:hover,
|
||||||
.fleet-actions button:hover,
|
|
||||||
.session-actions button:hover,
|
.session-actions button:hover,
|
||||||
.window-close:hover {
|
.window-close:hover {
|
||||||
border-color: rgba(126, 212, 255, 0.4);
|
border-color: rgba(126, 212, 255, 0.4);
|
||||||
@@ -272,10 +269,6 @@ button:disabled {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fleet-actions {
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-body {
|
.window-body {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -285,6 +278,59 @@ button:disabled {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ship-window-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid rgba(126, 212, 255, 0.1);
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ship-window-row:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ship-window-row[data-selected="true"] {
|
||||||
|
color: #ffbf69;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ship-window-name {
|
||||||
|
color: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ship-window-meta {
|
||||||
|
color: inherit;
|
||||||
|
line-height: 1.35;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 0.92em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ship-window-group + .ship-window-group {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ship-window-group-title {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ship-window-group-title:hover {
|
||||||
|
color: #ffbf69;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ship-window-empty {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
.window-resize-handle {
|
.window-resize-handle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
@@ -317,141 +363,6 @@ button:disabled {
|
|||||||
height: 6px;
|
height: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fleet-card {
|
|
||||||
border: 1px solid rgba(126, 212, 255, 0.14);
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 14px;
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(8, 18, 35, 0.84), rgba(5, 11, 22, 0.8)),
|
|
||||||
repeating-linear-gradient(
|
|
||||||
90deg,
|
|
||||||
rgba(126, 212, 255, 0.02) 0,
|
|
||||||
rgba(126, 212, 255, 0.02) 1px,
|
|
||||||
transparent 1px,
|
|
||||||
transparent 14px
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fleet-card[data-active="true"] {
|
|
||||||
border-color: rgba(255, 191, 105, 0.44);
|
|
||||||
box-shadow: inset 0 0 0 1px rgba(255, 191, 105, 0.16);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fleet-select {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fleet-card-title,
|
|
||||||
.fleet-card-line,
|
|
||||||
.fleet-wing-line {
|
|
||||||
display: block;
|
|
||||||
color: var(--muted);
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fleet-card-title {
|
|
||||||
color: var(--text);
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fleet-wing-line {
|
|
||||||
color: #bdd9ea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fleet-tree,
|
|
||||||
.fleet-tree-children {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fleet-tree-root,
|
|
||||||
.fleet-node-card {
|
|
||||||
position: relative;
|
|
||||||
border: 1px solid rgba(126, 212, 255, 0.14);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
background: linear-gradient(180deg, rgba(9, 20, 38, 0.88), rgba(5, 12, 24, 0.84));
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fleet-tree-root::before,
|
|
||||||
.fleet-node-card::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: -1px;
|
|
||||||
top: -1px;
|
|
||||||
bottom: -1px;
|
|
||||||
width: 3px;
|
|
||||||
border-radius: 12px 0 0 12px;
|
|
||||||
background: rgba(126, 212, 255, 0.48);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fleet-tree-node {
|
|
||||||
position: relative;
|
|
||||||
margin-left: 18px;
|
|
||||||
padding-left: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fleet-tree-node::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 18px;
|
|
||||||
width: 1px;
|
|
||||||
background: rgba(126, 212, 255, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fleet-tree-node::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 18px;
|
|
||||||
width: 14px;
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(126, 212, 255, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ship-node .fleet-node-card::before {
|
|
||||||
background: rgba(255, 191, 105, 0.44);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fleet-node-title,
|
|
||||||
.fleet-node-meta {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fleet-node-title {
|
|
||||||
color: var(--text);
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fleet-node-meta {
|
|
||||||
color: var(--muted);
|
|
||||||
line-height: 1.4;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fleet-tree-root:hover,
|
|
||||||
.fleet-node-card:hover {
|
|
||||||
border-color: rgba(126, 212, 255, 0.32);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fleet-tree-root[data-selected="true"],
|
|
||||||
.fleet-node-card[data-selected="true"] {
|
|
||||||
border-color: rgba(255, 191, 105, 0.46);
|
|
||||||
box-shadow: inset 0 0 0 1px rgba(255, 191, 105, 0.16);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fleet-tree-root[data-selected="true"]::before,
|
|
||||||
.fleet-node-card[data-selected="true"]::before {
|
|
||||||
background: rgba(255, 191, 105, 0.72);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.details {
|
.details {
|
||||||
width: auto;
|
width: auto;
|
||||||
@@ -463,8 +374,7 @@ button:disabled {
|
|||||||
bottom: 16px;
|
bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.window-launchers,
|
.window-launchers {
|
||||||
.fleet-actions {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user