1
0
Fork 0

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
This commit is contained in:
Jason Fraley 2026-07-28 14:02:52 -04:00
parent d0262baab8
commit 7bcbd534e4

View file

@ -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<string>();
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).
*/