Refactor ship control layers and update docs
This commit is contained in:
130
NEXT-STEPS.md
Normal file
130
NEXT-STEPS.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# Next Steps
|
||||||
|
|
||||||
|
## Economic Growth
|
||||||
|
|
||||||
|
The current economy already supports:
|
||||||
|
|
||||||
|
1. mining ore
|
||||||
|
2. hauling to refining
|
||||||
|
3. refining / fabricating goods
|
||||||
|
4. spending those goods on ships and outposts
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## Concrete Implementation Order
|
||||||
|
|
||||||
|
1. Add faction production heuristics based on current economy and losses.
|
||||||
|
2. Make pirate target selection explicitly prefer economic targets.
|
||||||
|
3. Surface faction stocks, throughput, and build priorities in the HUD/debug views.
|
||||||
|
4. Expand the order/behavior set with higher-value RTS actions like `hold-here`, `attack`, and `defend-area`.
|
||||||
|
5. Break [src/game/GameApp.ts](/home/jbourdon/repos/space-game/src/game/GameApp.ts) into smaller planning / faction / combat / logistics modules.
|
||||||
|
|
||||||
|
## Migration To .NET
|
||||||
|
|
||||||
|
If the long-term goal is multiplayer, scale, persistence, or server authority, a .NET migration is a sensible next architectural track.
|
||||||
|
|
||||||
|
Recommended direction:
|
||||||
|
|
||||||
|
- keep the current Vite / Three.js client for rendering and input
|
||||||
|
- move simulation authority into a .NET backend
|
||||||
|
- treat the browser client as a renderer + command UI
|
||||||
|
|
||||||
|
Suggested migration phases:
|
||||||
|
|
||||||
|
1. Define a shared simulation contract.
|
||||||
|
- ship state snapshots
|
||||||
|
- orders
|
||||||
|
- behaviors
|
||||||
|
- assignments
|
||||||
|
- combat / economy events
|
||||||
|
|
||||||
|
2. Extract the pure simulation model from the Three.js runtime.
|
||||||
|
- separate rendering state from simulation state
|
||||||
|
- remove direct scene dependencies from game logic
|
||||||
|
|
||||||
|
3. Rebuild the simulation core in .NET.
|
||||||
|
- `Order`
|
||||||
|
- `DefaultBehavior`
|
||||||
|
- `Assignment`
|
||||||
|
- `ControllerTask`
|
||||||
|
- faction economy
|
||||||
|
- combat resolution
|
||||||
|
|
||||||
|
4. Add server-driven ticking.
|
||||||
|
- authoritative world step on the server
|
||||||
|
- deterministic or near-deterministic update model
|
||||||
|
- event stream / snapshot replication to clients
|
||||||
|
|
||||||
|
5. Add persistence and multiplayer infrastructure.
|
||||||
|
- saves
|
||||||
|
- world seeds
|
||||||
|
- reconnect support
|
||||||
|
- eventually player ownership / fog / permissions
|
||||||
|
|
||||||
|
Suggested .NET stack:
|
||||||
|
|
||||||
|
- ASP.NET Core for API / realtime transport
|
||||||
|
- SignalR or custom websocket layer for simulation updates
|
||||||
|
- PostgreSQL for persistence
|
||||||
|
- background hosted service for world ticks
|
||||||
|
|
||||||
|
Suggested immediate prep before migration:
|
||||||
|
|
||||||
|
- isolate simulation data structures from rendering objects
|
||||||
|
- isolate faction AI from UI code
|
||||||
|
- isolate travel / docking / mining / combat systems into separate modules
|
||||||
|
- make event emission explicit and serializable
|
||||||
|
|
||||||
|
The key rule for the migration is:
|
||||||
|
|
||||||
|
- do not port Three.js-shaped code into .NET
|
||||||
|
- first separate the simulation from rendering in TypeScript
|
||||||
|
- then move the pure simulation into .NET cleanly
|
||||||
359
SESSION.md
359
SESSION.md
@@ -1,196 +1,239 @@
|
|||||||
# Session Summary
|
# Session Summary
|
||||||
|
|
||||||
## Project State
|
## Current State
|
||||||
|
|
||||||
This repository now contains a playable Three.js/Vite autonomous space-sim prototype that has moved away from a player-command RTS testbed and toward a game-master / observer simulation.
|
The project is a Three.js/Vite space simulation with:
|
||||||
|
|
||||||
The codebase is still TypeScript + Three.js on Vite, with authored catalogs under `src/game/data/`, but the runtime now centers on:
|
- autonomous ships
|
||||||
|
- orbital travel
|
||||||
|
- docking and undocking
|
||||||
|
- mining and refinery delivery
|
||||||
|
- refining / fabrication
|
||||||
|
- faction growth through ship and outpost production
|
||||||
|
- observer-focused debugging tools
|
||||||
|
|
||||||
- procedural universe generation
|
The active runtime model now follows the intended layered architecture more closely:
|
||||||
- autonomous faction behavior
|
|
||||||
- direct per-ship faction control
|
|
||||||
- economic production loops
|
|
||||||
- pirate harassment
|
|
||||||
- strategic system control
|
|
||||||
- observer-oriented HUD and camera controls
|
|
||||||
|
|
||||||
## Current Prototype
|
- `order`
|
||||||
|
- `defaultBehavior`
|
||||||
|
- `assignment`
|
||||||
|
- `controllerTask`
|
||||||
|
- `state`
|
||||||
|
|
||||||
The current build includes:
|
The previous `captainGoal` layer has been removed.
|
||||||
|
|
||||||
- a generated universe with a few dozen systems
|
## Ship Runtime Model
|
||||||
- 4 empire factions inspired by EVE-style sovereign powers
|
|
||||||
- multiple pirate factions that raid empire space
|
|
||||||
- rich central systems that factions contest for control
|
|
||||||
- faction-owned stations, ships, inventories, and combat stats
|
|
||||||
- autonomous shipbuilding and limited outpost growth
|
|
||||||
- observer controls for camera orbit, pan, focus, and inspection
|
|
||||||
|
|
||||||
## Major Gameplay / Sim Systems
|
Ships now carry:
|
||||||
|
|
||||||
### Universe Generation
|
- `order`
|
||||||
|
- direct one-shot instruction such as `move-to`, `mine-this`, `dock-at`
|
||||||
|
- `defaultBehavior`
|
||||||
|
- standing automation such as `auto-mine`, `patrol`, `escort-assigned`, `idle`
|
||||||
|
- `assignment`
|
||||||
|
- contextual ownership / doctrine such as `unassigned`, `commander-subordinate`, `station-based`, `mining-group`
|
||||||
|
- `controllerTask`
|
||||||
|
- immediate executable task such as `travel`, `dock`, `extract`, `unload`, `follow`, `undock`
|
||||||
|
- `state`
|
||||||
|
- physical ship state such as `spooling-warp`, `warping`, `arriving`, `docking`, `docked`, `undocking`, `transferring`
|
||||||
|
|
||||||
- Startup no longer uses the fixed two-system authored sandbox.
|
Current precedence is:
|
||||||
- `src/game/world/universeGenerator.ts` now generates:
|
|
||||||
- empire capitals
|
|
||||||
- empire mining systems
|
|
||||||
- pirate base systems
|
|
||||||
- central high-value systems
|
|
||||||
- frontier filler systems
|
|
||||||
- The generated scenario also assigns:
|
|
||||||
- faction definitions
|
|
||||||
- initial faction-owned stations
|
|
||||||
- initial ship formations
|
|
||||||
- central system IDs
|
|
||||||
|
|
||||||
### Factions
|
1. `order`
|
||||||
|
2. `defaultBehavior`
|
||||||
|
3. assignment-derived fallback behavior
|
||||||
|
4. idle fallback
|
||||||
|
|
||||||
- Runtime faction state now exists in `src/game/types.ts`.
|
The main loop in [src/game/GameApp.ts](/home/jbourdon/repos/space-game/src/game/GameApp.ts) is now:
|
||||||
- Factions track:
|
|
||||||
- credits
|
|
||||||
- ore mined
|
|
||||||
- goods produced
|
|
||||||
- ships built
|
|
||||||
- stations built
|
|
||||||
- ships lost
|
|
||||||
- enemy ships destroyed
|
|
||||||
- raids completed
|
|
||||||
- stolen cargo
|
|
||||||
- owned systems
|
|
||||||
- Empire factions and pirate factions are distinct runtime kinds.
|
|
||||||
|
|
||||||
### High-Level AI / Delegation
|
- `refreshControlLayers()`
|
||||||
|
- `planControllerTask()`
|
||||||
|
- `updateControllerTask()`
|
||||||
|
- `advanceControlState()`
|
||||||
|
|
||||||
- Faction AI now acts at a strategic level and issues direct orders to ships.
|
## Travel Model
|
||||||
- Empire AI chooses high-level goals such as:
|
|
||||||
- secure home and mining space
|
|
||||||
- contest central systems
|
|
||||||
- send miners to resource systems
|
|
||||||
- send military ships to rally and patrol targets
|
|
||||||
- Pirate AI chooses raid targets and moves military ships into hostile space.
|
|
||||||
- The fleet / wing layer has been removed from both simulation and UI.
|
|
||||||
|
|
||||||
### Economy / Production
|
Travel is destination-driven and orbital-centric.
|
||||||
|
|
||||||
- Mining, refining, and fabrication still run through recipe-driven station logic.
|
- same-system travel:
|
||||||
- Faction-owned inventories are effectively pooled across faction stations for recipe consumption.
|
- `spooling-warp -> warping -> arriving`
|
||||||
- Factions can build new ships when enough goods exist.
|
- inter-system travel:
|
||||||
- Empires can build limited defense outposts in central systems they control.
|
- `spooling-ftl -> ftl -> arriving`
|
||||||
|
- arrival anchors the ship to the destination orbital when appropriate
|
||||||
|
|
||||||
### Combat / Control
|
Destination ownership lives in the `controllerTask`.
|
||||||
|
|
||||||
- Ships and relevant stations now have combat stats:
|
Examples:
|
||||||
- health
|
|
||||||
- damage
|
|
||||||
- range
|
|
||||||
- cooldown
|
|
||||||
- Combat is lightweight and proximity-based.
|
|
||||||
- Central systems track control progress and controlling faction.
|
|
||||||
- Pirate ships can steal cargo from vulnerable civilian ships.
|
|
||||||
|
|
||||||
## Starting State
|
- `travel(destination)`
|
||||||
|
- `dock(host, bay)`
|
||||||
|
- `extract(node)`
|
||||||
|
- `unload(station)`
|
||||||
|
- `undock(host)`
|
||||||
|
|
||||||
- Empires now start very small for easier debugging and growth observation.
|
## Mining / Delivery / Refining
|
||||||
- Each empire currently starts with:
|
|
||||||
- 1 miner
|
|
||||||
- 1 manufactory
|
|
||||||
- 1 refinery
|
|
||||||
- Each pirate faction currently starts with:
|
|
||||||
- 1 frigate
|
|
||||||
- 1 trade hub
|
|
||||||
- This is a bootstrap-oriented setup: factions mine first, then try to grow from minimal infrastructure.
|
|
||||||
|
|
||||||
## UI / UX State
|
Current industrial loop:
|
||||||
|
|
||||||
### Observer HUD
|
1. miner travels to node
|
||||||
|
2. miner extracts ore
|
||||||
|
3. miner travels to refinery
|
||||||
|
4. miner docks
|
||||||
|
5. miner unloads over time
|
||||||
|
6. miner undocks
|
||||||
|
7. loop repeats
|
||||||
|
|
||||||
- The old summary panel is gone.
|
Important details:
|
||||||
- The old bottom RTS command bar has been removed.
|
|
||||||
- The bottom HUD is now a selection dock that shows:
|
|
||||||
- selection title
|
|
||||||
- status line
|
|
||||||
- horizontally scrolling cards for selected entities
|
|
||||||
- fallback observer details when nothing specific is selected
|
|
||||||
- A dedicated `Debug` window now contains the `New Universe` button.
|
|
||||||
|
|
||||||
### Selection / Inspection
|
- `mine-this` is a one-shot order and currently completes when cargo is full
|
||||||
|
- `auto-mine` is persistent behavior and includes its own internal phase state
|
||||||
|
- unloading is time-based through `transferRate` in [src/game/data/balance.json](/home/jbourdon/repos/space-game/src/game/data/balance.json)
|
||||||
|
- unload state is now `transferring`
|
||||||
|
- unload completion emits `<unloaded>`
|
||||||
|
|
||||||
- Selection is no longer limited to ships and stations.
|
Refineries and fabricators feed faction production.
|
||||||
- It is now possible to select:
|
|
||||||
- systems
|
|
||||||
- planets
|
|
||||||
- ships
|
|
||||||
- stations
|
|
||||||
- Double-click centers / focuses the clicked target.
|
|
||||||
- Multiple ship selections render as horizontal cards in the bottom dock.
|
|
||||||
|
|
||||||
### Windows
|
The faction economy now uses fabricated goods to:
|
||||||
|
|
||||||
- Generic draggable / resizable app windows still exist.
|
- build new ships
|
||||||
- Main windows currently in use:
|
- build defense outposts in valuable systems
|
||||||
- `Ships`
|
|
||||||
- `Debug`
|
|
||||||
- The `Ships` window:
|
|
||||||
- lists ships grouped by faction
|
|
||||||
- selects a ship on click
|
|
||||||
- focuses a ship on double click
|
|
||||||
- focuses a faction home system when clicking that faction header
|
|
||||||
|
|
||||||
### Strategic Rendering
|
Current production behavior lives in:
|
||||||
|
|
||||||
- Strategic overlay and minimap infrastructure still exist.
|
- [src/game/GameApp.ts](/home/jbourdon/repos/space-game/src/game/GameApp.ts)
|
||||||
- The minimap canvas is still created for renderer use, but it is no longer shown in the visible HUD.
|
- `tryBuildShipForFaction()`
|
||||||
- Fleet link overlays and fleet counters were removed along with the fleet system.
|
- `tryBuildOutpostForFaction()`
|
||||||
|
|
||||||
## Controls
|
## Faction Growth Loop
|
||||||
|
|
||||||
- `Left Click`: inspect / select systems, planets, ships, or stations
|
The active empire growth loop is:
|
||||||
- `Shift + Left Click`: add ships to multi-selection
|
|
||||||
- `Ctrl/Cmd + Left Click`: toggle ships in multi-selection
|
|
||||||
- `Left Drag`: marquee-select multiple visible ships
|
|
||||||
- `Double Click`: center / focus the clicked target
|
|
||||||
- `Middle Drag`: orbit camera
|
|
||||||
- `Shift + Middle Drag`: pan camera
|
|
||||||
- `Mouse Wheel` or `-` / `=`: zoom
|
|
||||||
- `W A S D`: pan camera using the same motion as `Shift + Middle Drag`
|
|
||||||
- `Q / E`: rotate camera
|
|
||||||
- `F`: focus current selection
|
|
||||||
- `G`: toggle ships window
|
|
||||||
- `Tab`: jump camera between systems
|
|
||||||
|
|
||||||
## Technical Notes
|
1. mine ore
|
||||||
|
2. refine / fabricate goods
|
||||||
|
3. spend goods on ships
|
||||||
|
4. spend goods on military outposts
|
||||||
|
5. project power into central / contested systems
|
||||||
|
|
||||||
- Main runtime remains concentrated in `src/game/GameApp.ts`
|
This means the simulation is no longer missing a use for refined goods.
|
||||||
- World construction and entity instancing:
|
|
||||||
- `src/game/world/worldFactory.ts`
|
|
||||||
- Procedural universe generation:
|
|
||||||
- `src/game/world/universeGenerator.ts`
|
|
||||||
- Selection state:
|
|
||||||
- `src/game/state/selectionManager.ts`
|
|
||||||
- HUD / presentation:
|
|
||||||
- `src/game/ui/hud.ts`
|
|
||||||
- `src/game/ui/presenters.ts`
|
|
||||||
- `src/game/ui/strategicRenderer.ts`
|
|
||||||
- Production build is currently passing with `npm run build`
|
|
||||||
|
|
||||||
## Known Limitations / Caveats
|
What is still missing is stronger strategic prioritization, for example:
|
||||||
|
|
||||||
- `GameApp.ts` is still carrying too much simulation responsibility.
|
- when to build more miners vs escorts vs warships
|
||||||
- Faction AI is improved, but still fairly heuristic and not yet a deep planning system.
|
- how to react to throughput shortages
|
||||||
- Combat is lightweight and does not yet model formations, threat evaluation, or target priorities in a sophisticated way.
|
- how to react to pirate pressure
|
||||||
- Economic logistics are still abstracted heavily.
|
|
||||||
- Ship construction is recipe-gated but still simplified.
|
|
||||||
- Stations consume pooled faction stock rather than explicit transport delivery chains.
|
|
||||||
- Bootstrap progression is still constrained by the current station recipe / stock model.
|
|
||||||
- The ships window is useful for inspection, but the overall UI is still only partially refit for observer mode.
|
|
||||||
- There is still no persistence layer for window layouts, saves, or generated universe seeds.
|
|
||||||
|
|
||||||
## Suggested Next Steps
|
## Pirates / Threats
|
||||||
|
|
||||||
- Extract faction strategy into a dedicated AI / planning module
|
Pirates already exist as an active faction and can raid / fight.
|
||||||
- Separate economic simulation from UI and rendering concerns
|
|
||||||
- Improve transport logistics so goods physically move through faction supply chains
|
Current pirate support includes:
|
||||||
- Add explicit shipyard construction queues and faction production priorities
|
|
||||||
- Rework bootstrap progression so factions can genuinely grow from near-zero infrastructure
|
- pirate faction command logic
|
||||||
- Add system-level threat, ownership, and economy views for game-master inspection
|
- hostile target selection
|
||||||
- Add save/load support for generated universes and long-running simulations
|
- ship combat and destruction
|
||||||
|
|
||||||
|
What is still underdeveloped:
|
||||||
|
|
||||||
|
- explicit preference for miners, haulers, and refinery traffic
|
||||||
|
- clearer harassment behavior around resource chains
|
||||||
|
|
||||||
|
## Debug History
|
||||||
|
|
||||||
|
The debug window is focused on the selected ship and includes:
|
||||||
|
|
||||||
|
- `order`
|
||||||
|
- `defaultBehavior`
|
||||||
|
- `assignment`
|
||||||
|
- `controllerTask`
|
||||||
|
- `state`
|
||||||
|
- task target
|
||||||
|
- anchor
|
||||||
|
|
||||||
|
History is event-oriented plus explicit state lines.
|
||||||
|
|
||||||
|
Current notation includes:
|
||||||
|
|
||||||
|
- controller commands:
|
||||||
|
- `[travel]`, `[dock]`, `[unload]`, `[undock]`
|
||||||
|
- state snapshots:
|
||||||
|
- `state=move-to:.../travel-to-node [travel]/(warping)`
|
||||||
|
- events:
|
||||||
|
- `<arrived ...>`
|
||||||
|
- `<docked>`
|
||||||
|
- `<unloaded>`
|
||||||
|
- `<undocked>`
|
||||||
|
- `<cargo-full>`
|
||||||
|
- `<cargo-empty>`
|
||||||
|
- `<order ...>`
|
||||||
|
- `<default-behavior ...>`
|
||||||
|
- `<assignment ...>`
|
||||||
|
- `<docking-clearance ...>`
|
||||||
|
- `<docking-bay ...>`
|
||||||
|
- `<anchor ...>`
|
||||||
|
|
||||||
|
History remains HTML-escaped before rendering and same-tick changes are still batched.
|
||||||
|
|
||||||
|
Copy-to-clipboard includes:
|
||||||
|
|
||||||
|
- current live summary block
|
||||||
|
- event history
|
||||||
|
|
||||||
|
## Selection / HUD
|
||||||
|
|
||||||
|
The HUD currently supports selecting:
|
||||||
|
|
||||||
|
- ships
|
||||||
|
- stations
|
||||||
|
- systems
|
||||||
|
- planets
|
||||||
|
- asteroid field nodes
|
||||||
|
|
||||||
|
Notable UI status:
|
||||||
|
|
||||||
|
- ship cards show cargo and current layered control summary
|
||||||
|
- station cards show ore stored and refined stock
|
||||||
|
- Fleet and Debug window toggle buttons exist
|
||||||
|
- debug history is scrollable and copyable
|
||||||
|
|
||||||
|
## Important Recent Changes
|
||||||
|
|
||||||
|
- removed the old `captainGoal` layer
|
||||||
|
- planner now derives `controllerTask` directly from `order` and `defaultBehavior`
|
||||||
|
- moved mining / patrol progress state into `order` and `defaultBehavior`
|
||||||
|
- updated debug / selection UI to show the active layered model
|
||||||
|
- removed confirmed dead code found by strict TypeScript unused checks
|
||||||
|
|
||||||
|
## Current Known Limitations
|
||||||
|
|
||||||
|
- [src/game/GameApp.ts](/home/jbourdon/repos/space-game/src/game/GameApp.ts) is still too large and owns too much simulation responsibility
|
||||||
|
- order types are still narrow
|
||||||
|
- currently focused on `move-to`, `mine-this`, `dock-at`
|
||||||
|
- default behavior set is still narrow
|
||||||
|
- currently focused on `idle`, `auto-mine`, `patrol`, `escort-assigned`
|
||||||
|
- pirate harassment exists but is not yet economically targeted enough
|
||||||
|
- faction production logic is timer-driven and only lightly reactive
|
||||||
|
- no persistence for saves, seeds, or layouts
|
||||||
|
|
||||||
|
## Important Files
|
||||||
|
|
||||||
|
- [src/game/GameApp.ts](/home/jbourdon/repos/space-game/src/game/GameApp.ts)
|
||||||
|
- main simulation loop
|
||||||
|
- layered planning
|
||||||
|
- travel, docking, mining, unloading, faction growth, combat, debug history
|
||||||
|
- [src/game/types.ts](/home/jbourdon/repos/space-game/src/game/types.ts)
|
||||||
|
- `order` / `defaultBehavior` / `assignment` / `controllerTask` / `state` model
|
||||||
|
- [src/game/world/worldFactory.ts](/home/jbourdon/repos/space-game/src/game/world/worldFactory.ts)
|
||||||
|
- ship and station instancing
|
||||||
|
- [src/game/ui/presenters.ts](/home/jbourdon/repos/space-game/src/game/ui/presenters.ts)
|
||||||
|
- selection cards
|
||||||
|
- station cards
|
||||||
|
- debug history markup
|
||||||
|
- [src/game/data/balance.json](/home/jbourdon/repos/space-game/src/game/data/balance.json)
|
||||||
|
- travel, docking, transfer rates
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Validation passing at the end of this session:
|
||||||
|
|
||||||
|
- `npx tsc --noEmit --noUnusedLocals --noUnusedParameters`
|
||||||
|
- `npm run build`
|
||||||
|
|||||||
543
STATES.md
Normal file
543
STATES.md
Normal file
@@ -0,0 +1,543 @@
|
|||||||
|
# States, Orders, Behaviours, Assignments
|
||||||
|
|
||||||
|
This document defines the intended gameplay model for the project: a multiplayer X4-inspired RTS with strong automation and strong direct control.
|
||||||
|
|
||||||
|
It is no longer purely aspirational. The current runtime now uses the same top-level control layers described here:
|
||||||
|
|
||||||
|
1. `order`
|
||||||
|
2. `defaultBehavior`
|
||||||
|
3. `assignment`
|
||||||
|
4. `controllerTask`
|
||||||
|
5. `state`
|
||||||
|
|
||||||
|
What is still incomplete is scope, not structure.
|
||||||
|
|
||||||
|
## Design Goals
|
||||||
|
|
||||||
|
The game should support both:
|
||||||
|
|
||||||
|
- RTS-style direct control: `move-to`, `mine-this`, `hold-here`, `dock-there`, `attack-this`
|
||||||
|
- X4-style automation: `auto-mine`, `patrol`, `escort`, `trade`, `defend-area`
|
||||||
|
|
||||||
|
The intended player experience is:
|
||||||
|
|
||||||
|
- you can micro units at any time
|
||||||
|
- automation resumes when direct intervention is over
|
||||||
|
- fleets, stations, carriers, and sectors can provide reusable command structure
|
||||||
|
|
||||||
|
## Control Layers
|
||||||
|
|
||||||
|
Each ship should be modeled with five layers:
|
||||||
|
|
||||||
|
1. `order`
|
||||||
|
2. `defaultBehavior`
|
||||||
|
3. `assignment`
|
||||||
|
4. `controllerTask`
|
||||||
|
5. `state`
|
||||||
|
|
||||||
|
### `order`
|
||||||
|
|
||||||
|
A direct player-issued instruction with highest priority.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
|
||||||
|
- one-shot unless explicitly marked persistent
|
||||||
|
- can preempt automation
|
||||||
|
- may complete, fail, cancel, or timeout
|
||||||
|
- after resolution, ship falls back to `defaultBehavior`
|
||||||
|
|
||||||
|
Current active examples:
|
||||||
|
|
||||||
|
- `move-to(destination)`
|
||||||
|
- `mine-this(node, refinery)`
|
||||||
|
- `dock-at(target)`
|
||||||
|
|
||||||
|
### `defaultBehavior`
|
||||||
|
|
||||||
|
Persistent automation the ship should run when no direct order is active.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
|
||||||
|
- long-lived
|
||||||
|
- self-replanning
|
||||||
|
- normally loops or maintains a standing objective
|
||||||
|
- resumes automatically after one-shot order resolution
|
||||||
|
|
||||||
|
Current active examples:
|
||||||
|
|
||||||
|
- `idle`
|
||||||
|
- `auto-mine(area, refinery)`
|
||||||
|
- `patrol(route)`
|
||||||
|
- `escort-assigned`
|
||||||
|
|
||||||
|
### `assignment`
|
||||||
|
|
||||||
|
The command relationship the ship belongs to.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- assigned to fleet
|
||||||
|
- assigned to carrier
|
||||||
|
- assigned to station
|
||||||
|
- assigned to sector command
|
||||||
|
- assigned to mining group
|
||||||
|
|
||||||
|
Assignment does not necessarily tell the ship what exact task to do every second. It provides authority, context, operating area, logistics ownership, and fallback doctrine.
|
||||||
|
|
||||||
|
Current active examples:
|
||||||
|
|
||||||
|
- `unassigned`
|
||||||
|
- `commander-subordinate(commanderId, role)`
|
||||||
|
- `station-based(stationId, role)`
|
||||||
|
- `mining-group(controllerId)`
|
||||||
|
|
||||||
|
### `controllerTask`
|
||||||
|
|
||||||
|
The immediate executable action.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- travel
|
||||||
|
- dock
|
||||||
|
- undock
|
||||||
|
- mine/extract
|
||||||
|
- unload
|
||||||
|
- follow
|
||||||
|
- hold-position
|
||||||
|
|
||||||
|
Current active examples:
|
||||||
|
|
||||||
|
- `idle`
|
||||||
|
- `travel(destination, threshold)`
|
||||||
|
- `dock(host, bay)`
|
||||||
|
- `undock(host)`
|
||||||
|
- `extract(node)`
|
||||||
|
- `unload(target)`
|
||||||
|
- `follow(target, offset)`
|
||||||
|
|
||||||
|
### `state`
|
||||||
|
|
||||||
|
The physical/runtime state of the unit.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- idle
|
||||||
|
- spooling-warp
|
||||||
|
- warping
|
||||||
|
- docking
|
||||||
|
- docked
|
||||||
|
- mining
|
||||||
|
- attacking
|
||||||
|
- holding
|
||||||
|
|
||||||
|
## Precedence Rules
|
||||||
|
|
||||||
|
Control precedence should be:
|
||||||
|
|
||||||
|
1. `order`
|
||||||
|
2. `defaultBehavior`
|
||||||
|
3. `assignment doctrine`
|
||||||
|
4. safety/failsafe logic
|
||||||
|
|
||||||
|
Interpretation:
|
||||||
|
|
||||||
|
- if a one-shot order exists, it drives planning
|
||||||
|
- if no order exists, the ship executes its default behavior
|
||||||
|
- if no explicit default behavior exists, assignment may supply one
|
||||||
|
- if none of the above apply, ship uses idle/safe fallback
|
||||||
|
|
||||||
|
This is the active planner model in the codebase.
|
||||||
|
|
||||||
|
## Order Lifecycle
|
||||||
|
|
||||||
|
One-shot orders are the RTS-facing layer.
|
||||||
|
|
||||||
|
Each order should move through:
|
||||||
|
|
||||||
|
1. `queued`
|
||||||
|
2. `accepted`
|
||||||
|
3. `planning`
|
||||||
|
4. `executing`
|
||||||
|
5. one of `completed`, `failed`, `cancelled`, `blocked`
|
||||||
|
|
||||||
|
Recommended behavior:
|
||||||
|
|
||||||
|
- `completed`: clear order and resume `defaultBehavior`
|
||||||
|
- `cancelled`: clear order immediately and resume `defaultBehavior`
|
||||||
|
- `failed`: clear order or retry depending on order policy
|
||||||
|
- `blocked`: wait, reroute, or escalate depending on order policy
|
||||||
|
|
||||||
|
Current implementation notes:
|
||||||
|
|
||||||
|
- direct orders already move through `queued`, `accepted`, `planning`, `executing`
|
||||||
|
- `completed`, `failed`, and `blocked` are already represented
|
||||||
|
- full queued-order chains do not exist yet
|
||||||
|
|
||||||
|
## Command Categories
|
||||||
|
|
||||||
|
### One-shot direct orders
|
||||||
|
|
||||||
|
These are the core RTS commands.
|
||||||
|
|
||||||
|
| Order | Intent | Status |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `move-to(destination)` | go to a position or object | active |
|
||||||
|
| `mine-this(node)` | mine this specific node | active |
|
||||||
|
| `dock-at(target)` | dock at target | active |
|
||||||
|
| `undock` | leave current host | not first-class yet |
|
||||||
|
| `hold-here(position)` | stay at current or assigned point | not implemented |
|
||||||
|
| `attack(target)` | attack a specific unit/object | not implemented as direct order |
|
||||||
|
| `intercept(target)` | chase and engage moving target | not implemented |
|
||||||
|
| `escort(target)` | follow and defend a specific target | not implemented as direct one-shot order |
|
||||||
|
| `transfer-cargo(target, item, amount)` | move cargo between entities | not implemented |
|
||||||
|
| `build-here(site)` | construct at location | not implemented |
|
||||||
|
| `salvage-this(target)` | recover specific wreck/resource | not implemented |
|
||||||
|
| `retreat-to(destination)` | disengage and move to safety | not implemented |
|
||||||
|
|
||||||
|
### Persistent default behaviours
|
||||||
|
|
||||||
|
These are the automation layer.
|
||||||
|
|
||||||
|
| Behavior | Intent | Status |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `idle` | do nothing beyond safety drift / local maintenance | active |
|
||||||
|
| `auto-mine(area, refinery)` | find nodes, mine, deliver, repeat | active |
|
||||||
|
| `patrol(route or area)` | move across checkpoints and react to threats | active |
|
||||||
|
| `escort-assigned` | stay with assigned commander or protected ship | active |
|
||||||
|
| `defend-area(area)` | hold area and engage threats inside rules | not implemented |
|
||||||
|
| `trade(route or policy)` | acquire, move, and deliver goods automatically | not implemented |
|
||||||
|
| `resupply(host or fleet)` | fetch and deliver fuel/ammo/cargo | not implemented |
|
||||||
|
| `ferry-units(host)` | shuttle units between hosts or waypoints | not implemented |
|
||||||
|
| `harvest(area)` | collect local resources automatically | not implemented |
|
||||||
|
| `hold-area(center, radius)` | remain in zone with low autonomy | not implemented |
|
||||||
|
|
||||||
|
### Assignments
|
||||||
|
|
||||||
|
Assignments define command context.
|
||||||
|
|
||||||
|
| Assignment | Meaning | Status |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `unassigned` | independent unit | active |
|
||||||
|
| `fleet-member(fleetId, role)` | unit belongs to a fleet structure | not implemented |
|
||||||
|
| `commander-subordinate(commanderId, role)` | subordinate follows commander doctrine | active |
|
||||||
|
| `carrier-wing(carrierId, role)` | launch/recover/escort under carrier | not implemented as distinct type |
|
||||||
|
| `station-based(stationId, role)` | station-owned logistics or defense unit | active |
|
||||||
|
| `sector-command(sectorId, role)` | unit operates inside a sector mandate | not implemented |
|
||||||
|
| `mining-group(controllerId)` | industrial unit tied to a mining controller | active |
|
||||||
|
|
||||||
|
Assignments can supply default behavior if the unit has none explicitly set.
|
||||||
|
|
||||||
|
## Planner Model
|
||||||
|
|
||||||
|
Planning works like this:
|
||||||
|
|
||||||
|
1. if `order` exists, derive `controllerTask` from it
|
||||||
|
2. else if `defaultBehavior` exists, derive `controllerTask` from it
|
||||||
|
3. else if `assignment` implies doctrine, derive `defaultBehavior`
|
||||||
|
4. else run idle fallback
|
||||||
|
|
||||||
|
This preserves RTS responsiveness while keeping X4-style automation.
|
||||||
|
|
||||||
|
Current implementation detail:
|
||||||
|
|
||||||
|
- mining and patrol progress are stored directly on `order` or `defaultBehavior`
|
||||||
|
- there is no separate high-level “captain” intent layer anymore
|
||||||
|
|
||||||
|
## Physical States
|
||||||
|
|
||||||
|
These are the intended physical states for ships.
|
||||||
|
|
||||||
|
| State | Meaning | Status |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `idle` | no active movement or operation | active |
|
||||||
|
| `holding` | maintaining current position/formation/anchor | reserved |
|
||||||
|
| `spooling-warp` | charging local fast-travel | active |
|
||||||
|
| `warping` | high-speed in-system travel | active |
|
||||||
|
| `spooling-ftl` | charging inter-system travel | active |
|
||||||
|
| `ftl` | inter-system travel | active |
|
||||||
|
| `arriving` | final approach after warp/ftl | active |
|
||||||
|
| `approaching` | standard approach to target | reserved |
|
||||||
|
| `docking-approach` | approach to docking lane or bay | active |
|
||||||
|
| `docking` | docking procedure in progress | active |
|
||||||
|
| `docked` | physically docked | active |
|
||||||
|
| `undocking` | undocking procedure in progress | active |
|
||||||
|
| `mining-approach` | aligning to resource node | active |
|
||||||
|
| `mining` | active extraction | active |
|
||||||
|
| `transferring` | moving cargo, fuel, or units | active |
|
||||||
|
| `building` | performing construction | reserved |
|
||||||
|
| `repairing` | performing repair actions | reserved |
|
||||||
|
| `patrolling` | movement as part of patrol | active |
|
||||||
|
| `escorting` | movement as part of escort | active |
|
||||||
|
| `attacking` | active weapons engagement | reserved |
|
||||||
|
| `retreating` | disengaging toward safety | reserved |
|
||||||
|
| `disabled` | cannot act | reserved |
|
||||||
|
| `destroyed` | removed from play | active outcome |
|
||||||
|
|
||||||
|
## Controller Tasks
|
||||||
|
|
||||||
|
These are the low-level tasks the planner can issue.
|
||||||
|
|
||||||
|
| Task | Purpose | Status |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `idle` | no task | active |
|
||||||
|
| `travel(destination, threshold)` | move to destination | active |
|
||||||
|
| `hold(position, radius)` | maintain a point | not implemented |
|
||||||
|
| `dock(host, bay)` | dock at host | active |
|
||||||
|
| `undock(host)` | undock from host | active |
|
||||||
|
| `extract(node)` | mine/extract resource | active |
|
||||||
|
| `unload(target, item, amount?)` | move cargo out | active |
|
||||||
|
| `load(target, item, amount?)` | move cargo in | not implemented |
|
||||||
|
| `follow(target, offset)` | maintain formation | active |
|
||||||
|
| `attack(target)` | engage target | not implemented as controller task |
|
||||||
|
| `intercept(target)` | chase moving target | not implemented |
|
||||||
|
| `orbit(target, radius)` | remain in orbit around target | not implemented |
|
||||||
|
| `build(site)` | perform construction | not implemented |
|
||||||
|
| `repair(target)` | perform repair | not implemented |
|
||||||
|
| `scan(target)` | gather intel | not implemented |
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
Events should be explicit and usable by both gameplay and debug tools.
|
||||||
|
|
||||||
|
### Order events
|
||||||
|
|
||||||
|
| Event | Meaning | Status |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `order-issued` | player or AI created an order | partially implicit |
|
||||||
|
| `order-accepted` | unit accepted order | represented in order status |
|
||||||
|
| `order-rejected` | unit cannot accept order | not explicit yet |
|
||||||
|
| `order-started` | execution began | represented in order status |
|
||||||
|
| `order-blocked` | execution cannot proceed for now | represented in order status |
|
||||||
|
| `order-completed` | order finished successfully | represented and logged |
|
||||||
|
| `order-failed` | order failed | represented and logged |
|
||||||
|
| `order-cancelled` | order was manually cleared or overridden | reserved |
|
||||||
|
|
||||||
|
### Behavior events
|
||||||
|
|
||||||
|
| Event | Meaning | Status |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `behavior-set` | default behavior assigned | represented in debug history |
|
||||||
|
| `behavior-cleared` | default behavior removed | represented in debug history |
|
||||||
|
| `behavior-resumed` | resumed after order completion | implicit |
|
||||||
|
| `behavior-paused` | temporarily suspended | implicit |
|
||||||
|
| `behavior-phase-changed` | automation phase updated | represented in debug history |
|
||||||
|
|
||||||
|
### Assignment events
|
||||||
|
|
||||||
|
| Event | Meaning | Status |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `assignment-set` | unit assigned | represented in debug history |
|
||||||
|
| `assignment-cleared` | assignment removed | represented in debug history |
|
||||||
|
| `assignment-role-changed` | subordinate role changed | partially represented |
|
||||||
|
| `commander-lost` | assigned leader unavailable | implicit |
|
||||||
|
|
||||||
|
### Controller/runtime events
|
||||||
|
|
||||||
|
| Event | Meaning | Status |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `arrived` | destination reached | active |
|
||||||
|
| `docking-clearance-granted` | docking permission accepted | active via history |
|
||||||
|
| `docking-clearance-denied` | docking permission denied | active via history |
|
||||||
|
| `docking-begin` | docking procedure started | active |
|
||||||
|
| `docked` | docking complete | active |
|
||||||
|
| `undocked` | undocking complete | active |
|
||||||
|
| `cargo-full` | cargo reached capacity | active via history |
|
||||||
|
| `cargo-empty` | cargo emptied | active via history |
|
||||||
|
| `target-lost` | attack/escort target unavailable | not explicit yet |
|
||||||
|
| `hostile-detected` | threat found | not explicit yet |
|
||||||
|
| `resource-depleted` | node exhausted | not explicit yet |
|
||||||
|
| `damaged` | ship took damage | not explicit yet |
|
||||||
|
| `destroyed` | ship destroyed | active outcome |
|
||||||
|
|
||||||
|
## Transition Rules
|
||||||
|
|
||||||
|
### Core fallback rule
|
||||||
|
|
||||||
|
When a direct order resolves:
|
||||||
|
|
||||||
|
- clear `order`
|
||||||
|
- emit terminal order event
|
||||||
|
- resume `defaultBehavior`
|
||||||
|
- if no explicit `defaultBehavior`, ask `assignment` for fallback
|
||||||
|
- if none exists, become `idle`
|
||||||
|
|
||||||
|
This is the main rule that keeps automation and RTS control compatible.
|
||||||
|
|
||||||
|
### Example: direct move order
|
||||||
|
|
||||||
|
1. player issues `move-to`
|
||||||
|
2. ship order becomes `move-to(destination)`
|
||||||
|
3. planner emits `travel(destination)`
|
||||||
|
4. ship moves through travel states
|
||||||
|
5. on `arrived`, order completes
|
||||||
|
6. ship resumes previous default behavior, such as `patrol` or `auto-mine`
|
||||||
|
|
||||||
|
### Example: direct mine-this order
|
||||||
|
|
||||||
|
1. player issues `mine-this(node)`
|
||||||
|
2. order overrides `auto-mine`
|
||||||
|
3. planner travels to node
|
||||||
|
4. planner extracts until cargo is full
|
||||||
|
5. order completes
|
||||||
|
6. unit returns to `auto-mine`
|
||||||
|
|
||||||
|
Current limitation:
|
||||||
|
|
||||||
|
- the one-shot `mine-this` order does not yet auto-deliver once before completing
|
||||||
|
|
||||||
|
### Example: assignment-driven escort
|
||||||
|
|
||||||
|
1. fighter assigned to `commander-subordinate(commanderId, escort)`
|
||||||
|
2. assignment implies default behavior `escort-assigned`
|
||||||
|
3. on no direct order, fighter follows commander
|
||||||
|
4. when direct order ends, fighter resumes escort automatically
|
||||||
|
|
||||||
|
## Blocking and Waiting
|
||||||
|
|
||||||
|
Blocked execution should be explicit, not hidden inside `idle`.
|
||||||
|
|
||||||
|
Common blocked reasons:
|
||||||
|
|
||||||
|
- no docking bay free
|
||||||
|
- no path to destination
|
||||||
|
- no cargo space
|
||||||
|
- no valid mining target
|
||||||
|
- no ammo/fuel/energy
|
||||||
|
- target out of command scope
|
||||||
|
- waiting for commander or carrier
|
||||||
|
|
||||||
|
Recommended blocked substate or status:
|
||||||
|
|
||||||
|
- `blocked(waiting-for-bay)`
|
||||||
|
- `blocked(waiting-for-target)`
|
||||||
|
- `blocked(waiting-for-resource)`
|
||||||
|
- `blocked(waiting-for-order-scope)`
|
||||||
|
|
||||||
|
Current implementation:
|
||||||
|
|
||||||
|
- blocked status exists on `order`
|
||||||
|
- blocked reason metadata does not yet exist
|
||||||
|
|
||||||
|
## Queue and Override Rules
|
||||||
|
|
||||||
|
For RTS feel, orders should support:
|
||||||
|
|
||||||
|
- immediate replace
|
||||||
|
- optional shift-queue
|
||||||
|
- cancel current order
|
||||||
|
- clear all queued orders
|
||||||
|
|
||||||
|
Recommended semantics:
|
||||||
|
|
||||||
|
- normal click command replaces current direct order
|
||||||
|
- shift-command appends to order queue
|
||||||
|
- automation only runs when order queue is empty
|
||||||
|
|
||||||
|
Current implementation:
|
||||||
|
|
||||||
|
- immediate replace behavior exists
|
||||||
|
- shift queues do not exist yet
|
||||||
|
|
||||||
|
## Suggested Data Shape
|
||||||
|
|
||||||
|
Current runtime shape is effectively:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type ShipMind = {
|
||||||
|
order?: ShipOrder;
|
||||||
|
defaultBehavior: ShipBehavior;
|
||||||
|
assignment: ShipAssignment;
|
||||||
|
controllerTask: ControllerTask;
|
||||||
|
state: UnitState;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Future expansion can add:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type ShipMindExtensions = {
|
||||||
|
queuedOrders?: ShipOrder[];
|
||||||
|
blockedReason?: BlockedReason;
|
||||||
|
stance?: CombatStance;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mapping To Current Project
|
||||||
|
|
||||||
|
Current runtime concepts map like this:
|
||||||
|
|
||||||
|
| Runtime concept | Current implementation |
|
||||||
|
| --- | --- |
|
||||||
|
| direct move | one-shot `order = move-to` |
|
||||||
|
| direct mining | one-shot `order = mine-this` |
|
||||||
|
| direct docking | one-shot `order = dock-at` |
|
||||||
|
| automation mining | `defaultBehavior = auto-mine` |
|
||||||
|
| automation patrol | `defaultBehavior = patrol` |
|
||||||
|
| assignment escort | `assignment = commander-subordinate` plus `defaultBehavior = escort-assigned` |
|
||||||
|
| execution | `controllerTask` |
|
||||||
|
| physical movement / interaction | `state` |
|
||||||
|
|
||||||
|
## Recommended Next Scope
|
||||||
|
|
||||||
|
To move the game forward without overbuilding, the highest-value next steps are:
|
||||||
|
|
||||||
|
### Direct orders
|
||||||
|
|
||||||
|
- `hold-here`
|
||||||
|
- `attack`
|
||||||
|
- `escort` as a first-class direct order
|
||||||
|
|
||||||
|
### Default behaviors
|
||||||
|
|
||||||
|
- `defend-area`
|
||||||
|
- `trade`
|
||||||
|
- `resupply`
|
||||||
|
|
||||||
|
### Assignments
|
||||||
|
|
||||||
|
- `fleet-member`
|
||||||
|
- `carrier-wing`
|
||||||
|
- `sector-command`
|
||||||
|
|
||||||
|
### Runtime support
|
||||||
|
|
||||||
|
- blocked reason handling
|
||||||
|
- optional order queue
|
||||||
|
- better event emission
|
||||||
|
- more explicit threat / target events
|
||||||
|
|
||||||
|
### Faction strategy
|
||||||
|
|
||||||
|
- reactive shipbuilding priorities
|
||||||
|
- better pirate harassment of industrial targets
|
||||||
|
- clearer economic pressure and throughput visibility
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
This model should not force:
|
||||||
|
|
||||||
|
- full economic AI before direct control feels good
|
||||||
|
- full hierarchy simulation before ship orders work
|
||||||
|
- perfect X4 parity
|
||||||
|
|
||||||
|
The target remains a practical hybrid:
|
||||||
|
|
||||||
|
- faster and clearer than a pure sim
|
||||||
|
- deeper and more autonomous than a pure RTS
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The active design is now:
|
||||||
|
|
||||||
|
- one-shot orders for direct RTS control
|
||||||
|
- default behaviors for persistent automation
|
||||||
|
- assignments for organizational structure
|
||||||
|
- controller tasks for immediate execution
|
||||||
|
- physical states for movement and interaction
|
||||||
|
|
||||||
|
That structure supports the gameplay goal cleanly:
|
||||||
|
|
||||||
|
- players can micro ships directly
|
||||||
|
- ships remain useful when not microed
|
||||||
|
- factions can scale through automation
|
||||||
|
- the game can feel like an RTS without losing simulation depth
|
||||||
1753
src/game/GameApp.ts
1753
src/game/GameApp.ts
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
|||||||
"yPlane": 4,
|
"yPlane": 4,
|
||||||
"arrivalThreshold": 16,
|
"arrivalThreshold": 16,
|
||||||
"miningRate": 28,
|
"miningRate": 28,
|
||||||
|
"transferRate": 56,
|
||||||
"dockingDuration": 1.2,
|
"dockingDuration": 1.2,
|
||||||
"undockDistance": 42,
|
"undockDistance": 42,
|
||||||
"energy": {
|
"energy": {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import type { PlanetInstance, ShipInstance, SolarSystemInstance, StationInstance } from "../types";
|
import type { PlanetInstance, ResourceNode, ShipInstance, SolarSystemInstance, StationInstance } from "../types";
|
||||||
|
|
||||||
export class SelectionManager {
|
export class SelectionManager {
|
||||||
private shipSelection: ShipInstance[] = [];
|
private shipSelection: ShipInstance[] = [];
|
||||||
private stationSelection?: StationInstance;
|
private stationSelection?: StationInstance;
|
||||||
private systemSelection?: SolarSystemInstance;
|
private systemSelection?: SolarSystemInstance;
|
||||||
private planetSelection?: PlanetInstance;
|
private planetSelection?: PlanetInstance;
|
||||||
|
private nodeSelection?: ResourceNode;
|
||||||
|
|
||||||
getShips() {
|
getShips() {
|
||||||
return this.shipSelection;
|
return this.shipSelection;
|
||||||
@@ -23,6 +24,10 @@ export class SelectionManager {
|
|||||||
return this.planetSelection;
|
return this.planetSelection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNode() {
|
||||||
|
return this.nodeSelection;
|
||||||
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.shipSelection.forEach((ship) => this.setShipVisual(ship, false));
|
this.shipSelection.forEach((ship) => this.setShipVisual(ship, false));
|
||||||
this.shipSelection = [];
|
this.shipSelection = [];
|
||||||
@@ -38,6 +43,10 @@ export class SelectionManager {
|
|||||||
this.setPlanetVisual(this.planetSelection, false);
|
this.setPlanetVisual(this.planetSelection, false);
|
||||||
this.planetSelection = undefined;
|
this.planetSelection = undefined;
|
||||||
}
|
}
|
||||||
|
if (this.nodeSelection) {
|
||||||
|
this.setNodeVisual(this.nodeSelection, false);
|
||||||
|
this.nodeSelection = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceShips(ships: ShipInstance[]) {
|
replaceShips(ships: ShipInstance[]) {
|
||||||
@@ -72,6 +81,15 @@ export class SelectionManager {
|
|||||||
this.setPlanetVisual(planet, true);
|
this.setPlanetVisual(planet, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setNode(node?: ResourceNode) {
|
||||||
|
this.clear();
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.nodeSelection = node;
|
||||||
|
this.setNodeVisual(node, true);
|
||||||
|
}
|
||||||
|
|
||||||
addShip(ship: ShipInstance) {
|
addShip(ship: ShipInstance) {
|
||||||
if (this.shipSelection.includes(ship)) {
|
if (this.shipSelection.includes(ship)) {
|
||||||
return;
|
return;
|
||||||
@@ -88,6 +106,10 @@ export class SelectionManager {
|
|||||||
this.setPlanetVisual(this.planetSelection, false);
|
this.setPlanetVisual(this.planetSelection, false);
|
||||||
this.planetSelection = undefined;
|
this.planetSelection = undefined;
|
||||||
}
|
}
|
||||||
|
if (this.nodeSelection) {
|
||||||
|
this.setNodeVisual(this.nodeSelection, false);
|
||||||
|
this.nodeSelection = undefined;
|
||||||
|
}
|
||||||
this.shipSelection.push(ship);
|
this.shipSelection.push(ship);
|
||||||
this.setShipVisual(ship, true);
|
this.setShipVisual(ship, true);
|
||||||
}
|
}
|
||||||
@@ -133,4 +155,10 @@ export class SelectionManager {
|
|||||||
(planet.selectionRing.material as THREE.MeshBasicMaterial).opacity = selected ? 0.95 : 0;
|
(planet.selectionRing.material as THREE.MeshBasicMaterial).opacity = selected ? 0.95 : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setNodeVisual(node: ResourceNode, selected: boolean) {
|
||||||
|
if (node.selectionRing) {
|
||||||
|
(node.selectionRing.material as THREE.MeshBasicMaterial).opacity = selected ? 0.95 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,16 @@ export type ConstructibleCategory =
|
|||||||
| "gate";
|
| "gate";
|
||||||
export type UnitState =
|
export type UnitState =
|
||||||
| "idle"
|
| "idle"
|
||||||
| "moving"
|
| "holding"
|
||||||
| "leaving-gravity-well"
|
| "spooling-warp"
|
||||||
| "spooling-ftl"
|
| "spooling-ftl"
|
||||||
|
| "ftl"
|
||||||
| "warping"
|
| "warping"
|
||||||
| "arriving"
|
| "arriving"
|
||||||
|
| "approaching"
|
||||||
| "mining-approach"
|
| "mining-approach"
|
||||||
| "mining"
|
| "mining"
|
||||||
| "delivering"
|
| "transferring"
|
||||||
| "docking-approach"
|
| "docking-approach"
|
||||||
| "docking"
|
| "docking"
|
||||||
| "docked"
|
| "docked"
|
||||||
@@ -28,7 +30,8 @@ export type UnitState =
|
|||||||
| "patrolling"
|
| "patrolling"
|
||||||
| "escorting"
|
| "escorting"
|
||||||
| "forming";
|
| "forming";
|
||||||
export type UnitOrderKind = "idle" | "move" | "transfer" | "mine" | "patrol" | "escort" | "dock";
|
export type ControllerTaskKind = "idle" | "travel" | "dock" | "extract" | "follow" | "undock";
|
||||||
|
export type OrderStatus = "queued" | "accepted" | "planning" | "executing" | "completed" | "failed" | "cancelled" | "blocked";
|
||||||
export type ItemStorageKind = "bulk-solid" | "bulk-liquid" | "bulk-gas" | "container" | "manufactured";
|
export type ItemStorageKind = "bulk-solid" | "bulk-liquid" | "bulk-gas" | "container" | "manufactured";
|
||||||
export type ModuleCategory =
|
export type ModuleCategory =
|
||||||
| "bridge"
|
| "bridge"
|
||||||
@@ -43,6 +46,7 @@ export type ModuleCategory =
|
|||||||
| "habitat"
|
| "habitat"
|
||||||
| "production";
|
| "production";
|
||||||
export type ViewLevel = "local" | "solar" | "universe";
|
export type ViewLevel = "local" | "solar" | "universe";
|
||||||
|
export type TravelDestinationKind = "system" | "planet" | "station" | "resource-node" | "ship" | "orbit";
|
||||||
|
|
||||||
export interface ModuleDefinition {
|
export interface ModuleDefinition {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -219,6 +223,7 @@ export interface GameBalance {
|
|||||||
yPlane: number;
|
yPlane: number;
|
||||||
arrivalThreshold: number;
|
arrivalThreshold: number;
|
||||||
miningRate: number;
|
miningRate: number;
|
||||||
|
transferRate: number;
|
||||||
dockingDuration: number;
|
dockingDuration: number;
|
||||||
undockDistance: number;
|
undockDistance: number;
|
||||||
energy: {
|
energy: {
|
||||||
@@ -233,20 +238,37 @@ export interface GameBalance {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UnitOrder =
|
export type ShipOrder =
|
||||||
|
| { kind: "move-to"; status: OrderStatus; destination: TravelDestination }
|
||||||
|
| { kind: "mine-this"; status: OrderStatus; nodeId: string; refineryId: string; phase: "travel-to-node" | "extract" }
|
||||||
|
| { kind: "dock-at"; status: OrderStatus; hostKind: "station" | "ship"; hostId: string };
|
||||||
|
|
||||||
|
export type DefaultBehavior =
|
||||||
| { kind: "idle" }
|
| { kind: "idle" }
|
||||||
| { kind: "move"; destination: THREE.Vector3; systemId: string }
|
|
||||||
| {
|
| {
|
||||||
kind: "transfer";
|
kind: "auto-mine";
|
||||||
destination: THREE.Vector3;
|
areaSystemId: string;
|
||||||
destinationSystemId: string;
|
refineryId: string;
|
||||||
exitPoint: THREE.Vector3;
|
nodeId?: string;
|
||||||
arrivalPoint: THREE.Vector3;
|
phase: "travel-to-node" | "extract" | "travel-to-station" | "dock" | "unload" | "undock";
|
||||||
}
|
}
|
||||||
| { kind: "mine"; nodeId: string; refineryId: string; phase: "to-node" | "mining" | "to-refinery" | "transfer" }
|
| { kind: "patrol"; points: TravelDestination[]; systemId: string; index: number }
|
||||||
| { kind: "patrol"; points: THREE.Vector3[]; systemId: string; index: number }
|
| { kind: "escort-assigned"; offset: THREE.Vector3 };
|
||||||
| { kind: "escort"; targetShipId: string; offset: THREE.Vector3 }
|
|
||||||
| { kind: "dock"; carrierShipId: string };
|
export type Assignment =
|
||||||
|
| { kind: "unassigned" }
|
||||||
|
| { kind: "commander-subordinate"; commanderId: string; role: string }
|
||||||
|
| { kind: "station-based"; stationId: string; role: string }
|
||||||
|
| { kind: "mining-group"; controllerId: string };
|
||||||
|
|
||||||
|
export type ControllerTask =
|
||||||
|
| { kind: "idle" }
|
||||||
|
| { kind: "travel"; destination: TravelDestination; threshold: number; suppliedPlan?: TravelPlan }
|
||||||
|
| { kind: "dock"; hostKind: "station" | "ship"; hostId: string; portIndex: number }
|
||||||
|
| { kind: "extract"; nodeId: string }
|
||||||
|
| { kind: "unload"; stationId: string }
|
||||||
|
| { kind: "follow"; targetShipId: string; offset: THREE.Vector3; threshold: number }
|
||||||
|
| { kind: "undock"; hostKind: "station" | "ship"; hostId: string };
|
||||||
|
|
||||||
export interface InventoryState {
|
export interface InventoryState {
|
||||||
"bulk-solid": number;
|
"bulk-solid": number;
|
||||||
@@ -256,11 +278,19 @@ export interface InventoryState {
|
|||||||
manufactured: number;
|
manufactured: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TravelDestination {
|
||||||
|
kind: TravelDestinationKind;
|
||||||
|
systemId: string;
|
||||||
|
label: string;
|
||||||
|
position: THREE.Vector3;
|
||||||
|
orbitalAnchor: THREE.Vector3;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TravelPlan {
|
export interface TravelPlan {
|
||||||
destination: THREE.Vector3;
|
destination: TravelDestination;
|
||||||
destinationSystemId: string;
|
|
||||||
exitPoint: THREE.Vector3;
|
|
||||||
arrivalPoint: THREE.Vector3;
|
arrivalPoint: THREE.Vector3;
|
||||||
|
interSystem: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShipInstance {
|
export interface ShipInstance {
|
||||||
@@ -273,11 +303,17 @@ export interface ShipInstance {
|
|||||||
ring: THREE.Mesh;
|
ring: THREE.Mesh;
|
||||||
systemId: string;
|
systemId: string;
|
||||||
state: UnitState;
|
state: UnitState;
|
||||||
order: UnitOrder;
|
order?: ShipOrder;
|
||||||
|
defaultBehavior: DefaultBehavior;
|
||||||
|
assignment: Assignment;
|
||||||
|
controllerTask: ControllerTask;
|
||||||
inventory: InventoryState;
|
inventory: InventoryState;
|
||||||
cargoItemId?: string;
|
cargoItemId?: string;
|
||||||
actionTimer: number;
|
actionTimer: number;
|
||||||
travelPlan?: TravelPlan;
|
travelPlan?: TravelPlan;
|
||||||
|
landedDestination?: TravelDestination;
|
||||||
|
landedOffset: THREE.Vector3;
|
||||||
|
dockingClearanceStatus?: string;
|
||||||
dockedStationId?: string;
|
dockedStationId?: string;
|
||||||
dockedCarrierId?: string;
|
dockedCarrierId?: string;
|
||||||
dockingPortIndex?: number;
|
dockingPortIndex?: number;
|
||||||
@@ -347,6 +383,7 @@ export interface ResourceNode {
|
|||||||
systemId: string;
|
systemId: string;
|
||||||
position: THREE.Vector3;
|
position: THREE.Vector3;
|
||||||
mesh: THREE.Object3D;
|
mesh: THREE.Object3D;
|
||||||
|
selectionRing?: THREE.Mesh;
|
||||||
oreRemaining: number;
|
oreRemaining: number;
|
||||||
maxOre: number;
|
maxOre: number;
|
||||||
itemId: string;
|
itemId: string;
|
||||||
@@ -372,7 +409,8 @@ export type SelectableTarget =
|
|||||||
| { kind: "ship"; ship: ShipInstance }
|
| { kind: "ship"; ship: ShipInstance }
|
||||||
| { kind: "station"; station: StationInstance }
|
| { kind: "station"; station: StationInstance }
|
||||||
| { kind: "system"; system: SolarSystemInstance }
|
| { kind: "system"; system: SolarSystemInstance }
|
||||||
| { kind: "planet"; system: SolarSystemInstance; planet: PlanetInstance };
|
| { kind: "planet"; system: SolarSystemInstance; planet: PlanetInstance }
|
||||||
|
| { kind: "node"; node: ResourceNode };
|
||||||
|
|
||||||
export interface HudElements {
|
export interface HudElements {
|
||||||
details: HTMLDivElement;
|
details: HTMLDivElement;
|
||||||
@@ -390,4 +428,7 @@ export interface HudElements {
|
|||||||
fleetWindowBody: HTMLDivElement;
|
fleetWindowBody: HTMLDivElement;
|
||||||
fleetWindowTitle: HTMLHeadingElement;
|
fleetWindowTitle: HTMLHeadingElement;
|
||||||
debugWindow: HTMLDivElement;
|
debugWindow: HTMLDivElement;
|
||||||
|
debugHistory: HTMLDivElement;
|
||||||
|
debugAutoScrollToggle: HTMLButtonElement;
|
||||||
|
debugCopyHistory: HTMLButtonElement;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ export function createHud(container: HTMLElement, handlers: HudHandlers): HudEle
|
|||||||
<h2 class="selection-title">No Selection</h2>
|
<h2 class="selection-title">No Selection</h2>
|
||||||
<div class="mode"></div>
|
<div class="mode"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="window-launchers">
|
||||||
|
<button type="button" data-window-action="toggle-fleet-command">Fleet</button>
|
||||||
|
<button type="button" data-window-action="toggle-debug">Debug</button>
|
||||||
|
</div>
|
||||||
<div class="selection-strip"></div>
|
<div class="selection-strip"></div>
|
||||||
<div class="content"></div>
|
<div class="content"></div>
|
||||||
</section>
|
</section>
|
||||||
@@ -38,7 +42,10 @@ export function createHud(container: HTMLElement, handlers: HudHandlers): HudEle
|
|||||||
<div class="window-body">
|
<div class="window-body">
|
||||||
<div class="session-actions">
|
<div class="session-actions">
|
||||||
<button type="button" data-window-action="new-universe">New Universe</button>
|
<button type="button" data-window-action="new-universe">New Universe</button>
|
||||||
|
<button type="button" data-window-action="toggle-debug-autoscroll">Pause Scroll</button>
|
||||||
|
<button type="button" data-window-action="copy-debug-history">Copy History</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="debug-history"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="window-resize-handle" aria-hidden="true"></div>
|
<div class="window-resize-handle" aria-hidden="true"></div>
|
||||||
</section>
|
</section>
|
||||||
@@ -89,6 +96,9 @@ export function createHud(container: HTMLElement, handlers: HudHandlers): HudEle
|
|||||||
fleetWindowBody: fleetWindowBody as HTMLDivElement,
|
fleetWindowBody: fleetWindowBody as HTMLDivElement,
|
||||||
fleetWindowTitle: root.querySelector(".fleet-window h2") as HTMLHeadingElement,
|
fleetWindowTitle: root.querySelector(".fleet-window h2") as HTMLHeadingElement,
|
||||||
debugWindow: root.querySelector(".debug-window") as HTMLDivElement,
|
debugWindow: root.querySelector(".debug-window") as HTMLDivElement,
|
||||||
|
debugHistory: root.querySelector(".debug-history") as HTMLDivElement,
|
||||||
|
debugAutoScrollToggle: root.querySelector('[data-window-action="toggle-debug-autoscroll"]') as HTMLButtonElement,
|
||||||
|
debugCopyHistory: root.querySelector('[data-window-action="copy-debug-history"]') as HTMLButtonElement,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { getShipCargoAmount } from "../state/inventory";
|
|||||||
import type {
|
import type {
|
||||||
FactionInstance,
|
FactionInstance,
|
||||||
PlanetInstance,
|
PlanetInstance,
|
||||||
|
ResourceNode,
|
||||||
ShipInstance,
|
ShipInstance,
|
||||||
SolarSystemInstance,
|
SolarSystemInstance,
|
||||||
StationInstance,
|
StationInstance,
|
||||||
@@ -18,7 +19,11 @@ export function getSelectionTitle(
|
|||||||
selectedStation?: StationInstance,
|
selectedStation?: StationInstance,
|
||||||
selectedSystem?: SolarSystemInstance,
|
selectedSystem?: SolarSystemInstance,
|
||||||
selectedPlanet?: PlanetInstance,
|
selectedPlanet?: PlanetInstance,
|
||||||
|
selectedNode?: ResourceNode,
|
||||||
) {
|
) {
|
||||||
|
if (selectedNode) {
|
||||||
|
return `Asteroid Field ${selectedNode.id}`;
|
||||||
|
}
|
||||||
if (selectedPlanet) {
|
if (selectedPlanet) {
|
||||||
return selectedPlanet.definition.label;
|
return selectedPlanet.definition.label;
|
||||||
}
|
}
|
||||||
@@ -42,7 +47,11 @@ export function getSelectionStripLabels(
|
|||||||
selectedStation?: StationInstance,
|
selectedStation?: StationInstance,
|
||||||
selectedSystem?: SolarSystemInstance,
|
selectedSystem?: SolarSystemInstance,
|
||||||
selectedPlanet?: PlanetInstance,
|
selectedPlanet?: PlanetInstance,
|
||||||
|
selectedNode?: ResourceNode,
|
||||||
) {
|
) {
|
||||||
|
if (selectedNode) {
|
||||||
|
return [`Asteroid Field ${selectedNode.id}`];
|
||||||
|
}
|
||||||
if (selectedPlanet) {
|
if (selectedPlanet) {
|
||||||
return [selectedPlanet.definition.label];
|
return [selectedPlanet.definition.label];
|
||||||
}
|
}
|
||||||
@@ -63,7 +72,15 @@ export function getSelectionCardsMarkup(
|
|||||||
selectedStation: StationInstance | undefined,
|
selectedStation: StationInstance | undefined,
|
||||||
selectedSystem: SolarSystemInstance | undefined,
|
selectedSystem: SolarSystemInstance | undefined,
|
||||||
selectedPlanet: PlanetInstance | undefined,
|
selectedPlanet: PlanetInstance | undefined,
|
||||||
|
selectedNode?: ResourceNode,
|
||||||
) {
|
) {
|
||||||
|
if (selectedNode) {
|
||||||
|
return renderCard(`Asteroid Field ${selectedNode.id}`, [
|
||||||
|
selectedNode.systemId,
|
||||||
|
getItemLabel(selectedNode.itemId),
|
||||||
|
`Ore ${Math.round(selectedNode.oreRemaining)}/${selectedNode.maxOre}`,
|
||||||
|
]);
|
||||||
|
}
|
||||||
if (selectedPlanet) {
|
if (selectedPlanet) {
|
||||||
return renderCard(
|
return renderCard(
|
||||||
selectedPlanet.definition.label,
|
selectedPlanet.definition.label,
|
||||||
@@ -92,6 +109,8 @@ export function getSelectionCardsMarkup(
|
|||||||
[
|
[
|
||||||
selectedStation.factionId,
|
selectedStation.factionId,
|
||||||
selectedStation.definition.category,
|
selectedStation.definition.category,
|
||||||
|
`Ore ${Math.round(selectedStation.oreStored)}`,
|
||||||
|
`Refined ${Math.round(selectedStation.refinedStock)}`,
|
||||||
`HP ${Math.round(selectedStation.health)}/${selectedStation.maxHealth}`,
|
`HP ${Math.round(selectedStation.health)}/${selectedStation.maxHealth}`,
|
||||||
`Dock ${selectedStation.dockedShipIds.size}/${selectedStation.definition.dockingCapacity}`,
|
`Dock ${selectedStation.dockedShipIds.size}/${selectedStation.definition.dockingCapacity}`,
|
||||||
],
|
],
|
||||||
@@ -105,7 +124,8 @@ export function getSelectionCardsMarkup(
|
|||||||
renderCard(ship.definition.label, [
|
renderCard(ship.definition.label, [
|
||||||
ship.factionId,
|
ship.factionId,
|
||||||
ship.state,
|
ship.state,
|
||||||
ship.order.kind,
|
`${getOrderSummary(ship)} / ${getBehaviorSummary(ship)} / ${ship.controllerTask.kind}`,
|
||||||
|
`Cargo ${Math.round(getShipCargoAmount(ship))}/${ship.definition.cargoCapacity || 0} ${getItemLabel(ship.cargoItemId)}`,
|
||||||
`HP ${Math.round(ship.health)}/${ship.maxHealth}`,
|
`HP ${Math.round(ship.health)}/${ship.maxHealth}`,
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
@@ -117,11 +137,15 @@ export function getSelectionDetails(
|
|||||||
selectedStation: StationInstance | undefined,
|
selectedStation: StationInstance | undefined,
|
||||||
selectedSystem: SolarSystemInstance | undefined,
|
selectedSystem: SolarSystemInstance | undefined,
|
||||||
selectedPlanet: PlanetInstance | undefined,
|
selectedPlanet: PlanetInstance | undefined,
|
||||||
|
selectedNode: ResourceNode | undefined,
|
||||||
systems: SolarSystemInstance[],
|
systems: SolarSystemInstance[],
|
||||||
viewLevel: ViewLevel,
|
viewLevel: ViewLevel,
|
||||||
ships: ShipInstance[],
|
ships: ShipInstance[],
|
||||||
factions: FactionInstance[],
|
factions: FactionInstance[],
|
||||||
) {
|
) {
|
||||||
|
if (selectedNode) {
|
||||||
|
return `Asteroid Field ${selectedNode.id} • ${selectedNode.systemId}\nResource: ${getItemLabel(selectedNode.itemId)}\nOre Remaining: ${Math.round(selectedNode.oreRemaining)}/${selectedNode.maxOre}`;
|
||||||
|
}
|
||||||
if (selectedPlanet) {
|
if (selectedPlanet) {
|
||||||
return `${selectedPlanet.definition.label} • ${selectedPlanet.systemId}\nOrbit Radius: ${Math.round(selectedPlanet.definition.orbitRadius)}\nSize: ${selectedPlanet.definition.size}\nOrbit Speed: ${selectedPlanet.definition.orbitSpeed.toFixed(2)}\nTilt: ${selectedPlanet.definition.tilt.toFixed(2)}\nRing: ${selectedPlanet.definition.hasRing ? "Yes" : "No"}`;
|
return `${selectedPlanet.definition.label} • ${selectedPlanet.systemId}\nOrbit Radius: ${Math.round(selectedPlanet.definition.orbitRadius)}\nSize: ${selectedPlanet.definition.size}\nOrbit Speed: ${selectedPlanet.definition.orbitSpeed.toFixed(2)}\nTilt: ${selectedPlanet.definition.tilt.toFixed(2)}\nRing: ${selectedPlanet.definition.hasRing ? "Yes" : "No"}`;
|
||||||
}
|
}
|
||||||
@@ -154,16 +178,20 @@ export function getSelectionDetails(
|
|||||||
ship.definition.dockingCapacity && ship.definition.dockingCapacity > 0
|
ship.definition.dockingCapacity && ship.definition.dockingCapacity > 0
|
||||||
? `\nHangar: ${ship.dockedShipIds.size}/${ship.definition.dockingCapacity} for ${(ship.definition.dockingClasses ?? []).join(", ")}`
|
? `\nHangar: ${ship.dockedShipIds.size}/${ship.definition.dockingCapacity} for ${(ship.definition.dockingClasses ?? []).join(", ")}`
|
||||||
: "";
|
: "";
|
||||||
return `${ship.definition.label} • ${ship.systemId}\nFaction: ${ship.factionId}\nClass: ${ship.definition.shipClass}\nState: ${ship.state}${dockedAt ? ` @ ${dockedAt}` : ""}\nOrder: ${ship.order.kind}\nHealth: ${Math.round(ship.health)}/${ship.maxHealth}\nCargo: ${Math.round(getShipCargoAmount(ship))}/${ship.definition.cargoCapacity || 0} ${getItemLabel(ship.cargoItemId)}\nFuel: ${ship.fuel.toFixed(0)}/${ship.maxFuel}\nEnergy: ${ship.energy.toFixed(0)}/${ship.maxEnergy}\nHold Type: ${ship.definition.cargoKind ?? "none"}${hangarStatus}\nModules: ${ship.definition.modules.map(getModuleLabel).join(", ")}`;
|
const controllerDestination = getControllerTaskDestinationLabel(ship);
|
||||||
|
const destination = controllerDestination
|
||||||
|
? `\nTask Target: ${controllerDestination}`
|
||||||
|
: "";
|
||||||
|
return `${ship.definition.label} • ${ship.systemId}\nFaction: ${ship.factionId}\nClass: ${ship.definition.shipClass}\nState: ${ship.state}${dockedAt ? ` @ ${dockedAt}` : ""}${destination}\nOrder: ${getOrderSummary(ship)}\nDefault: ${getBehaviorSummary(ship)}\nAssignment: ${getAssignmentSummary(ship)}\nController: ${ship.controllerTask.kind}\nHealth: ${Math.round(ship.health)}/${ship.maxHealth}\nCargo: ${Math.round(getShipCargoAmount(ship))}/${ship.definition.cargoCapacity || 0} ${getItemLabel(ship.cargoItemId)}\nFuel: ${ship.fuel.toFixed(0)}/${ship.maxFuel}\nEnergy: ${ship.energy.toFixed(0)}/${ship.maxEnergy}\nHold Type: ${ship.definition.cargoKind ?? "none"}${hangarStatus}\nModules: ${ship.definition.modules.map(getModuleLabel).join(", ")}`;
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function describeStation(station: StationInstance, ships: ShipInstance[]) {
|
export function describeStation(station: StationInstance, ships: ShipInstance[]) {
|
||||||
const miners = ships.filter((ship) => ship.systemId === station.systemId && ship.order.kind === "mine").length;
|
const miners = ships.filter((ship) => ship.systemId === station.systemId && ship.defaultBehavior.kind === "auto-mine").length;
|
||||||
const escorts = ships.filter((ship) => ship.systemId === station.systemId && ship.order.kind === "escort").length;
|
const escorts = ships.filter((ship) => ship.systemId === station.systemId && ship.defaultBehavior.kind === "escort-assigned").length;
|
||||||
const patrols = ships.filter((ship) => ship.systemId === station.systemId && ship.order.kind === "patrol").length;
|
const patrols = ships.filter((ship) => ship.systemId === station.systemId && ship.defaultBehavior.kind === "patrol").length;
|
||||||
const localShips = ships.filter((ship) => ship.systemId === station.systemId).length;
|
const localShips = ships.filter((ship) => ship.systemId === station.systemId).length;
|
||||||
const activeRecipe = station.activeRecipeId
|
const activeRecipe = station.activeRecipeId
|
||||||
? recipeDefinitions.find((recipe) => recipe.id === station.activeRecipeId)
|
? recipeDefinitions.find((recipe) => recipe.id === station.activeRecipeId)
|
||||||
@@ -238,8 +266,75 @@ export function getShipWindowMarkup(ships: ShipInstance[], selection: ShipInstan
|
|||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDebugHistoryMarkup(
|
||||||
|
selectedShip: ShipInstance | undefined,
|
||||||
|
historyByShipId: Map<string, string[]>,
|
||||||
|
) {
|
||||||
|
if (!selectedShip) {
|
||||||
|
return `<div class="debug-history-empty">Select a ship to inspect its history.</div>`;
|
||||||
|
}
|
||||||
|
const entries = historyByShipId.get(selectedShip.id);
|
||||||
|
if (!entries || entries.length === 0) {
|
||||||
|
return `
|
||||||
|
<section class="debug-history-ship" data-selected="true">
|
||||||
|
<h3 class="debug-history-title">${escapeHtml(selectedShip.definition.label)} • ${escapeHtml(selectedShip.id)}</h3>
|
||||||
|
<div class="debug-history-empty">No ship history recorded yet.</div>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
const destination = getControllerTaskDestinationLabel(selectedShip) ?? "none";
|
||||||
|
const anchor = selectedShip.landedDestination
|
||||||
|
? `${selectedShip.landedDestination.label} @ ${selectedShip.landedDestination.systemId}`
|
||||||
|
: "free";
|
||||||
|
return `
|
||||||
|
<section class="debug-history-ship" data-selected="true">
|
||||||
|
<h3 class="debug-history-title">${escapeHtml(selectedShip.definition.label)} • ${escapeHtml(selectedShip.id)}</h3>
|
||||||
|
<div class="debug-history-summary">
|
||||||
|
<div><strong>Order:</strong> ${escapeHtml(getOrderSummary(selectedShip))}</div>
|
||||||
|
<div><strong>Default behavior:</strong> ${escapeHtml(getBehaviorSummary(selectedShip))}</div>
|
||||||
|
<div><strong>Assignment:</strong> ${escapeHtml(getAssignmentSummary(selectedShip))}</div>
|
||||||
|
<div><strong>Controller task:</strong> ${escapeHtml(selectedShip.controllerTask.kind)}</div>
|
||||||
|
<div><strong>Flight state:</strong> ${escapeHtml(selectedShip.state)}</div>
|
||||||
|
<div><strong>Task target:</strong> ${escapeHtml(destination)}</div>
|
||||||
|
<div><strong>Anchor:</strong> ${escapeHtml(anchor)}</div>
|
||||||
|
</div>
|
||||||
|
${entries.map((entry) => `<div class="debug-history-entry">${escapeHtml(entry)}</div>`).join("")}
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function describeShipNode(ship: ShipInstance): string {
|
function describeShipNode(ship: ShipInstance): string {
|
||||||
return `${ship.state} - ${ship.systemId}`;
|
const intent = `${getOrderSummary(ship)} / ${getBehaviorSummary(ship)} / ${ship.controllerTask.kind}`;
|
||||||
|
const controllerDestination = getControllerTaskDestinationLabel(ship);
|
||||||
|
return controllerDestination
|
||||||
|
? `${intent} • ${ship.state} -> ${controllerDestination}`
|
||||||
|
: `${intent} • ${ship.state} - ${ship.systemId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getControllerTaskDestinationLabel(ship: ShipInstance) {
|
||||||
|
const task = ship.controllerTask;
|
||||||
|
if (task.kind === "travel") {
|
||||||
|
return `${task.destination.label} @ ${task.destination.systemId}`;
|
||||||
|
}
|
||||||
|
if (task.kind === "dock" || task.kind === "undock") {
|
||||||
|
return `${task.hostKind}:${task.hostId}`;
|
||||||
|
}
|
||||||
|
if (task.kind === "extract") {
|
||||||
|
return `Asteroid Field ${task.nodeId}`;
|
||||||
|
}
|
||||||
|
if (task.kind === "follow") {
|
||||||
|
return `ship:${task.targetShipId}`;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value: string) {
|
||||||
|
return value
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCard(title: string, lines: string[]) {
|
function renderCard(title: string, lines: string[]) {
|
||||||
@@ -251,6 +346,47 @@ function renderCard(title: string, lines: string[]) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOrderSummary(ship: ShipInstance) {
|
||||||
|
if (!ship.order) {
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
if (ship.order.kind === "move-to") {
|
||||||
|
return `move-to (${ship.order.status})`;
|
||||||
|
}
|
||||||
|
if (ship.order.kind === "mine-this") {
|
||||||
|
return `mine-this:${ship.order.phase} (${ship.order.status})`;
|
||||||
|
}
|
||||||
|
return `dock-at (${ship.order.status})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBehaviorSummary(ship: ShipInstance) {
|
||||||
|
const behavior = ship.defaultBehavior;
|
||||||
|
if (behavior.kind === "auto-mine") {
|
||||||
|
return `auto-mine:${behavior.phase} ${behavior.areaSystemId}`;
|
||||||
|
}
|
||||||
|
if (behavior.kind === "patrol") {
|
||||||
|
return `patrol:${behavior.index + 1} ${behavior.systemId}`;
|
||||||
|
}
|
||||||
|
if (behavior.kind === "escort-assigned") {
|
||||||
|
return "escort-assigned";
|
||||||
|
}
|
||||||
|
return "idle";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAssignmentSummary(ship: ShipInstance) {
|
||||||
|
const assignment = ship.assignment;
|
||||||
|
if (assignment.kind === "commander-subordinate") {
|
||||||
|
return `${assignment.kind} ${assignment.commanderId}`;
|
||||||
|
}
|
||||||
|
if (assignment.kind === "station-based") {
|
||||||
|
return `${assignment.kind} ${assignment.stationId}`;
|
||||||
|
}
|
||||||
|
if (assignment.kind === "mining-group") {
|
||||||
|
return `${assignment.kind} ${assignment.controllerId}`;
|
||||||
|
}
|
||||||
|
return "unassigned";
|
||||||
|
}
|
||||||
|
|
||||||
export function getItemLabel(itemId?: string) {
|
export function getItemLabel(itemId?: string) {
|
||||||
return itemId ? itemDefinitionsById.get(itemId)?.label ?? itemId : "None";
|
return itemId ? itemDefinitionsById.get(itemId)?.label ?? itemId : "None";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ interface RenderOverlayOptions {
|
|||||||
ships: ShipInstance[];
|
ships: ShipInstance[];
|
||||||
selection: ShipInstance[];
|
selection: ShipInstance[];
|
||||||
selectedStation?: StationInstance;
|
selectedStation?: StationInstance;
|
||||||
selectedSystemIndex: number;
|
|
||||||
viewLevel: ViewLevel;
|
viewLevel: ViewLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +96,6 @@ export function drawStrategicOverlay({
|
|||||||
ships,
|
ships,
|
||||||
selection,
|
selection,
|
||||||
selectedStation,
|
selectedStation,
|
||||||
selectedSystemIndex,
|
|
||||||
viewLevel,
|
viewLevel,
|
||||||
}: RenderOverlayOptions) {
|
}: RenderOverlayOptions) {
|
||||||
context.clearRect(0, 0, width, height);
|
context.clearRect(0, 0, width, height);
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ function createSolarSystem(
|
|||||||
return orbitLine;
|
return orbitLine;
|
||||||
});
|
});
|
||||||
|
|
||||||
const asteroidDecorations = createAsteroidField(definition, root, nodes, nextNodeId);
|
const asteroidDecorations = createAsteroidField(definition, root, nodes, nextNodeId, selectableTargets);
|
||||||
const strategicMarker = createStrategicMarker(scene, definition);
|
const strategicMarker = createStrategicMarker(scene, definition);
|
||||||
const system = {
|
const system = {
|
||||||
definition,
|
definition,
|
||||||
@@ -263,6 +263,7 @@ function createAsteroidField(
|
|||||||
root: THREE.Group,
|
root: THREE.Group,
|
||||||
nodes: ResourceNode[],
|
nodes: ResourceNode[],
|
||||||
nextNodeId: () => string,
|
nextNodeId: () => string,
|
||||||
|
selectableTargets?: Map<THREE.Object3D, SelectableTarget>,
|
||||||
) {
|
) {
|
||||||
const rockGeometry = new THREE.IcosahedronGeometry(1, 0);
|
const rockGeometry = new THREE.IcosahedronGeometry(1, 0);
|
||||||
const rockMaterial = new THREE.MeshStandardMaterial({
|
const rockMaterial = new THREE.MeshStandardMaterial({
|
||||||
@@ -309,17 +310,32 @@ function createAsteroidField(
|
|||||||
shard.position.set((Math.random() - 0.5) * 18, (Math.random() - 0.5) * 12, (Math.random() - 0.5) * 18);
|
shard.position.set((Math.random() - 0.5) * 18, (Math.random() - 0.5) * 12, (Math.random() - 0.5) * 18);
|
||||||
cluster.add(shard);
|
cluster.add(shard);
|
||||||
}
|
}
|
||||||
|
const selectionRing = new THREE.Mesh(
|
||||||
|
new THREE.RingGeometry(14, 19, 32),
|
||||||
|
new THREE.MeshBasicMaterial({
|
||||||
|
color: 0xffdd75,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
selectionRing.rotation.x = -Math.PI / 2;
|
||||||
|
selectionRing.position.y = -6;
|
||||||
|
cluster.add(selectionRing);
|
||||||
root.add(cluster);
|
root.add(cluster);
|
||||||
decorations.push(cluster);
|
decorations.push(cluster);
|
||||||
nodes.push({
|
const node = {
|
||||||
id: nextNodeId(),
|
id: nextNodeId(),
|
||||||
systemId: definition.id,
|
systemId: definition.id,
|
||||||
position: cluster.getWorldPosition(new THREE.Vector3()),
|
position: cluster.getWorldPosition(new THREE.Vector3()),
|
||||||
mesh: cluster,
|
mesh: cluster,
|
||||||
|
selectionRing,
|
||||||
oreRemaining: resourceNode.oreAmount,
|
oreRemaining: resourceNode.oreAmount,
|
||||||
maxOre: resourceNode.oreAmount,
|
maxOre: resourceNode.oreAmount,
|
||||||
itemId: resourceNode.itemId,
|
itemId: resourceNode.itemId,
|
||||||
});
|
};
|
||||||
|
nodes.push(node);
|
||||||
|
cluster.traverse((child) => selectableTargets?.set(child, { kind: "node", node }));
|
||||||
});
|
});
|
||||||
|
|
||||||
return decorations;
|
return decorations;
|
||||||
@@ -693,10 +709,14 @@ export function createShipInstance({
|
|||||||
ring,
|
ring,
|
||||||
systemId,
|
systemId,
|
||||||
state: "idle",
|
state: "idle",
|
||||||
order: { kind: "idle" },
|
order: undefined,
|
||||||
|
defaultBehavior: { kind: "idle" },
|
||||||
|
assignment: { kind: "unassigned" },
|
||||||
|
controllerTask: { kind: "idle" },
|
||||||
inventory: createEmptyInventory(),
|
inventory: createEmptyInventory(),
|
||||||
cargoItemId: definition.cargoItemId,
|
cargoItemId: definition.cargoItemId,
|
||||||
actionTimer: 0,
|
actionTimer: 0,
|
||||||
|
dockingClearanceStatus: undefined,
|
||||||
factionId,
|
factionId,
|
||||||
factionColor,
|
factionColor,
|
||||||
health: definition.maxHealth,
|
health: definition.maxHealth,
|
||||||
@@ -711,6 +731,7 @@ export function createShipInstance({
|
|||||||
energy: 260,
|
energy: 260,
|
||||||
maxFuel: 220,
|
maxFuel: 220,
|
||||||
maxEnergy: 260,
|
maxEnergy: 260,
|
||||||
|
landedOffset: new THREE.Vector3(),
|
||||||
idleOrbitRadius: Math.max(120, group.position.length()),
|
idleOrbitRadius: Math.max(120, group.position.length()),
|
||||||
idleOrbitAngle: 0,
|
idleOrbitAngle: 0,
|
||||||
warpFx,
|
warpFx,
|
||||||
|
|||||||
@@ -331,6 +331,65 @@ button:disabled {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.debug-history {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-history-ship {
|
||||||
|
border: 1px solid rgba(126, 212, 255, 0.12);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: rgba(8, 17, 33, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-history-ship[data-selected="true"] {
|
||||||
|
border-color: rgba(255, 191, 105, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-history-title {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-history-summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(126, 212, 255, 0.08);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-history-summary strong {
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-history-entry {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-history-empty {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
.window-resize-handle {
|
.window-resize-handle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
|
|||||||
Reference in New Issue
Block a user