Something went wrong. Try again.
Starship Simulator Controls thoriumsim.com
Something went wrong. Try again.
21 kB · 624 lines
TSX
at develop
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625import { pubsub } from "@thorium/.server/init/pubsub";import { t } from "@thorium/.server/init/t";import { spawnTorpedo } from "@thorium/.server/spawners/torpedo";import { getCurrentTarget } from "@thorium/.server/systems/PhasersSystem";import { getInventoryTemplates } from "@thorium/utils/.server/getInventoryTemplates";import { cancelLoopingSound, playShipSound } from "@thorium/utils/.server/playRangedSound";import { checkSystemStability } from "@thorium/utils/.server/ship/checkSystemStability";import { getShipSystem, getShipSystems } from "@thorium/utils/.server/ship/getShipSystem";import type { ECS, Entity } from "@thorium/utils/ecs";import { randomFromList } from "@thorium/utils/operations/randomFromList";import z from "zod";
import { calculateCargoUsed, getRoomBySystem } from "../CargoControl/data.server";
export const targeting = t.router({ targetedContact: t.procedure .input(z.object({ shipId: z.number() })) .filter((publish: { shipId: number }, { input }) => { if (publish && publish.shipId !== input.shipId) return false; return true; }) .autoPublish( ["isTargeting"], (entity) => entity.components.isShipSystem && { shipId: entity.components.isShipSystem.shipId, }, ) .request(({ ctx, input }) => { const system = getShipSystem(ctx.ecs, { systemType: "targeting", shipId: input.shipId, }); const target = system.components.isTargeting?.target; if (typeof target !== "number") return null; const targetEntity = ctx.flight?.ecs.getEntityById(target); return targetEntity ? { id: target, name: targetEntity.components.identity?.name, description: targetEntity.components.identity?.description, } : null; }), setTarget: t.procedure .input(z.object({ shipId: z.number(), target: z.union([z.number(), z.null()]) })) .output(z.object({ shipId: z.number(), targetId: z.number().nullable() })) .meta({ event: true }) .send(({ input, ctx }) => { const { shipId } = input; const targeting = getShipSystem(ctx.ecs, { systemType: "targeting", shipId, }); if (!targeting.components.isTargeting) throw new Error("System is not targeting");
targeting.updateComponent("isTargeting", { target: input.target }); pubsub.publish.targeting.targetedContact({ shipId, }); return { shipId, targetId: input.target }; }), torpedoes: t.router({ list: t.procedure .input(z.object({ shipId: z.number() })) .filter((publish: { shipId: number }, { input }) => { if (publish && publish.shipId !== input.shipId) return false; return true; }) .autoPublish([], () => null) .request(({ ctx, input }) => { return getShipTorpedos(ctx.ecs, input.shipId); }), launchers: t.procedure .input(z.object({ shipId: z.number() })) .filter((publish: { shipId: number }, { input }) => { if (publish && publish.shipId !== input.shipId) return false; return true; }) .autoPublish( ["isTorpedoLauncher"], (entity) => entity.components.isShipSystem && { shipId: entity.components.isShipSystem.shipId, }, ) .request(({ ctx, input: { shipId } }) => { if (!ctx.flight) return []; const systems = getShipSystems(ctx.ecs, { systemType: "TorpedoLauncher", shipId, }).filter((system) => system.components.isShipSystem?.shipId === shipId);
return systems.flatMap((system) => { if (!system.components.isTorpedoLauncher) return []; const torpedoEntity = system.components.isTorpedoLauncher?.torpedoEntity; const torpedo = torpedoEntity ? ctx.flight?.ecs.getEntityById(torpedoEntity) : null; return { id: system.id, name: system.components.identity?.name || "Torpedo Launcher", state: system.components.isTorpedoLauncher.status, fireTime: system.components.isTorpedoLauncher.fireTime, loadTime: system.components.isTorpedoLauncher.loadTime, torpedo: torpedo ? { id: torpedo.id, casingColor: torpedo.components.isInventory?.flags.torpedoCasing?.color, warheadColor: torpedo.components.isInventory?.flags.torpedoWarhead?.color, warheadDamageType: torpedo.components.isInventory?.flags.torpedoWarhead?.damageType, guidanceColor: torpedo.components.isInventory?.flags.torpedoGuidance?.color, guidanceMode: torpedo.components.isInventory?.flags.torpedoGuidance?.guidanceMode, } : null, }; }); }), load: t.procedure .input( z.object({ launcherId: z.number(), torpedoId: z.string().nullable(), }), ) .output(z.object({ shipId: z.number() })) .meta({ event: true }) .send(({ input, ctx }) => { const launcher = ctx.ecs.getEntityById(input.launcherId); if (!launcher?.components.isTorpedoLauncher) throw new Error("System is not a torpedo launcher");
checkSystemStability(launcher, "Failed to load torpedo");
if (input.torpedoId && launcher.components.isTorpedoLauncher.status !== "ready") { throw new Error("Torpedo launcher is not ready"); } if (!input.torpedoId && launcher.components.isTorpedoLauncher.status !== "loaded") { throw new Error("Torpedo launcher is not loaded"); } const torpedoEntity = adjustTorpedoInventory(input.torpedoId, launcher);
launcher.updateComponent("isTorpedoLauncher", { status: torpedoEntity ? "loading" : "unloading", progress: launcher.components.isTorpedoLauncher.loadTime, torpedoEntity, });
const ship = ctx.ecs.getEntityById(launcher.components.isShipSystem?.shipId || -1); if (ship) { pubsub.publish.targeting.torpedoes.launchers({ shipId: ship.id, }); if (torpedoEntity) { cancelLoopingSound(launcher, "unload"); playShipSound(launcher, ship, "load"); } else { cancelLoopingSound(launcher, "load"); playShipSound(launcher, ship, "unload"); } } return { shipId: ship?.id || -1 }; }), fire: t.procedure .input( z.object({ launcherId: z.number(), }), ) .output(z.object({ shipId: z.number() })) .meta({ event: true }) .send(({ input, ctx }) => { const launcher = ctx.ecs.getEntityById(input.launcherId);
if (!launcher?.components.isTorpedoLauncher) throw new Error("System is not a torpedo launcher");
cancelLoopingSound(launcher, "load");
checkSystemStability(launcher, "Failed to fire torpedo");
if (launcher.components.isTorpedoLauncher.status !== "loaded") { throw new Error("Torpedo launcher is not loaded"); } const power = launcher.components.power; const powerLevels = power?.powerLevels || [0]; const currentPower = power?.currentPower || 1; const requiredPower = powerLevels[0];
if (currentPower < requiredPower) throw new Error("Insufficient power to fire.");
const inventoryTemplate = ctx.flight?.ecs.getEntityById( launcher.components.isTorpedoLauncher.torpedoEntity!, ); if (!inventoryTemplate) throw new Error("Torpedo not found");
launcher.updateComponent("isTorpedoLauncher", { status: "firing", progress: launcher.components.isTorpedoLauncher.fireTime, firingEnergy: 0, firedClientId: ctx.clientId, }); const ship = ctx.ecs.getEntityById(launcher.components.isShipSystem?.shipId || -1);
if (ship) { pubsub.publish.targeting.torpedoes.launchers({ shipId: ship.id, }); playShipSound(launcher, ship, "firingPowerUp"); }
return { shipId: ship?.id || -1 }; }), // Triggered by the torpedo launcher ECS system, but can be triggered manually to bypass the firing sequence. fired: t.procedure .input(z.object({ launcherId: z.number() })) .output( z.object({ shipId: z.number(), torpedoId: z.number(), targetId: z.number().nullable() }), ) .meta({ event: true }) .send(({ input, ctx }) => { const launcher = ctx.ecs.getEntityById(input.launcherId);
if (!launcher?.components.isTorpedoLauncher) throw new Error("System is not a torpedo launcher"); cancelLoopingSound(launcher, "load"); cancelLoopingSound(launcher, "firingPowerUp"); if (!launcher.components.isTorpedoLauncher.torpedoEntity) { throw new Error("Torpedo launcher is not loaded"); } const torpedo = spawnTorpedo(launcher); launcher.ecs?.addEntity(torpedo); pubsub.publish.starmapCore.torpedos({ systemId: torpedo.components.position?.parentId || null, }); launcher.updateComponent("isTorpedoLauncher", { firedClientId: null, status: "fired", progress: 1000, firingEnergy: 0, torpedoEntity: null, }); const ship = ctx.ecs.getEntityById(launcher.components.isShipSystem?.shipId || -1); if (ship) { pubsub.publish.targeting.torpedoes.launchers({ shipId: ship.id, }); playShipSound(launcher, ship, "fire"); }
return { shipId: ship?.id || -1, torpedoId: torpedo.id, targetId: torpedo.components.isTorpedo!.targetId, }; }), }), hull: t.procedure .input(z.object({ shipId: z.number() })) .filter((publish: { shipId: number }, { input }) => { if (publish && publish.shipId !== input.shipId) return false; return true; }) .autoPublish(["hull"], (entity) => ({ shipId: entity.id }))
.request(({ ctx, input }) => { const ship = ctx.ecs.getEntityById(input.shipId); return ship?.components.hull?.hull || 0; }), shields: 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( ["isShields"], (entity) => entity.components.isShipSystem && { shipId: entity.components.isShipSystem.shipId, }, )
.request(({ ctx, input: { shipId } }) => { if (!ctx.flight) return []; const systems = getShipSystems(ctx.ecs, { systemType: "Shields", shipId, }).filter((system) => system.components.isShipSystem?.shipId === shipId);
return systems.flatMap((system) => { if (!system.components.isShields) return []; return { id: system.id, state: system.components.isShields.state, strength: system.components.isShields.strength, maxStrength: system.components.isShields.maxStrength, direction: system.components.isShields.direction, frequency: system.components.isShields.frequency, }; }); }), setState: t.procedure .input( z.union([ z.object({ shieldId: z.number(), state: z.enum(["up", "down"]), }), z.object({ shipId: z.number(), state: z.enum(["up", "down"]), }), ]), ) .output(z.object({ shipId: z.number(), state: z.enum(["up", "down"]) })) .meta({ event: true }) .send(({ input, ctx }) => { let shipId: number; if ("shieldId" in input) { const shield = getShipSystem(ctx.ecs, { systemId: input.shieldId, }); if (!shield.components.isShields) throw new Error("System is not a shield generator"); shield.updateComponent("isShields", { state: input.state, }); shipId = shield.components.isShipSystem?.shipId || -1; pubsub.publish.targeting.shields.get({ shipId, }); } else { shipId = input.shipId; const shields = getShipSystems(ctx.ecs, { systemType: "Shields", shipId, }).filter((system) => system.components.isShipSystem?.shipId === shipId); for (const shield of shields) { shield.updateComponent("isShields", { state: input.state, }); } pubsub.publish.targeting.shields.get({ shipId, }); } return { shipId, state: input.state }; }), }), phasers: t.router({ list: t.procedure .input(z.object({ shipId: z.number() })) .filter((publish: { shipId: number }, { input }) => { if (publish && publish.shipId !== input.shipId) return false; return true; }) .autoPublish( ["isPhasers"], (entity) => entity.components.isShipSystem && { shipId: entity.components.isShipSystem.shipId, }, )
.request(({ ctx, input }) => { if (!ctx.flight) return []; const systems = getShipSystems(ctx.flight.ecs, { systemType: "Phasers", shipId: input.shipId, }).filter((system) => system.components.isShipSystem?.shipId === input.shipId);
return systems.flatMap((system) => { if (!system.components.isPhasers) return [];
const connectedBattery = ctx.ecs.getEntityById( system.components.power?.batterySource || -1, ); return { id: system.id, name: system.components.identity?.name || "Phasers", firePercent: system.components.isPhasers.firePercent, arc: system.components.isPhasers.arc, heading: system.components.isPhasers.headingDegree, pitch: system.components.isPhasers.pitchDegree, maxOutput: connectedBattery?.components.isBattery?.outputRate || 0, maxRange: system.components.isPhasers.maxRange, maxArc: system.components.isPhasers.maxArc, nominalHeat: system.components.heat?.nominalHeat || 0, maxSafeHeat: system.components.heat?.maxSafeHeat || 1, }; }); }), /** * All of the phasers in a system or the same system as the requesting ship * which are currently being fired. */ firing: t.procedure .input( z.object({ systemId: z.number().nullable(), }), ) .filter((publish: { systemId: number | null }, { input }) => { if (!publish) return true; if ("systemId" in input && publish.systemId !== input.systemId) return false; return true; }) .autoPublish(["isPhasers"], (entity) => { const systemId = entity.ecs.getEntityById(entity.components.isShipSystem?.shipId || -1) ?.components.position?.parentId; if (systemId) return { systemId }; return null; })
.request(({ input, ctx }) => { const systemId = input.systemId;
// Get all of the ships in the system const ships: Entity[] = []; for (const ship of ctx.flight?.ecs.componentCache.get("isShip") || []) { if (ship.components.position?.parentId === systemId) { ships.push(ship); } } const shipIds = ships.map((ship) => ship.id);
// Get all of the ship phasers that are currently firing const firingPhasers: Entity[] = []; const phaserEntities = ctx.flight?.ecs.componentCache.get("isPhasers"); for (const phaser of phaserEntities || []) { if ( shipIds.includes(phaser.components.isShipSystem?.shipId || -1) && phaser.components.isPhasers && phaser.components.isPhasers.firePercent > 0 ) { firingPhasers.push(phaser); } }
return firingPhasers.flatMap((phaser) => { const ship = phaser.ecs?.getEntityById(phaser.components.isShipSystem?.shipId || -1); if (!ship) return []; const target = getCurrentTarget(ship); if (!target) return []; return { id: phaser.id, shipId: phaser.components.isShipSystem?.shipId || -1, targetId: target.id, firePercent: phaser.components.isPhasers?.firePercent || 0, }; }); }), setArc: t.procedure .input( z.object({ phaserId: z.number(), arc: z.number(), }), ) .output(z.object({ phaserId: z.number(), arc: z.number(), shipId: z.number() })) .meta({ event: true }) .send(({ input, ctx }) => { const phaser = getShipSystem(ctx.ecs, { systemId: input.phaserId, }); if (!phaser.components.isPhasers) throw new Error("System is not a phaser");
checkSystemStability(phaser, "Failed to set phaser arc");
phaser.updateComponent("isPhasers", { arc: input.arc, }); pubsub.publish.targeting.phasers.list({ shipId: phaser.components.isShipSystem?.shipId || -1, }); return { ...input, shipId: phaser.components.isShipSystem?.shipId || -1 }; }), fire: t.procedure .input( z.object({ phaserId: z.number(), firePercent: z.number(), }), ) .output(z.object({ shipId: z.number(), phaserId: z.number(), firePercent: z.number() })) .meta({ event: true }) .send(({ input, ctx }) => { const phaser = getShipSystem(ctx.ecs, { systemId: input.phaserId, }); if (!phaser.components.isPhasers) throw new Error("System is not a phaser");
checkSystemStability(phaser, "Failed to fire phaser");
// TODO: Check if the phaser has sufficient power // to be able to fire at the requested power level phaser.updateComponent("isPhasers", { firePercent: input.firePercent, }); const batterySource = ctx.ecs.getEntityById(phaser.components.power?.batterySource || -1); const currentPower = batterySource?.components.isBattery?.storage || 0;
const ship = ctx.flight?.ecs.getEntityById(phaser.components.isShipSystem?.shipId || -1);
pubsub.publish.targeting.phasers.list({ shipId: phaser.components.isShipSystem?.shipId || -1, });
if (input.firePercent === 0 || currentPower < 0.01) { cancelLoopingSound(phaser, "fire"); } else { if (phaser.components.soundEffects?.soundBank.fire) { playShipSound(phaser, ship!, "fire"); } } pubsub.publish.targeting.phasers.firing({ systemId: ship?.components.position?.parentId || null, }); return { shipId: ship?.id || -1, ...input }; }), }), stream: t.procedure.input(z.object({ shipId: z.number() })).dataStream(({ ctx, input }) => { const set = new Set<Entity>(); for (const entity of ctx.ecs.componentCache.get("isShields") || []) { if (entity.components.isShipSystem?.shipId === input.shipId) { set.add(entity); } } for (const entity of ctx.ecs.componentCache.get("isPhasers") || []) { if (entity.components.isShipSystem?.shipId === input.shipId) { set.add(entity); } } return set; }),});
export function adjustTorpedoInventory(torpedoId: string | null, launcher: Entity) { let adjustment = -1; const ecs = launcher.ecs!; const ship = ecs.getEntityById(launcher.components.isShipSystem?.shipId || -1); if (!ship) throw new Error("Torpedo launcher ship not found"); if (!torpedoId) { adjustment = 1; const torpedo = ecs.getEntityById(launcher.components.isTorpedoLauncher?.torpedoEntity || -1); if (!torpedo) throw new Error("Torpedo not found"); torpedoId = torpedo.components.identity?.name || ""; }
const inventoryTemplates = getInventoryTemplates(ecs); const inventoryTemplate = inventoryTemplates[torpedoId]; if (!inventoryTemplate) throw new Error("Torpedo not found");
const torpedo = ecs.getEntityById(inventoryTemplate.entityId); if (!torpedo) throw new Error("Torpedo not found"); const torpedoEntity = adjustment >= 1 ? null : torpedo.id; const torpedoRoom = randomFromList( getRoomBySystem(ship, "torpedoLauncher").filter((room) => { if (adjustment < 1) { return room.contents[torpedoId!]?.count > 0; } const cargoUsed = calculateCargoUsed(room.contents, inventoryTemplates); const volume = room.volume || 0; const torpedoVolume = inventoryTemplates[torpedoId].volume; return cargoUsed + torpedoVolume <= volume; }), );
if (!torpedoRoom) throw new Error("No torpedo room found"); if (adjustment < 1 && torpedoRoom.contents[torpedoId].count <= 0) { throw new Error("No torpedoes available"); } torpedoRoom.contents[torpedoId].count += adjustment;
pubsub.publish.cargoControl.rooms({ shipId: ship.id, }); pubsub.publish.targeting.torpedoes.list({ shipId: ship.id, });
return torpedoEntity;}
export function getShipTorpedos(ecs: ECS, shipId: number) { const ship = ecs.getEntityById(shipId); const templates = getInventoryTemplates(ecs); const torpedoList: Record< string, { count: number; yield: number; speed: number; energy: number } > = {}; function handleContents( contents: Record< string, { count: number; } >, ) { for (const item in contents) { const template = templates[item]; if (!template || !template.flags.torpedoCasing || !template.flags.torpedoWarhead) continue; if (!torpedoList[item]) { torpedoList[item] = { count: 0, yield: template.flags.torpedoWarhead.yield, speed: template.flags.torpedoCasing.speed, energy: template.flags.torpedoCasing.requiredLaunchEnergyMWs, }; } torpedoList[item].count += contents[item].count; } } if (ship?.components.shipMap) { const torpedoRooms = getRoomBySystem(ship, "torpedoLauncher"); for (const room of torpedoRooms) { handleContents(room.contents); } } else if (ship?.components.cargoContainer) { handleContents(ship.components.cargoContainer.contents); }
return torpedoList;}