feat: add grid-inventory
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
# targetType can be one of: single, none
|
||||
|
||||
type,name,shape,costType,costCount,targetType,desc
|
||||
string,string,string,string,int,string,string
|
||||
string,string,string,'energy'|'uses',int,'single'|'none',string
|
||||
weapon,剑,oee,energy,1,single,【攻击2】【攻击2】
|
||||
weapon,长斧,oees,energy,2,none,对全体【攻击5】
|
||||
weapon,长枪,oeee,energy,1,single,【攻击2】【攻击2】【攻击2】
|
||||
|
||||
|
@@ -2,11 +2,13 @@ type HeroItemFighter1Table = readonly {
|
||||
readonly type: string;
|
||||
readonly name: string;
|
||||
readonly shape: string;
|
||||
readonly costType: string;
|
||||
readonly costType: "energy" | "uses";
|
||||
readonly costCount: number;
|
||||
readonly targetType: string;
|
||||
readonly targetType: "single" | "none";
|
||||
readonly desc: string;
|
||||
}[];
|
||||
|
||||
export type HeroItemFighter1 = HeroItemFighter1Table[number];
|
||||
|
||||
declare const data: HeroItemFighter1Table;
|
||||
export default data;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export type { CellCoordinate, GridInventory, InventoryItem, PlacementResult } from './types';
|
||||
export {
|
||||
createGridInventory,
|
||||
flipItem,
|
||||
getAdjacentItems,
|
||||
getItemAtCell,
|
||||
getOccupiedCellSet,
|
||||
moveItem,
|
||||
placeItem,
|
||||
removeItem,
|
||||
rotateItem,
|
||||
validatePlacement,
|
||||
} from './transform';
|
||||
@@ -0,0 +1,232 @@
|
||||
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 { GridInventory, InventoryItem, 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<string, InventoryItem>(),
|
||||
occupiedCells: new Set<string>(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a Set of occupied cell keys from a shape and transform.
|
||||
*/
|
||||
function getShapeCellKeys(shape: ParsedShape, transform: Transform2D): Set<string> {
|
||||
const cells = transformShape(shape, transform);
|
||||
return new Set(cells.map(c => `${c.x},${c.y}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates whether an item can be placed at the given transform.
|
||||
* Checks bounds and collision with all other items.
|
||||
*/
|
||||
export function validatePlacement(
|
||||
inventory: GridInventory,
|
||||
shape: InventoryItem['shape'],
|
||||
transform: Transform2D
|
||||
): PlacementResult {
|
||||
if (!checkBounds(shape, transform, inventory.width, inventory.height)) {
|
||||
return { valid: false, reason: '超出边界' };
|
||||
}
|
||||
|
||||
if (checkBoardCollision(shape, transform, inventory.occupiedCells)) {
|
||||
return { valid: false, reason: '与已有物品重叠' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Places an item onto the grid.
|
||||
* **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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
const cells = getShapeCellKeys(item.shape, item.transform);
|
||||
for (const cellKey of cells) {
|
||||
inventory.occupiedCells.delete(cellKey);
|
||||
}
|
||||
inventory.items.delete(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves an item to a new position with a new transform.
|
||||
* **Mutates directly** — call inside a `.produce()` callback.
|
||||
* Validates before applying; returns result indicating success.
|
||||
*/
|
||||
export function moveItem(
|
||||
inventory: GridInventory,
|
||||
itemId: string,
|
||||
newTransform: Transform2D
|
||||
): { success: true } | { success: false; reason: string } {
|
||||
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);
|
||||
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.add(cellKey);
|
||||
}
|
||||
return { success: false, reason: validation.reason };
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates an item by the given degrees (typically 90, 180, or 270).
|
||||
* **Mutates directly** — call inside a `.produce()` callback.
|
||||
* Validates before applying; returns result indicating success.
|
||||
*/
|
||||
export function rotateItem(
|
||||
inventory: GridInventory,
|
||||
itemId: string,
|
||||
degrees: number
|
||||
): { success: true } | { success: false; reason: string } {
|
||||
const item = inventory.items.get(itemId);
|
||||
if (!item) {
|
||||
return { success: false, reason: '物品不存在' };
|
||||
}
|
||||
|
||||
const rotatedTransform = rotateTransform(item.transform, degrees);
|
||||
return moveItem(inventory, itemId, rotatedTransform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flips an item horizontally or vertically.
|
||||
* **Mutates directly** — call inside a `.produce()` callback.
|
||||
* Validates before applying; returns result indicating success.
|
||||
*/
|
||||
export function flipItem(
|
||||
inventory: GridInventory,
|
||||
itemId: string,
|
||||
axis: 'x' | 'y'
|
||||
): { success: true } | { success: false; reason: string } {
|
||||
const item = inventory.items.get(itemId);
|
||||
if (!item) {
|
||||
return { success: false, reason: '物品不存在' };
|
||||
}
|
||||
|
||||
const flippedTransform = axis === 'x'
|
||||
? flipXTransform(item.transform)
|
||||
: flipYTransform(item.transform);
|
||||
|
||||
return moveItem(inventory, itemId, flippedTransform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the occupied cells set.
|
||||
*/
|
||||
export function getOccupiedCellSet(inventory: GridInventory): Set<string> {
|
||||
return new Set(inventory.occupiedCells);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the item occupying the given cell, if any.
|
||||
*/
|
||||
export function getItemAtCell(
|
||||
inventory: GridInventory,
|
||||
x: number,
|
||||
y: number
|
||||
): InventoryItem | undefined {
|
||||
const cellKey = `${x},${y}`;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all items adjacent to the given item (orthogonally, not diagonally).
|
||||
* Returns a Map of itemId -> item for deduplication.
|
||||
*/
|
||||
export function getAdjacentItems(
|
||||
inventory: GridInventory,
|
||||
itemId: string
|
||||
): Map<string, InventoryItem> {
|
||||
const item = inventory.items.get(itemId);
|
||||
if (!item) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const ownCells = getShapeCellKeys(item.shape, item.transform);
|
||||
const adjacent = new Map<string, InventoryItem>();
|
||||
|
||||
for (const cellKey of ownCells) {
|
||||
const [cx, cy] = cellKey.split(',').map(Number);
|
||||
const neighbors = [
|
||||
`${cx + 1},${cy}`,
|
||||
`${cx - 1},${cy}`,
|
||||
`${cx},${cy + 1}`,
|
||||
`${cx},${cy - 1}`,
|
||||
];
|
||||
|
||||
for (const neighborKey of neighbors) {
|
||||
if (inventory.occupiedCells.has(neighborKey) && !ownCells.has(neighborKey)) {
|
||||
const neighborItem = getItemAtCell(inventory, ...neighborKey.split(',').map(Number) as [number, number]);
|
||||
if (neighborItem) {
|
||||
adjacent.set(neighborItem.id, neighborItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return adjacent;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { ParsedShape } from '../utils/parse-shape';
|
||||
import type { Transform2D } from '../utils/shape-collision';
|
||||
|
||||
/**
|
||||
* Simple 2D coordinate for grid cells.
|
||||
*/
|
||||
export interface CellCoordinate {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* An item placed on the grid inventory.
|
||||
*/
|
||||
export interface InventoryItem {
|
||||
/** Unique item identifier */
|
||||
id: string;
|
||||
/** Reference to the item's shape definition */
|
||||
shape: ParsedShape;
|
||||
/** Current transformation (position, rotation, flips) */
|
||||
transform: Transform2D;
|
||||
/** Optional metadata for game-specific data */
|
||||
meta?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a placement validation check.
|
||||
*/
|
||||
export type PlacementResult = { valid: true } | { valid: false; reason: string };
|
||||
|
||||
/**
|
||||
* Grid inventory state.
|
||||
* Designed to be mutated directly inside a `mutative .produce()` callback.
|
||||
*/
|
||||
export interface GridInventory {
|
||||
/** Board width in cells */
|
||||
width: number;
|
||||
/** Board height in cells */
|
||||
height: number;
|
||||
/** Map of itemId -> InventoryItem for all placed items */
|
||||
items: Map<string, InventoryItem>;
|
||||
/** Set of occupied cells in "x,y" format for O(1) collision lookups */
|
||||
occupiedCells: Set<string>;
|
||||
}
|
||||
Reference in New Issue
Block a user