Something went wrong. Try again.
Starship Simulator Controls thoriumsim.com
Something went wrong. Try again.
25 kB · 830 lines
TSX
at develop
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831import type ShipPlugin from "@thorium/.server/classes/Plugins/Ship";import { pubsub } from "@thorium/.server/init/pubsub";import { t } from "@thorium/.server/init/t";import type { ComponentIds, ComponentProperties } from "@thorium/ecs-components";import type { isDestroyed } from "@thorium/ecs-components/isDestroyed";import { Entity } from "@thorium/utils/ecs";import { shipObjectives } from "@thorium/utils/flags/shipObjectives";import { pathfinder } from "@thorium/utils/starmap/pathfinder.server";import { getCompletePositionFromOrbit, getObjectOffsetPosition, getObjectSystem,} from "@thorium/utils/starmap/position";import type { Coordinates } from "@thorium/utils/unitTypes";import { matchSorter } from "match-sorter";import { Vector3 } from "three";import z from "zod";
type IsDestroyed = z.infer<typeof isDestroyed>;
const objectDetailsComponents: ComponentIds[] = [ "isShip", "position", "velocity", "rotation", "rotationVelocity", "hull", "mass", "size", "tags", "identity", "theme", "isStar", "isPlanet", "satellite", "temperature", "population", "reputation",];
export const starmapCore = t.router({ systems: t.procedure .autoPublish(["isSolarSystem"], () => null) .request(({ ctx }) => { if (!ctx.flight) return [];
return [...(ctx.ecs.componentCache.get("isSolarSystem") || [])].map((e) => ({ id: e.id, position: e.components.position, identity: e.components.identity, })); }), system: t.procedure .input(z.object({ systemId: z.number().nullish() })) .autoPublish(["isSolarSystem"], () => null) .request(({ ctx, input }) => { if (!ctx.flight) throw new Error("No flight in progress"); if (input?.systemId === null || input?.systemId === undefined) throw new Error("No system id provided"); const data = ctx.flight.ecs.getEntityById(input.systemId); if (!data?.components.isSolarSystem) throw new Error("Not a solar system"); return { id: data.id, components: data.components }; }), /** Includes all the things in a system that isn't a ship */ entities: t.procedure .input(z.object({ systemId: z.number().nullable() })) .autoPublish([], () => null) .request(({ ctx, input }) => { if (!ctx.flight) return []; if (input?.systemId === null || input?.systemId === undefined) return []; const checkEntities = [ ...(ctx.ecs.componentCache.get("position") || []), ...(ctx.ecs.componentCache.get("satellite") || []), ]; const data = checkEntities.reduce((prev: Pick<Entity, "components" | "id">[], entity) => { if (entity.components.isShip || entity.components.isTorpedo || entity.components.isWaypoint) return prev; if ( entity.components.position?.parentId === input.systemId || entity.components.satellite?.parentId === input.systemId || getObjectSystem(entity)?.id === input.systemId ) prev.push({ components: entity.components, id: entity.id }); return prev; }, []); return data; }), /** Includes all the ship in a system or interstellar space */ ships: t.procedure .input(z.object({ systemId: z.number().nullable() })) .filter((publish: { systemId: number | null }, { input }) => { if (!publish) return true; if (!publish.systemId && !input.systemId) return true; if (publish.systemId === input.systemId) return true; return false; }) .autoPublish( ["isShip"], (entity) => entity.components.position && { systemId: entity.components.position.parentId, }, )
.request(({ ctx, input }) => { if (!ctx.flight) return []; const shipEntities = ctx.flight.ecs.componentCache.get("isShip") || []; const data: { id: number; modelUrl?: string; logoUrl?: string; size: number; isDestroyed?: IsDestroyed; /** * Whether the ThreeJS object should render an HTML element on top. * Mostly used for training **/ html: boolean; }[] = []; for (const { components, id } of shipEntities) { if ( components.isShip && ((typeof input?.systemId === "number" && components.position?.parentId === input.systemId) || (input?.systemId == null && components.position?.type === "interstellar")) ) { data.push({ id, modelUrl: components.isShip.assets.model, logoUrl: components.isShip.assets.logo, size: components.size?.length || 50, isDestroyed: components.isDestroyed, html: !!components.tags?.tags.includes("html"), }); } }
return data; }), shipsSetPosition: t.procedure .input( z.object({ ships: z .object({ x: z.number(), y: z.number(), z: z.number(), id: z.number(), }) .array(), }), ) .send(({ ctx, input }) => { for (const ship of input.ships) { const entity = ctx.ecs.getEntityById(ship.id); if (entity) { entity.updateComponent("position", { x: ship.x, y: ship.y, z: ship.z, }); entity.addComponent("snapInterpolation", { snapInterpolation: true }); // TODO May 28, 2025 - Also switch which physics world the ship is operating in } } }), torpedos: t.procedure .input(z.object({ systemId: z.number().nullable() })) .filter((publish: { systemId: number | null }, { input }) => { if (!publish) return true; if (!publish.systemId && !input.systemId) return true; if (publish.systemId === input.systemId) return true; return false; }) .autoPublish( ["isTorpedo"], (entity) => entity.components.position && { systemId: entity.components.position.parentId, }, )
.request(({ ctx, input }) => { if (!ctx.flight) return []; const torpedoEntities = ctx.flight.ecs.componentCache.get("isTorpedo") || []; const data: { id: number; color: string; isDestroyed?: IsDestroyed; }[] = []; for (const { components, id } of torpedoEntities) { if ( components.isTorpedo && ((typeof input?.systemId === "number" && components.position?.parentId === input.systemId) || (input?.systemId == null && components.position?.type === "interstellar")) ) { if ( typeof components.isDestroyed?.timeToDestroy === "number" && components.isDestroyed.timer > components.isDestroyed.timeToDestroy ) continue; data.push({ id, color: components.isTorpedo.color, isDestroyed: components.isDestroyed, }); } } return data; }), /** Useful for fetching a single ship when following that ship */ ship: t.procedure .input(z.object({ shipId: z.number().optional().nullish() })) .filter((publish: { shipId: number | null }, { input }) => { if (!input.shipId) return true; if (publish && publish.shipId !== input.shipId) return false;
return true; }) .autoPublish(["isShip"], (entity) => entity.components.position && { shipId: entity.id })
.request(({ ctx, input }) => { if (!input.shipId) return null; if (!ctx.flight) return null;
const entity = ctx.flight.ecs.getEntityById(input.shipId); if (!entity) return null; return { id: entity.id, systemId: entity.components.position?.parentId, behavior: entity.components.shipBehavior, }; }), /** Useful for displaying properties of any object in the starmap */ object: t.procedure .input(z.object({ objectId: z.number().optional() })) .filter((publish: { objectId: number | null }, { input }) => { if (publish && publish.objectId !== input.objectId) return false;
return true; }) .autoPublish(objectDetailsComponents, (entity) => ({ objectId: entity.id }))
.request(({ ctx, input }) => { if (!ctx.flight || !input.objectId) return null; const entity = ctx.flight.ecs.getEntityById(input.objectId); if (!entity) return null; const position = getCompletePositionFromOrbit(entity); const system = getObjectSystem(entity); return { id: entity.id, components: objectDetailsComponents.reduce((acc: Partial<ComponentProperties>, key) => { // @ts-expect-error acc[key] = entity.components[key]; return acc; }, {}), position: { x: position.x, y: position.y, z: position.z, parentId: system?.id || null, }, }; }), reputation: t.procedure .input(z.object({ entityId: z.number() })) .filter((publish: { entityId: number | null }, { input }) => { if (publish && publish.entityId !== input.entityId) return false;
return true; }) .autoPublish(["reputation"], (entity) => ({ entityId: entity.id })) .request(({ ctx, input }) => { if (!ctx.flight || !input.entityId) return [];
const entity = ctx.flight.ecs.getEntityById(input.entityId); if (!entity?.components.reputation) return []; const reputation: { id: number; name: string; value: number }[] = []; for (const id in entity.components.reputation.reputation) { const value = entity.components.reputation.reputation?.[id]; const reputationEntity = ctx.flight.ecs.getEntityById(Number(id)); if (!reputationEntity) continue; const name = reputationEntity.components.identity?.name; if (!name) continue; reputation.push({ id: Number(id), name, value }); } return reputation; }), setReputation: t.procedure .input( z.object({ entityId: z.number(), targetId: z.number(), value: z.number(), }), ) .send(({ ctx, input }) => { if (!ctx.flight) return; const entity = ctx.flight.ecs.getEntityById(input.entityId); if (!entity) return; entity.updateComponent("reputation", { reputation: { ...entity.components.reputation?.reputation, [input.targetId.toString()]: input.value, }, }); // We pretty much always want to make it mutual const targetEntity = ctx.flight.ecs.getEntityById(input.targetId); targetEntity?.updateComponent("reputation", { reputation: { ...targetEntity.components.reputation?.reputation, [entity.id.toString()]: input.value, }, }); pubsub.publish.starmapCore.reputation({ entityId: input.entityId }); pubsub.publish.starmapCore.reputation({ entityId: input.targetId }); }), spawnSearch: t.procedure .input(z.object({ query: z.string().optional(), allPlugins: z.boolean().optional() })) .autoPublish([], () => null)
.request(({ ctx, input }) => { if (!input.allPlugins && !ctx.flight) return []; const shipTemplates = ctx.server.plugins .filter((p) => (input.allPlugins ? true : ctx.flight?.pluginIds.includes(p.id))) .reduce((acc: ShipPlugin[], plugin) => { return acc.concat(plugin.aspects.ships); }, []);
if (input.query === undefined) { return shipTemplates.map(({ pluginName, name, category, assets: { vanity } }) => ({ id: `${name}-${pluginName}`, pluginName, name, category, vanity, })); } // TODO August 20, 2022: Add faction here too return matchSorter(shipTemplates, input.query, { keys: ["name", "description", "category", "tags"], }) .slice(0, 10) .map(({ pluginName, name, category, assets: { vanity } }) => ({ id: `${name}-${pluginName}`, pluginName, name, category, vanity, })); }), autopilot: t.procedure .input(z.object({ systemId: z.number().nullable() })) .filter((publish: { systemId: number | null }, { input }) => { if (publish && publish.systemId !== input.systemId) return false; return true; }) .autoPublish( ["autopilot"], (entity) => entity.components.position && { systemId: entity.components.position.parentId, }, )
.request(({ ctx, input }) => { const ships: Entity[] = []; for (const system of ctx.ecs.systems) { if (system.constructor.name === "AutoThrustSystem") { for (const [_, entity] of system.entities) { if (entity?.components.position?.parentId === input.systemId) { ships.push(entity); } } } }
type AutopilotInfo = { forwardAutopilot: boolean; destinationName: string; destinationPosition: Coordinates<number> | null; destinationSystemPosition: Coordinates<number> | null; locked: boolean; path: { x: number; y: number; z: number }[]; };
return ( ships?.reduce((acc: { [id: number]: AutopilotInfo }, ship) => { const waypointId = ship.components.autopilot?.destinationWaypointId; let destinationName = ""; let waypoint: Entity | null | undefined; if (typeof waypointId === "number") { waypoint = ctx.flight?.ecs.getEntityById(waypointId); destinationName = waypoint?.components.identity?.name.replace(" Waypoint", "").trim() || ""; } const waypointParentId = waypoint?.components.position?.parentId;
const waypointSystemPosition = typeof waypointParentId === "number" ? ctx.flight?.ecs.getEntityById(waypointParentId)?.components.position || null : null;
acc[ship.id] = { forwardAutopilot: !!ship.components.autopilot?.forwardAutopilot, destinationName, destinationPosition: ship.components.autopilot?.desiredCoordinates || null, destinationSystemPosition: waypointSystemPosition, locked: !!ship.components.autopilot?.desiredCoordinates, path: ship.components.autopilot?.path || [], }; return acc; }, {}) || {} ); }), setDestinations: t.procedure .input( z.object({ ships: z .object({ id: z.number(), position: z.object({ x: z.number(), y: z.number(), z: z.number() }), systemId: z.number().nullable(), }) .array(), }), ) .send(({ ctx, input }) => { const systemIds = new Set<number | null>();
input.ships.forEach((ship) => { const entity = ctx.flight?.ecs.getEntityById(ship.id); const path = ship.systemId === entity?.components.position?.parentId && ship.systemId ? pathfinder(entity, new Vector3(ship.position.x, ship.position.y, ship.position.z)) : []; const nextCoordinates = path?.shift(); entity?.updateComponent("autopilot", { desiredCoordinates: ship.position, desiredRotation: null, desiredSolarSystemId: ship.systemId, path, nextCoordinates, }); entity?.updateComponent("shipBehavior", { actionTarget: { parentId: ship.systemId, x: ship.position.x, y: ship.position.y, z: ship.position.z, }, behaviorTarget: { parentId: ship.systemId, x: ship.position.x, y: ship.position.y, z: ship.position.z, }, }); if (typeof entity?.components.position?.parentId !== "undefined") { systemIds.add(entity.components.position.parentId); } pubsub.publish.pilot.autopilot.get({ shipId: ship.id }); });
systemIds.forEach((id) => { pubsub.publish.starmapCore.autopilot({ systemId: id }); }); }), // This one is just used for timeline actions setShipDestination: t.procedure
.input( z.object({ shipId: z.number(), entityId: z.number().optional(), position: z .object({ parentId: z .union([z.number(), z.object({ name: z.string(), pluginId: z.string() })]) .nullable(), x: z.number(), y: z.number(), z: z.number(), }) .optional(), }), ) .meta({ action: () => { return { position: { name: "Position", type: "starmapCoordinates", helper: "A specific point in space to send the ship. Use as an alternative to Nearby Entity.", }, entityId: { name: "Nearby Entity", helper: "Send the ship somewhere near this entity. This option is preferred.", }, }; }, }) .send(({ ctx, input }) => { const ship = ctx.flight?.ecs.getEntityById(input.shipId); if (!ship) throw new Error("No ship found.");
let position = { x: 0, y: 0, z: 0 }; let systemId: number | null = null; let object: Entity | null = null; if ("entityId" in input) { // This ship is being sent close to an object object = ctx.ecs.getEntityById(input.entityId || -1); if (!object) throw new Error("No object found."); const targetParentId = ship.components.position ? ship.components.position.parentId : ship.components.satellite?.parentId || null; const targetPosition = { parentId: targetParentId, type: targetParentId ? "solar" : "interstellar", ...(ship.components.position || getCompletePositionFromOrbit(ship)), } as const; position = getObjectOffsetPosition( object, targetPosition, (ship.components.size?.length || 1 / 1000) * 2, ); const sys = getObjectSystem(object); systemId = sys?.id ?? null; if (sys?.id === object.id) systemId = null; } else if ("position" in input && input.position) { // This waypoint is just being plopped at some random point in space. position = input.position; const parentId = input.position.parentId; if (parentId && typeof parentId === "object") { // This waypoint is probably defined in a timeline action, so we need // to find which system matches the name. const solarSystems = ctx.flight?.ecs.componentCache.get("isSolarSystem") || []; for (const entity of solarSystems) { if (entity.components.identity?.name === parentId.name) { systemId = entity.id; break; } } } else { systemId = parentId; } } else { throw new Error("Either position or entityId are required"); }
const path = systemId === ship?.components.position?.parentId && systemId ? pathfinder(ship, new Vector3(position.x, position.y, position.z)) : []; const nextCoordinates = path?.shift(); ship?.updateComponent("autopilot", { desiredCoordinates: position, desiredSolarSystemId: systemId, path, nextCoordinates, }); ship?.updateComponent("shipBehavior", { actionTarget: { parentId: systemId, x: position.x, y: position.y, z: position.z, }, behaviorTarget: { parentId: systemId, x: position.x, y: position.y, z: position.z, }, }); pubsub.publish.pilot.autopilot.get({ shipId: ship.id }); pubsub.publish.starmapCore.autopilot({ systemId }); }),
setOrbit: t.procedure .input( z.object({ ships: z.number().array(), objectId: z.number(), }), ) .send(({ ctx, input }) => { const systemIds = new Set<number | null>();
const orbitedObject = ctx.flight?.ecs.getEntityById(input.objectId); if (!orbitedObject) return; const objectSystem = getObjectSystem(orbitedObject); if (!objectSystem) return;
for (const shipId of input.ships) { const entity = ctx.flight?.ecs.getEntityById(shipId); if (!entity) continue; const targetParentId = entity.components.position ? entity.components.position.parentId : entity.components.satellite?.parentId || null; const targetPosition = { parentId: targetParentId, type: targetParentId ? "solar" : "interstellar", ...(entity.components.position || getCompletePositionFromOrbit(entity)), } as const; const position = getObjectOffsetPosition( orbitedObject, targetPosition, (entity.components.size?.length || 1 / 1000) * 2, ); // TODO January 2025: Generate a function which creates an orbit path which the ship can use. entity.updateComponent("autopilot", { desiredCoordinates: position, desiredRotation: null, desiredSolarSystemId: objectSystem.id, }); entity?.updateComponent("shipBehavior", { actionTarget: { parentId: objectSystem.id, x: position.x, y: position.y, z: position.z, }, behaviorTarget: { parentId: objectSystem.id, x: position.x, y: position.y, z: position.z, }, });
if (typeof entity?.components.position?.parentId !== "undefined") { systemIds.add(entity.components.position.parentId); } pubsub.publish.pilot.autopilot.get({ shipId: entity.id }); } systemIds.forEach((id) => { pubsub.publish.starmapCore.autopilot({ systemId: id }); }); }), setFollowShip: t.procedure .meta({ action: true }) .input( z.object({ objective: shipObjectives, ships: z.union([z.number().array(), z.number()]), objectId: z.number(), }), ) .send(({ ctx, input }) => { const systemIds = new Set<number | null>();
const followedObject = ctx.flight?.ecs.getEntityById(input.objectId); if (!followedObject) return;
for (const shipId of Array.isArray(input.ships) ? input.ships : [input.ships]) { const entity = ctx.flight?.ecs.getEntityById(shipId); if (!entity) continue;
entity.updateComponent("shipBehavior", { objective: input.objective, behaviorTarget: followedObject.id, });
if (typeof entity?.components.position?.parentId !== "undefined") { systemIds.add(entity.components.position.parentId); } pubsub.publish.pilot.autopilot.get({ shipId: entity.id }); pubsub.publish.ship.get({ shipId: entity.id }); pubsub.publish.starmapCore.ship({ shipId: entity.id }); } systemIds.forEach((id) => { pubsub.publish.starmapCore.autopilot({ systemId: id }); }); }), fireTorpedo: t.procedure .input( z.object({ objectId: z.number(), position: z.object({ x: z.number(), y: z.number(), z: z.number(), parentId: z.number(), }), }), ) .send(({ ctx, input }) => { const torpedoEntity = new Entity();
const target = ctx.flight?.ecs.getEntityById(input.objectId); if (!target) return; const targetPosition = target.components.position ? new Vector3( target.components.position.x, target.components.position.y, target.components.position.z, ) : new Vector3();
torpedoEntity.addComponent("position", { x: input.position.x, y: input.position.y, z: input.position.z, parentId: input.position.parentId, type: "solar", }); // Point the torpedo at the target const directionVector = targetPosition .clone() .sub(new Vector3(input.position.x, input.position.y, input.position.z)) .normalize() .multiplyScalar(50);
torpedoEntity.addComponent("velocity", { x: directionVector.x, y: directionVector.y, z: directionVector.z, }); torpedoEntity.addComponent("isTorpedo", { launcherId: -1, targetId: input.objectId, yield: 1, damageType: null, color: "white", guidanceMode: "visual", guidanceRange: 5000, speed: 50, maxForce: 10, maxRange: 25000, }); torpedoEntity.addComponent("mass", { mass: 1500 });
ctx.flight?.ecs.addEntity(torpedoEntity);
pubsub.publish.starmapCore.torpedos({ systemId: torpedoEntity.components.position?.parentId || null, }); }), setShipsBehavior: t.procedure .meta({ action: true }) .input( z.object({ shipIds: z.union([z.number().array(), z.number()]), targetId: z.number().optional(), objective: shipObjectives, }), ) .send(({ ctx, input }) => { const ids = Array.isArray(input.shipIds) ? input.shipIds : [input.shipIds]; ids.forEach((shipId) => { const entity = ctx.flight?.ecs.getEntityById(shipId); entity?.updateComponent("shipBehavior", { objective: input.objective, behaviorTarget: input.targetId, });
if (input.objective === "hold") { const position = entity?.components.position; if (position) { entity.updateComponent("shipBehavior", { actionTarget: { parentId: position.parentId || null, x: position.x, y: position.y, z: position.z, }, }); entity.updateComponent("autopilot", { rotationAutopilot: true, forwardAutopilot: true, desiredCoordinates: { x: position.x, y: position.y, z: position.z, }, desiredRotation: null, path: [], nextCoordinates: { x: position.x, y: position.y, z: position.z, }, desiredSolarSystemId: position.parentId || null, }); } } if (input.objective === "patrol") { entity?.updateComponent("shipBehavior", { objective: input.objective, behaviorTarget: { ...entity.components.position! }, }); } pubsub.publish.pilot.autopilot.get({ shipId }); pubsub.publish.ship.get({ shipId }); pubsub.publish.starmapCore.ship({ shipId }); }); }), stream: t.procedure .input(z.object({ systemId: z.number().nullable() })) .dataStream(({ ctx, input }) => { const set = new Set<Entity>(); for (const entity of ctx.ecs.componentCache.get("isShip") || []) { if ( entity.components.position?.type === "interstellar" && (input.systemId === null || input.systemId === undefined) ) { set.add(entity); } if (entity.components.position?.parentId === input.systemId) { set.add(entity); } } for (const entity of ctx.ecs.componentCache.get("isTorpedo") || []) { if ( entity.components.position?.type === "interstellar" && (input.systemId === null || input.systemId === undefined) ) { set.add(entity); } if (entity.components.position?.parentId === input.systemId) { set.add(entity); } }
return set; }),});