Compare commits
No commits in common. "140b7aed861be3aa8d236a586aac58f6aad984a8" and "34cb828f60e4a4d20bd2c93089e1fa99fb70406e" have entirely different histories.
140b7aed86
...
34cb828f60
|
|
@ -52,7 +52,6 @@ export {
|
||||||
removeItem as removeItemFromGrid,
|
removeItem as removeItemFromGrid,
|
||||||
rotateItem,
|
rotateItem,
|
||||||
validatePlacement,
|
validatePlacement,
|
||||||
createItemIn,
|
|
||||||
} from "./system/grid-inventory";
|
} from "./system/grid-inventory";
|
||||||
|
|
||||||
// Map
|
// Map
|
||||||
|
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
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,8 +6,6 @@ export type {
|
||||||
MutationResult,
|
MutationResult,
|
||||||
PlacementResult,
|
PlacementResult,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
export type { GameItemMeta, GameItem } from "./types";
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createGridInventory,
|
createGridInventory,
|
||||||
flipItem,
|
flipItem,
|
||||||
|
|
@ -21,4 +19,4 @@ export {
|
||||||
validatePlacement,
|
validatePlacement,
|
||||||
} from "./transform";
|
} from "./transform";
|
||||||
|
|
||||||
export { createItemIn } from "./factory";
|
export type { GameItemMeta, GameItem } from "./types";
|
||||||
|
|
|
||||||
|
|
@ -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,23 +7,14 @@ import {
|
||||||
flipYTransform,
|
flipYTransform,
|
||||||
rotateTransform,
|
rotateTransform,
|
||||||
transformShape,
|
transformShape,
|
||||||
} from "../utils/shape-collision";
|
} from '../utils/shape-collision';
|
||||||
import type {
|
import type { CellKey, GridInventory, InventoryItem, MutationResult, PlacementResult } from './types';
|
||||||
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>>(
|
export function createGridInventory<TMeta = Record<string, unknown>>(width: number, height: number): GridInventory<TMeta> {
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
): GridInventory<TMeta> {
|
|
||||||
return {
|
return {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
|
@ -35,12 +26,9 @@ export function createGridInventory<TMeta = Record<string, unknown>>(
|
||||||
/**
|
/**
|
||||||
* 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(
|
function getShapeCellKeys(shape: ParsedShape, transform: Transform2D): Set<CellKey> {
|
||||||
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -49,15 +37,15 @@ function getShapeCellKeys(
|
||||||
*/
|
*/
|
||||||
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 };
|
||||||
|
|
@ -68,10 +56,7 @@ 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>>(
|
export function placeItem<TMeta = Record<string, unknown>>(inventory: GridInventory<TMeta>, item: InventoryItem<TMeta>): void {
|
||||||
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);
|
||||||
|
|
@ -83,10 +68,7 @@ export function placeItem<TMeta = Record<string, unknown>>(
|
||||||
* 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>>(
|
export function removeItem<TMeta = Record<string, unknown>>(inventory: GridInventory<TMeta>, itemId: string): void {
|
||||||
inventory: GridInventory<TMeta>,
|
|
||||||
itemId: string,
|
|
||||||
): void {
|
|
||||||
const item = inventory.items.get(itemId);
|
const item = inventory.items.get(itemId);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
|
|
@ -105,11 +87,11 @@ export function removeItem<TMeta = Record<string, unknown>>(
|
||||||
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
|
||||||
|
|
@ -148,11 +130,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);
|
||||||
|
|
@ -167,15 +149,14 @@ 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 =
|
const flippedTransform = axis === 'x'
|
||||||
axis === "x"
|
|
||||||
? flipXTransform(item.transform)
|
? flipXTransform(item.transform)
|
||||||
: flipYTransform(item.transform);
|
: flipYTransform(item.transform);
|
||||||
|
|
||||||
|
|
@ -185,9 +166,7 @@ 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>>(
|
export function getOccupiedCellSet<TMeta = Record<string, unknown>>(inventory: GridInventory<TMeta>): Set<CellKey> {
|
||||||
inventory: GridInventory<TMeta>,
|
|
||||||
): Set<CellKey> {
|
|
||||||
return new Set(inventory.occupiedCells);
|
return new Set(inventory.occupiedCells);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,7 +176,7 @@ export function getOccupiedCellSet<TMeta = Record<string, unknown>>(
|
||||||
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)) {
|
||||||
|
|
@ -220,7 +199,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) {
|
||||||
|
|
@ -231,7 +210,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,
|
||||||
|
|
@ -240,11 +219,8 @@ export function getAdjacentItems<TMeta>(
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const neighborKey of neighbors) {
|
for (const neighborKey of neighbors) {
|
||||||
if (
|
if (inventory.occupiedCells.has(neighborKey) && !ownCells.has(neighborKey)) {
|
||||||
inventory.occupiedCells.has(neighborKey) &&
|
const [nx, ny] = neighborKey.split(',').map(Number);
|
||||||
!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);
|
||||||
|
|
|
||||||
|
|
@ -15,48 +15,6 @@ import {
|
||||||
type GridInventory,
|
type GridInventory,
|
||||||
type InventoryItem,
|
type InventoryItem,
|
||||||
} from "@/samples/slay-the-spire-like/system/grid-inventory";
|
} from "@/samples/slay-the-spire-like/system/grid-inventory";
|
||||||
import { createItemIn } from "@/samples/slay-the-spire-like/system/grid-inventory/factory";
|
|
||||||
import type { GameItemMeta } from "@/samples/slay-the-spire-like/system/grid-inventory/types";
|
|
||||||
import type {
|
|
||||||
CardData,
|
|
||||||
ItemData,
|
|
||||||
} from "@/samples/slay-the-spire-like/system/types";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: create a minimal CardData for testing.
|
|
||||||
*/
|
|
||||||
function createTestCardData(id: string, name: string, desc: string): CardData {
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
desc,
|
|
||||||
type: "item",
|
|
||||||
costType: "energy",
|
|
||||||
costCount: 1,
|
|
||||||
targetType: "single",
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: create a minimal ItemData for testing.
|
|
||||||
*/
|
|
||||||
function createTestItemData(
|
|
||||||
id: string,
|
|
||||||
name: string,
|
|
||||||
shapeStr: string,
|
|
||||||
desc: string,
|
|
||||||
): ItemData {
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
type: "weapon",
|
|
||||||
name,
|
|
||||||
shape: shapeStr,
|
|
||||||
card: createTestCardData(id, name, desc),
|
|
||||||
price: 10,
|
|
||||||
description: desc,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper: create a test inventory item.
|
* Helper: create a test inventory item.
|
||||||
|
|
@ -577,81 +535,4 @@ describe("grid-inventory", () => {
|
||||||
expect(adjShield.has("sword")).toBe(true);
|
expect(adjShield.has("sword")).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("createItemIn", () => {
|
|
||||||
it("should place item at first valid position", () => {
|
|
||||||
const inv = createGridInventory<GameItemMeta>(6, 4);
|
|
||||||
const itemData = createTestItemData("sword", "长剑", "oee", "攻击");
|
|
||||||
|
|
||||||
const result = createItemIn(inv, "sword-1", itemData);
|
|
||||||
|
|
||||||
expect(result).toEqual({ success: true });
|
|
||||||
expect(inv.items.size).toBe(1);
|
|
||||||
expect(inv.occupiedCells.size).toBe(3); // shape "oee" has 3 cells
|
|
||||||
expect(inv.items.get("sword-1")?.meta?.itemData.id).toBe("sword");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should skip occupied cells and find next valid position", () => {
|
|
||||||
const inv = createGridInventory<GameItemMeta>(6, 4);
|
|
||||||
|
|
||||||
// Place first item manually at origin
|
|
||||||
const firstItem = createTestItem("existing", "oee");
|
|
||||||
placeItem(inv, firstItem);
|
|
||||||
|
|
||||||
const itemData = createTestItemData("shield", "盾牌", "o", "防御");
|
|
||||||
|
|
||||||
const result = createItemIn(inv, "shield-1", itemData);
|
|
||||||
|
|
||||||
expect(result).toEqual({ success: true });
|
|
||||||
// Shield should be at x=3, y=0 (first available spot after "oee" at x=0-2)
|
|
||||||
const placedItem = inv.items.get("shield-1");
|
|
||||||
expect(placedItem?.transform.offset).toEqual({ x: 3, y: 0 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return error when no valid placement exists", () => {
|
|
||||||
const inv = createGridInventory<GameItemMeta>(3, 3);
|
|
||||||
|
|
||||||
// Fill the grid completely with 1x1 items
|
|
||||||
for (let i = 0; i < 9; i++) {
|
|
||||||
const itemData = createTestItemData(`item${i}`, `物品${i}`, "o", "");
|
|
||||||
createItemIn(inv, `item-${i}`, itemData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to place one more item
|
|
||||||
const itemData = createTestItemData("overflow", "溢出", "oee", "");
|
|
||||||
const result = createItemIn(inv, "overflow-1", itemData);
|
|
||||||
|
|
||||||
expect(result).toEqual({ success: false, reason: "无可用位置" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle multi-cell items correctly", () => {
|
|
||||||
const inv = createGridInventory<GameItemMeta>(4, 4);
|
|
||||||
const itemData = createTestItemData("lshape", "L形", "oes", "特殊");
|
|
||||||
|
|
||||||
const result = createItemIn(inv, "lshape-1", itemData);
|
|
||||||
|
|
||||||
expect(result).toEqual({ success: true });
|
|
||||||
expect(inv.items.size).toBe(1);
|
|
||||||
// Shape "oes" has 3 cells
|
|
||||||
expect(inv.occupiedCells.size).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not place item when inventory is completely full", () => {
|
|
||||||
const inv = createGridInventory<GameItemMeta>(2, 2);
|
|
||||||
|
|
||||||
// Fill with 1x1 items
|
|
||||||
const itemData = createTestItemData("small", "小物品", "o", "");
|
|
||||||
createItemIn(inv, "small-1", itemData);
|
|
||||||
createItemIn(inv, "small-2", itemData);
|
|
||||||
createItemIn(inv, "small-3", itemData);
|
|
||||||
createItemIn(inv, "small-4", itemData);
|
|
||||||
|
|
||||||
// Grid is now full
|
|
||||||
const overflowItem = createTestItemData("overflow", "溢出", "o", "");
|
|
||||||
const result = createItemIn(inv, "overflow-5", overflowItem);
|
|
||||||
|
|
||||||
expect(result).toEqual({ success: false, reason: "无可用位置" });
|
|
||||||
expect(inv.items.size).toBe(4);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue