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:
parent
59769076ff
commit
5679020b42
3 changed files with 164 additions and 0 deletions
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue