diff --git a/src/samples/slay-the-spire-like/system/progress/encounter.ts b/src/samples/slay-the-spire-like/system/progress/encounter.ts index f3ff8eb..7beda7c 100644 --- a/src/samples/slay-the-spire-like/system/progress/encounter.ts +++ b/src/samples/slay-the-spire-like/system/progress/encounter.ts @@ -1,9 +1,19 @@ -import type { PointCrawlMap } from '../map/types'; -import type { CombatState, EnemyEntity, PlayerEntity, EffectTable } from '../combat/types'; -import type { EncounterData, EnemyData, EffectData, IntentData } from '../types'; -import type { RunState } from './types'; -import { generateDeckFromInventory } from '../deck/factory'; -import { ReadonlyRNG } from '@/utils/rng'; +import type { PointCrawlMap } from "../map/types"; +import type { + CombatState, + EnemyEntity, + PlayerEntity, + EffectTable, +} from "../combat/types"; +import type { + EncounterData, + EnemyData, + EffectData, + IntentData, +} from "../types"; +import type { RunState } from "./types"; +import { generateDeckFromInventory } from "../deck/factory"; +import { ReadonlyRNG } from "@/utils/rng"; // -- Encounter assignment to nodes -- @@ -12,15 +22,15 @@ import { ReadonlyRNG } from '@/utils/rng'; * Replaces any existing encounter on that node. */ export function assignEncounterToNode( - map: PointCrawlMap, - nodeId: string, - encounter: EncounterData + map: PointCrawlMap, + nodeId: string, + encounter: EncounterData, ): void { - const node = map.nodes.get(nodeId); - if (!node) { - throw new Error(`Node "${nodeId}" not found`); - } - node.encounter = encounter; + const node = map.nodes.get(nodeId); + if (!node) { + throw new Error(`Node "${nodeId}" not found`); + } + node.encounter = encounter; } /** @@ -28,19 +38,19 @@ export function assignEncounterToNode( * Uses RNG for random selection; each encounter can be assigned multiple times. */ export function assignEncountersFromPool( - map: PointCrawlMap, - encounterPool: EncounterData[], - rng: ReadonlyRNG + map: PointCrawlMap, + encounterPool: EncounterData[], + rng: ReadonlyRNG, ): void { - if (encounterPool.length === 0) return; + if (encounterPool.length === 0) return; - for (const node of map.nodes.values()) { - if (node.type === 'start' || node.type === 'end') continue; - if (node.encounter) continue; + for (const node of map.nodes.values()) { + if (node.type === "start" || node.type === "end") continue; + if (node.encounter) continue; - const assigned = encounterPool[rng.nextInt(encounterPool.length)]; - node.encounter = assigned; - } + const assigned = encounterPool[rng.nextInt(encounterPool.length)]; + node.encounter = assigned; + } } /** @@ -48,20 +58,20 @@ export function assignEncountersFromPool( * Keys in the index should match encounter type strings (e.g. 'minion', 'elite'). */ export function assignAllEncounters( - map: PointCrawlMap, - encounterIndex: Map, - rng: ReadonlyRNG + map: PointCrawlMap, + encounterIndex: Map, + rng: ReadonlyRNG, ): void { - for (const node of map.nodes.values()) { - if (node.type === 'start' || node.type === 'end') continue; - if (node.encounter) continue; + for (const node of map.nodes.values()) { + if (node.type === "start" || node.type === "end") continue; + if (node.encounter) continue; - const encounterType = node.type; - const pool = encounterIndex.get(encounterType); - if (!pool || pool.length === 0) continue; + const encounterType = node.type; + const pool = encounterIndex.get(encounterType); + if (!pool || pool.length === 0) continue; - node.encounter = pool[rng.nextInt(pool.length)]; - } + node.encounter = pool[rng.nextInt(pool.length)]; + } } // -- CombatState construction -- @@ -72,111 +82,111 @@ export function assignAllEncounters( * - Creates PlayerEntity with energy (3), deck from inventory, and HP from run state * - Sets initial phase to 'playerTurn', turn 1 */ -export function buildCombatState( - encounter: EncounterData, - runState: RunState, -): CombatState { - const enemies = createEnemyEntities(encounter); - const deck = generateDeckFromInventory(runState.inventory); - const player = createPlayerEntity(runState, deck); +export function buildCombatState(runState: RunState): CombatState { + const encounter = getCurrentEncounterData(runState); + if (!encounter) + throw new Error(`No encounter found for node ${runState.currentNodeId}`); - return { - enemies, - player, - inventory: runState.inventory, - phase: 'playerTurn', - turnNumber: 1, - result: null, - loot: [], - }; + const enemies = createEnemyEntities(encounter); + const deck = generateDeckFromInventory(runState.inventory); + const player = createPlayerEntity(runState, deck); + + return { + enemies, + player, + inventory: runState.inventory, + phase: "playerTurn", + turnNumber: 1, + result: null, + loot: [], + }; } /** * Creates EnemyEntity instances from encounter enemy definitions. * Each enemy gets: HP from encounter tuple, initial buffs from encounter, intents from enemy definition. */ -export function createEnemyEntities( - encounter: EncounterData, -): EnemyEntity[] { - const enemies: EnemyEntity[] = []; - let instanceCounter = 0; +export function createEnemyEntities(encounter: EncounterData): EnemyEntity[] { + const enemies: EnemyEntity[] = []; + let instanceCounter = 0; - for (const [enemyData, hp, encounterBuffs] of encounter.enemies) { - const instanceId = `${enemyData.id}-${instanceCounter++}`; - const intents = buildIntentMap(enemyData); - const initialIntent = findInitialIntent(enemyData); - const effects = buildEffectTable(encounterBuffs); + for (const [enemyData, hp, encounterBuffs] of encounter.enemies) { + const instanceId = `${enemyData.id}-${instanceCounter++}`; + const intents = buildIntentMap(enemyData); + const initialIntent = findInitialIntent(enemyData); + const effects = buildEffectTable(encounterBuffs); - const entity: EnemyEntity = { - id: instanceId, - enemy: enemyData, - hp, - maxHp: hp, - isAlive: true, - effects, - intents, - currentIntent: initialIntent, - }; - enemies.push(entity); - } + const entity: EnemyEntity = { + id: instanceId, + enemy: enemyData, + hp, + maxHp: hp, + isAlive: true, + effects, + intents, + currentIntent: initialIntent, + }; + enemies.push(entity); + } - return enemies; + return enemies; } /** * Builds a map of intent ID -> IntentData for an enemy. */ -function buildIntentMap( - enemy: EnemyData, -): Record { - const intents: Record = {}; - for (const intent of enemy.intents) { - intents[intent.id] = intent; - } - return intents; +function buildIntentMap(enemy: EnemyData): Record { + const intents: Record = {}; + for (const intent of enemy.intents) { + intents[intent.id] = intent; + } + return intents; } /** * Finds the initial intent ID for an enemy. */ function findInitialIntent(enemy: EnemyData): IntentData { - for (const intent of enemy.intents) { - if (intent.initialIntent) { - return intent; - } + for (const intent of enemy.intents) { + if (intent.initialIntent) { + return intent; } - if (enemy.intents.length === 0) { - throw new Error(`Enemy "${enemy.id}" has no intents`); - } - return enemy.intents[0]; + } + if (enemy.intents.length === 0) { + throw new Error(`Enemy "${enemy.id}" has no intents`); + } + return enemy.intents[0]; } /** * Builds an EffectTable from encounter buff definitions. */ function buildEffectTable(buffs: readonly [EffectData, number][]): EffectTable { - const table: EffectTable = {}; - for (const [effect, stacks] of buffs) { - table[effect.id] = { data: effect, stacks }; - } - return table; + const table: EffectTable = {}; + for (const [effect, stacks] of buffs) { + table[effect.id] = { data: effect, stacks }; + } + return table; } /** * Creates a PlayerEntity from the run state and deck. */ -function createPlayerEntity(runState: RunState, deck: ReturnType): PlayerEntity { - return { - id: "player", - hp: runState.player.currentHp, - maxHp: runState.player.maxHp, - isAlive: runState.player.currentHp > 0, - energy: 3, - maxEnergy: 3, - deck, - itemEffects: {}, - effects: {}, - }; +function createPlayerEntity( + runState: RunState, + deck: ReturnType, +): PlayerEntity { + return { + id: "player", + hp: runState.player.currentHp, + maxHp: runState.player.maxHp, + isAlive: runState.player.currentHp > 0, + energy: 3, + maxEnergy: 3, + deck, + itemEffects: {}, + effects: {}, + }; } // -- Encounter lifecycle -- @@ -184,17 +194,19 @@ function createPlayerEntity(runState: RunState, deck: ReturnType 0; + const encounter = getCurrentEncounterData(runState); + return encounter !== undefined && encounter.enemies.length > 0; } /** @@ -202,12 +214,12 @@ export function isCombatEncounter(runState: RunState): boolean { * Returns the constructed CombatState, or null if no combat encounter. */ export function startEncounter(runState: RunState): CombatState | null { - const encounter = getCurrentEncounterData(runState); - if (!encounter || encounter.enemies.length === 0) { - return null; - } + const encounter = getCurrentEncounterData(runState); + if (!encounter || encounter.enemies.length === 0) { + return null; + } - return buildCombatState(encounter, runState); + return buildCombatState(runState); } /** @@ -216,30 +228,30 @@ export function startEncounter(runState: RunState): CombatState | null { * Marks the encounter as resolved. */ export function resolveCombatEncounter( - runState: RunState, - combatState: CombatState + runState: RunState, + combatState: CombatState, ): { success: true } | { success: false; reason: string } { - if (runState.currentEncounter.resolved) { - return { success: false, reason: '该遭遇已解决' }; + if (runState.currentEncounter.resolved) { + return { success: false, reason: "该遭遇已解决" }; + } + + // Apply HP from combat state back to run state + runState.player.currentHp = Math.max(0, combatState.player.hp); + + // Apply loot + for (const loot of combatState.loot) { + if (loot.type === "gold") { + runState.player.gold += loot.amount; } + // Item rewards are handled by the caller via addItem() + } - // Apply HP from combat state back to run state - runState.player.currentHp = Math.max(0, combatState.player.hp); + // Mark as resolved + runState.currentEncounter.resolved = true; + runState.currentEncounter.result = { + hpLost: runState.player.maxHp - runState.player.currentHp, + }; + runState.resolvedNodeIds.add(runState.currentNodeId); - // Apply loot - for (const loot of combatState.loot) { - if (loot.type === 'gold') { - runState.player.gold += loot.amount; - } - // Item rewards are handled by the caller via addItem() - } - - // Mark as resolved - runState.currentEncounter.resolved = true; - runState.currentEncounter.result = { - hpLost: runState.player.maxHp - runState.player.currentHp, - }; - runState.resolvedNodeIds.add(runState.currentNodeId); - - return { success: true }; + return { success: true }; }