diff --git a/src/app/(frontend)/logistics/structures/actions.ts b/src/app/(frontend)/logistics/structures/actions.ts index d20d135..c83f8a9 100644 --- a/src/app/(frontend)/logistics/structures/actions.ts +++ b/src/app/(frontend)/logistics/structures/actions.ts @@ -4,6 +4,8 @@ import config from "@payload-config"; import { getPayload } from "payload"; import { GameStructure, Resource, Structure } from "@/payload-types"; import hasRoles from "@/utils/access-control/hasRoles"; +import { emitGameEvent } from "@/utils/event-log/emit"; +import { EventTypes } from "@/utils/event-log/eventTypes"; interface ActionResult { success: boolean; @@ -221,6 +223,16 @@ export async function addResource( 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 }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; @@ -310,6 +322,17 @@ export async function removeResource( 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 }; } catch (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}` }; } + 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 }; } catch (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 }); 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 }; } catch (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 }; 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 }; } catch (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 } }); + + 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 }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; @@ -680,6 +748,17 @@ export async function rotateGridItem( existing[idx] = { ...entry, rotated: newRotated }; 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 }; } catch (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); 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); 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 }; } catch (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." }; 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 }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; @@ -871,6 +976,17 @@ export async function retrieveFromVoid(structureId: number): Promise 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 { success: true, error: recovered > 0 @@ -1025,6 +1141,16 @@ export async function autoArrangeGrid(structureId: number): Promise 0) messages.push(`${recoveredCount} item(s) recovered from void.`); 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 }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; diff --git a/src/collections/game/GameStructures.ts b/src/collections/game/GameStructures.ts index a234457..6fbda89 100644 --- a/src/collections/game/GameStructures.ts +++ b/src/collections/game/GameStructures.ts @@ -1,5 +1,6 @@ import { CollectionConfig } from "payload"; import { isDeveloper } from "@/utils/access-control/isRole"; +import { emitGameEvent } from "@/utils/event-log/emit"; export const GameStructures: CollectionConfig = { slug: "game-structures", @@ -13,6 +14,33 @@ export const GameStructures: CollectionConfig = { update: 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: [ { name: "name", diff --git a/src/collections/logistics/Structures.ts b/src/collections/logistics/Structures.ts index 49a3d52..028f072 100644 --- a/src/collections/logistics/Structures.ts +++ b/src/collections/logistics/Structures.ts @@ -1,6 +1,7 @@ import { CollectionConfig } from "payload"; import { isDeveloper } from "@/utils/access-control/isRole"; import type { Resource } from "@/payload-types"; +import { emitGameEvent } from "@/utils/event-log/emit"; export const Structures: CollectionConfig = { slug: "structures", @@ -222,6 +223,15 @@ export const Structures: CollectionConfig = { 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;