1
0
Fork 0
polaris-task-force/src/collections/game/GameStructures.ts
Z8MB1E b2718b3633 feat(base): add npc staffing and labor classification collections
Adds the npc-staffing and labor-classifications collections, a shared staffing field factory applied to both structure blueprints (Staffing Defaults group) and game structure instances, blueprint-derived staffing defaults on game structure create, staff-hiring toggles on Game Rules, and their database migrations.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-06 01:24:08 -04:00

197 lines
5.2 KiB
TypeScript

import type { CollectionConfig } from "payload";
import { requirePermission } from "@/utils/access-control/hasPermission";
import { emitGameEvent } from "@/utils/event-log/emit";
import { staffingDefaultsFromBlueprint } from "@/lib/base/staffingDefaults";
import { createStaffingFields } from "./staffingFields";
export const GameStructures: CollectionConfig = {
slug: "game-structures",
admin: {
group: "Game",
useAsTitle: "name",
},
access: {
read: ({ req }) => !!req.user,
create: requirePermission("game-structures:create"),
update: requirePermission("game-structures:update"),
delete: requirePermission("game-structures:delete"),
},
hooks: {
beforeChange: [
async ({ data, operation, req }) => {
if (operation !== "create" || !data?.type) return data;
const blueprintId = typeof data.type === "object" ? data.type.id : data.type;
const blueprint = await req.payload.findByID({
collection: "structures",
id: blueprintId,
depth: 1,
overrideAccess: true,
});
const defaults = staffingDefaultsFromBlueprint(blueprint);
return {
...data,
...(data.laborRequirements === undefined
? { laborRequirements: defaults.laborRequirements }
: {}),
...(data.staffPositions === undefined ? { staffPositions: defaults.staffPositions } : {}),
...(data.maintenanceCost === undefined
? { maintenanceCost: defaults.maintenanceCost }
: {}),
};
},
],
afterChange: [
async ({ doc, previousDoc, req }) => {
const d = doc as any;
const prev = previousDoc as any;
if (!prev) return;
if (!req.user) return;
const oldStored = JSON.stringify(prev.storedResources ?? []);
const newStored = JSON.stringify(d.storedResources ?? []);
const oldVoid = JSON.stringify(prev.voidStorage ?? []);
const newVoid = JSON.stringify(d.voidStorage ?? []);
if (oldStored === newStored && oldVoid === newVoid) return;
await emitGameEvent(req.payload, {
type: "storage:withdraw",
message: `Admin updated storage in "${d.name}"`,
actor: req.user.id,
structure: d.id,
targetCollection: "game-structures",
targetId: d.id,
data: {
previousStored: prev.storedResources,
newStored: d.storedResources,
previousVoid: prev.voidStorage,
newVoid: d.voidStorage,
},
});
},
],
},
fields: [
{
name: "name",
type: "text",
label: "Display Name",
admin: {
description: "The name of the structure as displayed in-game and on the frontend.",
},
required: true,
},
{
name: "type",
type: "relationship",
relationTo: "structures",
required: true,
admin: {
description: "The type of structure this is.",
},
},
{
name: "faction",
type: "relationship",
relationTo: "factions",
admin: {
description: "The faction that owns this structure.",
},
},
...createStaffingFields("instance"),
{
label: "Geolocation",
type: "group",
fields: [
{
name: "coordinates",
type: "point",
},
],
},
{
name: "storedResources",
type: "array",
fields: [
{
name: "resource",
type: "relationship",
relationTo: "resources",
required: true,
},
{
name: "amount",
type: "number",
required: true,
min: 1,
},
{
name: "gridX",
type: "number",
required: true,
min: 0,
admin: {
description: "Column position in the inventory grid (0-indexed).",
},
},
{
name: "gridY",
type: "number",
required: true,
min: 0,
admin: {
description: "Row position in the inventory grid (0-indexed).",
},
},
{
name: "rotated",
type: "checkbox",
defaultValue: false,
admin: {
description: "Whether this item is rotated 90 degrees (swaps width and height).",
},
},
],
},
{
name: "voidStorage",
type: "array",
admin: {
description:
"Items that no longer fit in the grid after a resize, stored here until retrieved.",
},
fields: [
{
name: "resource",
type: "relationship",
relationTo: "resources",
required: true,
},
{
name: "amount",
type: "number",
required: true,
min: 1,
},
{
name: "gridX",
type: "number",
required: true,
min: 0,
},
{
name: "gridY",
type: "number",
required: true,
min: 0,
},
{
name: "rotated",
type: "checkbox",
defaultValue: false,
},
],
},
],
};