- 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
237 lines
6.3 KiB
TypeScript
237 lines
6.3 KiB
TypeScript
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<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).
|
|
*/
|
|
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;
|
|
}
|
|
}
|