diff --git a/src/samples/slay-the-spire-like/index.ts b/src/samples/slay-the-spire-like/index.ts index 363fd80..b211e0b 100644 --- a/src/samples/slay-the-spire-like/index.ts +++ b/src/samples/slay-the-spire-like/index.ts @@ -52,6 +52,7 @@ export { removeItem as removeItemFromGrid, rotateItem, validatePlacement, + createItemIn, } from "./system/grid-inventory"; // Map diff --git a/src/samples/slay-the-spire-like/system/grid-inventory/factory.ts b/src/samples/slay-the-spire-like/system/grid-inventory/factory.ts new file mode 100644 index 0000000..edc4dc6 --- /dev/null +++ b/src/samples/slay-the-spire-like/system/grid-inventory/factory.ts @@ -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, + 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, + 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; +} diff --git a/src/samples/slay-the-spire-like/system/grid-inventory/index.ts b/src/samples/slay-the-spire-like/system/grid-inventory/index.ts index 465ac33..42beaa8 100644 --- a/src/samples/slay-the-spire-like/system/grid-inventory/index.ts +++ b/src/samples/slay-the-spire-like/system/grid-inventory/index.ts @@ -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"; diff --git a/src/samples/slay-the-spire-like/system/grid-inventory/transform.ts b/src/samples/slay-the-spire-like/system/grid-inventory/transform.ts index ef3cff3..c1e0cb1 100644 --- a/src/samples/slay-the-spire-like/system/grid-inventory/transform.ts +++ b/src/samples/slay-the-spire-like/system/grid-inventory/transform.ts @@ -1,34 +1,46 @@ -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, - flipXTransform, - flipYTransform, - rotateTransform, - transformShape, -} from '../utils/shape-collision'; -import type { CellKey, GridInventory, InventoryItem, MutationResult, PlacementResult } from './types'; + checkBoardCollision, + checkBounds, + flipXTransform, + flipYTransform, + rotateTransform, + transformShape, +} 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>(width: number, height: number): GridInventory { - return { - width, - height, - items: new Map>(), - occupiedCells: new Set(), - }; +export function createGridInventory>( + width: number, + height: number, +): GridInventory { + return { + width, + height, + items: new Map>(), + occupiedCells: new Set(), + }; } /** * Builds a Set of occupied cell keys from a shape and transform. */ -function getShapeCellKeys(shape: ParsedShape, transform: Transform2D): Set { - const cells = transformShape(shape, transform); - return new Set(cells.map(c => `${c.x},${c.y}` as CellKey)); +function getShapeCellKeys( + shape: ParsedShape, + transform: Transform2D, +): Set { + const cells = transformShape(shape, transform); + return new Set(cells.map((c) => `${c.x},${c.y}` as CellKey)); } /** @@ -36,19 +48,19 @@ function getShapeCellKeys(shape: ParsedShape, transform: Transform2D): Set>( - inventory: GridInventory, - shape: InventoryItem['shape'], - transform: Transform2D + inventory: GridInventory, + shape: InventoryItem["shape"], + transform: Transform2D, ): PlacementResult { - if (!checkBounds(shape, transform, inventory.width, inventory.height)) { - return { valid: false, reason: '超出边界' }; - } + if (!checkBounds(shape, transform, inventory.width, inventory.height)) { + return { valid: false, reason: "超出边界" }; + } - if (checkBoardCollision(shape, transform, inventory.occupiedCells)) { - return { valid: false, reason: '与已有物品重叠' }; - } + if (checkBoardCollision(shape, transform, inventory.occupiedCells)) { + return { valid: false, reason: "与已有物品重叠" }; + } - return { valid: true }; + return { valid: true }; } /** @@ -56,27 +68,33 @@ export function validatePlacement>( * **Mutates directly** — call inside a `.produce()` callback. * Does not validate; call `validatePlacement` first. */ -export function placeItem>(inventory: GridInventory, item: InventoryItem): void { - const cells = getShapeCellKeys(item.shape, item.transform); - for (const cellKey of cells) { - inventory.occupiedCells.add(cellKey); - } - inventory.items.set(item.id, item); +export function placeItem>( + inventory: GridInventory, + item: InventoryItem, +): void { + const cells = getShapeCellKeys(item.shape, item.transform); + for (const cellKey of cells) { + inventory.occupiedCells.add(cellKey); + } + inventory.items.set(item.id, item); } /** * Removes an item from the grid by its ID. * **Mutates directly** — call inside a `.produce()` callback. */ -export function removeItem>(inventory: GridInventory, itemId: string): void { - const item = inventory.items.get(itemId); - if (!item) return; +export function removeItem>( + inventory: GridInventory, + itemId: string, +): void { + const item = inventory.items.get(itemId); + if (!item) return; - const cells = getShapeCellKeys(item.shape, item.transform); - for (const cellKey of cells) { - inventory.occupiedCells.delete(cellKey); - } - inventory.items.delete(itemId); + const cells = getShapeCellKeys(item.shape, item.transform); + for (const cellKey of cells) { + inventory.occupiedCells.delete(cellKey); + } + inventory.items.delete(itemId); } /** @@ -85,41 +103,41 @@ export function removeItem>(inventory: GridInven * Validates before applying; returns result indicating success. */ export function moveItem>( - inventory: GridInventory, - itemId: string, - newTransform: Transform2D + inventory: GridInventory, + itemId: string, + newTransform: Transform2D, ): MutationResult { - const item = inventory.items.get(itemId); - if (!item) { - return { success: false, reason: '物品不存在' }; - } + const item = inventory.items.get(itemId); + if (!item) { + return { success: false, reason: "物品不存在" }; + } - // Temporarily remove item's cells for validation - const oldCells = getShapeCellKeys(item.shape, item.transform); + // Temporarily remove item's cells for validation + const oldCells = getShapeCellKeys(item.shape, item.transform); + for (const cellKey of oldCells) { + inventory.occupiedCells.delete(cellKey); + } + + // Validate new position + const validation = validatePlacement(inventory, item.shape, newTransform); + if (!validation.valid) { + // Restore old cells for (const cellKey of oldCells) { - inventory.occupiedCells.delete(cellKey); + inventory.occupiedCells.add(cellKey); } + return { success: false, reason: validation.reason }; + } - // Validate new position - const validation = validatePlacement(inventory, item.shape, newTransform); - if (!validation.valid) { - // Restore old cells - for (const cellKey of oldCells) { - inventory.occupiedCells.add(cellKey); - } - return { success: false, reason: validation.reason }; - } + // Apply new transform + item.transform = newTransform; - // Apply new transform - item.transform = newTransform; + // Add new cells + const newCells = getShapeCellKeys(item.shape, item.transform); + for (const cellKey of newCells) { + inventory.occupiedCells.add(cellKey); + } - // Add new cells - const newCells = getShapeCellKeys(item.shape, item.transform); - for (const cellKey of newCells) { - inventory.occupiedCells.add(cellKey); - } - - return { success: true }; + return { success: true }; } /** @@ -128,17 +146,17 @@ export function moveItem>( * Validates before applying; returns result indicating success. */ export function rotateItem>( - inventory: GridInventory, - itemId: string, - degrees: number + inventory: GridInventory, + itemId: string, + degrees: number, ): MutationResult { - const item = inventory.items.get(itemId); - if (!item) { - return { success: false, reason: '物品不存在' }; - } + const item = inventory.items.get(itemId); + if (!item) { + return { success: false, reason: "物品不存在" }; + } - const rotatedTransform = rotateTransform(item.transform, degrees); - return moveItem(inventory, itemId, rotatedTransform); + const rotatedTransform = rotateTransform(item.transform, degrees); + return moveItem(inventory, itemId, rotatedTransform); } /** @@ -147,50 +165,53 @@ export function rotateItem>( * Validates before applying; returns result indicating success. */ export function flipItem>( - inventory: GridInventory, - itemId: string, - axis: 'x' | 'y' + inventory: GridInventory, + itemId: string, + axis: "x" | "y", ): MutationResult { - const item = inventory.items.get(itemId); - if (!item) { - return { success: false, reason: '物品不存在' }; - } + const item = inventory.items.get(itemId); + if (!item) { + return { success: false, reason: "物品不存在" }; + } - const flippedTransform = axis === 'x' - ? flipXTransform(item.transform) - : flipYTransform(item.transform); + const flippedTransform = + axis === "x" + ? flipXTransform(item.transform) + : flipYTransform(item.transform); - return moveItem(inventory, itemId, flippedTransform); + return moveItem(inventory, itemId, flippedTransform); } /** * Returns a copy of the occupied cells set. */ -export function getOccupiedCellSet>(inventory: GridInventory): Set { - return new Set(inventory.occupiedCells); +export function getOccupiedCellSet>( + inventory: GridInventory, +): Set { + return new Set(inventory.occupiedCells); } /** * Finds the item occupying the given cell, if any. */ export function getItemAtCell>( - inventory: GridInventory, - x: number, - y: number + inventory: GridInventory, + x: number, + y: number, ): InventoryItem | undefined { - const cellKey = `${x},${y}` as CellKey; - if (!inventory.occupiedCells.has(cellKey)) { - return undefined; - } - - for (const item of inventory.items.values()) { - const cells = getShapeCellKeys(item.shape, item.transform); - if (cells.has(cellKey)) { - return item; - } - } - + const cellKey = `${x},${y}` as CellKey; + if (!inventory.occupiedCells.has(cellKey)) { return undefined; + } + + for (const item of inventory.items.values()) { + const cells = getShapeCellKeys(item.shape, item.transform); + if (cells.has(cellKey)) { + return item; + } + } + + return undefined; } /** @@ -198,36 +219,39 @@ export function getItemAtCell>( * Returns a Map of itemId -> item for deduplication. */ export function getAdjacentItems( - inventory: GridInventory, - itemId: string + inventory: GridInventory, + itemId: string, ): Map> { - const item = inventory.items.get(itemId); - if (!item) { - return new Map(); - } + const item = inventory.items.get(itemId); + if (!item) { + return new Map(); + } - const ownCells = getShapeCellKeys(item.shape, item.transform); - const adjacent = new Map>(); + const ownCells = getShapeCellKeys(item.shape, item.transform); + const adjacent = new Map>(); - for (const cellKey of ownCells) { - const [cx, cy] = cellKey.split(',').map(Number); - const neighbors: CellKey[] = [ - `${cx + 1},${cy}` as CellKey, - `${cx - 1},${cy}` as CellKey, - `${cx},${cy + 1}` as CellKey, - `${cx},${cy - 1}` as CellKey, - ]; + for (const cellKey of ownCells) { + const [cx, cy] = cellKey.split(",").map(Number); + const neighbors: CellKey[] = [ + `${cx + 1},${cy}` as CellKey, + `${cx - 1},${cy}` as CellKey, + `${cx},${cy + 1}` as CellKey, + `${cx},${cy - 1}` as CellKey, + ]; - for (const neighborKey of neighbors) { - 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); - } - } + for (const neighborKey of neighbors) { + 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); } + } } + } - return adjacent; + return adjacent; }