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
|
||||
357
SESSION.md
357
SESSION.md
@@ -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
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
|
||||
1741
src/game/GameApp.ts
1741
src/game/GameApp.ts
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
"yPlane": 4,
|
||||
"arrivalThreshold": 16,
|
||||
"miningRate": 28,
|
||||
"transferRate": 56,
|
||||
"dockingDuration": 1.2,
|
||||
"undockDistance": 42,
|
||||
"energy": {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user