From d5ec0995406dbf3fe10bb924adc97c872b3e07c9 Mon Sep 17 00:00:00 2001 From: hypercross Date: Mon, 20 Apr 2026 14:57:03 +0800 Subject: [PATCH] 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. --- .../system/encounter/combat.ts | 6 ++ .../system/encounter/encounter.ts | 100 ++++++++++++++++++ .../system/encounter/index.ts | 5 +- .../system/encounter/shop.ts | 59 +++++++++++ 4 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 src/samples/slay-the-spire-like/system/encounter/encounter.ts diff --git a/src/samples/slay-the-spire-like/system/encounter/combat.ts b/src/samples/slay-the-spire-like/system/encounter/combat.ts index bf65206..a771158 100644 --- a/src/samples/slay-the-spire-like/system/encounter/combat.ts +++ b/src/samples/slay-the-spire-like/system/encounter/combat.ts @@ -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; diff --git a/src/samples/slay-the-spire-like/system/encounter/encounter.ts b/src/samples/slay-the-spire-like/system/encounter/encounter.ts new file mode 100644 index 0000000..2a70799 --- /dev/null +++ b/src/samples/slay-the-spire-like/system/encounter/encounter.ts @@ -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(); + + 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(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, + 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">); + } +} diff --git a/src/samples/slay-the-spire-like/system/encounter/index.ts b/src/samples/slay-the-spire-like/system/encounter/index.ts index 23bf6aa..49136e8 100644 --- a/src/samples/slay-the-spire-like/system/encounter/index.ts +++ b/src/samples/slay-the-spire-like/system/encounter/index.ts @@ -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"; diff --git a/src/samples/slay-the-spire-like/system/encounter/shop.ts b/src/samples/slay-the-spire-like/system/encounter/shop.ts index 20fd882..b291c4d 100644 --- a/src/samples/slay-the-spire-like/system/encounter/shop.ts +++ b/src/samples/slay-the-spire-like/system/encounter/shop.ts @@ -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(); + + 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( + 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 }; +}