|
|
@@ -1,5 +1,5 @@
|
|
|
|
import type { ParsedShape } from '../utils/parse-shape';
|
|
|
|
import type { ParsedShape } from "../utils/parse-shape";
|
|
|
|
import type { Transform2D } from '../utils/shape-collision';
|
|
|
|
import type { Transform2D } from "../utils/shape-collision";
|
|
|
|
import {
|
|
|
|
import {
|
|
|
|
checkBoardCollision,
|
|
|
|
checkBoardCollision,
|
|
|
|
checkBounds,
|
|
|
|
checkBounds,
|
|
|
@@ -7,14 +7,23 @@ import {
|
|
|
|
flipYTransform,
|
|
|
|
flipYTransform,
|
|
|
|
rotateTransform,
|
|
|
|
rotateTransform,
|
|
|
|
transformShape,
|
|
|
|
transformShape,
|
|
|
|
} from '../utils/shape-collision';
|
|
|
|
} from "../utils/shape-collision";
|
|
|
|
import type { CellKey, GridInventory, InventoryItem, MutationResult, PlacementResult } from './types';
|
|
|
|
import type {
|
|
|
|
|
|
|
|
CellKey,
|
|
|
|
|
|
|
|
GridInventory,
|
|
|
|
|
|
|
|
InventoryItem,
|
|
|
|
|
|
|
|
MutationResult,
|
|
|
|
|
|
|
|
PlacementResult,
|
|
|
|
|
|
|
|
} from "./types";
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Creates a new empty grid inventory.
|
|
|
|
* Creates a new empty grid inventory.
|
|
|
|
* Note: When used inside `.produce()`, call this before returning the draft.
|
|
|
|
* Note: When used inside `.produce()`, call this before returning the draft.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
export function createGridInventory<TMeta = Record<string, unknown>>(width: number, height: number): GridInventory<TMeta> {
|
|
|
|
export function createGridInventory<TMeta = Record<string, unknown>>(
|
|
|
|
|
|
|
|
width: number,
|
|
|
|
|
|
|
|
height: number,
|
|
|
|
|
|
|
|
): GridInventory<TMeta> {
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
width,
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
height,
|
|
|
@@ -26,9 +35,12 @@ export function createGridInventory<TMeta = Record<string, unknown>>(width: numb
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Builds a Set of occupied cell keys from a shape and transform.
|
|
|
|
* Builds a Set of occupied cell keys from a shape and transform.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
function getShapeCellKeys(shape: ParsedShape, transform: Transform2D): Set<CellKey> {
|
|
|
|
function getShapeCellKeys(
|
|
|
|
|
|
|
|
shape: ParsedShape,
|
|
|
|
|
|
|
|
transform: Transform2D,
|
|
|
|
|
|
|
|
): Set<CellKey> {
|
|
|
|
const cells = transformShape(shape, transform);
|
|
|
|
const cells = transformShape(shape, transform);
|
|
|
|
return new Set(cells.map(c => `${c.x},${c.y}` as CellKey));
|
|
|
|
return new Set(cells.map((c) => `${c.x},${c.y}` as CellKey));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
@@ -37,15 +49,15 @@ function getShapeCellKeys(shape: ParsedShape, transform: Transform2D): Set<CellK
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
export function validatePlacement<TMeta = Record<string, unknown>>(
|
|
|
|
export function validatePlacement<TMeta = Record<string, unknown>>(
|
|
|
|
inventory: GridInventory<TMeta>,
|
|
|
|
inventory: GridInventory<TMeta>,
|
|
|
|
shape: InventoryItem<TMeta>['shape'],
|
|
|
|
shape: InventoryItem<TMeta>["shape"],
|
|
|
|
transform: Transform2D
|
|
|
|
transform: Transform2D,
|
|
|
|
): PlacementResult {
|
|
|
|
): PlacementResult {
|
|
|
|
if (!checkBounds(shape, transform, inventory.width, inventory.height)) {
|
|
|
|
if (!checkBounds(shape, transform, inventory.width, inventory.height)) {
|
|
|
|
return { valid: false, reason: '超出边界' };
|
|
|
|
return { valid: false, reason: "超出边界" };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (checkBoardCollision(shape, transform, inventory.occupiedCells)) {
|
|
|
|
if (checkBoardCollision(shape, transform, inventory.occupiedCells)) {
|
|
|
|
return { valid: false, reason: '与已有物品重叠' };
|
|
|
|
return { valid: false, reason: "与已有物品重叠" };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { valid: true };
|
|
|
|
return { valid: true };
|
|
|
@@ -56,7 +68,10 @@ export function validatePlacement<TMeta = Record<string, unknown>>(
|
|
|
|
* **Mutates directly** — call inside a `.produce()` callback.
|
|
|
|
* **Mutates directly** — call inside a `.produce()` callback.
|
|
|
|
* Does not validate; call `validatePlacement` first.
|
|
|
|
* Does not validate; call `validatePlacement` first.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
export function placeItem<TMeta = Record<string, unknown>>(inventory: GridInventory<TMeta>, item: InventoryItem<TMeta>): void {
|
|
|
|
export function placeItem<TMeta = Record<string, unknown>>(
|
|
|
|
|
|
|
|
inventory: GridInventory<TMeta>,
|
|
|
|
|
|
|
|
item: InventoryItem<TMeta>,
|
|
|
|
|
|
|
|
): void {
|
|
|
|
const cells = getShapeCellKeys(item.shape, item.transform);
|
|
|
|
const cells = getShapeCellKeys(item.shape, item.transform);
|
|
|
|
for (const cellKey of cells) {
|
|
|
|
for (const cellKey of cells) {
|
|
|
|
inventory.occupiedCells.add(cellKey);
|
|
|
|
inventory.occupiedCells.add(cellKey);
|
|
|
@@ -68,7 +83,10 @@ export function placeItem<TMeta = Record<string, unknown>>(inventory: GridInvent
|
|
|
|
* Removes an item from the grid by its ID.
|
|
|
|
* Removes an item from the grid by its ID.
|
|
|
|
* **Mutates directly** — call inside a `.produce()` callback.
|
|
|
|
* **Mutates directly** — call inside a `.produce()` callback.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
export function removeItem<TMeta = Record<string, unknown>>(inventory: GridInventory<TMeta>, itemId: string): void {
|
|
|
|
export function removeItem<TMeta = Record<string, unknown>>(
|
|
|
|
|
|
|
|
inventory: GridInventory<TMeta>,
|
|
|
|
|
|
|
|
itemId: string,
|
|
|
|
|
|
|
|
): void {
|
|
|
|
const item = inventory.items.get(itemId);
|
|
|
|
const item = inventory.items.get(itemId);
|
|
|
|
if (!item) return;
|
|
|
|
if (!item) return;
|
|
|
|
|
|
|
|
|
|
|
@@ -87,11 +105,11 @@ export function removeItem<TMeta = Record<string, unknown>>(inventory: GridInven
|
|
|
|
export function moveItem<TMeta = Record<string, unknown>>(
|
|
|
|
export function moveItem<TMeta = Record<string, unknown>>(
|
|
|
|
inventory: GridInventory<TMeta>,
|
|
|
|
inventory: GridInventory<TMeta>,
|
|
|
|
itemId: string,
|
|
|
|
itemId: string,
|
|
|
|
newTransform: Transform2D
|
|
|
|
newTransform: Transform2D,
|
|
|
|
): MutationResult {
|
|
|
|
): MutationResult {
|
|
|
|
const item = inventory.items.get(itemId);
|
|
|
|
const item = inventory.items.get(itemId);
|
|
|
|
if (!item) {
|
|
|
|
if (!item) {
|
|
|
|
return { success: false, reason: '物品不存在' };
|
|
|
|
return { success: false, reason: "物品不存在" };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Temporarily remove item's cells for validation
|
|
|
|
// Temporarily remove item's cells for validation
|
|
|
@@ -130,11 +148,11 @@ export function moveItem<TMeta = Record<string, unknown>>(
|
|
|
|
export function rotateItem<TMeta = Record<string, unknown>>(
|
|
|
|
export function rotateItem<TMeta = Record<string, unknown>>(
|
|
|
|
inventory: GridInventory<TMeta>,
|
|
|
|
inventory: GridInventory<TMeta>,
|
|
|
|
itemId: string,
|
|
|
|
itemId: string,
|
|
|
|
degrees: number
|
|
|
|
degrees: number,
|
|
|
|
): MutationResult {
|
|
|
|
): MutationResult {
|
|
|
|
const item = inventory.items.get(itemId);
|
|
|
|
const item = inventory.items.get(itemId);
|
|
|
|
if (!item) {
|
|
|
|
if (!item) {
|
|
|
|
return { success: false, reason: '物品不存在' };
|
|
|
|
return { success: false, reason: "物品不存在" };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const rotatedTransform = rotateTransform(item.transform, degrees);
|
|
|
|
const rotatedTransform = rotateTransform(item.transform, degrees);
|
|
|
@@ -149,14 +167,15 @@ export function rotateItem<TMeta = Record<string, unknown>>(
|
|
|
|
export function flipItem<TMeta = Record<string, unknown>>(
|
|
|
|
export function flipItem<TMeta = Record<string, unknown>>(
|
|
|
|
inventory: GridInventory<TMeta>,
|
|
|
|
inventory: GridInventory<TMeta>,
|
|
|
|
itemId: string,
|
|
|
|
itemId: string,
|
|
|
|
axis: 'x' | 'y'
|
|
|
|
axis: "x" | "y",
|
|
|
|
): MutationResult {
|
|
|
|
): MutationResult {
|
|
|
|
const item = inventory.items.get(itemId);
|
|
|
|
const item = inventory.items.get(itemId);
|
|
|
|
if (!item) {
|
|
|
|
if (!item) {
|
|
|
|
return { success: false, reason: '物品不存在' };
|
|
|
|
return { success: false, reason: "物品不存在" };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const flippedTransform = axis === 'x'
|
|
|
|
const flippedTransform =
|
|
|
|
|
|
|
|
axis === "x"
|
|
|
|
? flipXTransform(item.transform)
|
|
|
|
? flipXTransform(item.transform)
|
|
|
|
: flipYTransform(item.transform);
|
|
|
|
: flipYTransform(item.transform);
|
|
|
|
|
|
|
|
|
|
|
@@ -166,7 +185,9 @@ export function flipItem<TMeta = Record<string, unknown>>(
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Returns a copy of the occupied cells set.
|
|
|
|
* Returns a copy of the occupied cells set.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
export function getOccupiedCellSet<TMeta = Record<string, unknown>>(inventory: GridInventory<TMeta>): Set<CellKey> {
|
|
|
|
export function getOccupiedCellSet<TMeta = Record<string, unknown>>(
|
|
|
|
|
|
|
|
inventory: GridInventory<TMeta>,
|
|
|
|
|
|
|
|
): Set<CellKey> {
|
|
|
|
return new Set(inventory.occupiedCells);
|
|
|
|
return new Set(inventory.occupiedCells);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@@ -176,7 +197,7 @@ export function getOccupiedCellSet<TMeta = Record<string, unknown>>(inventory: G
|
|
|
|
export function getItemAtCell<TMeta = Record<string, unknown>>(
|
|
|
|
export function getItemAtCell<TMeta = Record<string, unknown>>(
|
|
|
|
inventory: GridInventory<TMeta>,
|
|
|
|
inventory: GridInventory<TMeta>,
|
|
|
|
x: number,
|
|
|
|
x: number,
|
|
|
|
y: number
|
|
|
|
y: number,
|
|
|
|
): InventoryItem<TMeta> | undefined {
|
|
|
|
): InventoryItem<TMeta> | undefined {
|
|
|
|
const cellKey = `${x},${y}` as CellKey;
|
|
|
|
const cellKey = `${x},${y}` as CellKey;
|
|
|
|
if (!inventory.occupiedCells.has(cellKey)) {
|
|
|
|
if (!inventory.occupiedCells.has(cellKey)) {
|
|
|
@@ -199,7 +220,7 @@ export function getItemAtCell<TMeta = Record<string, unknown>>(
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
export function getAdjacentItems<TMeta>(
|
|
|
|
export function getAdjacentItems<TMeta>(
|
|
|
|
inventory: GridInventory<TMeta>,
|
|
|
|
inventory: GridInventory<TMeta>,
|
|
|
|
itemId: string
|
|
|
|
itemId: string,
|
|
|
|
): Map<string, InventoryItem<TMeta>> {
|
|
|
|
): Map<string, InventoryItem<TMeta>> {
|
|
|
|
const item = inventory.items.get(itemId);
|
|
|
|
const item = inventory.items.get(itemId);
|
|
|
|
if (!item) {
|
|
|
|
if (!item) {
|
|
|
@@ -210,7 +231,7 @@ export function getAdjacentItems<TMeta>(
|
|
|
|
const adjacent = new Map<string, InventoryItem<TMeta>>();
|
|
|
|
const adjacent = new Map<string, InventoryItem<TMeta>>();
|
|
|
|
|
|
|
|
|
|
|
|
for (const cellKey of ownCells) {
|
|
|
|
for (const cellKey of ownCells) {
|
|
|
|
const [cx, cy] = cellKey.split(',').map(Number);
|
|
|
|
const [cx, cy] = cellKey.split(",").map(Number);
|
|
|
|
const neighbors: CellKey[] = [
|
|
|
|
const neighbors: CellKey[] = [
|
|
|
|
`${cx + 1},${cy}` as CellKey,
|
|
|
|
`${cx + 1},${cy}` as CellKey,
|
|
|
|
`${cx - 1},${cy}` as CellKey,
|
|
|
|
`${cx - 1},${cy}` as CellKey,
|
|
|
@@ -219,8 +240,11 @@ export function getAdjacentItems<TMeta>(
|
|
|
|
];
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
for (const neighborKey of neighbors) {
|
|
|
|
for (const neighborKey of neighbors) {
|
|
|
|
if (inventory.occupiedCells.has(neighborKey) && !ownCells.has(neighborKey)) {
|
|
|
|
if (
|
|
|
|
const [nx, ny] = neighborKey.split(',').map(Number);
|
|
|
|
inventory.occupiedCells.has(neighborKey) &&
|
|
|
|
|
|
|
|
!ownCells.has(neighborKey)
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
const [nx, ny] = neighborKey.split(",").map(Number);
|
|
|
|
const neighborItem = getItemAtCell(inventory, nx, ny);
|
|
|
|
const neighborItem = getItemAtCell(inventory, nx, ny);
|
|
|
|
if (neighborItem) {
|
|
|
|
if (neighborItem) {
|
|
|
|
adjacent.set(neighborItem.id, neighborItem);
|
|
|
|
adjacent.set(neighborItem.id, neighborItem);
|
|
|
|