Download universe/ShipSystem.ts from Snapkitty/grisp-shadow-fleet: direct link, hf CLI and curl.
- Browser
- Download file 18.5 kB
-
https://huggingface.co/Snapkitty/grisp-shadow-fleet/resolve/main/universe/ShipSystem.ts
- Command line
-
hf download hf://Snapkitty/grisp-shadow-fleet/universe/ShipSystem.ts
-
curl -L -o ShipSystem.ts https://huggingface.co/Snapkitty/grisp-shadow-fleet/resolve/main/universe/ShipSystem.ts
18.5 kB
| import { addVector, clamp, clampMagnitude, distance, magnitude, scaleVector, vector } from './deterministic.js'; | |
| import { emitWorldEvent, findRequired } from './state-utils.js'; | |
| import type { CommandResult, EntityId, ShipControlInput, ShipState, UniverseState, Vector3 } from './types.js'; | |
| const EMPTY_CONTROLS: ShipControlInput = { thrust: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 }, brake: false, boost: false }; | |
| export class ShipSystem { | |
| private readonly controls = new Map<EntityId, ShipControlInput>(); | |
| constructor(private state: UniverseState) {} | |
| replaceState(state: UniverseState): void { | |
| this.state = state; | |
| this.controls.clear(); | |
| } | |
| board(shipId: EntityId): CommandResult { | |
| const ship = findRequired(this.state.ships, shipId, 'Ship'); | |
| const player = this.state.player; | |
| if (player.boardedShipId) return this.failure('Player is already aboard a ship.'); | |
| const accessible = (ship.flightMode === 'docked' && ['room-hangar', 'room-docking'].includes(player.locationId)) | |
| || (ship.flightMode === 'landed' && player.locationId === ship.landedRegionId) | |
| || (player.traversalMode === 'eva' && distance(player.transform.position, ship.transform.position) <= 12); | |
| if (!accessible) return this.failure(`${ship.name} is not within boarding access.`); | |
| player.boardedShipId = ship.id; | |
| player.locationId = ship.id; | |
| player.layer = 'sovereign-interior'; | |
| player.traversalMode = 'ship-boarded'; | |
| player.grounded = true; | |
| player.transform.position = { ...ship.transform.position }; | |
| if (!ship.passengerIds.includes(player.id)) ship.passengerIds.push(player.id); | |
| const event = emitWorldEvent(this.state, 'ship-boarded', player.id, ship.id, `Player boarded ${ship.name}.`); | |
| return { ok: true, message: event.summary, eventIds: [event.id] }; | |
| } | |
| start(shipId: EntityId): CommandResult { | |
| const ship = findRequired(this.state.ships, shipId, 'Ship'); | |
| if (this.state.player.boardedShipId !== ship.id) return this.failure('Board the ship before startup.'); | |
| if (ship.startupStage === 'ready') return { ok: true, message: `${ship.name} is already ready.`, eventIds: [] }; | |
| if (ship.fuel <= 0 || ship.subsystems.reactor.health <= 0.2) return this.failure(`${ship.name} cannot start: reactor or fuel unavailable.`); | |
| ship.startupStage = 'powering'; | |
| ship.startupElapsedMs = 0; | |
| ship.subsystems.reactor.online = true; | |
| ship.subsystems.reactor.power = 0.2; | |
| const event = emitWorldEvent(this.state, 'ship-startup-began', this.state.player.id, ship.id, `${ship.name} startup sequence began.`, { persistent: false }); | |
| return { ok: true, message: event.summary, eventIds: [event.id] }; | |
| } | |
| takeControl(shipId: EntityId): CommandResult { | |
| const ship = findRequired(this.state.ships, shipId, 'Ship'); | |
| if (this.state.player.boardedShipId !== ship.id) return this.failure('Player must be aboard the ship.'); | |
| if (ship.startupStage !== 'ready') return this.failure('Ship startup must complete before taking control.'); | |
| ship.pilotId = this.state.player.id; | |
| this.state.player.pilotedShipId = ship.id; | |
| this.state.player.traversalMode = 'piloting'; | |
| const event = emitWorldEvent(this.state, 'ship-control-assumed', this.state.player.id, ship.id, `Player assumed control of ${ship.name}.`); | |
| return { ok: true, message: event.summary, eventIds: [event.id] }; | |
| } | |
| setControls(shipId: EntityId, input: ShipControlInput): CommandResult { | |
| const ship = findRequired(this.state.ships, shipId, 'Ship'); | |
| if (ship.pilotId !== this.state.player.id || this.state.player.pilotedShipId !== ship.id) return this.failure('Player is not piloting this ship.'); | |
| if (ship.flightMode !== 'free-flight' && ship.flightMode !== 'launching') return this.failure('Flight controls require a free-flight ship.'); | |
| this.controls.set(ship.id, { | |
| thrust: clampMagnitude(input.thrust, 1), rotation: clampMagnitude(input.rotation, 1), | |
| brake: input.brake, boost: input.boost, | |
| }); | |
| return { ok: true, message: 'Ship control input queued.', eventIds: [] }; | |
| } | |
| undock(shipId: EntityId): CommandResult { | |
| const ship = findRequired(this.state.ships, shipId, 'Ship'); | |
| if (ship.pilotId !== this.state.player.id) return this.failure('Only the active pilot can undock.'); | |
| if (ship.startupStage !== 'ready') return this.failure('Ship is not flight ready.'); | |
| if (ship.flightMode !== 'docked' && ship.flightMode !== 'landed') return this.failure('Ship is not docked or landed.'); | |
| ship.flightMode = 'launching'; | |
| ship.dockedAtId = null; | |
| ship.landedRegionId = null; | |
| ship.transform.velocity = vector(0, 0, 4); | |
| const event = emitWorldEvent(this.state, 'ship-undocked', this.state.player.id, ship.id, `${ship.name} released clamps and entered the launch corridor.`); | |
| return { ok: true, message: event.summary, eventIds: [event.id] }; | |
| } | |
| dock(shipId: EntityId, dockId: EntityId): CommandResult { | |
| const ship = findRequired(this.state.ships, shipId, 'Ship'); | |
| if (ship.pilotId !== this.state.player.id) return this.failure('Only the active pilot can dock.'); | |
| if (ship.flightMode !== 'free-flight' && ship.flightMode !== 'docking' && ship.flightMode !== 'launching') return this.failure('Ship is not in a dockable flight mode.'); | |
| const dockPosition = this.dockPosition(dockId); | |
| if (!dockPosition) return this.failure(`Dock '${dockId}' does not exist.`); | |
| if (distance(ship.transform.position, dockPosition) > 250) return this.failure('Ship is outside docking capture range.'); | |
| if (magnitude(ship.transform.velocity) > 25) return this.failure('Ship relative velocity is too high for docking.'); | |
| ship.flightMode = 'docked'; | |
| ship.dockedAtId = dockId; | |
| ship.landedRegionId = null; | |
| ship.targetDockId = null; | |
| ship.transform.position = { ...dockPosition }; | |
| ship.transform.velocity = vector(); | |
| const event = emitWorldEvent(this.state, 'ship-docked', this.state.player.id, ship.id, `${ship.name} docked at ${dockId}.`); | |
| return { ok: true, message: event.summary, eventIds: [event.id] }; | |
| } | |
| requestDocking(shipId: EntityId, dockId: EntityId): CommandResult { | |
| const ship = findRequired(this.state.ships, shipId, 'Ship'); | |
| if (ship.flightMode !== 'free-flight') return this.failure('Docking request requires free flight.'); | |
| if (!this.dockPosition(dockId)) return this.failure(`Dock '${dockId}' does not exist.`); | |
| ship.flightMode = 'docking'; | |
| ship.targetDockId = dockId; | |
| const event = emitWorldEvent(this.state, 'docking-clearance-granted', 'agent-002', ship.id, `${ship.name} received docking guidance for ${dockId}.`, { persistent: false }); | |
| return { ok: true, message: event.summary, eventIds: [event.id] }; | |
| } | |
| land(shipId: EntityId, regionId: EntityId): CommandResult { | |
| const ship = findRequired(this.state.ships, shipId, 'Ship'); | |
| const region = findRequired(this.state.planetaryRegions, regionId, 'Planetary region'); | |
| if (ship.pilotId !== this.state.player.id) return this.failure('Only the active pilot can land.'); | |
| if (ship.flightMode !== 'free-flight') return this.failure('Ship must be in free flight to land.'); | |
| if (distance(ship.transform.position, region.center) > region.radiusM + 20_000) return this.failure('Ship has not approached the landing region.'); | |
| if (magnitude(ship.transform.velocity) > 160) return this.failure('Ship velocity is too high for landing approach.'); | |
| ship.flightMode = 'landed'; | |
| ship.landedRegionId = region.id; | |
| ship.dockedAtId = null; | |
| ship.transform.position = { ...region.center, y: region.center.y + 3 }; | |
| ship.transform.velocity = vector(); | |
| this.state.player.layer = 'planet-surface'; | |
| const event = emitWorldEvent(this.state, 'ship-landed', this.state.player.id, ship.id, `${ship.name} landed in ${region.name}.`, { data: { regionId } }); | |
| return { ok: true, message: event.summary, eventIds: [event.id] }; | |
| } | |
| beginJump(shipId: EntityId, targetId: EntityId): CommandResult { | |
| const ship = findRequired(this.state.ships, shipId, 'Ship'); | |
| const targetExists = this.state.celestialBodies.some((body) => body.id === targetId) | |
| || this.state.discoveries.some((discovery) => discovery.id === targetId) | |
| || this.state.settlements.some((settlement) => settlement.id === targetId) | |
| || this.state.jumpConnections.some((connection) => connection.id === targetId); | |
| if (!targetExists) return this.failure(`Jump target '${targetId}' does not exist.`); | |
| if (ship.pilotId !== this.state.player.id || ship.flightMode !== 'free-flight') return this.failure('Active piloting in free flight is required for jump travel.'); | |
| if (ship.fuel < 0.08 || ship.subsystems.engines.health < 0.5) return this.failure('Insufficient jump capability.'); | |
| const connection = this.state.jumpConnections.find((candidate) => candidate.id === targetId); | |
| if (connection?.requiredDiscoveryId && !this.state.discoveries.find((discovery) => discovery.id === connection.requiredDiscoveryId)?.discovered) { | |
| return this.failure(`${connection.name} has not been discovered and calibrated.`); | |
| } | |
| ship.flightMode = 'jumping'; | |
| ship.jumpTargetId = targetId; | |
| ship.jumpElapsedMs = 0; | |
| ship.fuel = clamp(ship.fuel - 0.08, 0, 1); | |
| const event = emitWorldEvent(this.state, 'ship-jump-began', this.state.player.id, ship.id, `${ship.name} entered a continuous travel corridor toward ${targetId}.`); | |
| return { ok: true, message: event.summary, eventIds: [event.id] }; | |
| } | |
| exit(shipId: EntityId): CommandResult { | |
| const ship = findRequired(this.state.ships, shipId, 'Ship'); | |
| const player = this.state.player; | |
| if (player.boardedShipId !== ship.id) return this.failure('Player is not aboard this ship.'); | |
| if (ship.flightMode !== 'docked' && ship.flightMode !== 'landed' && (!player.suit.equipped || !player.suit.sealed)) { | |
| return this.failure('A sealed suit is required to exit a ship in space.'); | |
| } | |
| if (ship.pilotId === player.id) ship.pilotId = null; | |
| player.pilotedShipId = null; | |
| player.boardedShipId = null; | |
| ship.passengerIds = ship.passengerIds.filter((id) => id !== player.id); | |
| if (ship.flightMode === 'docked') { | |
| player.locationId = ship.dockedAtId === 'dock-hangar-01' ? 'room-hangar' : 'room-docking'; | |
| player.layer = 'sovereign-interior'; | |
| player.traversalMode = 'walking'; | |
| player.grounded = true; | |
| } else if (ship.flightMode === 'landed' && ship.landedRegionId) { | |
| player.locationId = ship.landedRegionId; | |
| player.layer = 'planet-surface'; | |
| player.traversalMode = 'walking'; | |
| player.grounded = true; | |
| } else { | |
| player.locationId = ship.id; | |
| player.layer = 'orbital-space'; | |
| player.traversalMode = 'eva'; | |
| player.grounded = false; | |
| } | |
| player.transform.position = addVector(ship.transform.position, vector(0, 0, -6)); | |
| player.transform.velocity = { ...ship.transform.velocity }; | |
| const event = emitWorldEvent(this.state, 'ship-exited', player.id, ship.id, `Player exited ${ship.name}.`); | |
| return { ok: true, message: event.summary, eventIds: [event.id] }; | |
| } | |
| repair(shipId: EntityId, subsystem: keyof ShipState['subsystems'], amount: number): CommandResult { | |
| const ship = findRequired(this.state.ships, shipId, 'Ship'); | |
| if (this.state.player.boardedShipId !== ship.id && distance(this.state.player.transform.position, ship.transform.position) > 10) return this.failure('Ship is outside repair reach.'); | |
| const tool = this.state.player.inventory.find((item) => item.tags.includes('repair')); | |
| if (!tool) return this.failure('A repair tool is required.'); | |
| ship.subsystems[subsystem].health = clamp(ship.subsystems[subsystem].health + Math.max(0, amount), 0, 1); | |
| const event = emitWorldEvent(this.state, 'ship-subsystem-repaired', this.state.player.id, ship.id, `${ship.name} ${subsystem} repaired.`, { data: { subsystem, amount } }); | |
| return { ok: true, message: event.summary, eventIds: [event.id] }; | |
| } | |
| applyDamage(shipId: EntityId, amount: number, subsystem?: keyof ShipState['subsystems']): void { | |
| const ship = findRequired(this.state.ships, shipId, 'Ship'); | |
| const residual = Math.max(0, amount - ship.shield); | |
| ship.shield = clamp(ship.shield - amount, 0, 1); | |
| ship.hullIntegrity = clamp(ship.hullIntegrity - residual, 0, 1); | |
| if (subsystem) ship.subsystems[subsystem].health = clamp(ship.subsystems[subsystem].health - residual, 0, 1); | |
| emitWorldEvent(this.state, 'ship-damaged', 'environment', ship.id, `${ship.name} sustained damage.`, { severity: amount, data: { amount, subsystem: subsystem ?? null } }); | |
| } | |
| tick(deltaMs: number): void { | |
| const deltaSeconds = deltaMs / 1_000; | |
| for (const ship of this.state.ships) { | |
| this.tickStartup(ship, deltaMs); | |
| if (ship.flightMode === 'jumping') { | |
| this.tickJump(ship, deltaMs); | |
| continue; | |
| } | |
| if (ship.flightMode === 'launching' && ship.startupStage === 'ready') { | |
| ship.transform.position = addVector(ship.transform.position, scaleVector(ship.transform.velocity, deltaSeconds)); | |
| if (distance(ship.transform.position, this.dockPosition('dock-hangar-01') ?? vector()) > 80) ship.flightMode = 'free-flight'; | |
| } else if (ship.flightMode === 'free-flight' || ship.flightMode === 'docking') { | |
| this.tickFlight(ship, deltaSeconds); | |
| } | |
| if (ship.flightMode === 'docking' && ship.targetDockId) this.tickAutodock(ship, deltaSeconds); | |
| if (this.state.player.boardedShipId === ship.id) { | |
| this.state.player.transform.position = { ...ship.transform.position }; | |
| this.state.player.transform.velocity = { ...ship.transform.velocity }; | |
| } | |
| } | |
| } | |
| private tickStartup(ship: ShipState, deltaMs: number): void { | |
| if (ship.startupStage !== 'powering') return; | |
| ship.startupElapsedMs += deltaMs; | |
| const progress = clamp(ship.startupElapsedMs / 3_000, 0, 1); | |
| ship.subsystems.reactor.power = progress; | |
| if (progress < 0.25) return; | |
| for (const [name, subsystem] of Object.entries(ship.subsystems)) { | |
| if (name === 'reactor') continue; | |
| subsystem.online = subsystem.health > 0.2; | |
| subsystem.power = subsystem.online ? progress : 0; | |
| } | |
| if (progress >= 1) { | |
| ship.startupStage = 'ready'; | |
| emitWorldEvent(this.state, 'ship-startup-completed', ship.id, null, `${ship.name} is flight ready.`, { persistent: false }); | |
| } | |
| } | |
| private tickFlight(ship: ShipState, deltaSeconds: number): void { | |
| const input = this.controls.get(ship.id) ?? EMPTY_CONTROLS; | |
| if (ship.startupStage === 'ready' && ship.subsystems.engines.online && ship.fuel > 0) { | |
| const boost = input.boost ? 1.5 : 1; | |
| const acceleration = ship.maxThrustN / ship.massKg * boost; | |
| ship.transform.velocity = addVector(ship.transform.velocity, scaleVector(input.thrust, acceleration * deltaSeconds)); | |
| ship.transform.angularVelocity = scaleVector(input.rotation, ship.maneuverThrustN / ship.massKg * 0.08); | |
| ship.transform.rotation.pitch += ship.transform.angularVelocity.x * deltaSeconds; | |
| ship.transform.rotation.yaw += ship.transform.angularVelocity.y * deltaSeconds; | |
| ship.transform.rotation.roll += ship.transform.angularVelocity.z * deltaSeconds; | |
| const thrustAmount = magnitude(input.thrust); | |
| ship.fuel = clamp(ship.fuel - thrustAmount * deltaSeconds * 0.00025 * boost, 0, 1); | |
| } | |
| if (input.brake && ship.subsystems.thrusters.online) ship.transform.velocity = scaleVector(ship.transform.velocity, Math.max(0, 1 - deltaSeconds * 0.7)); | |
| ship.transform.velocity = clampMagnitude(ship.transform.velocity, 12_000); | |
| ship.transform.position = addVector(ship.transform.position, scaleVector(ship.transform.velocity, deltaSeconds)); | |
| ship.shield = clamp(ship.shield + deltaSeconds * 0.002 * ship.subsystems.shields.health, 0, 1); | |
| } | |
| private tickAutodock(ship: ShipState, deltaSeconds: number): void { | |
| const target = ship.targetDockId ? this.dockPosition(ship.targetDockId) : null; | |
| if (!target) return; | |
| const offset = { x: target.x - ship.transform.position.x, y: target.y - ship.transform.position.y, z: target.z - ship.transform.position.z }; | |
| const range = magnitude(offset); | |
| if (range <= 4 && magnitude(ship.transform.velocity) <= 25) { | |
| this.dock(ship.id, ship.targetDockId as string); | |
| return; | |
| } | |
| const desired = scaleVector(offset, Math.min(12, range * 0.25) / Math.max(range, 1)); | |
| ship.transform.velocity = addVector(scaleVector(ship.transform.velocity, Math.max(0, 1 - deltaSeconds * 1.4)), scaleVector(desired, deltaSeconds)); | |
| } | |
| private tickJump(ship: ShipState, deltaMs: number): void { | |
| ship.jumpElapsedMs += deltaMs; | |
| const connection = ship.jumpTargetId ? this.state.jumpConnections.find((candidate) => candidate.id === ship.jumpTargetId) : undefined; | |
| const travelTimeMs = connection?.travelTimeMs ?? 5_000; | |
| if (ship.jumpElapsedMs < travelTimeMs || !ship.jumpTargetId) return; | |
| const celestial = this.state.celestialBodies.find((body) => body.id === ship.jumpTargetId); | |
| const discovery = this.state.discoveries.find((item) => item.id === ship.jumpTargetId); | |
| const settlement = this.state.settlements.find((item) => item.id === ship.jumpTargetId); | |
| const target = celestial?.position ?? discovery?.position ?? settlement?.position ?? connection?.exitPosition; | |
| if (!target) { | |
| ship.flightMode = 'free-flight'; | |
| ship.jumpTargetId = null; | |
| return; | |
| } | |
| const approachRegion = celestial ? this.state.planetaryRegions.find((region) => region.planetId === celestial.id) : undefined; | |
| ship.transform.position = approachRegion | |
| ? addVector(approachRegion.center, vector(0, 0, approachRegion.radiusM + 5_000)) | |
| : connection | |
| ? { ...target } | |
| : addVector(target, vector(0, 0, (celestial?.radiusM ?? 1_000) + 15_000)); | |
| ship.transform.velocity = vector(); | |
| ship.flightMode = 'free-flight'; | |
| const targetId = ship.jumpTargetId; | |
| ship.jumpTargetId = null; | |
| ship.jumpElapsedMs = 0; | |
| emitWorldEvent(this.state, 'ship-jump-completed', ship.id, targetId, `${ship.name} emerged near ${targetId}.`); | |
| } | |
| private dockPosition(dockId: EntityId): Vector3 | null { | |
| if (dockId === 'dock-hangar-01') return vector(170, -18, -30); | |
| if (dockId === 'dock-ring-01') return vector(260, 0, 0); | |
| return null; | |
| } | |
| private failure(message: string): CommandResult { | |
| return { ok: false, message, eventIds: [] }; | |
| } | |
| } | |