Something went wrong. Try again.
Starship Simulator Controls thoriumsim.com
Something went wrong. Try again.
27 kB · 923 lines
TypeScript
at develop
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924import { ShipSystemTypes } from "@thorium/.server/classes/Plugins/ShipSystems/shipSystemTypes";import type { DataContext } from "@thorium/.server/DataContext";import { pubsub } from "@thorium/.server/init/pubsub";import { t } from "@thorium/.server/init/t";import { findClosestNode } from "@thorium/.server/systems/PassengerMovementSystem";import type { shipMap } from "@thorium/ecs-components/shipMap";import { getInventoryTemplates, getPluginInventoryTemplates,} from "@thorium/utils/.server/getInventoryTemplates";import { getGraph } from "@thorium/utils/.server/ship/shipMapGraph";import { calculateShipMapPath } from "@thorium/utils/.server/ship/shipMapPathfinder";import type { ECS, Entity } from "@thorium/utils/ecs";import { nodeFlags, nodeFlagsSchema, type NodeFlag } from "@thorium/utils/flags/DeckNode";import { randomFromList } from "@thorium/utils/operations/randomFromList";import { matchSorter } from "match-sorter";import z from "zod";
type ShipMapDeckNode = z.infer<typeof shipMap>["deckNodes"][number];
const transferId = z.object({ type: z.union([z.literal("room"), z.literal("entity")]), shipId: z.number(), id: z.number(),});
const cargoRoomsCache = new Map<Entity, ShipMapDeckNode[]>();
const deckRoomOutput = z.object({ shipId: z.number(), deckName: z.string(), deckIndex: z.number(), roomName: z.string(), roomId: z.number(),});export const cargoControl = t.router({ inventoryTypes: t.procedure .autoPublish([], () => null) .request(({ ctx }) => { for (const system of ctx.ecs.systems) { if (system.constructor.name === "FilterInventorySystem") { return Object.fromEntries( Array.from(system?.entities.entries()).map(([, entity]) => [ entity.components.identity?.name, { ...entity.components.identity, ...entity.components.isInventory, }, ]) || [], ); } }
return {}; }), rooms: t.procedure .input(z.object({ shipId: z.number() })) .filter((publish: { shipId: number } | null, { input }) => { if (publish && publish.shipId !== input.shipId) return false; return true; }) .autoPublish(["shipMap"], (entity) => entity.components.shipMap && { shipId: entity.id }) .request(({ ctx, input }) => { const ship = ctx.ecs.getEntityById(input.shipId); if (!ship) throw new Error("No ship selected"); const rooms = getCargoRooms(ship); const decks = ship.components.shipMap?.decks || []; return { rooms, decks, shipLength: ship.components.size?.length || 100, }; }), containers: t.procedure .input(z.object({ shipId: z.number() })) .filter((publish: { shipId: number } | null, { input }) => { if (publish && publish.shipId !== input.shipId) return false; return true; }) .autoPublish(["isCargoContainer", "passengerMovement"], (entity) => entity.components.cargoContainer && entity.components.position?.parentId ? { shipId: entity.components.position.parentId } : null, ) .request(({ ctx, input }) => { const inventoryTemplates = getInventoryTemplates(ctx.ecs); const matchEntities = [...(ctx.ecs.componentCache.get("isCargoContainer") || [])]; return ( matchEntities .filter( (entity) => entity.components.position?.parentId === input.shipId && entity.components.cargoContainer && entity.components.position && entity.components.passengerMovement, ) .map((entity) => { const entityState: "idle" | "enRoute" = entity.components.passengerMovement?.nodePath.length === 0 ? "idle" : "enRoute"; return { id: entity.id, name: entity.components.identity?.name || `Container ${entity.id}`, position: entity.components.position, contents: entity.components.cargoContainer?.contents || {}, used: calculateCargoUsed( entity.components.cargoContainer?.contents || {}, inventoryTemplates, ), volume: entity.components.cargoContainer?.volume || 0, destinationNode: entity.components.passengerMovement?.destinationNode || null, entityState, }; }) || [] ); }), search: t.procedure .input(z.object({ shipId: z.number(), query: z.string() })) .autoPublish([], () => null) .request(({ ctx, input }) => { const ship = ctx.ecs.getEntityById(input.shipId); if (!ship) throw new Error("No ship selected");
const output: { id: number; type: "deck" | "room" | "inventory"; room?: string; count?: number; roomId?: number; name: string; deck: string; deckIndex: number; systemFlags: string[]; }[] = []; // We're searching for decks, rooms, and cargo items. // First decks. ship.components.shipMap?.decks.forEach((deck, i) => { output.push({ id: i, type: "deck", name: deck.name, deck: deck.name, deckIndex: i, systemFlags: [], }); });
// Then rooms. ship.components.shipMap?.deckNodes.forEach((node) => { if (node.isRoom && node.flags?.includes("cargo")) { output.push({ id: node.id, type: "room", name: node.name || "", roomId: node.id, deck: ship.components.shipMap?.decks[node.deckIndex].name || "", deckIndex: node.deckIndex, systemFlags: node.systems, });
// And the cargo items in the room. Object.entries(node.contents).forEach(([name, { count }], i) => { if (count === 0) return; output.push({ id: Number(`${node.id}${i}${count}`), type: "inventory", name, room: node.name, roomId: node.id, count, deck: ship.components.shipMap?.decks[node.deckIndex].name || "", deckIndex: node.deckIndex, systemFlags: [], }); }); } });
return matchSorter(output, input.query, { keys: ["name", "systemFlags"] }).slice(0, 10); }), stream: t.procedure.input(z.object({ shipId: z.number() })).dataStream(({ input, ctx }) => { const set = new Set<Entity>(); for (let entity of ctx.ecs.componentCache.get("isCargoContainer") || []) { if ( entity.components.position?.parentId === input.shipId && entity.components.passengerMovement ) { set.add(entity); } } return set; }), containerSummon: t.procedure .input( z.object({ shipId: z.number(), roomId: z.number(), containerId: z.number().optional(), }), ) .meta({ event: true }) .output( z.object({ shipId: z.number(), roomId: z.number(), containerId: z.number(), }), ) .send(({ ctx, input }) => { const ship = ctx.ecs.getEntityById(input.shipId); if (!ship) throw new Error("No ship selected"); if (!ship.components.shipMap) throw new Error("Invalid ship map."); const graph = getGraph(ship); const room = ship.components.shipMap?.deckNodes.find((d) => d.id === input.roomId); if (!room) throw new Error("No room found");
let container: Entity | null | undefined; if (typeof input.containerId === "number") { container = ctx.flight?.ecs.getEntityById(input.containerId); } else { const matchEntities = [...(ctx.ecs.componentCache.get("isCargoContainer") || [])]; // Find the closest container. container = matchEntities.reduce((acc: Entity | null, entity) => { if ( !entity.components.cargoContainer || !entity.components.position || entity.components.position.parentId !== input.shipId ) return acc; if (!acc) return entity;
// If the entity is busy, skip it if (entity.components.passengerMovement?.nodePath.length) return acc;
// Prioritize entities that are close to the target deck, but not busy. if ( Math.abs(room.deckIndex - (acc.components.position?.z ?? Number.POSITIVE_INFINITY)) < Math.abs(room.deckIndex - entity.components.position.z) ) { // If the acc entity is not busy, use it. return acc; } let accDistance = Number.POSITIVE_INFINITY; if (acc?.components.position) { const { x, y } = acc.components.position; accDistance = Math.hypot(room.x - x, room.y - y); } let entityDistance = Number.POSITIVE_INFINITY; if (entity.components.position) { const { x, y } = entity.components.position; entityDistance = Math.hypot(room.x - x, room.y - y); } if (entityDistance < accDistance) { return entity; } return acc; }, null); }
if (!container?.components.position) throw new Error("No container available.");
const closestNode = findClosestNode( ship.components.shipMap.deckNodes, container.components.position, ); if (!closestNode) throw new Error("No container available.");
const nodePath = calculateShipMapPath(graph, closestNode.id, input.roomId);
if (nodePath) { container.updateComponent("passengerMovement", { nodePath, nextNodeIndex: 0, destinationNode: input.roomId, }); } else { throw new Error("No path to room."); }
if (container.components.position.parentId) { pubsub.publish.cargoControl.containers({ shipId: container.components.position.parentId, }); } return { shipId: ship.id, containerId: container.id, roomId: input.roomId }; }), // Mostly just used for notification purposes containerArrived: t.procedure .input(z.object({ containerId: z.number() })) .output( z.object({ containerId: z.number(), shipId: z.number(), roomId: z.number(), roomName: z.string(), }), ) .meta({ event: true }) .send(({ ctx, input }) => { const container = ctx.ecs.getEntityById(input.containerId); if (!container) throw new Error("Unable to find container"); const shipId = container.components.position?.parentId || -1; const ship = ctx.ecs.getEntityById(shipId); const roomId = container.components.passengerMovement?.destinationNode || -1; const room = ship?.components.shipMap?.deckNodes.find((d) => d.id === roomId);
pubsub.publish.cargoControl.containers({ shipId, }); return { containerId: input.containerId, shipId, roomId, roomName: room?.name || "", }; }), containerMoveToRoomInstant: t.procedure .input(z.object({ containerId: z.number(), roomId: z.number().optional() })) .send(({ input, ctx }) => { const container = ctx.ecs.getEntityById(input.containerId); const ship = ctx.ecs.getEntityById(container?.components.position?.parentId || -1); if (!container || !ship?.components.isShip) throw new Error("Unable to determine ship for cargo container.");
const roomId = input.roomId ?? container?.components.passengerMovement?.destinationNode; const room = ship.components.shipMap?.deckNodes.find((d) => d.id === roomId); if (!room || !roomId) throw new Error("Unable to determine room");
container.updateComponent("passengerMovement", { destinationNode: roomId, nextNodeIndex: 0, nodePath: [roomId], }); container.updateComponent("position", { x: room.x, y: room.y, z: room.deckIndex, });
pubsub.publish.cargoControl.containers({ shipId: ship.id, }); }), transfer: t.procedure .input( z.object({ fromId: transferId, toId: transferId, transfers: z.object({ item: z.string(), count: z.number() }).array(), }), ) .meta({ event: true }) .output( z.object({ toShipId: z.number(), fromShipId: z.number(), toId: z.number(), fromId: z.number(), transferNames: z.string().array(), }), ) .send(({ ctx, input }) => { const fromContainer = getCargoContents(ctx.ecs, input.fromId); if (!fromContainer) throw new Error("No source container found."); const toContainer = getCargoContents(ctx.ecs, input.toId); if (!toContainer) throw new Error("No destination container found.");
const result = transferInventory(ctx.ecs, fromContainer, toContainer, input.transfers);
pubsub.publish.cargoControl.containers({ shipId: input.fromId.shipId, }); pubsub.publish.cargoControl.rooms({ shipId: input.fromId.shipId, }); if (input.fromId.shipId !== input.toId.shipId) { pubsub.publish.cargoControl.containers({ shipId: input.toId.shipId, }); pubsub.publish.cargoControl.rooms({ shipId: input.toId.shipId, }); }
return { toShipId: input.toId.shipId, fromShipId: input.fromId.shipId, toId: input.toId.id, fromId: input.fromId.id, transferNames: Object.keys(result.transferredCounts), }; }), getRoomByFlag: t.procedure
.input( z.object({ shipId: z.number(), flags: z.union([nodeFlagsSchema, nodeFlagsSchema.array()]).optional(), systems: z.array(z.string()).optional(), }), ) .meta({ action: (ctx: DataContext) => { const inventoryTemplates = getInventoryTemplates(ctx.flight?.ecs); const items = Object.keys(inventoryTemplates); return { item: { name: "Inventory Items", type: "select", values: items, }, flags: { name: "Filter Room By Flags", type: "select", inputProps: { multiple: true }, values: nodeFlags, }, systems: { name: "Filter Room By Systems", type: "select", inputProps: { multiple: true }, values: Object.keys(ShipSystemTypes), }, }; }, }) .output(deckRoomOutput) .send(({ ctx, input }) => { const ship = ctx.flight?.ecs.getEntityById(input.shipId); if (!ship) throw new Error("Ship not found.");
const room = getRoomFromFlagsAndSystems(ship, input.flags, input.systems); if ("deckIndex" in room) { return { shipId: ship.id, deckName: room.deck || "Unknown", deckIndex: room.deckIndex, roomName: room.name || "Unknown", roomId: room.id, }; } return { shipId: ship.id, deckName: "", deckIndex: -1, roomName: "", roomId: ship.id }; }), setItemCountInRoom: t.procedure
.input( z.object({ shipId: z.number(), flags: z.union([nodeFlagsSchema, nodeFlagsSchema.array()]).optional(), systems: z.array(z.string()).optional(), item: z.string().optional(), count: z.number(), }), ) .meta({ action: (ctx: DataContext) => { const inventoryTemplates = getInventoryTemplates(ctx.flight?.ecs); const items = Object.keys(inventoryTemplates); return { item: { name: "Inventory Items", type: "select", values: items, }, flags: { name: "Filter Room By Flags", type: "select", inputProps: { multiple: true }, values: nodeFlags, }, systems: { name: "Filter Room By Systems", type: "select", inputProps: { multiple: true }, values: Object.keys(ShipSystemTypes), }, }; }, }) .output(deckRoomOutput) .send(({ ctx, input }) => { const ship = ctx.flight?.ecs.getEntityById(input.shipId); if (!ship) throw new Error("Ship not found.");
const room = getRoomFromFlagsAndSystems(ship, input.flags, input.systems);
const inventoryTemplates = getInventoryTemplates(ctx.flight?.ecs); const inventoryItem = input.item || randomFromList(Object.keys(inventoryTemplates));
if (!inventoryTemplates[inventoryItem]) throw new Error("Inventory item not found.");
if (!room.contents[inventoryItem]) room.contents[inventoryItem] = { count: 0 };
room.contents[inventoryItem].count = input.count;
pubsub.publish.cargoControl.rooms({ shipId: ship.id, });
if ("deckIndex" in room) { return { shipId: ship.id, deckName: room.deck || "Unknown", deckIndex: room.deckIndex, roomName: room.name || "Unknown", roomId: room.id, }; } return { shipId: ship.id, deckName: "", deckIndex: -1, roomName: "", roomId: ship.id }; }),
addItemToRoom: t.procedure
.input( z.object({ shipId: z.number(), flags: z.union([nodeFlagsSchema, nodeFlagsSchema.array()]).optional(), systems: z.array(z.string()).optional(), item: z.string().optional(), count: z.number(), avoidContainer: z.boolean(), }), ) .meta({ action: (ctx: DataContext) => { const inventoryTemplates = getInventoryTemplates(ctx.flight?.ecs); const items = Object.keys(inventoryTemplates); return { item: { name: "Inventory Item", type: "select", values: items, }, flags: { name: "Filter Room By Flags", type: "select", inputProps: { multiple: true }, values: nodeFlags, }, systems: { name: "Filter Room By Systems", type: "select", inputProps: { multiple: true }, values: Object.keys(ShipSystemTypes), }, avoidContainer: { name: "Avoid Container", type: "checkbox", helper: "Choose a room that doesn't already have a container in it.", }, }; }, }) .output(deckRoomOutput) .send(({ ctx, input }) => { const ship = ctx.flight?.ecs.getEntityById(input.shipId); if (!ship) throw new Error("Ship not found.");
const room = getRoomFromFlagsAndSystems( ship, input.flags, input.systems, input.avoidContainer, );
const inventoryTemplates = getInventoryTemplates(ctx.flight?.ecs); const inventoryItem = input.item || randomFromList(Object.keys(inventoryTemplates));
if (!inventoryTemplates[inventoryItem]) throw new Error("Inventory item not found.");
if (!room.contents[inventoryItem]) room.contents[inventoryItem] = { count: 0 };
room.contents[inventoryItem].count += input.count;
pubsub.publish.cargoControl.rooms({ shipId: ship.id, }); if ("deckIndex" in room) { return { shipId: ship.id, deckName: room.deck || "Unknown", deckIndex: room.deckIndex, roomName: room.name || "Unknown", roomId: room.id, }; } return { shipId: ship.id, deckName: "", deckIndex: -1, roomName: "", roomId: ship.id }; }), removeItemFromRoom: t.procedure
.input( z.object({ shipId: z.number(), flags: z.union([nodeFlagsSchema, nodeFlagsSchema.array()]).optional(), systems: z.array(z.string()).optional(), item: z.string(), count: z.number(), }), ) .meta({ action: (ctx: DataContext) => { const inventoryTemplates = getPluginInventoryTemplates(ctx); return { item: { name: "Inventory Items", type: "select", values: inventoryTemplates, }, flags: { name: "Filter Room By Flags", type: "select", inputProps: { multiple: true }, values: nodeFlags, }, systems: { name: "Filter Room By Systems", type: "select", inputProps: { multiple: true }, values: Object.keys(ShipSystemTypes), }, }; }, }) .output(deckRoomOutput) .send(({ ctx, input }) => { const ship = ctx.flight?.ecs.getEntityById(input.shipId); if (!ship) throw new Error("Ship not found.");
const room = getRoomFromFlagsAndSystems(ship, input.flags, input.systems);
if (!room.contents[input.item]) throw new Error("Item not found in room.");
room.contents[input.item].count -= input.count; if (room.contents[input.item].count <= 0) { delete room.contents[input.item]; }
pubsub.publish.cargoControl.rooms({ shipId: ship.id, }); if ("deckIndex" in room) { return { shipId: ship.id, deckName: room.deck || "Unknown", deckIndex: room.deckIndex, roomName: room.name || "Unknown", roomId: room.id, }; } return { shipId: ship.id, deckName: "", deckIndex: -1, roomName: "", roomId: ship.id }; }), emptyRoomInventory: t.procedure
.input( z.object({ shipId: z.number(), flags: z.union([nodeFlagsSchema, nodeFlagsSchema.array()]).optional(), systems: z.array(z.string()).optional(), }), ) .meta({ action: () => ({ flags: { name: "Filter Room By Flags", type: "select", inputProps: { multiple: true }, values: nodeFlags, }, systems: { name: "Filter Room By Systems", type: "select", inputProps: { multiple: true }, values: Object.keys(ShipSystemTypes), }, }), }) .output(deckRoomOutput) .send(({ ctx, input }) => { const ship = ctx.flight?.ecs.getEntityById(input.shipId); if (!ship) throw new Error("Ship not found.");
const room = getRoomFromFlagsAndSystems(ship, input.flags, input.systems);
room.contents = {};
pubsub.publish.cargoControl.rooms({ shipId: ship.id, }); if ("deckIndex" in room) { return { shipId: ship.id, deckName: room.deck || "Unknown", deckIndex: room.deckIndex, roomName: room.name || "Unknown", roomId: room.id, }; } return { shipId: ship.id, deckName: "", deckIndex: -1, roomName: "", roomId: ship.id }; }),});
export function calculateCargoUsed( contents: { [inventoryTemplateName: string]: { count: number }; }, inventory: { [inventoryTemplateName: string]: { volume: number }; },) { if (!contents) return 0; const value = Object.keys(contents).reduce((acc, key) => { const template = inventory[key]; if (!template) { return acc; } return acc + contents[key].count * template.volume; }, 0);
return Math.round(value * 1000) / 1000;}
export function getCargoContents( ecs: ECS, { type, id, shipId }: { type: "room" | "entity"; shipId: number; id: number },) { if (type === "entity") { const entity = ecs.getEntityById(id); const container = entity?.components.cargoContainer; if (!container) return null; return { volume: container.volume, contents: container.contents }; } if (type === "room") { const room = ecs.getEntityById(shipId)?.components.shipMap?.deckNodes.find((d) => d.id === id); if (!room) return null; return { volume: room.volume, contents: room.contents }; } return null;}
export function getCargoRooms(ship: Entity | null) { if (!ship) return [];
const inventoryTemplates = getInventoryTemplates(ship.ecs);
if (!cargoRoomsCache.get(ship)) { cargoRoomsCache.set( ship, ship.components.shipMap?.deckNodes.filter( (node) => node.isRoom && node.flags?.includes("cargo"), ) || [], ); } const rooms = cargoRoomsCache.get(ship)!.map((node) => { return { id: node.id, name: node.name, deckIndex: node.deckIndex, deck: ship?.components.shipMap?.decks[node.deckIndex].name, position: { x: node.x, y: node.y }, volume: node.volume, contents: node.contents, used: calculateCargoUsed(node.contents, inventoryTemplates), flags: node.flags, systems: node.systems, }; }) || [];
return rooms;}
export function getRoomsForInventory(ship: Entity | null, inventoryName: string) { if (!ship) return [];
const cargoRooms = getCargoRooms(ship); return cargoRooms.filter( (r) => inventoryName in r.contents && r.contents[inventoryName].count > 0, );}
export function getRoomByFlag(ship: Entity, flag: NodeFlag) { if (!ship.components.shipMap && ship.components.cargoContainer) { return [ship.components.cargoContainer]; }
const rooms = getCargoRooms(ship);
return rooms.filter((room) => room.flags?.includes(flag));}
export function getRoomBySystem(ship: Entity | null, system: string) { if (!ship?.components.shipMap && ship?.components.cargoContainer) { return [{ id: ship.id, ...ship.components.cargoContainer }]; }
const rooms = getCargoRooms(ship);
return rooms.filter((room) => room.systems?.includes(system));}
function getRoomFromFlagsAndSystems( ship: Entity, flags?: NodeFlag[] | NodeFlag, systems?: string[], avoidContainer?: boolean,): | { volume: number; contents: Record< string, { count: number; } >; } | ReturnType<typeof getCargoRooms>[number] { if (!ship.components.shipMap && ship.components.cargoContainer) { return ship.components.cargoContainer; }
const containerRooms = avoidContainer ? [...(ship.ecs.componentCache.get("isCargoContainer") || [])] .filter((c) => c.components.position?.parentId === ship.id) .flatMap((c) => c.components.passengerMovement?.destinationNode || []) : [];
const rooms = getCargoRooms(ship).filter((room) => { if (avoidContainer && containerRooms.includes(room.id)) return false; if (flags) { for (const flag of Array.isArray(flags) ? flags : [flags]) { if (!room.flags?.includes(flag)) return false; } } if (systems) { for (const system of systems) { if (!room.systems?.includes(system)) return false; } } return true; }); const room = randomFromList(rooms);
if (!room) throw new Error("Room not found.");
return room;}
type Container = { volume: number; contents: Record< string, { count: number; } >;};
export function transferInventory( ecs: ECS, fromContainer: Container, toContainer: Container, transfers: { item: string; count: number; }[],) { const inventoryTemplates = getInventoryTemplates(ecs);
let volumeLeft = toContainer.volume || 0; let errors: string[] = []; const transferredCounts: Record<string, number> = {}; for (const { count, item } of transfers) { // Check to see if the item is in the fromContainer if (!fromContainer.contents[item] || fromContainer.contents[item].count <= 0) { errors.push(`${item} not available`); continue; } const itemVolume = inventoryTemplates[item].volume; const maxItemsByVolume = Math.floor(volumeLeft / itemVolume); if (fromContainer.contents[item].count < count) { errors.push(`Not enough ${item} to completely fulfill request.`); } if (maxItemsByVolume < count) { errors.push(`Not enough space in destination to completely fulfill request.`); } const transferCount = Math.min( count, maxItemsByVolume, fromContainer.contents[item]?.count || 0, ); transferredCounts[item] = transferCount;
if (!toContainer.contents[item]) toContainer.contents[item] = { count: 0 }; fromContainer.contents[item].count -= transferCount; toContainer.contents[item].count += transferCount; volumeLeft -= transferCount * itemVolume; }
return { transferredCounts, errors };}
const listFormatter = new Intl.ListFormat("en-US", { style: "long", type: "conjunction" });const pluralRules = new Intl.PluralRules("en-US");export function inventoryToString(ecs: ECS, inventory: Record<string, number>): string { const inventoryTemplates = getInventoryTemplates(ecs); return listFormatter.format( Object.entries(inventory).map(([key, count]) => { const item = inventoryTemplates[key]; return `${pluralize(count, item.name, item.plural)}`; }), );}
function pluralize(count: number, singular: string, plural: string = singular) { const grammaticalNumber = pluralRules.select(count); switch (grammaticalNumber) { case "one": return count + " " + singular; case "other": return count + " " + plural; default: throw new Error("Unknown: " + grammaticalNumber); }}