1
0
Fork 0

feat(collections): add grid fields to game-structures and resources schemas

GameStructures:
- Add gridX, gridY, rotated fields to storedResources rows
- Remove beforeValidate hook that merged duplicate resources
- Add read access (was missing)
- Add min: 1 constraint on amount

Resources:
- Add gridWidth, gridHeight, stackable, maxStackSize, rotatable fields
- Add read access (was missing)
- Wrap game-rules afterRead hook in try/catch to handle missing global
This commit is contained in:
Jason Fraley 2026-07-28 14:02:35 -04:00
parent 275cb49a92
commit 159755af8d
3 changed files with 136 additions and 60 deletions

View file

@ -1,6 +1,5 @@
import { CollectionConfig } from "payload";
import { isDeveloper } from "@/utils/access-control/isRole";
import { GameStructure } from "@/payload-types";
export const GameStructures: CollectionConfig = {
slug: "game-structures",
@ -9,59 +8,11 @@ export const GameStructures: CollectionConfig = {
useAsTitle: "name",
},
access: {
read: ({ req }) => !!req.user,
create: isDeveloper,
update: isDeveloper,
delete: isDeveloper,
},
hooks: {
beforeValidate: [
({ data }) => {
const rows = data?.storedResources;
if (!Array.isArray(rows) || rows.length === 0) return data;
type NormalizedRow = { id?: string | null; resource: unknown; amount: number };
const byResource = new Map<string, NormalizedRow>();
for (const row of rows) {
if (!row) continue;
// Normalize relationship value to an ID-like scalar when possible
const rawResource = (row as any).resource;
const resourceId =
rawResource && typeof rawResource === "object" && "id" in rawResource
? (rawResource as any).id
: rawResource;
if (resourceId == null) continue;
const resourceKey = String(resourceId);
const amount =
typeof (row as any).amount === "number" && Number.isFinite((row as any).amount)
? (row as any).amount
: 0;
const existing = byResource.get(resourceKey);
if (existing) {
existing.amount += amount;
// Keep the first row's id to preserve admin UI row identity
} else {
byResource.set(resourceKey, {
id: (row as any).id ?? undefined,
resource: resourceId,
amount,
});
}
}
data!.storedResources = Array.from(byResource.values());
return data;
},
],
},
fields: [
{
name: "name",
@ -113,6 +64,33 @@ export const GameStructures: CollectionConfig = {
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).",
},
},
],
},

View file

@ -38,6 +38,7 @@ export const Resources: CollectionConfig = {
group: "Logistics",
},
access: {
read: ({ req }) => !!req.user,
update: onlyIfNotPrimaryCurrency,
delete: onlyIfNotPrimaryCurrency,
},
@ -76,18 +77,22 @@ export const Resources: CollectionConfig = {
hooks: {
afterRead: [
async ({ data, req }) => {
const gameRules = await req.payload.findGlobal({
slug: "game-rules",
depth: 0,
});
try {
const gameRules = await req.payload.findGlobal({
slug: "game-rules",
depth: 0,
});
const primaryCurrencyId =
typeof gameRules.mainCurrency === "object"
? gameRules.mainCurrency?.id
: gameRules.mainCurrency;
const primaryCurrencyId =
typeof gameRules.mainCurrency === "object"
? gameRules.mainCurrency?.id
: gameRules.mainCurrency;
if (primaryCurrencyId) {
return data?.id === primaryCurrencyId;
if (primaryCurrencyId) {
return data?.id === primaryCurrencyId;
}
} catch (_) {
// Silently fail — game-rules global may not exist yet
}
},
],
@ -281,6 +286,59 @@ export const Resources: CollectionConfig = {
"Maximum amount that fits in a single standard cargo slot (if applicable.)",
},
},
{
label: "Grid Dimensions",
type: "group",
fields: [
{
name: "gridWidth",
type: "number",
required: true,
defaultValue: 1,
min: 1,
admin: {
description: "Width of this resource in grid cells.",
},
},
{
name: "gridHeight",
type: "number",
required: true,
defaultValue: 1,
min: 1,
admin: {
description: "Height of this resource in grid cells.",
},
},
{
name: "stackable",
type: "checkbox",
defaultValue: true,
admin: {
description:
"Can multiple units of this resource stack in a single grid cell? If checked, the amount displayed on the grid 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 resource be rotated 90 degrees in the grid? When rotated, width and height are swapped.",
},
},
],
},
],
},
],

View file

@ -239,6 +239,18 @@ export interface GameStructure {
| {
resource: number | Resource;
amount: number;
/**
* Column position in the inventory grid (0-indexed).
*/
gridX: number;
/**
* Row position in the inventory grid (0-indexed).
*/
gridY: number;
/**
* Whether this item is rotated 90 degrees (swaps width and height).
*/
rotated?: boolean | null;
id?: string | null;
}[]
| null;
@ -516,6 +528,26 @@ export interface Resource {
* Maximum amount that fits in a single standard cargo slot (if applicable.)
*/
stackSize?: number | null;
/**
* Width of this resource in grid cells.
*/
gridWidth: number;
/**
* Height of this resource in grid cells.
*/
gridHeight: number;
/**
* Can multiple units of this resource stack in a single grid cell? If checked, the amount displayed on the grid 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 resource be rotated 90 degrees in the grid? When rotated, width and height are swapped.
*/
rotatable?: boolean | null;
updatedAt: string;
createdAt: string;
}
@ -1558,6 +1590,9 @@ export interface GameStructuresSelect<T extends boolean = true> {
| {
resource?: T;
amount?: T;
gridX?: T;
gridY?: T;
rotated?: T;
id?: T;
};
updatedAt?: T;
@ -1982,6 +2017,11 @@ export interface ResourcesSelect<T extends boolean = true> {
unitOfMeasure?: T;
massPerUnit?: T;
stackSize?: T;
gridWidth?: T;
gridHeight?: T;
stackable?: T;
maxStackSize?: T;
rotatable?: T;
updatedAt?: T;
createdAt?: T;
}