diff --git a/src/app/(frontend)/logistics/structures/actions.ts b/src/app/(frontend)/logistics/structures/actions.ts index 2b3c591..41ce7fc 100644 --- a/src/app/(frontend)/logistics/structures/actions.ts +++ b/src/app/(frontend)/logistics/structures/actions.ts @@ -110,22 +110,105 @@ export async function addResource( }; } - // Update stored resources + // Auto-place on grid if structure has grid dimensions const existing = [...(structure.storedResources ?? [])]; - const existingIndex = existing.findIndex((e) => { - const res = e.resource; - return typeof res === "object" && res !== null && "id" in res - ? res.id === resourceId - : res === resourceId; - }); + const gridW = structureType.xSize ?? 0; + const gridH = structureType.ySize ?? 0; + const hasGrid = gridW > 0 && gridH > 0; - if (existingIndex >= 0) { - existing[existingIndex] = { - ...existing[existingIndex], - amount: existing[existingIndex].amount + amount, - }; + 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 { - existing.push({ resource: resourceId, amount }); + // 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({ @@ -287,3 +370,460 @@ export async function transferResource( 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 } = await authenticate(); + + if (amount <= 0) return { success: false, error: "Amount must be greater than zero." }; + + const structure = await getStructure(payload, structureId); + const structureType = structure.type; + + const isProhibited = structureType.prohibitedStorage?.some((rule) => { + if (!rule.item) return false; + const item = rule.item; + if (typeof item === "object" && "id" in item) { + return item.relationTo === "resources" && item.value === resourceId; + } + return false; + }); + if (isProhibited) { + return { success: false, error: "This resource is prohibited from storage here." }; + } + + const resource = await payload.findByID({ collection: "resources", id: resourceId, depth: 0 }).catch(() => null); + if (!resource) return { success: false, error: "Resource not found." }; + + 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 } }); + 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 } = await authenticate(); + 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 } }); + 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 } = await authenticate(); + 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 } }); + 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 } = await authenticate(); + 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 } }); + 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 } = await authenticate(); + 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." }; + + existing.splice(idx, 1); + await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: existing } }); + 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 } = await authenticate(); + + 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 === 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}`)) { + 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 } }); + 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 autoArrangeGrid(structureId: number): Promise { + try { + const { payload } = await authenticate(); + 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 items = [...(structure.storedResources ?? [])].map((e) => ({ + ...e, + _sortKey: e.gridY * 1000 + e.gridX, + })); + items.sort((a, b) => (a._sortKey ?? 0) - (b._sortKey ?? 0)); + + const placed: typeof items = []; + + for (const item of items) { + 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) { + placed.push({ ...item, gridX: 0, gridY: 0 }); + } + } + + const updated = placed.map(({ _sortKey, ...rest }) => rest); + await payload.update({ collection: "game-structures", id: structureId, data: { storedResources: updated } }); + 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 }; + } +}