feat: add event engine with age trigger, random pool, choice filtering
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"id": "daily_training",
|
||||
"name": "日常修炼",
|
||||
"description": "你坚持日常修炼,体质有所提升。",
|
||||
"trigger": {
|
||||
"type": "random",
|
||||
"pool": "normal"
|
||||
},
|
||||
"weight": 50,
|
||||
"effects": [
|
||||
{
|
||||
"type": "addStat",
|
||||
"stat": "体质",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "study_hard",
|
||||
"name": "刻苦学习",
|
||||
"description": "你发奋读书,悟性有所提升。",
|
||||
"trigger": {
|
||||
"type": "random",
|
||||
"pool": "normal"
|
||||
},
|
||||
"weight": 50,
|
||||
"effects": [
|
||||
{
|
||||
"type": "addStat",
|
||||
"stat": "悟性",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "invasion_military_40",
|
||||
"name": "蛮族入侵",
|
||||
"description": "北方蛮族大举入侵,边境告急!",
|
||||
"trigger": {
|
||||
"type": "age",
|
||||
"value": 40
|
||||
},
|
||||
"isChoice": true,
|
||||
"choices": [
|
||||
{
|
||||
"id": "resist",
|
||||
"text": "率军抵抗",
|
||||
"requirements": {
|
||||
"type": "careerLevel",
|
||||
"careerId": "soldier",
|
||||
"op": ">=",
|
||||
"value": 50
|
||||
},
|
||||
"effects": [
|
||||
{
|
||||
"type": "addLog",
|
||||
"text": "你率领大军击退了蛮族入侵,威震四方!"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "flee",
|
||||
"text": "弃城逃跑",
|
||||
"effects": [
|
||||
{
|
||||
"type": "addLog",
|
||||
"text": "你选择了弃城逃跑,虽然保住了性命,但名声扫地。"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "invasion_spiritual_45",
|
||||
"name": "天魔降世",
|
||||
"description": "天魔降世,人间陷入混乱,你该如何应对?",
|
||||
"trigger": {
|
||||
"type": "age",
|
||||
"value": 45
|
||||
},
|
||||
"isChoice": true,
|
||||
"choices": [
|
||||
{
|
||||
"id": "suppress",
|
||||
"text": "出手镇压",
|
||||
"requirements": {
|
||||
"type": "careerLevel",
|
||||
"careerId": "taoist",
|
||||
"op": ">=",
|
||||
"value": 50
|
||||
},
|
||||
"effects": [
|
||||
{
|
||||
"type": "addLog",
|
||||
"text": "你以无上道法镇压天魔,拯救了苍生!"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "hide",
|
||||
"text": "躲藏起来",
|
||||
"effects": [
|
||||
{
|
||||
"type": "addLog",
|
||||
"text": "你选择了躲藏起来,避开了这场浩劫。"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "invasion_political_50",
|
||||
"name": "王朝崩坏",
|
||||
"description": "王朝内部腐败,民不聊生,天下大乱。",
|
||||
"trigger": {
|
||||
"type": "age",
|
||||
"value": 50
|
||||
},
|
||||
"isChoice": true,
|
||||
"choices": [
|
||||
{
|
||||
"id": "assist",
|
||||
"text": "辅佐明君",
|
||||
"requirements": {
|
||||
"type": "stat",
|
||||
"stat": "智力",
|
||||
"op": ">=",
|
||||
"value": 50
|
||||
},
|
||||
"effects": [
|
||||
{
|
||||
"type": "addLog",
|
||||
"text": "你以智谋辅佐明君,力挽狂澜,拯救了王朝。"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "selfish",
|
||||
"text": "独善其身",
|
||||
"effects": [
|
||||
{
|
||||
"type": "addLog",
|
||||
"text": "你选择了独善其身,在乱世中保全了自己。"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "final_crisis_60",
|
||||
"name": "最终危机",
|
||||
"description": "世界面临最终危机,这是你最后的选择。",
|
||||
"trigger": {
|
||||
"type": "age",
|
||||
"value": 60
|
||||
},
|
||||
"isChoice": true,
|
||||
"choices": [
|
||||
{
|
||||
"id": "general",
|
||||
"text": "以将军之力平定天下",
|
||||
"requirements": {
|
||||
"type": "careerLevel",
|
||||
"careerId": "soldier",
|
||||
"op": ">=",
|
||||
"value": 50
|
||||
},
|
||||
"effects": [
|
||||
{
|
||||
"type": "addLog",
|
||||
"text": "你以将军之力平定天下,成为一代名将!"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chancellor",
|
||||
"text": "以宰相之智治理国家",
|
||||
"requirements": {
|
||||
"type": "stat",
|
||||
"stat": "智力",
|
||||
"op": ">=",
|
||||
"value": 50
|
||||
},
|
||||
"effects": [
|
||||
{
|
||||
"type": "addLog",
|
||||
"text": "你以宰相之智治理国家,名垂青史!"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "cultivator",
|
||||
"text": "以修仙之道超脱轮回",
|
||||
"requirements": {
|
||||
"type": "careerLevel",
|
||||
"careerId": "taoist",
|
||||
"op": ">=",
|
||||
"value": 50
|
||||
},
|
||||
"effects": [
|
||||
{
|
||||
"type": "addLog",
|
||||
"text": "你以修仙之道超脱轮回,飞升成仙!"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "powerless",
|
||||
"text": "无力回天",
|
||||
"effects": [
|
||||
{
|
||||
"type": "addLog",
|
||||
"text": "你感到无力回天,只能接受命运的安排。"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// ==================== 事件引擎 ====================
|
||||
|
||||
import { evaluateCondition } from './conditionEvaluator.js';
|
||||
|
||||
let _eventConfig = null;
|
||||
|
||||
export function setEventConfig(config) {
|
||||
_eventConfig = config;
|
||||
}
|
||||
|
||||
export function getEventConfig() {
|
||||
return _eventConfig;
|
||||
}
|
||||
|
||||
export function checkAgeEvents(config, state) {
|
||||
const triggered = [];
|
||||
if (!config || !config.events) return triggered;
|
||||
if (!state.triggeredEvents) state.triggeredEvents = new Set();
|
||||
|
||||
for (const event of config.events) {
|
||||
if (event.trigger?.type !== 'age') continue;
|
||||
if (state.triggeredEvents.has(event.id)) continue;
|
||||
if (state.age === event.trigger.value) {
|
||||
triggered.push(event);
|
||||
state.triggeredEvents.add(event.id);
|
||||
}
|
||||
}
|
||||
return triggered;
|
||||
}
|
||||
|
||||
export function pickRandomEvent(config, state, pool) {
|
||||
if (!config || !config.events) return null;
|
||||
if (!state.triggeredEvents) state.triggeredEvents = new Set();
|
||||
|
||||
const candidates = config.events.filter(event => {
|
||||
if (event.trigger?.type !== 'random') return false;
|
||||
if (event.trigger.pool !== pool) return false;
|
||||
if (state.triggeredEvents.has(event.id)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (candidates.length === 0) return null;
|
||||
|
||||
const totalWeight = candidates.reduce((sum, e) => sum + (e.weight || 0), 0);
|
||||
if (totalWeight <= 0) return null;
|
||||
|
||||
let roll = Math.random() * totalWeight;
|
||||
for (const event of candidates) {
|
||||
roll -= (event.weight || 0);
|
||||
if (roll <= 0) return event;
|
||||
}
|
||||
return candidates[candidates.length - 1];
|
||||
}
|
||||
|
||||
export function getAvailableChoices(event, state) {
|
||||
if (!event || !event.choices) return [];
|
||||
return event.choices.filter(choice => {
|
||||
if (!choice.requirements) return true;
|
||||
return evaluateCondition(choice.requirements, state);
|
||||
});
|
||||
}
|
||||
|
||||
export function executeEvent(event, state, applyEffectFn) {
|
||||
if (!event || !event.effects) return;
|
||||
if (!state.triggeredEvents) state.triggeredEvents = new Set();
|
||||
|
||||
for (const effect of event.effects) {
|
||||
applyEffectFn(effect, state);
|
||||
}
|
||||
state.triggeredEvents.add(event.id);
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
import {
|
||||
setEventConfig,
|
||||
getEventConfig,
|
||||
checkAgeEvents,
|
||||
pickRandomEvent,
|
||||
getAvailableChoices,
|
||||
executeEvent,
|
||||
} from '../src/engine/eventEngine.js';
|
||||
import { evaluateCondition } from '../src/engine/conditionEvaluator.js';
|
||||
import { applyEffect } from '../src/engine/effectApplier.js';
|
||||
|
||||
// ==================== 测试数据 ====================
|
||||
|
||||
const testConfig = {
|
||||
events: [
|
||||
{
|
||||
id: 'daily_training',
|
||||
name: '日常修炼',
|
||||
trigger: { type: 'random', pool: 'normal' },
|
||||
weight: 50,
|
||||
effects: [{ type: 'addStat', stat: '体质', value: 1 }],
|
||||
},
|
||||
{
|
||||
id: 'study_hard',
|
||||
name: '刻苦学习',
|
||||
trigger: { type: 'random', pool: 'normal' },
|
||||
weight: 50,
|
||||
effects: [{ type: 'addStat', stat: '悟性', value: 1 }],
|
||||
},
|
||||
{
|
||||
id: 'invasion_military_40',
|
||||
name: '蛮族入侵',
|
||||
trigger: { type: 'age', value: 40 },
|
||||
isChoice: true,
|
||||
choices: [
|
||||
{
|
||||
id: 'resist',
|
||||
text: '率军抵抗',
|
||||
requirements: {
|
||||
type: 'careerLevel',
|
||||
careerId: 'soldier',
|
||||
op: '>=',
|
||||
value: 50,
|
||||
},
|
||||
effects: [{ type: 'addLog', text: '你率领大军击退了蛮族入侵!' }],
|
||||
},
|
||||
{
|
||||
id: 'flee',
|
||||
text: '弃城逃跑',
|
||||
effects: [{ type: 'addLog', text: '你选择了弃城逃跑。' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'invasion_spiritual_45',
|
||||
name: '天魔降世',
|
||||
trigger: { type: 'age', value: 45 },
|
||||
isChoice: true,
|
||||
choices: [
|
||||
{
|
||||
id: 'suppress',
|
||||
text: '出手镇压',
|
||||
requirements: {
|
||||
type: 'careerLevel',
|
||||
careerId: 'taoist',
|
||||
op: '>=',
|
||||
value: 50,
|
||||
},
|
||||
effects: [{ type: 'addLog', text: '你以无上道法镇压天魔!' }],
|
||||
},
|
||||
{
|
||||
id: 'hide',
|
||||
text: '躲藏起来',
|
||||
effects: [{ type: 'addLog', text: '你选择了躲藏起来。' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'invasion_political_50',
|
||||
name: '王朝崩坏',
|
||||
trigger: { type: 'age', value: 50 },
|
||||
isChoice: true,
|
||||
choices: [
|
||||
{
|
||||
id: 'assist',
|
||||
text: '辅佐明君',
|
||||
requirements: {
|
||||
type: 'stat',
|
||||
stat: '智力',
|
||||
op: '>=',
|
||||
value: 50,
|
||||
},
|
||||
effects: [{ type: 'addLog', text: '你以智谋辅佐明君!' }],
|
||||
},
|
||||
{
|
||||
id: 'selfish',
|
||||
text: '独善其身',
|
||||
effects: [{ type: 'addLog', text: '你选择了独善其身。' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'final_crisis_60',
|
||||
name: '最终危机',
|
||||
trigger: { type: 'age', value: 60 },
|
||||
isChoice: true,
|
||||
choices: [
|
||||
{
|
||||
id: 'general',
|
||||
text: '以将军之力平定天下',
|
||||
requirements: {
|
||||
type: 'careerLevel',
|
||||
careerId: 'soldier',
|
||||
op: '>=',
|
||||
value: 50,
|
||||
},
|
||||
effects: [{ type: 'addLog', text: '你以将军之力平定天下!' }],
|
||||
},
|
||||
{
|
||||
id: 'chancellor',
|
||||
text: '以宰相之智治理国家',
|
||||
requirements: {
|
||||
type: 'stat',
|
||||
stat: '智力',
|
||||
op: '>=',
|
||||
value: 50,
|
||||
},
|
||||
effects: [{ type: 'addLog', text: '你以宰相之智治理国家!' }],
|
||||
},
|
||||
{
|
||||
id: 'cultivator',
|
||||
text: '以修仙之道超脱轮回',
|
||||
requirements: {
|
||||
type: 'careerLevel',
|
||||
careerId: 'taoist',
|
||||
op: '>=',
|
||||
value: 50,
|
||||
},
|
||||
effects: [{ type: 'addLog', text: '你以修仙之道超脱轮回!' }],
|
||||
},
|
||||
{
|
||||
id: 'powerless',
|
||||
text: '无力回天',
|
||||
effects: [{ type: 'addLog', text: '你感到无力回天。' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// ==================== 测试辅助函数 ====================
|
||||
|
||||
function assertEqual(actual, expected, message) {
|
||||
const actualStr = JSON.stringify(actual);
|
||||
const expectedStr = JSON.stringify(expected);
|
||||
if (actualStr !== expectedStr) {
|
||||
throw new Error(
|
||||
`${message}\n 期望: ${expectedStr}\n 实际: ${actualStr}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertTrue(value, message) {
|
||||
if (!value) {
|
||||
throw new Error(message || `期望为 true,实际为 ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertFalse(value, message) {
|
||||
if (value) {
|
||||
throw new Error(message || `期望为 false,实际为 ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 测试用例 ====================
|
||||
|
||||
function testSetAndGetEventConfig() {
|
||||
setEventConfig(testConfig);
|
||||
const config = getEventConfig();
|
||||
assertEqual(config.events.length, 6, '配置应有6个事件');
|
||||
}
|
||||
|
||||
function testCheckAgeEvents() {
|
||||
// 测试40岁触发入侵事件
|
||||
const state = {
|
||||
age: 40,
|
||||
triggeredEvents: new Set(),
|
||||
};
|
||||
|
||||
const triggered = checkAgeEvents(testConfig, state);
|
||||
assertEqual(triggered.length, 1, '40岁应触发1个事件');
|
||||
assertEqual(triggered[0].id, 'invasion_military_40', '40岁应触发蛮族入侵事件');
|
||||
assertTrue(
|
||||
state.triggeredEvents.has('invasion_military_40'),
|
||||
'事件ID应加入triggeredEvents'
|
||||
);
|
||||
|
||||
// 再次检查,不应重复触发
|
||||
const triggeredAgain = checkAgeEvents(testConfig, state);
|
||||
assertEqual(triggeredAgain.length, 0, '已触发的事件不应重复触发');
|
||||
|
||||
// 测试45岁触发
|
||||
const state45 = {
|
||||
age: 45,
|
||||
triggeredEvents: new Set(),
|
||||
};
|
||||
const triggered45 = checkAgeEvents(testConfig, state45);
|
||||
assertEqual(triggered45.length, 1, '45岁应触发1个事件');
|
||||
assertEqual(triggered45[0].id, 'invasion_spiritual_45', '45岁应触发天魔降世事件');
|
||||
|
||||
// 测试50岁触发
|
||||
const state50 = {
|
||||
age: 50,
|
||||
triggeredEvents: new Set(),
|
||||
};
|
||||
const triggered50 = checkAgeEvents(testConfig, state50);
|
||||
assertEqual(triggered50.length, 1, '50岁应触发1个事件');
|
||||
assertEqual(triggered50[0].id, 'invasion_political_50', '50岁应触发王朝崩坏事件');
|
||||
|
||||
// 测试60岁触发
|
||||
const state60 = {
|
||||
age: 60,
|
||||
triggeredEvents: new Set(),
|
||||
};
|
||||
const triggered60 = checkAgeEvents(testConfig, state60);
|
||||
assertEqual(triggered60.length, 1, '60岁应触发1个事件');
|
||||
assertEqual(triggered60[0].id, 'final_crisis_60', '60岁应触发最终危机事件');
|
||||
|
||||
// 测试非触发年龄
|
||||
const state30 = {
|
||||
age: 30,
|
||||
triggeredEvents: new Set(),
|
||||
};
|
||||
const triggered30 = checkAgeEvents(testConfig, state30);
|
||||
assertEqual(triggered30.length, 0, '30岁不应触发任何事件');
|
||||
}
|
||||
|
||||
function testPickRandomEvent() {
|
||||
// 从normal池抽取
|
||||
const state = {
|
||||
triggeredEvents: new Set(),
|
||||
};
|
||||
|
||||
const event = pickRandomEvent(testConfig, state, 'normal');
|
||||
assertTrue(event !== null, '应从normal池抽到一个事件');
|
||||
assertTrue(
|
||||
event.id === 'daily_training' || event.id === 'study_hard',
|
||||
'抽到的事件应在normal池中'
|
||||
);
|
||||
|
||||
// 测试空池
|
||||
const emptyEvent = pickRandomEvent(testConfig, state, 'nonexistent');
|
||||
assertEqual(emptyEvent, null, '不存在的池应返回null');
|
||||
|
||||
// 测试所有事件都被触发后
|
||||
state.triggeredEvents.add('daily_training');
|
||||
state.triggeredEvents.add('study_hard');
|
||||
const noEvent = pickRandomEvent(testConfig, state, 'normal');
|
||||
assertEqual(noEvent, null, '所有事件触发后应返回null');
|
||||
}
|
||||
|
||||
function testGetAvailableChoices() {
|
||||
// 测试无requirement的选项始终可用
|
||||
const event = testConfig.events.find(e => e.id === 'invasion_military_40');
|
||||
const stateNoSoldier = {
|
||||
careers: {},
|
||||
};
|
||||
const choicesNoSoldier = getAvailableChoices(event, stateNoSoldier);
|
||||
assertEqual(choicesNoSoldier.length, 1, '无士兵职业时应有1个可用选项');
|
||||
assertEqual(choicesNoSoldier[0].id, 'flee', '可用选项应为逃跑');
|
||||
|
||||
// 测试满足requirement
|
||||
const stateWithSoldier = {
|
||||
careers: {
|
||||
soldier: { level: 50 },
|
||||
},
|
||||
};
|
||||
const choicesWithSoldier = getAvailableChoices(event, stateWithSoldier);
|
||||
assertEqual(choicesWithSoldier.length, 2, '有士兵50级时应有2个可用选项');
|
||||
|
||||
// 测试最终危机事件
|
||||
const finalEvent = testConfig.events.find(e => e.id === 'final_crisis_60');
|
||||
const stateAll = {
|
||||
careers: {
|
||||
soldier: { level: 50 },
|
||||
taoist: { level: 50 },
|
||||
},
|
||||
stats: {
|
||||
智力: 50,
|
||||
},
|
||||
};
|
||||
const allChoices = getAvailableChoices(finalEvent, stateAll);
|
||||
assertEqual(allChoices.length, 4, '满足所有条件时应有4个可用选项');
|
||||
|
||||
const stateNone = {
|
||||
careers: {},
|
||||
stats: {},
|
||||
};
|
||||
const noChoices = getAvailableChoices(finalEvent, stateNone);
|
||||
assertEqual(noChoices.length, 1, '不满足任何条件时应有1个可用选项(无力回天)');
|
||||
assertEqual(noChoices[0].id, 'powerless', '唯一可用选项应为无力回天');
|
||||
}
|
||||
|
||||
function testExecuteEvent() {
|
||||
const event = testConfig.events.find(e => e.id === 'daily_training');
|
||||
const state = {
|
||||
stats: {},
|
||||
triggeredEvents: new Set(),
|
||||
};
|
||||
|
||||
executeEvent(event, state, applyEffect);
|
||||
assertEqual(state.stats['体质'], 1, '执行事件后体质应增加1');
|
||||
assertTrue(
|
||||
state.triggeredEvents.has('daily_training'),
|
||||
'事件ID应加入triggeredEvents'
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 运行测试 ====================
|
||||
|
||||
function runTests() {
|
||||
const tests = [
|
||||
{ name: 'testSetAndGetEventConfig', fn: testSetAndGetEventConfig },
|
||||
{ name: 'testCheckAgeEvents', fn: testCheckAgeEvents },
|
||||
{ name: 'testPickRandomEvent', fn: testPickRandomEvent },
|
||||
{ name: 'testGetAvailableChoices', fn: testGetAvailableChoices },
|
||||
{ name: 'testExecuteEvent', fn: testExecuteEvent },
|
||||
];
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const test of tests) {
|
||||
try {
|
||||
test.fn();
|
||||
console.log(` PASS: ${test.name}`);
|
||||
passed++;
|
||||
} catch (err) {
|
||||
console.log(` FAIL: ${test.name}`);
|
||||
console.log(` ${err.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n${passed} passed, ${failed} failed`);
|
||||
|
||||
if (failed === 0) {
|
||||
console.log('All eventEngine tests PASS');
|
||||
process.exit(0);
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
runTests();
|
||||
Reference in New Issue
Block a user