refactor(slay-the-spire-like): simplify exports and reorganize system

Clean up the sample's entry point by using barrel exports and move
encounter logic into the grid-inventory factory to reduce duplication
and improve module structure.
This commit is contained in:
hypercross 2026-04-21 22:57:28 +08:00
parent 9b20f7ad6f
commit b509284bc6
8 changed files with 88 additions and 303 deletions

View File

@ -1,130 +1,6 @@
// Types
export type {
EffectData,
EffectLifecycle,
EnemyData,
CardType,
CardCostType,
CardTargetType,
EffectTarget,
CardData,
CardEffectTrigger,
CardEffectTarget,
EncounterType,
EncounterData,
IntentData,
ItemData,
} from "./system/types";
// Deck
export type {
GameCard,
GameCardMeta,
PlayerDeck,
DeckRegions,
} from "./system/deck";
export {
generateDeckFromInventory,
createCard,
createPlayerDeck,
generateCardId,
} from "./system/deck";
// Grid Inventory
export type {
CellCoordinate,
CellKey,
GridInventory,
InventoryItem,
MutationResult,
PlacementResult,
GameItem,
GameItemMeta,
} from "./system/grid-inventory";
export {
createGridInventory,
flipItem,
getAdjacentItems,
getItemAtCell,
getOccupiedCellSet,
moveItem,
placeItem,
removeItem as removeItemFromGrid,
rotateItem,
validatePlacement,
createItemIn,
} from "./system/grid-inventory";
// Map
export { MapNodeType, MapLayerType } from "./system/map";
export type {
MapNode,
MapLayer,
PointCrawlMap,
MapGenerationConfig,
} from "./system/map";
export {
generatePointCrawlMap,
getNode,
getChildren,
getParents,
findAllPaths,
} from "./system/map";
// Progress / Run
export type { EncounterState, RunState } from "./system/encounter";
export { buildCombatState } from "./system/encounter";
// Combat
export type {
EffectTable,
CombatEntity,
PlayerEntity,
EnemyEntity,
CombatPhase,
CombatResult,
LootEntry,
CombatState,
CombatGameContext,
} from "./system/combat/types";
export {
addEffect,
addEntityEffect,
addItemEffect,
onEntityEffectUpkeep,
onEntityPostureDamage,
onPlayerItemEffectUpkeep,
onItemPlay,
onItemDiscard,
getAliveEnemies,
getEffectTargets,
getCombatEntity,
canPlayCard,
payCardCost,
} from "./system/combat/effects";
export {
prompts as combatPrompts,
promptMainAction,
} from "./system/combat/prompts";
export { createStartWith, type Triggers } from "./system/combat/triggers";
// Utils
export { parseShapeString, type ParsedShape } from "./system/utils/parse-shape";
export {
IDENTITY_TRANSFORM,
type Transform2D,
type Point2D,
getOccupiedCells,
transformPoint,
transformShape,
checkCollision,
checkBoardCollision,
checkBounds,
rotateTransform,
flipXTransform,
flipYTransform,
} from "./system/utils/shape-collision";
// Data
export type { ContentModule } from "./data";
export { default as data } from "./data";
export * from "./system/combat";
export * from "./system/deck";
export * from "./system/encounter";
export * from "./system/grid-inventory";
export * from "./system/map";
export * from "./system/utils/parse-shape";

View File

@ -0,0 +1,5 @@
export * from "./effects";
export * from "./factory";
export * from "./prompts";
export * from "./triggers";
export * from "./types";

View File

