import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { getPayload, type CollectionSlug, type Payload, type Where } from "payload"; import type { Map as MapDoc, Resource, Structure } from "@/payload-types"; import config from "@payload-config"; import dotenv from "dotenv"; import { processProductionTick } from "@/lib/base/production"; import { installModule, installedModuleEffects, uninstallModule } from "@/lib/base/modules"; dotenv.config({ path: ".env" }); const RUN = `mods-${Date.now()}`; const LEXICAL_EMPTY = { root: { children: [{ text: "" }], direction: null, format: "" as const, indent: 0, type: "text", version: 1, }, }; let payload: Payload; const createdIds = { maps: [] as number[], resources: [] as number[], structureBlueprints: [] as number[], gameStructures: [] as number[], classifications: [] as number[], }; async function makeResource(name: string, massPerUnit = 1): Promise { return (await payload.create({ collection: "resources", data: { name, codeName: `${RUN}-${name.toLowerCase().replace(/\s+/g, "-")}`, approvalStatus: "in_progress", type: "physical", baseValue: 1, rarity: "common", unitOfMeasure: "kg", massPerUnit, gridWidth: 1, gridHeight: 1, }, overrideAccess: true, depth: 0, })) as unknown as Resource; } async function makeBlueprint(data: { name: string; materialResource: number; moduleSlots: number; outputs?: { resource: number; amountPerHour: number }[]; maxCapacityMass?: number; }): Promise { return (await payload.create({ collection: "structures", data: { name: data.name, codeName: data.name.toLowerCase().replace(/\s+/g, "_"), approvalStatus: "in_progress", description: LEXICAL_EMPTY as unknown as Structure["description"], category: "logistics", materials: [{ resource: data.materialResource, amount: 1 }], constructionDurationMinutes: 1, terrainType: "land", maxHealth: 100, maxCapacityMass: data.maxCapacityMass, productionOutputs: data.outputs, moduleSlots: data.moduleSlots, moduleOptions: [ { name: `${RUN} Reinforced Flooring`, category: "logistics", effects: [{ type: "capacity_mass", value: 500 }], materials: [{ resource: data.materialResource, amount: 5 }], }, { name: `${RUN} Overclocked Line`, category: "production", effects: [{ type: "production_multiplier", value: 0.5 }], materials: [{ resource: data.materialResource, amount: 2 }], }, { name: `${RUN} Extra Racking`, category: "logistics", effects: [{ type: "storage_slots", value: 4 }], materials: [{ resource: data.materialResource, amount: 1 }], }, ], }, overrideAccess: true, depth: 0, })) as unknown as Structure; } async function makeSite(data: { name: string; blueprintId: number; materialResource: number; complete?: boolean; }): Promise<{ id: number }> { return (await payload.create({ collection: "game-structures", data: { name: data.name, type: data.blueprintId, map: createdIds.maps[0], coordinates: [500, 500], constructionStatus: data.complete === false ? "building" : "complete", storedResources: [{ resource: data.materialResource, amount: 100, gridX: 0, gridY: 0 }], }, overrideAccess: true, depth: 0, })) as unknown as { id: number }; } async function assignLabor(siteId: number): Promise { const classification = (await payload.create({ collection: "labor-classifications", data: { name: `${RUN} Crew ${siteId}`, key: `${RUN}-crew-${siteId}`, defaultUnitSalary: 1, availableHeadcount: 50, }, overrideAccess: true, depth: 0, })) as unknown as { id: number }; createdIds.classifications.push(classification.id); await payload.create({ collection: "npc-staffing", data: { structure: siteId, positionType: "other", headcount: 5, laborClassification: classification.id, positionTitle: "Labor crew", status: "active", }, overrideAccess: true, }); } async function storedAmountOf( siteId: number, resourceId: number, ): Promise<{ stored: number; void: number; fuel: number }> { const doc = (await payload.findByID({ collection: "game-structures", id: siteId, depth: 1, overrideAccess: true, })) as unknown as { storedResources: { resource: number | Resource; amount: number }[]; voidStorage: { resource: number | Resource; amount: number }[]; }; const sum = (entries: { resource: number | Resource; amount: number }[]): number => entries.reduce( (total, entry) => total + ((typeof entry.resource === "object" ? entry.resource.id : entry.resource) === resourceId ? entry.amount : 0), 0, ); const fuelEntry = doc.voidStorage.find( (entry) => (typeof entry.resource === "object" ? entry.resource.id : entry.resource) === resourceId, ); return { stored: sum(doc.storedResources), void: sum(doc.voidStorage), fuel: fuelEntry?.amount ?? 0, }; } describe("Module system", () => { describe("pure effects", () => { it("aggregates installed module effects against the blueprint catalog", () => { const blueprint = { moduleSlots: 2, moduleOptions: [ { name: "A", effects: [{ type: "capacity_mass", value: 500 }] }, { name: "B", effects: [{ type: "production_multiplier", value: 0.25 }] }, { name: "C", effects: [{ type: "storage_slots", value: 2.7 }] }, ], }; expect(installedModuleEffects(blueprint, ["A", "B", "C"])).toEqual({ capacityMass: 500, productionMultiplier: 0.25, storageSlots: 2, }); // Unknown and empty installs contribute nothing. expect(installedModuleEffects(blueprint, ["Z"])).toEqual({ capacityMass: 0, productionMultiplier: 0, storageSlots: 0, }); expect(installedModuleEffects({}, [])).toEqual({ capacityMass: 0, productionMultiplier: 0, storageSlots: 0, }); }); }); describe("install / uninstall", () => { beforeAll(async () => { payload = await getPayload({ config }); createdIds.maps.push( ( (await payload.create({ collection: "maps", data: { name: `${RUN} map`, worldSizeWidth: 8192, worldSizeHeight: 8192, basemapMode: "image", }, overrideAccess: true, depth: 0, })) as unknown as MapDoc ).id, ); }); afterAll(async () => { const sweep = async (collection: CollectionSlug, where: Where) => { const found = await payload .find({ collection, where, limit: 500, depth: 0, overrideAccess: true }) .catch(() => null); for (const doc of found?.docs ?? []) { await payload.delete({ collection, id: doc.id, overrideAccess: true }).catch(() => {}); } }; await sweep("npc-staffing", { structure: { in: createdIds.gameStructures } }); await sweep("game-event-logs", { structure: { in: createdIds.gameStructures } }); await sweep("game-structures", { name: { like: `${RUN}%` } }); await sweep("structures", { name: { like: `${RUN}%` } }); await sweep("labor-classifications", { name: { like: `${RUN}%` } }); await sweep("resources", { name: { like: `${RUN}%` } }); await sweep("maps", { name: { like: `${RUN}%` } }); }); it("consumes materials, records the module, and enforces slots", async () => { const timber = await makeResource("Timber"); createdIds.resources.push(timber.id); const blueprint = await makeBlueprint({ name: `${RUN} Warehouse`, materialResource: timber.id, moduleSlots: 2, }); createdIds.structureBlueprints.push(blueprint.id); const site = await makeSite({ name: `${RUN} Warehouse site`, blueprintId: blueprint.id, materialResource: timber.id, }); createdIds.gameStructures.push(site.id); await installModule(payload, site.id, `${RUN} Reinforced Flooring`); const afterFirst = await storedAmountOf(site.id, timber.id); expect(afterFirst.stored).toBe(95); await expect(installModule(payload, site.id, `${RUN} Reinforced Flooring`)).rejects.toThrow( "already installed", ); await expect(installModule(payload, site.id, "No Such Module")).rejects.toThrow( "Unknown module", ); await installModule(payload, site.id, `${RUN} Overclocked Line`); await expect(installModule(payload, site.id, `${RUN} Extra Racking`)).rejects.toThrow( "All 2 module slot(s)", ); const uninstalled = await uninstallModule(payload, site.id, `${RUN} Overclocked Line`); expect(uninstalled.map((entry) => entry.name)).toEqual([`${RUN} Reinforced Flooring`]); await expect(uninstallModule(payload, site.id, `${RUN} Overclocked Line`)).rejects.toThrow( "not installed", ); await installModule(payload, site.id, `${RUN} Extra Racking`); }); it("refuses installation outside operational structures", async () => { const timber = await makeResource("Planks"); createdIds.resources.push(timber.id); const blueprint = await makeBlueprint({ name: `${RUN} Yard`, materialResource: timber.id, moduleSlots: 1, }); createdIds.structureBlueprints.push(blueprint.id); const site = await makeSite({ name: `${RUN} Yard site`, blueprintId: blueprint.id, materialResource: timber.id, complete: false, }); createdIds.gameStructures.push(site.id); await expect(installModule(payload, site.id, `${RUN} Reinforced Flooring`)).rejects.toThrow( "operational", ); }); it("applies production multiplier and capacity bonus in the tick", async () => { const timber = await makeResource("Beams"); const fuel = await makeResource("Fuel"); createdIds.resources.push(timber.id, fuel.id); const blueprint = await makeBlueprint({ name: `${RUN} Refinery`, materialResource: timber.id, moduleSlots: 2, outputs: [{ resource: fuel.id, amountPerHour: 1000 }], maxCapacityMass: 100, }); createdIds.structureBlueprints.push(blueprint.id); const site = await makeSite({ name: `${RUN} Refinery site`, blueprintId: blueprint.id, materialResource: timber.id, }); createdIds.gameStructures.push(site.id); await assignLabor(site.id); // Baseline without modules: 1000 kg clamped to the 100 kg cap. const start = new Date(); await processProductionTick(payload, undefined, start, [site.id]); await processProductionTick(payload, undefined, new Date(start.getTime() + 3_600_000), [ site.id, ]); expect((await storedAmountOf(site.id, fuel.id)).fuel).toBe(100); // Install capacity (+500) and multiplier (+50%): output clamps at 600. await installModule(payload, site.id, `${RUN} Reinforced Flooring`); await installModule(payload, site.id, `${RUN} Overclocked Line`); await processProductionTick(payload, undefined, new Date(start.getTime() + 7_200_000), [ site.id, ]); expect((await storedAmountOf(site.id, fuel.id)).fuel).toBe(600); }); }); });