1
0
Fork 0

feat: wire storage actions and collection hooks to emit game events

- Emit events from all storage/grid server actions: addResource,
  removeResource, transferResource, placeResourceOnGrid, moveGridItem,
  mergeGridStacks, rotateGridItem, removeGridItem, splitGridStack,
  retrieveFromVoid, and autoArrangeGrid
- Add afterChange hook on GameStructures to capture admin-panel edits
- Emit structure:resize event from Structures beforeChange hook on grid
  dimension changes
This commit is contained in:
Jason Fraley 2026-07-28 16:08:08 -04:00
parent 59769076ff
commit 5679020b42
3 changed files with 164 additions and 0 deletions

View file

@ -4,6 +4,8 @@ import config from "@payload-config";
import { getPayload } from "payload"; import { getPayload } from "payload";
import { GameStructure, Resource, Structure } from "@/payload-types"; import { GameStructure, Resource, Structure } from "@/payload-types";
import hasRoles from "@/utils/access-control/hasRoles"; import hasRoles from "@/utils/access-control/hasRoles";
import { emitGameEvent } from "@/utils/event-log/emit";
import { EventTypes } from "@/utils/event-log/eventTypes";
interface ActionResult { interface ActionResult {
success: boolean; success: boolean;
@ -221,6 +223,16 @@ export async function addResource(
data: { storedResources: existing }, data: { storedResources: existing },
}); });
await emitGameEvent(payload, {
type: EventTypes.StorageDeposit,
message: `Deposited ${amount.toLocaleString()}× ${resource.name} into "${structure.name}"`,
actor: user.id,
structure: structureId,
targetCollection: "game-structures",
targetId: structureId,
data: { resourceId, amount, resourceName: resource.name },
});
return { success: true, data: updated as unknown as GameStructure }; return { success: true, data: updated as unknown as GameStructure };
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Unknown error"; const message = error instanceof Error ? error.message : "Unknown error";
@ -310,6 +322,17 @@ export async function removeResource(
data: { storedResources, voidStorage }, data: { storedResources, voidStorage },
}); });
const resName = (await payload.findByID({ collection: "resources", id: resourceId, depth: 0 }).catch(() => null))?.name ?? `resource #${resourceId}`;
await emitGameEvent(payload, {
type: EventTypes.StorageWithdraw,
message: `Withdrew ${amount.toLocaleString()}× ${resName} from "${structure.name}"`,
actor: user.id,
structure: structureId,
targetCollection: "game-structures",
targetId: structureId,
data: { resourceId, amount, resourceName: resName },
});
return { success: true, data: updated as unknown as GameStructure }; return { success: true, data: updated as unknown as GameStructure };
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Unknown error"; const message = error instanceof Error ? error.message : "Unknown error";
@ -390,6 +413,17 @@ export async function transferResource(
return { success: false, error: `Transfer failed at destination: ${addResult.error}` }; return { success: false, error: `Transfer failed at destination: ${addResult.error}` };
} }
const resName = resource?.name ?? `resource #${resourceId}`;
await emitGameEvent(payload, {
type: EventTypes.StorageTransfer,
message: `Transferred ${amount.toLocaleString()}× ${resName} from "${fromStructure.name}" to "${toStructure.name}"`,
actor: user.id,
structure: toStructureId,
targetCollection: "game-structures",
targetId: toStructureId,
data: { resourceId, amount, resourceName: resName, fromStructureId, toStructureId, fromStructureName: fromStructure.name, toStructureName: toStructure.name },
});
return { success: true, data: addResult.data }; return { success: true, data: addResult.data };
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Unknown error"; const message = error instanceof Error ? error.message : "Unknown error";
@ -494,6 +528,17 @@ export async function placeResourceOnGrid(
existing.push({ resource: resourceId, amount, gridX, gridY, rotated }); existing.push({ resource: resourceId, amount, gridX, gridY, rotated });
await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } }); await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } });
await emitGameEvent(payload, {
type: EventTypes.GridPlace,
message: `Placed ${amount.toLocaleString()}× ${resource.name} at (${gridX}, ${gridY}) in "${structure.name}"`,
actor: user.id,
structure: structureId,
targetCollection: "game-structures",
targetId: structureId,
data: { resourceId, amount, resourceName: resource.name, gridX, gridY, rotated },
});
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Unknown error"; const message = error instanceof Error ? error.message : "Unknown error";
@ -561,6 +606,17 @@ export async function moveGridItem(
? { ...entry, gridX, gridY, rotated } ? { ...entry, gridX, gridY, rotated }
: { ...entry, gridX, gridY }; : { ...entry, gridX, gridY };
await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } }); await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } });
await emitGameEvent(payload, {
type: EventTypes.GridMove,
message: `Moved ${res.name} to (${gridX}, ${gridY}) in "${structure.name}"`,
actor: user.id,
structure: structureId,
targetCollection: "game-structures",
targetId: structureId,
data: { entryId, resourceId: (typeof res === "object" ? res.id : res), resourceName: res.name, gridX, gridY, rotated },
});
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Unknown error"; const message = error instanceof Error ? error.message : "Unknown error";
@ -617,6 +673,18 @@ export async function mergeGridStacks(
} }
await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } }); await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } });
const mergeResName = typeof srcRes === "object" ? srcRes.name : `resource #${srcResId}`;
await emitGameEvent(payload, {
type: EventTypes.GridMerge,
message: `Merged ${transferAmount}× ${mergeResName} stacks in "${structure.name}"`,
actor: user.id,
structure: structureId,
targetCollection: "game-structures",
targetId: structureId,
data: { sourceId, targetId, resourceId: srcResId, resourceName: mergeResName, transferAmount },
});
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Unknown error"; const message = error instanceof Error ? error.message : "Unknown error";
@ -680,6 +748,17 @@ export async function rotateGridItem(
existing[idx] = { ...entry, rotated: newRotated }; existing[idx] = { ...entry, rotated: newRotated };
await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } }); await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } });
await emitGameEvent(payload, {
type: EventTypes.GridRotate,
message: `Rotated ${res.name} in "${structure.name}"`,
actor: user.id,
structure: structureId,
targetCollection: "game-structures",
targetId: structureId,
data: { entryId, resourceId: (typeof res === "object" ? res.id : res), resourceName: res.name, rotated: newRotated },
});
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Unknown error"; const message = error instanceof Error ? error.message : "Unknown error";
@ -703,8 +782,22 @@ export async function removeGridItem(
const idx = existing.findIndex((e) => e.id === entryId); const idx = existing.findIndex((e) => e.id === entryId);
if (idx < 0) return { success: false, error: "Item not found in grid." }; if (idx < 0) return { success: false, error: "Item not found in grid." };
const removedEntry = existing[idx];
const removedRes = removedEntry.resource as Resource;
existing.splice(idx, 1); existing.splice(idx, 1);
await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } }); await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } });
const removedName = removedRes?.name ?? `resource #${typeof removedRes === "object" ? removedRes.id : removedRes}`;
await emitGameEvent(payload, {
type: EventTypes.StorageWithdraw,
message: `Removed ${removedEntry.amount}× ${removedName} from "${structure.name}"`,
actor: user.id,
structure: structureId,
targetCollection: "game-structures",
targetId: structureId,
data: { entryId, resourceId: typeof removedRes === "object" ? removedRes.id : removedRes, resourceName: removedName, amount: removedEntry.amount },
});
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Unknown error"; const message = error instanceof Error ? error.message : "Unknown error";
@ -791,6 +884,18 @@ export async function splitGridStack(
if (!placed) return { success: false, error: "No empty spot available for the split stack." }; if (!placed) return { success: false, error: "No empty spot available for the split stack." };
await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } }); await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } });
const splitResName = typeof res === "object" ? res.name : `resource #${res}`;
await emitGameEvent(payload, {
type: EventTypes.GridSplit,
message: `Split ${splitAmount}× ${splitResName} from stack in "${structure.name}"`,
actor: user.id,
structure: structureId,
targetCollection: "game-structures",
targetId: structureId,
data: { entryId, resourceId: typeof res === "object" ? res.id : res, resourceName: splitResName, splitAmount, remainingAmount: entry.amount - splitAmount },
});
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Unknown error"; const message = error instanceof Error ? error.message : "Unknown error";
@ -871,6 +976,17 @@ export async function retrieveFromVoid(structureId: number): Promise<GridActionR
}); });
const recovered = voidStorage.length - remainingVoid.length; const recovered = voidStorage.length - remainingVoid.length;
await emitGameEvent(payload, {
type: EventTypes.StorageRecover,
message: recovered > 0
? `Retrieved ${recovered} item(s) from void in "${structure.name}"`
: `No void items could be recovered in "${structure.name}" — grid full`,
actor: user.id,
structure: structureId,
targetCollection: "game-structures",
targetId: structureId,
data: { recovered, remaining: remainingVoid.length, totalVoid: voidStorage.length },
});
return { return {
success: true, success: true,
error: recovered > 0 error: recovered > 0
@ -1025,6 +1141,16 @@ export async function autoArrangeGrid(structureId: number): Promise<GridActionRe
if (recoveredCount > 0) messages.push(`${recoveredCount} item(s) recovered from void.`); if (recoveredCount > 0) messages.push(`${recoveredCount} item(s) recovered from void.`);
if (overflow.length > 0) messages.push(`${overflow.length} item(s) moved to void — grid full.`); if (overflow.length > 0) messages.push(`${overflow.length} item(s) moved to void — grid full.`);
await emitGameEvent(payload, {
type: EventTypes.GridArrange,
message: `Arranged grid in "${structure.name}"${messages.length > 0 ? ` — ${messages.join("; ")}` : ""}`,
actor: user.id,
structure: structureId,
targetCollection: "game-structures",
targetId: structureId,
data: { recoveredCount, overflowCount: overflow.length, totalVoid: voidStorage.length },
});
return { success: true, error: messages.length > 0 ? messages.join(" ") : undefined }; return { success: true, error: messages.length > 0 ? messages.join(" ") : undefined };
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Unknown error"; const message = error instanceof Error ? error.message : "Unknown error";

View file

@ -1,5 +1,6 @@
import { CollectionConfig } from "payload"; import { CollectionConfig } from "payload";
import { isDeveloper } from "@/utils/access-control/isRole"; import { isDeveloper } from "@/utils/access-control/isRole";
import { emitGameEvent } from "@/utils/event-log/emit";
export const GameStructures: CollectionConfig = { export const GameStructures: CollectionConfig = {
slug: "game-structures", slug: "game-structures",
@ -13,6 +14,33 @@ export const GameStructures: CollectionConfig = {
update: isDeveloper, update: isDeveloper,
delete: isDeveloper, delete: isDeveloper,
}, },
hooks: {
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: [ fields: [
{ {
name: "name", name: "name",

View file

@ -1,6 +1,7 @@
import { CollectionConfig } from "payload"; import { CollectionConfig } from "payload";
import { isDeveloper } from "@/utils/access-control/isRole"; import { isDeveloper } from "@/utils/access-control/isRole";
import type { Resource } from "@/payload-types"; import type { Resource } from "@/payload-types";
import { emitGameEvent } from "@/utils/event-log/emit";
export const Structures: CollectionConfig = { export const Structures: CollectionConfig = {
slug: "structures", slug: "structures",
@ -222,6 +223,15 @@ export const Structures: CollectionConfig = {
context: { skipGridRepack: true }, context: { skipGridRepack: true },
}); });
} }
await emitGameEvent(req.payload, {
type: "structure:resize",
message: `Resized "${gsDoc.name}" from ${oldX}×${oldY} to ${newX}×${newY}`,
structure: gsDoc.id,
targetCollection: "game-structures",
targetId: gsDoc.id,
data: { oldX, oldY, newX, newY, structureName: gsDoc.name, voidCount: newVoid.length, repositioned: changed },
});
} }
return data; return data;