Something went wrong. Try again.
Starship Simulator Controls thoriumsim.com
Something went wrong. Try again.
27 kB · 865 lines
TypeScript
at develop
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866import { pubsub } from "@thorium/.server/init/pubsub";import { t } from "@thorium/.server/init/t";import { replaceCharacters, rotateCharacters } from "@thorium/cards/LongRangeComm/shared";import type { isLongRangeMessage } from "@thorium/ecs-components/shipSystems";import { getShipSystem } from "@thorium/utils/.server/ship/getShipSystem";import { calculateShipMapPath } from "@thorium/utils/.server/ship/shipMapPathfinder";import { createShipMapGraph } from "@thorium/utils/.server/ship/shipMapPathfinder";import { type ECS, Entity } from "@thorium/utils/ecs";import type { RNG } from "@thorium/utils/rng";import { getObjectSystem } from "@thorium/utils/starmap/position";import { lightMinuteToLightYear, lightYearToLightMinute } from "@thorium/utils/unitTypes";import z from "zod";
const encodingSchema = z.union([ z.object({ type: z.literal("waves"), waveCount: z.number().optional(), hasPhase: z.boolean().optional(), }), z.object({ type: z.literal("replacement"), includedLetters: z.number(), }), z.object({ type: z.literal("rotation"), rotation: z.number() }), z.object({ type: z.literal("decoded") }),]);
export const longRangeComm = t.router({ get: t.procedure .input(z.object({ shipId: z.number() })) .filter((publish: { shipId: number }, { input }) => { if (publish && publish.shipId !== input.shipId) return false; return true; }) .autoPublish( ["isLongRangeComm"], (entity) => entity.components.isLongRangeComm && [ { shipId: entity.components.isShipSystem?.shipId || -1 }, ], ) .request(({ input, ctx }) => { const lrcomm = getShipSystem(ctx.ecs, { systemType: "longRangeComm", shipId: input.shipId, }); if (!lrcomm?.components.isLongRangeComm) throw new Error("No Long Range Comm System"); return { id: lrcomm.id, requiredPower: lrcomm.components.power?.powerLevels[0] || 0, maxSafePower: lrcomm.components.power?.powerLevels.at(-1) || 1, currentPower: lrcomm.components.power?.currentPower || 0, frequency: lrcomm.components.isLongRangeComm.antennaFrequency, gain: lrcomm.components.isLongRangeComm.antennaGain, minSatelliteRange: lrcomm.components.isLongRangeComm.minSatelliteRange, maxSatelliteRange: lrcomm.components.isLongRangeComm.maxSatelliteRange, }; }), commSatellites: t.procedure .autoPublish(["isCommSatellite"], () => null) .request(({ ctx }) => { const satellites = ctx.ecs.componentCache.get("isCommSatellite") || []; const output: { id: number; frequency: number; radius: number; position: [number, number, number]; }[] = [];
for (const satellite of satellites) { const commSatellite = satellite.components.isCommSatellite; if (!commSatellite) continue; const commSatelliteSystem = getObjectSystem(satellite); if (!commSatelliteSystem?.components.position) continue; const { x, y, z } = commSatelliteSystem.components.position; output.push({ id: satellite.id, frequency: commSatellite.frequency, radius: commSatellite.radius, position: [ lightMinuteToLightYear(x), lightMinuteToLightYear(y), lightMinuteToLightYear(z), ], }); }
return output; }), setFrequency: t.procedure .input(z.object({ shipId: z.number(), frequency: z.number() })) .meta({ event: true }) .output(z.object({ shipId: z.number(), frequency: z.number() })) .send(({ ctx, input }) => { const lrcomm = getShipSystem(ctx.ecs, { systemType: "longRangeComm", shipId: input.shipId, }); if (!lrcomm?.components.isLongRangeComm) throw new Error("No Long Range Comm System"); lrcomm.updateComponent("isLongRangeComm", { antennaFrequency: input.frequency, }); pubsub.publish.longRangeComm.get({ shipId: input.shipId }); return input; }), setGain: t.procedure .input(z.object({ shipId: z.number(), gain: z.number() })) .meta({ event: true }) .output(z.object({ shipId: z.number(), gain: z.number() })) .send(({ ctx, input }) => { const lrcomm = getShipSystem(ctx.ecs, { systemType: "longRangeComm", shipId: input.shipId, }); if (!lrcomm?.components.isLongRangeComm) throw new Error("No Long Range Comm System");
lrcomm.updateComponent("isLongRangeComm", { antennaGain: input.gain }); pubsub.publish.longRangeComm.get({ shipId: input.shipId }); return input; }),
systemStream: t.procedure.input(z.object({ shipId: z.number() })).dataStream(({ input, ctx }) => { const set = new Set<Entity>(); for (const entity of ctx.ecs.componentCache.get("isLongRangeComm") || []) { if (entity.components.power && entity.components.isShipSystem?.shipId === input.shipId) { set.add(entity); } }
return set; }), addressBook: t.procedure .input(z.object({ shipId: z.number() })) .filter((publish: { shipId: number }, { input }) => { if (publish && publish.shipId !== input.shipId) return false; return true; }) .autoPublish( ["isLongRangeComm"], (entity) => entity.components.isLongRangeComm && [ { shipId: entity.components.isShipSystem?.shipId || -1 }, ], ) .request(({ input, ctx }) => { try { const lrcomm = getShipSystem(ctx.ecs, { systemType: "longRangeComm", shipId: input.shipId, }); return ( lrcomm.components.isLongRangeComm?.addressBook.map((m) => { const entity = ctx.ecs.getEntityById(m.contactId); return { id: m.contactId, name: m.name, entityName: entity?.components.identity?.name || "Unknown", actions: m.actions, }; }) || [] ); } catch { return []; } }), outgoingMessages: t.procedure .input( z.object({ shipId: z.number(), filter: z.enum(["pending", "sent", "all"]), }), ) .filter((publish: { shipId: number }, { input }) => { if (publish && publish.shipId !== input.shipId) return false; return true; }) .autoPublish( ["isLongRangeMessage"], (entity) => entity.components.isLongRangeMessage && [ { shipId: entity.components.isLongRangeMessage.senderId }, ], ) .request(({ input, ctx }) => { const messages: (Pick< z.infer<typeof isLongRangeMessage>, "message" | "senderStation" | "state" | "destinationId" | "failureReason" | "timestamp" > & { id: number; destinationShipName: string; encodedMessage: string; encodingType: z.infer<typeof isLongRangeMessage>["encoding"]["type"]; })[] = []; const lrcomm = getShipSystem(ctx.ecs, { systemType: "longRangeComm", shipId: input.shipId, }); for (const message of ctx.ecs.componentCache.get("isLongRangeMessage") || []) { if (message.components.isLongRangeMessage?.senderId !== input.shipId) continue; if (input.filter === "sent" && message.components.isLongRangeMessage.state === "pending") continue; if (input.filter === "pending" && message.components.isLongRangeMessage.state !== "pending") continue;
const destinationId = message.components.isLongRangeMessage.destinationId; let destinationShipName = lrcomm.components.isLongRangeComm?.addressBook.find( (f) => f.contactId === destinationId, )?.name; if (!destinationShipName) { const destination = ctx.ecs.getEntityById(destinationId); destinationShipName = destination?.components.identity?.name || "Unknown"; } messages.push({ id: message.id, destinationShipName, message: message.components.isLongRangeMessage.message, encodedMessage: encodeMessage( message.components.isLongRangeMessage.message, message.components.isLongRangeMessage.encoding, ), encodingType: message.components.isLongRangeMessage.encoding.type, destinationId: message.components.isLongRangeMessage.destinationId, senderStation: message.components.isLongRangeMessage.senderStation, state: message.components.isLongRangeMessage.state, failureReason: message.components.isLongRangeMessage.failureReason, timestamp: message.components.isLongRangeMessage.timestamp, }); }
return messages; }), incomingMessages: t.procedure .input(z.object({ shipId: z.number() })) .filter((publish: { shipId: number }, { input }) => { if (publish && publish.shipId !== input.shipId) return false; return true; }) .autoPublish( ["isLongRangeMessage"], (entity) => entity.components.isLongRangeMessage && [ { shipId: entity.components.isLongRangeMessage.destinationId }, ], ) .request(({ input, ctx }) => { const messages: { id: number; senderShipName: string; destinationShipName: string; destinationId: number; timestamp: number; encodedMessage: string; encoding: z.infer<typeof isLongRangeMessage>["encoding"]; unread: boolean; }[] = []; const lrcomm = getShipSystem(ctx.ecs, { systemType: "longRangeComm", shipId: input.shipId, }); for (const longRangeMessage of ctx.ecs.componentCache.get("isLongRangeMessage") || []) { const isLongRangeMessage = longRangeMessage.components.isLongRangeMessage; if (!isLongRangeMessage) continue; const { destinationId, senderId, interceptorId, timestamp, state, message, encoding } = isLongRangeMessage; if ( (destinationId !== input.shipId && interceptorId !== input.shipId) || !["delivered", "intercepted", "read"].includes(state) ) continue;
if (interceptorId === input.shipId && state !== "intercepted") continue;
let senderShipName = lrcomm.components.isLongRangeComm?.addressBook.find( (f) => f.contactId === senderId, )?.name; let destinationShipName = lrcomm.components.isLongRangeComm?.addressBook.find( (f) => f.contactId === destinationId, )?.name; if (!senderShipName) { const sender = ctx.ecs.getEntityById(senderId); senderShipName = sender?.components.identity?.name || "Unknown"; } if (!destinationShipName) { const destination = ctx.ecs.getEntityById(destinationId); destinationShipName = destination?.components.identity?.name || "Unknown"; } messages.push({ id: longRangeMessage.id, senderShipName, destinationShipName, destinationId, timestamp, encodedMessage: encodeMessage(message, encoding), encoding, unread: state !== "read", }); }
messages.sort((a, b) => b.timestamp - a.timestamp);
return messages; }), addToAddressBook: t.procedure .meta({ action: true }) .input( z.object({ shipId: z.number(), contactId: z.number(), name: z.string().optional(), actions: z .object({ params: z.string().array(), intent: z.string(), blocks: z.any().array(), }) .array() .optional(), }), ) .send(({ ctx, input }) => { if (input.shipId === input.contactId) return;
const lrcomm = getShipSystem(ctx.ecs, { systemType: "longRangeComm", shipId: input.shipId, }); lrcomm.updateComponent("isLongRangeComm", { addressBook: [ ...(lrcomm.components.isLongRangeComm?.addressBook || []), { contactId: input.contactId, actions: input.actions || [], name: input.name, }, ], }); pubsub.publish.longRangeComm.addressBook({ shipId: input.shipId }); }), removeFromAddressBook: t.procedure .input( z.object({ shipId: z.number(), contactId: z.number(), }), ) .send(({ ctx, input }) => { const lrcomm = getShipSystem(ctx.ecs, { systemType: "longRangeComm", shipId: input.shipId, }); lrcomm.updateComponent("isLongRangeComm", { addressBook: (lrcomm.components.isLongRangeComm?.addressBook || []).filter( (a) => a.contactId !== input.contactId, ), }); pubsub.publish.longRangeComm.addressBook({ shipId: input.shipId }); }), composeMessage: t.procedure .input( z.object({ senderId: z.number(), destinationId: z.number(), message: z.string(), state: z.enum(["pending", "sending"]).optional(), senderStation: z.string().optional(), encoding: z.enum(["decoded", "waves", "replacement", "rotation"]).optional(), }), ) .output( z.object({ senderId: z.number(), destinationId: z.number(), state: z.string(), messageId: z.number(), }), ) .meta({ action: () => ({ message: { type: "textarea" } }), event: true }) .send(async ({ ctx, input }) => { const message = new Entity();
const encoding = getEncodingSettings(input.encoding, ctx.ecs.rng);
message.addComponent("isLongRangeMessage", { destinationId: input.destinationId, senderId: input.senderId, message: input.message, state: input.state || "pending", timestamp: Date.now(), senderStation: input.senderStation, encoding: generateEncoding(encoding, ctx.ecs.rng), }); ctx.ecs.addEntity(message); if (input.state === "sending") { const ship = ctx.ecs.getEntityById(input.senderId);
if (!ship) throw new Error("Invalid Long Range Message"); try { // Find the closest satellite to the position const satellites = Array.from(ctx.ecs.componentCache.get("isCommSatellite") || []); const satellite = findClosestSatellite(satellites, ship); if (!satellite) throw new Error("First satellite not found"); await ctx.ecs.triggerAction("longRangeComm.sendMessage", { messageId: message.id, satelliteId: satellite.id, }); } catch (e) { console.error(e); message.removeComponent("position"); message.updateComponent("isLongRangeMessage", { state: "undelivered", failureReason: "Failed to find route through communications network", }); } } pubsub.publish.longRangeComm.outgoingMessages({ shipId: input.senderId }); return { senderId: input.senderId, destinationId: input.destinationId, messageId: message.id, state: message.components.isLongRangeMessage?.state || input.state || "pending", }; }), setMessageEncoding: t.procedure .input( z.object({ messageId: z.number(), encoding: z.enum(["decoded", "waves", "replacement", "rotation"]), }), ) .send(({ ctx, input }) => { const message = ctx.ecs.getEntityById(input.messageId); if (!message?.components.isLongRangeMessage) throw new Error("Invalid Long Range Message");
const encoding = getEncodingSettings(input.encoding, ctx.ecs.rng); message.updateComponent("isLongRangeMessage", { encoding: generateEncoding(encoding, ctx.ecs.rng), });
const shipId = message.components.isLongRangeMessage.senderId;
pubsub.publish.longRangeComm.outgoingMessages({ shipId });
return { encodedMessage: encodeMessage( message.components.isLongRangeMessage.message, message.components.isLongRangeMessage.encoding, ), }; }), sendMessage: t.procedure .input( z.object({ messageId: z.number(), satelliteId: z.number(), }), ) .output( z.object({ senderId: z.number(), destinationId: z.number(), state: z.string(), messageId: z.number(), }), ) .meta({ action: true, event: true }) .send(({ ctx, input }) => { const message = ctx.ecs.getEntityById(input.messageId); if (!message?.components.isLongRangeMessage) throw new Error("Invalid Long Range Message"); const shipId = message.components.isLongRangeMessage.senderId; const ship = ctx.ecs.getEntityById(shipId);
if (!ship) throw new Error("Invalid Long Range Message"); const system = getObjectSystem(ship); const position = system?.components.position || ship.components.position; message.addComponent("position", { ...position });
try { const nextNodeId = pickNextLongRangeMessageNode( ctx.ecs, input.satelliteId, message.components.isLongRangeMessage.destinationId, message.components.isLongRangeMessage.visitedNodeIds, ); message.updateComponent("isLongRangeMessage", { nextNodeId, state: "sending", }); } catch (e) { console.error(e); message.removeComponent("position"); message.updateComponent("isLongRangeMessage", { state: "undelivered", failureReason: "Failed to find route through communications network", }); } finally { pubsub.publish.longRangeComm.outgoingMessages({ shipId }); } return { messageId: message.id, destinationId: message.components.isLongRangeMessage.destinationId, senderId: message.components.isLongRangeMessage.senderId, state: message.components.isLongRangeMessage.state, }; }), updateMessageDecoding: t.procedure .input( z.object({ messageId: z.number(), decoding: z.union([ z.object({ type: z.literal("rotation"), rotation: z.number() }), z.object({ type: z.literal("replacement"), letterMap: z.string() }), z.object({ type: z.literal("waves"), waves: z .object({ amplitude: z.number(), frequency: z.number(), phase: z.number(), }) .array(), }), z.object({ type: z.literal("decoded") }), ]), }), ) .send(({ ctx, input }) => { const message = ctx.ecs.getEntityById(input.messageId); if (!message?.components.isLongRangeMessage) throw new Error("Invalid Long Range Message");
const { type: _, ...properties } = input.decoding; const newEncoding = { ...message.components.isLongRangeMessage.encoding, ...("waves" in properties && message.components.isLongRangeMessage.encoding.type === "waves" ? { waves: message.components.isLongRangeMessage.encoding.waves.map((wave, i) => ({ ...wave, ...properties.waves[i], })), } : properties), }; const encodedMessage = encodeMessage( message.components.isLongRangeMessage.message, newEncoding, ); const { state, message: lrmMessage } = message.components.isLongRangeMessage; message.updateComponent("isLongRangeMessage", { encoding: newEncoding, state: state !== "intercepted" && encodedMessage.toLowerCase() === lrmMessage.toLowerCase() ? "read" : state, });
pubsub.publish.longRangeComm.incomingMessages({ shipId: message.components.isLongRangeMessage.destinationId, }); if (message.components.isLongRangeMessage.interceptorId) { pubsub.publish.longRangeComm.incomingMessages({ shipId: message.components.isLongRangeMessage.interceptorId, }); } return { encodedMessage, }; }), forwardInterceptedMessage: t.procedure .input(z.object({ messageId: z.number() })) .send(({ ctx, input }) => { const message = ctx.ecs.getEntityById(input.messageId); if (!message?.components.isLongRangeMessage) throw new Error("Invalid Long Range Message"); const shipId = message.components.isLongRangeMessage.interceptorId; if (!shipId) throw new Error("Invalid Long Range Message");
message.updateComponent("isLongRangeMessage", { state: "sending", }); pubsub.publish.longRangeComm.incomingMessages({ shipId: message.components.isLongRangeMessage.destinationId, });
pubsub.publish.longRangeComm.incomingMessages({ shipId: message.components.isLongRangeMessage.destinationId, }); pubsub.publish.longRangeComm.incomingMessages({ shipId, }); }),});
function getEncodingSettings( encodingType: "decoded" | "waves" | "replacement" | "rotation" | undefined, rng: RNG,) { let encoding: z.infer<typeof encodingSchema> = { type: "decoded" }; switch (encodingType) { case "waves": encoding = { type: "waves", hasPhase: false, waveCount: 1 }; break; case "rotation": encoding = { type: "rotation", rotation: Math.floor(rng.nextAsPercentage() * rotateCharacters.length), }; break; case "replacement": encoding = { type: "replacement", includedLetters: 10 }; break; case "decoded": break; } return encoding;}
function generateEncoding( encoding: z.infer<typeof encodingSchema>, rng: RNG,): z.infer<typeof isLongRangeMessage>["encoding"] { switch (encoding.type) { case "replacement": { let requiredLetterMap = ""; const requiredLetters = replaceCharacters.split(""); while (requiredLetters.length > 0) { const letterIndex = Math.trunc(rng.nextAsPercentage() * requiredLetters.length); const letter = requiredLetters.splice(letterIndex, 1); requiredLetterMap = `${requiredLetterMap}${letter.join("")}`; } let letterMap = replaceCharacters; for (let i = 0; i < encoding.includedLetters; i++) { const splitLetters = letterMap.split(""); const correctLetterIndex = Math.trunc(rng.nextAsPercentage() * splitLetters.length); splitLetters[correctLetterIndex] = requiredLetterMap[correctLetterIndex]; letterMap = splitLetters.join(""); } return { type: "replacement", requiredLetterMap, letterMap }; } case "waves": { const waves: Extract< z.infer<typeof isLongRangeMessage>["encoding"], { type: "waves" } >["waves"] = []; for (let i = 0; i < (encoding.waveCount || 1); i++) { waves.push({ amplitude: 0.25, frequency: 5, phase: 0, requiredAmplitude: rng.nextAsPercentage() * (0.5 - 0.01) + 0.01, requiredFrequency: rng.nextInt(3, 20), requiredPhase: encoding.hasPhase ? rng.nextAsPercentage() * Math.PI : 0, }); } return { type: "waves", waves }; } case "rotation": return { type: "rotation", rotation: 0, requiredRotation: encoding.rotation, }; case "decoded": return { type: "decoded" }; default: encoding satisfies never; return { type: "decoded" }; }}
function encodeMessage(message: string, encoding: z.infer<typeof isLongRangeMessage>["encoding"]) { switch (encoding.type) { case "rotation": { const encodedMessage = rotateMessage( message, rotateCharacters.length - encoding.requiredRotation, ); const decodedMessage = rotateMessage(encodedMessage, encoding.rotation); return decodedMessage; } case "replacement": { const encodedMessage = replaceMessage(message, replaceCharacters, encoding.requiredLetterMap); return replaceMessage(encodedMessage, replaceCharacters, encoding.letterMap); } case "waves": { function getWavePoint( i: number, waves: { frequency: number; phase: number; amplitude: number }[], ) { let point = 0; for (const { frequency, phase, amplitude } of waves) { point += Math.sin(i / 2 / frequency + phase) * amplitude; } return point; }
const output = message .split("") .map((letter, i) => { const letterIndex = rotateCharacters.indexOf(letter); if (letterIndex === -1) return letter; const diff = getWavePoint( i, encoding.waves.map((w) => ({ frequency: w.requiredFrequency, amplitude: w.requiredAmplitude, phase: w.requiredPhase, })), ) - getWavePoint(i, encoding.waves); const newLetterIndex = Math.round(letterIndex + diff * rotateCharacters.length) % rotateCharacters.length; const newLetter = rotateCharacters.split("").at(newLetterIndex); return newLetter; }) .join("");
return output; } case "decoded": return message; }}function rotateMessage(message: string, rotation: number) { let output = ""; const rotatedCharacters = rotateCharacters.slice(rotation) + rotateCharacters.slice(0, rotation);
for (const letter of message.split("")) { const index = rotateCharacters.indexOf(letter); if (index === -1) { output = output + letter; continue; } output = output + rotatedCharacters[index]; } return output;}function replaceMessage(message: string, originalLetterMap: string, letterMap: string) { const splitMessage = message.split(""); return splitMessage .map((letter) => { const mapIndex = originalLetterMap.indexOf(letter.toLowerCase()); if (mapIndex === -1) return letter; return letterMap[mapIndex].toLowerCase(); }) .join("");}
export function pickNextLongRangeMessageNode( ecs: ECS, startId: number, destinationId: number, visitedNodeIds: number[],) { // Figure out the closest node to the message's position const satellites = Array.from(ecs.componentCache.get("isCommSatellite") || []); const destination = ecs.getEntityById(destinationId); if (!destination) throw new Error("Unable to send long range message: Invalid Destination"); const closestEndNode = findClosestSatellite(satellites, destination); if (!closestEndNode) throw new Error("Unable to send long range message: Unable to find comm satellite route");
if (startId === closestEndNode.id) { return startId; }
// Create the graph and determine the path const graph = generateSatelliteGraph(satellites); const nodePath = calculateShipMapPath(graph, startId, closestEndNode.id); if (!nodePath) throw new Error("Unable to send long range message: Unable to find comm satellite route");
for (const nodeId of nodePath) { if (visitedNodeIds.includes(nodeId)) { continue; } return nodeId; }
return nodePath.at(-1)!;}
export function generateSatelliteGraph(satellites: Entity[]) { const nodes: { id: number; radius: number; x: number; y: number; z: number; priorityMultiplier: number; }[] = []; const edgesMap = new Map<string, [number, number]>(); for (const satellite of satellites) { const system = getObjectSystem(satellite); const position = system?.components.position || satellite.components.position; if (!position) continue; nodes.push({ id: satellite.id, x: position.x, y: position.y, z: position.z, radius: lightYearToLightMinute(satellite.components.isCommSatellite?.radius || 0), // We heavily prioritize player ships so they can intercept messages priorityMultiplier: satellite.components.isPlayerShip ? 0.5 : 1, }); } for (const node of nodes) { for (const node2 of nodes) { if (node === node2) continue; const key = [node.id, node2.id].sort((a, b) => a - b).join(","); if (edgesMap.has(key)) continue; const distance = Math.hypot(node.x - node2.x, node.y - node2.y, node.z - node2.z); if (distance <= node.radius || distance <= node2.radius) { edgesMap.set(key, [node.id, node2.id]); } } }
return createShipMapGraph( Array.from(edgesMap.values()).map((e) => ({ from: e[0], to: e[1] })), nodes, );}
export function findClosestSatellite( nodes: Entity[], object: Entity, useRange?: boolean,): { distance: number; id: number } | null { const objectSystem = getObjectSystem(object); const objectPosition = objectSystem?.components.position || object.components.position;
if (!objectPosition) return null; let minDistance: { distance: number; playerShip: boolean; id: number } = { distance: Number.POSITIVE_INFINITY, playerShip: false, id: -1, }; for (const node of nodes) { const system = getObjectSystem(node); const position = system?.components.position || node.components.position; if (!position) continue;
const distance = Math.hypot( objectPosition.x - position.x, objectPosition.y - position.y, objectPosition.z - position.z, ); if (useRange) { const range = node.components.isCommSatellite?.radius; if (!range) continue; if (distance > lightYearToLightMinute(range)) continue; }
if ( distance < minDistance.distance || (distance === minDistance.distance && node.components.isPlayerShip && !minDistance.playerShip) ) { minDistance = { distance, playerShip: !!node.components.isPlayerShip, id: node.id, }; } }
return !minDistance.id ? null : minDistance;}