refactor: simplify buildCombatState and fix formatting in encounter

system

Refactor `buildCombatState` to derive encounter data directly from
`runState` instead of requiring it as an argument. Also apply
consistent 2-space indentation and formatting to the encounter
lifecycle module.
This commit is contained in:
hypercross 2026-04-19 15:42:47 +08:00
parent 89d96d838b
commit 8142fbfa60
1 changed files with 151 additions and 139 deletions

View File

@ -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<string, EncounterData[]>,
rng: ReadonlyRNG
map: PointCrawlMap,
encounterIndex: Map<string, EncounterData[]>,
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<string, IntentData> {
const intents: Record<string, IntentData> = {};
for (const intent of enemy.intents) {
intents[intent.id] = intent;
}
return intents;
function buildIntentMap(enemy: EnemyData): Record<string, IntentData> {
const intents: Record<string, IntentData> = {};
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<typeof generateDeckFromInventory>): 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<typeof generateDeckFromInventory>,
): 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<typeof generate
/**
* Gets the encounter data for the current node.
*/
export function getCurrentEncounterData(runState: RunState): EncounterData | undefined {
const node = runState.map.nodes.get(runState.currentNodeId);
return node?.encounter;
export function getCurrentEncounterData(
runState: RunState,
): EncounterData | undefined {
const node = runState.map.nodes.get(runState.currentNodeId);
return node?.encounter;
}
/**
* Checks if the current node has a combat encounter.
*/
export function isCombatEncounter(runState: RunState): boolean {
const encounter = getCurrentEncounterData(runState);
return encounter !== undefined && encounter.enemies.length > 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 };
}