Add fuel logistics, modular construction, and pad docking
This commit is contained in:
210
NEXT-STEPS.md
210
NEXT-STEPS.md
@@ -1,101 +1,74 @@
|
||||
# Next Steps
|
||||
|
||||
## Galaxy / Viewer Fit
|
||||
## Immediate Backend Follow-Up
|
||||
|
||||
The world is now much larger and more varied:
|
||||
The simulation now has the beginnings of a self-sustaining bootstrap:
|
||||
|
||||
- roughly galaxy-scale system counts
|
||||
- elevated system height variance
|
||||
- procedural orbital metadata
|
||||
- moons, rings, binary stars, asteroid belts, gas clouds
|
||||
- special-case `Sol` system content
|
||||
- ore miner
|
||||
- gas miner
|
||||
- station-side `gas -> fuel`
|
||||
- constructor-led station module installation
|
||||
- pad-based docking with reservation
|
||||
- timed mining, spool-up, docking, and undocking
|
||||
|
||||
The next step is not “make the map larger.” That is already done for the current runtime.
|
||||
The next step is to close the remaining logistics and simulation gaps around that bootstrap.
|
||||
|
||||
Recommended work:
|
||||
|
||||
- tune galaxy readability at scale
|
||||
- better starfield depth cues
|
||||
- stronger color/size differentiation for star classes
|
||||
- improved system label decluttering
|
||||
- add galaxy navigation affordances
|
||||
- jump-to-system search
|
||||
- constellation / region overlays
|
||||
- bookmarks for notable systems such as `Sol`
|
||||
- tighten viewer performance
|
||||
- reduce orbit/moon draw cost when zoomed out
|
||||
- pool or simplify distant celestial meshes
|
||||
- profile high-system-count scenes
|
||||
- add constructor material logistics
|
||||
- allow the constructor to fetch module materials instead of assuming they are already on the target station
|
||||
- add pickup / load phases and delivery priorities
|
||||
- expose station installed modules and active construction to the viewer
|
||||
- installed modules
|
||||
- current build target
|
||||
- build progress
|
||||
- harden dock management
|
||||
- make dock eligibility depend on dock type / ship class if needed
|
||||
- surface pad occupancy in debug UI
|
||||
- add stronger action timing coverage
|
||||
- timed final approach / berthing if desired
|
||||
- timed station-side processing queues if multiple modules run concurrently
|
||||
- add recovery behavior for stranded ships
|
||||
- rescue / tow / emergency refuel
|
||||
- better handling after full fuel and power depletion
|
||||
|
||||
## Economic Growth
|
||||
## Economy / Logistics
|
||||
|
||||
The current economy already supports:
|
||||
The economy now has a more explicit resource chain:
|
||||
|
||||
1. mining ore
|
||||
2. hauling to refining
|
||||
3. refining / fabricating goods
|
||||
4. spending those goods on ships and outposts
|
||||
2. harvesting gas
|
||||
3. processing `gas -> fuel`
|
||||
4. refining ore
|
||||
5. spending refined goods on station growth
|
||||
|
||||
The next step is not “invent a use for refined goods.” That use already exists.
|
||||
|
||||
The next step is to make faction growth more intentional and legible.
|
||||
The next step is to make those logistics deliberate instead of bootstrap-scripted.
|
||||
|
||||
Recommended work:
|
||||
|
||||
- make shipbuilding priorities reactive
|
||||
- build more miners / haulers when ore throughput is low
|
||||
- build escorts when industrial losses rise
|
||||
- build warships when frontier pressure rises
|
||||
- make expansion logic consume the economy more visibly
|
||||
- use industrial stock to claim and fortify central systems
|
||||
- expose production pressure in UI
|
||||
- show ore throughput
|
||||
- show fabricated goods
|
||||
- show queued faction priorities
|
||||
- make resource type differences matter
|
||||
- ore belts vs gas clouds
|
||||
- gas-aware logistics and production choices
|
||||
|
||||
## Pirate Harassment
|
||||
|
||||
Pirates already exist and can raid, fight, and destroy ships.
|
||||
|
||||
What they are missing is sharper industrial harassment behavior.
|
||||
|
||||
Recommended work:
|
||||
|
||||
- prioritize miners, haulers, and refinery approaches as pirate targets
|
||||
- add local threat weighting around:
|
||||
- resource nodes
|
||||
- refinery docking lanes
|
||||
- undefended transport routes
|
||||
- force empires to react by:
|
||||
- escorting miners
|
||||
- patrolling refinery systems
|
||||
- building defensive stations sooner
|
||||
|
||||
This will make the industrial loop produce strategic tension instead of just passive growth.
|
||||
|
||||
## High-Value Gameplay Sequence
|
||||
|
||||
The most useful short-term gameplay loop to solidify is:
|
||||
|
||||
1. miners feed refining
|
||||
2. refining feeds ship production
|
||||
3. pirates harass industry
|
||||
4. empires respond with escorts, patrols, and new outposts
|
||||
5. stronger economies produce stronger military presence
|
||||
6. system control shifts based on industrial strength and protection
|
||||
|
||||
That turns the simulation into a real strategy loop.
|
||||
- split logistics roles more clearly
|
||||
- ore miner
|
||||
- gas miner
|
||||
- hauler / constructor transport
|
||||
- make station build priorities responsive
|
||||
- fuel chain first when reserves are low
|
||||
- ore refining when fuel is stable
|
||||
- docking expansion when traffic backs up
|
||||
- make fuel scarcity visible in debug UI
|
||||
- fuel throughput
|
||||
- gas stock
|
||||
- dock contention
|
||||
- move away from generic node selection
|
||||
- let miners prefer nearby valid nodes
|
||||
- factor travel cost and dock turnaround into throughput planning
|
||||
|
||||
## Concrete Implementation Order
|
||||
|
||||
1. Add viewer-scale performance controls for the larger galaxy.
|
||||
2. Add faction production heuristics based on current economy and losses.
|
||||
3. Make pirate target selection explicitly prefer economic targets.
|
||||
4. Surface faction stocks, throughput, and build priorities in the HUD/debug views.
|
||||
5. Expand the order/behavior set with higher-value RTS actions like `hold-here`, `attack`, and `defend-area`.
|
||||
1. Add constructor pickup / delivery behavior for module materials.
|
||||
2. Expose station installed modules, dock pads, and active construction in the contracts and viewer.
|
||||
3. Add rescue / recovery behavior for power-starved ships.
|
||||
4. Add faction build priorities based on fuel, ore throughput, and dock saturation.
|
||||
5. Improve pirate targeting so industrial ships and docking lanes are high-value harassment targets.
|
||||
6. Break backend simulation responsibilities into smaller planning / faction / combat / logistics modules.
|
||||
|
||||
## Network / Multiplayer
|
||||
@@ -133,68 +106,31 @@ Recommended work:
|
||||
- versioning
|
||||
- reconnect / catch-up semantics
|
||||
|
||||
## Viewer / Debugging
|
||||
|
||||
The viewer still works as an observer/debug client first.
|
||||
|
||||
Recommended work:
|
||||
|
||||
- fix the current `followedShipId` regression in `GameViewer.ts`
|
||||
- show station module state and construction state
|
||||
- show dock occupancy and waiting ships
|
||||
- expose fuel-chain health
|
||||
- gas stock
|
||||
- fuel stock
|
||||
- refinery / processor activity
|
||||
- improve event typing for:
|
||||
- dock request
|
||||
- dock granted
|
||||
- refuel
|
||||
- construction started / completed
|
||||
|
||||
## Interest Management
|
||||
|
||||
The current stream is world-wide.
|
||||
|
||||
That means every observer receives deltas for the full simulation, even when only looking at one part of space.
|
||||
The stream is still world-wide.
|
||||
|
||||
Recommended work:
|
||||
|
||||
- add observer/view-scoped subscriptions
|
||||
- visible systems
|
||||
- nearby ships / stations / nodes
|
||||
- faction-scoped or player-scoped channels later
|
||||
- support subscribe / unsubscribe as camera focus changes
|
||||
- send only relevant deltas per observer
|
||||
- keep coarse strategic updates available for off-screen context
|
||||
- system ownership
|
||||
- major combat
|
||||
- economy summaries
|
||||
|
||||
This is the key step that makes many simultaneous observers practical without broadcasting the entire world to everyone.
|
||||
|
||||
## Replication Quality
|
||||
|
||||
The backend already sends:
|
||||
|
||||
- initial snapshot
|
||||
- incremental deltas
|
||||
- event records
|
||||
|
||||
Recommended work:
|
||||
|
||||
- add stronger event typing
|
||||
- spawn
|
||||
- destroy
|
||||
- dock
|
||||
- undock
|
||||
- cargo transfer
|
||||
- combat hit / kill
|
||||
- improve interpolation and extrapolation policies per entity type
|
||||
- add per-layer presentation tuning in the viewer
|
||||
- smoother fade bands between local / system / universe
|
||||
- better visual density control at galaxy scale
|
||||
- moon/orbit LOD based on zoom level
|
||||
- add resync handling when a client falls too far behind
|
||||
- consider switching from SSE to websocket transport if bidirectional command traffic becomes heavy
|
||||
|
||||
## Celestial Depth
|
||||
|
||||
The current celestial layer is procedurally rich, but still mostly decorative outside of resource nodes.
|
||||
|
||||
Recommended work:
|
||||
|
||||
- add authored moon metadata when needed
|
||||
- labels
|
||||
- resource-bearing moons
|
||||
- special landmarks
|
||||
- support multiple belts / cloud bands per system explicitly
|
||||
- add stellar gameplay hooks
|
||||
- hazardous neutron-star systems
|
||||
- high-value binary systems
|
||||
- rich-gas outer systems
|
||||
- expose notable-system summaries in the viewer
|
||||
- star class
|
||||
- resource profile
|
||||
- moon count
|
||||
- keep strategic summaries for off-screen context
|
||||
|
||||
62
SESSION.md
62
SESSION.md
@@ -108,15 +108,27 @@ The backend simulation already includes:
|
||||
|
||||
- autonomous ships
|
||||
- orbital travel
|
||||
- docking and undocking
|
||||
- pad-based docking and undocking
|
||||
- stations expose docking capacity through installed dock-bay modules
|
||||
- ships reserve an empty pad before docking
|
||||
- ships wait in a holding pattern when no pad is available
|
||||
- mining and refinery delivery
|
||||
- module-gated ship and station capabilities
|
||||
- ships require fitted modules such as reactor, capacitor, and mining or gun turrets
|
||||
- stations require fitted modules such as power, refinery, and storage modules
|
||||
- ships require fitted modules such as reactor, capacitor, mining turret, gas extractor, or gun turrets
|
||||
- stations require installed modules such as power, docking, refinery, fuel processing, and storage modules
|
||||
- fuel-to-energy power simulation
|
||||
- ship reactors consume `gas` fuel to charge capacitors
|
||||
- station power cores consume `gas` fuel to charge station energy buffers
|
||||
- gas clouds provide raw `gas`
|
||||
- station fuel processors convert `gas` into `fuel`
|
||||
- ship reactors consume `fuel` to charge capacitors
|
||||
- station power cores consume `fuel` to charge station energy buffers
|
||||
- powered actions stop when fuel and energy are depleted
|
||||
- constructor-led station module construction
|
||||
- stations now track installed modules per instance instead of relying only on static constructible definitions
|
||||
- module construction uses station inventory plus timed build progress
|
||||
- explicit action timing in the control loop
|
||||
- mining now runs on a fixed cycle
|
||||
- warp and FTL travel require spool time
|
||||
- docking and undocking have explicit durations
|
||||
- refining / fabrication
|
||||
- faction growth through ship and outpost production
|
||||
- pirate pressure and combat
|
||||
@@ -129,7 +141,7 @@ The backend simulation already includes:
|
||||
- systems in galaxy space
|
||||
- in-system entities in local space
|
||||
- item-based inventories for ships and stations
|
||||
- ore, refined metals, gas fuel, and cargo now flow through per-item inventories instead of ad hoc stock fields
|
||||
- ore, refined metals, raw gas, fuel, and cargo now flow through per-item inventories instead of ad hoc stock fields
|
||||
|
||||
The runtime model still follows the intended layered control architecture:
|
||||
|
||||
@@ -164,13 +176,20 @@ The runtime model still follows the intended layered control architecture:
|
||||
- one modular station
|
||||
- one constructor ship
|
||||
- one mining ship
|
||||
- added a gas mining ship for bootstrap fuel logistics
|
||||
- moved the starting ships close to the `helios` star and next to each other
|
||||
- added modular ship and station data
|
||||
- new station power and storage modules
|
||||
- new ship reactor, capacitor, mining turret, and gun turret modules
|
||||
- new fuel processing and docking bay modules
|
||||
- new ship reactor, capacitor, mining turret, gas extractor, and gun turret modules
|
||||
- refactored simulation inventories to per-item storage
|
||||
- stations and ships now replicate inventories instead of specialized ore/refined/cargo counters
|
||||
- added fuel-driven power generation and energy consumption in the simulation loop
|
||||
- split raw `gas` from burnable `fuel` in the simulation loop
|
||||
- added module recipe data and per-station installed-module runtime state
|
||||
- added constructor-led station module construction for the bootstrap station
|
||||
- added gas harvesting, gas-to-fuel processing, and explicit ship refueling behavior
|
||||
- reworked docking into pad reservation with visible stand-off positions instead of snapping ships into station centers
|
||||
- added action timing for mining cycles, warp / FTL spool-up, and undocking
|
||||
- replaced the viewer bottom faction strip with a horizontal ship-card debugging rail
|
||||
- added movable, resizable, multi-window history panels in the viewer
|
||||
- fixed the auto-miner undock controller transition
|
||||
@@ -190,9 +209,14 @@ The runtime model still follows the intended layered control architecture:
|
||||
- the galaxy is much larger now, so viewer performance and visual density need active tuning
|
||||
- moon rendering is procedural from counts, not authored moon-by-moon data
|
||||
- resource extraction behavior still treats all resource nodes generically
|
||||
- item inventories exist, but storage enforcement and module-slot restrictions are still lightweight
|
||||
- cargo/storage compatibility is mostly data-convention driven
|
||||
- item inventories exist, but storage/module restrictions are still partial
|
||||
- station storage capacity is now enforced by storage class and installed module
|
||||
- ship cargo compatibility is still mostly data-convention driven
|
||||
- hull-specific module restrictions are not enforced yet
|
||||
- constructor logic only builds from station-local inventory
|
||||
- it does not yet fetch module materials from other stations or ships
|
||||
- station installed modules and active construction are not yet exposed in the viewer contract
|
||||
- some viewer follow-camera code is currently broken by a pre-existing missing `followedShipId` property
|
||||
- piracy and faction growth are still functional rather than strategically deep
|
||||
- no persistence for saves, seeds, or reconnect state
|
||||
|
||||
@@ -205,26 +229,31 @@ The runtime model still follows the intended layered control architecture:
|
||||
- [apps/backend/Simulation/SimulationEngine.cs](/home/jbourdon/repos/space-game/apps/backend/Simulation/SimulationEngine.cs)
|
||||
- simulation advancement
|
||||
- snapshot / delta composition for galaxy-space systems and local-space entities
|
||||
- inventory, fuel, and energy processing
|
||||
- auto-miner undock state transition fix
|
||||
- inventory, gas/fuel, and energy processing
|
||||
- station module construction
|
||||
- gas harvesting, refueling, and pad-based docking
|
||||
- action timing for mining, spool-up, docking, and undocking
|
||||
- [apps/backend/Simulation/ScenarioLoader.cs](/home/jbourdon/repos/space-game/apps/backend/Simulation/ScenarioLoader.cs)
|
||||
- faction bootstrap
|
||||
- galaxy generation
|
||||
- special systems
|
||||
- procedural celestial/resource content
|
||||
- normalization of authored placements into system-local space
|
||||
- minimal startup seeding for ships, station, and fuel
|
||||
- minimal startup seeding for ships, station, fuel, and module materials
|
||||
- [apps/backend/Contracts/WorldContracts.cs](/home/jbourdon/repos/space-game/apps/backend/Contracts/WorldContracts.cs)
|
||||
- snapshot and delta contract shape
|
||||
- station dock-pad count
|
||||
- [apps/backend/Simulation/RuntimeModels.cs](/home/jbourdon/repos/space-game/apps/backend/Simulation/RuntimeModels.cs)
|
||||
- runtime vector math and world model
|
||||
- per-item inventories on ships and stations
|
||||
- per-station installed modules and docking pad assignments
|
||||
- [apps/viewer/src/GameViewer.ts](/home/jbourdon/repos/space-game/apps/viewer/src/GameViewer.ts)
|
||||
- camera, selection, streaming integration, and presentation
|
||||
- layered local/remote system presentation
|
||||
- orbital reconstruction and moon rendering
|
||||
- projected shell markers and hover labels
|
||||
- ship card list and multi-window history debugging UI
|
||||
- station HUD docked/pad count readout
|
||||
- [apps/viewer/src/style.css](/home/jbourdon/repos/space-game/apps/viewer/src/style.css)
|
||||
- HUD layout
|
||||
- ship-card rail
|
||||
@@ -235,14 +264,19 @@ The runtime model still follows the intended layered control architecture:
|
||||
- viewer-side snapshot contract for galaxy-space systems and local-space entities
|
||||
- [shared/data](/home/jbourdon/repos/space-game/shared/data)
|
||||
- scenario and world data definitions
|
||||
- `module-recipes.json` now defines timed module construction costs
|
||||
|
||||
## Validation
|
||||
|
||||
Validation passing at the end of this session:
|
||||
|
||||
- `dotnet build apps/backend/SpaceGame.Simulation.Api.csproj`
|
||||
|
||||
Validation currently failing / blocked:
|
||||
|
||||
- `cd apps/viewer && npm run build`
|
||||
- fails because `apps/viewer/src/GameViewer.ts` references a missing `followedShipId` property
|
||||
|
||||
## Last Commit
|
||||
|
||||
- `1747d84`
|
||||
- `ef62577`
|
||||
|
||||
@@ -86,6 +86,7 @@ public sealed record StationSnapshot(
|
||||
Vector3Dto LocalPosition,
|
||||
string Color,
|
||||
int DockedShips,
|
||||
int DockingPads,
|
||||
float EnergyStored,
|
||||
IReadOnlyList<InventoryEntry> Inventory,
|
||||
string FactionId);
|
||||
@@ -98,6 +99,7 @@ public sealed record StationDelta(
|
||||
Vector3Dto LocalPosition,
|
||||
string Color,
|
||||
int DockedShips,
|
||||
int DockingPads,
|
||||
float EnergyStored,
|
||||
IReadOnlyList<InventoryEntry> Inventory,
|
||||
string FactionId);
|
||||
|
||||
@@ -5,8 +5,10 @@ public sealed class BalanceDefinition
|
||||
public float YPlane { get; set; }
|
||||
public float ArrivalThreshold { get; set; }
|
||||
public float MiningRate { get; set; }
|
||||
public float MiningCycleSeconds { get; set; }
|
||||
public float TransferRate { get; set; }
|
||||
public float DockingDuration { get; set; }
|
||||
public float UndockingDuration { get; set; }
|
||||
public float UndockDistance { get; set; }
|
||||
public EnergyBalanceDefinition Energy { get; set; } = new();
|
||||
public FuelBalanceDefinition Fuel { get; set; } = new();
|
||||
@@ -68,6 +70,19 @@ public sealed class ItemDefinition
|
||||
public string Summary { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class RecipeInputDefinition
|
||||
{
|
||||
public required string ItemId { get; set; }
|
||||
public float Amount { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ModuleRecipeDefinition
|
||||
{
|
||||
public required string ModuleId { get; set; }
|
||||
public float Duration { get; set; }
|
||||
public required List<RecipeInputDefinition> Inputs { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PlanetDefinition
|
||||
{
|
||||
public required string Label { get; set; }
|
||||
|
||||
@@ -14,6 +14,7 @@ public sealed class SimulationWorld
|
||||
public required List<FactionRuntime> Factions { get; init; }
|
||||
public required Dictionary<string, ShipDefinition> ShipDefinitions { get; init; }
|
||||
public required Dictionary<string, ItemDefinition> ItemDefinitions { get; init; }
|
||||
public required Dictionary<string, ModuleRecipeDefinition> ModuleRecipes { get; init; }
|
||||
public int TickIntervalMs { get; init; } = 200;
|
||||
public DateTimeOffset GeneratedAtUtc { get; set; }
|
||||
}
|
||||
@@ -43,13 +44,24 @@ public sealed class StationRuntime
|
||||
public required ConstructibleDefinition Definition { get; init; }
|
||||
public required Vector3 Position { get; init; }
|
||||
public required string FactionId { get; init; }
|
||||
public HashSet<string> InstalledModules { get; } = new(StringComparer.Ordinal);
|
||||
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
|
||||
public Dictionary<int, string> DockingPadAssignments { get; } = new();
|
||||
public float EnergyStored { get; set; }
|
||||
public float ProcessTimer { get; set; }
|
||||
public HashSet<string> DockedShipIds { get; } = [];
|
||||
public ModuleConstructionRuntime? ActiveConstruction { get; set; }
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class ModuleConstructionRuntime
|
||||
{
|
||||
public required string ModuleId { get; init; }
|
||||
public float ProgressSeconds { get; set; }
|
||||
public float RequiredSeconds { get; init; }
|
||||
public string AssignedConstructorShipId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class ShipRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
@@ -67,6 +79,7 @@ public sealed class ShipRuntime
|
||||
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
|
||||
public float EnergyStored { get; set; }
|
||||
public string? DockedStationId { get; set; }
|
||||
public int? AssignedDockingPadIndex { get; set; }
|
||||
public float Health { get; set; }
|
||||
public List<string> History { get; } = [];
|
||||
public string LastSignature { get; set; } = string.Empty;
|
||||
@@ -98,8 +111,10 @@ public sealed class DefaultBehaviorRuntime
|
||||
{
|
||||
public required string Kind { get; set; }
|
||||
public string? AreaSystemId { get; set; }
|
||||
public string? StationId { get; set; }
|
||||
public string? RefineryId { get; set; }
|
||||
public string? NodeId { get; set; }
|
||||
public string? ModuleId { get; set; }
|
||||
public string? Phase { get; set; }
|
||||
public List<Vector3> PatrolPoints { get; set; } = [];
|
||||
public int PatrolIndex { get; set; }
|
||||
|
||||
@@ -88,11 +88,13 @@ public sealed class ScenarioLoader
|
||||
var ships = Read<List<ShipDefinition>>("ships.json");
|
||||
var constructibles = Read<List<ConstructibleDefinition>>("constructibles.json");
|
||||
var items = Read<List<ItemDefinition>>("items.json");
|
||||
var moduleRecipes = Read<List<ModuleRecipeDefinition>>("module-recipes.json");
|
||||
var balance = Read<BalanceDefinition>("balance.json");
|
||||
|
||||
var shipDefinitions = ships.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
||||
var constructibleDefinitions = constructibles.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
||||
var itemDefinitions = items.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
||||
var moduleRecipeDefinitions = moduleRecipes.ToDictionary((definition) => definition.ModuleId, StringComparer.Ordinal);
|
||||
var systemRuntimes = systems
|
||||
.Select((definition) => new SystemRuntime
|
||||
{
|
||||
@@ -141,17 +143,23 @@ public sealed class ScenarioLoader
|
||||
Position = ResolveStationPosition(system, plan, balance),
|
||||
FactionId = plan.FactionId ?? DefaultFactionId,
|
||||
});
|
||||
|
||||
foreach (var moduleId in definition.Modules)
|
||||
{
|
||||
stations[^1].InstalledModules.Add(moduleId);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var station in stations)
|
||||
{
|
||||
station.Inventory["gas"] = 320f;
|
||||
station.Inventory["fuel"] = 240f;
|
||||
station.Inventory["refined-metals"] = 120f;
|
||||
}
|
||||
|
||||
var refinery = stations.FirstOrDefault((station) =>
|
||||
HasModules(station.Definition, "refinery-stack", "power-core", "liquid-tank", "gas-tank") &&
|
||||
HasInstalledModules(station, "power-core", "liquid-tank") &&
|
||||
station.SystemId == scenario.MiningDefaults.RefinerySystemId)
|
||||
?? stations.FirstOrDefault((station) => HasModules(station.Definition, "refinery-stack", "power-core", "liquid-tank", "gas-tank"));
|
||||
?? stations.FirstOrDefault((station) => HasInstalledModules(station, "power-core", "liquid-tank"));
|
||||
|
||||
var patrolRoutes = scenario.PatrolRoutes.ToDictionary(
|
||||
(route) => route.SystemId,
|
||||
@@ -185,9 +193,15 @@ public sealed class ScenarioLoader
|
||||
});
|
||||
|
||||
shipsRuntime[^1].Inventory["gas"] = definition.Id switch
|
||||
{
|
||||
_ => 0f,
|
||||
};
|
||||
shipsRuntime[^1].Inventory.Remove("gas");
|
||||
shipsRuntime[^1].Inventory["fuel"] = definition.Id switch
|
||||
{
|
||||
"constructor" => 90f,
|
||||
"miner" => 90f,
|
||||
"gas-miner" => 90f,
|
||||
_ => 120f,
|
||||
};
|
||||
}
|
||||
@@ -208,6 +222,7 @@ public sealed class ScenarioLoader
|
||||
Factions = factions,
|
||||
ShipDefinitions = shipDefinitions,
|
||||
ItemDefinitions = itemDefinitions,
|
||||
ModuleRecipes = moduleRecipeDefinitions,
|
||||
GeneratedAtUtc = DateTimeOffset.UtcNow,
|
||||
};
|
||||
}
|
||||
@@ -818,13 +833,33 @@ public sealed class ScenarioLoader
|
||||
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
|
||||
StationRuntime? refinery)
|
||||
{
|
||||
if (HasModules(definition, "fabricator-array", "docking-clamps") && refinery is not null)
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "construct-station",
|
||||
StationId = refinery.Id,
|
||||
Phase = "travel-to-station",
|
||||
};
|
||||
}
|
||||
|
||||
if (HasModules(definition, "reactor-core", "capacitor-bank", "gas-extractor") && refinery is not null)
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "auto-harvest-gas",
|
||||
StationId = refinery.Id,
|
||||
Phase = "travel-to-node",
|
||||
};
|
||||
}
|
||||
|
||||
if (HasModules(definition, "reactor-core", "capacitor-bank", "mining-turret") && refinery is not null)
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "auto-mine",
|
||||
AreaSystemId = scenario.MiningDefaults.NodeSystemId,
|
||||
RefineryId = refinery.Id,
|
||||
StationId = refinery.Id,
|
||||
Phase = "travel-to-node",
|
||||
};
|
||||
}
|
||||
@@ -883,6 +918,9 @@ public sealed class ScenarioLoader
|
||||
private static bool HasModules(ConstructibleDefinition definition, params string[] modules) =>
|
||||
modules.All((moduleId) => definition.Modules.Contains(moduleId, StringComparer.Ordinal));
|
||||
|
||||
private static bool HasInstalledModules(StationRuntime station, params string[] modules) =>
|
||||
modules.All((moduleId) => station.InstalledModules.Contains(moduleId, StringComparer.Ordinal));
|
||||
|
||||
private static bool HasModules(ShipDefinition definition, params string[] modules) =>
|
||||
modules.All((moduleId) => definition.Modules.Contains(moduleId, StringComparer.Ordinal));
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@ import type {
|
||||
type ZoomLevel = "local" | "system" | "universe";
|
||||
type SelectionGroup = "ships" | "structures" | "celestials";
|
||||
type DragMode = "orbit" | "marquee";
|
||||
type CameraMode = "tactical" | "follow";
|
||||
type Selectable =
|
||||
| { kind: "ship"; id: string }
|
||||
| { kind: "station"; id: string }
|
||||
@@ -245,6 +246,7 @@ export class GameViewer {
|
||||
private desiredDistance = ZOOM_DISTANCE.system;
|
||||
private orbitYaw = -2.3;
|
||||
private orbitPitch = 0.62;
|
||||
private cameraMode: CameraMode = "tactical";
|
||||
private dragMode?: DragMode;
|
||||
private dragPointerId?: number;
|
||||
private dragStart = new THREE.Vector2();
|
||||
@@ -252,7 +254,12 @@ export class GameViewer {
|
||||
private marqueeActive = false;
|
||||
private suppressClickSelection = false;
|
||||
private activeSystemId?: string;
|
||||
private followedShipId?: string;
|
||||
private cameraTargetShipId?: string;
|
||||
private readonly followCameraPosition = new THREE.Vector3();
|
||||
private readonly followCameraFocus = new THREE.Vector3();
|
||||
private readonly followCameraDirection = new THREE.Vector3(0, 0.16, 1);
|
||||
private readonly followCameraDesiredDirection = new THREE.Vector3(0, 0.16, 1);
|
||||
private readonly followCameraOffset = new THREE.Vector3();
|
||||
private readonly historyWindows: HistoryWindowState[] = [];
|
||||
private historyWindowCounter = 0;
|
||||
private historyWindowZCounter = 10;
|
||||
@@ -334,6 +341,7 @@ export class GameViewer {
|
||||
this.renderer.domElement.addEventListener("dblclick", this.onDoubleClick);
|
||||
this.renderer.domElement.addEventListener("wheel", this.onWheel, { passive: false });
|
||||
this.factionStripEl.addEventListener("click", this.onShipStripClick);
|
||||
this.factionStripEl.addEventListener("dblclick", this.onShipStripDoubleClick);
|
||||
this.historyLayerEl.addEventListener("click", this.onHistoryLayerClick);
|
||||
this.historyLayerEl.addEventListener("pointerdown", this.onHistoryLayerPointerDown);
|
||||
window.addEventListener("pointermove", this.onHistoryWindowPointerMove);
|
||||
@@ -709,18 +717,30 @@ export class GameViewer {
|
||||
.map((ship) => {
|
||||
const fuel = this.inventoryAmount(ship.inventory, "gas");
|
||||
const isSelected = this.selectedItems.length === 1 && this.selectedItems[0].kind === "ship" && this.selectedItems[0].id === ship.id;
|
||||
const isFollowed = this.followedShipId === ship.id;
|
||||
const isFollowed = this.cameraMode === "follow" && this.cameraTargetShipId === ship.id;
|
||||
return `
|
||||
<article class="ship-card${isSelected ? " is-selected" : ""}${isFollowed ? " is-followed" : ""}" data-ship-id="${ship.id}">
|
||||
<div class="ship-card-header">
|
||||
<h3>${ship.label}</h3>
|
||||
<span class="ship-card-badge">${ship.shipClass}</span>
|
||||
<div class="ship-card-meta">
|
||||
<span class="ship-card-badge">${ship.shipClass}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="ship-card-history-button"
|
||||
data-history-ship-id="${ship.id}"
|
||||
aria-label="Open history for ${ship.label}"
|
||||
title="Open history"
|
||||
>🕔</button>
|
||||
</div>
|
||||
</div>
|
||||
<p>${ship.systemId}</p>
|
||||
<p>Fuel ${fuel.toFixed(0)} · Energy ${ship.energyStored.toFixed(0)}</p>
|
||||
<p>State ${ship.state}</p>
|
||||
<p>Order ${ship.orderKind ?? "none"}</p>
|
||||
<button type="button" class="ship-card-history-button" data-history-ship-id="${ship.id}">Open History</button>
|
||||
<div class="ship-card-ai">
|
||||
<p>Order ${ship.orderKind ?? "none"}</p>
|
||||
<p>Behavior ${ship.defaultBehaviorKind}</p>
|
||||
<p>Task ${ship.controllerTaskKind}</p>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
@@ -767,14 +787,12 @@ export class GameViewer {
|
||||
this.detailTitleEl.textContent = ship.label;
|
||||
const cargoUsed = ship.inventory.reduce((sum, entry) => sum + entry.amount, 0);
|
||||
this.detailBodyEl.innerHTML = `
|
||||
<p>${ship.shipClass} · ${ship.role} · ${ship.systemId}</p>
|
||||
<p>Parent ${parent}</p>
|
||||
<p>State ${ship.state}<br>Behavior ${ship.defaultBehaviorKind}<br>Task ${ship.controllerTaskKind}</p>
|
||||
<p>State ${ship.state}</p>
|
||||
<p>Energy ${ship.energyStored.toFixed(0)}<br>Cargo ${cargoUsed.toFixed(0)} / ${ship.cargoCapacity.toFixed(0)}</p>
|
||||
<p>Inventory ${this.formatInventory(ship.inventory)}</p>
|
||||
<p>Velocity ${this.formatVector(ship.localVelocity)}</p>
|
||||
<p>${this.followedShipId === ship.id ? "Camera follow engaged" : "Camera follow idle"}</p>
|
||||
<p>History available from the ship card list.</p>
|
||||
<p>Camera ${this.cameraMode === "follow" && this.cameraTargetShipId === ship.id ? "camera-follow" : "tactical"}<br>Press C to toggle follow</p>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
@@ -789,7 +807,7 @@ export class GameViewer {
|
||||
this.detailBodyEl.innerHTML = `
|
||||
<p>${station.category} · ${station.systemId}</p>
|
||||
<p>Parent ${parent}</p>
|
||||
<p>Energy ${station.energyStored.toFixed(0)}<br>Docked ${station.dockedShips}</p>
|
||||
<p>Energy ${station.energyStored.toFixed(0)}<br>Docked ${station.dockedShips} / ${station.dockingPads}</p>
|
||||
<p>Inventory ${this.formatInventory(station.inventory)}</p>
|
||||
<p>History available in the separate history window.</p>
|
||||
`;
|
||||
@@ -879,7 +897,10 @@ export class GameViewer {
|
||||
this.currentDistance = THREE.MathUtils.damp(this.currentDistance, this.desiredDistance, 7.5, delta);
|
||||
this.zoomLevel = this.classifyZoomLevel(this.currentDistance);
|
||||
this.updateActiveSystem();
|
||||
this.updateFollowCamera(delta);
|
||||
if (this.cameraMode === "follow" && this.updateFollowCamera(delta)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updatePanFromKeyboard(delta);
|
||||
this.orbitPitch = THREE.MathUtils.clamp(this.orbitPitch, 0.18, 1.3);
|
||||
|
||||
@@ -895,10 +916,6 @@ export class GameViewer {
|
||||
}
|
||||
|
||||
private updatePanFromKeyboard(delta: number) {
|
||||
if (this.followedShipId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const move = new THREE.Vector3();
|
||||
if (this.keyState.has("w")) {
|
||||
move.z -= 1;
|
||||
@@ -944,8 +961,8 @@ export class GameViewer {
|
||||
const iconAlpha = isProjectedSystemIcon
|
||||
? 0
|
||||
: entry.hideIconInUniverse
|
||||
? blend.systemWeight * (isActiveDetail ? 1 : 0)
|
||||
: Math.max(blend.systemWeight, blend.universeWeight);
|
||||
? blend.systemWeight * (isActiveDetail ? 1 : 0)
|
||||
: Math.max(blend.systemWeight, blend.universeWeight);
|
||||
|
||||
this.setObjectOpacity(entry.detail, detailAlpha);
|
||||
this.setObjectOpacity(entry.icon, iconAlpha);
|
||||
@@ -1064,21 +1081,14 @@ export class GameViewer {
|
||||
const now = performance.now();
|
||||
const worldTimeSeconds = this.currentWorldTimeSeconds();
|
||||
for (const visual of this.shipVisuals.values()) {
|
||||
const elapsedMs = now - visual.receivedAtMs;
|
||||
const blendT = THREE.MathUtils.clamp(elapsedMs / visual.blendDurationMs, 0, 1);
|
||||
const worldPosition = new THREE.Vector3().lerpVectors(visual.startPosition, visual.authoritativePosition, blendT);
|
||||
|
||||
if (blendT >= 1) {
|
||||
const extrapolationSeconds = Math.min((elapsedMs - visual.blendDurationMs) / 1000, 0.35);
|
||||
worldPosition.copy(visual.authoritativePosition).addScaledVector(visual.velocity, extrapolationSeconds);
|
||||
}
|
||||
const worldPosition = this.getAnimatedShipLocalPosition(visual, now);
|
||||
|
||||
visual.mesh.position.copy(this.toDisplayLocalPosition(worldPosition, visual.systemId));
|
||||
visual.icon.position.copy(visual.mesh.position);
|
||||
const shipVisible = visual.systemId === this.activeSystemId;
|
||||
visual.mesh.visible = shipVisible;
|
||||
visual.icon.visible = shipVisible && visual.icon.visible;
|
||||
const desiredHeading = visual.targetPosition.clone().sub(worldPosition);
|
||||
const desiredHeading = this.resolveShipHeading(visual, worldPosition);
|
||||
if (desiredHeading.lengthSq() > 0.01) {
|
||||
visual.mesh.lookAt(visual.mesh.position.clone().add(desiredHeading));
|
||||
}
|
||||
@@ -1091,8 +1101,7 @@ export class GameViewer {
|
||||
visual.mesh.visible = visual.systemId === this.activeSystemId;
|
||||
}
|
||||
for (const visual of this.stationVisuals.values()) {
|
||||
const animatedLocalPosition = this.computeStructureLocalPosition(visual, this.currentWorldTimeSeconds(), 0.09);
|
||||
visual.mesh.position.copy(this.toDisplayLocalPosition(animatedLocalPosition, visual.systemId));
|
||||
visual.mesh.position.copy(this.toDisplayLocalPosition(visual.localPosition, visual.systemId));
|
||||
visual.icon.position.copy(visual.mesh.position);
|
||||
visual.mesh.visible = visual.systemId === this.activeSystemId;
|
||||
}
|
||||
@@ -1102,6 +1111,25 @@ export class GameViewer {
|
||||
this.updateSystemSummaryPresentation();
|
||||
}
|
||||
|
||||
private getAnimatedShipLocalPosition(visual: ShipVisual, now = performance.now()) {
|
||||
const elapsedMs = now - visual.receivedAtMs;
|
||||
const blendT = THREE.MathUtils.clamp(elapsedMs / visual.blendDurationMs, 0, 1);
|
||||
return new THREE.Vector3().lerpVectors(visual.startPosition, visual.authoritativePosition, blendT);
|
||||
}
|
||||
|
||||
private resolveShipHeading(visual: ShipVisual, worldPosition: THREE.Vector3) {
|
||||
const desiredHeading = visual.targetPosition.clone().sub(worldPosition);
|
||||
if (desiredHeading.lengthSq() > 0.01) {
|
||||
return desiredHeading;
|
||||
}
|
||||
|
||||
if (visual.velocity.lengthSq() > 0.01) {
|
||||
return visual.velocity.clone();
|
||||
}
|
||||
|
||||
return new THREE.Vector3(Math.cos(this.orbitYaw), 0, Math.sin(this.orbitYaw));
|
||||
}
|
||||
|
||||
private updatePlanetPresentation() {
|
||||
const nowSeconds = this.currentWorldTimeSeconds();
|
||||
for (const visual of this.planetVisuals) {
|
||||
@@ -1652,8 +1680,10 @@ export class GameViewer {
|
||||
? new Date(this.world.generatedAtUtc).toLocaleTimeString()
|
||||
: "n/a";
|
||||
const activeSystem = this.activeSystemId ?? "deep-space";
|
||||
const cameraModeLabel = this.cameraMode === "follow" ? "camera-follow" : "tactical";
|
||||
this.statusEl.textContent = [
|
||||
`mode: ${mode}`,
|
||||
`camera: ${cameraModeLabel}`,
|
||||
`zoom: ${this.zoomLevel}`,
|
||||
`system: ${activeSystem}`,
|
||||
`sequence: ${sequence}`,
|
||||
@@ -2009,6 +2039,30 @@ export class GameViewer {
|
||||
this.updatePanels();
|
||||
};
|
||||
|
||||
private onShipStripDoubleClick = (event: MouseEvent) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.closest("[data-history-ship-id]")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const card = target.closest<HTMLElement>("[data-ship-id]");
|
||||
const shipId = card?.dataset.shipId;
|
||||
if (!shipId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedItems = [{ kind: "ship", id: shipId }];
|
||||
this.syncFollowStateFromSelection();
|
||||
this.focusOnSelection(this.selectedItems[0]);
|
||||
this.toggleCameraMode("follow");
|
||||
this.updatePanels();
|
||||
this.updateGamePanel("Live");
|
||||
};
|
||||
|
||||
private openHistoryWindow(target: Selectable) {
|
||||
const existing = this.historyWindows.find((windowState) => JSON.stringify(windowState.target) === JSON.stringify(target));
|
||||
if (existing) {
|
||||
@@ -2286,14 +2340,7 @@ export class GameViewer {
|
||||
if (this.selectedItems.length !== 1) {
|
||||
return;
|
||||
}
|
||||
const nextFocus = this.resolveSelectionPosition(this.selectedItems[0]);
|
||||
if (nextFocus) {
|
||||
if (this.activeSystemId && this.isSelectionInActiveSystem(this.selectedItems[0])) {
|
||||
this.systemFocusLocal.copy(nextFocus);
|
||||
} else {
|
||||
this.galaxyFocus.copy(nextFocus);
|
||||
}
|
||||
}
|
||||
this.focusOnSelection(this.selectedItems[0]);
|
||||
this.syncFollowStateFromSelection();
|
||||
};
|
||||
|
||||
@@ -2312,7 +2359,10 @@ export class GameViewer {
|
||||
const key = event.key.toLowerCase();
|
||||
this.keyState.add(key);
|
||||
if (["w", "a", "s", "d"].includes(key)) {
|
||||
this.followedShipId = undefined;
|
||||
this.cameraMode = "tactical";
|
||||
}
|
||||
if (key === "c") {
|
||||
this.toggleCameraMode();
|
||||
}
|
||||
if (key === "1") {
|
||||
this.desiredDistance = ZOOM_DISTANCE.local;
|
||||
@@ -2328,6 +2378,51 @@ export class GameViewer {
|
||||
this.keyState.delete(event.key.toLowerCase());
|
||||
};
|
||||
|
||||
private toggleCameraMode(forceMode?: CameraMode) {
|
||||
const nextMode = forceMode ?? (this.cameraMode === "follow" ? "tactical" : "follow");
|
||||
if (nextMode === "tactical") {
|
||||
this.cameraMode = "tactical";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.cameraTargetShipId && this.selectedItems.length === 1 && this.selectedItems[0].kind === "ship") {
|
||||
this.cameraTargetShipId = this.selectedItems[0].id;
|
||||
}
|
||||
|
||||
if (!this.cameraTargetShipId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cameraMode = "follow";
|
||||
this.desiredDistance = Math.min(this.desiredDistance, 1800);
|
||||
this.followCameraPosition.set(0, 0, 0);
|
||||
this.followCameraFocus.set(0, 0, 0);
|
||||
}
|
||||
|
||||
private focusOnSelection(selection: Selectable) {
|
||||
const nextFocus = this.resolveSelectionPosition(selection);
|
||||
if (!nextFocus) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectionSystemId = this.resolveSelectableSystemId(selection);
|
||||
if (selectionSystemId && selection.kind !== "system" && this.world) {
|
||||
const system = this.world.systems.get(selectionSystemId);
|
||||
if (system) {
|
||||
this.galaxyFocus.copy(this.toThreeVector(system.galaxyPosition));
|
||||
this.systemFocusLocal.copy(nextFocus);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.activeSystemId && this.isSelectionInActiveSystem(selection)) {
|
||||
this.systemFocusLocal.copy(nextFocus);
|
||||
return;
|
||||
}
|
||||
|
||||
this.galaxyFocus.copy(nextFocus);
|
||||
}
|
||||
|
||||
private resolveSelectionPosition(selection: Selectable) {
|
||||
if (!this.world) {
|
||||
return undefined;
|
||||
@@ -2339,10 +2434,7 @@ export class GameViewer {
|
||||
}
|
||||
if (selection.kind === "station") {
|
||||
const station = this.world.stations.get(selection.id);
|
||||
const visual = station ? this.stationVisuals.get(station.id) : undefined;
|
||||
return visual
|
||||
? this.computeStructureLocalPosition(visual, this.currentWorldTimeSeconds(), 0.09)
|
||||
: (station ? this.toThreeVector(station.localPosition) : undefined);
|
||||
return station ? this.toThreeVector(station.localPosition) : undefined;
|
||||
}
|
||||
if (selection.kind === "node") {
|
||||
const node = this.world.nodes.get(selection.id);
|
||||
@@ -2473,7 +2565,15 @@ export class GameViewer {
|
||||
}
|
||||
|
||||
private determineActiveSystemId() {
|
||||
if (!this.world || this.currentDistance >= 12000) {
|
||||
if (!this.world) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this.cameraMode === "follow" && this.cameraTargetShipId) {
|
||||
return this.world.ships.get(this.cameraTargetShipId)?.systemId;
|
||||
}
|
||||
|
||||
if (this.currentDistance >= 12000) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -2491,10 +2591,6 @@ export class GameViewer {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.followedShipId) {
|
||||
return this.world.ships.get(this.followedShipId)?.systemId;
|
||||
}
|
||||
|
||||
let nearestSystemId: string | undefined;
|
||||
let nearestDistance = Number.POSITIVE_INFINITY;
|
||||
for (const system of this.world.systems.values()) {
|
||||
@@ -2512,28 +2608,62 @@ export class GameViewer {
|
||||
}
|
||||
|
||||
private updateFollowCamera(delta: number) {
|
||||
if (!this.followedShipId || !this.world) {
|
||||
return;
|
||||
if (!this.cameraTargetShipId || !this.world) {
|
||||
this.cameraMode = "tactical";
|
||||
return false;
|
||||
}
|
||||
|
||||
const ship = this.world.ships.get(this.followedShipId);
|
||||
if (!ship) {
|
||||
this.followedShipId = undefined;
|
||||
return;
|
||||
const ship = this.world.ships.get(this.cameraTargetShipId);
|
||||
const visual = this.shipVisuals.get(this.cameraTargetShipId);
|
||||
if (!ship || !visual) {
|
||||
this.cameraTargetShipId = undefined;
|
||||
this.cameraMode = "tactical";
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = this.toThreeVector(ship.localPosition);
|
||||
this.systemFocusLocal.lerp(target, 1 - Math.exp(-delta * 8));
|
||||
const shipLocalPosition = this.getAnimatedShipLocalPosition(visual);
|
||||
const shipWorldPosition = this.toDisplayLocalPosition(shipLocalPosition, ship.systemId);
|
||||
this.systemFocusLocal.lerp(shipLocalPosition, 1 - Math.exp(-delta * 8));
|
||||
|
||||
this.followCameraDesiredDirection.copy(this.resolveShipHeading(visual, shipLocalPosition)).normalize();
|
||||
this.followCameraDirection.lerp(this.followCameraDesiredDirection, 1 - Math.exp(-delta * 5));
|
||||
this.followCameraDirection.normalize();
|
||||
|
||||
const distance = THREE.MathUtils.clamp(this.currentDistance * 0.72, 320, 6800);
|
||||
const height = THREE.MathUtils.clamp(distance * 0.18, 70, 1100);
|
||||
const lookAhead = THREE.MathUtils.clamp(distance * 0.9, 220, 2400);
|
||||
this.followCameraOffset.copy(this.followCameraDirection).multiplyScalar(-distance);
|
||||
this.followCameraOffset.y += height;
|
||||
|
||||
const desiredPosition = shipWorldPosition.clone().add(this.followCameraOffset);
|
||||
const desiredFocus = shipWorldPosition.clone().addScaledVector(this.followCameraDirection, lookAhead);
|
||||
desiredFocus.y += height * 0.28;
|
||||
|
||||
const positionLerp = 1 - Math.exp(-delta * 6);
|
||||
const focusLerp = 1 - Math.exp(-delta * 8);
|
||||
if (this.followCameraPosition.lengthSq() === 0) {
|
||||
this.followCameraPosition.copy(desiredPosition);
|
||||
this.followCameraFocus.copy(desiredFocus);
|
||||
} else {
|
||||
this.followCameraPosition.lerp(desiredPosition, positionLerp);
|
||||
this.followCameraFocus.lerp(desiredFocus, focusLerp);
|
||||
}
|
||||
|
||||
this.camera.position.copy(this.followCameraPosition);
|
||||
this.camera.lookAt(this.followCameraFocus);
|
||||
return true;
|
||||
}
|
||||
|
||||
private syncFollowStateFromSelection() {
|
||||
if (this.selectedItems.length === 1 && this.selectedItems[0].kind === "ship") {
|
||||
this.followedShipId = this.selectedItems[0].id;
|
||||
this.desiredDistance = Math.min(this.desiredDistance, 1600);
|
||||
this.cameraTargetShipId = this.selectedItems[0].id;
|
||||
return;
|
||||
}
|
||||
|
||||
this.followedShipId = undefined;
|
||||
this.cameraTargetShipId = undefined;
|
||||
if (this.cameraMode === "follow") {
|
||||
this.cameraMode = "tactical";
|
||||
}
|
||||
}
|
||||
|
||||
private updateSystemDetailVisibility() {
|
||||
@@ -2694,8 +2824,8 @@ export class GameViewer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.followedShipId) {
|
||||
const followedShip = this.world.ships.get(this.followedShipId);
|
||||
if (this.cameraMode === "follow" && this.cameraTargetShipId) {
|
||||
const followedShip = this.world.ships.get(this.cameraTargetShipId);
|
||||
if (followedShip?.systemId === systemId) {
|
||||
this.systemFocusLocal.copy(this.toThreeVector(followedShip.localPosition));
|
||||
return;
|
||||
@@ -2761,8 +2891,8 @@ export class GameViewer {
|
||||
moonCount += planet.moonCount;
|
||||
}
|
||||
|
||||
const followText = activeContext && this.followedShipId
|
||||
? `<p>Camera locked to ${this.world.ships.get(this.followedShipId)?.label ?? this.followedShipId}</p>`
|
||||
const followText = activeContext && this.cameraMode === "follow" && this.cameraTargetShipId
|
||||
? `<p>Camera locked to ${this.world.ships.get(this.cameraTargetShipId)?.label ?? this.cameraTargetShipId}</p>`
|
||||
: "";
|
||||
|
||||
return `
|
||||
|
||||
@@ -90,6 +90,7 @@ export interface StationSnapshot {
|
||||
localPosition: Vector3Dto;
|
||||
color: string;
|
||||
dockedShips: number;
|
||||
dockingPads: number;
|
||||
energyStored: number;
|
||||
inventory: InventoryEntry[];
|
||||
factionId: string;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"yPlane": 4,
|
||||
"arrivalThreshold": 16,
|
||||
"miningRate": 28,
|
||||
"miningRate": 10,
|
||||
"miningCycleSeconds": 10,
|
||||
"transferRate": 56,
|
||||
"dockingDuration": 1.2,
|
||||
"undockingDuration": 1.2,
|
||||
"undockDistance": 42,
|
||||
"energy": {
|
||||
"idleDrain": 0.7,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"bulk-liquid": 600,
|
||||
"bulk-gas": 600
|
||||
},
|
||||
"modules": ["docking-clamps", "refinery-stack", "fabricator-array", "power-core", "bulk-bay", "liquid-tank", "gas-tank"]
|
||||
"modules": ["docking-clamps", "dock-bay-small", "power-core", "bulk-bay", "liquid-tank"]
|
||||
},
|
||||
{
|
||||
"id": "trade-hub",
|
||||
@@ -32,7 +32,7 @@
|
||||
"radius": 24,
|
||||
"dockingCapacity": 3,
|
||||
"storage": { "bulk-solid": 2000, "manufactured": 1000, "bulk-liquid": 400, "bulk-gas": 400 },
|
||||
"modules": ["docking-clamps", "refinery-stack", "bulk-bay", "fabricator-array", "power-core", "liquid-tank", "gas-tank"]
|
||||
"modules": ["docking-clamps", "power-core", "bulk-bay", "liquid-tank", "gas-tank", "refinery-stack", "fuel-processor"]
|
||||
},
|
||||
{
|
||||
"id": "farm-ring",
|
||||
|
||||
@@ -47,6 +47,12 @@
|
||||
"storage": "bulk-gas",
|
||||
"summary": "Compressed gas reserves for future chemical and fuel chains."
|
||||
},
|
||||
{
|
||||
"id": "fuel",
|
||||
"label": "Reactor Fuel",
|
||||
"storage": "bulk-liquid",
|
||||
"summary": "Processed liquid fuel consumed by ships and station power systems."
|
||||
},
|
||||
{
|
||||
"id": "water",
|
||||
"label": "Water",
|
||||
|
||||
30
shared/data/module-recipes.json
Normal file
30
shared/data/module-recipes.json
Normal file
@@ -0,0 +1,30 @@
|
||||
[
|
||||
{
|
||||
"moduleId": "dock-bay-small",
|
||||
"duration": 12,
|
||||
"inputs": [
|
||||
{ "itemId": "refined-metals", "amount": 34 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"moduleId": "gas-tank",
|
||||
"duration": 10,
|
||||
"inputs": [
|
||||
{ "itemId": "refined-metals", "amount": 30 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"moduleId": "fuel-processor",
|
||||
"duration": 14,
|
||||
"inputs": [
|
||||
{ "itemId": "refined-metals", "amount": 42 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"moduleId": "refinery-stack",
|
||||
"duration": 14,
|
||||
"inputs": [
|
||||
{ "itemId": "refined-metals", "amount": 38 }
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -41,6 +41,12 @@
|
||||
"category": "mining",
|
||||
"summary": "Articulated mining head for shipborne extraction."
|
||||
},
|
||||
{
|
||||
"id": "gas-extractor",
|
||||
"label": "Gas Extractor",
|
||||
"category": "mining",
|
||||
"summary": "Cryogenic intake and compression rig for harvesting gas clouds."
|
||||
},
|
||||
{
|
||||
"id": "gun-turret",
|
||||
"label": "Gun Turret",
|
||||
@@ -65,6 +71,12 @@
|
||||
"category": "dock",
|
||||
"summary": "Docking collar and transfer arms."
|
||||
},
|
||||
{
|
||||
"id": "dock-bay-small",
|
||||
"label": "Small Dock Bay",
|
||||
"category": "dock",
|
||||
"summary": "External docking truss with two independent ship pads."
|
||||
},
|
||||
{
|
||||
"id": "carrier-bay",
|
||||
"label": "Carrier Bay",
|
||||
@@ -77,6 +89,12 @@
|
||||
"category": "refinery",
|
||||
"summary": "Ore cracking and metal separation."
|
||||
},
|
||||
{
|
||||
"id": "fuel-processor",
|
||||
"label": "Fuel Processor",
|
||||
"category": "refinery",
|
||||
"summary": "Gas cracking and catalytic processing for reactor fuel."
|
||||
},
|
||||
{
|
||||
"id": "turret-grid",
|
||||
"label": "Turret Grid",
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
],
|
||||
"shipFormations": [
|
||||
{ "shipId": "constructor", "count": 1, "center": [45, 0, 20], "systemId": "helios" },
|
||||
{ "shipId": "miner", "count": 1, "center": [52, 0, 24], "systemId": "helios" }
|
||||
{ "shipId": "miner", "count": 1, "center": [52, 0, 24], "systemId": "helios" },
|
||||
{ "shipId": "gas-miner", "count": 1, "center": [60, 0, 28], "systemId": "helios" }
|
||||
],
|
||||
"patrolRoutes": [],
|
||||
"miningDefaults": {
|
||||
|
||||
@@ -111,5 +111,22 @@
|
||||
"size": 6,
|
||||
"maxHealth": 150,
|
||||
"modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "mining-turret", "bulk-bay", "docking-clamps"]
|
||||
},
|
||||
{
|
||||
"id": "gas-miner",
|
||||
"label": "Nimbus Gas Harvester",
|
||||
"role": "mining",
|
||||
"shipClass": "industrial",
|
||||
"speed": 24,
|
||||
"ftlSpeed": 2350,
|
||||
"spoolTime": 3.2,
|
||||
"cargoCapacity": 120,
|
||||
"cargoKind": "bulk-gas",
|
||||
"cargoItemId": "gas",
|
||||
"color": "#8ce5ff",
|
||||
"hullColor": "#2a5668",
|
||||
"size": 6,
|
||||
"maxHealth": 150,
|
||||
"modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "gas-extractor", "gas-tank", "docking-clamps"]
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user