@ -1,100 +0,0 @@
import { createGridInventory, placeItem } from "../grid-inventory";
import { GameItemMeta } from "../grid-inventory/types";
import { IDENTITY_TRANSFORM } from "../utils/shape-collision";
import { parseShapeString } from "../utils/parse-shape";
import { EncounterData, EncounterType, ItemData } from "../types";
import type { RNG } from "@/utils/rng";
import type {
CampEncounterState,
CombatEncounterState,
CurioEncounterState,
DialogueEncounterState,
EncounterState,
ShopEncounterState,
} from "./types";
import { buildCombatEncounterState } from "./combat";
import { buildShopEncounterState } from "./shop";
function createCurioItems(allItems: ItemData[], rng: RNG): GameItemMeta[] {
const curioItems: GameItemMeta[] = [];
const rolledIndices = new Set<number>();
for (let i = 0; i < 3 && rolledIndices.size < allItems.length; i++) {
let index: number;
do {
index = rng.nextInt(allItems.length);
} while (rolledIndices.has(index));
rolledIndices.add(index);
const itemData = allItems[index];
const shape = parseShapeString(itemData.shape);
curioItems.push({ itemData, shape });
}
return curioItems;
}
export function buildCurioEncounterState(
data: EncounterData<"curio">,
allItems: ItemData[],
rng: RNG,
): CurioEncounterState {
const items = createCurioItems(allItems, rng);
const inventory = createGridInventory<GameItemMeta>(6, 4);
for (let i = 0; i < items.length; i++) {
const meta = items[i];
placeItem(inventory, {
id: `curio-item-${i}`,
shape: meta.shape,
transform: { ...IDENTITY_TRANSFORM, offset: { x: i, y: 0 } },
meta,
});
}
return { data, items: inventory };
}
export function buildCampEncounterState(
data: EncounterData<"camp">,
): CampEncounterState {
return { data };
}
export function buildDialogueEncounterState(
data: EncounterData<"event">,
): DialogueEncounterState {
return { data, blocked: false };
}
export function buildEncounterState(
data: EncounterData<EncounterType>,
allItems: ItemData[],
rng: RNG,
idCounter: { value: number },
): EncounterState {
switch (data.type) {
case "minion":
case "elite":
return buildCombatEncounterState(
data as EncounterData<"minion" | "elite">,
);
case "shop":
return buildShopEncounterState(
data as EncounterData<"shop">,
allItems,
rng,
idCounter,
);
case "curio":
return buildCurioEncounterState(
data as EncounterData<"curio">,
allItems,
rng,
);
case "camp":
return buildCampEncounterState(data as EncounterData<"camp">);
case "event":
return buildDialogueEncounterState(data as EncounterData<"event">);
}
}

View File

@ -1,4 +1,2 @@
export { buildCombatState, buildCombatEncounterState } from "./combat";
export { buildShopEncounterState, generateInstanceId } from "./shop";
export { buildEncounterState } from "./encounter";
export { RunState, EncounterState } from "./types";
export * from "./types";
export * from "./run";

View File

