Compare commits
No commits in common. "760cfc9954b76b0689b0ff76640884bd1a4c8429" and "204198b10fa98c0c4cacafcf5e94fbd03992b5ce" have entirely different histories.
760cfc9954
...
204198b10f
|
|
@ -1,204 +0,0 @@
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
export type { GameCard, GameCardMeta, PlayerDeck, DeckRegions } from './types';
|
|
||||||
export {
|
|
||||||
generateDeckFromInventory,
|
|
||||||
createStatusCard,
|
|
||||||
createDeckRegions,
|
|
||||||
createPlayerDeck,
|
|
||||||
generateCardId,
|
|
||||||
} from './factory';
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
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,16 +3,6 @@ export { heroItemFighter1Data, encounterDesertData } from './data';
|
||||||
export { default as encounterDesertCsv } from './data/encounterDesert.csv';
|
export { default as encounterDesertCsv } from './data/encounterDesert.csv';
|
||||||
export type { EncounterDesert } 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
|
// Grid Inventory
|
||||||
export type { CellCoordinate, CellKey, GridInventory, InventoryItem, MutationResult, PlacementResult } from './grid-inventory';
|
export type { CellCoordinate, CellKey, GridInventory, InventoryItem, MutationResult, PlacementResult } from './grid-inventory';
|
||||||
export {
|
export {
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,12 @@ function buildEncounterIndex(): Map<string, EncounterDesert[]> {
|
||||||
|
|
||||||
/** Map from MapNodeType to encounter type key */
|
/** Map from MapNodeType to encounter type key */
|
||||||
const NODE_TYPE_TO_ENCOUNTER: Partial<Record<MapNodeType, string>> = {
|
const NODE_TYPE_TO_ENCOUNTER: Partial<Record<MapNodeType, string>> = {
|
||||||
[MapNodeType.Minion]: 'minion',
|
[MapNodeType.Minion]: 'enemy',
|
||||||
[MapNodeType.Elite]: 'elite',
|
[MapNodeType.Elite]: 'elite',
|
||||||
[MapNodeType.Event]: 'event',
|
[MapNodeType.Event]: 'event',
|
||||||
[MapNodeType.Camp]: 'camp',
|
[MapNodeType.Camp]: 'shelter',
|
||||||
[MapNodeType.Shop]: 'shop',
|
[MapNodeType.Shop]: 'npc',
|
||||||
[MapNodeType.Curio]: 'curio',
|
[MapNodeType.Curio]: 'shelter',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Default map generation configuration */
|
/** Default map generation configuration */
|
||||||
|
|
@ -125,15 +125,9 @@ export function generatePointCrawlMap(seed?: number): PointCrawlMap {
|
||||||
const nodeIds: string[] = [];
|
const nodeIds: string[] = [];
|
||||||
const layerNodes: MapNode[] = [];
|
const layerNodes: MapNode[] = [];
|
||||||
|
|
||||||
// Pre-generate settlement types if this is a settlement layer
|
|
||||||
let settlementTypes: MapNodeType[] | undefined;
|
|
||||||
if (structure.layerType === MapLayerType.Settlement) {
|
|
||||||
settlementTypes = generateSettlementTypes(rng);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let j = 0; j < structure.count; j++) {
|
for (let j = 0; j < structure.count; j++) {
|
||||||
const id = `node-${i}-${j}`;
|
const id = `node-${i}-${j}`;
|
||||||
const type = resolveNodeType(structure.layerType, j, structure.count, rng, wildPairTypes.get(i), j, settlementTypes, j);
|
const type = resolveNodeType(structure.layerType, j, structure.count, rng, wildPairTypes.get(i), j);
|
||||||
const encounter = pickEncounterForNode(type, rng);
|
const encounter = pickEncounterForNode(type, rng);
|
||||||
const node: MapNode = {
|
const node: MapNode = {
|
||||||
id,
|
id,
|
||||||
|
|
@ -181,9 +175,7 @@ function resolveNodeType(
|
||||||
_layerCount: number,
|
_layerCount: number,
|
||||||
rng: RNG,
|
rng: RNG,
|
||||||
preGeneratedTypes?: MapNodeType[],
|
preGeneratedTypes?: MapNodeType[],
|
||||||
nodeIndex?: number,
|
nodeIndex?: number
|
||||||
settlementTypes?: MapNodeType[],
|
|
||||||
settlementIndex?: number
|
|
||||||
): MapNodeType {
|
): MapNodeType {
|
||||||
switch (layerType) {
|
switch (layerType) {
|
||||||
case 'start':
|
case 'start':
|
||||||
|
|
@ -197,11 +189,8 @@ function resolveNodeType(
|
||||||
}
|
}
|
||||||
return pickWildNodeType(rng);
|
return pickWildNodeType(rng);
|
||||||
case MapLayerType.Settlement:
|
case MapLayerType.Settlement:
|
||||||
// Use pre-generated settlement types if available
|
// This will be overridden by assignSettlementTypes
|
||||||
if (settlementTypes && settlementIndex !== undefined) {
|
return MapNodeType.Camp; // placeholder
|
||||||
return settlementTypes[settlementIndex];
|
|
||||||
}
|
|
||||||
return MapNodeType.Camp; // fallback
|
|
||||||
default:
|
default:
|
||||||
return MapNodeType.Minion;
|
return MapNodeType.Minion;
|
||||||
}
|
}
|
||||||
|
|
@ -306,22 +295,9 @@ function generateOptimalWildPair(
|
||||||
return [bestLayer1, bestLayer2];
|
return [bestLayer1, bestLayer2];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates settlement node types ensuring at least 1 of each: camp, shop, curio.
|
|
||||||
* The 4th node is randomly chosen from the three.
|
|
||||||
* Returns shuffled array of 4 node types.
|
|
||||||
*/
|
|
||||||
function generateSettlementTypes(rng: RNG): MapNodeType[] {
|
|
||||||
const requiredTypes = [MapNodeType.Camp, MapNodeType.Shop, MapNodeType.Curio];
|
|
||||||
const randomType = requiredTypes[rng.nextInt(3)];
|
|
||||||
const types = [...requiredTypes, randomType];
|
|
||||||
return fisherYatesShuffle(types, rng);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assigns settlement node types ensuring at least 1 of each: camp, shop, curio.
|
* Assigns settlement node types ensuring at least 1 of each: camp, shop, curio.
|
||||||
* The 4th node is randomly chosen from the three.
|
* The 4th node is randomly chosen from the three.
|
||||||
* @deprecated Use generateSettlementTypes() during node creation instead.
|
|
||||||
*/
|
*/
|
||||||
function assignSettlementTypes(nodeIds: string[], nodes: MapNode[], rng: RNG): void {
|
function assignSettlementTypes(nodeIds: string[], nodes: MapNode[], rng: RNG): void {
|
||||||
// Shuffle node order to randomize which position gets which type
|
// Shuffle node order to randomize which position gets which type
|
||||||
|
|
@ -347,8 +323,10 @@ function generateLayerEdges(
|
||||||
nodes: Map<string, MapNode>,
|
nodes: Map<string, MapNode>,
|
||||||
rng: RNG
|
rng: RNG
|
||||||
): void {
|
): void {
|
||||||
// Settlement types are now pre-generated during node creation
|
// Assign settlement types when creating settlement layer
|
||||||
// No need to assign them here anymore
|
if (targetLayer.layerType === MapLayerType.Settlement) {
|
||||||
|
assignSettlementTypes(targetLayer.nodeIds, targetLayer.nodes, rng);
|
||||||
|
}
|
||||||
|
|
||||||
const sourceType = sourceLayer.layerType;
|
const sourceType = sourceLayer.layerType;
|
||||||
const targetType = targetLayer.layerType;
|
const targetType = targetLayer.layerType;
|
||||||
|
|
|
||||||
|
|
@ -1,207 +0,0 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
|
||||||
generateDeckFromInventory,
|
|
||||||
createStatusCard,
|
|
||||||
createDeckRegions,
|
|
||||||
createPlayerDeck,
|
|
||||||
generateCardId,
|
|
||||||
} from '@/samples/slay-the-spire-like/deck/factory';
|
|
||||||
import { createGridInventory, placeItem } from '@/samples/slay-the-spire-like/grid-inventory';
|
|
||||||
import type { GridInventory, InventoryItem } from '@/samples/slay-the-spire-like/grid-inventory';
|
|
||||||
import type { GameItemMeta } from '@/samples/slay-the-spire-like/progress/types';
|
|
||||||
import { IDENTITY_TRANSFORM } from '@/samples/slay-the-spire-like/utils/shape-collision';
|
|
||||||
import { parseShapeString } from '@/samples/slay-the-spire-like/utils/parse-shape';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: create a minimal GameItemMeta for testing.
|
|
||||||
*/
|
|
||||||
function createTestMeta(name: string, desc: string, shapeStr: string): GameItemMeta {
|
|
||||||
const shape = parseShapeString(shapeStr);
|
|
||||||
return {
|
|
||||||
itemData: {
|
|
||||||
type: 'weapon',
|
|
||||||
name,
|
|
||||||
shape: shapeStr,
|
|
||||||
costType: 'energy',
|
|
||||||
costCount: 1,
|
|
||||||
targetType: 'single',
|
|
||||||
price: 10,
|
|
||||||
desc,
|
|
||||||
},
|
|
||||||
shape,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: create a test inventory with some items.
|
|
||||||
*/
|
|
||||||
function createTestInventory(): GridInventory<GameItemMeta> {
|
|
||||||
const inv = createGridInventory<GameItemMeta>(6, 4);
|
|
||||||
|
|
||||||
// Item "短刀" with shape "oe" (2 cells)
|
|
||||||
const meta1 = createTestMeta('短刀', '【攻击3】【攻击3】', 'oe');
|
|
||||||
const item1: InventoryItem<GameItemMeta> = {
|
|
||||||
id: 'dagger-1',
|
|
||||||
shape: meta1.shape,
|
|
||||||
transform: { ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } },
|
|
||||||
meta: meta1,
|
|
||||||
};
|
|
||||||
placeItem(inv, item1);
|
|
||||||
|
|
||||||
// Item "盾" with shape "oesw" (4 cells)
|
|
||||||
const meta2 = createTestMeta('盾', '【防御3】', 'oesw');
|
|
||||||
const item2: InventoryItem<GameItemMeta> = {
|
|
||||||
id: 'shield-1',
|
|
||||||
shape: meta2.shape,
|
|
||||||
transform: { ...IDENTITY_TRANSFORM, offset: { x: 3, y: 0 } },
|
|
||||||
meta: meta2,
|
|
||||||
};
|
|
||||||
placeItem(inv, item2);
|
|
||||||
|
|
||||||
return inv;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('deck/factory', () => {
|
|
||||||
describe('generateCardId', () => {
|
|
||||||
it('should generate deterministic unique IDs', () => {
|
|
||||||
expect(generateCardId('item-1', 0)).toBe('card-item-1-0');
|
|
||||||
expect(generateCardId('item-1', 1)).toBe('card-item-1-1');
|
|
||||||
expect(generateCardId('item-2', 0)).toBe('card-item-2-0');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('createStatusCard', () => {
|
|
||||||
it('should create a card with null sourceItemId and itemData', () => {
|
|
||||||
const card = createStatusCard('wound-1', '伤口', '无法被弃牌');
|
|
||||||
|
|
||||||
expect(card.id).toBe('wound-1');
|
|
||||||
expect(card.sourceItemId).toBeNull();
|
|
||||||
expect(card.itemData).toBeNull();
|
|
||||||
expect(card.cellKey).toBeNull();
|
|
||||||
expect(card.displayName).toBe('伤口');
|
|
||||||
expect(card.description).toBe('无法被弃牌');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have empty region and position', () => {
|
|
||||||
const card = createStatusCard('stun-1', '眩晕', '跳过出牌阶段');
|
|
||||||
|
|
||||||
expect(card.regionId).toBe('');
|
|
||||||
expect(card.position).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('generateDeckFromInventory', () => {
|
|
||||||
it('should generate correct number of cards based on shape cell counts', () => {
|
|
||||||
const inv = createTestInventory();
|
|
||||||
|
|
||||||
// "短刀" has 2 cells, "盾" has 4 cells = 6 total
|
|
||||||
const deck = generateDeckFromInventory(inv);
|
|
||||||
|
|
||||||
expect(Object.keys(deck.cards).length).toBe(6);
|
|
||||||
expect(deck.drawPile.length).toBe(6);
|
|
||||||
expect(deck.hand).toEqual([]);
|
|
||||||
expect(deck.discardPile).toEqual([]);
|
|
||||||
expect(deck.exhaustPile).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should link cards to their source items', () => {
|
|
||||||
const inv = createTestInventory();
|
|
||||||
const deck = generateDeckFromInventory(inv);
|
|
||||||
|
|
||||||
const daggerCards = Object.values(deck.cards).filter(
|
|
||||||
c => c.sourceItemId === 'dagger-1'
|
|
||||||
);
|
|
||||||
const shieldCards = Object.values(deck.cards).filter(
|
|
||||||
c => c.sourceItemId === 'shield-1'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(daggerCards.length).toBe(2);
|
|
||||||
expect(shieldCards.length).toBe(4);
|
|
||||||
|
|
||||||
// Verify item data
|
|
||||||
expect(daggerCards[0].itemData?.name).toBe('短刀');
|
|
||||||
expect(shieldCards[0].itemData?.name).toBe('盾');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set displayName and description from item data', () => {
|
|
||||||
const inv = createTestInventory();
|
|
||||||
const deck = generateDeckFromInventory(inv);
|
|
||||||
|
|
||||||
for (const card of Object.values(deck.cards)) {
|
|
||||||
expect(card.displayName).toBeTruthy();
|
|
||||||
expect(card.description).toBeTruthy();
|
|
||||||
}
|
|
||||||
|
|
||||||
const daggerCard = Object.values(deck.cards).find(
|
|
||||||
c => c.itemData?.name === '短刀'
|
|
||||||
);
|
|
||||||
expect(daggerCard?.displayName).toBe('短刀');
|
|
||||||
expect(daggerCard?.description).toBe('【攻击3】【攻击3】');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should assign unique cell keys to each card from same item', () => {
|
|
||||||
const inv = createTestInventory();
|
|
||||||
const deck = generateDeckFromInventory(inv);
|
|
||||||
|
|
||||||
const daggerCards = Object.values(deck.cards).filter(
|
|
||||||
c => c.sourceItemId === 'dagger-1'
|
|
||||||
);
|
|
||||||
|
|
||||||
const cellKeys = daggerCards.map(c => c.cellKey);
|
|
||||||
const uniqueKeys = new Set(cellKeys);
|
|
||||||
expect(uniqueKeys.size).toBe(cellKeys.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle empty inventory', () => {
|
|
||||||
const inv = createGridInventory<GameItemMeta>(6, 4);
|
|
||||||
const deck = generateDeckFromInventory(inv);
|
|
||||||
|
|
||||||
expect(Object.keys(deck.cards).length).toBe(0);
|
|
||||||
expect(deck.drawPile).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should place all cards in draw pile initially', () => {
|
|
||||||
const inv = createTestInventory();
|
|
||||||
const deck = generateDeckFromInventory(inv);
|
|
||||||
|
|
||||||
for (const cardId of deck.drawPile) {
|
|
||||||
expect(deck.cards[cardId]).toBeDefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
// All cards are in draw pile
|
|
||||||
expect(new Set(deck.drawPile).size).toBe(Object.keys(deck.cards).length);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('createDeckRegions', () => {
|
|
||||||
it('should create regions for all deck zones', () => {
|
|
||||||
const regions = createDeckRegions();
|
|
||||||
|
|
||||||
expect(regions.drawPile.id).toBe('drawPile');
|
|
||||||
expect(regions.hand.id).toBe('hand');
|
|
||||||
expect(regions.discardPile.id).toBe('discardPile');
|
|
||||||
expect(regions.exhaustPile.id).toBe('exhaustPile');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have empty childIds initially', () => {
|
|
||||||
const regions = createDeckRegions();
|
|
||||||
|
|
||||||
expect(regions.drawPile.childIds).toEqual([]);
|
|
||||||
expect(regions.hand.childIds).toEqual([]);
|
|
||||||
expect(regions.discardPile.childIds).toEqual([]);
|
|
||||||
expect(regions.exhaustPile.childIds).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('createPlayerDeck', () => {
|
|
||||||
it('should create an empty deck structure', () => {
|
|
||||||
const deck = createPlayerDeck();
|
|
||||||
|
|
||||||
expect(deck.cards).toEqual({});
|
|
||||||
expect(deck.drawPile).toEqual([]);
|
|
||||||
expect(deck.hand).toEqual([]);
|
|
||||||
expect(deck.discardPile).toEqual([]);
|
|
||||||
expect(deck.exhaustPile).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -287,36 +287,19 @@ describe('generatePointCrawlMap', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should assign encounters to all non-Start/End nodes', () => {
|
it('should assign encounters to nodes', () => {
|
||||||
const map = generatePointCrawlMap(456);
|
const map = generatePointCrawlMap(456);
|
||||||
|
|
||||||
|
let nodesWithEncounter = 0;
|
||||||
for (const node of map.nodes.values()) {
|
for (const node of map.nodes.values()) {
|
||||||
if (node.type === MapNodeType.Start || node.type === MapNodeType.End) {
|
if (node.encounter) {
|
||||||
// Start and End nodes should not have encounters
|
nodesWithEncounter++;
|
||||||
expect(node.encounter).toBeUndefined();
|
expect(node.encounter.name).toBeTruthy();
|
||||||
} else {
|
expect(node.encounter.description).toBeTruthy();
|
||||||
// All other nodes (minion/elite/event/camp/shop/curio) must have encounters
|
|
||||||
expect(node.encounter, `Node ${node.id} (${node.type}) should have encounter data`).toBeDefined();
|
|
||||||
expect(node.encounter!.name).toBeTruthy();
|
|
||||||
expect(node.encounter!.description).toBeTruthy();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
it('should assign encounters to all nodes across multiple seeds', () => {
|
expect(nodesWithEncounter).toBeGreaterThan(0);
|
||||||
// Test multiple seeds to ensure no random failure
|
|
||||||
for (let seed = 0; seed < 20; seed++) {
|
|
||||||
const map = generatePointCrawlMap(seed);
|
|
||||||
|
|
||||||
for (const node of map.nodes.values()) {
|
|
||||||
if (node.type === MapNodeType.Start || node.type === MapNodeType.End) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
expect(node.encounter, `Seed ${seed}: Node ${node.id} (${node.type}) missing encounter`).toBeDefined();
|
|
||||||
expect(node.encounter!.name).toBeTruthy();
|
|
||||||
expect(node.encounter!.description).toBeTruthy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should minimize same-layer repetitions in wild layer pairs', () => {
|
it('should minimize same-layer repetitions in wild layer pairs', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue