From 7bcbd534e41683a8be277c6fb24ca692f5a4a7ff Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Tue, 28 Jul 2026 14:02:52 -0400 Subject: [PATCH] feat(grid): add grid utility functions - GridItem type and GridDimensions helper type - getItemFootprint: effective size accounting for rotation - getOccupiedCells: list of "x,y" cell keys - isValidPlacement: overlap and bounds checking - findEmptySpot: first-fit scan for available placement - autoArrangeItems: pack items into grid - getGridOccupancy: used/total cell counts --- src/lib/logistics.ts | 132 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/src/lib/logistics.ts b/src/lib/logistics.ts index e4bdfd6..f6f4c66 100644 --- a/src/lib/logistics.ts +++ b/src/lib/logistics.ts @@ -1,5 +1,17 @@ import { GameStructure, Resource, Structure } from "@/payload-types"; +export type GridItem = { + id: string; + resourceId: number; + amount: number; + gridX: number; + gridY: number; + rotated: boolean; + resource: Resource; +}; + +export type GridDimensions = { width: number; height: number }; + /** * Calculate the total mass of all resources stored in a game structure. * Requires resources to be populated (depth >= 1 on storedResources.resource). @@ -47,6 +59,126 @@ export function getGridSlots(structure: Structure): number | null { return x * y; } +/** + * Get the effective dimensions of a grid item, accounting for rotation. + */ +export function getItemFootprint(resource: Resource, rotated: boolean): GridDimensions { + const w = resource.gridWidth ?? 1; + const h = resource.gridHeight ?? 1; + return rotated ? { width: h, height: w } : { width: w, height: h }; +} + +/** + * Get the cells occupied by a grid item as an array of "x,y" strings. + */ +export function getOccupiedCells( + gridX: number, + gridY: number, + width: number, + height: number, +): string[] { + const cells: string[] = []; + for (let dx = 0; dx < width; dx++) { + for (let dy = 0; dy < height; dy++) { + cells.push(`${gridX + dx},${gridY + dy}`); + } + } + return cells; +} + +/** + * Check whether placing an item at (targetX, targetY) would overlap any existing items + * or go out of bounds. Returns true if the placement is VALID. + */ +export function isValidPlacement( + items: GridItem[], + excludeId: string | null, + targetX: number, + targetY: number, + width: number, + height: number, + gridWidth: number, + gridHeight: number, +): boolean { + if (targetX < 0 || targetY < 0) return false; + if (targetX + width > gridWidth || targetY + height > gridHeight) return false; + + const targetCells = new Set(getOccupiedCells(targetX, targetY, width, height)); + + for (const item of items) { + if (item.id === excludeId) continue; + const fp = getItemFootprint(item.resource, item.rotated); + const occupied = getOccupiedCells(item.gridX, item.gridY, fp.width, fp.height); + for (const cell of occupied) { + if (targetCells.has(cell)) return false; + } + } + + return true; +} + +/** + * Find the first available spot for an item of given dimensions, scanning left-to-right, top-to-bottom. + * Returns {x, y} or null if no spot is available. + */ +export function findEmptySpot( + items: GridItem[], + width: number, + height: number, + gridWidth: number, + gridHeight: number, +): { x: number; y: number } | null { + for (let y = 0; y <= gridHeight - height; y++) { + for (let x = 0; x <= gridWidth - width; x++) { + if (isValidPlacement(items, null, x, y, width, height, gridWidth, gridHeight)) { + return { x, y }; + } + } + } + return null; +} + +/** + * Auto-arrange all items into the grid. Returns a new array with updated positions. + * Items that don't fit are placed at 0,0 (they should be flagged as overflowing). + */ +export function autoArrangeItems( + items: GridItem[], + gridWidth: number, + gridHeight: number, +): GridItem[] { + const result: GridItem[] = []; + + for (const item of items) { + const fp = getItemFootprint(item.resource, item.rotated); + const spot = findEmptySpot(result, fp.width, fp.height, gridWidth, gridHeight); + if (spot) { + result.push({ ...item, gridX: spot.x, gridY: spot.y }); + } else { + result.push({ ...item, gridX: 0, gridY: 0 }); + } + } + + return result; +} + +/** + * Get the number of grid cells occupied vs total. + */ +export function getGridOccupancy( + items: GridItem[], + gridWidth: number, + gridHeight: number, +): { used: number; total: number } { + const occupied = new Set(); + for (const item of items) { + const fp = getItemFootprint(item.resource, item.rotated); + const cells = getOccupiedCells(item.gridX, item.gridY, fp.width, fp.height); + for (const c of cells) occupied.add(c); + } + return { used: occupied.size, total: gridWidth * gridHeight }; +} + /** * Map a resource type to a fallback icon name (used when no icon upload is set). */