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