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). */ export function calculateStoredMass(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); } /** * Calculate the storage utilization percentage for a game structure. * Returns a value between 0 and 100 (or null if maxCapacityMass is not set). */ export function calculateUtilization( structure: Structure, storedResources: GameStructure["storedResources"], ): number | null { const maxMass = structure.maxCapacityMass; if (!maxMass || maxMass <= 0) return null; const currentMass = calculateStoredMass(storedResources); return Math.min((currentMass / maxMass) * 100, 100); } /** * Get the total number of distinct resources stored. */ export function getResourceCount(storedResources: GameStructure["storedResources"]): number { if (!storedResources) return 0; return storedResources.length; } /** * Get the total grid slots available (xSize * ySize), or null if not defined. */ export function getGridSlots(structure: Structure): number | null { const x = structure.xSize; const y = structure.ySize; if (!x || !y) return 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). */ export function getResourceTypeLabel(type: Resource["type"]): string { switch (type) { case "physical": return "Physical"; case "fluid": return "Fluid"; case "energy": return "Energy"; case "abstract": return "Abstract"; default: return "Resource"; } } /** * Map a structure category to a display-friendly label. */ export function getCategoryLabel(category: Structure["category"]): string { switch (category) { case "logistics": return "Logistics"; case "military": return "Military"; case "civilian": return "Civilian"; default: return category; } } /** * Map a structure functionType to a display-friendly label. */ export function getFunctionTypeLabel(functionType: Structure["functionType"]): string | null { if (!functionType) return null; switch (functionType) { case "producer": return "Producer"; case "storage": return "Storage"; case "sensor": return "Sensor"; case "converter": return "Converter"; case "defense": return "Defense"; case "miscellaneous": return "Miscellaneous"; default: return functionType; } }