feat: adds fleet, windows, building
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,16 @@
|
||||
"storage": { "bulk-liquid": 600, "container": 400 },
|
||||
"modules": ["habitat-ring", "fabricator-array", "container-bay"]
|
||||
},
|
||||
{
|
||||
"id": "manufactory",
|
||||
"label": "Orbital Manufactory",
|
||||
"category": "station",
|
||||
"color": "#8df0d2",
|
||||
"radius": 24,
|
||||
"dockingCapacity": 3,
|
||||
"storage": { "manufactured": 2200, "container": 1600 },
|
||||
"modules": ["fabricator-array", "fabricator-array", "container-bay", "docking-clamps"]
|
||||
},
|
||||
{
|
||||
"id": "shipyard",
|
||||
"label": "Orbital Shipyard",
|
||||
@@ -48,5 +58,15 @@
|
||||
"dockingCapacity": 1,
|
||||
"storage": { "manufactured": 300 },
|
||||
"modules": ["turret-grid", "command-bridge"]
|
||||
},
|
||||
{
|
||||
"id": "stargate",
|
||||
"label": "Stargate",
|
||||
"category": "gate",
|
||||
"color": "#76f0ff",
|
||||
"radius": 34,
|
||||
"dockingCapacity": 0,
|
||||
"storage": { "manufactured": 2400, "container": 800 },
|
||||
"modules": ["ftl-core", "fabricator-array", "docking-clamps"]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -11,6 +11,36 @@
|
||||
"storage": "manufactured",
|
||||
"summary": "Processed structural metals used by stations and shipyards."
|
||||
},
|
||||
{
|
||||
"id": "hull-sections",
|
||||
"label": "Hull Sections",
|
||||
"storage": "manufactured",
|
||||
"summary": "Prefabricated structural assemblies for ships and stations."
|
||||
},
|
||||
{
|
||||
"id": "ammo-crates",
|
||||
"label": "Ammo Crates",
|
||||
"storage": "container",
|
||||
"summary": "Containerized magazines for turrets, launchers, and point defense."
|
||||
},
|
||||
{
|
||||
"id": "naval-guns",
|
||||
"label": "Naval Guns",
|
||||
"storage": "manufactured",
|
||||
"summary": "Shipboard turret and cannon assemblies."
|
||||
},
|
||||
{
|
||||
"id": "ship-equipment",
|
||||
"label": "Ship Equipment",
|
||||
"storage": "container",
|
||||
"summary": "Shield emitters, avionics, cooling loops, and service kits."
|
||||
},
|
||||
{
|
||||
"id": "ship-parts",
|
||||
"label": "Ship Parts",
|
||||
"storage": "manufactured",
|
||||
"summary": "High-value integration kits for hull fitting and final assembly."
|
||||
},
|
||||
{
|
||||
"id": "gas",
|
||||
"label": "Volatile Gas",
|
||||
@@ -28,5 +58,47 @@
|
||||
"label": "Drone Parts",
|
||||
"storage": "container",
|
||||
"summary": "Containerized industrial freight."
|
||||
},
|
||||
{
|
||||
"id": "trade-hub-kit",
|
||||
"label": "Trade Hub Kit",
|
||||
"storage": "manufactured",
|
||||
"summary": "Deployable prefab package for a trade hub station."
|
||||
},
|
||||
{
|
||||
"id": "refinery-kit",
|
||||
"label": "Refinery Kit",
|
||||
"storage": "manufactured",
|
||||
"summary": "Deployable prefab package for a refining station."
|
||||
},
|
||||
{
|
||||
"id": "farm-ring-kit",
|
||||
"label": "Farm Ring Kit",
|
||||
"storage": "manufactured",
|
||||
"summary": "Deployable prefab package for a farm station."
|
||||
},
|
||||
{
|
||||
"id": "manufactory-kit",
|
||||
"label": "Manufactory Kit",
|
||||
"storage": "manufactured",
|
||||
"summary": "Deployable prefab package for an orbital manufactory."
|
||||
},
|
||||
{
|
||||
"id": "shipyard-kit",
|
||||
"label": "Shipyard Kit",
|
||||
"storage": "manufactured",
|
||||
"summary": "Deployable prefab package for an orbital shipyard."
|
||||
},
|
||||
{
|
||||
"id": "defense-grid-kit",
|
||||
"label": "Defense Grid Kit",
|
||||
"storage": "manufactured",
|
||||
"summary": "Deployable prefab package for a defense platform."
|
||||
},
|
||||
{
|
||||
"id": "stargate-kit",
|
||||
"label": "Stargate Kit",
|
||||
"storage": "manufactured",
|
||||
"summary": "Deployable prefab package for a stargate structure."
|
||||
}
|
||||
]
|
||||
|
||||
@@ -41,6 +41,12 @@
|
||||
"category": "dock",
|
||||
"summary": "Docking collar and transfer arms."
|
||||
},
|
||||
{
|
||||
"id": "carrier-bay",
|
||||
"label": "Carrier Bay",
|
||||
"category": "dock",
|
||||
"summary": "Internal hangar decks and launch recovery systems."
|
||||
},
|
||||
{
|
||||
"id": "refinery-stack",
|
||||
"label": "Refinery Stack",
|
||||
|
||||
@@ -4,11 +4,255 @@
|
||||
"label": "Ore Refining",
|
||||
"facilityCategory": "refining",
|
||||
"duration": 8,
|
||||
"priority": 100,
|
||||
"inputs": [
|
||||
{ "itemId": "ore", "amount": 60 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "itemId": "refined-metals", "amount": 60 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ore-reclamation",
|
||||
"label": "Ore Reclamation",
|
||||
"facilityCategory": "station",
|
||||
"duration": 7,
|
||||
"priority": 8,
|
||||
"requiredModules": ["fabricator-array"],
|
||||
"inputs": [
|
||||
{ "itemId": "refined-metals", "amount": 16 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "itemId": "ore", "amount": 24 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "gas-synthesis",
|
||||
"label": "Gas Synthesis",
|
||||
"facilityCategory": "station",
|
||||
"duration": 6,
|
||||
"priority": 12,
|
||||
"requiredModules": ["fabricator-array"],
|
||||
"inputs": [
|
||||
{ "itemId": "refined-metals", "amount": 10 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "itemId": "gas", "amount": 20 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "water-reclamation",
|
||||
"label": "Water Reclamation",
|
||||
"facilityCategory": "farm",
|
||||
"duration": 6,
|
||||
"priority": 14,
|
||||
"requiredModules": ["fabricator-array"],
|
||||
"inputs": [
|
||||
{ "itemId": "gas", "amount": 8 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "itemId": "water", "amount": 18 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "drone-parts-assembly",
|
||||
"label": "Drone Parts Assembly",
|
||||
"facilityCategory": "station",
|
||||
"duration": 7,
|
||||
"priority": 18,
|
||||
"requiredModules": ["fabricator-array"],
|
||||
"inputs": [
|
||||
{ "itemId": "refined-metals", "amount": 12 },
|
||||
{ "itemId": "ship-equipment", "amount": 6 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "itemId": "drone-parts", "amount": 16 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "hull-fabrication",
|
||||
"label": "Hull Fabrication",
|
||||
"facilityCategory": "station",
|
||||
"duration": 10,
|
||||
"priority": 40,
|
||||
"requiredModules": ["fabricator-array"],
|
||||
"inputs": [
|
||||
{ "itemId": "refined-metals", "amount": 70 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "itemId": "hull-sections", "amount": 35 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ammo-fabrication",
|
||||
"label": "Ammo Fabrication",
|
||||
"facilityCategory": "station",
|
||||
"duration": 6,
|
||||
"priority": 34,
|
||||
"requiredModules": ["fabricator-array"],
|
||||
"inputs": [
|
||||
{ "itemId": "refined-metals", "amount": 24 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "itemId": "ammo-crates", "amount": 30 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "gun-assembly",
|
||||
"label": "Gun Assembly",
|
||||
"facilityCategory": "station",
|
||||
"duration": 9,
|
||||
"priority": 32,
|
||||
"requiredModules": ["fabricator-array"],
|
||||
"inputs": [
|
||||
{ "itemId": "refined-metals", "amount": 36 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "itemId": "naval-guns", "amount": 12 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "equipment-assembly",
|
||||
"label": "Equipment Assembly",
|
||||
"facilityCategory": "station",
|
||||
"duration": 11,
|
||||
"priority": 30,
|
||||
"requiredModules": ["fabricator-array"],
|
||||
"inputs": [
|
||||
{ "itemId": "refined-metals", "amount": 28 },
|
||||
{ "itemId": "water", "amount": 8 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "itemId": "ship-equipment", "amount": 18 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ship-parts-integration",
|
||||
"label": "Ship Parts Integration",
|
||||
"facilityCategory": "station",
|
||||
"duration": 14,
|
||||
"priority": 50,
|
||||
"requiredModules": ["fabricator-array"],
|
||||
"inputs": [
|
||||
{ "itemId": "hull-sections", "amount": 24 },
|
||||
{ "itemId": "naval-guns", "amount": 6 },
|
||||
{ "itemId": "ship-equipment", "amount": 10 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "itemId": "ship-parts", "amount": 20 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "trade-hub-assembly",
|
||||
"label": "Trade Hub Assembly",
|
||||
"facilityCategory": "station",
|
||||
"duration": 18,
|
||||
"priority": 24,
|
||||
"requiredModules": ["fabricator-array"],
|
||||
"inputs": [
|
||||
{ "itemId": "ship-parts", "amount": 26 },
|
||||
{ "itemId": "ship-equipment", "amount": 16 },
|
||||
{ "itemId": "drone-parts", "amount": 10 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "itemId": "trade-hub-kit", "amount": 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "refinery-assembly",
|
||||
"label": "Refinery Assembly",
|
||||
"facilityCategory": "station",
|
||||
"duration": 20,
|
||||
"priority": 26,
|
||||
"requiredModules": ["fabricator-array"],
|
||||
"inputs": [
|
||||
{ "itemId": "ship-parts", "amount": 32 },
|
||||
{ "itemId": "hull-sections", "amount": 24 },
|
||||
{ "itemId": "ship-equipment", "amount": 14 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "itemId": "refinery-kit", "amount": 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "farm-ring-assembly",
|
||||
"label": "Farm Ring Assembly",
|
||||
"facilityCategory": "station",
|
||||
"duration": 18,
|
||||
"priority": 22,
|
||||
"requiredModules": ["fabricator-array"],
|
||||
"inputs": [
|
||||
{ "itemId": "ship-parts", "amount": 22 },
|
||||
{ "itemId": "ship-equipment", "amount": 18 },
|
||||
{ "itemId": "water", "amount": 22 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "itemId": "farm-ring-kit", "amount": 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "manufactory-assembly",
|
||||
"label": "Manufactory Assembly",
|
||||
"facilityCategory": "station",
|
||||
"duration": 22,
|
||||
"priority": 28,
|
||||
"requiredModules": ["fabricator-array"],
|
||||
"inputs": [
|
||||
{ "itemId": "ship-parts", "amount": 34 },
|
||||
{ "itemId": "hull-sections", "amount": 16 },
|
||||
{ "itemId": "ship-equipment", "amount": 18 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "itemId": "manufactory-kit", "amount": 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "shipyard-assembly",
|
||||
"label": "Shipyard Assembly",
|
||||
"facilityCategory": "station",
|
||||
"duration": 26,
|
||||
"priority": 30,
|
||||
"requiredModules": ["fabricator-array"],
|
||||
"inputs": [
|
||||
{ "itemId": "ship-parts", "amount": 42 },
|
||||
{ "itemId": "hull-sections", "amount": 30 },
|
||||
{ "itemId": "naval-guns", "amount": 10 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "itemId": "shipyard-kit", "amount": 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "defense-grid-assembly",
|
||||
"label": "Defense Grid Assembly",
|
||||
"facilityCategory": "station",
|
||||
"duration": 16,
|
||||
"priority": 20,
|
||||
"requiredModules": ["fabricator-array"],
|
||||
"inputs": [
|
||||
{ "itemId": "ship-parts", "amount": 18 },
|
||||
{ "itemId": "naval-guns", "amount": 12 },
|
||||
{ "itemId": "ammo-crates", "amount": 18 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "itemId": "defense-grid-kit", "amount": 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "stargate-assembly",
|
||||
"label": "Stargate Assembly",
|
||||
"facilityCategory": "station",
|
||||
"duration": 34,
|
||||
"priority": 36,
|
||||
"requiredModules": ["fabricator-array"],
|
||||
"inputs": [
|
||||
{ "itemId": "ship-parts", "amount": 60 },
|
||||
{ "itemId": "hull-sections", "amount": 44 },
|
||||
{ "itemId": "ship-equipment", "amount": 26 },
|
||||
{ "itemId": "naval-guns", "amount": 8 }
|
||||
],
|
||||
"outputs": [
|
||||
{ "itemId": "stargate-kit", "amount": 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -7,10 +7,13 @@
|
||||
{ "constructibleId": "defense-grid", "systemId": "helios", "planetIndex": 2, "lagrangeSide": 1 }
|
||||
],
|
||||
"shipFormations": [
|
||||
{ "shipId": "carrier", "count": 1, "center": [120, 0, 60], "systemId": "helios" },
|
||||
{ "shipId": "frigate", "count": 6, "center": [180, 0, 90], "systemId": "helios" },
|
||||
{ "shipId": "destroyer", "count": 3, "center": [260, 0, 120], "systemId": "helios" },
|
||||
{ "shipId": "cruiser", "count": 2, "center": [220, 0, 180], "systemId": "helios" },
|
||||
{ "shipId": "hauler", "count": 4, "center": [310, 0, -150], "systemId": "helios" },
|
||||
{ "shipId": "frigate", "count": 4, "center": [4350, 0, 560], "systemId": "perseus" },
|
||||
{ "shipId": "cruiser", "count": 1, "center": [4430, 0, 640], "systemId": "perseus" },
|
||||
{ "shipId": "miner", "count": 6, "center": [4620, 0, 700], "systemId": "perseus" }
|
||||
],
|
||||
"patrolRoutes": [
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"id": "frigate",
|
||||
"label": "Vanguard Frigate",
|
||||
"role": "military",
|
||||
"shipClass": "frigate",
|
||||
"speed": 50,
|
||||
"ftlSpeed": 3200,
|
||||
"spoolTime": 2.2,
|
||||
@@ -17,6 +18,7 @@
|
||||
"id": "destroyer",
|
||||
"label": "Bulwark Destroyer",
|
||||
"role": "military",
|
||||
"shipClass": "destroyer",
|
||||
"speed": 34,
|
||||
"ftlSpeed": 2900,
|
||||
"spoolTime": 2.8,
|
||||
@@ -27,10 +29,43 @@
|
||||
"maxHealth": 240,
|
||||
"modules": ["command-bridge", "ion-drive", "ftl-core", "turret-grid", "turret-grid"]
|
||||
},
|
||||
{
|
||||
"id": "cruiser",
|
||||
"label": "Aegis Cruiser",
|
||||
"role": "military",
|
||||
"shipClass": "cruiser",
|
||||
"speed": 28,
|
||||
"ftlSpeed": 2750,
|
||||
"spoolTime": 3.1,
|
||||
"cargoCapacity": 0,
|
||||
"color": "#9ec1ff",
|
||||
"hullColor": "#314562",
|
||||
"size": 10,
|
||||
"maxHealth": 340,
|
||||
"modules": ["command-bridge", "ion-drive", "ftl-core", "turret-grid", "turret-grid", "docking-clamps"]
|
||||
},
|
||||
{
|
||||
"id": "carrier",
|
||||
"label": "Citadel Carrier",
|
||||
"role": "military",
|
||||
"shipClass": "capital",
|
||||
"speed": 18,
|
||||
"ftlSpeed": 2500,
|
||||
"spoolTime": 4.1,
|
||||
"cargoCapacity": 0,
|
||||
"color": "#c6f4ff",
|
||||
"hullColor": "#35586d",
|
||||
"size": 16,
|
||||
"maxHealth": 900,
|
||||
"modules": ["command-bridge", "ion-drive", "ftl-core", "carrier-bay", "carrier-bay", "turret-grid", "habitat-ring"],
|
||||
"dockingCapacity": 6,
|
||||
"dockingClasses": ["frigate", "destroyer", "cruiser"]
|
||||
},
|
||||
{
|
||||
"id": "hauler",
|
||||
"label": "Atlas Hauler",
|
||||
"role": "transport",
|
||||
"shipClass": "industrial",
|
||||
"speed": 22,
|
||||
"ftlSpeed": 2600,
|
||||
"spoolTime": 3.3,
|
||||
@@ -47,6 +82,7 @@
|
||||
"id": "miner",
|
||||
"label": "Prospector Miner",
|
||||
"role": "mining",
|
||||
"shipClass": "industrial",
|
||||
"speed": 26,
|
||||
"ftlSpeed": 2400,
|
||||
"spoolTime": 3.1,
|
||||
|
||||
236
src/game/fleet/runtime.ts
Normal file
236
src/game/fleet/runtime.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import * as THREE from "three";
|
||||
import type { FleetBehavior, FleetInstance, FleetWingInstance, ShipInstance } from "../types";
|
||||
|
||||
interface FleetBuildSpec {
|
||||
id: string;
|
||||
label: string;
|
||||
stance: FleetInstance["stance"];
|
||||
systemId: string;
|
||||
commander: ShipInstance;
|
||||
wings: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
behavior: FleetBehavior;
|
||||
parentWingId?: string;
|
||||
ships: ShipInstance[];
|
||||
}>;
|
||||
}
|
||||
|
||||
export function createDefaultFleets(ships: ShipInstance[]) {
|
||||
clearFleetAssignments(ships);
|
||||
|
||||
const heliosMilitary = ships.filter((ship) => ship.systemId === "helios" && ship.definition.role === "military");
|
||||
const heliosCarriers = heliosMilitary.filter((ship) => ship.definition.shipClass === "capital");
|
||||
const heliosDestroyers = heliosMilitary.filter((ship) => ship.definition.id === "destroyer");
|
||||
const allHaulers = ships.filter((ship) => ship.definition.role === "transport");
|
||||
const perseusMilitary = ships.filter((ship) => ship.systemId === "perseus" && ship.definition.role === "military");
|
||||
const miners = ships.filter((ship) => ship.definition.role === "mining");
|
||||
|
||||
const miningHaulers = allHaulers.slice(0, 2);
|
||||
const homeHaulers = allHaulers.slice(2);
|
||||
|
||||
const specs: FleetBuildSpec[] = [];
|
||||
|
||||
const homeCommander = heliosCarriers[0] ?? heliosDestroyers[0] ?? heliosMilitary[0];
|
||||
if (homeCommander) {
|
||||
const homeScreenShips = heliosMilitary.filter(
|
||||
(ship) => ship.id !== homeCommander.id && ship.definition.shipClass !== "destroyer",
|
||||
);
|
||||
specs.push({
|
||||
id: "helios-home-fleet",
|
||||
label: "Helios Home Fleet",
|
||||
stance: "defensive",
|
||||
systemId: "helios",
|
||||
commander: homeCommander,
|
||||
wings: [
|
||||
{
|
||||
id: "command",
|
||||
label: "Command Wing",
|
||||
behavior: "command",
|
||||
ships: [homeCommander, ...heliosDestroyers.slice(1)],
|
||||
},
|
||||
{
|
||||
id: "screen",
|
||||
label: "Screen Wing",
|
||||
behavior: "screen",
|
||||
parentWingId: "command",
|
||||
ships: homeScreenShips,
|
||||
},
|
||||
{
|
||||
id: "logistics",
|
||||
label: "Logistics Wing",
|
||||
behavior: "logistics",
|
||||
parentWingId: "command",
|
||||
ships: homeHaulers,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const extractionCommander = perseusMilitary[0] ?? miners[0];
|
||||
if (extractionCommander) {
|
||||
specs.push({
|
||||
id: "perseus-extraction-fleet",
|
||||
label: "Perseus Extraction Group",
|
||||
stance: "industrial",
|
||||
systemId: "perseus",
|
||||
commander: extractionCommander,
|
||||
wings: [
|
||||
{
|
||||
id: "command",
|
||||
label: "Command Wing",
|
||||
behavior: "command",
|
||||
ships: [extractionCommander],
|
||||
},
|
||||
{
|
||||
id: "miners",
|
||||
label: "Mining Wing",
|
||||
behavior: "mining",
|
||||
parentWingId: "command",
|
||||
ships: miners,
|
||||
},
|
||||
{
|
||||
id: "escort",
|
||||
label: "Escort Wing",
|
||||
behavior: "escort",
|
||||
parentWingId: "miners",
|
||||
ships: perseusMilitary.filter((ship) => ship.id !== extractionCommander.id),
|
||||
},
|
||||
{
|
||||
id: "transport",
|
||||
label: "Transport Wing",
|
||||
behavior: "logistics",
|
||||
parentWingId: "miners",
|
||||
ships: miningHaulers,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
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);
|
||||
}
|
||||
}
|
||||
76
src/game/state/selectionManager.ts
Normal file
76
src/game/state/selectionManager.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import * as THREE from "three";
|
||||
import type { ShipInstance, StationInstance } from "../types";
|
||||
|
||||
export class SelectionManager {
|
||||
private shipSelection: ShipInstance[] = [];
|
||||
private stationSelection?: StationInstance;
|
||||
|
||||
getShips() {
|
||||
return this.shipSelection;
|
||||
}
|
||||
|
||||
getStation() {
|
||||
return this.stationSelection;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.shipSelection.forEach((ship) => this.setShipVisual(ship, false));
|
||||
this.shipSelection = [];
|
||||
if (this.stationSelection) {
|
||||
this.setStationVisual(this.stationSelection, false);
|
||||
this.stationSelection = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
replaceShips(ships: ShipInstance[]) {
|
||||
this.clear();
|
||||
ships.forEach((ship) => this.addShip(ship));
|
||||
}
|
||||
|
||||
setStation(station?: StationInstance) {
|
||||
this.clear();
|
||||
if (!station) {
|
||||
return;
|
||||
}
|
||||
this.stationSelection = station;
|
||||
this.setStationVisual(station, true);
|
||||
}
|
||||
|
||||
addShip(ship: ShipInstance) {
|
||||
if (this.shipSelection.includes(ship)) {
|
||||
return;
|
||||
}
|
||||
if (this.stationSelection) {
|
||||
this.setStationVisual(this.stationSelection, false);
|
||||
this.stationSelection = undefined;
|
||||
}
|
||||
this.shipSelection.push(ship);
|
||||
this.setShipVisual(ship, true);
|
||||
}
|
||||
|
||||
removeShip(ship: ShipInstance) {
|
||||
this.shipSelection = this.shipSelection.filter((candidate) => candidate.id !== ship.id);
|
||||
this.setShipVisual(ship, false);
|
||||
}
|
||||
|
||||
toggleShip(ship: ShipInstance) {
|
||||
if (this.shipSelection.includes(ship)) {
|
||||
this.removeShip(ship);
|
||||
return;
|
||||
}
|
||||
this.addShip(ship);
|
||||
}
|
||||
|
||||
hasShip(ship: ShipInstance) {
|
||||
return this.shipSelection.includes(ship);
|
||||
}
|
||||
|
||||
private setShipVisual(ship: ShipInstance, selected: boolean) {
|
||||
ship.selected = selected;
|
||||
(ship.ring.material as THREE.MeshBasicMaterial).opacity = selected ? 0.95 : 0;
|
||||
}
|
||||
|
||||
private setStationVisual(station: StationInstance, selected: boolean) {
|
||||
(station.ring.material as THREE.MeshBasicMaterial).opacity = selected ? 0.95 : 0;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
import * as THREE from "three";
|
||||
|
||||
export type ShipRole = "military" | "transport" | "mining";
|
||||
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";
|
||||
export type ConstructibleCategory =
|
||||
| "station"
|
||||
| "refining"
|
||||
| "farm"
|
||||
| "shipyard"
|
||||
| "defense";
|
||||
| "defense"
|
||||
| "gate";
|
||||
export type UnitState =
|
||||
| "idle"
|
||||
| "moving"
|
||||
@@ -22,8 +27,9 @@ export type UnitState =
|
||||
| "docked"
|
||||
| "undocking"
|
||||
| "patrolling"
|
||||
| "escorting";
|
||||
export type UnitOrderKind = "idle" | "move" | "transfer" | "mine" | "patrol" | "escort";
|
||||
| "escorting"
|
||||
| "forming";
|
||||
export type UnitOrderKind = "idle" | "move" | "transfer" | "mine" | "patrol" | "escort" | "dock";
|
||||
export type ItemStorageKind = "bulk-solid" | "bulk-liquid" | "bulk-gas" | "container" | "manufactured";
|
||||
export type ModuleCategory =
|
||||
| "bridge"
|
||||
@@ -63,6 +69,8 @@ export interface RecipeDefinition {
|
||||
label: string;
|
||||
facilityCategory: ConstructibleCategory;
|
||||
duration: number;
|
||||
priority?: number;
|
||||
requiredModules?: string[];
|
||||
inputs: RecipeComponentDefinition[];
|
||||
outputs: RecipeComponentDefinition[];
|
||||
}
|
||||
@@ -71,6 +79,7 @@ export interface ShipDefinition {
|
||||
id: string;
|
||||
label: string;
|
||||
role: ShipRole;
|
||||
shipClass: ShipClass;
|
||||
speed: number;
|
||||
ftlSpeed: number;
|
||||
spoolTime: number;
|
||||
@@ -82,6 +91,8 @@ export interface ShipDefinition {
|
||||
size: number;
|
||||
maxHealth: number;
|
||||
modules: string[];
|
||||
dockingCapacity?: number;
|
||||
dockingClasses?: ShipClass[];
|
||||
}
|
||||
|
||||
export interface ConstructibleDefinition {
|
||||
@@ -153,6 +164,12 @@ export interface PatrolRouteDefinition {
|
||||
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 {
|
||||
initialStations: InitialStationDefinition[];
|
||||
shipFormations: ShipFormationDefinition[];
|
||||
@@ -193,7 +210,8 @@ export type UnitOrder =
|
||||
}
|
||||
| { kind: "mine"; nodeId: string; refineryId: string; phase: "to-node" | "mining" | "to-refinery" | "transfer" }
|
||||
| { kind: "patrol"; points: THREE.Vector3[]; systemId: string; index: number }
|
||||
| { kind: "escort"; targetShipId: string; offset: THREE.Vector3 };
|
||||
| { kind: "escort"; targetShipId: string; offset: THREE.Vector3 }
|
||||
| { kind: "dock"; carrierShipId: string };
|
||||
|
||||
export interface InventoryState {
|
||||
"bulk-solid": number;
|
||||
@@ -210,6 +228,27 @@ export interface TravelPlan {
|
||||
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;
|
||||
shipIds: string[];
|
||||
wings: FleetWingInstance[];
|
||||
order: FleetOrder;
|
||||
}
|
||||
|
||||
export interface ShipInstance {
|
||||
id: string;
|
||||
definition: ShipDefinition;
|
||||
@@ -226,6 +265,7 @@ export interface ShipInstance {
|
||||
actionTimer: number;
|
||||
travelPlan?: TravelPlan;
|
||||
dockedStationId?: string;
|
||||
dockedCarrierId?: string;
|
||||
dockingPortIndex?: number;
|
||||
fuel: number;
|
||||
energy: number;
|
||||
@@ -234,6 +274,14 @@ export interface ShipInstance {
|
||||
idleOrbitRadius: number;
|
||||
idleOrbitAngle: number;
|
||||
warpFx: THREE.Group;
|
||||
fleetId?: string;
|
||||
wingId?: string;
|
||||
behavior: FleetBehavior | "independent";
|
||||
isFleetCommander: boolean;
|
||||
isWingLeader: boolean;
|
||||
formationOffset: THREE.Vector3;
|
||||
dockedShipIds: Set<string>;
|
||||
dockingPorts: THREE.Vector3[];
|
||||
}
|
||||
|
||||
export interface StationInstance {
|
||||
@@ -248,6 +296,7 @@ export interface StationInstance {
|
||||
activeBatch: number;
|
||||
activeRecipeId?: string;
|
||||
inventory: InventoryState;
|
||||
itemStocks: Record<string, number>;
|
||||
dockedShipIds: Set<string>;
|
||||
dockingPorts: THREE.Vector3[];
|
||||
modules: string[];
|
||||
@@ -302,4 +351,8 @@ export interface HudElements {
|
||||
marquee: HTMLDivElement;
|
||||
strategicOverlay: HTMLCanvasElement;
|
||||
strategicOverlayContext: CanvasRenderingContext2D;
|
||||
fleetWindow: HTMLDivElement;
|
||||
fleetWindowBody: HTMLDivElement;
|
||||
fleetWindowTitle: HTMLHeadingElement;
|
||||
fleetWindowSubtitle: HTMLParagraphElement;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import type { HudElements } from "../types";
|
||||
|
||||
export function createHud(container: HTMLElement, onOrderAction: (action: string) => void): HudElements {
|
||||
interface HudHandlers {
|
||||
onOrderAction: (action: string) => void;
|
||||
onWindowAction: (action: string) => void;
|
||||
onFleetAction: (action: string, fleetId?: string) => void;
|
||||
onSelectionAction: (kind: string, id: string) => void;
|
||||
}
|
||||
|
||||
export function createHud(container: HTMLElement, handlers: HudHandlers): HudElements {
|
||||
const root = document.createElement("div");
|
||||
root.className = "hud";
|
||||
root.innerHTML = `
|
||||
@@ -9,7 +16,7 @@ export function createHud(container: HTMLElement, onOrderAction: (action: string
|
||||
<h1>Helios Reach Command</h1>
|
||||
<p>
|
||||
Dual-star-system prototype with gravity-well exits, FTL spooling, inter-system travel,
|
||||
and unit orders for patrol, escort, mining, and manual fleet movement.
|
||||
and layered fleet command with wing behaviors, escort screens, and logistics groups.
|
||||
</p>
|
||||
</section>
|
||||
<section class="panel details">
|
||||
@@ -23,25 +30,69 @@ export function createHud(container: HTMLElement, onOrderAction: (action: string
|
||||
</div>
|
||||
<div class="orders-panel">
|
||||
<div class="mode"></div>
|
||||
<div class="window-launchers">
|
||||
<button type="button" data-window-action="toggle-fleet-command">Fleets</button>
|
||||
<button type="button" data-window-action="toggle-ship-designer" disabled>Designer</button>
|
||||
<button type="button" data-window-action="toggle-station-manager" disabled>Stations</button>
|
||||
</div>
|
||||
<div class="orders">
|
||||
<button type="button" data-action="move">Move</button>
|
||||
<button type="button" data-action="mine">Mine</button>
|
||||
<button type="button" data-action="patrol">Patrol</button>
|
||||
<button type="button" data-action="escort">Escort</button>
|
||||
<button type="button" data-action="dock">Dock</button>
|
||||
<button type="button" data-action="focus">Focus</button>
|
||||
</div>
|
||||
<div class="hint">Left click select ships or stations. Shift+click adds ships. Right click moves selected ships. Mouse wheel or -/= zoom. B build. 1-5 constructible. M miners mine. P patrol. E escort. Tab jump systems. F focus/follow.</div>
|
||||
<div class="hint">Left click selects ships or stations. Shift adds. Right click moves selection, or the active fleet when nothing is selected. G toggles fleet command. R recovers selected ships to the nearest friendly carrier. Mouse wheel or -/= zoom. B build. M miners mine. P patrol. E escort.</div>
|
||||
</div>
|
||||
<div class="minimap-panel">
|
||||
<canvas class="minimap" width="220" height="160"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
<section class="app-window fleet-window" data-window-id="fleet-command">
|
||||
<div class="window-header">
|
||||
<div>
|
||||
<h2>Fleet Command</h2>
|
||||
<p class="window-subtitle">No fleet selected</p>
|
||||
</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 class="window-body fleet-window-body"></div>
|
||||
<div class="window-resize-handle" aria-hidden="true"></div>
|
||||
</section>
|
||||
<div class="marquee"></div>
|
||||
`;
|
||||
|
||||
container.append(root);
|
||||
initializeWindowInteractions(root);
|
||||
root.querySelectorAll<HTMLButtonElement>(".orders button").forEach((button) => {
|
||||
button.addEventListener("click", () => onOrderAction(button.dataset.action ?? ""));
|
||||
button.addEventListener("click", () => handlers.onOrderAction(button.dataset.action ?? ""));
|
||||
});
|
||||
root.querySelectorAll<HTMLButtonElement>("[data-window-action]").forEach((button) => {
|
||||
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");
|
||||
fleetWindowBody?.addEventListener("click", (event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const selectionNode = target.closest<HTMLElement>("[data-select-kind][data-select-id]");
|
||||
if (selectionNode) {
|
||||
handlers.onSelectionAction(selectionNode.dataset.selectKind ?? "", selectionNode.dataset.selectId ?? "");
|
||||
return;
|
||||
}
|
||||
const fleetButton = target.closest<HTMLButtonElement>("[data-fleet-id]");
|
||||
if (fleetButton?.dataset.fleetId) {
|
||||
handlers.onFleetAction("select", fleetButton.dataset.fleetId);
|
||||
}
|
||||
});
|
||||
|
||||
const minimap = root.querySelector<HTMLCanvasElement>(".minimap");
|
||||
@@ -66,5 +117,93 @@ export function createHud(container: HTMLElement, onOrderAction: (action: string
|
||||
marquee: root.querySelector(".marquee") as HTMLDivElement,
|
||||
strategicOverlay,
|
||||
strategicOverlayContext,
|
||||
fleetWindow: root.querySelector(".fleet-window") as HTMLDivElement,
|
||||
fleetWindowBody: fleetWindowBody as HTMLDivElement,
|
||||
fleetWindowTitle: root.querySelector(".fleet-window h2") as HTMLHeadingElement,
|
||||
fleetWindowSubtitle: root.querySelector(".window-subtitle") as HTMLParagraphElement,
|
||||
};
|
||||
}
|
||||
|
||||
function initializeWindowInteractions(root: HTMLDivElement) {
|
||||
root.querySelectorAll<HTMLElement>(".app-window").forEach((windowEl) => {
|
||||
initializeWindowPosition(windowEl);
|
||||
|
||||
const header = windowEl.querySelector<HTMLElement>(".window-header");
|
||||
const resizeHandle = windowEl.querySelector<HTMLElement>(".window-resize-handle");
|
||||
|
||||
header?.addEventListener("pointerdown", (event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.closest("button")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = windowEl.getBoundingClientRect();
|
||||
const offsetX = event.clientX - rect.left;
|
||||
const offsetY = event.clientY - rect.top;
|
||||
windowEl.dataset.dragging = "true";
|
||||
|
||||
const move = (moveEvent: PointerEvent) => {
|
||||
const nextLeft = moveEvent.clientX - offsetX;
|
||||
const nextTop = moveEvent.clientY - offsetY;
|
||||
applyWindowRect(windowEl, nextLeft, nextTop, rect.width, rect.height);
|
||||
};
|
||||
|
||||
const end = () => {
|
||||
windowEl.dataset.dragging = "false";
|
||||
window.removeEventListener("pointermove", move);
|
||||
window.removeEventListener("pointerup", end);
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", move);
|
||||
window.addEventListener("pointerup", end);
|
||||
});
|
||||
|
||||
resizeHandle?.addEventListener("pointerdown", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const rect = windowEl.getBoundingClientRect();
|
||||
const startX = event.clientX;
|
||||
const startY = event.clientY;
|
||||
windowEl.dataset.resizing = "true";
|
||||
|
||||
const move = (moveEvent: PointerEvent) => {
|
||||
const nextWidth = rect.width + (moveEvent.clientX - startX);
|
||||
const nextHeight = rect.height + (moveEvent.clientY - startY);
|
||||
applyWindowRect(windowEl, rect.left, rect.top, nextWidth, nextHeight);
|
||||
};
|
||||
|
||||
const end = () => {
|
||||
windowEl.dataset.resizing = "false";
|
||||
window.removeEventListener("pointermove", move);
|
||||
window.removeEventListener("pointerup", end);
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", move);
|
||||
window.addEventListener("pointerup", end);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initializeWindowPosition(windowEl: HTMLElement) {
|
||||
const defaultWidth = Math.min(480, window.innerWidth - 48);
|
||||
const defaultHeight = Math.min(680, Math.floor(window.innerHeight * 0.68));
|
||||
const left = Math.max(16, Math.round(window.innerWidth * 0.5 - defaultWidth * 0.5));
|
||||
const top = Math.max(24, 104);
|
||||
applyWindowRect(windowEl, left, top, defaultWidth, defaultHeight);
|
||||
}
|
||||
|
||||
function applyWindowRect(windowEl: HTMLElement, left: number, top: number, width: number, height: number) {
|
||||
const minWidth = 340;
|
||||
const minHeight = 240;
|
||||
const maxWidth = window.innerWidth - 32;
|
||||
const maxHeight = window.innerHeight - 32;
|
||||
const clampedWidth = Math.max(minWidth, Math.min(width, maxWidth));
|
||||
const clampedHeight = Math.max(minHeight, Math.min(height, maxHeight));
|
||||
const clampedLeft = Math.max(16, Math.min(left, window.innerWidth - clampedWidth - 16));
|
||||
const clampedTop = Math.max(16, Math.min(top, window.innerHeight - clampedHeight - 16));
|
||||
|
||||
windowEl.style.left = `${clampedLeft}px`;
|
||||
windowEl.style.top = `${clampedTop}px`;
|
||||
windowEl.style.width = `${clampedWidth}px`;
|
||||
windowEl.style.height = `${clampedHeight}px`;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ import {
|
||||
moduleDefinitionsById,
|
||||
recipeDefinitions,
|
||||
} from "../data/catalog";
|
||||
import { describeFleetOrder } from "../fleet/runtime";
|
||||
import { getShipCargoAmount } from "../state/inventory";
|
||||
import type {
|
||||
FleetInstance,
|
||||
ShipInstance,
|
||||
SolarSystemInstance,
|
||||
StationInstance,
|
||||
@@ -30,45 +32,167 @@ export function getSelectionDetails(
|
||||
systems: SolarSystemInstance[],
|
||||
viewLevel: ViewLevel,
|
||||
ships: ShipInstance[],
|
||||
fleets: FleetInstance[],
|
||||
) {
|
||||
if (selectedStation) {
|
||||
return describeStation(selectedStation, ships);
|
||||
return describeStation(selectedStation, ships, fleets);
|
||||
}
|
||||
if (selection.length === 0) {
|
||||
return `Systems online: ${systems.map((system) => system.definition.label).join(", ")}\n\nOrders: Move, Patrol, Escort, Mine\nView: ${viewLevel}`;
|
||||
return `Systems online: ${systems.map((system) => system.definition.label).join(", ")}\nFleets active: ${fleets.length}\n\nOrders: Move, Patrol, Escort, Mine\nView: ${viewLevel}`;
|
||||
}
|
||||
|
||||
return selection
|
||||
.map(
|
||||
(ship) =>
|
||||
`${ship.definition.label} • ${ship.systemId}\nState: ${ship.state}${ship.dockedStationId ? ` @ ${ship.dockedStationId}` : ""}\nOrder: ${ship.order.kind}\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"}\nModules: ${ship.definition.modules.map(getModuleLabel).join(", ")}`,
|
||||
(ship) => {
|
||||
const dockedAt = ship.dockedCarrierId ?? ship.dockedStationId;
|
||||
const hangarStatus =
|
||||
ship.definition.dockingCapacity && ship.definition.dockingCapacity > 0
|
||||
? `\nHangar: ${ship.dockedShipIds.size}/${ship.definition.dockingCapacity} for ${(ship.definition.dockingClasses ?? []).join(", ")}`
|
||||
: "";
|
||||
return `${ship.definition.label} • ${ship.systemId}\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}\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");
|
||||
}
|
||||
|
||||
export function describeStation(station: StationInstance, ships: ShipInstance[]) {
|
||||
export function describeStation(station: StationInstance, ships: ShipInstance[], fleets: FleetInstance[]) {
|
||||
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 patrols = ships.filter((ship) => ship.systemId === station.systemId && ship.order.kind === "patrol").length;
|
||||
const localFleets = fleets.filter((fleet) => fleet.systemId === station.systemId).length;
|
||||
const activeRecipe = station.activeRecipeId
|
||||
? recipeDefinitions.find((recipe) => recipe.id === station.activeRecipeId)
|
||||
: undefined;
|
||||
const refineryStatus =
|
||||
station.definition.category === "refining"
|
||||
? `Ore: ${Math.round(station.oreStored)}\nRefined: ${Math.round(station.refinedStock)}\nBatch: ${Math.round(station.activeBatch)}\nRecipe: ${activeRecipe?.label ?? "Idle"}\nTime Remaining: ${station.activeBatch > 0 ? `${station.processTimer.toFixed(1)}s` : "Idle"}\n`
|
||||
const stockSummary = Object.entries(station.itemStocks)
|
||||
.filter(([, amount]) => amount > 0)
|
||||
.sort((left, right) => right[1] - left[1])
|
||||
.slice(0, 5)
|
||||
.map(([itemId, amount]) => `${getItemLabel(itemId)} ${Math.round(amount)}`)
|
||||
.join(", ");
|
||||
const productionStatus =
|
||||
station.modules.includes("fabricator-array") || station.definition.category === "refining"
|
||||
? `Ore: ${Math.round(station.oreStored)}\nRefined Metals: ${Math.round(station.refinedStock)}\nBatch: ${Math.round(station.activeBatch)}\nRecipe: ${activeRecipe?.label ?? "Idle"}\nTime Remaining: ${station.activeBatch > 0 ? `${station.processTimer.toFixed(1)}s` : "Idle"}\nStocks: ${stockSummary || "None"}\n`
|
||||
: "";
|
||||
const activity =
|
||||
station.definition.category === "refining"
|
||||
? `Refining ore for ${miners} mining ships`
|
||||
? `Refining and fabricating for ${miners} mining ships`
|
||||
: station.definition.category === "shipyard"
|
||||
? `Maintaining ${patrols} patrol craft`
|
||||
? `Building ship parts for ${patrols} patrol craft`
|
||||
: station.definition.category === "farm"
|
||||
? "Supplying agricultural goods"
|
||||
? "Supplying agricultural goods and industrial consumables"
|
||||
: station.definition.category === "defense"
|
||||
? `Coordinating ${escorts} escort wings`
|
||||
: station.definition.category === "gate"
|
||||
? "Assembling transit infrastructure and gate components"
|
||||
: station.modules.includes("fabricator-array")
|
||||
? "Fabricating industrial parts and equipment"
|
||||
: "Managing local trade traffic";
|
||||
|
||||
return `${station.definition.label} • ${station.systemId}\nRole: ${station.definition.category}\nActivity: ${activity}\nDocking: ${station.dockedShipIds.size}/${station.definition.dockingCapacity}\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${refineryStatus}Radius: ${station.definition.radius}`;
|
||||
return `${station.definition.label} • ${station.systemId}\nRole: ${station.definition.category}\nActivity: ${activity}\nLocal Fleets: ${localFleets}\nDocking: ${station.dockedShipIds.size}/${station.definition.dockingCapacity}\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(
|
||||
fleets: FleetInstance[],
|
||||
shipsById: Map<string, ShipInstance>,
|
||||
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));
|
||||
|
||||
return fleets
|
||||
.map((fleet) => {
|
||||
const commander = shipsById.get(fleet.commanderShipId);
|
||||
const rootWings = fleet.wings.filter((wing) => !wing.parentWingId);
|
||||
const tree = rootWings.map((wing) => renderWingNode(fleet, wing.id, shipsById, selectedShipIds)).join("");
|
||||
const fleetSelected = fleet.shipIds.length > 0 && fleet.shipIds.every((shipId) => selectedShipIds.has(shipId));
|
||||
|
||||
return `
|
||||
<article class="fleet-card" data-active="${fleet.id === activeFleetId}">
|
||||
<button type="button" class="fleet-select" data-select-kind="fleet" data-select-id="${fleet.id}">${fleet.label}</button>
|
||||
<div class="fleet-tree">
|
||||
<div class="fleet-tree-root" data-select-kind="fleet" data-select-id="${fleet.id}" data-selected="${fleetSelected}">
|
||||
<span class="fleet-card-title">${commander?.definition.label ?? fleet.commanderShipId}</span>
|
||||
<span class="fleet-card-line">Commander • ${fleet.shipIds.length} ships • ${fleet.systemId}</span>
|
||||
<span class="fleet-card-line">Fleet Order: ${describeFleetOrder(fleet)}</span>
|
||||
<span class="fleet-card-line">Stance: ${fleet.stance}</span>
|
||||
</div>
|
||||
<div class="fleet-tree-children">${tree}</div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
.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 {
|
||||
return `${ship.definition.shipClass} • ${ship.state} • ${ship.order.kind} • ${ship.behavior}`;
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as THREE from "three";
|
||||
import type { ShipRole, ShipInstance, SolarSystemInstance, StationInstance, ViewLevel } from "../types";
|
||||
import { getFleetCommander, getWingLeader } from "../fleet/runtime";
|
||||
import type { FleetInstance, ShipRole, ShipInstance, SolarSystemInstance, StationInstance, ViewLevel } from "../types";
|
||||
|
||||
interface RenderMinimapOptions {
|
||||
context: CanvasRenderingContext2D;
|
||||
@@ -11,6 +12,8 @@ interface RenderMinimapOptions {
|
||||
selection: ShipInstance[];
|
||||
selectedStation?: StationInstance;
|
||||
cameraFocus: THREE.Vector3;
|
||||
fleets: FleetInstance[];
|
||||
activeFleetId?: string;
|
||||
}
|
||||
|
||||
interface RenderOverlayOptions {
|
||||
@@ -25,6 +28,8 @@ interface RenderOverlayOptions {
|
||||
selectedStation?: StationInstance;
|
||||
selectedSystemIndex: number;
|
||||
viewLevel: ViewLevel;
|
||||
fleets: FleetInstance[];
|
||||
activeFleetId?: string;
|
||||
}
|
||||
|
||||
export function drawMinimap({
|
||||
@@ -37,6 +42,8 @@ export function drawMinimap({
|
||||
selection,
|
||||
selectedStation,
|
||||
cameraFocus,
|
||||
fleets,
|
||||
activeFleetId,
|
||||
}: RenderMinimapOptions) {
|
||||
context.clearRect(0, 0, width, height);
|
||||
context.fillStyle = "rgba(4, 9, 20, 0.92)";
|
||||
@@ -82,6 +89,16 @@ export function drawMinimap({
|
||||
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);
|
||||
context.strokeStyle = "rgba(255,255,255,0.7)";
|
||||
context.strokeRect(focus.x - 9, focus.y - 9, 18, 18);
|
||||
@@ -99,6 +116,8 @@ export function drawStrategicOverlay({
|
||||
selectedStation,
|
||||
selectedSystemIndex,
|
||||
viewLevel,
|
||||
fleets,
|
||||
activeFleetId,
|
||||
}: RenderOverlayOptions) {
|
||||
context.clearRect(0, 0, width, height);
|
||||
if (viewLevel === "local") {
|
||||
@@ -130,6 +149,8 @@ export function drawStrategicOverlay({
|
||||
drawShipSymbol(context, screen.x, screen.y, ship, 10, selection.includes(ship));
|
||||
}
|
||||
});
|
||||
|
||||
drawFleetLinks(context, camera, fleets, ships, systems[selectedSystemIndex]?.definition.id, activeFleetId);
|
||||
} else {
|
||||
systems.forEach((system) => {
|
||||
const screen = projectWorldToScreen(system.center, camera);
|
||||
@@ -139,19 +160,19 @@ export function drawStrategicOverlay({
|
||||
|
||||
drawSystemFrame(context, screen.x, screen.y, system.definition.label);
|
||||
|
||||
const fleets = new Map<ShipRole, ShipInstance[]>();
|
||||
const roleBuckets = new Map<ShipRole, ShipInstance[]>();
|
||||
ships.forEach((ship) => {
|
||||
if (ship.systemId !== system.definition.id) {
|
||||
return;
|
||||
}
|
||||
const bucket = fleets.get(ship.definition.role) ?? [];
|
||||
const bucket = roleBuckets.get(ship.definition.role) ?? [];
|
||||
bucket.push(ship);
|
||||
fleets.set(ship.definition.role, bucket);
|
||||
roleBuckets.set(ship.definition.role, bucket);
|
||||
});
|
||||
|
||||
const roleOrder: ShipRole[] = ["military", "transport", "mining"];
|
||||
roleOrder.forEach((role, index) => {
|
||||
const bucket = fleets.get(role);
|
||||
const bucket = roleBuckets.get(role);
|
||||
if (!bucket || bucket.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -165,6 +186,13 @@ export function drawStrategicOverlay({
|
||||
if (stationCount > 0) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -260,6 +288,54 @@ function drawFleetSymbol(
|
||||
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(
|
||||
context: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
@@ -410,6 +486,14 @@ function drawStationSymbol(
|
||||
context.beginPath();
|
||||
context.arc(0, 0, 5, 0, Math.PI * 2);
|
||||
context.stroke();
|
||||
} else if (station.definition.category === "gate") {
|
||||
context.beginPath();
|
||||
context.arc(0, 0, 6, 0, Math.PI * 2);
|
||||
context.stroke();
|
||||
context.beginPath();
|
||||
context.moveTo(-6, 0);
|
||||
context.lineTo(6, 0);
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
context.restore();
|
||||
@@ -438,5 +522,8 @@ function getStationSymbolColor(station: StationInstance) {
|
||||
if (station.definition.category === "shipyard") {
|
||||
return "rgba(208, 162, 255, 0.95)";
|
||||
}
|
||||
if (station.definition.category === "gate") {
|
||||
return "rgba(118, 240, 255, 0.95)";
|
||||
}
|
||||
return "rgba(180, 201, 218, 0.95)";
|
||||
}
|
||||
|
||||
@@ -432,6 +432,7 @@ export function createStationInstance({
|
||||
processTimer: 0,
|
||||
activeBatch: 0,
|
||||
inventory: createEmptyInventory(),
|
||||
itemStocks: {},
|
||||
dockedShipIds: new Set(),
|
||||
dockingPorts,
|
||||
modules: definition.modules,
|
||||
@@ -462,6 +463,7 @@ function createShip({
|
||||
const visual = new THREE.Group();
|
||||
visual.rotation.y = Math.PI / 2;
|
||||
group.add(visual);
|
||||
const dockingCapacity = definition.dockingCapacity ?? 0;
|
||||
|
||||
const warpFx = new THREE.Group();
|
||||
warpFx.visible = false;
|
||||
@@ -511,6 +513,30 @@ function createShip({
|
||||
visual.add(wing);
|
||||
});
|
||||
|
||||
if (dockingCapacity > 0) {
|
||||
const hangarBody = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(definition.size * 2.8, definition.size * 1.2, definition.size * 1.5),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: 0x4c6272,
|
||||
emissive: new THREE.Color(definition.color).multiplyScalar(0.04),
|
||||
roughness: 0.5,
|
||||
metalness: 0.75,
|
||||
}),
|
||||
);
|
||||
hangarBody.position.x = -definition.size * 0.5;
|
||||
hangarBody.castShadow = true;
|
||||
visual.add(hangarBody);
|
||||
|
||||
[-1, 1].forEach((side) => {
|
||||
const bay = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(definition.size * 1.1, definition.size * 0.38, definition.size * 0.86),
|
||||
new THREE.MeshBasicMaterial({ color: definition.color, transparent: true, opacity: 0.45 }),
|
||||
);
|
||||
bay.position.set(-definition.size * 0.3, side * definition.size * 0.52, 0);
|
||||
visual.add(bay);
|
||||
});
|
||||
}
|
||||
|
||||
const engineGlow = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(definition.size * 0.35, 14, 14),
|
||||
new THREE.MeshBasicMaterial({ color: definition.color, transparent: true, opacity: 0.72 }),
|
||||
@@ -531,6 +557,24 @@ function createShip({
|
||||
ring.position.y = -definition.size * 0.55;
|
||||
group.add(ring);
|
||||
|
||||
const dockingPorts = Array.from({ length: dockingCapacity }, (_, index) => {
|
||||
const lane = index % 2 === 0 ? -1 : 1;
|
||||
const row = Math.floor(index / 2);
|
||||
const port = new THREE.Vector3(
|
||||
-definition.size * (0.4 + row * 0.7),
|
||||
0,
|
||||
lane * definition.size * 1.35,
|
||||
);
|
||||
const beacon = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(definition.size * 0.26, 0.9, definition.size * 0.42),
|
||||
new THREE.MeshBasicMaterial({ color: definition.color, transparent: true, opacity: 0.52 }),
|
||||
);
|
||||
beacon.position.copy(port);
|
||||
beacon.visible = dockingCapacity > 0;
|
||||
group.add(beacon);
|
||||
return port;
|
||||
});
|
||||
|
||||
const pickHull = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(definition.size * 1.6, 12, 12),
|
||||
new THREE.MeshBasicMaterial({ visible: false }),
|
||||
@@ -558,6 +602,12 @@ function createShip({
|
||||
idleOrbitRadius: Math.max(120, group.position.length()),
|
||||
idleOrbitAngle: 0,
|
||||
warpFx,
|
||||
behavior: "independent",
|
||||
isFleetCommander: false,
|
||||
isWingLeader: false,
|
||||
formationOffset: new THREE.Vector3(),
|
||||
dockedShipIds: new Set(),
|
||||
dockingPorts,
|
||||
};
|
||||
|
||||
selectableTargets.set(pickHull, { kind: "ship", ship });
|
||||
|
||||
Reference in New Issue
Block a user