1
0
Fork 0

feat(locker): add locker-storages and loadouts collections

This commit is contained in:
Jason Fraley 2026-08-02 14:32:40 -04:00
parent 9d990d2965
commit 7f4df6597a
6 changed files with 500 additions and 0 deletions

View file

@ -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.",
},
},
],
};

View file

@ -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(","),
},
},
})),
},
],
};

View file

@ -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;
},
],
},
},
],
};

View file

@ -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",

View file

@ -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<false> | BankAccountsSelect<true>;
'bank-transactions': BankTransactionsSelect<false> | BankTransactionsSelect<true>;
'ledger-entries': LedgerEntriesSelect<false> | LedgerEntriesSelect<true>;
'locker-storages': LockerStoragesSelect<false> | LockerStoragesSelect<true>;
loadouts: LoadoutsSelect<false> | LoadoutsSelect<true>;
maps: MapsSelect<false> | MapsSelect<true>;
'narrative-events': NarrativeEventsSelect<false> | NarrativeEventsSelect<true>;
'mission-files': MissionFilesSelect<false> | MissionFilesSelect<true>;
@ -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<T extends boolean = true> {
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<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "locker-storages_select".
*/
export interface LockerStoragesSelect<T extends boolean = true> {
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<T extends boolean = true> {
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<T extends boolean = true> {
intelAssignments?: T;
proximityThreshold?: T;
gameTickIntervalMinutes?: T;
lockerGridWidth?: T;
lockerGridHeight?: T;
updatedAt?: T;
createdAt?: T;
globalType?: T;

View file

@ -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,