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:
parent
d0262baab8
commit
7bcbd534e4
1 changed files with 132 additions and 0 deletions
|
|
@ -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).
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue