feat(slay-the-spire-like): implement encounter system

Add logic for handling different encounter types including combat,
shop, curio, camp, and events. Includes shop item generation and
grid inventory placement for shop encounters.
This commit is contained in:
hypercross 2026-04-20 14:57:03 +08:00
parent 52b6cecd64
commit d5ec099540
4 changed files with 168 additions and 2 deletions

View File

@ -29,6 +29,12 @@ export function buildCombatState(
};
}
export function buildCombatEncounterState(
data: EncounterData<"minion" | "elite">,
): CombatEncounterState {
return { data, blocked: false };
}
function createEnemyEntities(encounter: EncounterData): EnemyEntity[] {
const enemies: EnemyEntity[] = [];
let instanceCounter = 0;

View File

@ -0,0 +1,100 @@
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,3 +1,4 @@
export { buildCombatState, buildCombatEncounterState } from "./combat";
export { buildShopEncounterState, generateInstanceId } from "./shop";
export { buildEncounterState } from "./encounter";
export { RunState, EncounterState } from "./types";
export { buildCombatState } from "./combat";
export { generateInstanceId } from "./shop";

View File

@ -1,4 +1,63 @@
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 };
}