feat: deck generation
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
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,
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export type { GameCard, GameCardMeta, PlayerDeck, DeckRegions } from './types';
|
||||
export {
|
||||
generateDeckFromInventory,
|
||||
createStatusCard,
|
||||
createDeckRegions,
|
||||
createPlayerDeck,
|
||||
generateCardId,
|
||||
} from './factory';
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { Part } from '@/core/part';
|
||||
import type { Region } from '@/core/region';
|
||||
import type { HeroItemFighter1 } from '../data/heroItemFighter1.csv';
|
||||
import type { CellKey } from '../grid-inventory/types';
|
||||
|
||||
/**
|
||||
* Metadata for a game card.
|
||||
* Bridges inventory item data with the card system.
|
||||
*/
|
||||
export interface GameCardMeta {
|
||||
/**
|
||||
* Source item instance ID that this card was generated from.
|
||||
* `null` for status cards (e.g. wound, stun) that don't correspond to an inventory item.
|
||||
*/
|
||||
sourceItemId: string | null;
|
||||
/**
|
||||
* Original CSV item data. `null` for status cards.
|
||||
*/
|
||||
itemData: HeroItemFighter1 | null;
|
||||
/**
|
||||
* The cell key ("x,y") this card represents within the source item's shape.
|
||||
* `null` for status cards.
|
||||
*/
|
||||
cellKey: CellKey | null;
|
||||
/**
|
||||
* Display name of the card.
|
||||
* For item cards: derived from itemData.name.
|
||||
* For status cards: custom name (e.g. "伤口", "眩晕").
|
||||
*/
|
||||
displayName: string;
|
||||
/**
|
||||
* Card description / ability text.
|
||||
* For item cards: derived from itemData.desc.
|
||||
* For status cards: custom description.
|
||||
*/
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A card instance in the game.
|
||||
* Cards are generated from inventory items or created as status effects.
|
||||
*/
|
||||
export type GameCard = Part<GameCardMeta>;
|
||||
|
||||
/**
|
||||
* Player deck structure containing card pools.
|
||||
*/
|
||||
export interface PlayerDeck {
|
||||
/** All cards indexed by ID */
|
||||
cards: Record<string, GameCard>;
|
||||
/** Card IDs in the draw pile */
|
||||
drawPile: string[];
|
||||
/** Card IDs in the player's hand */
|
||||
hand: string[];
|
||||
/** Card IDs in the discard pile */
|
||||
discardPile: string[];
|
||||
/** Card IDs in the exhaust pile (removed from combat) */
|
||||
exhaustPile: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Region structure for deck management.
|
||||
*/
|
||||
export interface DeckRegions {
|
||||
/** Draw pile region */
|
||||
drawPile: Region;
|
||||
/** Hand region */
|
||||
hand: Region;
|
||||
/** Discard pile region */
|
||||
discardPile: Region;
|
||||
/** Exhaust pile region */
|
||||
exhaustPile: Region;
|
||||
}
|
||||
@@ -3,6 +3,16 @@ export { heroItemFighter1Data, encounterDesertData } from './data';
|
||||
export { default as encounterDesertCsv } from './data/encounterDesert.csv';
|
||||
export type { EncounterDesert } from './data/encounterDesert.csv';
|
||||
|
||||
// Deck
|
||||
export type { GameCard, GameCardMeta, PlayerDeck, DeckRegions } from './deck';
|
||||
export {
|
||||
generateDeckFromInventory,
|
||||
createStatusCard,
|
||||
createDeckRegions,
|
||||
createPlayerDeck,
|
||||
generateCardId,
|
||||
} from './deck';
|
||||
|
||||
// Grid Inventory
|
||||
export type { CellCoordinate, CellKey, GridInventory, InventoryItem, MutationResult, PlacementResult } from './grid-inventory';
|
||||
export {
|
||||
|
||||
Reference in New Issue
Block a user