boardgame-core/src/samples/slay-the-spire-like/deck/factory.ts

205 lines
5.6 KiB
TypeScript

import type { CellKey, GridInventory, InventoryItem } from '../grid-inventory/types';
import type { GameItemMeta } from '../progress/types';
import { createRegion, createRegionAxis } from '@/core/region';
import type { GameCard, GameCardMeta, PlayerDeck, DeckRegions } from './types';
/**
* Generates a unique card ID for a cell within an item.
*/
function generateCardId(itemId: string, cellIndex: number): string {
return `card-${itemId}-${cellIndex}`;
}
/**
* Collects all cell keys from an item's shape in a deterministic order.
* Iterates the shape grid row by row, left to right, top to bottom.
*/
function getItemCells(item: InventoryItem<GameItemMeta>): CellKey[] {
const cells: CellKey[] = [];
const { shape, transform } = item;
const { grid } = shape;
const { offset, rotation, flipX, flipY } = transform;
// Track local dimensions (may swap on rotation)
let localWidth = grid[0]?.length || 1;
let localHeight = grid.length;
for (let gy = 0; gy < grid.length; gy++) {
for (let gx = 0; gx < grid[gy].length; gx++) {
if (!grid[gy][gx]) continue;
// Start from grid coordinates
let x = gx;
let y = gy;
// Apply rotation (90 degree increments, clockwise)
const rotTimes = ((rotation % 4) + 4) % 4;
for (let r = 0; r < rotTimes; r++) {
const newX = localHeight - 1 - y;
const newY = x;
x = newX;
y = newY;
// Swap dimensions for next iteration
const tmp = localWidth;
localWidth = localHeight;
localHeight = tmp;
}
// Reset local dimensions for fresh computation per cell
localWidth = grid[0]?.length || 1;
localHeight = grid.length;
// Apply flips
if (flipX) {
x = -x;
}
if (flipY) {
y = -y;
}
// Apply offset
const finalX = x + offset.x;
const finalY = y + offset.y;
cells.push(`${finalX},${finalY}`);
}
}
return cells;
}
/**
* Creates a single card from an inventory item.
*
* @param itemId - The inventory item instance ID
* @param itemData - The CSV item data
* @param cellKey - The cell key ("x,y") this card represents
* @param cellIndex - Index of the cell for unique ID generation
*/
function createItemCard(
itemId: string,
itemData: GameItemMeta['itemData'],
cellKey: CellKey,
cellIndex: number
): GameCard {
const cardId = generateCardId(itemId, cellIndex);
return {
id: cardId,
regionId: '',
position: [],
sourceItemId: itemId,
itemData,
cellKey,
displayName: itemData.name,
description: itemData.desc,
};
}
/**
* Creates a status card that does not correspond to any inventory item.
* Status cards represent temporary effects like wounds, stuns, etc.
*
* @param id - Unique identifier for the card instance
* @param displayName - Display name (e.g. "伤口", "眩晕")
* @param description - Card description / ability text
*/
function createStatusCard(
id: string,
displayName: string,
description: string
): GameCard {
return {
id,
regionId: '',
position: [],
sourceItemId: null,
itemData: null,
cellKey: null,
displayName,
description,
};
}
/**
* Generates a complete player deck from the current inventory state.
*
* For each item in the inventory, N cards are generated where N is the
* number of cells the item occupies (shape.count).
*
* All generated cards are placed in the draw pile initially.
*
* @param inventory - The player's grid inventory
* @returns A PlayerDeck with all cards in the draw pile
*/
function generateDeckFromInventory(inventory: GridInventory<GameItemMeta>): PlayerDeck {
const cards: Record<string, GameCard> = {};
const drawPile: string[] = [];
for (const item of inventory.items.values()) {
const itemData = item.meta?.itemData;
if (!itemData) continue;
// Generate one card per occupied cell in the item's shape
const cellCount = item.shape.count;
const cells = getItemCells(item);
for (let i = 0; i < cellCount; i++) {
const cellKey = cells[i] ?? `${i},0`;
const card = createItemCard(item.id, itemData, cellKey, i);
cards[card.id] = card;
drawPile.push(card.id);
}
}
return {
cards,
drawPile,
hand: [],
discardPile: [],
exhaustPile: [],
};
}
/**
* Creates region definitions for deck management.
* Returns regions for draw pile, hand, discard pile, and exhaust pile.
*/
function createDeckRegions(): DeckRegions {
return {
drawPile: createRegion('drawPile', [
createRegionAxis('index', 0, 0), // unbounded
]),
hand: createRegion('hand', [
createRegionAxis('index', 0, 0),
]),
discardPile: createRegion('discardPile', [
createRegionAxis('index', 0, 0),
]),
exhaustPile: createRegion('exhaustPile', [
createRegionAxis('index', 0, 0),
]),
};
}
/**
* Creates an empty player deck structure.
*/
function createPlayerDeck(): PlayerDeck {
return {
cards: {},
drawPile: [],
hand: [],
discardPile: [],
exhaustPile: [],
};
}
export {
generateDeckFromInventory,
createStatusCard,
createDeckRegions,
createPlayerDeck,
generateCardId,
};