"use server"; import config from "@payload-config"; import { getPayload } from "payload"; import { GameStructure, Resource, Structure } from "@/payload-types"; import { isSuperuser } from "@/utils/access-control/hasPermission"; import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification"; import { emitGameEvent } from "@/utils/event-log/emit"; import { EventTypes } from "@/utils/event-log/eventTypes"; import { checkStorageDeposit, storageViolationMessage } from "@/lib/storageRules"; interface ActionResult { success: boolean; error?: string; data?: GameStructure; } type PayloadClient = Awaited>; type AuthedUser = Awaited>["user"]; // SECURITY: creates/destroys stored items, overriding economy/production — superusers only async function canManageStorage(payload: PayloadClient, user: AuthedUser): Promise { return isSuperuser(payload, user); } // SECURITY: conserves quantities (moves items around) — logistics personnel + superusers async function canHandleStorage(payload: PayloadClient, user: AuthedUser): Promise { return (await isSuperuser(payload, user)) || (await hasLogisticsQualification(payload, user)); } async function authenticate() { const payloadConfig = await config; const payload = await getPayload({ config: payloadConfig }); const { headers } = await import("next/headers"); const hdrs = await headers(); const { user } = await payload.auth({ headers: hdrs, canSetHeaders: false }); if (!user) { throw new Error("Unauthorized"); } return { payload, user }; } async function getStructure(payload: Awaited>, id: number) { const structure = await payload .findByID({ collection: "game-structures", id, depth: 2, }) .catch(() => null); if (!structure) { throw new Error("Structure not found"); } return structure as unknown as GameStructure & { type: Structure }; } function getStoredAmount( storedResources: GameStructure["storedResources"], resourceId: number, ): number { if (!storedResources) return 0; const entry = storedResources.find((e) => { const res = e.resource; return typeof res === "object" && res !== null && "id" in res ? res.id === resourceId : res === resourceId; }); return entry?.amount ?? 0; } function calculateMass(storedResources: GameStructure["storedResources"]): number { if (!storedResources) return 0; return storedResources.reduce((total, entry) => { const resource = entry.resource as Resource; if (!resource || typeof resource === "number") return total; return total + (resource.massPerUnit ?? 0) * entry.amount; }, 0); } export async function addResource( structureId: number, resourceId: number, amount: number, ): Promise { try { const { payload, user } = await authenticate(); if (!(await canManageStorage(payload, user))) { return { success: false, error: "Only superusers may deposit resources into structures.", }; } return await addResourceInternal(payload, user, structureId, resourceId, amount); } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; if (message === "Unauthorized") { return { success: false, error: "You must be logged in." }; } return { success: false, error: message }; } } async function addResourceInternal( payload: PayloadClient, user: AuthedUser, structureId: number, resourceId: number, amount: number, ): Promise { try { if (amount <= 0) { return { success: false, error: "Amount must be greater than zero." }; } const structure = await getStructure(payload, structureId); const structureType = structure.type; // Fetch the resource to check rules + mass const resource = await payload .findByID({ collection: "resources", id: resourceId, depth: 0, }) .catch(() => null); if (!resource) { return { success: false, error: "Resource not found." }; } // Check storage rules (prohibited → whitelist → per-item cap) const currentStored = getStoredAmount(structure.storedResources, resourceId) + getStoredAmount(structure.voidStorage ?? [], resourceId); const ruleCheck = checkStorageDeposit(structureType, resourceId, amount, currentStored); if (!ruleCheck.allowed) { return { success: false, error: storageViolationMessage(resource.name, ruleCheck), }; } // Check capacity const currentMass = calculateMass(structure.storedResources); const addedMass = (resource.massPerUnit ?? 0) * amount; const maxMass = structureType.maxCapacityMass; if (maxMass && currentMass + addedMass > maxMass) { return { success: false, error: `Exceeds capacity. Available: ${(maxMass - currentMass).toLocaleString()} kg, needed: ${addedMass.toLocaleString()} kg.`, }; } // Auto-place on grid if structure has grid dimensions const existing = [...(structure.storedResources ?? [])]; const gridW = structureType.xSize ?? 0; const gridH = structureType.ySize ?? 0; const hasGrid = gridW > 0 && gridH > 0; if (hasGrid) { const resW = resource.gridWidth ?? 1; const resH = resource.gridHeight ?? 1; const ew = resW; const eh = resH; const maxStack = resource.stackable === false ? 1 : (resource.maxStackSize ?? Infinity); // 1. Fill existing stacks of the same resource that have space let remaining = amount; const sameType = existing.filter((e) => { const r = e.resource; return typeof r === "object" && r !== null && "id" in r ? r.id === resourceId : r === resourceId; }); for (const entry of sameType) { if (remaining <= 0) break; const space = maxStack === Infinity ? remaining : Math.min(maxStack - entry.amount, remaining); if (space <= 0) continue; const idx = existing.findIndex((e) => e.id === entry.id); if (idx >= 0) { existing[idx] = { ...entry, amount: entry.amount + space }; remaining -= space; } } // 2. Create new entries for remaining amount while (remaining > 0) { let spot: { x: number; y: number } | null = null; for (let y = 0; y <= gridH - eh && !spot; y++) { for (let x = 0; x <= gridW - ew && !spot; x++) { const cells = new Set(); for (let dx = 0; dx < ew; dx++) { for (let dy = 0; dy < eh; dy++) { cells.add(`${x + dx},${y + dy}`); } } let overlaps = false; for (const other of existing) { if (other.id == null) continue; const otherRes = other.resource as Resource; if (!otherRes || typeof otherRes === "number") continue; const otherRotated = other.rotated ?? false; const ow = otherRotated ? (otherRes.gridHeight ?? 1) : (otherRes.gridWidth ?? 1); const oh = otherRotated ? (otherRes.gridWidth ?? 1) : (otherRes.gridHeight ?? 1); for (let dx = 0; dx < ow; dx++) { for (let dy = 0; dy < oh; dy++) { if (cells.has(`${other.gridX + dx},${other.gridY + dy}`)) { overlaps = true; break; } } if (overlaps) break; } if (overlaps) break; } if (!overlaps) spot = { x, y }; } } if (!spot) { return { success: false, error: `No space on grid for ${resource.name}. Need ${ew}×${eh} free cells.`, }; } const stackAmount = Math.min(remaining, maxStack === Infinity ? remaining : maxStack); existing.push({ resource: resourceId, amount: stackAmount, gridX: spot.x, gridY: spot.y, rotated: false, }); remaining -= stackAmount; } } else { // No grid — flat-list behavior const existingIndex = existing.findIndex((e) => { const res = e.resource; return typeof res === "object" && res !== null && "id" in res ? res.id === resourceId : res === resourceId; }); if (existingIndex >= 0) { existing[existingIndex] = { ...existing[existingIndex], amount: existing[existingIndex].amount + amount, }; } else { existing.push({ resource: resourceId, amount, gridX: 0, gridY: 0, rotated: false }); } } const updated = await payload.update({ collection: "game-structures", id: structureId, 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"; if (message === "Unauthorized") { return { success: false, error: "You must be logged in." }; } return { success: false, error: message }; } } export async function removeResource( structureId: number, resourceId: number, amount: number, ): Promise { try { const { payload, user } = await authenticate(); if (!(await canManageStorage(payload, user))) { return { success: false, error: "Only superusers may withdraw resources from structures.", }; } return await removeResourceInternal(payload, user, structureId, resourceId, amount); } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; if (message === "Unauthorized") { return { success: false, error: "You must be logged in." }; } return { success: false, error: message }; } } async function removeResourceInternal( payload: PayloadClient, user: AuthedUser, structureId: number, resourceId: number, amount: number, ): Promise { try { if (amount <= 0) { return { success: false, error: "Amount must be greater than zero." }; } const structure = await getStructure(payload, structureId); const storedAmount = getStoredAmount(structure.storedResources, resourceId); const voidAmount = getStoredAmount((structure as any).voidStorage, resourceId); const totalAmount = storedAmount + voidAmount; if (totalAmount <= 0) { return { success: false, error: "This resource is not stored here." }; } if (amount > totalAmount) { return { success: false, error: `Cannot remove more than stored. Available: ${totalAmount.toLocaleString()} (${storedAmount.toLocaleString()} in grid, ${voidAmount.toLocaleString()} in void).`, }; } let remainingToRemove = amount; const storedResources = [...(structure.storedResources ?? [])]; const voidStorage = [...((structure as any).voidStorage ?? [])]; // Remove from void storage first — void should empty itself before touching the grid const voidIdx = voidStorage.findIndex((e: any) => { const res = e.resource; return typeof res === "object" && res !== null && "id" in res ? (res as any).id === resourceId : res === resourceId; }); if (voidIdx >= 0) { const entry = voidStorage[voidIdx]; if (entry.amount > remainingToRemove) { voidStorage[voidIdx] = { ...entry, amount: entry.amount - remainingToRemove }; remainingToRemove = 0; } else { remainingToRemove -= entry.amount; voidStorage.splice(voidIdx, 1); } } // Remove remaining from grid storage if (remainingToRemove > 0) { const storedIdx = storedResources.findIndex((e) => { const res = e.resource; return typeof res === "object" && res !== null && "id" in res ? res.id === resourceId : res === resourceId; }); if (storedIdx >= 0) { const entry = storedResources[storedIdx]; if (entry.amount > remainingToRemove) { storedResources[storedIdx] = { ...entry, amount: entry.amount - remainingToRemove }; } else { storedResources.splice(storedIdx, 1); } } } const updated = await payload.update({ collection: "game-structures", id: structureId, 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"; if (message === "Unauthorized") { return { success: false, error: "You must be logged in." }; } return { success: false, error: message }; } } export async function transferResource( fromStructureId: number, toStructureId: number, resourceId: number, amount: number, ): Promise { try { const { payload, user } = await authenticate(); if (!(await canHandleStorage(payload, user))) { return { success: false, error: "Logistics personnel or superuser role required for transfers.", }; } if (amount <= 0) { return { success: false, error: "Amount must be greater than zero." }; } if (fromStructureId === toStructureId) { return { success: false, error: "Source and destination cannot be the same." }; } const fromStructure = await getStructure(payload, fromStructureId); const toStructure = await getStructure(payload, toStructureId); // Check proximity — if beyond threshold, require shipment const fromCoords = fromStructure.coordinates as [number, number] | null; const toCoords = toStructure.coordinates as [number, number] | null; if (fromCoords && toCoords) { const gameRules = await payload.findGlobal({ slug: "game-rules" }); const threshold = (gameRules.proximityThreshold as number) || 10; const { calculateDistance } = await import("@/lib/distance"); const distance = calculateDistance(fromCoords, toCoords); if (distance > threshold) { return { success: false, error: `Structures are too far apart for instant transfer (${distance.toFixed(1)} units, max ${threshold}). Use the Shipments system to transport resources over long distances.`, }; } } const sourceStored = getStoredAmount(fromStructure.storedResources, resourceId); const sourceVoid = getStoredAmount((fromStructure as any).voidStorage, resourceId); const sourceAmount = sourceStored + sourceVoid; if (sourceAmount < amount) { return { success: false, error: `Insufficient resources. Available: ${sourceAmount.toLocaleString()} (${sourceStored.toLocaleString()} in grid, ${sourceVoid.toLocaleString()} in void).`, }; } // Fetch the resource for rule + capacity checks const resource = await payload .findByID({ collection: "resources", id: resourceId, depth: 0, }) .catch(() => null); if (!resource) { return { success: false, error: "Resource not found." }; } // Check destination storage rules (prohibited → whitelist → per-item cap) const destCurrentStored = getStoredAmount(toStructure.storedResources, resourceId) + getStoredAmount(toStructure.voidStorage ?? [], resourceId); const destRuleCheck = checkStorageDeposit( toStructure.type, resourceId, amount, destCurrentStored, ); if (!destRuleCheck.allowed) { return { success: false, error: storageViolationMessage(resource.name, destRuleCheck), }; } // Check destination capacity const destMass = calculateMass(toStructure.storedResources); const addedMass = (resource.massPerUnit ?? 0) * amount; const destMaxMass = toStructure.type.maxCapacityMass; if (destMaxMass && destMass + addedMass > destMaxMass) { return { success: false, error: `Destination exceeds capacity. Available: ${(destMaxMass - destMass).toLocaleString()} kg.`, }; } // Remove from source (unguarded internal — gate already applied above) const removeResult = await removeResourceInternal( payload, user, fromStructureId, resourceId, amount, ); if (!removeResult.success) { return removeResult; } // Add to destination const addResult = await addResourceInternal(payload, user, toStructureId, resourceId, amount); if (!addResult.success) { // Rollback: re-add to source await addResourceInternal(payload, user, fromStructureId, resourceId, amount); 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"; if (message === "Unauthorized") { return { success: false, error: "You must be logged in." }; } return { success: false, error: message }; } } // --- Grid-specific actions --- interface GridActionResult { success: boolean; error?: string; } export async function placeResourceOnGrid( structureId: number, resourceId: number, amount: number, gridX: number, gridY: number, rotated: boolean, ): Promise { try { const { payload, user } = await authenticate(); if (!(await canManageStorage(payload, user))) { return { success: false, error: "Only superusers may place resources on the storage grid.", }; } if (amount <= 0) return { success: false, error: "Amount must be greater than zero." }; const structure = await getStructure(payload, structureId); const structureType = structure.type; const resource = await payload .findByID({ collection: "resources", id: resourceId, depth: 0 }) .catch(() => null); if (!resource) return { success: false, error: "Resource not found." }; // Check storage rules (prohibited → whitelist → per-item cap) const currentStored = getStoredAmount(structure.storedResources, resourceId) + getStoredAmount(structure.voidStorage ?? [], resourceId); const ruleCheck = checkStorageDeposit(structureType, resourceId, amount, currentStored); if (!ruleCheck.allowed) { return { success: false, error: storageViolationMessage(resource.name, ruleCheck) }; } if (resource.stackable && resource.maxStackSize && amount > resource.maxStackSize) { return { success: false, error: `Cannot stack more than ${resource.maxStackSize} units.` }; } const currentMass = calculateMass(structure.storedResources); const addedMass = (resource.massPerUnit ?? 0) * amount; const maxMass = structureType.maxCapacityMass; if (maxMass && currentMass + addedMass > maxMass) { return { success: false, error: `Exceeds mass capacity. Available: ${(maxMass - currentMass).toLocaleString()} kg.`, }; } const gridW = structureType.xSize ?? 0; const gridH = structureType.ySize ?? 0; if (!gridW || !gridH) { return { success: false, error: "This structure does not have grid dimensions configured." }; } const resW = resource.gridWidth ?? 1; const resH = resource.gridHeight ?? 1; const effectiveW = rotated ? resH : resW; const effectiveH = rotated ? resW : resH; if (gridX < 0 || gridY < 0 || gridX + effectiveW > gridW || gridY + effectiveH > gridH) { return { success: false, error: "Placement is outside grid bounds." }; } const existing = [...(structure.storedResources ?? [])]; const targetCells = new Set(); for (let dx = 0; dx < effectiveW; dx++) { for (let dy = 0; dy < effectiveH; dy++) { targetCells.add(`${gridX + dx},${gridY + dy}`); } } for (const entry of existing) { if (entry.id == null) continue; const entryRes = entry.resource as Resource; if (!entryRes || typeof entryRes === "number") continue; const entryRotated = entry.rotated ?? false; const ew = entryRotated ? (entryRes.gridHeight ?? 1) : (entryRes.gridWidth ?? 1); const eh = entryRotated ? (entryRes.gridWidth ?? 1) : (entryRes.gridHeight ?? 1); for (let dx = 0; dx < ew; dx++) { for (let dy = 0; dy < eh; dy++) { if (targetCells.has(`${entry.gridX + dx},${entry.gridY + dy}`)) { return { success: false, error: "Placement overlaps with an existing item." }; } } } } 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"; return message === "Unauthorized" ? { success: false, error: "You must be logged in." } : { success: false, error: message }; } } export async function moveGridItem( structureId: number, entryId: string, gridX: number, gridY: number, rotated?: boolean, ): Promise { try { const { payload, user } = await authenticate(); if (!(await canHandleStorage(payload, user))) { return { success: false, error: "Logistics personnel or superuser role required.", }; } const structure = await getStructure(payload, structureId); const existing = [...(structure.storedResources ?? [])]; const idx = existing.findIndex((e) => e.id === entryId); if (idx < 0) return { success: false, error: "Item not found in grid." }; const entry = existing[idx]; const res = entry.resource as Resource; if (!res || typeof res === "number") return { success: false, error: "Invalid resource." }; const effectiveRotated = rotated ?? entry.rotated ?? false; const ew = effectiveRotated ? (res.gridHeight ?? 1) : (res.gridWidth ?? 1); const eh = effectiveRotated ? (res.gridWidth ?? 1) : (res.gridHeight ?? 1); const gridW = structure.type.xSize ?? 0; const gridH = structure.type.ySize ?? 0; if (gridX < 0 || gridY < 0 || gridX + ew > gridW || gridY + eh > gridH) { return { success: false, error: "Placement is outside grid bounds." }; } const targetCells = new Set(); for (let dx = 0; dx < ew; dx++) { for (let dy = 0; dy < eh; dy++) { targetCells.add(`${gridX + dx},${gridY + dy}`); } } for (const other of existing) { if (other.id === entryId || other.id == null) continue; const otherRes = other.resource as Resource; if (!otherRes || typeof otherRes === "number") continue; const otherRotated = other.rotated ?? false; const ow = otherRotated ? (otherRes.gridHeight ?? 1) : (otherRes.gridWidth ?? 1); const oh = otherRotated ? (otherRes.gridWidth ?? 1) : (otherRes.gridHeight ?? 1); for (let dx = 0; dx < ow; dx++) { for (let dy = 0; dy < oh; dy++) { if (targetCells.has(`${other.gridX + dx},${other.gridY + dy}`)) { return { success: false, error: "Cannot move here: overlaps with another item." }; } } } } existing[idx] = rotated !== undefined ? { ...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"; return message === "Unauthorized" ? { success: false, error: "You must be logged in." } : { success: false, error: message }; } } export async function mergeGridStacks( structureId: number, sourceId: string, targetId: string, ): Promise { try { const { payload, user } = await authenticate(); if (!(await canHandleStorage(payload, user))) { return { success: false, error: "Logistics personnel or superuser role required.", }; } const structure = await getStructure(payload, structureId); const existing = [...(structure.storedResources ?? [])]; const srcIdx = existing.findIndex((e) => e.id === sourceId); const tgtIdx = existing.findIndex((e) => e.id === targetId); if (srcIdx < 0 || tgtIdx < 0) return { success: false, error: "Item not found." }; const src = existing[srcIdx]; const tgt = existing[tgtIdx]; const srcRes = src.resource as Resource; const tgtRes = tgt.resource as Resource; if (!srcRes || typeof srcRes === "number" || !tgtRes || typeof tgtRes === "number") { return { success: false, error: "Invalid resource." }; } const srcResId = typeof srcRes === "object" ? srcRes.id : srcRes; const tgtResId = typeof tgtRes === "object" ? tgtRes.id : tgtRes; if (srcResId !== tgtResId) return { success: false, error: "Cannot merge different resource types." }; if (tgtRes.stackable === false) return { success: false, error: "This resource cannot be stacked." }; const maxStack = tgtRes.maxStackSize; const spaceInTarget = maxStack ? maxStack - tgt.amount : Infinity; if (spaceInTarget <= 0) return { success: false, error: "Target stack is full." }; const transferAmount = Math.min(src.amount, spaceInTarget); existing[tgtIdx] = { ...tgt, amount: tgt.amount + transferAmount }; if (transferAmount >= src.amount) { existing.splice(srcIdx, 1); } else { existing[srcIdx] = { ...src, amount: src.amount - transferAmount }; } 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"; return message === "Unauthorized" ? { success: false, error: "You must be logged in." } : { success: false, error: message }; } } export async function rotateGridItem( structureId: number, entryId: string, ): Promise { try { const { payload, user } = await authenticate(); if (!(await canHandleStorage(payload, user))) { return { success: false, error: "Logistics personnel or superuser role required.", }; } const structure = await getStructure(payload, structureId); const existing = [...(structure.storedResources ?? [])]; const idx = existing.findIndex((e) => e.id === entryId); if (idx < 0) return { success: false, error: "Item not found in grid." }; const entry = existing[idx]; const res = entry.resource as Resource; if (!res || typeof res === "number") return { success: false, error: "Invalid resource." }; if (res.rotatable === false) return { success: false, error: "This resource cannot be rotated." }; const newRotated = !(entry.rotated ?? false); const ew = newRotated ? (res.gridHeight ?? 1) : (res.gridWidth ?? 1); const eh = newRotated ? (res.gridWidth ?? 1) : (res.gridHeight ?? 1); const gridW = structure.type.xSize ?? 0; const gridH = structure.type.ySize ?? 0; if (entry.gridX + ew > gridW || entry.gridY + eh > gridH) { return { success: false, error: "Rotation would push the item out of bounds." }; } const targetCells = new Set(); for (let dx = 0; dx < ew; dx++) { for (let dy = 0; dy < eh; dy++) { targetCells.add(`${entry.gridX + dx},${entry.gridY + dy}`); } } for (const other of existing) { if (other.id === entryId || other.id == null) continue; const otherRes = other.resource as Resource; if (!otherRes || typeof otherRes === "number") continue; const otherRotated = other.rotated ?? false; const ow = otherRotated ? (otherRes.gridHeight ?? 1) : (otherRes.gridWidth ?? 1); const oh = otherRotated ? (otherRes.gridWidth ?? 1) : (otherRes.gridHeight ?? 1); for (let dx = 0; dx < ow; dx++) { for (let dy = 0; dy < oh; dy++) { if (targetCells.has(`${other.gridX + dx},${other.gridY + dy}`)) { return { success: false, error: "Rotation would overlap with another item." }; } } } } 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"; return message === "Unauthorized" ? { success: false, error: "You must be logged in." } : { success: false, error: message }; } } export async function removeGridItem( structureId: number, entryId: string, ): Promise { try { const { payload, user } = await authenticate(); if (!(await canManageStorage(payload, user))) { return { success: false, error: "Only superusers may remove items from the storage grid.", }; } const structure = await getStructure(payload, structureId); const existing = [...(structure.storedResources ?? [])]; 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"; return message === "Unauthorized" ? { success: false, error: "You must be logged in." } : { success: false, error: message }; } } export async function splitGridStack( structureId: number, entryId: string, splitAmount: number, ): Promise { try { const { payload, user } = await authenticate(); if (!(await canHandleStorage(payload, user))) { return { success: false, error: "Logistics personnel or superuser role required.", }; } if (splitAmount <= 0) return { success: false, error: "Split amount must be positive." }; const structure = await getStructure(payload, structureId); const existing = [...(structure.storedResources ?? [])]; const idx = existing.findIndex((e) => e.id === entryId); if (idx < 0) return { success: false, error: "Item not found in grid." }; const entry = existing[idx]; if (splitAmount >= entry.amount) return { success: false, error: "Split amount must be less than the stack size." }; const res = entry.resource as Resource; if (!res || typeof res === "number") return { success: false, error: "Invalid resource." }; const rotated = entry.rotated ?? false; const ew = rotated ? (res.gridHeight ?? 1) : (res.gridWidth ?? 1); const eh = rotated ? (res.gridWidth ?? 1) : (res.gridHeight ?? 1); const gridW = structure.type.xSize ?? 0; const gridH = structure.type.ySize ?? 0; let placed = false; for (let y = 0; y <= gridH - eh && !placed; y++) { for (let x = 0; x <= gridW - ew && !placed; x++) { const targetCells = new Set(); for (let dx = 0; dx < ew; dx++) { for (let dy = 0; dy < eh; dy++) { targetCells.add(`${x + dx},${y + dy}`); } } let overlaps = false; for (const other of existing) { if (other.id == null) continue; const otherRes = other.resource as Resource; if (!otherRes || typeof otherRes === "number") continue; const otherRotated = other.rotated ?? false; const ow = otherRotated ? (otherRes.gridHeight ?? 1) : (otherRes.gridWidth ?? 1); const oh = otherRotated ? (otherRes.gridWidth ?? 1) : (otherRes.gridHeight ?? 1); for (let dx = 0; dx < ow; dx++) { for (let dy = 0; dy < oh; dy++) { if (targetCells.has(`${other.gridX + dx},${other.gridY + dy}`)) { overlaps = true; break; } } if (overlaps) break; } if (overlaps) break; } if (!overlaps) { existing[idx] = { ...entry, amount: entry.amount - splitAmount }; existing.push({ resource: entry.resource, amount: splitAmount, gridX: x, gridY: y, rotated, }); placed = true; } } } 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"; return message === "Unauthorized" ? { success: false, error: "You must be logged in." } : { success: false, error: message }; } } export async function retrieveFromVoid(structureId: number): Promise { try { const { payload, user } = await authenticate(); if (!(await canHandleStorage(payload, user))) { return { success: false, error: "Logistics personnel or superuser role required." }; } const structure = await getStructure(payload, structureId); const gridW = structure.type.xSize ?? 0; const gridH = structure.type.ySize ?? 0; if (!gridW || !gridH) return { success: false, error: "Structure has no grid dimensions." }; const storedResources = [...(structure.storedResources ?? [])]; const voidStorage = [...((structure as any).voidStorage ?? [])]; const remainingVoid: typeof voidStorage = []; for (const entry of voidStorage) { const res = entry.resource as Resource; if (!res || typeof res === "number") { remainingVoid.push(entry); continue; } const rotated = entry.rotated ?? false; const ew = rotated ? (res.gridHeight ?? 1) : (res.gridWidth ?? 1); const eh = rotated ? (res.gridWidth ?? 1) : (res.gridHeight ?? 1); let spot: { x: number; y: number } | null = null; for (let y = 0; y <= gridH - eh && !spot; y++) { for (let x = 0; x <= gridW - ew && !spot; x++) { const targetCells = new Set(); for (let dx = 0; dx < ew; dx++) { for (let dy = 0; dy < eh; dy++) { targetCells.add(`${x + dx},${y + dy}`); } } let overlaps = false; for (const p of storedResources) { const pRes = p.resource as Resource; if (!pRes || typeof pRes === "number") continue; const pRotated = p.rotated ?? false; const pw = pRotated ? (pRes.gridHeight ?? 1) : (pRes.gridWidth ?? 1); const ph = pRotated ? (pRes.gridWidth ?? 1) : (pRes.gridHeight ?? 1); for (let dx = 0; dx < pw; dx++) { for (let dy = 0; dy < ph; dy++) { if (targetCells.has(`${p.gridX + dx},${p.gridY + dy}`)) { overlaps = true; break; } } if (overlaps) break; } if (overlaps) break; } if (!overlaps) spot = { x, y }; } } if (spot) { storedResources.push({ ...entry, gridX: spot.x, gridY: spot.y }); } else { remainingVoid.push(entry); } } await payload.update({ collection: "game-structures", id: structureId, data: { storedResources, voidStorage: remainingVoid }, }); 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 { success: true, error: recovered > 0 ? `${recovered} item(s) retrieved from void storage.` : "No items could be retrieved: the grid is full.", }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return message === "Unauthorized" ? { success: false, error: "You must be logged in." } : { success: false, error: message }; } } export async function autoArrangeGrid(structureId: number): Promise { try { const { payload, user } = await authenticate(); if (!(await canHandleStorage(payload, user))) { return { success: false, error: "Logistics personnel or superuser role required.", }; } const structure = await getStructure(payload, structureId); const gridW = structure.type.xSize ?? 0; const gridH = structure.type.ySize ?? 0; if (!gridW || !gridH) return { success: false, error: "Structure has no grid dimensions." }; // Phase 1 — repack grid items const gridItems = [...(structure.storedResources ?? [])].map((e) => ({ ...e, _sortKey: e.gridY * 1000 + e.gridX, })); gridItems.sort((a, b) => (a._sortKey ?? 0) - (b._sortKey ?? 0)); const placed: typeof gridItems = []; const overflow: typeof gridItems = []; for (const item of gridItems) { const res = item.resource as Resource; if (!res || typeof res === "number") continue; const rotated = item.rotated ?? false; const ew = rotated ? (res.gridHeight ?? 1) : (res.gridWidth ?? 1); const eh = rotated ? (res.gridWidth ?? 1) : (res.gridHeight ?? 1); let spotFound = false; for (let y = 0; y <= gridH - eh && !spotFound; y++) { for (let x = 0; x <= gridW - ew && !spotFound; x++) { const targetCells = new Set(); for (let dx = 0; dx < ew; dx++) { for (let dy = 0; dy < eh; dy++) { targetCells.add(`${x + dx},${y + dy}`); } } let overlaps = false; for (const p of placed) { const pRes = p.resource as Resource; if (!pRes || typeof pRes === "number") continue; const pRotated = p.rotated ?? false; const pw = pRotated ? (pRes.gridHeight ?? 1) : (pRes.gridWidth ?? 1); const ph = pRotated ? (pRes.gridWidth ?? 1) : (pRes.gridHeight ?? 1); for (let dx = 0; dx < pw; dx++) { for (let dy = 0; dy < ph; dy++) { if (targetCells.has(`${p.gridX + dx},${p.gridY + dy}`)) { overlaps = true; break; } } if (overlaps) break; } if (overlaps) break; } if (!overlaps) { placed.push({ ...item, gridX: x, gridY: y }); spotFound = true; } } } if (!spotFound) { overflow.push(item); } } // Phase 2 — try to recover void items into any remaining grid space const voidStorage = [...((structure as any).voidStorage ?? [])]; const remainingVoid: typeof placed = []; for (const entry of voidStorage) { const res = entry.resource as Resource; if (!res || typeof res === "number") { remainingVoid.push(entry); continue; } const rotated = entry.rotated ?? false; const ew = rotated ? (res.gridHeight ?? 1) : (res.gridWidth ?? 1); const eh = rotated ? (res.gridWidth ?? 1) : (res.gridHeight ?? 1); let spot: { x: number; y: number } | null = null; for (let y = 0; y <= gridH - eh && !spot; y++) { for (let x = 0; x <= gridW - ew && !spot; x++) { const targetCells = new Set(); for (let dx = 0; dx < ew; dx++) { for (let dy = 0; dy < eh; dy++) { targetCells.add(`${x + dx},${y + dy}`); } } let overlaps = false; for (const p of placed) { const pRes = p.resource as Resource; if (!pRes || typeof pRes === "number") continue; const pRotated = p.rotated ?? false; const pw = pRotated ? (pRes.gridHeight ?? 1) : (pRes.gridWidth ?? 1); const ph = pRotated ? (pRes.gridWidth ?? 1) : (pRes.gridHeight ?? 1); for (let dx = 0; dx < pw; dx++) { for (let dy = 0; dy < ph; dy++) { if (targetCells.has(`${p.gridX + dx},${p.gridY + dy}`)) { overlaps = true; break; } } if (overlaps) break; } if (overlaps) break; } if (!overlaps) spot = { x, y }; } } if (spot) { placed.push({ ...entry, gridX: spot.x, gridY: spot.y }); } else { remainingVoid.push(entry); } } const updated = placed.map(({ _sortKey, ...rest }) => rest); const overflowItems = overflow.map(({ _sortKey, ...rest }) => rest); const voidCount = voidStorage.length; const recoveredCount = voidCount - remainingVoid.length; await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: updated, voidStorage: [...remainingVoid, ...overflowItems], }, }); const messages: string[] = []; 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).`); 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"; return message === "Unauthorized" ? { success: false, error: "You must be logged in." } : { success: false, error: message }; } }