refactor: type rewrite
This commit is contained in:
parent
a469b4024a
commit
1c238aec3a
|
|
@ -1,102 +0,0 @@
|
||||||
# slay-the-spire-like
|
|
||||||
|
|
||||||
A Slay the Spire + Backpack Heroes hybrid roguelike sample. Players explore a point-crawl map, manage a tetris-style grid inventory, and fight enemies using cards generated from their equipment.
|
|
||||||
|
|
||||||
## Game Design Docs
|
|
||||||
|
|
||||||
Design docs are in the markdown files at this level:
|
|
||||||
- `01-overview.md` — core game concept, zones, encounter structure, combat rules, buff/debuff system
|
|
||||||
- `02-fighter.md` — Fighter class items (weapons, armor, tools, consumables, relics)
|
|
||||||
- `03-desert.md` — Desert zone enemies (minions, elites, boss)
|
|
||||||
- `data/rules.md` — combat state machine, turn order, effect timing rules
|
|
||||||
|
|
||||||
## Module Structure
|
|
||||||
|
|
||||||
This is **not** a `GameModule` yet — there is no `createInitialState`/`start`/`registry` wired up to `createGameHost`. The code is a library of subsystems that can be composed into a game module.
|
|
||||||
|
|
||||||
### Subsystems
|
|
||||||
|
|
||||||
| Directory | Purpose | Key exports |
|
|
||||||
|-----------|---------|-------------|
|
|
||||||
| `progress/` | Run state, player HP/gold, inventory management, map progression | `createRunState`, `moveToNode`, `resolveEncounter`, `damagePlayer`, `healPlayer`, `addItemFromCsv`, `removeItem`, `getReachableChildren` |
|
|
||||||
| `map/` | Point-crawl map generation and traversal | `generatePointCrawlMap`, `getNode`, `getChildren`, `getParents`, `hasPath`, `findAllPaths` |
|
|
||||||
| `grid-inventory/` | Tetris-style grid placement (place, move, rotate, flip items) | `createGridInventory`, `placeItem`, `removeItem`, `moveItem`, `rotateItem`, `flipItem`, `validatePlacement`, `getAdjacentItems` |
|
|
||||||
| `deck/` | Card/deck system (draw pile, hand, discard, exhaust) | `generateDeckFromInventory`, `createStatusCard`, `createDeckRegions`, `createPlayerDeck` |
|
|
||||||
| `data/` | CSV game data loaded via `inline-schema/csv-loader`. `.d.ts` files are auto-generated by the csv-loader plugin — do not edit by hand. | `heroItemFighter1Data`, `encounterDesertData`, `enemyDesertData`, `enemyIntentDesertData`, `effectDesertData`, `statusCardDesertData` |
|
|
||||||
| `dialogue/` | Yarn Spinner dialogue files (placeholder). Loaded via `yarn-spinner-loader`, a local peer dependency at `../yarn-spinner-loader` (like `inline-schema`, it can be changed and published if needed). | `encounters` yarnproject |
|
|
||||||
| `utils/` | Shape parsing and collision math | `parseShapeString`, `checkCollision`, `checkBounds`, `transformShape`, `rotateTransform`, `flipXTransform`, `flipYTransform` |
|
|
||||||
|
|
||||||
### Data flow
|
|
||||||
|
|
||||||
```
|
|
||||||
CSV files (data/)
|
|
||||||
→ inline-schema/csv-loader → typed JS objects (e.g. HeroItemFighter1)
|
|
||||||
→ parseShapeString() converts shape strings → ParsedShape
|
|
||||||
→ GridInventory<GameItemMeta> holds placed items
|
|
||||||
→ generateDeckFromInventory() generates cards per occupied cell
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key types
|
|
||||||
|
|
||||||
- **`RunState`** — top-level state: seed, map, player, inventory, currentNodeId, encounter state, resolved set. Designed for `MutableSignal.produce()` mutation.
|
|
||||||
- **`GridInventory<TMeta>`** — `items: Map<string, InventoryItem<TMeta>>` + `occupiedCells: Set<CellKey>` for O(1) collision. Mutated directly inside `.produce()`.
|
|
||||||
- **`InventoryItem<TMeta>`** — id, shape (ParsedShape), transform (Transform2D), meta. Shape + transform determines which cells are occupied.
|
|
||||||
- **`GameCard`** — a `Part<GameCardMeta>` bridging inventory items to the deck system. `sourceItemId` links back to the inventory item; `null` for status cards.
|
|
||||||
- **`PointCrawlMap`** — layered DAG: 10 layers (start → wild×2 → settlement → wild×2 → settlement → wild×2 → end). Wild = 3 nodes, Settlement = 4 nodes.
|
|
||||||
- **`MapNode`** — id, type (MapNodeType enum), childIds, optional encounter data from CSV.
|
|
||||||
|
|
||||||
### Map generation
|
|
||||||
|
|
||||||
`generatePointCrawlMap(seed?)` produces a deterministic map:
|
|
||||||
- 10 layers: Start → Wild(3) → Wild(3) → Settlement(4) → Wild(3) → Wild(3) → Settlement(4) → Wild(3) → Wild(3) → End
|
|
||||||
- Settlement layers guarantee ≥1 camp, ≥1 shop, ≥1 curio (4th slot random)
|
|
||||||
- Wild pair types are optimized to minimize same-type repetition
|
|
||||||
- Edge patterns avoid crossings: Start→all wild, Wild→Wild 1:1, Wild↔Settlement 3:4 or 4:3, Wild→all End
|
|
||||||
|
|
||||||
### Shape system
|
|
||||||
|
|
||||||
Items have shapes defined as movement strings parsed by `parseShapeString`:
|
|
||||||
- `o` = origin cell, `n/s/e/w` = move + fill, `r` = return to previous position
|
|
||||||
- Example: `"oesw"` = 2×2 block (origin, east, south, west = full square)
|
|
||||||
- Example: `"oe"` = 1×2 horizontal
|
|
||||||
- Example: `"onrersrw"` = cross/X shape
|
|
||||||
|
|
||||||
Shapes are positioned via `Transform2D` (offset, rotation, flipX, flipY) and validated against the 6×4 grid.
|
|
||||||
|
|
||||||
### Grid inventory
|
|
||||||
|
|
||||||
All mutation functions (`placeItem`, `removeItem`, `moveItem`, `rotateItem`, `flipItem`) mutate the `GridInventory` **directly** — they must be called inside `produce()` callbacks. `validatePlacement` checks bounds + collisions before placement.
|
|
||||||
|
|
||||||
### Card generation
|
|
||||||
|
|
||||||
`generateDeckFromInventory(inventory)` creates one card per occupied cell in each item's shape. Cards carry `GameCardMeta` linking back to the source item and cell position. Status cards (wound, venom, etc.) are created separately via `createStatusCard`.
|
|
||||||
|
|
||||||
## CSV data format
|
|
||||||
|
|
||||||
All CSVs use `inline-schema` typed headers. The first row is a comment header, the second row is the schema row with types and references:
|
|
||||||
- `'energy'|'uses'` — union type
|
|
||||||
- `@enemyDesert` — foreign key reference to another CSV
|
|
||||||
- `[effect: @effectDesert; number][]` — array of structured references
|
|
||||||
|
|
||||||
### heroItemFighter1.csv columns
|
|
||||||
|
|
||||||
| Column | Type | Notes |
|
|
||||||
|--------|------|-------|
|
|
||||||
| type | `'weapon'|'armor'|'consumable'|'tool'` | |
|
|
||||||
| name | string | Display name (Chinese) |
|
|
||||||
| shape | string | Movement string for `parseShapeString` |
|
|
||||||
| costType | `'energy'|'uses'` | Energy = per-turn cost; Uses = limited uses |
|
|
||||||
| costCount | int | Cost amount |
|
|
||||||
| targetType | `'single'|'none'` | |
|
|
||||||
| price | int | Shop price |
|
|
||||||
| desc | string | Ability description (Chinese) |
|
|
||||||
| effects | `['self'|'target'|'all'|'random'; @effectDesert; number][]` | Effect references |
|
|
||||||
|
|
||||||
## Conventions
|
|
||||||
|
|
||||||
- Chinese is used for all user-facing strings (item names, error messages, effect descriptions)
|
|
||||||
- Discriminated union result types: `{ success: true } | { success: false, reason: string }`
|
|
||||||
- Mutation functions mutate state directly (inside `produce()`); validation is separate
|
|
||||||
- `Map` and `Set` are used in `GridInventory` and `PointCrawlMap` (not plain objects) — requires careful handling with `mutative` since it drafts Maps/Sets differently than plain objects
|
|
||||||
- Starter items defined in `progress/index.ts`: `['治疗药剂', '绷带', '水袋', '短刀', '剑']`
|
|
||||||
- Default player stats: 50 HP, 50 gold, 6×4 inventory
|
|
||||||
|
|
@ -41,4 +41,9 @@ export function onPlayerItemEffectUpkeep(entity: PlayerEntity){
|
||||||
addItemEffect(entity, itemKey, effect.data, -effect.stacks);
|
addItemEffect(entity, itemKey, effect.data, -effect.stacks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
export function onDraw(entity: PlayerEntity, cardId:string ){}
|
||||||
|
// TODO
|
||||||
|
export function onDiscard(entity: PlayerEntity, cardId: string){}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import {createMiddlewareChain} from "../utils/middleware";
|
||||||
import {CombatGameContext} from "./types";
|
import {CombatGameContext} from "./types";
|
||||||
import {getAliveEnemies} from "@/samples/slay-the-spire-like/system/combat/utils";
|
import {getAliveEnemies} from "@/samples/slay-the-spire-like/system/combat/utils";
|
||||||
import {
|
import {
|
||||||
|
onDiscard, onDraw,
|
||||||
onEntityEffectUpkeep,
|
onEntityEffectUpkeep,
|
||||||
onPlayerItemEffectUpkeep
|
onPlayerItemEffectUpkeep
|
||||||
} from "@/samples/slay-the-spire-like/system/combat/effects";
|
} from "@/samples/slay-the-spire-like/system/combat/effects";
|
||||||
|
|
@ -12,7 +13,7 @@ type TriggerTypes = {
|
||||||
onTurnStart: { entityKey: "player" | string, },
|
onTurnStart: { entityKey: "player" | string, },
|
||||||
onTurnEnd: { entityKey: "player" | string, },
|
onTurnEnd: { entityKey: "player" | string, },
|
||||||
onShuffle: { entityKey: "player" | string, },
|
onShuffle: { entityKey: "player" | string, },
|
||||||
onCardPlayed: { cardId: string, },
|
onCardPlayed: { cardId: string, targetId?: string },
|
||||||
onCardDiscarded: { cardId: string, },
|
onCardDiscarded: { cardId: string, },
|
||||||
onCardDrawn: { cardId: string, },
|
onCardDrawn: { cardId: string, },
|
||||||
onEffectApplied: { effectId: string, entityKey: "player" | string, stacks: number, },
|
onEffectApplied: { effectId: string, entityKey: "player" | string, stacks: number, },
|
||||||
|
|
@ -37,6 +38,8 @@ export function createStartWith(build: (triggers: Triggers) => void){
|
||||||
return async function(game: CombatGameContext){
|
return async function(game: CombatGameContext){
|
||||||
await triggers.onCombatStart.execute(game,{});
|
await triggers.onCombatStart.execute(game,{});
|
||||||
|
|
||||||
|
// TODO at the end of a damage effect, if win/loss is achieved, break the loop with a throw
|
||||||
|
// catch the throw and return the result here
|
||||||
while(true){
|
while(true){
|
||||||
await triggers.onTurnStart.execute(game,{entityKey: "player"});
|
await triggers.onTurnStart.execute(game,{entityKey: "player"});
|
||||||
await game.produceAsync(draft => {
|
await game.produceAsync(draft => {
|
||||||
|
|
@ -46,11 +49,23 @@ export function createStartWith(build: (triggers: Triggers) => void){
|
||||||
while(true){
|
while(true){
|
||||||
const action = await promptMainAction(game);
|
const action = await promptMainAction(game);
|
||||||
if(action.action === "end-turn") break;
|
if(action.action === "end-turn") break;
|
||||||
//TODO resolve action here
|
if(action.action === "play"){
|
||||||
|
await game.produceAsync(draft => onDiscard(draft.player, action.cardId));
|
||||||
|
await triggers.onCardPlayed.execute(game, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(const cardId of [...game.value.player.deck.hand]){
|
||||||
|
await game.produceAsync(draft => onDiscard(draft.player, cardId));
|
||||||
|
await triggers.onCardDiscarded.execute(game,{cardId});
|
||||||
}
|
}
|
||||||
// TODO discard cards here
|
|
||||||
await triggers.onTurnEnd.execute(game,{entityKey: "player"});
|
await triggers.onTurnEnd.execute(game,{entityKey: "player"});
|
||||||
// TODO recover energy, draw new cards here
|
await game.produceAsync(draft => draft.player.energy = draft.player.maxEnergy);
|
||||||
|
for(let i = 0; i < 5; i++){
|
||||||
|
const cardId = game.value.player.deck.drawPile[0]; // TODO: should this be drawPile[-1] ?
|
||||||
|
if(!cardId) break;
|
||||||
|
await game.produceAsync(draft => onDraw(draft.player, cardId));
|
||||||
|
await triggers.onCardDrawn.execute(game,{cardId});
|
||||||
|
}
|
||||||
|
|
||||||
for(const enemy of getAliveEnemies(game.value)){
|
for(const enemy of getAliveEnemies(game.value)){
|
||||||
await triggers.onTurnStart.execute(game,{entityKey: enemy.id});
|
await triggers.onTurnStart.execute(game,{entityKey: enemy.id});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import type { PlayerDeck } from "../deck/types";
|
import type { PlayerDeck } from "../deck/types";
|
||||||
import {EnemyData, IntentData} from "@/samples/slay-the-spire-like/system/types";
|
import {EnemyData, IntentData} from "@/samples/slay-the-spire-like/system/types";
|
||||||
import {EffectData} from "@/samples/slay-the-spire-like/system/types";
|
import {EffectData} from "@/samples/slay-the-spire-like/system/types";
|
||||||
|
import {GridInventory} from "@/samples/slay-the-spire-like/system/grid-inventory";
|
||||||
|
import {GameItemMeta} from "@/samples/slay-the-spire-like/system/progress";
|
||||||
|
|
||||||
export type EffectTable = Record<string, {data: EffectData, stacks: number}>;
|
export type EffectTable = Record<string, {data: EffectData, stacks: number}>;
|
||||||
|
|
||||||
|
|
@ -39,6 +41,7 @@ export type LootEntry = {
|
||||||
export type CombatState = {
|
export type CombatState = {
|
||||||
enemies: EnemyEntity[];
|
enemies: EnemyEntity[];
|
||||||
player: PlayerEntity;
|
player: PlayerEntity;
|
||||||
|
inventory: GridInventory<GameItemMeta>;
|
||||||
|
|
||||||
phase: CombatPhase;
|
phase: CombatPhase;
|
||||||
turnNumber: number;
|
turnNumber: number;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {CombatState} from "@/samples/slay-the-spire-like/system";
|
import {CombatState} from "./types";
|
||||||
|
|
||||||
export function* getAliveEnemies(state: CombatState) {
|
export function* getAliveEnemies(state: CombatState) {
|
||||||
for (let enemy of state.enemies) {
|
for (let enemy of state.enemies) {
|
||||||
|
|
|
||||||
|
|
@ -1,188 +1,66 @@
|
||||||
import type { CellKey, GridInventory, InventoryItem } from '../grid-inventory/types';
|
import {moveToRegion } from '@/core/region';
|
||||||
|
import { createRegion } from '@/core/region';
|
||||||
|
import type { GridInventory } from '../grid-inventory/types';
|
||||||
import type { GameItemMeta } from '../progress/types';
|
import type { GameItemMeta } from '../progress/types';
|
||||||
import { createRegion, createRegionAxis } from '@/core/region';
|
import type { CardData } from '../types';
|
||||||
import type { GameCard, GameCardMeta, PlayerDeck, DeckRegions } from './types';
|
import type {DeckRegions, GameCard, PlayerDeck} from './types';
|
||||||
import { cardDesertData } from '../data';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a unique card ID for a cell within an item.
|
|
||||||
*/
|
|
||||||
function generateCardId(itemId: string, cellIndex: number): string {
|
function generateCardId(itemId: string, cellIndex: number): string {
|
||||||
return `card-${itemId}-${cellIndex}`;
|
return `card-${itemId}-${cellIndex}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function createCard( itemId: string, cardData: CardData, cellIndex: number): GameCard {
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
function createItemCard(
|
|
||||||
itemId: string,
|
|
||||||
itemData: GameItemMeta['itemData'],
|
|
||||||
cellKey: CellKey,
|
|
||||||
cellIndex: number
|
|
||||||
): GameCard {
|
|
||||||
const cardId = generateCardId(itemId, cellIndex);
|
|
||||||
const cardData = cardDesertData.find(c => c.id === itemData.card.id);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: cardId,
|
id: generateCardId(itemId, cellIndex),
|
||||||
regionId: '',
|
regionId: '',
|
||||||
position: [],
|
position: [],
|
||||||
sourceItemId: itemId,
|
itemId,
|
||||||
itemData: cardData ?? null,
|
cardData
|
||||||
cellKey,
|
|
||||||
displayName: cardData?.name ?? itemData.name,
|
|
||||||
description: cardData?.desc ?? '',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function createDeckRegions(): DeckRegions {
|
||||||
* Creates a status card that does not correspond to any inventory item.
|
|
||||||
* Status cards represent temporary effects like wounds, stuns, etc.
|
|
||||||
*/
|
|
||||||
function createStatusCard(
|
|
||||||
id: string,
|
|
||||||
displayName: string,
|
|
||||||
description: string
|
|
||||||
): GameCard {
|
|
||||||
return {
|
return {
|
||||||
id,
|
drawPile: createRegion('drawPile', []),
|
||||||
regionId: '',
|
hand: createRegion('hand', []),
|
||||||
position: [],
|
discardPile: createRegion('discardPile', []),
|
||||||
sourceItemId: null,
|
exhaustPile: createRegion('exhaustPile', []),
|
||||||
itemData: null,
|
|
||||||
cellKey: null,
|
|
||||||
displayName,
|
|
||||||
description,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a complete player deck from the current inventory state.
|
|
||||||
*/
|
|
||||||
function generateDeckFromInventory(inventory: GridInventory<GameItemMeta>): PlayerDeck {
|
function generateDeckFromInventory(inventory: GridInventory<GameItemMeta>): PlayerDeck {
|
||||||
const cards: Record<string, GameCard> = {};
|
const cards: Record<string, GameCard> = {};
|
||||||
const drawPile: string[] = [];
|
const regions = createDeckRegions();
|
||||||
|
|
||||||
for (const item of inventory.items.values()) {
|
for (const item of inventory.items.values()) {
|
||||||
const itemData = item.meta?.itemData;
|
const itemData = item.meta?.itemData;
|
||||||
if (!itemData) continue;
|
if (!itemData) continue;
|
||||||
|
|
||||||
// Generate one card per occupied cell in the item's shape
|
const count = item.shape.count;
|
||||||
const cellCount = item.shape.count;
|
for (let i = 0; i < count; i++) {
|
||||||
const cells = getItemCells(item);
|
const card = createCard(item.id, itemData.card, i);
|
||||||
|
|
||||||
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;
|
cards[card.id] = card;
|
||||||
drawPile.push(card.id);
|
moveToRegion(card, null, regions.drawPile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cards,
|
cards,
|
||||||
drawPile,
|
regions
|
||||||
hand: [],
|
|
||||||
discardPile: [],
|
|
||||||
exhaustPile: [],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates region definitions for deck management.
|
|
||||||
*/
|
|
||||||
function createDeckRegions(): DeckRegions {
|
|
||||||
return {
|
|
||||||
drawPile: createRegion('drawPile', [
|
|
||||||
createRegionAxis('index', 0, 0),
|
|
||||||
]),
|
|
||||||
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 {
|
function createPlayerDeck(): PlayerDeck {
|
||||||
return {
|
return {
|
||||||
cards: {},
|
cards: {},
|
||||||
drawPile: [],
|
regions: createDeckRegions(),
|
||||||
hand: [],
|
|
||||||
discardPile: [],
|
|
||||||
exhaustPile: [],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
generateDeckFromInventory,
|
generateDeckFromInventory,
|
||||||
createStatusCard,
|
createCard,
|
||||||
createDeckRegions,
|
|
||||||
createPlayerDeck,
|
createPlayerDeck,
|
||||||
|
createDeckRegions,
|
||||||
generateCardId,
|
generateCardId,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
export type { GameCard, GameCardMeta, PlayerDeck, DeckRegions } from './types';
|
export type { GameCard, GameCardMeta, PlayerDeck, DeckRegions } from './types';
|
||||||
export {
|
export {
|
||||||
generateDeckFromInventory,
|
generateDeckFromInventory,
|
||||||
createStatusCard,
|
createCard,
|
||||||
createDeckRegions,
|
|
||||||
createPlayerDeck,
|
createPlayerDeck,
|
||||||
generateCardId,
|
generateCardId,
|
||||||
} from './factory';
|
} from './factory';
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,14 @@
|
||||||
import type { Part } from '@/core/part';
|
import type { Part } from '@/core/part';
|
||||||
import type { Region } from '@/core/region';
|
import {CardData} from "@/samples/slay-the-spire-like/system/types";
|
||||||
import type { HeroItemFighter1 } from '../data/heroItemFighter1.csv';
|
import {Region} from "@/core/region";
|
||||||
import type { CardDesert } from '../data/cardDesert.csv';
|
|
||||||
import type { CellKey } from '../grid-inventory/types';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata for a game card.
|
* Metadata for a game card.
|
||||||
* Bridges inventory item data with the card system.
|
* Bridges inventory item data with the card system.
|
||||||
*/
|
*/
|
||||||
export interface GameCardMeta {
|
export interface GameCardMeta {
|
||||||
/**
|
cardData: CardData;
|
||||||
* Source item instance ID that this card was generated from.
|
itemId: string;
|
||||||
* `null` for status cards (e.g. wound, stun) that don't correspond to an inventory item.
|
|
||||||
*/
|
|
||||||
sourceItemId: string | null;
|
|
||||||
/**
|
|
||||||
* Original card data from cardDesert.csv. `null` for status cards not in the CSV.
|
|
||||||
*/
|
|
||||||
itemData: CardDesert | 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -49,26 +23,17 @@ export type GameCard = Part<GameCardMeta>;
|
||||||
export interface PlayerDeck {
|
export interface PlayerDeck {
|
||||||
/** All cards indexed by ID */
|
/** All cards indexed by ID */
|
||||||
cards: Record<string, GameCard>;
|
cards: Record<string, GameCard>;
|
||||||
/** Card IDs in the draw pile */
|
|
||||||
drawPile: string[];
|
regions: DeckRegions;
|
||||||
/** 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[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export interface DeckRegions{
|
||||||
* Region structure for deck management.
|
/** Card IDs in the draw pile */
|
||||||
*/
|
|
||||||
export interface DeckRegions {
|
|
||||||
/** Draw pile region */
|
|
||||||
drawPile: Region;
|
drawPile: Region;
|
||||||
/** Hand region */
|
/** Card IDs in the player's hand */
|
||||||
hand: Region;
|
hand: Region;
|
||||||
/** Discard pile region */
|
/** Card IDs in the discard pile */
|
||||||
discardPile: Region;
|
discardPile: Region;
|
||||||
/** Exhaust pile region */
|
/** Card IDs in the exhaust pile (removed from combat) */
|
||||||
exhaustPile: Region;
|
exhaustPile: Region;
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { PointCrawlMap } from '../map/types';
|
import type { PointCrawlMap } from '../map/types';
|
||||||
import type { GridInventory, InventoryItem } from '../grid-inventory/types';
|
import type { GridInventory, InventoryItem } from '../grid-inventory/types';
|
||||||
import type { ParsedShape } from '../utils/parse-shape';
|
import type { ParsedShape } from '../utils/parse-shape';
|
||||||
import type { HeroItemFighter1 } from '../data/heroItemFighter1.csv';
|
import {ItemData} from "@/samples/slay-the-spire-like/system/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Result of an encounter (combat, event, etc.).
|
* Result of an encounter (combat, event, etc.).
|
||||||
|
|
@ -35,9 +35,11 @@ export interface EncounterState {
|
||||||
*/
|
*/
|
||||||
export interface GameItemMeta {
|
export interface GameItemMeta {
|
||||||
/** Original CSV item data */
|
/** Original CSV item data */
|
||||||
itemData: HeroItemFighter1;
|
itemData: ItemData;
|
||||||
/** Parsed shape for grid placement */
|
/** Parsed shape for grid placement */
|
||||||
shape: ParsedShape;
|
shape: ParsedShape;
|
||||||
|
/** Consumed uses, if card cost type is uses**/
|
||||||
|
depletion?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue