Something went wrong. Try again.
Starship Simulator Controls thoriumsim.com
Something went wrong. Try again.
12 kB · 375 lines
TypeScript
at develop
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376import { pubsub } from "@thorium/.server/init/pubsub";import { t } from "@thorium/.server/init/t";import type { position as positionComponent } from "@thorium/ecs-components/position";import { sound } from "@thorium/ecs-components/sound";import type { SoundEffect } from "@thorium/ecs-components/soundEffect";import { applyCardHighlight } from "@thorium/utils/.server/applyCardHighlight";import { playServerSound } from "@thorium/utils/.server/playRangedSound";import { type EffectPayload, effectOptions, notBridgeStation } from "@thorium/utils/flags/effects";import { randomFromList } from "@thorium/utils/operations/randomFromList";import z from "zod";
type SoundPayload = | { type: "sound"; entityId: number; sound: SoundEffect & { id: string } } | { type: "cancelLooping"; entityId: number; soundId: string } | { type: "stop"; entityId: number; soundId: string } | { type: "stopAll" };
type DialoguePayload = | { type: "dialogue"; conversationId: number; audioFilepath: string; shipId: number; } | { type: "stopDialogue"; conversationId: number; shipId: number };
const stationOrClient = z.union([ z.object({ clientName: z.string() }), z.object({ shipId: z.number(), station: z .union([z.literal("all"), z.literal("bridge"), z.literal("random"), z.string()]) .optional(), }),]);
export const effects = t.router({ sub: t.procedure .input(z.object({ clientId: z.string() })) // This request can only be triggered by a publish. .autoPublish([], () => null) .filter((payload: EffectPayload | null, { ctx, input: { clientId } }) => { if (!payload) return true; const flightClient = ctx.getFlightClient(clientId); const client = ctx.getClient(clientId); const clientData = flightClient?.components.flightClient;
if ("clientId" in payload) { return payload.clientId === clientId || payload.clientId === client?.name; }
if (clientData?.shipId !== payload.shipId) return false;
switch (payload.station) { case "all": break; case "bridge": if (!clientData?.stationId || notBridgeStation.includes(clientData.stationId)) return false; break; default: if (clientData.stationId !== payload.station) return false; }
return true; }) .request(({ publish }) => { if (!publish) return null; return { effect: publish.effect }; }), events: t.procedure .input(z.object({ clientId: z.string() })) // This request can only be triggered by a publish. .autoPublish([], () => null) .filter( ( payload: { clientId: string; name: string; payload: any } | null, { ctx, input: { clientId } }, ) => { if (!payload) return true; const client = ctx.getClient(clientId);
return payload.clientId === clientId || payload.clientId === client?.name; }, ) .request(({ publish }) => { if (!publish) return null; return { name: publish.name, payload: publish.payload }; }), sounds: t.procedure .input(z.object({ clientId: z.string() })) // This request can only be triggered by a publish. .autoPublish([], () => null) .filter((payload: SoundPayload | null, { ctx, input: { clientId } }) => { const clientSettings = ctx.getClient(clientId)?.settings || {}; /** * Logic for filtering out sound payloads * - If the sound has `clients`, it only plays on those clients * - If the sound has `stations` and `ship`, it only plays on those stations * - If the sound has `ship`, it plays on all stations on that ships * - If the sound has `range`, it determines the position of the sound in space, * then finds all simulators within that range and plays the sound on all stations * on those ships * - Otherwise the sound doesn't play */ if (!payload) return false; if (payload.type === "stopAll") return true;
let sound: SoundEffect | null = null;
if (payload.type === "sound") { // Cancel starting a new sound if the the sound isn't targeted to a specific station or client, // and if the client has sound effects turned off if (!payload.sound.stations && !payload.sound.clients && !clientSettings.soundPlayer) { return false; } sound = payload.sound; } const entity = ctx.flight?.ecs.getEntityById(payload.entityId); if (payload.type === "cancelLooping" || payload.type === "stop") { if (!entity) return false; sound = entity.components.soundEffects?.looping?.find((s) => s.id === payload.soundId) || null;
entity.updateComponent("soundEffects", { looping: entity.components.soundEffects?.looping.filter((s) => s.key !== sound?.key) || [], }); }
if (!sound) return false;
const flightClient = ctx.getFlightClient(clientId); const clientData = flightClient?.components.flightClient; const ship = ctx.ecs.getEntityById(clientData?.shipId || -1); return matchSound( sound, clientId, clientData?.shipId, clientData?.stationId, ship?.components.position, ); }) .request(({ publish }) => { if (!publish) return null; return publish; }), /** Sounds that generate ambiance * - Basic ambiance which is applied by the ship itself * - Reactors * - Engines * - Short Range Communications * - Sensors * - Sensor Scans * - Internal Sensors * - Shields * - Tractor Beam * - Stealth Field * - Signal Jammer */ ambiance: t.procedure .input(z.object({ clientId: z.string() })) // This request can only be triggered by a publish. .autoPublish([], () => null) .filter((publish: { shipId?: number; stationId?: string }, { ctx, input: { clientId } }) => { const shipId = ctx.getFlightClient(clientId)?.components.flightClient?.shipId; if (publish && publish.shipId !== shipId) return false; return true; }) .request(({ ctx, input: { clientId } }) => { const loopingSounds: SoundEffect[] = []; const flightClient = ctx.getFlightClient(clientId); const clientData = flightClient?.components.flightClient; const ship = ctx.ecs.getEntityById(clientData?.shipId || -1); ctx.flight?.ecs.componentCache.get("soundEffects")?.forEach((entity) => entity.components.soundEffects?.looping.forEach((sound) => { if ( matchSound( sound, clientId, clientData?.shipId, clientData?.stationId, ship?.components.position, ) ) { loopingSounds.push(sound); } }), ); }), dialogue: t.procedure .input(z.object({ clientId: z.string() })) // This request can only be triggered by a publish. .autoPublish([], () => null) .filter((publish: DialoguePayload, { ctx, input: { clientId } }) => { const shipId = ctx.getFlightClient(clientId)?.components.flightClient?.shipId; const dialoguePlayer = ctx.getClient(clientId).settings.dialoguePlayer; if (!dialoguePlayer) return false;
if (publish && publish.shipId !== shipId) return false; return true; }) .request(({ publish }) => { return publish; }), playSound: t.procedure
.input( z.object({ shipId: z.number(), station: z.union([z.string(), z.string().array()]).optional(), sound, }), ) .meta({ action: () => ({ sound: { name: "Sound Effect", type: "sound", }, station: { name: "Station", type: "text", helper: "Leave blank to play on all stations.", }, }), event: true, }) .send(({ ctx, input }) => { const ship = ctx.ecs.getEntityById(input.shipId); if (!ship) return; playServerSound( ship, input.sound, Array.isArray(input.station) ? input.station : input.station ? [input.station] : undefined, ); }), trigger: t.procedure .meta({ action: true, event: true }) .input( z .object({ effect: effectOptions, }) .and( z.union([ z.object({ clientId: z.string() }), z.object({ shipId: z.number(), station: z.union([z.literal("all"), z.literal("bridge"), z.string()]).optional(), }), ]), ), ) .send(({ ctx, input }) => { // TODO: Properly handle all of the effects that are not handled client-side, such as // offline card transitions. if ("shipId" in input) { const stationList = ctx.flight?.ecs.getEntityById(input.shipId)?.components.stationComplement?.stations || [];
const station = input.station === "random" ? randomFromList(stationList)?.name : input.station;
pubsub.publish.effects.sub({ effect: input.effect, station, shipId: input.shipId, }); } else { pubsub.publish.effects.sub({ effect: input.effect, clientId: input.clientId, }); } }), sendClientEvent: t.procedure .input(z.object({ clientId: z.string(), name: z.string(), payload: z.coerce.string() })) .meta({ action: true }) .send(({ input }) => { pubsub.publish.effects.events(input); }), notify: t.procedure .meta({ action: true }) .input( z .object({ title: z.string(), body: z.string().optional(), duration: z.coerce.number().optional(), color: z.enum(["info", "success", "warning", "error", "notice"]), /** Add a highlight to the indicated cards, if they're present */ cards: z.string().array().optional(), }) .and(stationOrClient), ) .send(({ ctx, input }) => { if ("clientName" in input) { const { clientName: _, ...notification } = input; const clientId = ctx.server.getClientByName(input.clientName)?.id || ""; pubsub.publish.effects.sub({ effect: { type: "message", ...notification, action: { type: "cardChange", cards: input.cards || [] }, }, clientId, });
// Add the card highlight to this client's station const flightClient = ctx.getFlightClient(input.clientName); applyCardHighlight( ctx.ecs, flightClient?.components.flightClient?.shipId || -1, flightClient?.components.flightClient?.stationId, input.cards, );
pubsub.publish.station.get({ clientId }); } else { const { shipId, station, ...notification } = input;
pubsub.publish.effects.sub({ effect: { type: "message", ...notification, action: { type: "cardChange", cards: input.cards || [] }, }, shipId, station, }); // Add the card highlight applyCardHighlight(ctx.ecs, input.shipId, input.station, input.cards); // Find all clients assigned this station for (const client of ctx.ecs.componentCache.get("flightClient") || []) { const flightClient = client.components.flightClient; if ( flightClient && flightClient.shipId === shipId && flightClient.stationId === station ) { pubsub.publish.station.get({ clientId: flightClient.clientId }); } } } }),});
function matchSound( sound: SoundEffect, clientId?: string, shipId?: number | null, stationId?: string | null, shipPosition?: z.infer<typeof positionComponent>,) { if (clientId && sound.clients?.includes(clientId)) return true; if ( sound.stations?.some((s) => s.shipId === shipId! && (!s.stationId || s.stationId === stationId)) ) return true;
if (sound.range) { const position = sound.range.position; if (!shipPosition || shipPosition.parentId !== position.parentId) return false;
const distance = Math.hypot( shipPosition.x - position.x, shipPosition.y - position.y, shipPosition.z - position.z, ); // Sound attenuation will be calculated on the client. if (distance > sound.range.distance) return false; return true; } return false;}