fix: type issues
This commit is contained in:
parent
2f085cc0b6
commit
aedf82d264
|
|
@ -8,7 +8,7 @@ type CardTable = readonly {
|
||||||
readonly costType: "energy" | "uses" | "none";
|
readonly costType: "energy" | "uses" | "none";
|
||||||
readonly costCount: number;
|
readonly costCount: number;
|
||||||
readonly targetType: "single" | "none";
|
readonly targetType: "single" | "none";
|
||||||
readonly effects: readonly ["onPlay" | "onDraw" | "onDiscard", "self" | "target" | "all" | "random", Effect, number];
|
readonly effects: ["onPlay" | "onDraw" | "onDiscard", "self" | "target" | "all" | "random", Effect, number][];
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
export type Card = CardTable[number];
|
export type Card = CardTable[number];
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
# enemies: array of [enemyId; initialHp; buffs[]]
|
# enemies: array of [enemyId; initialHp; buffs[]]
|
||||||
|
|
||||||
id,type,name,description,enemies,dialogue
|
id,type,name,description,enemies,dialogue
|
||||||
string,'minion'|'elite'|'event'|'shop'|'camp'|'curio',string,string,[data: @enemy; hp: int; [effect: @effect;stacks: int]][],string
|
string,'minion'|'elite'|'event'|'shop'|'camp'|'curio',string,string,[data: @enemy; hp: int; effects: [effect: @effect;stacks: int][]][],string
|
||||||
cactus_pair,minion,仙人掌怪,概念:防+强化。【尖刺X】:对攻击者造成X点伤害。,[仙人掌怪;12;[]];[仙人掌怪;12;[]],
|
cactus_pair,minion,仙人掌怪,概念:防+强化。【尖刺X】:对攻击者造成X点伤害。,[仙人掌怪;12;[]];[仙人掌怪;12;[]],
|
||||||
snake_pair,minion,蛇,概念:攻+强化。给玩家塞入蛇毒牌(1费:打出时移除此牌。弃掉时受到3点伤害)。,[蛇;10;[]],
|
snake_pair,minion,蛇,概念:攻+强化。给玩家塞入蛇毒牌(1费:打出时移除此牌。弃掉时受到3点伤害)。,[蛇;10;[]],
|
||||||
mummy_cactus,minion,木乃伊,概念:攻+防。【诅咒】:受攻击时物品【攻击】-1,直到弃掉一张该物品的牌。,[木乃伊;14;[]];[仙人掌怪;12;[]],
|
mummy_cactus,minion,木乃伊,概念:攻+防。【诅咒】:受攻击时物品【攻击】-1,直到弃掉一张该物品的牌。,[木乃伊;14;[]];[仙人掌怪;12;[]],
|
||||||
|
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ type EncounterTable = readonly {
|
||||||
readonly type: "minion" | "elite" | "event" | "shop" | "camp" | "curio";
|
readonly type: "minion" | "elite" | "event" | "shop" | "camp" | "curio";
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
readonly enemies: readonly [readonly data: Enemy, readonly hp: number, readonly [readonly effect: Effect, readonly stacks: number]];
|
readonly enemies: [data: Enemy, hp: number, effects: [effect: Effect, stacks: number][]][];
|
||||||
readonly dialogue: string;
|
readonly dialogue: string;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
id,name,intentIds,description
|
id,name,intents,description
|
||||||
string,string,@intent[],string
|
string,string,@intent[],string
|
||||||
仙人掌怪,仙人掌怪,[仙人掌怪-boost;仙人掌怪-defend;仙人掌怪-attack],防+强化。【尖刺X】:对攻击者造成X点伤害。
|
仙人掌怪,仙人掌怪,[仙人掌怪-boost;仙人掌怪-defend;仙人掌怪-attack],防+强化。【尖刺X】:对攻击者造成X点伤害。
|
||||||
蛇,蛇,[蛇-poison;蛇-attack;蛇-boost],攻+强化。给玩家塞入蛇毒牌(1费:打出时移除此牌。弃掉时受到3点伤害)。
|
蛇,蛇,[蛇-poison;蛇-attack;蛇-boost],攻+强化。给玩家塞入蛇毒牌(1费:打出时移除此牌。弃掉时受到3点伤害)。
|
||||||
|
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import type { Intent } from './intent.csv';
|
||||||
type EnemyTable = readonly {
|
type EnemyTable = readonly {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly intentIds: readonly Intent[];
|
readonly intents: Intent[];
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ type IntentTable = readonly {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly enemy: Enemy;
|
readonly enemy: Enemy;
|
||||||
readonly initialIntent: boolean;
|
readonly initialIntent: boolean;
|
||||||
readonly nextIntents: readonly Intent[];
|
readonly nextIntents: Intent[];
|
||||||
readonly brokenIntent: readonly Intent[];
|
readonly brokenIntent: Intent[];
|
||||||
readonly effects: readonly ["self" | "player" | "team", Effect, number];
|
readonly effects: ["self" | "player" | "team", Effect, number][];
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
export type Intent = IntentTable[number];
|
export type Intent = IntentTable[number];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,268 @@
|
||||||
|
import type { PointCrawlMap, MapNode } from '../map/types';
|
||||||
|
import type { CombatState, EnemyEntity, PlayerEntity, EffectTable } from '../combat/types';
|
||||||
|
import type { EncounterData, EnemyData, EffectData, IntentData } from '../types';
|
||||||
|
import type { RunState, GameItemMeta } from './types';
|
||||||
|
import type { GridInventory } from '../grid-inventory/types';
|
||||||
|
import { generateDeckFromInventory } from '../deck/factory';
|
||||||
|
import { ReadonlyRNG } from '@/utils/rng';
|
||||||
|
import { createRegion } from '@/core/region';
|
||||||
|
|
||||||
|
// -- Encounter assignment to nodes --
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns an encounter from a pool to a specific node.
|
||||||
|
* Replaces any existing encounter on that node.
|
||||||
|
*/
|
||||||
|
export function assignEncounterToNode(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns encounters from a typed pool to all unassigned nodes of matching type.
|
||||||
|
* Uses RNG for random selection; each encounter can be assigned multiple times.
|
||||||
|
*/
|
||||||
|
export function assignEncountersFromPool(
|
||||||
|
map: PointCrawlMap,
|
||||||
|
encounterPool: EncounterData[],
|
||||||
|
rng: ReadonlyRNG
|
||||||
|
): void {
|
||||||
|
if (encounterPool.length === 0) return;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch-assigns encounters for all node types from a typed index.
|
||||||
|
* Keys in the index should match encounter type strings (e.g. 'minion', 'elite').
|
||||||
|
*/
|
||||||
|
export function assignAllEncounters(
|
||||||
|
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;
|
||||||
|
|
||||||
|
const encounterType = node.type;
|
||||||
|
const pool = encounterIndex.get(encounterType);
|
||||||
|
if (!pool || pool.length === 0) continue;
|
||||||
|
|
||||||
|
node.encounter = pool[rng.nextInt(pool.length)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- CombatState construction --
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a full CombatState from an encounter and the current run state.
|
||||||
|
* - Creates EnemyEntity instances with HP, initial buffs, and intents
|
||||||
|
* - 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,
|
||||||
|
intentPool: IntentData[] = []
|
||||||
|
): CombatState {
|
||||||
|
const intentIndex = buildIntentIndex(intentPool);
|
||||||
|
const enemies = createEnemyEntities(encounter, intentIndex);
|
||||||
|
const deck = generateDeckFromInventory(runState.inventory);
|
||||||
|
const player = createPlayerEntity(runState, deck);
|
||||||
|
|
||||||
|
return {
|
||||||
|
enemies,
|
||||||
|
player,
|
||||||
|
inventory: runState.inventory,
|
||||||
|
phase: 'playerTurn',
|
||||||
|
turnNumber: 1,
|
||||||
|
result: null,
|
||||||
|
loot: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an index of intents by their ID for fast lookup.
|
||||||
|
*/
|
||||||
|
function buildIntentIndex(intentPool: IntentData[]): Map<string, IntentData> {
|
||||||
|
const index = new Map<string, IntentData>();
|
||||||
|
for (const intent of intentPool) {
|
||||||
|
index.set(intent.id, intent);
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates EnemyEntity instances from encounter enemy definitions.
|
||||||
|
* Each enemy gets: initial HP from enemy data, initial buffs from encounter, intents from enemy definition.
|
||||||
|
*/
|
||||||
|
export function createEnemyEntities(
|
||||||
|
encounter: EncounterData,
|
||||||
|
intentIndex: Map<string, IntentData>
|
||||||
|
): EnemyEntity[] {
|
||||||
|
const enemies: EnemyEntity[] = [];
|
||||||
|
let instanceCounter = 0;
|
||||||
|
|
||||||
|
for (const [enemyData, count, encounterBuffs] of encounter.enemies) {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const instanceId = `${enemyData.id}-${instanceCounter++}`;
|
||||||
|
const intents = buildIntentMap(enemyData, intentIndex);
|
||||||
|
const initialIntentId = findInitialIntent(enemyData, intentIndex);
|
||||||
|
const effects = buildEffectTable(encounterBuffs);
|
||||||
|
|
||||||
|
const entity: EnemyEntity = {
|
||||||
|
id: instanceId,
|
||||||
|
enemy: enemyData,
|
||||||
|
hp: enemyData.hp,
|
||||||
|
maxHp: enemyData.hp,
|
||||||
|
isAlive: true,
|
||||||
|
effects,
|
||||||
|
intents,
|
||||||
|
currentIntentId: initialIntentId,
|
||||||
|
};
|
||||||
|
enemies.push(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return enemies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a map of intent ID -> IntentData for an enemy.
|
||||||
|
*/
|
||||||
|
function buildIntentMap(
|
||||||
|
enemy: EnemyData,
|
||||||
|
intentIndex: Map<string, IntentData>
|
||||||
|
): Record<string, IntentData> {
|
||||||
|
const intents: Record<string, IntentData> = {};
|
||||||
|
for (const intentId of enemy.intentIds) {
|
||||||
|
const intent = intentIndex.get(intentId);
|
||||||
|
if (intent) {
|
||||||
|
intents[intentId] = intent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return intents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the initial intent ID for an enemy.
|
||||||
|
*/
|
||||||
|
function findInitialIntent(
|
||||||
|
enemy: EnemyData,
|
||||||
|
intentIndex: Map<string, IntentData>
|
||||||
|
): string {
|
||||||
|
for (const intentId of enemy.intentIds) {
|
||||||
|
const intent = intentIndex.get(intentId);
|
||||||
|
if (intent?.initialIntent) {
|
||||||
|
return intentId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: first intent
|
||||||
|
return enemy.intentIds[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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a PlayerEntity from the run state and deck.
|
||||||
|
*/
|
||||||
|
function createPlayerEntity(runState: RunState, deck: ReturnType<typeof generateDeckFromInventory>): PlayerEntity {
|
||||||
|
return {
|
||||||
|
hp: runState.player.currentHp,
|
||||||
|
maxHp: runState.player.maxHp,
|
||||||
|
isAlive: runState.player.currentHp > 0,
|
||||||
|
energy: 3,
|
||||||
|
maxEnergy: 3,
|
||||||
|
deck,
|
||||||
|
itemEffects: {},
|
||||||
|
effects: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Encounter lifecycle --
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the encounter at the current node.
|
||||||
|
* Returns the constructed CombatState, or null if no combat encounter.
|
||||||
|
*/
|
||||||
|
export function startEncounter(runState: RunState, intentPool: IntentData[] = []): CombatState | null {
|
||||||
|
const encounter = getCurrentEncounterData(runState);
|
||||||
|
if (!encounter || encounter.enemies.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildCombatState(encounter, runState, intentPool);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a completed combat and applies rewards to the run state.
|
||||||
|
* Handles: gold loot, item rewards, HP changes.
|
||||||
|
* Marks the encounter as resolved.
|
||||||
|
*/
|
||||||
|
export function resolveCombatEncounter(
|
||||||
|
runState: RunState,
|
||||||
|
combatState: CombatState
|
||||||
|
): { success: true } | { success: false; reason: string } {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 };
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,19 @@ export type {
|
||||||
RunState,
|
RunState,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
|
// Re-export encounter construction functions
|
||||||
|
export {
|
||||||
|
assignEncounterToNode,
|
||||||
|
assignEncountersFromPool,
|
||||||
|
assignAllEncounters,
|
||||||
|
buildCombatState,
|
||||||
|
createEnemyEntities,
|
||||||
|
getCurrentEncounterData,
|
||||||
|
isCombatEncounter,
|
||||||
|
startEncounter,
|
||||||
|
resolveCombatEncounter,
|
||||||
|
} from './encounter';
|
||||||
|
|
||||||
// -- Constants --
|
// -- Constants --
|
||||||
|
|
||||||
const INVENTORY_WIDTH = 6;
|
const INVENTORY_WIDTH = 6;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export type EffectLifecycle = "instant" | "temporary" | "lingering" | "permanent
|
||||||
export type EnemyData = {
|
export type EnemyData = {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
|
readonly intents: readonly IntentData[];
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -36,17 +37,16 @@ export type EncounterData = {
|
||||||
readonly type: EncounterType;
|
readonly type: EncounterType;
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
readonly enemies: readonly [EnemyData, number, readonly [effect: EffectData, stacks: number]];
|
readonly enemies: readonly [data: EnemyData, hp: number, effects: [EffectData, stacks: number][]][];
|
||||||
readonly dialogue: string;
|
readonly dialogue: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IntentData = {
|
export type IntentData = {
|
||||||
|
readonly id: string;
|
||||||
readonly enemy: EnemyData;
|
readonly enemy: EnemyData;
|
||||||
readonly intentId: string;
|
|
||||||
readonly initialIntent: boolean;
|
readonly initialIntent: boolean;
|
||||||
readonly nextIntents: readonly string[];
|
readonly nextIntents: readonly IntentData[];
|
||||||
readonly brokenIntent: readonly string[];
|
readonly brokenIntent: readonly IntentData[];
|
||||||
readonly initBuffs: readonly [EffectData, stacks: number];
|
|
||||||
readonly effects: readonly [EffectTarget, EffectData, number][];
|
readonly effects: readonly [EffectTarget, EffectData, number][];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue