Refactor ship control layers and update docs

This commit is contained in:
2026-03-12 16:44:00 -04:00
parent 7e41274620
commit 0a76c60ab1
12 changed files with 2580 additions and 565 deletions

130
NEXT-STEPS.md Normal file
View 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

View File

@@ -1,196 +1,239 @@
# 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
- autonomous faction behavior
- direct per-ship faction control
- economic production loops
- pirate harassment
- strategic system control
- observer-oriented HUD and camera controls
The active runtime model now follows the intended layered architecture more closely:
## 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
- 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
## Ship Runtime Model
## 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.
- `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
Current precedence is:
### Factions
1. `order`
2. `defaultBehavior`
3. assignment-derived fallback behavior
4. idle fallback
- Runtime faction state now exists in `src/game/types.ts`.
- 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.
The main loop in [src/game/GameApp.ts](/home/jbourdon/repos/space-game/src/game/GameApp.ts) is now:
### High-Level AI / Delegation
- `refreshControlLayers()`
- `planControllerTask()`
- `updateControllerTask()`
- `advanceControlState()`
- Faction AI now acts at a strategic level and issues direct orders to ships.
- 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.
## Travel Model
### Economy / Production
Travel is destination-driven and orbital-centric.
- Mining, refining, and fabrication still run through recipe-driven station logic.
- Faction-owned inventories are effectively pooled across faction stations for recipe consumption.
- Factions can build new ships when enough goods exist.
- Empires can build limited defense outposts in central systems they control.
- same-system travel:
- `spooling-warp -> warping -> arriving`
- inter-system travel:
- `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:
- 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.
Examples:
## Starting State
- `travel(destination)`
- `dock(host, bay)`
- `extract(node)`
- `unload(station)`
- `undock(host)`
- Empires now start very small for easier debugging and growth observation.
- 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.
## Mining / Delivery / Refining
## 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.
- 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.
Important details:
### 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>`
Refineries and fabricators feed faction production.
The faction economy now uses fabricated goods to:
- build new ships
- build defense outposts in valuable systems
Current production behavior lives in:
- [src/game/GameApp.ts](/home/jbourdon/repos/space-game/src/game/GameApp.ts)
- `tryBuildShipForFaction()`
- `tryBuildOutpostForFaction()`
## Faction Growth Loop
The active empire growth loop is:
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
This means the simulation is no longer missing a use for refined goods.
What is still missing is stronger strategic prioritization, for example:
- when to build more miners vs escorts vs warships
- how to react to throughput shortages
- how to react to pirate pressure
## Pirates / Threats
Pirates already exist as an active faction and can raid / fight.
Current pirate support includes:
- pirate faction command logic
- hostile target selection
- 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:
- Selection is no longer limited to ships and stations.
- 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.
- systems
- planets
- asteroid field nodes
### Windows
Notable UI status:
- Generic draggable / resizable app windows still exist.
- Main windows currently in use:
- `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
- 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
### Strategic Rendering
## Important Recent Changes
- Strategic overlay and minimap infrastructure still exist.
- The minimap canvas is still created for renderer use, but it is no longer shown in the visible HUD.
- Fleet link overlays and fleet counters were removed along with the fleet system.
- 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
## Controls
## Current Known Limitations
- `Left Click`: inspect / select systems, planets, ships, or stations
- `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
- [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
## Technical Notes
## Important Files
- Main runtime remains concentrated in `src/game/GameApp.ts`
- 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`
- [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
## Known Limitations / Caveats
## Validation
- `GameApp.ts` is still carrying too much simulation responsibility.
- Faction AI is improved, but still fairly heuristic and not yet a deep planning system.
- Combat is lightweight and does not yet model formations, threat evaluation, or target priorities in a sophisticated way.
- 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.
Validation passing at the end of this session:
## Suggested Next Steps
- Extract faction strategy into a dedicated AI / planning module
- Separate economic simulation from UI and rendering concerns
- Improve transport logistics so goods physically move through faction supply chains
- Add explicit shipyard construction queues and faction production priorities
- Rework bootstrap progression so factions can genuinely grow from near-zero infrastructure
- Add system-level threat, ownership, and economy views for game-master inspection
- Add save/load support for generated universes and long-running simulations
- `npx tsc --noEmit --noUnusedLocals --noUnusedParameters`
- `npm run build`

543
STATES.md Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
"yPlane": 4,
"arrivalThreshold": 16,
"miningRate": 28,
"transferRate": 56,
"dockingDuration": 1.2,
"undockDistance": 42,
"energy": {

View File

@@ -1,11 +1,12 @@
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 {
private shipSelection: ShipInstance[] = [];
private stationSelection?: StationInstance;
private systemSelection?: SolarSystemInstance;
private planetSelection?: PlanetInstance;
private nodeSelection?: ResourceNode;
getShips() {
return this.shipSelection;
@@ -23,6 +24,10 @@ export class SelectionManager {
return this.planetSelection;
}
getNode() {
return this.nodeSelection;
}
clear() {
this.shipSelection.forEach((ship) => this.setShipVisual(ship, false));
this.shipSelection = [];
@@ -38,6 +43,10 @@ export class SelectionManager {
this.setPlanetVisual(this.planetSelection, false);
this.planetSelection = undefined;
}
if (this.nodeSelection) {
this.setNodeVisual(this.nodeSelection, false);
this.nodeSelection = undefined;
}
}
replaceShips(ships: ShipInstance[]) {
@@ -72,6 +81,15 @@ export class SelectionManager {
this.setPlanetVisual(planet, true);
}
setNode(node?: ResourceNode) {
this.clear();
if (!node) {
return;
}
this.nodeSelection = node;
this.setNodeVisual(node, true);
}
addShip(ship: ShipInstance) {
if (this.shipSelection.includes(ship)) {
return;
@@ -88,6 +106,10 @@ export class SelectionManager {
this.setPlanetVisual(this.planetSelection, false);
this.planetSelection = undefined;
}
if (this.nodeSelection) {
this.setNodeVisual(this.nodeSelection, false);
this.nodeSelection = undefined;
}
this.shipSelection.push(ship);
this.setShipVisual(ship, true);
}
@@ -133,4 +155,10 @@ export class SelectionManager {
(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;
}
}
}

View File

@@ -13,14 +13,16 @@ export type ConstructibleCategory =
| "gate";
export type UnitState =
| "idle"
| "moving"
| "leaving-gravity-well"
| "holding"
| "spooling-warp"
| "spooling-ftl"
| "ftl"
| "warping"
| "arriving"
| "approaching"
| "mining-approach"
| "mining"
| "delivering"
| "transferring"
| "docking-approach"
| "docking"
| "docked"
@@ -28,7 +30,8 @@ export type UnitState =
| "patrolling"
| "escorting"
| "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 ModuleCategory =
| "bridge"
@@ -43,6 +46,7 @@ export type ModuleCategory =
| "habitat"
| "production";
export type ViewLevel = "local" | "solar" | "universe";
export type TravelDestinationKind = "system" | "planet" | "station" | "resource-node" | "ship" | "orbit";
export interface ModuleDefinition {
id: string;
@@ -219,6 +223,7 @@ export interface GameBalance {
yPlane: number;
arrivalThreshold: number;
miningRate: number;
transferRate: number;
dockingDuration: number;
undockDistance: number;
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: "move"; destination: THREE.Vector3; systemId: string }
| {
kind: "transfer";
destination: THREE.Vector3;
destinationSystemId: string;
exitPoint: THREE.Vector3;
arrivalPoint: THREE.Vector3;
kind: "auto-mine";
areaSystemId: string;
refineryId: string;
nodeId?: string;
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: THREE.Vector3[]; systemId: string; index: number }
| { kind: "escort"; targetShipId: string; offset: THREE.Vector3 }
| { kind: "dock"; carrierShipId: string };
| { kind: "patrol"; points: TravelDestination[]; systemId: string; index: number }
| { kind: "escort-assigned"; offset: THREE.Vector3 };
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 {
"bulk-solid": number;
@@ -256,11 +278,19 @@ export interface InventoryState {
manufactured: number;
}
export interface TravelDestination {
kind: TravelDestinationKind;
systemId: string;
label: string;
position: THREE.Vector3;
orbitalAnchor: THREE.Vector3;
id?: string;
}
export interface TravelPlan {
destination: THREE.Vector3;
destinationSystemId: string;
exitPoint: THREE.Vector3;
destination: TravelDestination;
arrivalPoint: THREE.Vector3;
interSystem: boolean;
}
export interface ShipInstance {
@@ -273,11 +303,17 @@ export interface ShipInstance {
ring: THREE.Mesh;
systemId: string;
state: UnitState;
order: UnitOrder;
order?: ShipOrder;
defaultBehavior: DefaultBehavior;
assignment: Assignment;
controllerTask: ControllerTask;
inventory: InventoryState;
cargoItemId?: string;
actionTimer: number;
travelPlan?: TravelPlan;
landedDestination?: TravelDestination;
landedOffset: THREE.Vector3;
dockingClearanceStatus?: string;
dockedStationId?: string;
dockedCarrierId?: string;
dockingPortIndex?: number;
@@ -347,6 +383,7 @@ export interface ResourceNode {
systemId: string;
position: THREE.Vector3;
mesh: THREE.Object3D;
selectionRing?: THREE.Mesh;
oreRemaining: number;
maxOre: number;
itemId: string;
@@ -372,7 +409,8 @@ export type SelectableTarget =
| { kind: "ship"; ship: ShipInstance }
| { kind: "station"; station: StationInstance }
| { kind: "system"; system: SolarSystemInstance }
| { kind: "planet"; system: SolarSystemInstance; planet: PlanetInstance };
| { kind: "planet"; system: SolarSystemInstance; planet: PlanetInstance }
| { kind: "node"; node: ResourceNode };
export interface HudElements {
details: HTMLDivElement;
@@ -390,4 +428,7 @@ export interface HudElements {
fleetWindowBody: HTMLDivElement;
fleetWindowTitle: HTMLHeadingElement;
debugWindow: HTMLDivElement;
debugHistory: HTMLDivElement;
debugAutoScrollToggle: HTMLButtonElement;
debugCopyHistory: HTMLButtonElement;
}

View File

@@ -14,6 +14,10 @@ export function createHud(container: HTMLElement, handlers: HudHandlers): HudEle
<h2 class="selection-title">No Selection</h2>
<div class="mode"></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="content"></div>
</section>
@@ -38,7 +42,10 @@ export function createHud(container: HTMLElement, handlers: HudHandlers): HudEle
<div class="window-body">
<div class="session-actions">
<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 class="debug-history"></div>
</div>
<div class="window-resize-handle" aria-hidden="true"></div>
</section>
@@ -89,6 +96,9 @@ export function createHud(container: HTMLElement, handlers: HudHandlers): HudEle
fleetWindowBody: fleetWindowBody as HTMLDivElement,
fleetWindowTitle: root.querySelector(".fleet-window h2") as HTMLHeadingElement,
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,
};
}

View File

@@ -7,6 +7,7 @@ import { getShipCargoAmount } from "../state/inventory";
import type {
FactionInstance,
PlanetInstance,
ResourceNode,
ShipInstance,
SolarSystemInstance,
StationInstance,
@@ -18,7 +19,11 @@ export function getSelectionTitle(
selectedStation?: StationInstance,
selectedSystem?: SolarSystemInstance,
selectedPlanet?: PlanetInstance,
selectedNode?: ResourceNode,
) {
if (selectedNode) {
return `Asteroid Field ${selectedNode.id}`;
}
if (selectedPlanet) {
return selectedPlanet.definition.label;
}
@@ -42,7 +47,11 @@ export function getSelectionStripLabels(
selectedStation?: StationInstance,
selectedSystem?: SolarSystemInstance,
selectedPlanet?: PlanetInstance,
selectedNode?: ResourceNode,
) {
if (selectedNode) {
return [`Asteroid Field ${selectedNode.id}`];
}
if (selectedPlanet) {
return [selectedPlanet.definition.label];
}
@@ -63,7 +72,15 @@ export function getSelectionCardsMarkup(
selectedStation: StationInstance | undefined,
selectedSystem: SolarSystemInstance | 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) {
return renderCard(
selectedPlanet.definition.label,
@@ -92,6 +109,8 @@ export function getSelectionCardsMarkup(
[
selectedStation.factionId,
selectedStation.definition.category,
`Ore ${Math.round(selectedStation.oreStored)}`,
`Refined ${Math.round(selectedStation.refinedStock)}`,
`HP ${Math.round(selectedStation.health)}/${selectedStation.maxHealth}`,
`Dock ${selectedStation.dockedShipIds.size}/${selectedStation.definition.dockingCapacity}`,
],
@@ -105,7 +124,8 @@ export function getSelectionCardsMarkup(
renderCard(ship.definition.label, [
ship.factionId,
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}`,
]),
)
@@ -117,11 +137,15 @@ export function getSelectionDetails(
selectedStation: StationInstance | undefined,
selectedSystem: SolarSystemInstance | undefined,
selectedPlanet: PlanetInstance | undefined,
selectedNode: ResourceNode | undefined,
systems: SolarSystemInstance[],
viewLevel: ViewLevel,
ships: ShipInstance[],
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) {
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
? `\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");
}
export function describeStation(station: StationInstance, ships: ShipInstance[]) {
const miners = ships.filter((ship) => ship.systemId === station.systemId && ship.order.kind === "mine").length;
const escorts = ships.filter((ship) => ship.systemId === station.systemId && ship.order.kind === "escort").length;
const patrols = ships.filter((ship) => ship.systemId === station.systemId && ship.order.kind === "patrol").length;
const miners = ships.filter((ship) => ship.systemId === station.systemId && ship.defaultBehavior.kind === "auto-mine").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.defaultBehavior.kind === "patrol").length;
const localShips = ships.filter((ship) => ship.systemId === station.systemId).length;
const activeRecipe = station.activeRecipeId
? recipeDefinitions.find((recipe) => recipe.id === station.activeRecipeId)
@@ -238,8 +266,75 @@ export function getShipWindowMarkup(ships: ShipInstance[], selection: ShipInstan
.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 {
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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
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) {
return itemId ? itemDefinitionsById.get(itemId)?.label ?? itemId : "None";
}

View File

@@ -23,7 +23,6 @@ interface RenderOverlayOptions {
ships: ShipInstance[];
selection: ShipInstance[];
selectedStation?: StationInstance;
selectedSystemIndex: number;
viewLevel: ViewLevel;
}
@@ -97,7 +96,6 @@ export function drawStrategicOverlay({
ships,
selection,
selectedStation,
selectedSystemIndex,
viewLevel,
}: RenderOverlayOptions) {
context.clearRect(0, 0, width, height);

View File

@@ -226,7 +226,7 @@ function createSolarSystem(
return orbitLine;
});
const asteroidDecorations = createAsteroidField(definition, root, nodes, nextNodeId);
const asteroidDecorations = createAsteroidField(definition, root, nodes, nextNodeId, selectableTargets);
const strategicMarker = createStrategicMarker(scene, definition);
const system = {
definition,
@@ -263,6 +263,7 @@ function createAsteroidField(
root: THREE.Group,
nodes: ResourceNode[],
nextNodeId: () => string,
selectableTargets?: Map<THREE.Object3D, SelectableTarget>,
) {
const rockGeometry = new THREE.IcosahedronGeometry(1, 0);
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);
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);
decorations.push(cluster);
nodes.push({
const node = {
id: nextNodeId(),
systemId: definition.id,
position: cluster.getWorldPosition(new THREE.Vector3()),
mesh: cluster,
selectionRing,
oreRemaining: resourceNode.oreAmount,
maxOre: resourceNode.oreAmount,
itemId: resourceNode.itemId,
});
};
nodes.push(node);
cluster.traverse((child) => selectableTargets?.set(child, { kind: "node", node }));
});
return decorations;
@@ -693,10 +709,14 @@ export function createShipInstance({
ring,
systemId,
state: "idle",
order: { kind: "idle" },
order: undefined,
defaultBehavior: { kind: "idle" },
assignment: { kind: "unassigned" },
controllerTask: { kind: "idle" },
inventory: createEmptyInventory(),
cargoItemId: definition.cargoItemId,
actionTimer: 0,
dockingClearanceStatus: undefined,
factionId,
factionColor,
health: definition.maxHealth,
@@ -711,6 +731,7 @@ export function createShipInstance({
energy: 260,
maxFuel: 220,
maxEnergy: 260,
landedOffset: new THREE.Vector3(),
idleOrbitRadius: Math.max(120, group.position.length()),
idleOrbitAngle: 0,
warpFx,

View File

@@ -331,6 +331,65 @@ button:disabled {
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 {
position: absolute;
right: 10px;