Refactor universe sim and observer HUD
This commit is contained in:
369
SESSION.md
369
SESSION.md
@@ -2,252 +2,205 @@
|
||||
|
||||
## Project State
|
||||
|
||||
This repository now contains a playable Three.js/Vite prototype for a space RTS / economy sim testbed inspired by EVE Online and X4.
|
||||
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 codebase has been refactored away from a single monolithic `GameApp.ts` toward a more maintainable, data-driven structure. Authored game content now lives in JSON catalogs, while runtime code is split into domain types, world-building helpers, UI presenters, and rendering helpers.
|
||||
The codebase is still TypeScript + Three.js on Vite, with authored catalogs under `src/game/data/`, but the runtime now centers on:
|
||||
|
||||
The current prototype includes:
|
||||
- procedural universe generation
|
||||
- autonomous faction behavior
|
||||
- fleet / wing hierarchy
|
||||
- economic production loops
|
||||
- pirate harassment
|
||||
- strategic system control
|
||||
- observer-oriented HUD and camera controls
|
||||
|
||||
- Two solar systems: `Helios Reach` and `Perseus Gate`
|
||||
- A large space environment with stars, planets, orbit lines, nebulae, asteroid/resource fields, and starfield
|
||||
- RTS-style ship selection, command issuance, camera movement, zoom levels, and follow-camera support
|
||||
- Three view levels based on zoom: `local`, `solar`, and `universe`
|
||||
- A bottom command bar with selection info, order buttons, and a minimap
|
||||
- A strategic HUD overlay that switches to NATO / military-style symbols at higher zoom levels
|
||||
- A generic window layer, with an initial fleet command window for managing multi-wing groups
|
||||
- App windows can now be dragged by their headers and resized from a corner grip
|
||||
- `stargate` now exists as a constructible category in the same authored pipeline as other stations
|
||||
## Current Prototype
|
||||
|
||||
## Major Gameplay Systems Added
|
||||
The current build includes:
|
||||
|
||||
### World / Navigation
|
||||
- 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
|
||||
- fleet and wing structure with behaviors such as:
|
||||
- `command`
|
||||
- `screen`
|
||||
- `mining`
|
||||
- `logistics`
|
||||
- `escort`
|
||||
- observer controls for camera orbit, pan, focus, and inspection
|
||||
|
||||
- Ships can travel between the two systems using staged FTL travel
|
||||
- Travel flow includes:
|
||||
- leaving gravity well
|
||||
- FTL spool
|
||||
- warp
|
||||
- arrival
|
||||
- FTL speed was increased and a basic warp streak / tunnel effect was added
|
||||
- Local ship movement is no longer purely straight-line:
|
||||
- ships bias toward curved orbital-style transfers around the system center
|
||||
- idle ships hold a passive orbit instead of freezing in place
|
||||
## Major Gameplay / Sim Systems
|
||||
|
||||
### Orbital Model
|
||||
### Universe Generation
|
||||
|
||||
- Stations are no longer static arbitrary points
|
||||
- Stations in `Helios` are placed on Lagrange-style offsets relative to planets
|
||||
- Stations update position over time with the planetary orbital motion
|
||||
- Ships and stations are beginning to behave like orbitals rather than free-floating markers
|
||||
- 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
|
||||
|
||||
### Units / AI / Orders
|
||||
### Factions
|
||||
|
||||
- Ship roles currently in the prototype:
|
||||
- military
|
||||
- transport
|
||||
- mining
|
||||
- Ship classes now distinguish:
|
||||
- frigate
|
||||
- destroyer
|
||||
- cruiser
|
||||
- industrial
|
||||
- capital
|
||||
- Unit state machine now includes states for:
|
||||
- idle / moving
|
||||
- FTL travel
|
||||
- mining and delivery
|
||||
- docking approach / docking / docked / undocking
|
||||
- patrol / escort
|
||||
- Fleets are now a first-class gameplay concept:
|
||||
- fleets have a commander, stance, high-level fleet order, and explicit wings
|
||||
- wings can be nested via parent/child relationships to represent sub-wings
|
||||
- ships now carry behavior metadata such as command, mining, escort, screen, or logistics
|
||||
- fleet orders fan out into ship-level execution orders, keeping "order" separate from "behavior"
|
||||
- Fleet tree selection is now manager-backed:
|
||||
- fleets, wings, sub-wings, and ships can be selected directly from the fleet window
|
||||
- selection is now handled through a dedicated selection manager rather than scattered UI state mutations
|
||||
- Orders currently supported:
|
||||
- move
|
||||
- transfer
|
||||
- mine
|
||||
- patrol
|
||||
- escort
|
||||
- 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.
|
||||
|
||||
### Docking / Logistics
|
||||
### High-Level AI / Delegation
|
||||
|
||||
- Docking was added as a required step for transfer to stations
|
||||
- Stations have limited docking capacity and explicit docking ports
|
||||
- Carriers now act as mobile docking hosts for smaller combatants
|
||||
- Carrier recovery was corrected so docking ships reserve a pad early and approach the moving pad directly instead of stalling behind the hull
|
||||
- Mining ships now:
|
||||
- mine ore in `Perseus`
|
||||
- return to `Helios`
|
||||
- dock at a refinery
|
||||
- transfer ore
|
||||
- undock and repeat
|
||||
- Mining ships now correctly leave for the refinery once full even when the delivery leg is inter-system
|
||||
- Faction AI now acts at a strategic level instead of directly micromanaging every ship.
|
||||
- Empire AI chooses high-level goals such as:
|
||||
- secure home and mining space
|
||||
- contest central systems
|
||||
- assign industrial fleets to mining loops
|
||||
- Pirate AI chooses raid targets and dispatches fleets into hostile space.
|
||||
- Fleet-level orders are now the intended command boundary between:
|
||||
- faction strategy
|
||||
- fleet / wing execution
|
||||
- This work was specifically done to stop faction AI from stomping `screen` behavior with raw ship move orders.
|
||||
|
||||
### Economy / Inventory Foundations
|
||||
### Fleets / Wings
|
||||
|
||||
- Added item storage classes:
|
||||
- `bulk-solid`
|
||||
- `bulk-liquid`
|
||||
- `bulk-gas`
|
||||
- `container`
|
||||
- `manufactured`
|
||||
- Added itemized manufactured goods for industrial progression:
|
||||
- `hull-sections`
|
||||
- `ammo-crates`
|
||||
- `naval-guns`
|
||||
- `ship-equipment`
|
||||
- `ship-parts`
|
||||
- Added deployable constructible kit items so buildables are also producible:
|
||||
- `trade-hub-kit`
|
||||
- `refinery-kit`
|
||||
- `farm-ring-kit`
|
||||
- `manufactory-kit`
|
||||
- `shipyard-kit`
|
||||
- `defense-grid-kit`
|
||||
- `stargate-kit`
|
||||
- Added module categories and starter module definitions for ships/stations
|
||||
- Added explicit recipe data for refinery and fabrication processing
|
||||
- Ships and stations now expose compatible cargo/storage/module metadata
|
||||
- Refineries track:
|
||||
- ore stored
|
||||
- active refining batch
|
||||
- refining timer
|
||||
- refined output stock
|
||||
- Stations now also maintain per-item stock internally
|
||||
- Fabricator-array stations can now build ship parts, ammo, guns, and equipment from recipe data
|
||||
- Fabricator-array stations can now also assemble deployable kits for constructibles, including stargates
|
||||
- Refinery/manufactory processing now consumes itemized inputs and produces itemized outputs through a shared recipe-driven flow
|
||||
- Fleet creation now groups ships per faction and role in `src/game/fleet/runtime.ts`.
|
||||
- War fleets and industry fleets are generated from faction-owned ships.
|
||||
- Wing behaviors remain meaningful at the tactical layer.
|
||||
- `screen` is intended to remain subordinate to fleet command rather than independent faction micromanagement.
|
||||
|
||||
### Energy / Fuel
|
||||
### Economy / Production
|
||||
|
||||
- Ships now track:
|
||||
- fuel
|
||||
- energy
|
||||
- Stations now track:
|
||||
- fuel
|
||||
- energy
|
||||
- Ships consume energy/fuel depending on activity
|
||||
- Docked ships recharge energy
|
||||
- Stations recharge energy passively
|
||||
- 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.
|
||||
|
||||
## Testbed Layout
|
||||
### Combat / Control
|
||||
|
||||
- `Helios Reach` is now the industrial / infrastructure system
|
||||
- stations are concentrated there
|
||||
- refinery loop terminates there
|
||||
- `Perseus Gate` is now the extraction / resource system
|
||||
- resource asteroid nodes are concentrated there
|
||||
- miners operate there before hauling back
|
||||
- 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.
|
||||
|
||||
## Starting State
|
||||
|
||||
- Empires now start very small for easier debugging and growth observation.
|
||||
- Each empire currently starts with only 3 ships:
|
||||
- 1 frigate
|
||||
- 1 hauler
|
||||
- 1 miner
|
||||
- Pirates still start with small raiding groups.
|
||||
|
||||
## UI / UX State
|
||||
|
||||
- Ship and station selection is supported
|
||||
- Ship multi-selection is supported via click modifiers and marquee drag selection
|
||||
- Selected ships can now be ordered to dock with the nearest friendly carrier
|
||||
- Active fleets can be selected and focused through the fleet command window
|
||||
- The fleet window now renders fleets as a real tree rather than a flat list
|
||||
- Fleet tree nodes show order, behavior, and state on ship-level entries
|
||||
- `Solar` and `universe` views now overlay high-level tactical symbology instead of relying only on 3D meshes
|
||||
- `Solar` view now shows fleet hierarchy links between commanders and wing leaders
|
||||
- Ships use role-specific long-range symbols:
|
||||
- military: hostile/combat-style diamond iconography
|
||||
- transport: boxed logistics symbol
|
||||
- mining: angular resource / industrial symbol
|
||||
- Stations and constructibles use square strategic markers with category-specific internal glyphs
|
||||
- `Universe` view groups ships into fleet counts per system and role for cleaner strategic readability
|
||||
- Focusing works for:
|
||||
- single ships: follow camera
|
||||
- stations: focus camera on the station
|
||||
- Selection panels show:
|
||||
- ship state, order, cargo, hold type, fuel, energy, modules
|
||||
- station role, docking occupancy, stored resources, refinery timing, fuel, energy, modules
|
||||
### Observer HUD
|
||||
|
||||
- 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
|
||||
- Fleet launch controls were removed from the main HUD.
|
||||
- A dedicated `Debug` window now contains the `New Universe` button.
|
||||
|
||||
### Selection / Inspection
|
||||
|
||||
- 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.
|
||||
- Fleet window tree selection still works.
|
||||
|
||||
### Windows
|
||||
|
||||
- Generic draggable / resizable app windows still exist.
|
||||
- Main windows currently in use:
|
||||
- `Fleet Command`
|
||||
- `Debug`
|
||||
|
||||
### Strategic Rendering
|
||||
|
||||
- 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.
|
||||
|
||||
## Controls
|
||||
|
||||
- `Left Click`: select ships or stations
|
||||
- `Shift + Left Click`: add ships to ship selection
|
||||
- `Ctrl/Cmd + Left Click`: toggle ships in selection
|
||||
- `Left Drag`: marquee-select multiple ships
|
||||
- `Right Click`: issue move/transfer orders
|
||||
- `Right Click` with no ship selection and an active fleet: issue a fleet move order
|
||||
- `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
|
||||
- `Q / E`: rotate camera
|
||||
- `F`: focus selection, and follow a single selected ship
|
||||
- `G`: toggle the fleet command window
|
||||
- `R`: assign selected compatible ships to dock with the nearest friendly carrier
|
||||
- `F`: focus current selection
|
||||
- `G`: toggle fleet command window
|
||||
- `Tab`: jump camera between systems
|
||||
- `B`: toggle build mode
|
||||
- `1-7`: choose constructible
|
||||
- `M`: assign mining
|
||||
- `P`: assign patrol
|
||||
- `E`: assign escort
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- The prototype is built with:
|
||||
- Vite
|
||||
- TypeScript
|
||||
- Three.js
|
||||
- Authored data now lives in JSON files under `src/game/data/`, including:
|
||||
- `items.json`
|
||||
- `recipes.json`
|
||||
- `systems.json`
|
||||
- `modules.json`
|
||||
- `ships.json`
|
||||
- `constructibles.json`
|
||||
- `scenario.json`
|
||||
- `balance.json`
|
||||
- Shared domain and runtime types now live in `src/game/types.ts`
|
||||
- Stations now carry both coarse storage totals and itemized stock for recipes
|
||||
- The recipe graph now covers every current constructible and every current catalog item ID
|
||||
- World construction is extracted into `src/game/world/worldFactory.ts`
|
||||
- HUD creation and presentation logic are extracted into:
|
||||
- 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`
|
||||
- Fleet composition helpers:
|
||||
- `src/game/fleet/runtime.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`
|
||||
- Fleet composition helpers now live in:
|
||||
- `src/game/fleet/runtime.ts`
|
||||
- Inventory helpers now live in `src/game/state/inventory.ts`
|
||||
- Selection logic is now centralized in:
|
||||
- `src/game/state/selectionManager.ts`
|
||||
- Ship-to-ship docking now reuses the same generalized docking path as station docking inside `src/game/GameApp.ts`
|
||||
- High-level symbology is rendered through a dedicated 2D HUD overlay canvas layered above the 3D scene
|
||||
- Production build is currently passing with `npm run build`
|
||||
|
||||
## Known Limitations / Caveats
|
||||
|
||||
- Orbital behavior is still an approximation for gameplay, not a full orbital mechanics simulation
|
||||
- Stations are on Lagrange-style offsets, but not using a physically rigorous orbital solver
|
||||
- Ship transfer paths are curved and orbit-biased, but still use authored steering rather than patched conics or n-body integration
|
||||
- Fuel / energy exist but station refueling, resupply, and depletion failure states are still minimal
|
||||
- Module definitions exist, and a generic window framework now exists, but there is still no actual ship/station designer yet
|
||||
- Production is still automated by recipe priority; there is not yet a player-facing queue UI for choosing or reordering station recipes
|
||||
- Constructible recipes currently output kit items, but build mode still spawns structures directly rather than consuming those kits
|
||||
- Docking works for logistics, but there is not yet a richer docking queue / reservation UI
|
||||
- NATO-style symbology is gameplay-oriented inspiration, not a strict APP-6 / MIL-STD implementation
|
||||
- Fleet orders currently cover patrol, move, hold, and mining, but wing-specific doctrine editing is still minimal
|
||||
- Carrier recovery exists, but launch/redeploy flows and richer carrier doctrine are still minimal
|
||||
- Window positions and sizes are not yet persisted across reloads
|
||||
- `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.
|
||||
- Fleet window remains useful, but the overall UI is now only partially refit for observer mode.
|
||||
- There is still no persistence layer for window layouts, saves, or generated universe seeds.
|
||||
|
||||
## Suggested Next Steps
|
||||
|
||||
- Continue shrinking `GameApp.ts` by extracting simulation/order logic into dedicated gameplay systems once the current rules stabilize
|
||||
- Add JSON schema validation or runtime validation for the authored data catalogs to catch content errors earlier
|
||||
- Move constructible placement and future unit spawning onto a shared scenario/entity factory pipeline
|
||||
- Introduce explicit orbital anchors for:
|
||||
- stars
|
||||
- planets
|
||||
- stations
|
||||
- asteroid belts / resource fields
|
||||
- Replace the current movement approximation with a more formal orbital transfer model
|
||||
- Add refueling and power management gameplay
|
||||
- Add ship/station fitting data structures that can later drive a designer UI
|
||||
- Expand the economy beyond ore/refining into manufactured goods and trade lanes
|
||||
- Improve FTL visuals with a fullscreen post-process distortion or tunnel effect
|
||||
- Expand the strategic overlay with threat rings, route arrows, and fleet stance/status markers
|
||||
- Extract fleet command propagation and ship AI execution out of `GameApp.ts` into dedicated simulation systems now that the fleet model has stabilized enough to justify it
|
||||
- Extract faction strategy into a dedicated AI / planning module
|
||||
- Extract fleet order execution into its own gameplay system
|
||||
- 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
|
||||
- Improve combat behavior so `screen`, `escort`, and `command` have stronger distinct tactical roles
|
||||
- Add system-level threat, ownership, and economy views for game-master inspection
|
||||
- Add save/load support for generated universes and long-running simulations
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ interface FleetBuildSpec {
|
||||
label: string;
|
||||
stance: FleetInstance["stance"];
|
||||
systemId: string;
|
||||
factionId?: string;
|
||||
commander: ShipInstance;
|
||||
wings: Array<{
|
||||
id: string;
|
||||
@@ -18,94 +19,70 @@ interface FleetBuildSpec {
|
||||
|
||||
export function createDefaultFleets(ships: ShipInstance[]) {
|
||||
clearFleetAssignments(ships);
|
||||
|
||||
const heliosMilitary = ships.filter((ship) => ship.systemId === "helios" && ship.definition.role === "military");
|
||||
const heliosCarriers = heliosMilitary.filter((ship) => ship.definition.shipClass === "capital");
|
||||
const heliosDestroyers = heliosMilitary.filter((ship) => ship.definition.id === "destroyer");
|
||||
const allHaulers = ships.filter((ship) => ship.definition.role === "transport");
|
||||
const perseusMilitary = ships.filter((ship) => ship.systemId === "perseus" && ship.definition.role === "military");
|
||||
const miners = ships.filter((ship) => ship.definition.role === "mining");
|
||||
|
||||
const miningHaulers = allHaulers.slice(0, 2);
|
||||
const homeHaulers = allHaulers.slice(2);
|
||||
|
||||
const specs: FleetBuildSpec[] = [];
|
||||
const factionIds = [...new Set(ships.map((ship) => ship.factionId))];
|
||||
|
||||
const homeCommander = heliosCarriers[0] ?? heliosDestroyers[0] ?? heliosMilitary[0];
|
||||
if (homeCommander) {
|
||||
const homeScreenShips = heliosMilitary.filter(
|
||||
(ship) => ship.id !== homeCommander.id && ship.definition.shipClass !== "destroyer",
|
||||
);
|
||||
specs.push({
|
||||
id: "helios-home-fleet",
|
||||
label: "Helios Home Fleet",
|
||||
stance: "defensive",
|
||||
systemId: "helios",
|
||||
commander: homeCommander,
|
||||
wings: [
|
||||
{
|
||||
id: "command",
|
||||
label: "Command Wing",
|
||||
behavior: "command",
|
||||
ships: [homeCommander, ...heliosDestroyers.slice(1)],
|
||||
},
|
||||
{
|
||||
id: "screen",
|
||||
label: "Screen Wing",
|
||||
behavior: "screen",
|
||||
parentWingId: "command",
|
||||
ships: homeScreenShips,
|
||||
},
|
||||
{
|
||||
id: "logistics",
|
||||
label: "Logistics Wing",
|
||||
behavior: "logistics",
|
||||
parentWingId: "command",
|
||||
ships: homeHaulers,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
factionIds.forEach((factionId) => {
|
||||
const factionShips = ships.filter((ship) => ship.factionId === factionId);
|
||||
const military = factionShips.filter((ship) => ship.definition.role === "military");
|
||||
const industrial = factionShips.filter((ship) => ship.definition.role !== "military");
|
||||
const systems = [...new Set(factionShips.map((ship) => ship.systemId))];
|
||||
|
||||
const extractionCommander = perseusMilitary[0] ?? miners[0];
|
||||
if (extractionCommander) {
|
||||
specs.push({
|
||||
id: "perseus-extraction-fleet",
|
||||
label: "Perseus Extraction Group",
|
||||
stance: "industrial",
|
||||
systemId: "perseus",
|
||||
commander: extractionCommander,
|
||||
wings: [
|
||||
{
|
||||
id: "command",
|
||||
label: "Command Wing",
|
||||
behavior: "command",
|
||||
ships: [extractionCommander],
|
||||
},
|
||||
{
|
||||
id: "miners",
|
||||
label: "Mining Wing",
|
||||
behavior: "mining",
|
||||
parentWingId: "command",
|
||||
ships: miners,
|
||||
},
|
||||
{
|
||||
id: "escort",
|
||||
label: "Escort Wing",
|
||||
behavior: "escort",
|
||||
parentWingId: "miners",
|
||||
ships: perseusMilitary.filter((ship) => ship.id !== extractionCommander.id),
|
||||
},
|
||||
{
|
||||
id: "transport",
|
||||
label: "Transport Wing",
|
||||
behavior: "logistics",
|
||||
parentWingId: "miners",
|
||||
ships: miningHaulers,
|
||||
},
|
||||
],
|
||||
systems.forEach((systemId) => {
|
||||
const localMilitary = military.filter((ship) => ship.systemId === systemId);
|
||||
if (localMilitary.length === 0) {
|
||||
return;
|
||||
}
|
||||
const commander =
|
||||
localMilitary.find((ship) => ship.definition.shipClass === "capital") ??
|
||||
localMilitary.find((ship) => ship.definition.shipClass === "cruiser") ??
|
||||
localMilitary[0];
|
||||
const lineShips = localMilitary.filter((ship) => ship.id !== commander.id);
|
||||
specs.push({
|
||||
id: `${factionId}:${systemId}:warfleet`,
|
||||
label: `${commander.factionId} War Fleet`,
|
||||
stance: factionId.includes("pirate") || factionId.includes("flag") || factionId.includes("rats") ? "balanced" : "defensive",
|
||||
systemId,
|
||||
factionId,
|
||||
commander,
|
||||
wings: [
|
||||
{ id: "command", label: "Command Wing", behavior: "command", ships: [commander] },
|
||||
{ id: "screen", label: "Screen Wing", behavior: "screen", parentWingId: "command", ships: lineShips },
|
||||
],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const miners = industrial.filter((ship) => ship.definition.role === "mining");
|
||||
const haulers = industrial.filter((ship) => ship.definition.role === "transport");
|
||||
const logisticsCommander = haulers[0] ?? miners[0];
|
||||
if (logisticsCommander) {
|
||||
specs.push({
|
||||
id: `${factionId}:industry`,
|
||||
label: `${logisticsCommander.factionId} Industry Group`,
|
||||
stance: "industrial",
|
||||
systemId: logisticsCommander.systemId,
|
||||
factionId,
|
||||
commander: logisticsCommander,
|
||||
wings: [
|
||||
{ id: "command", label: "Command Wing", behavior: "command", ships: [logisticsCommander] },
|
||||
{
|
||||
id: "miners",
|
||||
label: "Mining Wing",
|
||||
behavior: "mining",
|
||||
parentWingId: "command",
|
||||
ships: miners.filter((ship) => ship.id !== logisticsCommander.id),
|
||||
},
|
||||
{
|
||||
id: "transport",
|
||||
label: "Transport Wing",
|
||||
behavior: "logistics",
|
||||
parentWingId: "command",
|
||||
ships: haulers.filter((ship) => ship.id !== logisticsCommander.id),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return specs.map((spec) => materializeFleet(spec));
|
||||
}
|
||||
@@ -140,11 +117,9 @@ export function describeFleetOrder(fleet: FleetInstance) {
|
||||
}
|
||||
|
||||
function materializeFleet(spec: FleetBuildSpec): FleetInstance {
|
||||
const wings = spec.wings
|
||||
.filter((wing) => wing.ships.length > 0)
|
||||
.map((wing) => buildWing(spec.id, wing));
|
||||
|
||||
const wings = spec.wings.filter((wing) => wing.ships.length > 0).map((wing) => buildWing(spec.id, wing));
|
||||
const shipIds = wings.flatMap((wing) => wing.shipIds);
|
||||
|
||||
shipIds.forEach((shipId, index) => {
|
||||
const ship = spec.wings.flatMap((wing) => wing.ships).find((candidate) => candidate.id === shipId);
|
||||
if (!ship) {
|
||||
@@ -168,6 +143,7 @@ function materializeFleet(spec: FleetBuildSpec): FleetInstance {
|
||||
stance: spec.stance,
|
||||
commanderShipId: spec.commander.id,
|
||||
systemId: spec.systemId,
|
||||
factionId: spec.factionId,
|
||||
shipIds,
|
||||
wings,
|
||||
order: { kind: "idle" },
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import * as THREE from "three";
|
||||
import type { ShipInstance, StationInstance } from "../types";
|
||||
import type { PlanetInstance, ShipInstance, SolarSystemInstance, StationInstance } from "../types";
|
||||
|
||||
export class SelectionManager {
|
||||
private shipSelection: ShipInstance[] = [];
|
||||
private stationSelection?: StationInstance;
|
||||
private systemSelection?: SolarSystemInstance;
|
||||
private planetSelection?: PlanetInstance;
|
||||
|
||||
getShips() {
|
||||
return this.shipSelection;
|
||||
@@ -13,6 +15,14 @@ export class SelectionManager {
|
||||
return this.stationSelection;
|
||||
}
|
||||
|
||||
getSystem() {
|
||||
return this.systemSelection;
|
||||
}
|
||||
|
||||
getPlanet() {
|
||||
return this.planetSelection;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.shipSelection.forEach((ship) => this.setShipVisual(ship, false));
|
||||
this.shipSelection = [];
|
||||
@@ -20,6 +30,14 @@ export class SelectionManager {
|
||||
this.setStationVisual(this.stationSelection, false);
|
||||
this.stationSelection = undefined;
|
||||
}
|
||||
if (this.systemSelection) {
|
||||
this.setSystemVisual(this.systemSelection, false);
|
||||
this.systemSelection = undefined;
|
||||
}
|
||||
if (this.planetSelection) {
|
||||
this.setPlanetVisual(this.planetSelection, false);
|
||||
this.planetSelection = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
replaceShips(ships: ShipInstance[]) {
|
||||
@@ -36,6 +54,24 @@ export class SelectionManager {
|
||||
this.setStationVisual(station, true);
|
||||
}
|
||||
|
||||
setSystem(system?: SolarSystemInstance) {
|
||||
this.clear();
|
||||
if (!system) {
|
||||
return;
|
||||
}
|
||||
this.systemSelection = system;
|
||||
this.setSystemVisual(system, true);
|
||||
}
|
||||
|
||||
setPlanet(planet?: PlanetInstance) {
|
||||
this.clear();
|
||||
if (!planet) {
|
||||
return;
|
||||
}
|
||||
this.planetSelection = planet;
|
||||
this.setPlanetVisual(planet, true);
|
||||
}
|
||||
|
||||
addShip(ship: ShipInstance) {
|
||||
if (this.shipSelection.includes(ship)) {
|
||||
return;
|
||||
@@ -44,6 +80,14 @@ export class SelectionManager {
|
||||
this.setStationVisual(this.stationSelection, false);
|
||||
this.stationSelection = undefined;
|
||||
}
|
||||
if (this.systemSelection) {
|
||||
this.setSystemVisual(this.systemSelection, false);
|
||||
this.systemSelection = undefined;
|
||||
}
|
||||
if (this.planetSelection) {
|
||||
this.setPlanetVisual(this.planetSelection, false);
|
||||
this.planetSelection = undefined;
|
||||
}
|
||||
this.shipSelection.push(ship);
|
||||
this.setShipVisual(ship, true);
|
||||
}
|
||||
@@ -73,4 +117,20 @@ export class SelectionManager {
|
||||
private setStationVisual(station: StationInstance, selected: boolean) {
|
||||
(station.ring.material as THREE.MeshBasicMaterial).opacity = selected ? 0.95 : 0;
|
||||
}
|
||||
|
||||
private setSystemVisual(system: SolarSystemInstance, selected: boolean) {
|
||||
if (system.strategicMarker instanceof THREE.Group) {
|
||||
system.strategicMarker.traverse((child) => {
|
||||
if ("material" in child && child.material instanceof THREE.MeshBasicMaterial) {
|
||||
child.material.opacity = selected ? Math.max(child.material.opacity, 0.9) : child === system.strategicMarker.children[0] ? 0.4 : 0.7;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private setPlanetVisual(planet: PlanetInstance, selected: boolean) {
|
||||
if (planet.selectionRing) {
|
||||
(planet.selectionRing.material as THREE.MeshBasicMaterial).opacity = selected ? 0.95 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ export type ShipRole = "military" | "transport" | "mining";
|
||||
export type ShipClass = "frigate" | "destroyer" | "cruiser" | "industrial" | "capital";
|
||||
export type FleetBehavior = "command" | "screen" | "escort" | "mining" | "logistics" | "reserve";
|
||||
export type FleetStance = "balanced" | "defensive" | "industrial";
|
||||
export type GameWindowId = "fleet-command" | "ship-designer" | "station-manager";
|
||||
export type GameWindowId = "fleet-command" | "ship-designer" | "station-manager" | "debug";
|
||||
export type FactionKind = "empire" | "pirate";
|
||||
export type ConstructibleCategory =
|
||||
| "station"
|
||||
| "refining"
|
||||
@@ -147,9 +148,11 @@ export interface SolarSystemDefinition {
|
||||
export interface InitialStationDefinition {
|
||||
constructibleId: string;
|
||||
systemId: string;
|
||||
factionId?: string;
|
||||
planetIndex?: number;
|
||||
lagrangeSide?: -1 | 1;
|
||||
position?: [number, number, number];
|
||||
seedStock?: Partial<Record<string, number>>;
|
||||
}
|
||||
|
||||
export interface ShipFormationDefinition {
|
||||
@@ -157,6 +160,7 @@ export interface ShipFormationDefinition {
|
||||
count: number;
|
||||
center: [number, number, number];
|
||||
systemId: string;
|
||||
factionId?: string;
|
||||
}
|
||||
|
||||
export interface PatrolRouteDefinition {
|
||||
@@ -178,6 +182,45 @@ export interface ScenarioDefinition {
|
||||
nodeSystemId: string;
|
||||
refinerySystemId: string;
|
||||
};
|
||||
factions?: FactionDefinition[];
|
||||
centralSystemIds?: string[];
|
||||
}
|
||||
|
||||
export interface UniverseDefinition {
|
||||
seed: number;
|
||||
label: string;
|
||||
systems: SolarSystemDefinition[];
|
||||
scenario: ScenarioDefinition;
|
||||
}
|
||||
|
||||
export interface FactionDefinition {
|
||||
id: string;
|
||||
label: string;
|
||||
kind: FactionKind;
|
||||
color: string;
|
||||
accent: string;
|
||||
homeSystemId: string;
|
||||
miningSystemId?: string;
|
||||
targetSystemIds: string[];
|
||||
rivals: string[];
|
||||
pirateForFactionId?: string;
|
||||
}
|
||||
|
||||
export interface FactionInstance {
|
||||
definition: FactionDefinition;
|
||||
credits: number;
|
||||
oreMined: number;
|
||||
goodsProduced: number;
|
||||
shipsBuilt: number;
|
||||
stationsBuilt: number;
|
||||
shipsLost: number;
|
||||
enemyShipsDestroyed: number;
|
||||
raidsCompleted: number;
|
||||
stolenCargo: number;
|
||||
ownedSystemIds: Set<string>;
|
||||
shipBuildTimer: number;
|
||||
stationBuildTimer: number;
|
||||
commandTick: number;
|
||||
}
|
||||
|
||||
export interface GameBalance {
|
||||
@@ -244,6 +287,7 @@ export interface FleetInstance {
|
||||
stance: FleetStance;
|
||||
commanderShipId: string;
|
||||
systemId: string;
|
||||
factionId?: string;
|
||||
shipIds: string[];
|
||||
wings: FleetWingInstance[];
|
||||
order: FleetOrder;
|
||||
@@ -267,6 +311,14 @@ export interface ShipInstance {
|
||||
dockedStationId?: string;
|
||||
dockedCarrierId?: string;
|
||||
dockingPortIndex?: number;
|
||||
factionId: string;
|
||||
factionColor: string;
|
||||
health: number;
|
||||
maxHealth: number;
|
||||
weaponRange: number;
|
||||
weaponDamage: number;
|
||||
weaponCooldown: number;
|
||||
weaponTimer: number;
|
||||
fuel: number;
|
||||
energy: number;
|
||||
maxFuel: number;
|
||||
@@ -302,6 +354,13 @@ export interface StationInstance {
|
||||
modules: string[];
|
||||
orbitalParentPlanetIndex?: number;
|
||||
lagrangeSide?: -1 | 1;
|
||||
factionId: string;
|
||||
factionColor: string;
|
||||
health: number;
|
||||
maxHealth: number;
|
||||
weaponRange: number;
|
||||
weaponDamage: number;
|
||||
weaponTimer: number;
|
||||
fuel: number;
|
||||
energy: number;
|
||||
maxFuel: number;
|
||||
@@ -309,10 +368,14 @@ export interface StationInstance {
|
||||
}
|
||||
|
||||
export interface PlanetInstance {
|
||||
definition: PlanetDefinition;
|
||||
group: THREE.Group;
|
||||
mesh: THREE.Mesh;
|
||||
orbitSpeed: number;
|
||||
ring?: THREE.Object3D;
|
||||
selectionRing?: THREE.Mesh;
|
||||
systemId: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export interface ResourceNode {
|
||||
@@ -335,17 +398,24 @@ export interface SolarSystemInstance {
|
||||
orbitLines: THREE.LineLoop[];
|
||||
asteroidDecorations: THREE.Object3D[];
|
||||
strategicMarker: THREE.Object3D;
|
||||
controllingFactionId?: string;
|
||||
controlProgress: number;
|
||||
strategicValue: "core" | "resource" | "frontier" | "central";
|
||||
}
|
||||
|
||||
export type SelectableTarget =
|
||||
| { kind: "ship"; ship: ShipInstance }
|
||||
| { kind: "station"; station: StationInstance };
|
||||
| { kind: "station"; station: StationInstance }
|
||||
| { kind: "system"; system: SolarSystemInstance }
|
||||
| { kind: "planet"; system: SolarSystemInstance; planet: PlanetInstance };
|
||||
|
||||
export interface HudElements {
|
||||
details: HTMLDivElement;
|
||||
status: HTMLDivElement;
|
||||
selectionTitle: HTMLHeadingElement;
|
||||
selectionStrip: HTMLDivElement;
|
||||
orders: HTMLDivElement;
|
||||
sessionActions: HTMLDivElement;
|
||||
minimap: HTMLCanvasElement;
|
||||
minimapContext: CanvasRenderingContext2D;
|
||||
marquee: HTMLDivElement;
|
||||
@@ -355,4 +425,5 @@ export interface HudElements {
|
||||
fleetWindowBody: HTMLDivElement;
|
||||
fleetWindowTitle: HTMLHeadingElement;
|
||||
fleetWindowSubtitle: HTMLParagraphElement;
|
||||
debugWindow: HTMLDivElement;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { HudElements } from "../types";
|
||||
|
||||
interface HudHandlers {
|
||||
onOrderAction: (action: string) => void;
|
||||
onWindowAction: (action: string) => void;
|
||||
onFleetAction: (action: string, fleetId?: string) => void;
|
||||
onSelectionAction: (kind: string, id: string) => void;
|
||||
@@ -12,43 +10,15 @@ export function createHud(container: HTMLElement, handlers: HudHandlers): HudEle
|
||||
root.className = "hud";
|
||||
root.innerHTML = `
|
||||
<canvas class="strategic-overlay"></canvas>
|
||||
<section class="panel summary">
|
||||
<h1>Helios Reach Command</h1>
|
||||
<p>
|
||||
Dual-star-system prototype with gravity-well exits, FTL spooling, inter-system travel,
|
||||
and layered fleet command with wing behaviors, escort screens, and logistics groups.
|
||||
</p>
|
||||
</section>
|
||||
<section class="panel details">
|
||||
<h2>Selection</h2>
|
||||
<div class="selection-meta">
|
||||
<h2 class="selection-title">No Selection</h2>
|
||||
<div class="mode"></div>
|
||||
</div>
|
||||
<div class="selection-strip"></div>
|
||||
<div class="content"></div>
|
||||
</section>
|
||||
<section class="panel commandbar">
|
||||
<div class="selection-panel">
|
||||
<h2 class="selection-title">No Selection</h2>
|
||||
<div class="content compact"></div>
|
||||
</div>
|
||||
<div class="orders-panel">
|
||||
<div class="mode"></div>
|
||||
<div class="window-launchers">
|
||||
<button type="button" data-window-action="toggle-fleet-command">Fleets</button>
|
||||
<button type="button" data-window-action="toggle-ship-designer" disabled>Designer</button>
|
||||
<button type="button" data-window-action="toggle-station-manager" disabled>Stations</button>
|
||||
</div>
|
||||
<div class="orders">
|
||||
<button type="button" data-action="move">Move</button>
|
||||
<button type="button" data-action="mine">Mine</button>
|
||||
<button type="button" data-action="patrol">Patrol</button>
|
||||
<button type="button" data-action="escort">Escort</button>
|
||||
<button type="button" data-action="dock">Dock</button>
|
||||
<button type="button" data-action="focus">Focus</button>
|
||||
</div>
|
||||
<div class="hint">Left click selects ships or stations. Shift adds. Right click moves selection, or the active fleet when nothing is selected. G toggles fleet command. R recovers selected ships to the nearest friendly carrier. Mouse wheel or -/= zoom. B build. M miners mine. P patrol. E escort.</div>
|
||||
</div>
|
||||
<div class="minimap-panel">
|
||||
<canvas class="minimap" width="220" height="160"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
<canvas class="minimap minimap-hidden" width="220" height="160"></canvas>
|
||||
<section class="app-window fleet-window" data-window-id="fleet-command">
|
||||
<div class="window-header">
|
||||
<div>
|
||||
@@ -66,14 +36,26 @@ export function createHud(container: HTMLElement, handlers: HudHandlers): HudEle
|
||||
<div class="window-body fleet-window-body"></div>
|
||||
<div class="window-resize-handle" aria-hidden="true"></div>
|
||||
</section>
|
||||
<section class="app-window debug-window" data-window-id="debug">
|
||||
<div class="window-header">
|
||||
<div>
|
||||
<h2>Debug</h2>
|
||||
<p class="window-subtitle">Simulation controls</p>
|
||||
</div>
|
||||
<button type="button" class="window-close" data-window-action="toggle-debug">Close</button>
|
||||
</div>
|
||||
<div class="window-body">
|
||||
<div class="session-actions">
|
||||
<button type="button" data-window-action="new-universe">New Universe</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="window-resize-handle" aria-hidden="true"></div>
|
||||
</section>
|
||||
<div class="marquee"></div>
|
||||
`;
|
||||
|
||||
container.append(root);
|
||||
initializeWindowInteractions(root);
|
||||
root.querySelectorAll<HTMLButtonElement>(".orders button").forEach((button) => {
|
||||
button.addEventListener("click", () => handlers.onOrderAction(button.dataset.action ?? ""));
|
||||
});
|
||||
root.querySelectorAll<HTMLButtonElement>("[data-window-action]").forEach((button) => {
|
||||
button.addEventListener("click", () => handlers.onWindowAction(button.dataset.windowAction ?? ""));
|
||||
});
|
||||
@@ -111,7 +93,9 @@ export function createHud(container: HTMLElement, handlers: HudHandlers): HudEle
|
||||
details: root.querySelector(".content") as HTMLDivElement,
|
||||
status: root.querySelector(".mode") as HTMLDivElement,
|
||||
selectionTitle: root.querySelector(".selection-title") as HTMLHeadingElement,
|
||||
orders: root.querySelector(".orders") as HTMLDivElement,
|
||||
selectionStrip: root.querySelector(".selection-strip") as HTMLDivElement,
|
||||
orders: document.createElement("div"),
|
||||
sessionActions: root.querySelector(".session-actions") as HTMLDivElement,
|
||||
minimap,
|
||||
minimapContext,
|
||||
marquee: root.querySelector(".marquee") as HTMLDivElement,
|
||||
@@ -121,6 +105,7 @@ export function createHud(container: HTMLElement, handlers: HudHandlers): HudEle
|
||||
fleetWindowBody: fleetWindowBody as HTMLDivElement,
|
||||
fleetWindowTitle: root.querySelector(".fleet-window h2") as HTMLHeadingElement,
|
||||
fleetWindowSubtitle: root.querySelector(".window-subtitle") as HTMLParagraphElement,
|
||||
debugWindow: root.querySelector(".debug-window") as HTMLDivElement,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,27 @@ import {
|
||||
import { describeFleetOrder } from "../fleet/runtime";
|
||||
import { getShipCargoAmount } from "../state/inventory";
|
||||
import type {
|
||||
FactionInstance,
|
||||
FleetInstance,
|
||||
PlanetInstance,
|
||||
ShipInstance,
|
||||
SolarSystemInstance,
|
||||
StationInstance,
|
||||
ViewLevel,
|
||||
} from "../types";
|
||||
|
||||
export function getSelectionTitle(selection: ShipInstance[], selectedStation?: StationInstance) {
|
||||
export function getSelectionTitle(
|
||||
selection: ShipInstance[],
|
||||
selectedStation?: StationInstance,
|
||||
selectedSystem?: SolarSystemInstance,
|
||||
selectedPlanet?: PlanetInstance,
|
||||
) {
|
||||
if (selectedPlanet) {
|
||||
return selectedPlanet.definition.label;
|
||||
}
|
||||
if (selectedSystem) {
|
||||
return selectedSystem.definition.label;
|
||||
}
|
||||
if (selectedStation) {
|
||||
return selectedStation.definition.label;
|
||||
}
|
||||
@@ -26,19 +39,114 @@ export function getSelectionTitle(selection: ShipInstance[], selectedStation?: S
|
||||
return `${selection.length} Ships Selected`;
|
||||
}
|
||||
|
||||
export function getSelectionStripLabels(
|
||||
selection: ShipInstance[],
|
||||
selectedStation?: StationInstance,
|
||||
selectedSystem?: SolarSystemInstance,
|
||||
selectedPlanet?: PlanetInstance,
|
||||
) {
|
||||
if (selectedPlanet) {
|
||||
return [selectedPlanet.definition.label];
|
||||
}
|
||||
if (selectedSystem) {
|
||||
return [selectedSystem.definition.label];
|
||||
}
|
||||
if (selectedStation) {
|
||||
return [selectedStation.definition.label];
|
||||
}
|
||||
if (selection.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return selection.map((ship) => ship.definition.label);
|
||||
}
|
||||
|
||||
export function getSelectionCardsMarkup(
|
||||
selection: ShipInstance[],
|
||||
selectedStation: StationInstance | undefined,
|
||||
selectedSystem: SolarSystemInstance | undefined,
|
||||
selectedPlanet: PlanetInstance | undefined,
|
||||
) {
|
||||
if (selectedPlanet) {
|
||||
return renderCard(
|
||||
selectedPlanet.definition.label,
|
||||
[
|
||||
selectedPlanet.systemId,
|
||||
`Orbit ${Math.round(selectedPlanet.definition.orbitRadius)}`,
|
||||
`Size ${selectedPlanet.definition.size}`,
|
||||
selectedPlanet.definition.hasRing ? "Ringed" : "No ring",
|
||||
],
|
||||
);
|
||||
}
|
||||
if (selectedSystem) {
|
||||
return renderCard(
|
||||
selectedSystem.definition.label,
|
||||
[
|
||||
selectedSystem.strategicValue,
|
||||
`${selectedSystem.planets.length} planets`,
|
||||
`${selectedSystem.definition.resourceNodes.length} nodes`,
|
||||
`${selectedSystem.controllingFactionId ?? "Contested"} ${Math.round(selectedSystem.controlProgress)}%`,
|
||||
],
|
||||
);
|
||||
}
|
||||
if (selectedStation) {
|
||||
return renderCard(
|
||||
selectedStation.definition.label,
|
||||
[
|
||||
selectedStation.factionId,
|
||||
selectedStation.definition.category,
|
||||
`HP ${Math.round(selectedStation.health)}/${selectedStation.maxHealth}`,
|
||||
`Dock ${selectedStation.dockedShipIds.size}/${selectedStation.definition.dockingCapacity}`,
|
||||
],
|
||||
);
|
||||
}
|
||||
if (selection.length === 0) {
|
||||
return `<span class="selection-strip-empty">No active selection</span>`;
|
||||
}
|
||||
return selection
|
||||
.map((ship) =>
|
||||
renderCard(ship.definition.label, [
|
||||
ship.factionId,
|
||||
ship.state,
|
||||
ship.order.kind,
|
||||
`HP ${Math.round(ship.health)}/${ship.maxHealth}`,
|
||||
]),
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
export function getSelectionDetails(
|
||||
selection: ShipInstance[],
|
||||
selectedStation: StationInstance | undefined,
|
||||
selectedSystem: SolarSystemInstance | undefined,
|
||||
selectedPlanet: PlanetInstance | undefined,
|
||||
systems: SolarSystemInstance[],
|
||||
viewLevel: ViewLevel,
|
||||
ships: ShipInstance[],
|
||||
fleets: FleetInstance[],
|
||||
factions: FactionInstance[],
|
||||
) {
|
||||
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"}`;
|
||||
}
|
||||
if (selectedSystem) {
|
||||
return `${selectedSystem.definition.label}\nType: ${selectedSystem.strategicValue}\nControl: ${selectedSystem.controllingFactionId ?? "Contested"} ${Math.round(selectedSystem.controlProgress)}%\nPlanets: ${selectedSystem.planets.length}\nResource Nodes: ${selectedSystem.definition.resourceNodes.length}\nGravity Well: ${Math.round(selectedSystem.gravityWellRadius)}`;
|
||||
}
|
||||
if (selectedStation) {
|
||||
return describeStation(selectedStation, ships, fleets);
|
||||
}
|
||||
if (selection.length === 0) {
|
||||
return `Systems online: ${systems.map((system) => system.definition.label).join(", ")}\nFleets active: ${fleets.length}\n\nOrders: Move, Patrol, Escort, Mine\nView: ${viewLevel}`;
|
||||
const central = systems
|
||||
.filter((system) => system.strategicValue === "central")
|
||||
.map((system) => `${system.definition.label}: ${system.controllingFactionId ?? "contested"} ${Math.round(system.controlProgress)}%`)
|
||||
.join("\n");
|
||||
const factionLines = factions
|
||||
.filter((faction) => faction.definition.kind === "empire")
|
||||
.map(
|
||||
(faction) =>
|
||||
`${faction.definition.label}: systems ${faction.ownedSystemIds.size} • mined ${Math.round(faction.oreMined)} • built ${faction.shipsBuilt} ships • losses ${faction.shipsLost}`,
|
||||
)
|
||||
.join("\n");
|
||||
return `Observer Mode\nSystems online: ${systems.length}\nFleets tracked: ${fleets.length}\nView: ${viewLevel}\n\nCentral systems:\n${central}\n\nEmpires:\n${factionLines}`;
|
||||
}
|
||||
|
||||
return selection
|
||||
@@ -49,7 +157,7 @@ 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}\nClass: ${ship.definition.shipClass}\nState: ${ship.state}${dockedAt ? ` @ ${dockedAt}` : ""}\nOrder: ${ship.order.kind}\nFleet: ${ship.fleetId ?? "Independent"}${ship.isFleetCommander ? " • Commander" : ship.isWingLeader ? " • Wing Leader" : ""}\nBehavior: ${ship.behavior}\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(", ")}`;
|
||||
return `${ship.definition.label} • ${ship.systemId}\nFaction: ${ship.factionId}\nClass: ${ship.definition.shipClass}\nState: ${ship.state}${dockedAt ? ` @ ${dockedAt}` : ""}\nOrder: ${ship.order.kind}\nFleet: ${ship.fleetId ?? "Independent"}${ship.isFleetCommander ? " • Commander" : ship.isWingLeader ? " • Wing Leader" : ""}\nBehavior: ${ship.behavior}\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");
|
||||
@@ -88,7 +196,7 @@ export function describeStation(station: StationInstance, ships: ShipInstance[],
|
||||
? "Fabricating industrial parts and equipment"
|
||||
: "Managing local trade traffic";
|
||||
|
||||
return `${station.definition.label} • ${station.systemId}\nRole: ${station.definition.category}\nActivity: ${activity}\nLocal Fleets: ${localFleets}\nDocking: ${station.dockedShipIds.size}/${station.definition.dockingCapacity}\nFuel: ${station.fuel.toFixed(0)}/${station.maxFuel}\nEnergy: ${station.energy.toFixed(0)}/${station.maxEnergy}\nBulk Solid: ${Math.round(station.inventory["bulk-solid"])}\nContainer: ${Math.round(station.inventory.container)}\nManufactured: ${Math.round(station.inventory.manufactured)}\nModules: ${station.modules.map(getModuleLabel).join(", ")}\n${productionStatus}Radius: ${station.definition.radius}`;
|
||||
return `${station.definition.label} • ${station.systemId}\nFaction: ${station.factionId}\nRole: ${station.definition.category}\nActivity: ${activity}\nLocal Fleets: ${localFleets}\nDocking: ${station.dockedShipIds.size}/${station.definition.dockingCapacity}\nHealth: ${Math.round(station.health)}/${station.maxHealth}\nFuel: ${station.fuel.toFixed(0)}/${station.maxFuel}\nEnergy: ${station.energy.toFixed(0)}/${station.maxEnergy}\nBulk Solid: ${Math.round(station.inventory["bulk-solid"])}\nContainer: ${Math.round(station.inventory.container)}\nManufactured: ${Math.round(station.inventory.manufactured)}\nModules: ${station.modules.map(getModuleLabel).join(", ")}\n${productionStatus}Radius: ${station.definition.radius}`;
|
||||
}
|
||||
|
||||
export function getFleetWindowMarkup(
|
||||
@@ -178,6 +286,15 @@ function describeShipNode(ship: ShipInstance): string {
|
||||
return `${ship.definition.shipClass} • ${ship.state} • ${ship.order.kind} • ${ship.behavior}`;
|
||||
}
|
||||
|
||||
function renderCard(title: string, lines: string[]) {
|
||||
return `
|
||||
<article class="selection-strip-card">
|
||||
<span class="selection-strip-card-title">${title}</span>
|
||||
${lines.map((line) => `<span class="selection-strip-card-line">${line}</span>`).join("")}
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
function collectWingShipIds(fleet: FleetInstance, rootWingId: string): string[] {
|
||||
const wingIds = new Set<string>([rootWingId]);
|
||||
let changed = true;
|
||||
|
||||
@@ -132,25 +132,21 @@ export function drawStrategicOverlay({
|
||||
context.textBaseline = "middle";
|
||||
|
||||
if (viewLevel === "solar") {
|
||||
stations
|
||||
.filter((station) => station.systemId === systems[selectedSystemIndex]?.definition.id)
|
||||
.forEach((station) => {
|
||||
const screen = projectWorldToScreen(station.group.position, camera);
|
||||
if (screen) {
|
||||
drawStationSymbol(context, screen.x, screen.y, station, 14, station === selectedStation);
|
||||
}
|
||||
});
|
||||
|
||||
ships
|
||||
.filter((ship) => ship.systemId === systems[selectedSystemIndex]?.definition.id && ship.state !== "docked")
|
||||
.forEach((ship) => {
|
||||
const screen = projectWorldToScreen(ship.group.position, camera);
|
||||
if (screen) {
|
||||
drawShipSymbol(context, screen.x, screen.y, ship, 10, selection.includes(ship));
|
||||
}
|
||||
});
|
||||
|
||||
drawFleetLinks(context, camera, fleets, ships, systems[selectedSystemIndex]?.definition.id, activeFleetId);
|
||||
|
||||
selection.forEach((ship) => {
|
||||
const screen = projectWorldToScreen(ship.group.position, camera);
|
||||
if (screen) {
|
||||
drawShipSymbol(context, screen.x, screen.y, ship, 10, true);
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedStation) {
|
||||
const screen = projectWorldToScreen(selectedStation.group.position, camera);
|
||||
if (screen) {
|
||||
drawStationSymbol(context, screen.x, screen.y, selectedStation, 14, true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
systems.forEach((system) => {
|
||||
const screen = projectWorldToScreen(system.center, camera);
|
||||
|
||||
353
src/game/world/universeGenerator.ts
Normal file
353
src/game/world/universeGenerator.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
import { solarSystemDefinitions } from "../data/catalog";
|
||||
import type {
|
||||
AsteroidFieldDefinition,
|
||||
FactionDefinition,
|
||||
PatrolRouteDefinition,
|
||||
PlanetDefinition,
|
||||
ResourceNodeDefinition,
|
||||
ScenarioDefinition,
|
||||
SolarSystemDefinition,
|
||||
UniverseDefinition,
|
||||
} from "../types";
|
||||
|
||||
const TOTAL_SYSTEMS = 28;
|
||||
const STAR_PALETTES = [
|
||||
{ starColor: "#ffd27a", starGlow: "#ffb14a" },
|
||||
{ starColor: "#9dc6ff", starGlow: "#66a0ff" },
|
||||
{ starColor: "#ffb7a1", starGlow: "#ff7d66" },
|
||||
{ starColor: "#f3f0ff", starGlow: "#b49cff" },
|
||||
{ starColor: "#b6ffe0", starGlow: "#5ed6b1" },
|
||||
{ starColor: "#ffe49a", starGlow: "#ffc14a" },
|
||||
];
|
||||
const PLANET_COLORS = ["#d4a373", "#58a36c", "#6ea7d4", "#6958a8", "#c48f6a", "#4f84c4", "#8f8fb0", "#d46e8a"];
|
||||
const FRONTIER_PREFIXES = ["Aquila", "Draco", "Lyra", "Cygnus", "Orion", "Vela", "Carina", "Pavo", "Vesper", "Altair"];
|
||||
const FRONTIER_SUFFIXES = ["Reach", "Gate", "Crown", "Run", "March", "Drift", "Anchor", "Span", "Wake", "Vale"];
|
||||
const EMPIRE_ARCHETYPES = [
|
||||
{ id: "solar-dominion", label: "Solar Dominion", color: "#f0c36d", accent: "#ffefb0" },
|
||||
{ id: "aegis-state", label: "Aegis State", color: "#72b7ff", accent: "#d5ecff" },
|
||||
{ id: "verdant-combine", label: "Verdant Combine", color: "#77dd8c", accent: "#d7ffe2" },
|
||||
{ id: "iron-clans", label: "Iron Clans", color: "#ff926c", accent: "#ffd8c9" },
|
||||
];
|
||||
const PIRATE_ARCHETYPES = [
|
||||
{ id: "black-flag", label: "Black Flag Cartel", color: "#ff5a6f", accent: "#ffd0d6" },
|
||||
{ id: "void-rats", label: "Void Rats", color: "#9a7cff", accent: "#e7dcff" },
|
||||
{ id: "grim-sons", label: "Grim Sons", color: "#ff8d54", accent: "#ffe1d1" },
|
||||
{ id: "night-jackals", label: "Night Jackals", color: "#a0ff7f", accent: "#e8ffd8" },
|
||||
{ id: "red-knives", label: "Red Knives", color: "#ff6a8c", accent: "#ffd7e2" },
|
||||
{ id: "dust-serpents", label: "Dust Serpents", color: "#c2a56f", accent: "#f0e1c3" },
|
||||
];
|
||||
|
||||
export function generateUniverse(seed = Math.floor(Math.random() * 0x7fffffff)): UniverseDefinition {
|
||||
const rng = createRng(seed);
|
||||
const systems: SolarSystemDefinition[] = [];
|
||||
const empires: FactionDefinition[] = [];
|
||||
const pirates: FactionDefinition[] = [];
|
||||
|
||||
const centralSystems = Array.from({ length: 3 }, (_, index) => createCentralSystem(index, rng));
|
||||
systems.push(...centralSystems);
|
||||
|
||||
EMPIRE_ARCHETYPES.forEach((archetype, index) => {
|
||||
const angle = (index / EMPIRE_ARCHETYPES.length) * Math.PI * 2;
|
||||
const capitalSystem = createEmpireCapitalSystem(archetype.label, archetype.id, angle, rng);
|
||||
const miningSystem = createEmpireMiningSystem(archetype.label, archetype.id, angle + 0.22, rng);
|
||||
systems.push(capitalSystem, miningSystem);
|
||||
empires.push({
|
||||
id: archetype.id,
|
||||
label: archetype.label,
|
||||
kind: "empire",
|
||||
color: archetype.color,
|
||||
accent: archetype.accent,
|
||||
homeSystemId: capitalSystem.id,
|
||||
miningSystemId: miningSystem.id,
|
||||
targetSystemIds: centralSystems.map((system) => system.id),
|
||||
rivals: EMPIRE_ARCHETYPES.filter((_, rivalIndex) => rivalIndex !== index).map((rival) => rival.id),
|
||||
});
|
||||
});
|
||||
|
||||
PIRATE_ARCHETYPES.forEach((archetype, index) => {
|
||||
const targetEmpire = empires[index % empires.length];
|
||||
const secondaryEmpire = empires[(index + 1) % empires.length];
|
||||
const pirateSystem = createPirateBaseSystem(archetype.label, archetype.id, index, rng);
|
||||
systems.push(pirateSystem);
|
||||
pirates.push({
|
||||
id: archetype.id,
|
||||
label: archetype.label,
|
||||
kind: "pirate",
|
||||
color: archetype.color,
|
||||
accent: archetype.accent,
|
||||
homeSystemId: pirateSystem.id,
|
||||
targetSystemIds: [targetEmpire.homeSystemId, targetEmpire.miningSystemId ?? targetEmpire.homeSystemId],
|
||||
rivals: [targetEmpire.id, secondaryEmpire.id],
|
||||
pirateForFactionId: targetEmpire.id,
|
||||
});
|
||||
});
|
||||
|
||||
while (systems.length < TOTAL_SYSTEMS) {
|
||||
systems.push(createFrontierSystem(systems.length, rng));
|
||||
}
|
||||
|
||||
const factions = [...empires, ...pirates];
|
||||
empires.forEach((empire, index) => {
|
||||
empire.rivals.push(
|
||||
pirates[index].id,
|
||||
pirates[(index + 4) % pirates.length].id,
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
seed,
|
||||
label: `Autonomous Cluster ${seed.toString(16).toUpperCase()}`,
|
||||
systems,
|
||||
scenario: createScenario(systems, factions),
|
||||
};
|
||||
}
|
||||
|
||||
function createScenario(systems: SolarSystemDefinition[], factions: FactionDefinition[]): ScenarioDefinition {
|
||||
const empires = factions.filter((faction) => faction.kind === "empire");
|
||||
const pirates = factions.filter((faction) => faction.kind === "pirate");
|
||||
const initialStations: ScenarioDefinition["initialStations"] = [];
|
||||
const shipFormations: ScenarioDefinition["shipFormations"] = [];
|
||||
const patrolRoutes: PatrolRouteDefinition[] = [];
|
||||
const centralSystemIds = systems.filter((system) => system.id.startsWith("central-")).map((system) => system.id);
|
||||
|
||||
empires.forEach((faction) => {
|
||||
const capital = systems.find((system) => system.id === faction.homeSystemId);
|
||||
const mining = systems.find((system) => system.id === faction.miningSystemId);
|
||||
if (!capital || !mining) {
|
||||
return;
|
||||
}
|
||||
|
||||
initialStations.push(
|
||||
{ constructibleId: "trade-hub", systemId: capital.id, factionId: faction.id, planetIndex: 1, lagrangeSide: 1 },
|
||||
{
|
||||
constructibleId: "farm-ring",
|
||||
systemId: capital.id,
|
||||
factionId: faction.id,
|
||||
planetIndex: 0,
|
||||
lagrangeSide: -1,
|
||||
seedStock: { gas: 120, water: 160 },
|
||||
},
|
||||
{
|
||||
constructibleId: "manufactory",
|
||||
systemId: capital.id,
|
||||
factionId: faction.id,
|
||||
planetIndex: Math.min(2, capital.planets.length - 1),
|
||||
lagrangeSide: 1,
|
||||
seedStock: { "refined-metals": 200, water: 100, "ship-equipment": 40, "naval-guns": 24 },
|
||||
},
|
||||
{
|
||||
constructibleId: "shipyard",
|
||||
systemId: capital.id,
|
||||
factionId: faction.id,
|
||||
planetIndex: Math.min(3, capital.planets.length - 1),
|
||||
lagrangeSide: -1,
|
||||
seedStock: { "ship-parts": 80, "ammo-crates": 70, "hull-sections": 100, "ship-equipment": 40 },
|
||||
},
|
||||
{ constructibleId: "defense-grid", systemId: capital.id, factionId: faction.id, planetIndex: 1, lagrangeSide: -1 },
|
||||
{
|
||||
constructibleId: "refinery",
|
||||
systemId: mining.id,
|
||||
factionId: faction.id,
|
||||
planetIndex: 0,
|
||||
lagrangeSide: 1,
|
||||
seedStock: { ore: 240, "refined-metals": 80 },
|
||||
},
|
||||
{ constructibleId: "defense-grid", systemId: mining.id, factionId: faction.id, planetIndex: 1, lagrangeSide: -1 },
|
||||
);
|
||||
|
||||
shipFormations.push(
|
||||
{ shipId: "frigate", count: 1, center: localPoint(capital, 180, 120), systemId: capital.id, factionId: faction.id },
|
||||
{ shipId: "hauler", count: 1, center: localPoint(capital, 280, -120), systemId: capital.id, factionId: faction.id },
|
||||
{ shipId: "miner", count: 1, center: localPoint(mining, 180, 100), systemId: mining.id, factionId: faction.id },
|
||||
);
|
||||
|
||||
patrolRoutes.push(createPatrolRoute(capital), createPatrolRoute(mining));
|
||||
});
|
||||
|
||||
pirates.forEach((faction) => {
|
||||
const base = systems.find((system) => system.id === faction.homeSystemId);
|
||||
if (!base) {
|
||||
return;
|
||||
}
|
||||
initialStations.push(
|
||||
{
|
||||
constructibleId: "trade-hub",
|
||||
systemId: base.id,
|
||||
factionId: faction.id,
|
||||
planetIndex: 0,
|
||||
lagrangeSide: 1,
|
||||
seedStock: { "refined-metals": 100, "ship-parts": 30, "ammo-crates": 30 },
|
||||
},
|
||||
{ constructibleId: "defense-grid", systemId: base.id, factionId: faction.id, planetIndex: 1, lagrangeSide: -1 },
|
||||
);
|
||||
shipFormations.push(
|
||||
{ shipId: "frigate", count: 4, center: localPoint(base, 180, 60), systemId: base.id, factionId: faction.id },
|
||||
{ shipId: "destroyer", count: 2, center: localPoint(base, 250, 120), systemId: base.id, factionId: faction.id },
|
||||
{ shipId: "hauler", count: 1, center: localPoint(base, 320, -90), systemId: base.id, factionId: faction.id },
|
||||
);
|
||||
patrolRoutes.push(createPatrolRoute(base));
|
||||
});
|
||||
|
||||
const firstEmpire = empires[0];
|
||||
return {
|
||||
initialStations,
|
||||
shipFormations,
|
||||
patrolRoutes,
|
||||
miningDefaults: {
|
||||
nodeSystemId: firstEmpire.miningSystemId ?? firstEmpire.homeSystemId,
|
||||
refinerySystemId: firstEmpire.homeSystemId,
|
||||
},
|
||||
factions,
|
||||
centralSystemIds,
|
||||
};
|
||||
}
|
||||
|
||||
function createEmpireCapitalSystem(label: string, factionId: string, angle: number, rng: () => number): SolarSystemDefinition {
|
||||
const base = solarSystemDefinitions[0];
|
||||
const radius = 6200 + Math.floor(rng() * 700);
|
||||
return {
|
||||
...base,
|
||||
id: `${factionId}-capital`,
|
||||
label: `${label} Prime`,
|
||||
position: [Math.round(Math.cos(angle) * radius), 0, Math.round(Math.sin(angle) * radius)],
|
||||
starSize: 48 + Math.floor(rng() * 10),
|
||||
gravityWellRadius: 210 + Math.floor(rng() * 18),
|
||||
asteroidField: createAsteroidFieldDefinition(rng, false),
|
||||
resourceNodes: [],
|
||||
planets: createPlanets(4 + Math.floor(rng() * 2), rng),
|
||||
};
|
||||
}
|
||||
|
||||
function createEmpireMiningSystem(label: string, factionId: string, angle: number, rng: () => number): SolarSystemDefinition {
|
||||
const base = solarSystemDefinitions[1];
|
||||
const radius = 7700 + Math.floor(rng() * 900);
|
||||
return {
|
||||
...base,
|
||||
id: `${factionId}-belt`,
|
||||
label: `${label} Belt`,
|
||||
position: [Math.round(Math.cos(angle) * radius), 0, Math.round(Math.sin(angle) * radius)],
|
||||
starSize: 42 + Math.floor(rng() * 10),
|
||||
gravityWellRadius: 220 + Math.floor(rng() * 20),
|
||||
asteroidField: createAsteroidFieldDefinition(rng, true),
|
||||
resourceNodes: createResourceNodes(4 + Math.floor(rng() * 2), rng, 3600, 5200),
|
||||
planets: createPlanets(3 + Math.floor(rng() * 2), rng),
|
||||
};
|
||||
}
|
||||
|
||||
function createCentralSystem(index: number, rng: () => number): SolarSystemDefinition {
|
||||
const palette = STAR_PALETTES[(index + 1) % STAR_PALETTES.length];
|
||||
const angle = (index / 3) * Math.PI * 2 + rng() * 0.3;
|
||||
const radius = 900 + Math.floor(rng() * 500);
|
||||
return {
|
||||
id: `central-${index + 1}`,
|
||||
label: ["Crown Basin", "Throne Verge", "Golden Axis"][index] ?? `Central ${index + 1}`,
|
||||
position: [Math.round(Math.cos(angle) * radius), 0, Math.round(Math.sin(angle) * radius)],
|
||||
starColor: palette.starColor,
|
||||
starGlow: palette.starGlow,
|
||||
starSize: 50 + Math.floor(rng() * 14),
|
||||
gravityWellRadius: 240 + Math.floor(rng() * 28),
|
||||
asteroidField: createAsteroidFieldDefinition(rng, true),
|
||||
resourceNodes: createResourceNodes(6 + Math.floor(rng() * 3), rng, 5200, 7600),
|
||||
planets: createPlanets(4 + Math.floor(rng() * 2), rng),
|
||||
};
|
||||
}
|
||||
|
||||
function createPirateBaseSystem(label: string, factionId: string, index: number, rng: () => number): SolarSystemDefinition {
|
||||
const palette = STAR_PALETTES[(index + 3) % STAR_PALETTES.length];
|
||||
const angle = (index / PIRATE_ARCHETYPES.length) * Math.PI * 2 + 0.35;
|
||||
const radius = 9800 + Math.floor(rng() * 1200);
|
||||
return {
|
||||
id: `${factionId}-den`,
|
||||
label: `${label} Den`,
|
||||
position: [Math.round(Math.cos(angle) * radius), 0, Math.round(Math.sin(angle) * radius)],
|
||||
starColor: palette.starColor,
|
||||
starGlow: palette.starGlow,
|
||||
starSize: 36 + Math.floor(rng() * 10),
|
||||
gravityWellRadius: 180 + Math.floor(rng() * 30),
|
||||
asteroidField: createAsteroidFieldDefinition(rng, true),
|
||||
resourceNodes: createResourceNodes(2 + Math.floor(rng() * 2), rng, 1600, 2600),
|
||||
planets: createPlanets(2 + Math.floor(rng() * 2), rng),
|
||||
};
|
||||
}
|
||||
|
||||
function createFrontierSystem(index: number, rng: () => number): SolarSystemDefinition {
|
||||
const angle = index * 2.399963229728653 + rng() * 0.4;
|
||||
const radius = 3600 + 900 * Math.sqrt(index) + rng() * 600;
|
||||
const palette = STAR_PALETTES[Math.floor(rng() * STAR_PALETTES.length)];
|
||||
const hasResources = rng() > 0.45;
|
||||
return {
|
||||
id: `frontier-${index + 1}`,
|
||||
label: `${FRONTIER_PREFIXES[index % FRONTIER_PREFIXES.length]} ${FRONTIER_SUFFIXES[Math.floor(rng() * FRONTIER_SUFFIXES.length)]}`,
|
||||
position: [Math.round(Math.cos(angle) * radius), 0, Math.round(Math.sin(angle) * radius)],
|
||||
starColor: palette.starColor,
|
||||
starGlow: palette.starGlow,
|
||||
starSize: 34 + Math.round(rng() * 18),
|
||||
gravityWellRadius: 185 + Math.round(rng() * 60),
|
||||
asteroidField: createAsteroidFieldDefinition(rng, hasResources),
|
||||
resourceNodes: hasResources ? createResourceNodes(1 + Math.floor(rng() * 3), rng, 1800, 3400) : [],
|
||||
planets: createPlanets(2 + Math.floor(rng() * 3), rng),
|
||||
};
|
||||
}
|
||||
|
||||
function createAsteroidFieldDefinition(rng: () => number, dense: boolean): AsteroidFieldDefinition {
|
||||
return {
|
||||
decorationCount: dense ? 180 + Math.floor(rng() * 70) : 90 + Math.floor(rng() * 70),
|
||||
radiusOffset: 290 + Math.floor(rng() * 100),
|
||||
radiusVariance: 70 + Math.floor(rng() * 80),
|
||||
heightVariance: 12 + Math.floor(rng() * 12),
|
||||
};
|
||||
}
|
||||
|
||||
function createPlanets(count: number, rng: () => number): PlanetDefinition[] {
|
||||
const planets: PlanetDefinition[] = [];
|
||||
let orbitRadius = 150 + Math.floor(rng() * 40);
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
orbitRadius += 120 + Math.floor(rng() * 90);
|
||||
planets.push({
|
||||
label: `${String.fromCharCode(65 + index)}-${Math.floor(rng() * 90 + 10)}`,
|
||||
orbitRadius,
|
||||
orbitSpeed: Number((0.05 + rng() * 0.14).toFixed(3)),
|
||||
size: 18 + Math.floor(rng() * 30),
|
||||
color: PLANET_COLORS[Math.floor(rng() * PLANET_COLORS.length)],
|
||||
tilt: Number(((rng() - 0.5) * 0.8).toFixed(2)),
|
||||
hasRing: rng() > 0.72,
|
||||
});
|
||||
}
|
||||
return planets;
|
||||
}
|
||||
|
||||
function createResourceNodes(count: number, rng: () => number, minOre: number, maxOre: number): ResourceNodeDefinition[] {
|
||||
return Array.from({ length: count }, (_, index) => ({
|
||||
angle: Number((((index / count) * Math.PI * 2 + rng() * 0.7) % (Math.PI * 2)).toFixed(6)),
|
||||
radiusOffset: 300 + Math.floor(rng() * 140),
|
||||
oreAmount: minOre + Math.floor(rng() * (maxOre - minOre)),
|
||||
itemId: "ore",
|
||||
shardCount: 5 + Math.floor(rng() * 5),
|
||||
}));
|
||||
}
|
||||
|
||||
function createPatrolRoute(system: SolarSystemDefinition): PatrolRouteDefinition {
|
||||
return {
|
||||
systemId: system.id,
|
||||
points: [
|
||||
localPoint(system, 160, 90),
|
||||
localPoint(system, 340, -180),
|
||||
localPoint(system, 560, 210),
|
||||
localPoint(system, 240, 340),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function localPoint(system: SolarSystemDefinition, x: number, z: number): [number, number, number] {
|
||||
return [system.position[0] + x, 0, system.position[2] + z];
|
||||
}
|
||||
|
||||
function createRng(seed: number) {
|
||||
let value = seed >>> 0;
|
||||
return () => {
|
||||
value += 0x6d2b79f5;
|
||||
let result = Math.imul(value ^ (value >>> 15), 1 | value);
|
||||
result ^= result + Math.imul(result ^ (result >>> 7), 61 | result);
|
||||
return ((result ^ (result >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
@@ -2,14 +2,13 @@ import * as THREE from "three";
|
||||
import {
|
||||
constructibleDefinitionsById,
|
||||
gameBalance,
|
||||
scenarioDefinition,
|
||||
shipDefinitionsById,
|
||||
solarSystemDefinitions,
|
||||
} from "../data/catalog";
|
||||
import { createEmptyInventory } from "../state/inventory";
|
||||
import type {
|
||||
ConstructibleDefinition,
|
||||
ResourceNode,
|
||||
ScenarioDefinition,
|
||||
SelectableTarget,
|
||||
ShipDefinition,
|
||||
ShipInstance,
|
||||
@@ -31,6 +30,8 @@ interface BuildWorldResult {
|
||||
export function buildInitialWorld(
|
||||
scene: THREE.Scene,
|
||||
selectableTargets: Map<THREE.Object3D, SelectableTarget>,
|
||||
systemsDefinition: SolarSystemDefinition[],
|
||||
scenarioDefinition: ScenarioDefinition,
|
||||
): BuildWorldResult {
|
||||
const systems: SolarSystemInstance[] = [];
|
||||
const nodes: ResourceNode[] = [];
|
||||
@@ -41,6 +42,9 @@ export function buildInitialWorld(
|
||||
let shipId = 0;
|
||||
let stationId = 0;
|
||||
let nodeId = 0;
|
||||
const factionColors = new Map(
|
||||
(scenarioDefinition.factions ?? []).map((faction) => [faction.id, faction.color]),
|
||||
);
|
||||
|
||||
scene.add(new THREE.HemisphereLight(0x6ba6ff, 0x03050a, 0.38));
|
||||
scene.add(new THREE.AmbientLight(0x8397b8, 0.28));
|
||||
@@ -49,11 +53,11 @@ export function buildInitialWorld(
|
||||
createNebulae(scene);
|
||||
const starfield = createStarfield(scene);
|
||||
|
||||
solarSystemDefinitions.forEach((definition) => {
|
||||
systemsDefinition.forEach((definition) => {
|
||||
systems.push(createSolarSystem(scene, definition, nodes, () => {
|
||||
nodeId += 1;
|
||||
return `node-${nodeId}`;
|
||||
}));
|
||||
}, selectableTargets));
|
||||
});
|
||||
|
||||
createStrategicLinks(strategicLinks, systems);
|
||||
@@ -70,8 +74,11 @@ export function buildInitialWorld(
|
||||
definition,
|
||||
systemId: plan.systemId,
|
||||
position: plan.position ? new THREE.Vector3(...plan.position) : new THREE.Vector3(),
|
||||
factionId: plan.factionId ?? "neutral",
|
||||
factionColor: factionColors.get(plan.factionId ?? "") ?? "#b4c9da",
|
||||
planetIndex: plan.planetIndex,
|
||||
lagrangeSide: plan.lagrangeSide,
|
||||
seedStock: plan.seedStock,
|
||||
selectableTargets,
|
||||
}),
|
||||
);
|
||||
@@ -83,10 +90,12 @@ export function buildInitialWorld(
|
||||
throw new Error(`Missing ship definition ${plan.shipId}`);
|
||||
}
|
||||
for (let i = 0; i < plan.count; i += 1) {
|
||||
const ship = createShip({
|
||||
const ship = createShipInstance({
|
||||
id: `ship-${++shipId}`,
|
||||
definition,
|
||||
systemId: plan.systemId,
|
||||
factionId: plan.factionId ?? "neutral",
|
||||
factionColor: factionColors.get(plan.factionId ?? "") ?? definition.color,
|
||||
selectableTargets,
|
||||
});
|
||||
ship.group.position
|
||||
@@ -110,6 +119,7 @@ function createSolarSystem(
|
||||
definition: SolarSystemDefinition,
|
||||
nodes: ResourceNode[],
|
||||
nextNodeId: () => string,
|
||||
selectableTargets?: Map<THREE.Object3D, SelectableTarget>,
|
||||
) {
|
||||
const root = new THREE.Group();
|
||||
root.position.set(...definition.position);
|
||||
@@ -155,6 +165,19 @@ function createSolarSystem(
|
||||
planet.receiveShadow = true;
|
||||
orbitRoot.add(planet);
|
||||
|
||||
const selectionRing = new THREE.Mesh(
|
||||
new THREE.RingGeometry(planetDefinition.size * 1.35, planetDefinition.size * 1.55, 40),
|
||||
new THREE.MeshBasicMaterial({
|
||||
color: 0xf5e8a5,
|
||||
transparent: true,
|
||||
opacity: 0,
|
||||
side: THREE.DoubleSide,
|
||||
}),
|
||||
);
|
||||
selectionRing.rotation.x = -Math.PI / 2;
|
||||
selectionRing.position.x = planetDefinition.orbitRadius;
|
||||
orbitRoot.add(selectionRing);
|
||||
|
||||
let ringObject: THREE.Object3D | undefined;
|
||||
if (planetDefinition.hasRing) {
|
||||
const ring = new THREE.Mesh(
|
||||
@@ -173,7 +196,16 @@ function createSolarSystem(
|
||||
}
|
||||
|
||||
root.add(orbitRoot);
|
||||
return { group: orbitRoot, mesh: planet, orbitSpeed: planetDefinition.orbitSpeed, ring: ringObject };
|
||||
return {
|
||||
definition: planetDefinition,
|
||||
group: orbitRoot,
|
||||
mesh: planet,
|
||||
orbitSpeed: planetDefinition.orbitSpeed,
|
||||
ring: ringObject,
|
||||
selectionRing,
|
||||
systemId: definition.id,
|
||||
index,
|
||||
};
|
||||
});
|
||||
|
||||
const orbitLines = definition.planets.map((planetDefinition) => {
|
||||
@@ -196,8 +228,7 @@ function createSolarSystem(
|
||||
|
||||
const asteroidDecorations = createAsteroidField(definition, root, nodes, nextNodeId);
|
||||
const strategicMarker = createStrategicMarker(scene, definition);
|
||||
|
||||
return {
|
||||
const system = {
|
||||
definition,
|
||||
root,
|
||||
center: new THREE.Vector3(...definition.position),
|
||||
@@ -207,7 +238,23 @@ function createSolarSystem(
|
||||
orbitLines,
|
||||
asteroidDecorations,
|
||||
strategicMarker,
|
||||
controlProgress: 0,
|
||||
strategicValue: "frontier" as const,
|
||||
};
|
||||
|
||||
if (selectableTargets) {
|
||||
selectableTargets.set(star, { kind: "system", system });
|
||||
selectableTargets.set(glow, { kind: "system", system });
|
||||
selectableTargets.set(strategicMarker, { kind: "system", system });
|
||||
planets.forEach((planet) => {
|
||||
selectableTargets.set(planet.mesh, { kind: "planet", system, planet });
|
||||
if (planet.ring) {
|
||||
selectableTargets.set(planet.ring, { kind: "planet", system, planet });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return system;
|
||||
}
|
||||
|
||||
function createAsteroidField(
|
||||
@@ -314,18 +361,35 @@ function createStrategicLinks(strategicLinks: THREE.Group, systems: SolarSystemI
|
||||
if (systems.length < 2) {
|
||||
return;
|
||||
}
|
||||
const line = new THREE.Line(
|
||||
new THREE.BufferGeometry().setFromPoints(systems.map((system) => system.center)),
|
||||
new THREE.LineDashedMaterial({
|
||||
color: 0x5e8fbe,
|
||||
dashSize: 120,
|
||||
gapSize: 80,
|
||||
transparent: true,
|
||||
opacity: 0.5,
|
||||
}),
|
||||
);
|
||||
line.computeLineDistances();
|
||||
strategicLinks.add(line);
|
||||
const material = new THREE.LineDashedMaterial({
|
||||
color: 0x5e8fbe,
|
||||
dashSize: 120,
|
||||
gapSize: 80,
|
||||
transparent: true,
|
||||
opacity: 0.5,
|
||||
});
|
||||
const links = new Set<string>();
|
||||
|
||||
systems.forEach((system) => {
|
||||
systems
|
||||
.filter((candidate) => candidate.definition.id !== system.definition.id)
|
||||
.sort((left, right) => system.center.distanceToSquared(left.center) - system.center.distanceToSquared(right.center))
|
||||
.slice(0, 2)
|
||||
.forEach((neighbor) => {
|
||||
const key = [system.definition.id, neighbor.definition.id].sort().join(":");
|
||||
if (links.has(key)) {
|
||||
return;
|
||||
}
|
||||
links.add(key);
|
||||
const line = new THREE.Line(
|
||||
new THREE.BufferGeometry().setFromPoints([system.center, neighbor.center]),
|
||||
material,
|
||||
);
|
||||
line.computeLineDistances();
|
||||
strategicLinks.add(line);
|
||||
});
|
||||
});
|
||||
|
||||
strategicLinks.visible = false;
|
||||
}
|
||||
|
||||
@@ -335,8 +399,11 @@ export function createStationInstance({
|
||||
definition,
|
||||
systemId,
|
||||
position,
|
||||
factionId,
|
||||
factionColor,
|
||||
planetIndex,
|
||||
lagrangeSide,
|
||||
seedStock,
|
||||
selectableTargets,
|
||||
}: {
|
||||
id: string;
|
||||
@@ -344,8 +411,11 @@ export function createStationInstance({
|
||||
definition: ConstructibleDefinition;
|
||||
systemId: string;
|
||||
position: THREE.Vector3;
|
||||
factionId: string;
|
||||
factionColor: string;
|
||||
planetIndex?: number;
|
||||
lagrangeSide?: -1 | 1;
|
||||
seedStock?: Partial<Record<string, number>>;
|
||||
selectableTargets: Map<THREE.Object3D, SelectableTarget>;
|
||||
}) {
|
||||
const group = new THREE.Group();
|
||||
@@ -355,7 +425,7 @@ export function createStationInstance({
|
||||
new THREE.CylinderGeometry(definition.radius * 0.4, definition.radius * 0.6, definition.radius * 1.2, 8),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: definition.color,
|
||||
emissive: new THREE.Color(definition.color).multiplyScalar(0.12),
|
||||
emissive: new THREE.Color(factionColor).multiplyScalar(0.12),
|
||||
roughness: 0.55,
|
||||
metalness: 0.45,
|
||||
}),
|
||||
@@ -369,7 +439,7 @@ export function createStationInstance({
|
||||
new THREE.TorusGeometry(definition.radius, Math.max(2.4, definition.radius * 0.08), 18, 48),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: 0xcdd8e5,
|
||||
emissive: new THREE.Color(definition.color).multiplyScalar(0.05),
|
||||
emissive: new THREE.Color(factionColor).multiplyScalar(0.05),
|
||||
roughness: 0.4,
|
||||
metalness: 0.7,
|
||||
}),
|
||||
@@ -380,7 +450,7 @@ export function createStationInstance({
|
||||
const selectionRing = new THREE.Mesh(
|
||||
new THREE.RingGeometry(definition.radius * 1.3, definition.radius * 1.5, 40),
|
||||
new THREE.MeshBasicMaterial({
|
||||
color: definition.color,
|
||||
color: factionColor,
|
||||
transparent: true,
|
||||
opacity: 0,
|
||||
side: THREE.DoubleSide,
|
||||
@@ -399,7 +469,7 @@ export function createStationInstance({
|
||||
);
|
||||
const beacon = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(5, 2, 9),
|
||||
new THREE.MeshBasicMaterial({ color: definition.color, transparent: true, opacity: 0.75 }),
|
||||
new THREE.MeshBasicMaterial({ color: factionColor, transparent: true, opacity: 0.75 }),
|
||||
);
|
||||
beacon.position.copy(port);
|
||||
beacon.lookAt(new THREE.Vector3(0, gameBalance.yPlane, 0));
|
||||
@@ -432,31 +502,62 @@ export function createStationInstance({
|
||||
processTimer: 0,
|
||||
activeBatch: 0,
|
||||
inventory: createEmptyInventory(),
|
||||
itemStocks: {},
|
||||
itemStocks: Object.fromEntries(
|
||||
Object.entries(seedStock ?? {}).map(([itemId, amount]) => [itemId, amount ?? 0]),
|
||||
),
|
||||
dockedShipIds: new Set(),
|
||||
dockingPorts,
|
||||
modules: definition.modules,
|
||||
orbitalParentPlanetIndex: planetIndex,
|
||||
lagrangeSide,
|
||||
factionId,
|
||||
factionColor,
|
||||
health: definition.radius * 160,
|
||||
maxHealth: definition.radius * 160,
|
||||
weaponRange: definition.category === "defense" ? 280 : definition.category === "shipyard" ? 180 : 0,
|
||||
weaponDamage: definition.category === "defense" ? 22 : definition.category === "shipyard" ? 8 : 0,
|
||||
weaponTimer: 0,
|
||||
fuel: 800,
|
||||
energy: 1200,
|
||||
maxFuel: 800,
|
||||
maxEnergy: 1200,
|
||||
};
|
||||
Object.entries(seedStock ?? {}).forEach(([itemId, rawAmount]) => {
|
||||
const amount = rawAmount ?? 0;
|
||||
if (itemId === "ore") {
|
||||
station.oreStored += amount;
|
||||
station.inventory["bulk-solid"] += amount;
|
||||
} else if (itemId === "water") {
|
||||
station.inventory["bulk-liquid"] += amount;
|
||||
} else if (itemId === "gas") {
|
||||
station.inventory["bulk-gas"] += amount;
|
||||
} else if (itemId === "refined-metals") {
|
||||
station.refinedStock += amount;
|
||||
station.inventory.manufactured += amount;
|
||||
} else if (itemId === "ammo-crates" || itemId === "ship-equipment" || itemId === "drone-parts") {
|
||||
station.inventory.container += amount;
|
||||
} else {
|
||||
station.inventory.manufactured += amount;
|
||||
}
|
||||
});
|
||||
selectableTargets.set(core, { kind: "station", station });
|
||||
selectableTargets.set(ring, { kind: "station", station });
|
||||
return station;
|
||||
}
|
||||
|
||||
function createShip({
|
||||
export function createShipInstance({
|
||||
id,
|
||||
definition,
|
||||
systemId,
|
||||
factionId,
|
||||
factionColor,
|
||||
selectableTargets,
|
||||
}: {
|
||||
id: string;
|
||||
definition: ShipDefinition;
|
||||
systemId: string;
|
||||
factionId: string;
|
||||
factionColor: string;
|
||||
selectableTargets: Map<THREE.Object3D, SelectableTarget>;
|
||||
}) {
|
||||
const group = new THREE.Group();
|
||||
@@ -470,7 +571,7 @@ function createShip({
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
const streak = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(0.12, 0.5, definition.size * 8, 8),
|
||||
new THREE.MeshBasicMaterial({ color: definition.color, transparent: true, opacity: 0.22 }),
|
||||
new THREE.MeshBasicMaterial({ color: factionColor, transparent: true, opacity: 0.22 }),
|
||||
);
|
||||
streak.rotation.z = Math.PI / 2;
|
||||
streak.position.set(-definition.size * (2 + i * 1.7), (i - 2) * 0.45, 0);
|
||||
@@ -480,7 +581,7 @@ function createShip({
|
||||
|
||||
const bodyMaterial = new THREE.MeshStandardMaterial({
|
||||
color: definition.hullColor,
|
||||
emissive: new THREE.Color(definition.color).multiplyScalar(0.08),
|
||||
emissive: new THREE.Color(factionColor).multiplyScalar(0.08),
|
||||
roughness: 0.45,
|
||||
metalness: 0.7,
|
||||
});
|
||||
@@ -496,8 +597,8 @@ function createShip({
|
||||
const nose = new THREE.Mesh(
|
||||
new THREE.ConeGeometry(definition.size * 0.7, definition.size * 1.8, 6),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: definition.color,
|
||||
emissive: new THREE.Color(definition.color).multiplyScalar(0.12),
|
||||
color: factionColor,
|
||||
emissive: new THREE.Color(factionColor).multiplyScalar(0.12),
|
||||
roughness: 0.35,
|
||||
metalness: 0.65,
|
||||
}),
|
||||
@@ -518,7 +619,7 @@ function createShip({
|
||||
new THREE.BoxGeometry(definition.size * 2.8, definition.size * 1.2, definition.size * 1.5),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: 0x4c6272,
|
||||
emissive: new THREE.Color(definition.color).multiplyScalar(0.04),
|
||||
emissive: new THREE.Color(factionColor).multiplyScalar(0.04),
|
||||
roughness: 0.5,
|
||||
metalness: 0.75,
|
||||
}),
|
||||
@@ -530,7 +631,7 @@ function createShip({
|
||||
[-1, 1].forEach((side) => {
|
||||
const bay = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(definition.size * 1.1, definition.size * 0.38, definition.size * 0.86),
|
||||
new THREE.MeshBasicMaterial({ color: definition.color, transparent: true, opacity: 0.45 }),
|
||||
new THREE.MeshBasicMaterial({ color: factionColor, transparent: true, opacity: 0.45 }),
|
||||
);
|
||||
bay.position.set(-definition.size * 0.3, side * definition.size * 0.52, 0);
|
||||
visual.add(bay);
|
||||
@@ -539,7 +640,7 @@ function createShip({
|
||||
|
||||
const engineGlow = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(definition.size * 0.35, 14, 14),
|
||||
new THREE.MeshBasicMaterial({ color: definition.color, transparent: true, opacity: 0.72 }),
|
||||
new THREE.MeshBasicMaterial({ color: factionColor, transparent: true, opacity: 0.72 }),
|
||||
);
|
||||
engineGlow.position.x = -definition.size * 1.8;
|
||||
visual.add(engineGlow);
|
||||
@@ -547,7 +648,7 @@ function createShip({
|
||||
const ring = new THREE.Mesh(
|
||||
new THREE.RingGeometry(definition.size * 1.5, definition.size * 1.9, 32),
|
||||
new THREE.MeshBasicMaterial({
|
||||
color: definition.color,
|
||||
color: factionColor,
|
||||
transparent: true,
|
||||
opacity: 0,
|
||||
side: THREE.DoubleSide,
|
||||
@@ -567,7 +668,7 @@ function createShip({
|
||||
);
|
||||
const beacon = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(definition.size * 0.26, 0.9, definition.size * 0.42),
|
||||
new THREE.MeshBasicMaterial({ color: definition.color, transparent: true, opacity: 0.52 }),
|
||||
new THREE.MeshBasicMaterial({ color: factionColor, transparent: true, opacity: 0.52 }),
|
||||
);
|
||||
beacon.position.copy(port);
|
||||
beacon.visible = dockingCapacity > 0;
|
||||
@@ -595,6 +696,16 @@ function createShip({
|
||||
inventory: createEmptyInventory(),
|
||||
cargoItemId: definition.cargoItemId,
|
||||
actionTimer: 0,
|
||||
factionId,
|
||||
factionColor,
|
||||
health: definition.maxHealth,
|
||||
maxHealth: definition.maxHealth,
|
||||
weaponRange:
|
||||
definition.shipClass === "capital" ? 260 : definition.shipClass === "cruiser" ? 220 : definition.shipClass === "destroyer" ? 180 : 140,
|
||||
weaponDamage:
|
||||
definition.shipClass === "capital" ? 30 : definition.shipClass === "cruiser" ? 18 : definition.shipClass === "destroyer" ? 12 : 7,
|
||||
weaponCooldown: definition.shipClass === "capital" ? 1.2 : definition.shipClass === "cruiser" ? 0.9 : 0.7,
|
||||
weaponTimer: 0,
|
||||
fuel: 220,
|
||||
energy: 260,
|
||||
maxFuel: 220,
|
||||
|
||||
181
src/style.css
181
src/style.css
@@ -72,30 +72,28 @@ canvas {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.summary {
|
||||
top: 24px;
|
||||
left: 24px;
|
||||
width: min(380px, calc(100vw - 48px));
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.summary h1 {
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.summary p {
|
||||
margin-top: 10px;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
.session-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.details {
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
top: 24px;
|
||||
width: min(320px, calc(100vw - 48px));
|
||||
padding: 18px 20px;
|
||||
bottom: 24px;
|
||||
min-height: 138px;
|
||||
padding: 16px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.selection-meta {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.details h2 {
|
||||
@@ -105,85 +103,79 @@ canvas {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.selection-strip {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding-bottom: 4px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.selection-strip-card {
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid rgba(126, 212, 255, 0.18);
|
||||
border-radius: 14px;
|
||||
min-width: 180px;
|
||||
padding: 10px 12px;
|
||||
color: var(--text);
|
||||
background: linear-gradient(180deg, rgba(13, 30, 56, 0.7), rgba(8, 17, 33, 0.82));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.selection-strip-card-title {
|
||||
font-size: 0.84rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.selection-strip-card-line {
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.35;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.selection-strip-empty {
|
||||
color: var(--muted);
|
||||
font-size: 0.84rem;
|
||||
padding: 8px 2px;
|
||||
}
|
||||
|
||||
.details .content {
|
||||
margin-top: 12px;
|
||||
color: var(--muted);
|
||||
line-height: 1.55;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.commandbar {
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
min-height: 180px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 300px) 1fr minmax(220px, 260px);
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
align-items: stretch;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.selection-panel,
|
||||
.orders-panel,
|
||||
.minimap-panel {
|
||||
border: 1px solid rgba(126, 212, 255, 0.14);
|
||||
border-radius: 14px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(7, 15, 29, 0.82), rgba(4, 10, 20, 0.72)),
|
||||
repeating-linear-gradient(
|
||||
90deg,
|
||||
rgba(126, 212, 255, 0.025) 0,
|
||||
rgba(126, 212, 255, 0.025) 1px,
|
||||
transparent 1px,
|
||||
transparent 16px
|
||||
);
|
||||
padding: 14px 16px;
|
||||
max-height: 132px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.selection-title,
|
||||
.orders-panel .mode {
|
||||
.selection-meta .mode {
|
||||
margin: 0;
|
||||
font-size: 0.86rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.14em;
|
||||
}
|
||||
|
||||
.selection-panel .compact {
|
||||
margin-top: 10px;
|
||||
color: var(--muted);
|
||||
line-height: 1.45;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.orders-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.window-launchers,
|
||||
.fleet-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.orders-panel .mode {
|
||||
.selection-meta .mode {
|
||||
color: var(--warning);
|
||||
text-shadow: 0 0 18px rgba(255, 191, 105, 0.24);
|
||||
}
|
||||
|
||||
.orders {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.window-launchers button,
|
||||
.orders button,
|
||||
.fleet-actions button,
|
||||
.session-actions button,
|
||||
.window-close {
|
||||
border: 1px solid rgba(126, 212, 255, 0.16);
|
||||
border-radius: 12px;
|
||||
@@ -198,35 +190,20 @@ canvas {
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.orders button:hover,
|
||||
.window-launchers button:hover,
|
||||
.fleet-actions button:hover,
|
||||
.session-actions button:hover,
|
||||
.window-close:hover {
|
||||
border-color: rgba(126, 212, 255, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.orders[data-mode="none"] button:not([data-action="focus"]) {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.orders-panel .hint {
|
||||
color: var(--muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.minimap-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.minimap {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
@@ -235,6 +212,10 @@ button:disabled {
|
||||
background: rgba(2, 6, 13, 0.92);
|
||||
}
|
||||
|
||||
.minimap-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-window {
|
||||
position: absolute;
|
||||
top: 104px;
|
||||
@@ -472,32 +453,16 @@ button:disabled {
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.summary,
|
||||
.details,
|
||||
.commandbar {
|
||||
.details {
|
||||
width: auto;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.summary,
|
||||
.details {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.details {
|
||||
top: auto;
|
||||
bottom: 92px;
|
||||
}
|
||||
|
||||
.commandbar {
|
||||
grid-template-columns: 1fr;
|
||||
bottom: 16px;
|
||||
}
|
||||
|
||||
.orders {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.window-launchers,
|
||||
.fleet-actions {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
Reference in New Issue
Block a user