From 6281044f142c5111d7cf2fce5ca2fa798fa27aed Mon Sep 17 00:00:00 2001 From: hyper Date: Sun, 12 Apr 2026 20:44:35 +0800 Subject: [PATCH] feat: add som shape parsing --- .../slay-the-spire-like/utils/parse-shape.ts | 6 +- .../utils/shape-collision.ts | 227 ++++++++++++++++++ 2 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 src/samples/slay-the-spire-like/utils/shape-collision.ts diff --git a/src/samples/slay-the-spire-like/utils/parse-shape.ts b/src/samples/slay-the-spire-like/utils/parse-shape.ts index 570b165..d0df888 100644 --- a/src/samples/slay-the-spire-like/utils/parse-shape.ts +++ b/src/samples/slay-the-spire-like/utils/parse-shape.ts @@ -8,6 +8,8 @@ export interface ParsedShape { width: number; /** Grid height (number of rows) */ height: number; + /** Number of occupied (filled) cells */ + count: number; /** Origin X coordinate within the grid */ originX: number; /** Origin Y coordinate within the grid */ @@ -76,7 +78,7 @@ export function parseShapeString(input: string): ParsedShape { } if (filledPoints.size === 0) { - return { grid: [[]], width: 0, height: 1, originX: 0, originY: 0 }; + return { grid: [[]], width: 0, height: 1, count: 0, originX: 0, originY: 0 }; } // Calculate bounding box to normalize the array @@ -103,5 +105,5 @@ export function parseShapeString(input: string): ParsedShape { const normalizedOriginX = originX - minX; const normalizedOriginY = originY - minY; - return { grid, width, height, originX: normalizedOriginX, originY: normalizedOriginY }; + return { grid, width, height, count: filledPoints.size, originX: normalizedOriginX, originY: normalizedOriginY }; } \ No newline at end of file diff --git a/src/samples/slay-the-spire-like/utils/shape-collision.ts b/src/samples/slay-the-spire-like/utils/shape-collision.ts new file mode 100644 index 0000000..fafdefb --- /dev/null +++ b/src/samples/slay-the-spire-like/utils/shape-collision.ts @@ -0,0 +1,227 @@ +import type { ParsedShape } from './parse-shape'; + +/** + * Represents a 2D point in grid coordinates. + */ +export interface Point2D { + x: number; + y: number; +} + +/** + * 2D transformation to apply to a shape. + */ +export interface Transform2D { + /** Translation offset in grid units */ + offset: Point2D; + /** Rotation in degrees (0, 90, 180, 270) */ + rotation: number; + /** Whether to flip horizontally */ + flipX: boolean; + /** Whether to flip vertically */ + flipY: boolean; +} + +/** + * Default transform (identity). + */ +export const IDENTITY_TRANSFORM: Transform2D = { + offset: { x: 0, y: 0 }, + rotation: 0, + flipX: false, + flipY: false, +}; + +/** + * Gets all occupied cell coordinates from a shape. + */ +export function getOccupiedCells(shape: ParsedShape): Point2D[] { + const cells: Point2D[] = []; + for (let y = 0; y < shape.height; y++) { + for (let x = 0; x < shape.width; x++) { + if (shape.grid[y]?.[x]) { + cells.push({ x, y }); + } + } + } + return cells; +} + +/** + * Applies a 2D transformation to a point. + */ +export function transformPoint( + point: Point2D, + transform: Transform2D, + shapeWidth: number, + shapeHeight: number +): Point2D { + let { x, y } = point; + + // Apply flips + if (transform.flipX) { + x = shapeWidth - 1 - x; + } + if (transform.flipY) { + y = shapeHeight - 1 - y; + } + + // Apply rotation (around origin 0,0) + const rotation = ((transform.rotation % 360) + 360) % 360; + let rotatedX = x; + let rotatedY = y; + + switch (rotation) { + case 90: + rotatedX = y; + rotatedY = -x; + break; + case 180: + rotatedX = -x; + rotatedY = -y; + break; + case 270: + rotatedX = -y; + rotatedY = x; + break; + } + + // Apply offset + return { + x: rotatedX + transform.offset.x, + y: rotatedY + transform.offset.y, + }; +} + +/** + * Transforms a shape and returnss its occupied cells in world coordinates. + */ +export function transformShape(shape: ParsedShape, transform: Transform2D): Point2D[] { + const cells = getOccupiedCells(shape); + return cells.map(cell => + transformPoint(cell, transform, shape.width, shape.height) + ); +} + +/** + * Checks if two transformed shapes collide (share any occupied cell). + */ +export function checkCollision( + shapeA: ParsedShape, + transformA: Transform2D, + shapeB: ParsedShape, + transformB: Transform2D +): boolean { + const cellsA = transformShape(shapeA, transformA); + const cellsB = transformShape(shapeB, transformB); + + const setA = new Set(cellsA.map(c => `${c.x},${c.y}`)); + + for (const cell of cellsB) { + if (setA.has(`${cell.x},${cell.y}`)) { + return true; + } + } + + return false; +} + +/** + * Checks if a transformed shape collides with any occupied cells on a board. + * @param shape The shape to check + * @param transform The transform to apply to the shape + * @param occupiedCells Set of occupied board cells in "x,y" format + */ +export function checkBoardCollision( + shape: ParsedShape, + transform: Transform2D, + occupiedCells: Set +): boolean { + const cells = transformShape(shape, transform); + + for (const cell of cells) { + if (occupiedCells.has(`${cell.x},${cell.y}`)) { + return true; + } + } + + return false; +} + +/** + * Checks if a transformed shape is within bounds. + * @param shape The shape to check + * @param transform The transform to apply to the shape + * @param boardWidth Board width + * @param boardHeight Board height + */ +export function checkBounds( + shape: ParsedShape, + transform: Transform2D, + boardWidth: number, + boardHeight: number +): boolean { + const cells = transformShape(shape, transform); + + for (const cell of cells) { + if (cell.x < 0 || cell.x >= boardWidth || cell.y < 0 || cell.y >= boardHeight) { + return false; + } + } + + return true; +} + +/** + * Validates that a placement is both in bounds and collision-free. + * @returns Object with `valid` flag and optional `reason` string + */ +export function validatePlacement( + shape: ParsedShape, + transform: Transform2D, + boardWidth: number, + boardHeight: number, + occupiedCells: Set +): { valid: true } | { valid: false; reason: string } { + if (!checkBounds(shape, transform, boardWidth, boardHeight)) { + return { valid: false, reason: '超出边界' }; + } + + if (checkBoardCollision(shape, transform, occupiedCells)) { + return { valid: false, reason: '与已有形状重叠' }; + } + + return { valid: true }; +} + +/** + * Rotates a transform by the given degrees. + * @param current The current transform + * @param degrees Degrees to rotate (typically 90, 180, or 270) + */ +export function rotateTransform(current: Transform2D, degrees: number): Transform2D { + return { + ...current, + rotation: ((current.rotation + degrees) % 360 + 360) % 360, + }; +} + +/** + * Flips a transform horizontally. + */ +export function flipXTransform(current: Transform2D): Transform2D { + return { + ...current, + flipX: !current.flipX, + }; +} + +/** + * Flips a transform vertically. + */ +export function flipYTransform(current: Transform2D): Transform2D { + return { + ...current, + flipY: !current.flipY, + }; +}