1
0
Fork 0
polaris-task-force/src/lib/logistics.ts
Z8MB1E 7bcbd534e4 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
2026-07-28 14:02:52 -04:00

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;
}
}