refactor: type rewrite

This commit is contained in:
2026-04-17 10:18:37 +08:00
parent a469b4024a
commit 1c238aec3a
9 changed files with 69 additions and 304 deletions
@@ -41,4 +41,9 @@ export function onPlayerItemEffectUpkeep(entity: PlayerEntity){
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 {getAliveEnemies} from "@/samples/slay-the-spire-like/system/combat/utils";
import {
onDiscard, onDraw,
onEntityEffectUpkeep,
onPlayerItemEffectUpkeep
} from "@/samples/slay-the-spire-like/system/combat/effects";
@@ -12,7 +13,7 @@ type TriggerTypes = {
onTurnStart: { entityKey: "player" | string, },
onTurnEnd: { entityKey: "player" | string, },
onShuffle: { entityKey: "player" | string, },
onCardPlayed: { cardId: string, },
onCardPlayed: { cardId: string, targetId?: string },
onCardDiscarded: { cardId: string, },
onCardDrawn: { cardId: string, },
onEffectApplied: { effectId: string, entityKey: "player" | string, stacks: number, },
@@ -37,6 +38,8 @@ export function createStartWith(build: (triggers: Triggers) => void){
return async function(game: CombatGameContext){
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){
await triggers.onTurnStart.execute(game,{entityKey: "player"});
await game.produceAsync(draft => {
@@ -46,11 +49,23 @@ export function createStartWith(build: (triggers: Triggers) => void){
while(true){
const action = await promptMainAction(game);
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"});
// 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)){
await triggers.onTurnStart.execute(game,{entityKey: enemy.id});
@@ -1,6 +1,8 @@
import type { PlayerDeck } from "../deck/types";
import {EnemyData, IntentData} 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}>;
@@ -39,6 +41,7 @@ export type LootEntry = {
export type CombatState = {
enemies: EnemyEntity[];
player: PlayerEntity;
inventory: GridInventory<GameItemMeta>;
phase: CombatPhase;
turnNumber: number;
@@ -1,4 +1,4 @@
import {CombatState} from "@/samples/slay-the-spire-like/system";
import {CombatState} from "./types";
export function* getAliveEnemies(state: CombatState) {
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 { createRegion, createRegionAxis } from '@/core/region';
import type { GameCard, GameCardMeta, PlayerDeck, DeckRegions } from './types';
import { cardDesertData } from '../data';
import type { CardData } from '../types';
import type {DeckRegions, GameCard, PlayerDeck} 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.
*/
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);
function createCard( itemId: string, cardData: CardData, cellIndex: number): GameCard {
return {
id: cardId,
id: generateCardId(itemId, cellIndex),
regionId: '',
position: [],
sourceItemId: itemId,
itemData: cardData ?? null,
cellKey,
displayName: cardData?.name ?? itemData.name,
description: cardData?.desc ?? '',
itemId,
cardData
};
}
/**
* 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 {
function createDeckRegions(): DeckRegions {
return {
id,
regionId: '',
position: [],
sourceItemId: null,
itemData: null,
cellKey: null,
displayName,
description,
drawPile: createRegion('drawPile', []),
hand: createRegion('hand', []),
discardPile: createRegion('discardPile', []),
exhaustPile: createRegion('exhaustPile', []),
};
}
/**
* Generates a complete player deck from the current inventory state.
*/
function generateDeckFromInventory(inventory: GridInventory<GameItemMeta>): PlayerDeck {
const cards: Record<string, GameCard> = {};
const drawPile: string[] = [];
const regions = createDeckRegions();
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);
const count = item.shape.count;
for (let i = 0; i < count; i++) {
const card = createCard(item.id, itemData.card, i);
cards[card.id] = card;
drawPile.push(card.id);
moveToRegion(card, null, regions.drawPile);
}
}
return {
cards,
drawPile,
hand: [],
discardPile: [],
exhaustPile: [],
regions
};
}
/**
* 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 {
return {
cards: {},
drawPile: [],
hand: [],
discardPile: [],
exhaustPile: [],
regions: createDeckRegions(),
};
}
export {
generateDeckFromInventory,
createStatusCard,
createDeckRegions,
createCard,
createPlayerDeck,
createDeckRegions,
generateCardId,
};
@@ -1,8 +1,7 @@
export type { GameCard, GameCardMeta, PlayerDeck, DeckRegions } from './types';
export {
generateDeckFromInventory,
createStatusCard,
createDeckRegions,
createCard,
createPlayerDeck,
generateCardId,
} from './factory';
@@ -1,40 +1,14 @@
import type { Part } from '@/core/part';
import type { Region } from '@/core/region';
import type { HeroItemFighter1 } from '../data/heroItemFighter1.csv';
import type { CardDesert } from '../data/cardDesert.csv';
import type { CellKey } from '../grid-inventory/types';
import {CardData} from "@/samples/slay-the-spire-like/system/types";
import {Region} from "@/core/region";
/**
* 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 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;
cardData: CardData;
itemId: string;
}
/**
@@ -49,26 +23,17 @@ export type GameCard = Part<GameCardMeta>;
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[];
regions: DeckRegions;
}
/**
* Region structure for deck management.
*/
export interface DeckRegions {
/** Draw pile region */
export interface DeckRegions{
/** Card IDs in the draw pile */
drawPile: Region;
/** Hand region */
/** Card IDs in the player's hand */
hand: Region;
/** Discard pile region */
/** Card IDs in the discard pile */
discardPile: Region;
/** Exhaust pile region */
/** Card IDs in the exhaust pile (removed from combat) */
exhaustPile: Region;
}
}
@@ -1,7 +1,7 @@
import type { PointCrawlMap } from '../map/types';
import type { GridInventory, InventoryItem } from '../grid-inventory/types';
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.).
@@ -35,9 +35,11 @@ export interface EncounterState {
*/
export interface GameItemMeta {
/** Original CSV item data */
itemData: HeroItemFighter1;
itemData: ItemData;
/** Parsed shape for grid placement */
shape: ParsedShape;
/** Consumed uses, if card cost type is uses**/
depletion?: number;
}
/**