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.", "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", name: "relatedAssets",
type: "relationship", type: "relationship",

View file

@ -91,6 +91,8 @@ export interface Config {
'bank-accounts': BankAccount; 'bank-accounts': BankAccount;
'bank-transactions': BankTransaction; 'bank-transactions': BankTransaction;
'ledger-entries': LedgerEntry; 'ledger-entries': LedgerEntry;
'locker-storages': LockerStorage;
loadouts: Loadout;
maps: Map; maps: Map;
'narrative-events': NarrativeEvent; 'narrative-events': NarrativeEvent;
'mission-files': MissionFile; 'mission-files': MissionFile;
@ -133,6 +135,8 @@ export interface Config {
'bank-accounts': BankAccountsSelect<false> | BankAccountsSelect<true>; 'bank-accounts': BankAccountsSelect<false> | BankAccountsSelect<true>;
'bank-transactions': BankTransactionsSelect<false> | BankTransactionsSelect<true>; 'bank-transactions': BankTransactionsSelect<false> | BankTransactionsSelect<true>;
'ledger-entries': LedgerEntriesSelect<false> | LedgerEntriesSelect<true>; 'ledger-entries': LedgerEntriesSelect<false> | LedgerEntriesSelect<true>;
'locker-storages': LockerStoragesSelect<false> | LockerStoragesSelect<true>;
loadouts: LoadoutsSelect<false> | LoadoutsSelect<true>;
maps: MapsSelect<false> | MapsSelect<true>; maps: MapsSelect<false> | MapsSelect<true>;
'narrative-events': NarrativeEventsSelect<false> | NarrativeEventsSelect<true>; 'narrative-events': NarrativeEventsSelect<false> | NarrativeEventsSelect<true>;
'mission-files': MissionFilesSelect<false> | MissionFilesSelect<true>; 'mission-files': MissionFilesSelect<false> | MissionFilesSelect<true>;
@ -764,6 +768,31 @@ export interface Asset {
batchSize: number; 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.) * 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-accounts'
| 'bank-transactions' | 'bank-transactions'
| 'ledger-entries' | 'ledger-entries'
| 'locker-storages'
| 'loadouts'
) )
| null; | null;
targetId?: number | null; targetId?: number | null;
@ -1732,6 +1763,127 @@ export interface LedgerEntry {
updatedAt: string; updatedAt: string;
createdAt: 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 * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "narrative-events". * via the `definition` "narrative-events".
@ -1893,6 +2045,14 @@ export interface PayloadLockedDocument {
relationTo: 'ledger-entries'; relationTo: 'ledger-entries';
value: number | LedgerEntry; value: number | LedgerEntry;
} | null) } | null)
| ({
relationTo: 'locker-storages';
value: number | LockerStorage;
} | null)
| ({
relationTo: 'loadouts';
value: number | Loadout;
} | null)
| ({ | ({
relationTo: 'maps'; relationTo: 'maps';
value: number | Map; value: number | Map;
@ -2473,6 +2633,15 @@ export interface AssetsSelect<T extends boolean = true> {
batchSize?: T; batchSize?: T;
}; };
}; };
storageDimensions?:
| T
| {
gridWidth?: T;
gridHeight?: T;
stackable?: T;
maxStackSize?: T;
rotatable?: T;
};
relatedAssets?: T; relatedAssets?: T;
isLive?: T; isLive?: T;
notes?: T; notes?: T;
@ -2737,6 +2906,55 @@ export interface LedgerEntriesSelect<T extends boolean = true> {
updatedAt?: T; updatedAt?: T;
createdAt?: 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 * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "maps_select". * 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. * How often the game tick runs, in minutes. This processes shipment movement, fuel consumption, and arrivals.
*/ */
gameTickIntervalMinutes?: number | null; 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; updatedAt?: string | null;
createdAt?: string | null; createdAt?: string | null;
} }
@ -2881,6 +3107,8 @@ export interface GameRulesSelect<T extends boolean = true> {
intelAssignments?: T; intelAssignments?: T;
proximityThreshold?: T; proximityThreshold?: T;
gameTickIntervalMinutes?: T; gameTickIntervalMinutes?: T;
lockerGridWidth?: T;
lockerGridHeight?: T;
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
globalType?: T; globalType?: T;

View file

@ -35,6 +35,8 @@ import { Experience } from "@/collections/users/Experience";
import { BankAccounts } from "@/collections/banking/BankAccounts"; import { BankAccounts } from "@/collections/banking/BankAccounts";
import { BankTransactions } from "@/collections/banking/BankTransactions"; import { BankTransactions } from "@/collections/banking/BankTransactions";
import { LedgerEntries } from "@/collections/banking/LedgerEntries"; 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 { nodemailerAdapter } from "@payloadcms/email-nodemailer";
import nodemailer from "nodemailer"; import nodemailer from "nodemailer";
@ -104,6 +106,10 @@ export default buildConfig({
BankTransactions, BankTransactions,
LedgerEntries, LedgerEntries,
// Locker
LockerStorages,
Loadouts,
// World // World
Maps, Maps,
NarrativeEvents, NarrativeEvents,