Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
350 lines
12 KiB
TypeScript
350 lines
12 KiB
TypeScript
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<Resource> {
|
|
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<Structure> {
|
|
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<void> {
|
|
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);
|
|
});
|
|
});
|
|
});
|