feat(slay-the-spire-like): add automatic item placement to grid
inventory Introduce `createItemIn` to automatically find the first valid placement for a new item within the grid inventory.
This commit is contained in:
parent
34cb828f60
commit
ca9912a5f5
|
|
@ -52,6 +52,7 @@ export {
|
|||
removeItem as removeItemFromGrid,
|
||||
rotateItem,
|
||||
validatePlacement,
|
||||
createItemIn,
|
||||
} from "./system/grid-inventory";
|
||||
|
||||
// Map
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
import { parseShapeString } from "../utils/parse-shape";
|
||||
import type { ParsedShape } from "../utils/parse-shape";
|
||||
import type { Transform2D } from "../utils/shape-collision";
|
||||
import { placeItem, validatePlacement } from "./transform";
|
||||
import type { GameItemMeta, GridInventory, MutationResult } from "./types";
|
||||
import type { ItemData } from "../types";
|
||||
|
||||
/**
|
||||
* Creates and places a GameItemMeta item into the grid inventory.
|
||||
* Parses the shape from itemData.shape and finds the first valid placement automatically.
|
||||
* **Mutates directly** — call inside a `.produce()` callback.
|
||||
*/
|
||||
export function createItemIn(
|
||||
inventory: GridInventory<GameItemMeta>,
|
||||
id: string,
|
||||
itemData: ItemData,
|
||||
): MutationResult {
|
||||
const shape = parseShapeString(itemData.shape);
|
||||
const transform = findFirstValidPlacement(inventory, shape);
|
||||
if (!transform) {
|
||||
return { success: false, reason: "无可用位置" };
|
||||
}
|
||||
|
||||
const meta: GameItemMeta = { itemData, shape };
|
||||
placeItem(inventory, { id, shape, transform, meta });
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for the first valid placement in the grid (row by row, left to right).
|
||||
* Returns the transform or null if no valid placement exists.
|
||||
*/
|
||||
function findFirstValidPlacement(
|
||||
inventory: GridInventory<GameItemMeta>,
|
||||
shape: ParsedShape,
|
||||
): Transform2D | null {
|
||||
for (let y = 0; y <= inventory.height - shape.height; y++) {
|
||||
for (let x = 0; x <= inventory.width - shape.width; x++) {
|
||||
const transform: Transform2D = {
|
||||
offset: { x, y },
|
||||
rotation: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
};
|
||||
const result = validatePlacement(inventory, shape, transform);
|
||||
if (result.valid) {
|
||||
return transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -6,6 +6,8 @@ export type {
|
|||
MutationResult,
|
||||
PlacementResult,
|
||||
} from "./types";
|
||||
export type { GameItemMeta, GameItem } from "./types";
|
||||
|
||||
export {
|
||||
createGridInventory,
|
||||
flipItem,
|
||||
|
|
@ -19,4 +21,4 @@ export {
|
|||
validatePlacement,
|
||||
} from "./transform";
|
||||
|
||||
export type { GameItemMeta, GameItem } from "./types";
|
||||
export { createItemIn } from "./factory";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ParsedShape } from '../utils/parse-shape';
|
||||
import type { Transform2D } from '../utils/shape-collision';
|
||||
import type { ParsedShape } from "../utils/parse-shape";
|
||||
import type { Transform2D } from "../utils/shape-collision";
|
||||
import {
|
||||
checkBoardCollision,
|
||||
checkBounds,
|
||||
|
|
@ -7,14 +7,23 @@ import {
|
|||
flipYTransform,
|
||||
rotateTransform,
|
||||
transformShape,
|
||||
} from '../utils/shape-collision';
|
||||
import type { CellKey, GridInventory, InventoryItem, MutationResult, PlacementResult } from './types';
|
||||
} from "../utils/shape-collision";
|
||||
import type {
|
||||
CellKey,
|
||||
GridInventory,
|
||||
InventoryItem,
|
||||
MutationResult,
|
||||
PlacementResult,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Creates a new empty grid inventory.
|
||||
* 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 {
|
||||
width,
|
||||
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.
|
||||
*/
|
||||
function getShapeCellKeys(shape: ParsedShape, transform: Transform2D): Set<CellKey> {
|
||||
function getShapeCellKeys(
|
||||
shape: ParsedShape,
|
||||
transform: Transform2D,
|
||||
): Set<CellKey> {
|
||||
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>>(
|
||||
inventory: GridInventory<TMeta>,
|
||||
shape: InventoryItem<TMeta>['shape'],
|
||||
transform: Transform2D
|
||||
shape: InventoryItem<TMeta>["shape"],
|
||||
transform: Transform2D,
|
||||
): PlacementResult {
|
||||
if (!checkBounds(shape, transform, inventory.width, inventory.height)) {
|
||||
return { valid: false, reason: '超出边界' };
|
||||
return { valid: false, reason: "超出边界" };
|
||||
}
|
||||
|
||||
if (checkBoardCollision(shape, transform, inventory.occupiedCells)) {
|
||||
return { valid: false, reason: '与已有物品重叠' };
|
||||
return { valid: false, reason: "与已有物品重叠" };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
|
|
@ -56,7 +68,10 @@ export function validatePlacement<TMeta = Record<string, unknown>>(
|
|||
* **Mutates directly** — call inside a `.produce()` callback.
|
||||
* 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);
|
||||
for (const cellKey of cells) {
|
||||
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.
|
||||
* **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);
|
||||
if (!item) return;
|
||||
|
||||
|
|
@ -87,11 +105,11 @@ export function removeItem<TMeta = Record<string, unknown>>(inventory: GridInven
|
|||
export function moveItem<TMeta = Record<string, unknown>>(
|
||||
inventory: GridInventory<TMeta>,
|
||||
itemId: string,
|
||||
newTransform: Transform2D
|
||||
newTransform: Transform2D,
|
||||
): MutationResult {
|
||||
const item = inventory.items.get(itemId);
|
||||
if (!item) {
|
||||
return { success: false, reason: '物品不存在' };
|
||||
return { success: false, reason: "物品不存在" };
|
||||
}
|
||||
|
||||
// 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>>(
|
||||
inventory: GridInventory<TMeta>,
|
||||
itemId: string,
|
||||
degrees: number
|
||||
degrees: number,
|
||||
): MutationResult {
|
||||
const item = inventory.items.get(itemId);
|
||||
if (!item) {
|
||||
return { success: false, reason: '物品不存在' };
|
||||
return { success: false, reason: "物品不存在" };
|
||||
}
|
||||
|
||||
const rotatedTransform = rotateTransform(item.transform, degrees);
|
||||
|
|
@ -149,14 +167,15 @@ export function rotateItem<TMeta = Record<string, unknown>>(
|
|||
export function flipItem<TMeta = Record<string, unknown>>(
|
||||
inventory: GridInventory<TMeta>,
|
||||
itemId: string,
|
||||
axis: 'x' | 'y'
|
||||
axis: "x" | "y",
|
||||
): MutationResult {
|
||||
const item = inventory.items.get(itemId);
|
||||
if (!item) {
|
||||
return { success: false, reason: '物品不存在' };
|
||||
return { success: false, reason: "物品不存在" };
|
||||
}
|
||||
|
||||
const flippedTransform = axis === 'x'
|
||||
const flippedTransform =
|
||||
axis === "x"
|
||||
? flipXTransform(item.transform)
|
||||
: flipYTransform(item.transform);
|
||||
|
||||
|
|
@ -166,7 +185,9 @@ export function flipItem<TMeta = Record<string, unknown>>(
|
|||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
|
|
@ -176,7 +197,7 @@ export function getOccupiedCellSet<TMeta = Record<string, unknown>>(inventory: G
|
|||
export function getItemAtCell<TMeta = Record<string, unknown>>(
|
||||
inventory: GridInventory<TMeta>,
|
||||
x: number,
|
||||
y: number
|
||||
y: number,
|
||||
): InventoryItem<TMeta> | undefined {
|
||||
const cellKey = `${x},${y}` as CellKey;
|
||||
if (!inventory.occupiedCells.has(cellKey)) {
|
||||
|
|
@ -199,7 +220,7 @@ export function getItemAtCell<TMeta = Record<string, unknown>>(
|
|||
*/
|
||||
export function getAdjacentItems<TMeta>(
|
||||
inventory: GridInventory<TMeta>,
|
||||
itemId: string
|
||||
itemId: string,
|
||||
): Map<string, InventoryItem<TMeta>> {
|
||||
const item = inventory.items.get(itemId);
|
||||
if (!item) {
|
||||
|
|
@ -210,7 +231,7 @@ export function getAdjacentItems<TMeta>(
|
|||
const adjacent = new Map<string, InventoryItem<TMeta>>();
|
||||
|
||||
for (const cellKey of ownCells) {
|
||||
const [cx, cy] = cellKey.split(',').map(Number);
|
||||
const [cx, cy] = cellKey.split(",").map(Number);
|
||||
const neighbors: CellKey[] = [
|
||||
`${cx + 1},${cy}` as CellKey,
|
||||
`${cx - 1},${cy}` as CellKey,
|
||||
|
|
@ -219,8 +240,11 @@ export function getAdjacentItems<TMeta>(
|
|||
];
|
||||
|
||||
for (const neighborKey of neighbors) {
|
||||
if (inventory.occupiedCells.has(neighborKey) && !ownCells.has(neighborKey)) {
|
||||
const [nx, ny] = neighborKey.split(',').map(Number);
|
||||
if (
|
||||
inventory.occupiedCells.has(neighborKey) &&
|
||||
!ownCells.has(neighborKey)
|
||||
) {
|
||||
const [nx, ny] = neighborKey.split(",").map(Number);
|
||||
const neighborItem = getItemAtCell(inventory, nx, ny);
|
||||
if (neighborItem) {
|
||||
adjacent.set(neighborItem.id, neighborItem);
|
||||
|
|
|
|||
Loading…
Reference in New Issue