diff --git a/app/.server/classes/FlightDataModel.ts b/app/.server/classes/FlightDataModel.ts index 33fcc483..e0e1c28d 100644 --- a/app/.server/classes/FlightDataModel.ts +++ b/app/.server/classes/FlightDataModel.ts @@ -1,4 +1,4 @@ -import { ECS, type Entity } from "@thorium/utils/ecs"; +import { ECS, Entity } from "@thorium/utils/ecs"; import randomWords from "@thorium/utils/random-words"; import type { ServerDataModel } from "./ServerDataModel"; import { DataStore, type DataStoreOptions } from "@thorium/utils/.server/db-fs"; @@ -15,7 +15,10 @@ export class FlightDataModel extends DataStore { paused!: boolean; hasFlightDirector!: boolean; state!: "in-progress" | "success" | "failure"; + /** Why the flight was successful or failed */ stateReason!: string; + /** How long destroyed player ships must wait before respawning. Null means no respawns. */ + defaultRespawnTimeMs!: null | number; ecs!: ECS; pluginIds!: string[]; private entities!: Entity[]; @@ -79,7 +82,16 @@ export class FlightDataModel extends DataStore { await this.#getDataPromise; this.ecs = new ECS(server); initECS(this.ecs, this.entities, this.mode); - this.run(); + if (!this.flightEntity) { + const flight = new Entity(); + flight.addComponent("isFlight", { + respawnTimeMs: this.defaultRespawnTimeMs, + }); + this.ecs.addEntity(flight); + } + if (!this.interval) { + this.run(); + } } async initPhysics() { // Fetch and calculate all of the colliders for the ships in the plugins @@ -130,6 +142,9 @@ export class FlightDataModel extends DataStore { async getSnapshots() { return DataStore.operations.getStore()!.getFlightSnapshots(this.name); } + get flightEntity() { + return this.ecs.componentCache.get("isFlight")?.values().next().value; + } /** * Ships that are available for spawning in the universe, based on the flight's plugins. */ @@ -170,6 +185,7 @@ export class FlightDataModel extends DataStore { state: this.state, stateReason: this.stateReason, startInput: this.startInput, + defaultRespawnTimeMs: this.defaultRespawnTimeMs, }; return data; } diff --git a/app/.server/data/ship.ts b/app/.server/data/ship.ts index d532a179..24285004 100644 --- a/app/.server/data/ship.ts +++ b/app/.server/data/ship.ts @@ -10,6 +10,7 @@ import { } from "@thorium/utils/starmap/position"; import { Vector3 } from "three"; import type { DataContext } from "@thorium/.server/DataContext"; +import { destroyShip } from "@thorium/utils/.server/ship/collisionDamage"; export const ship = t.router({ get: t.procedure @@ -112,7 +113,10 @@ export const ship = t.router({ if (!ship) return { id: input?.playerShipId || -1, + name: "", + registry: "", currentSystem: null, + alertLevel: "5", systemPosition: { parentId: null, type: "interstellar", @@ -128,10 +132,13 @@ export const ship = t.router({ const assets = ship.components.isShip!.assets; return { id: ship.id, + name: ship.components.identity!.name, + registry: ship.components.isShip!.registry, + alertLevel: ship.components.isShip!.alertLevel, currentSystem: systemId || null, systemPosition, assets, - isDestroyed: !!ship.components.isDestroyed, + isDestroyed: ship.components.isDestroyed, }; }), spawn: t.procedure @@ -245,6 +252,15 @@ export const ship = t.router({ }); return { id: shipEntity.id }; }), + destroy: t.procedure + .meta({ action: true }) + .input(z.object({ shipId: z.number() })) + .send(({ ctx, input }) => { + const ship = ctx.ecs.getEntityById(input.shipId); + if (!ship) throw new Error("Ship not found"); + + destroyShip(ship); + }), }); const objectPosition = new Vector3(); diff --git a/app/.server/systems/IsDestroyedSystem.ts b/app/.server/systems/IsDestroyedSystem.ts index fb58e55a..53c97978 100644 --- a/app/.server/systems/IsDestroyedSystem.ts +++ b/app/.server/systems/IsDestroyedSystem.ts @@ -1,6 +1,10 @@ import { pubsub } from "@thorium/.server/init/pubsub"; import { type Entity, System } from "@thorium/utils/ecs"; import type { World } from "@thorium-sim/rapier3d-node"; +import { + getCompletePositionFromOrbit, + getObjectOffsetPosition, +} from "@thorium/utils/starmap/position"; export class IsDestroyedSystem extends System { static flightMode = ["nova"]; @@ -9,44 +13,112 @@ export class IsDestroyedSystem extends System { } update(entity: Entity, elapsed: number) { const { timeToDestroy, timer } = entity.components.isDestroyed!; - if (timeToDestroy > 0) { + if (typeof timeToDestroy === "number" && timeToDestroy > 0) { entity.updateComponent("isDestroyed", { timer: timer + elapsed }); - if (timer + elapsed >= timeToDestroy) { - // Remove the entity from any physics worlds it is a part of - const handles = entity.components.physicsHandles?.handles as Map< - number, - number - >; - for (const [worldEntityId, handle] of handles.entries()) { - const worldEntity = this.ecs.getEntityById(worldEntityId); - if (!worldEntity) continue; - const world = worldEntity.components.physicsWorld?.world as World; - if (!world) continue; - const body = world.getRigidBody(handle); - if (!body) continue; - world.removeRigidBody(body); + if (!entity.components.isPlayerShip) { + deleteShip(entity); + } else { + respawnShip(entity); } - const systemId = entity.components.position?.parentId || null; - this.ecs.removeEntity(entity); + } + } + } +} + +function deleteShip(entity: Entity) { + // Remove the entity from any physics worlds it is a part of + const handles = entity.components.physicsHandles?.handles as Map< + number, + number + >; + for (const [worldEntityId, handle] of handles.entries()) { + const worldEntity = entity.ecs.getEntityById(worldEntityId); + if (!worldEntity) continue; + const world = worldEntity.components.physicsWorld?.world as World; + if (!world) continue; + const body = world.getRigidBody(handle); + if (!body) continue; + world.removeRigidBody(body); + } + const systemId = entity.components.position?.parentId || null; + entity.ecs.removeEntity(entity); + + if (entity.components.isTorpedo) { + pubsub.publish.starmapCore.torpedos({ + systemId, + }); + } + if (entity.components.isShip) { + // Also remove all the ship systems and crew + // TODO April 25, 2025 - Remove Crew + for (const shipSystem of entity.components.shipSystems?.shipSystems || []) { + entity.ecs.removeEntityById(shipSystem[0]); + } + pubsub.publish.starmapCore.ships({ + systemId, + }); + } +} - if (entity.components.isTorpedo) { - pubsub.publish.starmapCore.torpedos({ - systemId, - }); +function respawnShip(entity: Entity) { + // Bring the ship back from the dead. + // Restore hull entirely, bring all + // non-vulnerable systems back to operational, + // and move the ship to a more safe location, in case of collision. + entity.removeComponent("isDestroyed"); + entity.updateComponent("hull", { + hull: entity.components.hull?.maxHull || 1, + }); + + // Repair the ship systems + for (const systemId of entity.components.shipSystems?.shipSystems.keys() || + []) { + const system = entity.ecs.getEntityById(systemId); + if (!system?.components.isShipSystem || !system.components.damage) continue; + if (system.components.damage.vulnerability === "vulnerable") continue; + // Fix the system a bunch, but not all the way. + system.updateComponent("damage", { + offline: false, + efficiency: 1, + cascadeRisk: 0, + instability: 0, + heatMultiplier: 1, + }); + } + const position = entity.components.position!; + + // Find a point 1,000km from any large object and set the position to that + let closestPlanet: Entity | null = null; + let previousDistance = Number.POSITIVE_INFINITY; + for (const e of entity.ecs.componentCache.get("position") || []) { + if ( + e.components.position?.parentId === entity.components.position?.parentId + ) { + if (e.components.isPlanet || e.components.isStarbase) { + if (!closestPlanet) { + closestPlanet = e; + continue; } - if (entity.components.isShip) { - // Also remove all the ship systems and crew - // TODO April 25, 2025 - Remove Crew - for (const shipSystem of entity.components.shipSystems?.shipSystems || - []) { - this.ecs.removeEntityById(shipSystem[0]); - } - pubsub.publish.starmapCore.ships({ - systemId, - }); + const p = getCompletePositionFromOrbit(e); + const distance = Math.hypot( + position.x - p.x, + position.y - p.y, + position.z - p.z, + ); + if (distance < previousDistance) { + previousDistance = distance; + closestPlanet = e; } } } } + + // If we can't find a close by planet, then we'll just keep the current position. How bad could it be? + if (closestPlanet) { + const { x, y, z } = getObjectOffsetPosition(closestPlanet, position, 5_000); + entity.updateComponent("position", { x, y, z }); + } + + pubsub.publish.ship.player({ shipId: entity.id }); } diff --git a/app/cards/Login/index.tsx b/app/cards/Login/index.tsx index e07ecfde..0a02932b 100644 --- a/app/cards/Login/index.tsx +++ b/app/cards/Login/index.tsx @@ -1,12 +1,13 @@ import { clientId, q } from "@thorium/context/AppContext"; +import { useStation } from "@thorium/routes/station/useStation"; import Button from "@thorium/ui/Button"; import Input from "@thorium/ui/Input"; import { useState } from "react"; const Login = () => { + const { ship } = useStation(); const [loginName, setLoginName] = useState(""); // TODO: Support logging in with a ThoriumSim account - const [ship] = q.ship.get.useNetRequest({ clientId }); const login = () => { if (loginName.trim().length > 0) { // TODO: Play a sound effect when the user logs in @@ -16,17 +17,19 @@ const Login = () => { if (!ship) throw new Error("Station is not assigned to a ship."); return (