From 7f4df6597a39a4ae25442368fe908f6641705cd4 Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Sun, 2 Aug 2026 14:32:40 -0400 Subject: [PATCH] feat(locker): add locker-storages and loadouts collections --- src/collections/game/GameRules.ts | 20 ++ src/collections/locker/Loadouts.ts | 77 ++++++++ src/collections/locker/LockerStorages.ts | 111 +++++++++++ src/collections/logistics/Assets.ts | 58 ++++++ src/payload-types.ts | 228 +++++++++++++++++++++++ src/payload.config.ts | 6 + 6 files changed, 500 insertions(+) create mode 100644 src/collections/locker/Loadouts.ts create mode 100644 src/collections/locker/LockerStorages.ts diff --git a/src/collections/game/GameRules.ts b/src/collections/game/GameRules.ts index 4f5ba76..13a2759 100644 --- a/src/collections/game/GameRules.ts +++ b/src/collections/game/GameRules.ts @@ -56,5 +56,25 @@ export const GameRules: GlobalConfig = { "How often the game tick runs, in minutes. This processes shipment movement, fuel consumption, and arrivals.", }, }, + { + name: "lockerGridWidth", + type: "number", + defaultValue: 10, + min: 4, + admin: { + description: + "Width of each member's personal locker grid, in cells. Applies to newly provisioned lockers.", + }, + }, + { + name: "lockerGridHeight", + type: "number", + defaultValue: 6, + min: 4, + admin: { + description: + "Height of each member's personal locker grid, in cells. Applies to newly provisioned lockers.", + }, + }, ], }; diff --git a/src/collections/locker/Loadouts.ts b/src/collections/locker/Loadouts.ts new file mode 100644 index 0000000..4afd32d --- /dev/null +++ b/src/collections/locker/Loadouts.ts @@ -0,0 +1,77 @@ +import { CollectionConfig } from "payload"; +import { isDeveloper } from "@/utils/access-control/isRole"; +import { LOADOUT_SLOTS } from "@/lib/locker"; + +export const Loadouts: CollectionConfig = { + slug: "loadouts", + admin: { + group: "Locker", + useAsTitle: "name", + defaultColumns: ["name", "ownerUser", "updatedAt"], + }, + access: { + read: ({ req }) => { + if (!req.user) return false; + if (isDeveloper({ req })) return true; + return { ownerUser: { equals: req.user.id } }; + }, + create: ({ req }) => !!req.user, + update: ({ req }) => { + if (!req.user) return false; + if (isDeveloper({ req })) return true; + return { ownerUser: { equals: req.user.id } }; + }, + delete: ({ req }) => { + if (!req.user) return false; + if (isDeveloper({ req })) return true; + return { ownerUser: { equals: req.user.id } }; + }, + }, + fields: [ + { + name: "name", + type: "text", + required: true, + admin: { + description: 'Loadout name (e.g., "CQB", "Overwatch", "Resupply").', + }, + }, + { + name: "description", + type: "textarea", + admin: { + description: "Optional notes about this loadout.", + }, + }, + { + name: "ownerUser", + type: "relationship", + relationTo: "users", + required: true, + admin: { + description: "The operator this loadout belongs to.", + }, + }, + { + name: "slots", + type: "group", + label: "Equipment Slots", + admin: { + description: "Assign assets from your locker to each equipment slot.", + }, + fields: LOADOUT_SLOTS.map((slot) => ({ + name: slot.name, + type: "relationship", + relationTo: "assets", + admin: { + description: slot.description, + }, + filterOptions: { + assetType: { + in: slot.assetTypes.join(","), + }, + }, + })), + }, + ], +}; diff --git a/src/collections/locker/LockerStorages.ts b/src/collections/locker/LockerStorages.ts new file mode 100644 index 0000000..4b98cde --- /dev/null +++ b/src/collections/locker/LockerStorages.ts @@ -0,0 +1,111 @@ +import { CollectionConfig } from "payload"; +import { isDeveloper } from "@/utils/access-control/isRole"; + +export const LockerStorages: CollectionConfig = { + slug: "locker-storages", + admin: { + group: "Locker", + useAsTitle: "name", + defaultColumns: ["name", "ownerUser", "itemCount"], + }, + access: { + read: ({ req }) => { + if (!req.user) return false; + if (isDeveloper({ req })) return true; + return { ownerUser: { equals: req.user.id } }; + }, + create: ({ req }) => !!req.user && isDeveloper({ req }), + update: ({ req }) => { + if (!req.user) return false; + if (isDeveloper({ req })) return true; + return { ownerUser: { equals: req.user.id } }; + }, + delete: isDeveloper, + }, + fields: [ + { + name: "name", + type: "text", + required: true, + defaultValue: "Personal Locker", + admin: { + description: "Display name for this locker.", + }, + }, + { + name: "ownerUser", + type: "relationship", + relationTo: "users", + required: true, + admin: { + description: "The operator who owns this locker.", + }, + }, + { + name: "items", + type: "array", + admin: { + description: "Items stored in the locker grid. Position and rotation are managed from the member locker UI.", + }, + fields: [ + { + name: "asset", + type: "relationship", + relationTo: "assets", + required: true, + }, + { + name: "quantity", + type: "number", + required: true, + min: 1, + defaultValue: 1, + }, + { + name: "gridX", + type: "number", + required: true, + min: 0, + defaultValue: 0, + admin: { + description: "Column position in the locker grid (0-indexed).", + }, + }, + { + name: "gridY", + type: "number", + required: true, + min: 0, + defaultValue: 0, + admin: { + description: "Row position in the locker grid (0-indexed).", + }, + }, + { + name: "rotated", + type: "checkbox", + defaultValue: false, + admin: { + description: "Whether this item is rotated 90 degrees (swaps width and height).", + }, + }, + ], + }, + { + name: "itemCount", + type: "number", + admin: { + readOnly: true, + description: "Total number of distinct entries currently in this locker.", + }, + hooks: { + beforeChange: [ + ({ data }) => { + const items = (data as { items?: unknown[] } | undefined)?.items; + return Array.isArray(items) ? items.length : 0; + }, + ], + }, + }, + ], +}; diff --git a/src/collections/logistics/Assets.ts b/src/collections/logistics/Assets.ts index 2921b5e..9d8b3d3 100644 --- a/src/collections/logistics/Assets.ts +++ b/src/collections/logistics/Assets.ts @@ -489,6 +489,64 @@ export const Assets: CollectionConfig = { }, ], }, + { + name: "storageDimensions", + type: "group", + label: "Storage Dimensions", + admin: { + description: + "How this asset occupies space in member lockers and container grids.", + }, + fields: [ + { + name: "gridWidth", + type: "number", + required: true, + defaultValue: 1, + min: 1, + admin: { + description: "Width of this asset in grid cells.", + }, + }, + { + name: "gridHeight", + type: "number", + required: true, + defaultValue: 1, + min: 1, + admin: { + description: "Height of this asset in grid cells.", + }, + }, + { + name: "stackable", + type: "checkbox", + defaultValue: true, + admin: { + description: + "Can multiple units of this asset stack in a single grid cell? If checked, the amount displayed reflects the stack count.", + }, + }, + { + name: "maxStackSize", + type: "number", + admin: { + description: + "Maximum number of units that can exist in a single grid stack. Leave empty for unlimited. Only applies if stackable is checked.", + condition: (_, siblingData) => siblingData?.stackable === true, + }, + }, + { + name: "rotatable", + type: "checkbox", + defaultValue: true, + admin: { + description: + "Can this asset be rotated 90 degrees in the grid? When rotated, width and height are swapped.", + }, + }, + ], + }, { name: "relatedAssets", type: "relationship", diff --git a/src/payload-types.ts b/src/payload-types.ts index e6e222e..9f3ab0f 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -91,6 +91,8 @@ export interface Config { 'bank-accounts': BankAccount; 'bank-transactions': BankTransaction; 'ledger-entries': LedgerEntry; + 'locker-storages': LockerStorage; + loadouts: Loadout; maps: Map; 'narrative-events': NarrativeEvent; 'mission-files': MissionFile; @@ -133,6 +135,8 @@ export interface Config { 'bank-accounts': BankAccountsSelect | BankAccountsSelect; 'bank-transactions': BankTransactionsSelect | BankTransactionsSelect; 'ledger-entries': LedgerEntriesSelect | LedgerEntriesSelect; + 'locker-storages': LockerStoragesSelect | LockerStoragesSelect; + loadouts: LoadoutsSelect | LoadoutsSelect; maps: MapsSelect | MapsSelect; 'narrative-events': NarrativeEventsSelect | NarrativeEventsSelect; 'mission-files': MissionFilesSelect | MissionFilesSelect; @@ -764,6 +768,31 @@ export interface Asset { batchSize: number; }; }; + /** + * How this asset occupies space in member lockers and container grids. + */ + storageDimensions: { + /** + * Width of this asset in grid cells. + */ + gridWidth: number; + /** + * Height of this asset in grid cells. + */ + gridHeight: number; + /** + * Can multiple units of this asset stack in a single grid cell? If checked, the amount displayed reflects the stack count. + */ + stackable?: boolean | null; + /** + * Maximum number of units that can exist in a single grid stack. Leave empty for unlimited. Only applies if stackable is checked. + */ + maxStackSize?: number | null; + /** + * Can this asset be rotated 90 degrees in the grid? When rotated, width and height are swapped. + */ + rotatable?: boolean | null; + }; /** * Suggested complements to this asset (for example, if the asset was a Bandage, some related assets could include an IV, blood bag, epinephrine, etc.) */ @@ -1090,6 +1119,8 @@ export interface GameEventLog { | 'bank-accounts' | 'bank-transactions' | 'ledger-entries' + | 'locker-storages' + | 'loadouts' ) | null; targetId?: number | null; @@ -1732,6 +1763,127 @@ export interface LedgerEntry { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "locker-storages". + */ +export interface LockerStorage { + id: number; + /** + * Display name for this locker. + */ + name: string; + /** + * The operator who owns this locker. + */ + ownerUser: number | User; + /** + * Items stored in the locker grid. Position and rotation are managed from the member locker UI. + */ + items?: + | { + asset: number | Asset; + quantity: number; + /** + * Column position in the locker grid (0-indexed). + */ + gridX: number; + /** + * Row position in the locker grid (0-indexed). + */ + gridY: number; + /** + * Whether this item is rotated 90 degrees (swaps width and height). + */ + rotated?: boolean | null; + id?: string | null; + }[] + | null; + /** + * Total number of distinct entries currently in this locker. + */ + itemCount?: number | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "loadouts". + */ +export interface Loadout { + id: number; + /** + * Loadout name (e.g., "CQB", "Overwatch", "Resupply"). + */ + name: string; + /** + * Optional notes about this loadout. + */ + description?: string | null; + /** + * The operator this loadout belongs to. + */ + ownerUser: number | User; + /** + * Assign assets from your locker to each equipment slot. + */ + slots?: { + /** + * Primary weapon + */ + primary?: (number | null) | Asset; + /** + * Secondary weapon + */ + secondary?: (number | null) | Asset; + /** + * Launcher / heavy weapon + */ + launcher?: (number | null) | Asset; + /** + * Uniform + */ + uniform?: (number | null) | Asset; + /** + * Carrier / plate carrier + */ + vest?: (number | null) | Asset; + /** + * Backpack / rucksack + */ + backpack?: (number | null) | Asset; + /** + * Helmet + */ + helmet?: (number | null) | Asset; + /** + * Facewear / eyewear + */ + facewear?: (number | null) | Asset; + /** + * Night vision device + */ + nvg?: (number | null) | Asset; + /** + * Radio / comms + */ + radio?: (number | null) | Asset; + /** + * Optic attachment + */ + optic?: (number | null) | Asset; + /** + * Muzzle attachment + */ + muzzle?: (number | null) | Asset; + /** + * Bipod attachment + */ + bipod?: (number | null) | Asset; + }; + updatedAt: string; + createdAt: string; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "narrative-events". @@ -1893,6 +2045,14 @@ export interface PayloadLockedDocument { relationTo: 'ledger-entries'; value: number | LedgerEntry; } | null) + | ({ + relationTo: 'locker-storages'; + value: number | LockerStorage; + } | null) + | ({ + relationTo: 'loadouts'; + value: number | Loadout; + } | null) | ({ relationTo: 'maps'; value: number | Map; @@ -2473,6 +2633,15 @@ export interface AssetsSelect { batchSize?: T; }; }; + storageDimensions?: + | T + | { + gridWidth?: T; + gridHeight?: T; + stackable?: T; + maxStackSize?: T; + rotatable?: T; + }; relatedAssets?: T; isLive?: T; notes?: T; @@ -2737,6 +2906,55 @@ export interface LedgerEntriesSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "locker-storages_select". + */ +export interface LockerStoragesSelect { + name?: T; + ownerUser?: T; + items?: + | T + | { + asset?: T; + quantity?: T; + gridX?: T; + gridY?: T; + rotated?: T; + id?: T; + }; + itemCount?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "loadouts_select". + */ +export interface LoadoutsSelect { + name?: T; + description?: T; + ownerUser?: T; + slots?: + | T + | { + primary?: T; + secondary?: T; + launcher?: T; + uniform?: T; + vest?: T; + backpack?: T; + helmet?: T; + facewear?: T; + nvg?: T; + radio?: T; + optic?: T; + muzzle?: T; + bipod?: T; + }; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "maps_select". @@ -2868,6 +3086,14 @@ export interface GameRule { * How often the game tick runs, in minutes. This processes shipment movement, fuel consumption, and arrivals. */ gameTickIntervalMinutes?: number | null; + /** + * Width of each member's personal locker grid, in cells. Applies to newly provisioned lockers. + */ + lockerGridWidth?: number | null; + /** + * Height of each member's personal locker grid, in cells. Applies to newly provisioned lockers. + */ + lockerGridHeight?: number | null; updatedAt?: string | null; createdAt?: string | null; } @@ -2881,6 +3107,8 @@ export interface GameRulesSelect { intelAssignments?: T; proximityThreshold?: T; gameTickIntervalMinutes?: T; + lockerGridWidth?: T; + lockerGridHeight?: T; updatedAt?: T; createdAt?: T; globalType?: T; diff --git a/src/payload.config.ts b/src/payload.config.ts index f588d78..4923a4b 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -35,6 +35,8 @@ import { Experience } from "@/collections/users/Experience"; import { BankAccounts } from "@/collections/banking/BankAccounts"; import { BankTransactions } from "@/collections/banking/BankTransactions"; import { LedgerEntries } from "@/collections/banking/LedgerEntries"; +import { LockerStorages } from "@/collections/locker/LockerStorages"; +import { Loadouts } from "@/collections/locker/Loadouts"; import { nodemailerAdapter } from "@payloadcms/email-nodemailer"; import nodemailer from "nodemailer"; @@ -104,6 +106,10 @@ export default buildConfig({ BankTransactions, LedgerEntries, + // Locker + LockerStorages, + Loadouts, + // World Maps, NarrativeEvents,