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
|
# 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
|
- ore miner
|
||||||
- elevated system height variance
|
- gas miner
|
||||||
- procedural orbital metadata
|
- station-side `gas -> fuel`
|
||||||
- moons, rings, binary stars, asteroid belts, gas clouds
|
- constructor-led station module installation
|
||||||
- special-case `Sol` system content
|
- 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:
|
Recommended work:
|
||||||
|
|
||||||
- tune galaxy readability at scale
|
- add constructor material logistics
|
||||||
- better starfield depth cues
|
- allow the constructor to fetch module materials instead of assuming they are already on the target station
|
||||||
- stronger color/size differentiation for star classes
|
- add pickup / load phases and delivery priorities
|
||||||
- improved system label decluttering
|
- expose station installed modules and active construction to the viewer
|
||||||
- add galaxy navigation affordances
|
- installed modules
|
||||||
- jump-to-system search
|
- current build target
|
||||||
- constellation / region overlays
|
- build progress
|
||||||
- bookmarks for notable systems such as `Sol`
|
- harden dock management
|
||||||
- tighten viewer performance
|
- make dock eligibility depend on dock type / ship class if needed
|
||||||
- reduce orbit/moon draw cost when zoomed out
|
- surface pad occupancy in debug UI
|
||||||
- pool or simplify distant celestial meshes
|
- add stronger action timing coverage
|
||||||
- profile high-system-count scenes
|
- 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
|
1. mining ore
|
||||||
2. hauling to refining
|
2. harvesting gas
|
||||||
3. refining / fabricating goods
|
3. processing `gas -> fuel`
|
||||||
4. spending those goods on ships and outposts
|
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 those logistics deliberate instead of bootstrap-scripted.
|
||||||
|
|
||||||
The next step is to make faction growth more intentional and legible.
|
|
||||||
|
|
||||||
Recommended work:
|
Recommended work:
|
||||||
|
|
||||||
- make shipbuilding priorities reactive
|
- split logistics roles more clearly
|
||||||
- build more miners / haulers when ore throughput is low
|
- ore miner
|
||||||
- build escorts when industrial losses rise
|
- gas miner
|
||||||
- build warships when frontier pressure rises
|
- hauler / constructor transport
|
||||||
- make expansion logic consume the economy more visibly
|
- make station build priorities responsive
|
||||||
- use industrial stock to claim and fortify central systems
|
- fuel chain first when reserves are low
|
||||||
- expose production pressure in UI
|
- ore refining when fuel is stable
|
||||||
- show ore throughput
|
- docking expansion when traffic backs up
|
||||||
- show fabricated goods
|
- make fuel scarcity visible in debug UI
|
||||||
- show queued faction priorities
|
- fuel throughput
|
||||||
- make resource type differences matter
|
- gas stock
|
||||||
- ore belts vs gas clouds
|
- dock contention
|
||||||
- gas-aware logistics and production choices
|
- move away from generic node selection
|
||||||
|
- let miners prefer nearby valid nodes
|
||||||
## Pirate Harassment
|
- factor travel cost and dock turnaround into throughput planning
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Concrete Implementation Order
|
## Concrete Implementation Order
|
||||||
|
|
||||||
1. Add viewer-scale performance controls for the larger galaxy.
|
1. Add constructor pickup / delivery behavior for module materials.
|
||||||
2. Add faction production heuristics based on current economy and losses.
|
2. Expose station installed modules, dock pads, and active construction in the contracts and viewer.
|
||||||
3. Make pirate target selection explicitly prefer economic targets.
|
3. Add rescue / recovery behavior for power-starved ships.
|
||||||
4. Surface faction stocks, throughput, and build priorities in the HUD/debug views.
|
4. Add faction build priorities based on fuel, ore throughput, and dock saturation.
|
||||||
5. Expand the order/behavior set with higher-value RTS actions like `hold-here`, `attack`, and `defend-area`.
|
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.
|
6. Break backend simulation responsibilities into smaller planning / faction / combat / logistics modules.
|
||||||
|
|
||||||
## Network / Multiplayer
|
## Network / Multiplayer
|
||||||
@@ -133,68 +106,31 @@ Recommended work:
|
|||||||
- versioning
|
- versioning
|
||||||
- reconnect / catch-up semantics
|
- 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
|
## Interest Management
|
||||||
|
|
||||||
The current stream is world-wide.
|
The stream is still world-wide.
|
||||||
|
|
||||||
That means every observer receives deltas for the full simulation, even when only looking at one part of space.
|
|
||||||
|
|
||||||
Recommended work:
|
Recommended work:
|
||||||
|
|
||||||
- add observer/view-scoped subscriptions
|
- 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
|
- send only relevant deltas per observer
|
||||||
- keep coarse strategic updates available for off-screen context
|
- keep strategic summaries 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
|
|
||||||
|
|||||||
62
SESSION.md
62
SESSION.md
@@ -108,15 +108,27 @@ The backend simulation already includes:
|
|||||||
|
|
||||||
- autonomous ships
|
- autonomous ships
|
||||||
- orbital travel
|
- 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
|
- mining and refinery delivery
|
||||||
- module-gated ship and station capabilities
|
- module-gated ship and station capabilities
|
||||||
- ships require fitted modules such as reactor, capacitor, and mining or gun turrets
|
- ships require fitted modules such as reactor, capacitor, mining turret, gas extractor, or gun turrets
|
||||||
- stations require fitted modules such as power, refinery, and storage modules
|
- stations require installed modules such as power, docking, refinery, fuel processing, and storage modules
|
||||||
- fuel-to-energy power simulation
|
- fuel-to-energy power simulation
|
||||||
- ship reactors consume `gas` fuel to charge capacitors
|
- gas clouds provide raw `gas`
|
||||||
- station power cores consume `gas` fuel to charge station energy buffers
|
- 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
|
- 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
|
- refining / fabrication
|
||||||
- faction growth through ship and outpost production
|
- faction growth through ship and outpost production
|
||||||
- pirate pressure and combat
|
- pirate pressure and combat
|
||||||
@@ -129,7 +141,7 @@ The backend simulation already includes:
|
|||||||
- systems in galaxy space
|
- systems in galaxy space
|
||||||
- in-system entities in local space
|
- in-system entities in local space
|
||||||
- item-based inventories for ships and stations
|
- 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:
|
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 modular station
|
||||||
- one constructor ship
|
- one constructor ship
|
||||||
- one mining 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
|
- moved the starting ships close to the `helios` star and next to each other
|
||||||
- added modular ship and station data
|
- added modular ship and station data
|
||||||
- new station power and storage modules
|
- 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
|
- refactored simulation inventories to per-item storage
|
||||||
- stations and ships now replicate inventories instead of specialized ore/refined/cargo counters
|
- 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
|
- replaced the viewer bottom faction strip with a horizontal ship-card debugging rail
|
||||||
- added movable, resizable, multi-window history panels in the viewer
|
- added movable, resizable, multi-window history panels in the viewer
|
||||||
- fixed the auto-miner undock controller transition
|
- 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
|
- 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
|
- moon rendering is procedural from counts, not authored moon-by-moon data
|
||||||
- resource extraction behavior still treats all resource nodes generically
|
- resource extraction behavior still treats all resource nodes generically
|
||||||
- item inventories exist, but storage enforcement and module-slot restrictions are still lightweight
|
- item inventories exist, but storage/module restrictions are still partial
|
||||||
- cargo/storage compatibility is mostly data-convention driven
|
- 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
|
- 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
|
- piracy and faction growth are still functional rather than strategically deep
|
||||||
- no persistence for saves, seeds, or reconnect state
|
- 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)
|
- [apps/backend/Simulation/SimulationEngine.cs](/home/jbourdon/repos/space-game/apps/backend/Simulation/SimulationEngine.cs)
|
||||||
- simulation advancement
|
- simulation advancement
|
||||||
- snapshot / delta composition for galaxy-space systems and local-space entities
|
- snapshot / delta composition for galaxy-space systems and local-space entities
|
||||||
- inventory, fuel, and energy processing
|
- inventory, gas/fuel, and energy processing
|
||||||
- auto-miner undock state transition fix
|
- 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)
|
- [apps/backend/Simulation/ScenarioLoader.cs](/home/jbourdon/repos/space-game/apps/backend/Simulation/ScenarioLoader.cs)
|
||||||
- faction bootstrap
|
- faction bootstrap
|
||||||
- galaxy generation
|
- galaxy generation
|
||||||
- special systems
|
- special systems
|
||||||
- procedural celestial/resource content
|
- procedural celestial/resource content
|
||||||
- normalization of authored placements into system-local space
|
- 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)
|
- [apps/backend/Contracts/WorldContracts.cs](/home/jbourdon/repos/space-game/apps/backend/Contracts/WorldContracts.cs)
|
||||||
- snapshot and delta contract shape
|
- snapshot and delta contract shape
|
||||||
|
- station dock-pad count
|
||||||
- [apps/backend/Simulation/RuntimeModels.cs](/home/jbourdon/repos/space-game/apps/backend/Simulation/RuntimeModels.cs)
|
- [apps/backend/Simulation/RuntimeModels.cs](/home/jbourdon/repos/space-game/apps/backend/Simulation/RuntimeModels.cs)
|
||||||
- runtime vector math and world model
|
- runtime vector math and world model
|
||||||
- per-item inventories on ships and stations
|
- 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)
|
- [apps/viewer/src/GameViewer.ts](/home/jbourdon/repos/space-game/apps/viewer/src/GameViewer.ts)
|
||||||
- camera, selection, streaming integration, and presentation
|
- camera, selection, streaming integration, and presentation
|
||||||
- layered local/remote system presentation
|
- layered local/remote system presentation
|
||||||
- orbital reconstruction and moon rendering
|
- orbital reconstruction and moon rendering
|
||||||
- projected shell markers and hover labels
|
- projected shell markers and hover labels
|
||||||
- ship card list and multi-window history debugging UI
|
- 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)
|
- [apps/viewer/src/style.css](/home/jbourdon/repos/space-game/apps/viewer/src/style.css)
|
||||||
- HUD layout
|
- HUD layout
|
||||||
- ship-card rail
|
- 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
|
- viewer-side snapshot contract for galaxy-space systems and local-space entities
|
||||||
- [shared/data](/home/jbourdon/repos/space-game/shared/data)
|
- [shared/data](/home/jbourdon/repos/space-game/shared/data)
|
||||||
- scenario and world data definitions
|
- scenario and world data definitions
|
||||||
|
- `module-recipes.json` now defines timed module construction costs
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
Validation passing at the end of this session:
|
Validation passing at the end of this session:
|
||||||
|
|
||||||
- `dotnet build apps/backend/SpaceGame.Simulation.Api.csproj`
|
- `dotnet build apps/backend/SpaceGame.Simulation.Api.csproj`
|
||||||
|
|
||||||
|
Validation currently failing / blocked:
|
||||||
|
|
||||||
- `cd apps/viewer && npm run build`
|
- `cd apps/viewer && npm run build`
|
||||||
|
- fails because `apps/viewer/src/GameViewer.ts` references a missing `followedShipId` property
|
||||||
|
|
||||||
## Last Commit
|
## Last Commit
|
||||||
|
|
||||||
- `1747d84`
|
- `ef62577`
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ public sealed record StationSnapshot(
|
|||||||
Vector3Dto LocalPosition,
|
Vector3Dto LocalPosition,
|
||||||
string Color,
|
string Color,
|
||||||
int DockedShips,
|
int DockedShips,
|
||||||
|
int DockingPads,
|
||||||
float EnergyStored,
|
float EnergyStored,
|
||||||
IReadOnlyList<InventoryEntry> Inventory,
|
IReadOnlyList<InventoryEntry> Inventory,
|
||||||
string FactionId);
|
string FactionId);
|
||||||
@@ -98,6 +99,7 @@ public sealed record StationDelta(
|
|||||||
Vector3Dto LocalPosition,
|
Vector3Dto LocalPosition,
|
||||||
string Color,
|
string Color,
|
||||||
int DockedShips,
|
int DockedShips,
|
||||||
|
int DockingPads,
|
||||||
float EnergyStored,
|
float EnergyStored,
|
||||||
IReadOnlyList<InventoryEntry> Inventory,
|
IReadOnlyList<InventoryEntry> Inventory,
|
||||||
string FactionId);
|
string FactionId);
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ public sealed class BalanceDefinition
|
|||||||
public float YPlane { get; set; }
|
public float YPlane { get; set; }
|
||||||
public float ArrivalThreshold { get; set; }
|
public float ArrivalThreshold { get; set; }
|
||||||
public float MiningRate { get; set; }
|
public float MiningRate { get; set; }
|
||||||
|
public float MiningCycleSeconds { get; set; }
|
||||||
public float TransferRate { get; set; }
|
public float TransferRate { get; set; }
|
||||||
public float DockingDuration { get; set; }
|
public float DockingDuration { get; set; }
|
||||||
|
public float UndockingDuration { get; set; }
|
||||||
public float UndockDistance { get; set; }
|
public float UndockDistance { get; set; }
|
||||||
public EnergyBalanceDefinition Energy { get; set; } = new();
|
public EnergyBalanceDefinition Energy { get; set; } = new();
|
||||||
public FuelBalanceDefinition Fuel { 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 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 sealed class PlanetDefinition
|
||||||
{
|
{
|
||||||
public required string Label { get; set; }
|
public required string Label { get; set; }
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public sealed class SimulationWorld
|
|||||||
public required List<FactionRuntime> Factions { get; init; }
|
public required List<FactionRuntime> Factions { get; init; }
|
||||||
public required Dictionary<string, ShipDefinition> ShipDefinitions { get; init; }
|
public required Dictionary<string, ShipDefinition> ShipDefinitions { get; init; }
|
||||||
public required Dictionary<string, ItemDefinition> ItemDefinitions { 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 int TickIntervalMs { get; init; } = 200;
|
||||||
public DateTimeOffset GeneratedAtUtc { get; set; }
|
public DateTimeOffset GeneratedAtUtc { get; set; }
|
||||||
}
|
}
|
||||||
@@ -43,13 +44,24 @@ public sealed class StationRuntime
|
|||||||
public required ConstructibleDefinition Definition { get; init; }
|
public required ConstructibleDefinition Definition { get; init; }
|
||||||
public required Vector3 Position { get; init; }
|
public required Vector3 Position { get; init; }
|
||||||
public required string FactionId { 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<string, float> Inventory { get; } = new(StringComparer.Ordinal);
|
||||||
|
public Dictionary<int, string> DockingPadAssignments { get; } = new();
|
||||||
public float EnergyStored { get; set; }
|
public float EnergyStored { get; set; }
|
||||||
public float ProcessTimer { get; set; }
|
public float ProcessTimer { get; set; }
|
||||||
public HashSet<string> DockedShipIds { get; } = [];
|
public HashSet<string> DockedShipIds { get; } = [];
|
||||||
|
public ModuleConstructionRuntime? ActiveConstruction { get; set; }
|
||||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
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 sealed class ShipRuntime
|
||||||
{
|
{
|
||||||
public required string Id { get; init; }
|
public required string Id { get; init; }
|
||||||
@@ -67,6 +79,7 @@ public sealed class ShipRuntime
|
|||||||
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
|
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
|
||||||
public float EnergyStored { get; set; }
|
public float EnergyStored { get; set; }
|
||||||
public string? DockedStationId { get; set; }
|
public string? DockedStationId { get; set; }
|
||||||
|
public int? AssignedDockingPadIndex { get; set; }
|
||||||
public float Health { get; set; }
|
public float Health { get; set; }
|
||||||
public List<string> History { get; } = [];
|
public List<string> History { get; } = [];
|
||||||
public string LastSignature { get; set; } = string.Empty;
|
public string LastSignature { get; set; } = string.Empty;
|
||||||
@@ -98,8 +111,10 @@ public sealed class DefaultBehaviorRuntime
|
|||||||
{
|
{
|
||||||
public required string Kind { get; set; }
|
public required string Kind { get; set; }
|
||||||
public string? AreaSystemId { get; set; }
|
public string? AreaSystemId { get; set; }
|
||||||
|
public string? StationId { get; set; }
|
||||||
public string? RefineryId { get; set; }
|
public string? RefineryId { get; set; }
|
||||||
public string? NodeId { get; set; }
|
public string? NodeId { get; set; }
|
||||||
|
public string? ModuleId { get; set; }
|
||||||
public string? Phase { get; set; }
|
public string? Phase { get; set; }
|
||||||
public List<Vector3> PatrolPoints { get; set; } = [];
|
public List<Vector3> PatrolPoints { get; set; } = [];
|
||||||
public int PatrolIndex { get; set; }
|
public int PatrolIndex { get; set; }
|
||||||
|
|||||||
@@ -88,11 +88,13 @@ public sealed class ScenarioLoader
|
|||||||
var ships = Read<List<ShipDefinition>>("ships.json");
|
var ships = Read<List<ShipDefinition>>("ships.json");
|
||||||
var constructibles = Read<List<ConstructibleDefinition>>("constructibles.json");
|
var constructibles = Read<List<ConstructibleDefinition>>("constructibles.json");
|
||||||
var items = Read<List<ItemDefinition>>("items.json");
|
var items = Read<List<ItemDefinition>>("items.json");
|
||||||
|
var moduleRecipes = Read<List<ModuleRecipeDefinition>>("module-recipes.json");
|
||||||
var balance = Read<BalanceDefinition>("balance.json");
|
var balance = Read<BalanceDefinition>("balance.json");
|
||||||
|
|
||||||
var shipDefinitions = ships.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
var shipDefinitions = ships.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
||||||
var constructibleDefinitions = constructibles.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 itemDefinitions = items.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
||||||
|
var moduleRecipeDefinitions = moduleRecipes.ToDictionary((definition) => definition.ModuleId, StringComparer.Ordinal);
|
||||||
var systemRuntimes = systems
|
var systemRuntimes = systems
|
||||||
.Select((definition) => new SystemRuntime
|
.Select((definition) => new SystemRuntime
|
||||||
{
|
{
|
||||||
@@ -141,17 +143,23 @@ public sealed class ScenarioLoader
|
|||||||
Position = ResolveStationPosition(system, plan, balance),
|
Position = ResolveStationPosition(system, plan, balance),
|
||||||
FactionId = plan.FactionId ?? DefaultFactionId,
|
FactionId = plan.FactionId ?? DefaultFactionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
foreach (var moduleId in definition.Modules)
|
||||||
|
{
|
||||||
|
stations[^1].InstalledModules.Add(moduleId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var station in stations)
|
foreach (var station in stations)
|
||||||
{
|
{
|
||||||
station.Inventory["gas"] = 320f;
|
station.Inventory["fuel"] = 240f;
|
||||||
|
station.Inventory["refined-metals"] = 120f;
|
||||||
}
|
}
|
||||||
|
|
||||||
var refinery = stations.FirstOrDefault((station) =>
|
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)
|
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(
|
var patrolRoutes = scenario.PatrolRoutes.ToDictionary(
|
||||||
(route) => route.SystemId,
|
(route) => route.SystemId,
|
||||||
@@ -185,9 +193,15 @@ public sealed class ScenarioLoader
|
|||||||
});
|
});
|
||||||
|
|
||||||
shipsRuntime[^1].Inventory["gas"] = definition.Id switch
|
shipsRuntime[^1].Inventory["gas"] = definition.Id switch
|
||||||
|
{
|
||||||
|
_ => 0f,
|
||||||
|
};
|
||||||
|
shipsRuntime[^1].Inventory.Remove("gas");
|
||||||
|
shipsRuntime[^1].Inventory["fuel"] = definition.Id switch
|
||||||
{
|
{
|
||||||
"constructor" => 90f,
|
"constructor" => 90f,
|
||||||
"miner" => 90f,
|
"miner" => 90f,
|
||||||
|
"gas-miner" => 90f,
|
||||||
_ => 120f,
|
_ => 120f,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -208,6 +222,7 @@ public sealed class ScenarioLoader
|
|||||||
Factions = factions,
|
Factions = factions,
|
||||||
ShipDefinitions = shipDefinitions,
|
ShipDefinitions = shipDefinitions,
|
||||||
ItemDefinitions = itemDefinitions,
|
ItemDefinitions = itemDefinitions,
|
||||||
|
ModuleRecipes = moduleRecipeDefinitions,
|
||||||
GeneratedAtUtc = DateTimeOffset.UtcNow,
|
GeneratedAtUtc = DateTimeOffset.UtcNow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -818,13 +833,33 @@ public sealed class ScenarioLoader
|
|||||||
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
|
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
|
||||||
StationRuntime? refinery)
|
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)
|
if (HasModules(definition, "reactor-core", "capacitor-bank", "mining-turret") && refinery is not null)
|
||||||
{
|
{
|
||||||
return new DefaultBehaviorRuntime
|
return new DefaultBehaviorRuntime
|
||||||
{
|
{
|
||||||
Kind = "auto-mine",
|
Kind = "auto-mine",
|
||||||
AreaSystemId = scenario.MiningDefaults.NodeSystemId,
|
AreaSystemId = scenario.MiningDefaults.NodeSystemId,
|
||||||
RefineryId = refinery.Id,
|
StationId = refinery.Id,
|
||||||
Phase = "travel-to-node",
|
Phase = "travel-to-node",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -883,6 +918,9 @@ public sealed class ScenarioLoader
|
|||||||
private static bool HasModules(ConstructibleDefinition definition, params string[] modules) =>
|
private static bool HasModules(ConstructibleDefinition definition, params string[] modules) =>
|
||||||
modules.All((moduleId) => definition.Modules.Contains(moduleId, StringComparer.Ordinal));
|
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) =>
|
private static bool HasModules(ShipDefinition definition, params string[] modules) =>
|
||||||
modules.All((moduleId) => definition.Modules.Contains(moduleId, StringComparer.Ordinal));
|
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 ZoomLevel = "local" | "system" | "universe";
|
||||||
type SelectionGroup = "ships" | "structures" | "celestials";
|
type SelectionGroup = "ships" | "structures" | "celestials";
|
||||||
type DragMode = "orbit" | "marquee";
|
type DragMode = "orbit" | "marquee";
|
||||||
|
type CameraMode = "tactical" | "follow";
|
||||||
type Selectable =
|
type Selectable =
|
||||||
| { kind: "ship"; id: string }
|
| { kind: "ship"; id: string }
|
||||||
| { kind: "station"; id: string }
|
| { kind: "station"; id: string }
|
||||||
@@ -245,6 +246,7 @@ export class GameViewer {
|
|||||||
private desiredDistance = ZOOM_DISTANCE.system;
|
private desiredDistance = ZOOM_DISTANCE.system;
|
||||||
private orbitYaw = -2.3;
|
private orbitYaw = -2.3;
|
||||||
private orbitPitch = 0.62;
|
private orbitPitch = 0.62;
|
||||||
|
private cameraMode: CameraMode = "tactical";
|
||||||
private dragMode?: DragMode;
|
private dragMode?: DragMode;
|
||||||
private dragPointerId?: number;
|
private dragPointerId?: number;
|
||||||
private dragStart = new THREE.Vector2();
|
private dragStart = new THREE.Vector2();
|
||||||
@@ -252,7 +254,12 @@ export class GameViewer {
|
|||||||
private marqueeActive = false;
|
private marqueeActive = false;
|
||||||
private suppressClickSelection = false;
|
private suppressClickSelection = false;
|
||||||
private activeSystemId?: string;
|
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 readonly historyWindows: HistoryWindowState[] = [];
|
||||||
private historyWindowCounter = 0;
|
private historyWindowCounter = 0;
|
||||||
private historyWindowZCounter = 10;
|
private historyWindowZCounter = 10;
|
||||||
@@ -334,6 +341,7 @@ export class GameViewer {
|
|||||||
this.renderer.domElement.addEventListener("dblclick", this.onDoubleClick);
|
this.renderer.domElement.addEventListener("dblclick", this.onDoubleClick);
|
||||||
this.renderer.domElement.addEventListener("wheel", this.onWheel, { passive: false });
|
this.renderer.domElement.addEventListener("wheel", this.onWheel, { passive: false });
|
||||||
this.factionStripEl.addEventListener("click", this.onShipStripClick);
|
this.factionStripEl.addEventListener("click", this.onShipStripClick);
|
||||||
|
this.factionStripEl.addEventListener("dblclick", this.onShipStripDoubleClick);
|
||||||
this.historyLayerEl.addEventListener("click", this.onHistoryLayerClick);
|
this.historyLayerEl.addEventListener("click", this.onHistoryLayerClick);
|
||||||
this.historyLayerEl.addEventListener("pointerdown", this.onHistoryLayerPointerDown);
|
this.historyLayerEl.addEventListener("pointerdown", this.onHistoryLayerPointerDown);
|
||||||
window.addEventListener("pointermove", this.onHistoryWindowPointerMove);
|
window.addEventListener("pointermove", this.onHistoryWindowPointerMove);
|
||||||
@@ -709,18 +717,30 @@ export class GameViewer {
|
|||||||
.map((ship) => {
|
.map((ship) => {
|
||||||
const fuel = this.inventoryAmount(ship.inventory, "gas");
|
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 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 `
|
return `
|
||||||
<article class="ship-card${isSelected ? " is-selected" : ""}${isFollowed ? " is-followed" : ""}" data-ship-id="${ship.id}">
|
<article class="ship-card${isSelected ? " is-selected" : ""}${isFollowed ? " is-followed" : ""}" data-ship-id="${ship.id}">
|
||||||
<div class="ship-card-header">
|
<div class="ship-card-header">
|
||||||
<h3>${ship.label}</h3>
|
<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>
|
</div>
|
||||||
<p>${ship.systemId}</p>
|
<p>${ship.systemId}</p>
|
||||||
<p>Fuel ${fuel.toFixed(0)} · Energy ${ship.energyStored.toFixed(0)}</p>
|
<p>Fuel ${fuel.toFixed(0)} · Energy ${ship.energyStored.toFixed(0)}</p>
|
||||||
<p>State ${ship.state}</p>
|
<p>State ${ship.state}</p>
|
||||||
<p>Order ${ship.orderKind ?? "none"}</p>
|
<div class="ship-card-ai">
|
||||||
<button type="button" class="ship-card-history-button" data-history-ship-id="${ship.id}">Open History</button>
|
<p>Order ${ship.orderKind ?? "none"}</p>
|
||||||
|
<p>Behavior ${ship.defaultBehaviorKind}</p>
|
||||||
|
<p>Task ${ship.controllerTaskKind}</p>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
`;
|
`;
|
||||||
})
|
})
|
||||||
@@ -767,14 +787,12 @@ export class GameViewer {
|
|||||||
this.detailTitleEl.textContent = ship.label;
|
this.detailTitleEl.textContent = ship.label;
|
||||||
const cargoUsed = ship.inventory.reduce((sum, entry) => sum + entry.amount, 0);
|
const cargoUsed = ship.inventory.reduce((sum, entry) => sum + entry.amount, 0);
|
||||||
this.detailBodyEl.innerHTML = `
|
this.detailBodyEl.innerHTML = `
|
||||||
<p>${ship.shipClass} · ${ship.role} · ${ship.systemId}</p>
|
|
||||||
<p>Parent ${parent}</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>Energy ${ship.energyStored.toFixed(0)}<br>Cargo ${cargoUsed.toFixed(0)} / ${ship.cargoCapacity.toFixed(0)}</p>
|
||||||
<p>Inventory ${this.formatInventory(ship.inventory)}</p>
|
<p>Inventory ${this.formatInventory(ship.inventory)}</p>
|
||||||
<p>Velocity ${this.formatVector(ship.localVelocity)}</p>
|
<p>Velocity ${this.formatVector(ship.localVelocity)}</p>
|
||||||
<p>${this.followedShipId === ship.id ? "Camera follow engaged" : "Camera follow idle"}</p>
|
<p>Camera ${this.cameraMode === "follow" && this.cameraTargetShipId === ship.id ? "camera-follow" : "tactical"}<br>Press C to toggle follow</p>
|
||||||
<p>History available from the ship card list.</p>
|
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -789,7 +807,7 @@ export class GameViewer {
|
|||||||
this.detailBodyEl.innerHTML = `
|
this.detailBodyEl.innerHTML = `
|
||||||
<p>${station.category} · ${station.systemId}</p>
|
<p>${station.category} · ${station.systemId}</p>
|
||||||
<p>Parent ${parent}</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>Inventory ${this.formatInventory(station.inventory)}</p>
|
||||||
<p>History available in the separate history window.</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.currentDistance = THREE.MathUtils.damp(this.currentDistance, this.desiredDistance, 7.5, delta);
|
||||||
this.zoomLevel = this.classifyZoomLevel(this.currentDistance);
|
this.zoomLevel = this.classifyZoomLevel(this.currentDistance);
|
||||||
this.updateActiveSystem();
|
this.updateActiveSystem();
|
||||||
this.updateFollowCamera(delta);
|
if (this.cameraMode === "follow" && this.updateFollowCamera(delta)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.updatePanFromKeyboard(delta);
|
this.updatePanFromKeyboard(delta);
|
||||||
this.orbitPitch = THREE.MathUtils.clamp(this.orbitPitch, 0.18, 1.3);
|
this.orbitPitch = THREE.MathUtils.clamp(this.orbitPitch, 0.18, 1.3);
|
||||||
|
|
||||||
@@ -895,10 +916,6 @@ export class GameViewer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updatePanFromKeyboard(delta: number) {
|
private updatePanFromKeyboard(delta: number) {
|
||||||
if (this.followedShipId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const move = new THREE.Vector3();
|
const move = new THREE.Vector3();
|
||||||
if (this.keyState.has("w")) {
|
if (this.keyState.has("w")) {
|
||||||
move.z -= 1;
|
move.z -= 1;
|
||||||
@@ -944,8 +961,8 @@ export class GameViewer {
|
|||||||
const iconAlpha = isProjectedSystemIcon
|
const iconAlpha = isProjectedSystemIcon
|
||||||
? 0
|
? 0
|
||||||
: entry.hideIconInUniverse
|
: entry.hideIconInUniverse
|
||||||
? blend.systemWeight * (isActiveDetail ? 1 : 0)
|
? blend.systemWeight * (isActiveDetail ? 1 : 0)
|
||||||
: Math.max(blend.systemWeight, blend.universeWeight);
|
: Math.max(blend.systemWeight, blend.universeWeight);
|
||||||
|
|
||||||
this.setObjectOpacity(entry.detail, detailAlpha);
|
this.setObjectOpacity(entry.detail, detailAlpha);
|
||||||
this.setObjectOpacity(entry.icon, iconAlpha);
|
this.setObjectOpacity(entry.icon, iconAlpha);
|
||||||
@@ -1064,21 +1081,14 @@ export class GameViewer {
|
|||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
const worldTimeSeconds = this.currentWorldTimeSeconds();
|
const worldTimeSeconds = this.currentWorldTimeSeconds();
|
||||||
for (const visual of this.shipVisuals.values()) {
|
for (const visual of this.shipVisuals.values()) {
|
||||||
const elapsedMs = now - visual.receivedAtMs;
|
const worldPosition = this.getAnimatedShipLocalPosition(visual, now);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
visual.mesh.position.copy(this.toDisplayLocalPosition(worldPosition, visual.systemId));
|
visual.mesh.position.copy(this.toDisplayLocalPosition(worldPosition, visual.systemId));
|
||||||
visual.icon.position.copy(visual.mesh.position);
|
visual.icon.position.copy(visual.mesh.position);
|
||||||
const shipVisible = visual.systemId === this.activeSystemId;
|
const shipVisible = visual.systemId === this.activeSystemId;
|
||||||
visual.mesh.visible = shipVisible;
|
visual.mesh.visible = shipVisible;
|
||||||
visual.icon.visible = shipVisible && visual.icon.visible;
|
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) {
|
if (desiredHeading.lengthSq() > 0.01) {
|
||||||
visual.mesh.lookAt(visual.mesh.position.clone().add(desiredHeading));
|
visual.mesh.lookAt(visual.mesh.position.clone().add(desiredHeading));
|
||||||
}
|
}
|
||||||
@@ -1091,8 +1101,7 @@ export class GameViewer {
|
|||||||
visual.mesh.visible = visual.systemId === this.activeSystemId;
|
visual.mesh.visible = visual.systemId === this.activeSystemId;
|
||||||
}
|
}
|
||||||
for (const visual of this.stationVisuals.values()) {
|
for (const visual of this.stationVisuals.values()) {
|
||||||
const animatedLocalPosition = this.computeStructureLocalPosition(visual, this.currentWorldTimeSeconds(), 0.09);
|
visual.mesh.position.copy(this.toDisplayLocalPosition(visual.localPosition, visual.systemId));
|
||||||
visual.mesh.position.copy(this.toDisplayLocalPosition(animatedLocalPosition, visual.systemId));
|
|
||||||
visual.icon.position.copy(visual.mesh.position);
|
visual.icon.position.copy(visual.mesh.position);
|
||||||
visual.mesh.visible = visual.systemId === this.activeSystemId;
|
visual.mesh.visible = visual.systemId === this.activeSystemId;
|
||||||
}
|
}
|
||||||
@@ -1102,6 +1111,25 @@ export class GameViewer {
|
|||||||
this.updateSystemSummaryPresentation();
|
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() {
|
private updatePlanetPresentation() {
|
||||||
const nowSeconds = this.currentWorldTimeSeconds();
|
const nowSeconds = this.currentWorldTimeSeconds();
|
||||||
for (const visual of this.planetVisuals) {
|
for (const visual of this.planetVisuals) {
|
||||||
@@ -1652,8 +1680,10 @@ export class GameViewer {
|
|||||||
? new Date(this.world.generatedAtUtc).toLocaleTimeString()
|
? new Date(this.world.generatedAtUtc).toLocaleTimeString()
|
||||||
: "n/a";
|
: "n/a";
|
||||||
const activeSystem = this.activeSystemId ?? "deep-space";
|
const activeSystem = this.activeSystemId ?? "deep-space";
|
||||||
|
const cameraModeLabel = this.cameraMode === "follow" ? "camera-follow" : "tactical";
|
||||||
this.statusEl.textContent = [
|
this.statusEl.textContent = [
|
||||||
`mode: ${mode}`,
|
`mode: ${mode}`,
|
||||||
|
`camera: ${cameraModeLabel}`,
|
||||||
`zoom: ${this.zoomLevel}`,
|
`zoom: ${this.zoomLevel}`,
|
||||||
`system: ${activeSystem}`,
|
`system: ${activeSystem}`,
|
||||||
`sequence: ${sequence}`,
|
`sequence: ${sequence}`,
|
||||||
@@ -2009,6 +2039,30 @@ export class GameViewer {
|
|||||||
this.updatePanels();
|
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) {
|
private openHistoryWindow(target: Selectable) {
|
||||||
const existing = this.historyWindows.find((windowState) => JSON.stringify(windowState.target) === JSON.stringify(target));
|
const existing = this.historyWindows.find((windowState) => JSON.stringify(windowState.target) === JSON.stringify(target));
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -2286,14 +2340,7 @@ export class GameViewer {
|
|||||||
if (this.selectedItems.length !== 1) {
|
if (this.selectedItems.length !== 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nextFocus = this.resolveSelectionPosition(this.selectedItems[0]);
|
this.focusOnSelection(this.selectedItems[0]);
|
||||||
if (nextFocus) {
|
|
||||||
if (this.activeSystemId && this.isSelectionInActiveSystem(this.selectedItems[0])) {
|
|
||||||
this.systemFocusLocal.copy(nextFocus);
|
|
||||||
} else {
|
|
||||||
this.galaxyFocus.copy(nextFocus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.syncFollowStateFromSelection();
|
this.syncFollowStateFromSelection();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2312,7 +2359,10 @@ export class GameViewer {
|
|||||||
const key = event.key.toLowerCase();
|
const key = event.key.toLowerCase();
|
||||||
this.keyState.add(key);
|
this.keyState.add(key);
|
||||||
if (["w", "a", "s", "d"].includes(key)) {
|
if (["w", "a", "s", "d"].includes(key)) {
|
||||||
this.followedShipId = undefined;
|
this.cameraMode = "tactical";
|
||||||
|
}
|
||||||
|
if (key === "c") {
|
||||||
|
this.toggleCameraMode();
|
||||||
}
|
}
|
||||||
if (key === "1") {
|
if (key === "1") {
|
||||||
this.desiredDistance = ZOOM_DISTANCE.local;
|
this.desiredDistance = ZOOM_DISTANCE.local;
|
||||||
@@ -2328,6 +2378,51 @@ export class GameViewer {
|
|||||||
this.keyState.delete(event.key.toLowerCase());
|
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) {
|
private resolveSelectionPosition(selection: Selectable) {
|
||||||
if (!this.world) {
|
if (!this.world) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -2339,10 +2434,7 @@ export class GameViewer {
|
|||||||
}
|
}
|
||||||
if (selection.kind === "station") {
|
if (selection.kind === "station") {
|
||||||
const station = this.world.stations.get(selection.id);
|
const station = this.world.stations.get(selection.id);
|
||||||
const visual = station ? this.stationVisuals.get(station.id) : undefined;
|
return station ? this.toThreeVector(station.localPosition) : undefined;
|
||||||
return visual
|
|
||||||
? this.computeStructureLocalPosition(visual, this.currentWorldTimeSeconds(), 0.09)
|
|
||||||
: (station ? this.toThreeVector(station.localPosition) : undefined);
|
|
||||||
}
|
}
|
||||||
if (selection.kind === "node") {
|
if (selection.kind === "node") {
|
||||||
const node = this.world.nodes.get(selection.id);
|
const node = this.world.nodes.get(selection.id);
|
||||||
@@ -2473,7 +2565,15 @@ export class GameViewer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private determineActiveSystemId() {
|
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;
|
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 nearestSystemId: string | undefined;
|
||||||
let nearestDistance = Number.POSITIVE_INFINITY;
|
let nearestDistance = Number.POSITIVE_INFINITY;
|
||||||
for (const system of this.world.systems.values()) {
|
for (const system of this.world.systems.values()) {
|
||||||
@@ -2512,28 +2608,62 @@ export class GameViewer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateFollowCamera(delta: number) {
|
private updateFollowCamera(delta: number) {
|
||||||
if (!this.followedShipId || !this.world) {
|
if (!this.cameraTargetShipId || !this.world) {
|
||||||
return;
|
this.cameraMode = "tactical";
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ship = this.world.ships.get(this.followedShipId);
|
const ship = this.world.ships.get(this.cameraTargetShipId);
|
||||||
if (!ship) {
|
const visual = this.shipVisuals.get(this.cameraTargetShipId);
|
||||||
this.followedShipId = undefined;
|
if (!ship || !visual) {
|
||||||
return;
|
this.cameraTargetShipId = undefined;
|
||||||
|
this.cameraMode = "tactical";
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = this.toThreeVector(ship.localPosition);
|
const shipLocalPosition = this.getAnimatedShipLocalPosition(visual);
|
||||||
this.systemFocusLocal.lerp(target, 1 - Math.exp(-delta * 8));
|
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() {
|
private syncFollowStateFromSelection() {
|
||||||
if (this.selectedItems.length === 1 && this.selectedItems[0].kind === "ship") {
|
if (this.selectedItems.length === 1 && this.selectedItems[0].kind === "ship") {
|
||||||
this.followedShipId = this.selectedItems[0].id;
|
this.cameraTargetShipId = this.selectedItems[0].id;
|
||||||
this.desiredDistance = Math.min(this.desiredDistance, 1600);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.followedShipId = undefined;
|
this.cameraTargetShipId = undefined;
|
||||||
|
if (this.cameraMode === "follow") {
|
||||||
|
this.cameraMode = "tactical";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateSystemDetailVisibility() {
|
private updateSystemDetailVisibility() {
|
||||||
@@ -2694,8 +2824,8 @@ export class GameViewer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.followedShipId) {
|
if (this.cameraMode === "follow" && this.cameraTargetShipId) {
|
||||||
const followedShip = this.world.ships.get(this.followedShipId);
|
const followedShip = this.world.ships.get(this.cameraTargetShipId);
|
||||||
if (followedShip?.systemId === systemId) {
|
if (followedShip?.systemId === systemId) {
|
||||||
this.systemFocusLocal.copy(this.toThreeVector(followedShip.localPosition));
|
this.systemFocusLocal.copy(this.toThreeVector(followedShip.localPosition));
|
||||||
return;
|
return;
|
||||||
@@ -2761,8 +2891,8 @@ export class GameViewer {
|
|||||||
moonCount += planet.moonCount;
|
moonCount += planet.moonCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
const followText = activeContext && this.followedShipId
|
const followText = activeContext && this.cameraMode === "follow" && this.cameraTargetShipId
|
||||||
? `<p>Camera locked to ${this.world.ships.get(this.followedShipId)?.label ?? this.followedShipId}</p>`
|
? `<p>Camera locked to ${this.world.ships.get(this.cameraTargetShipId)?.label ?? this.cameraTargetShipId}</p>`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
return `
|
return `
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export interface StationSnapshot {
|
|||||||
localPosition: Vector3Dto;
|
localPosition: Vector3Dto;
|
||||||
color: string;
|
color: string;
|
||||||
dockedShips: number;
|
dockedShips: number;
|
||||||
|
dockingPads: number;
|
||||||
energyStored: number;
|
energyStored: number;
|
||||||
inventory: InventoryEntry[];
|
inventory: InventoryEntry[];
|
||||||
factionId: string;
|
factionId: string;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
"yPlane": 4,
|
"yPlane": 4,
|
||||||
"arrivalThreshold": 16,
|
"arrivalThreshold": 16,
|
||||||
"miningRate": 28,
|
"miningRate": 10,
|
||||||
|
"miningCycleSeconds": 10,
|
||||||
"transferRate": 56,
|
"transferRate": 56,
|
||||||
"dockingDuration": 1.2,
|
"dockingDuration": 1.2,
|
||||||
|
"undockingDuration": 1.2,
|
||||||
"undockDistance": 42,
|
"undockDistance": 42,
|
||||||
"energy": {
|
"energy": {
|
||||||
"idleDrain": 0.7,
|
"idleDrain": 0.7,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"bulk-liquid": 600,
|
"bulk-liquid": 600,
|
||||||
"bulk-gas": 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",
|
"id": "trade-hub",
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"radius": 24,
|
"radius": 24,
|
||||||
"dockingCapacity": 3,
|
"dockingCapacity": 3,
|
||||||
"storage": { "bulk-solid": 2000, "manufactured": 1000, "bulk-liquid": 400, "bulk-gas": 400 },
|
"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",
|
"id": "farm-ring",
|
||||||
|
|||||||
@@ -47,6 +47,12 @@
|
|||||||
"storage": "bulk-gas",
|
"storage": "bulk-gas",
|
||||||
"summary": "Compressed gas reserves for future chemical and fuel chains."
|
"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",
|
"id": "water",
|
||||||
"label": "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",
|
"category": "mining",
|
||||||
"summary": "Articulated mining head for shipborne extraction."
|
"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",
|
"id": "gun-turret",
|
||||||
"label": "Gun Turret",
|
"label": "Gun Turret",
|
||||||
@@ -65,6 +71,12 @@
|
|||||||
"category": "dock",
|
"category": "dock",
|
||||||
"summary": "Docking collar and transfer arms."
|
"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",
|
"id": "carrier-bay",
|
||||||
"label": "Carrier Bay",
|
"label": "Carrier Bay",
|
||||||
@@ -77,6 +89,12 @@
|
|||||||
"category": "refinery",
|
"category": "refinery",
|
||||||
"summary": "Ore cracking and metal separation."
|
"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",
|
"id": "turret-grid",
|
||||||
"label": "Turret Grid",
|
"label": "Turret Grid",
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
],
|
],
|
||||||
"shipFormations": [
|
"shipFormations": [
|
||||||
{ "shipId": "constructor", "count": 1, "center": [45, 0, 20], "systemId": "helios" },
|
{ "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": [],
|
"patrolRoutes": [],
|
||||||
"miningDefaults": {
|
"miningDefaults": {
|
||||||
|
|||||||
@@ -111,5 +111,22 @@
|
|||||||
"size": 6,
|
"size": 6,
|
||||||
"maxHealth": 150,
|
"maxHealth": 150,
|
||||||
"modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "mining-turret", "bulk-bay", "docking-clamps"]
|
"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