@ -3,7 +3,7 @@ import { RunState } from "./types";
const DEFAULT_MAX_HP = 50;
const DEFAULT_GOLD = 50;
export function createRunState(startNode: string): RunState {
export function createRunState(): RunState {
return {
maxHp: DEFAULT_MAX_HP,
currentHp: DEFAULT_MAX_HP,

View File

@ -1,63 +0,0 @@
import { createGridInventory, placeItem } from "../grid-inventory";
import { GameItemMeta } from "../grid-inventory/types";
import { IDENTITY_TRANSFORM } from "../utils/shape-collision";
import { parseShapeString } from "../utils/parse-shape";
import { EncounterData, ItemData } from "../types";
import type { RNG } from "@/utils/rng";
import type { ShopEncounterState } from "./types";
export function generateInstanceId(counter: { value: number }): string {
counter.value++;
return `item-${counter.value}`;
}
function createShopItems(
allItems: ItemData[],
rng: RNG,
): (GameItemMeta & { sellPrice: number })[] {
const shopItems: (GameItemMeta & { sellPrice: number })[] = [];
const rolledIndices = new Set<number>();
for (let i = 0; i < 5 && rolledIndices.size < allItems.length; i++) {
let index: number;
do {
index = rng.nextInt(allItems.length);
} while (rolledIndices.has(index));
rolledIndices.add(index);
const itemData = allItems[index];
const shape = parseShapeString(itemData.shape);
const sellPrice = Math.floor(
(rng.nextInt(5) + rng.nextInt(5) + 1) * 0.2 * itemData.price,
);
shopItems.push({ itemData, shape, sellPrice });
}
return shopItems;
}
export function buildShopEncounterState(
data: EncounterData<"shop">,
allItems: ItemData[],
rng: RNG,
idCounter: { value: number },
): ShopEncounterState {
const items = createShopItems(allItems, rng);
const inventory = createGridInventory<GameItemMeta & { sellPrice: number }>(
6,
4,
);
for (let i = 0; i < items.length; i++) {
const meta = items[i];
placeItem(inventory, {
id: generateInstanceId(idCounter),
shape: meta.shape,
transform: { ...IDENTITY_TRANSFORM, offset: { x: i, y: 0 } },
meta,
});
}
return { data, items: inventory };
}

View File

@ -1,9 +1,10 @@
import { parseShapeString } from "../utils/parse-shape";
import type { ParsedShape } from "../utils/parse-shape";
import type { Transform2D } from "../utils/shape-collision";
import { placeItem, validatePlacement } from "./transform";
import { IDENTITY_TRANSFORM, type Transform2D } from "../utils/shape-collision";
import { createGridInventory, placeItem, validatePlacement } from "./transform";
import type { GameItemMeta, GridInventory, MutationResult } from "./types";
import type { ItemData } from "../types";
import { ReadonlyRNG, RNG } from "@/utils/rng";
/**
* Creates and places a GameItemMeta item into the grid inventory.
@ -13,15 +14,16 @@ import type { ItemData } from "../types";
export function createItemIn(
inventory: GridInventory<GameItemMeta>,
id: string,
itemData: ItemData,
item: ItemData | GameItemMeta,
): MutationResult {
const itemData = "itemData" in item ? item.itemData : item;
const shape = parseShapeString(itemData.shape);
const transform = findFirstValidPlacement(inventory, shape);
if (!transform) {
return { success: false, reason: "无可用位置" };
}
const meta: GameItemMeta = { itemData, shape };
const meta: GameItemMeta = "itemData" in item ? item : { itemData, shape };
placeItem(inventory, { id, shape, transform, meta });
return { success: true };
}
@ -50,3 +52,70 @@ function findFirstValidPlacement(
}
return null;
}
function generateInstanceId(counter: { value: number }): string {
counter.value++;
return `item-${counter.value}`;
}
export function createShopInventory(
allItems: ItemData[],
rng: ReadonlyRNG,
counter: { value: number },
) {
const shopItems: GameItemMeta[] = [];
const rolledIndices = new Set<number>();
for (let i = 0; i < 5 && rolledIndices.size < allItems.length; i++) {
let index: number;
do {
index = rng.nextInt(allItems.length);
} while (rolledIndices.has(index));
rolledIndices.add(index);
const itemData = allItems[index];
const shape = parseShapeString(itemData.shape);
const tradePrice = Math.floor(
(rng.nextInt(5) + rng.nextInt(5) + 1) * 0.2 * itemData.price,
);
shopItems.push({ itemData, shape, tradePrice });
}
const inventory = createGridInventory<GameItemMeta>(6, 4);
for (let i = 0; i < shopItems.length; i++) {
createItemIn(inventory, generateInstanceId(counter), shopItems[i]);
}
return inventory;
}
export function createCurioItems(
allItems: ItemData[],
rng: RNG,
counter: { value: number },
) {
const curioItems: GameItemMeta[] = [];
const rolledIndices = new Set<number>();
for (let i = 0; i < 2 && rolledIndices.size < allItems.length; i++) {
let index: number;
do {
index = rng.nextInt(allItems.length);
} while (rolledIndices.has(index));
rolledIndices.add(index);
const itemData = allItems[index];
const shape = parseShapeString(itemData.shape);
curioItems.push({ itemData, shape });
}
const inventory = createGridInventory<GameItemMeta>(4, 3);
for (let i = 0; i < curioItems.length; i++) {
createItemIn(inventory, generateInstanceId(counter), curioItems[i]);
}
return inventory;
}

View File

@ -21,4 +21,4 @@ export {
validatePlacement,
} from "./transform";
export { createItemIn } from "./factory";
export * from "./factory";