diff --git a/src/engine/effectApplier.js b/src/engine/effectApplier.js new file mode 100644 index 0000000..c80e179 --- /dev/null +++ b/src/engine/effectApplier.js @@ -0,0 +1,126 @@ +// ==================== 效果执行器 ==================== + +export function applyEffect(effect, state) { + if (!effect || typeof effect !== 'object') return; + + const { type } = effect; + + switch (type) { + case 'addMoney': { + if (typeof effect.value === 'number') { + state.money = (state.money || 0) + effect.value; + } + break; + } + + case 'addStat': { + const stat = effect.stat; + if (stat && typeof effect.value === 'number') { + if (!state.stats) state.stats = {}; + state.stats[stat] = (state.stats[stat] || 0) + effect.value; + } + break; + } + + case 'setStat': { + const stat = effect.stat; + if (stat && typeof effect.value === 'number') { + if (!state.stats) state.stats = {}; + state.stats[stat] = effect.value; + } + break; + } + + case 'setFlag': { + const flag = effect.flag; + if (flag !== undefined) { + if (!state.worldFlags) state.worldFlags = {}; + state.worldFlags[flag] = effect.value; + } + break; + } + + case 'unlockCareer': { + const careerId = effect.careerId; + if (careerId) { + if (!state.careers) state.careers = {}; + if (!state.careers[careerId]) { + state.careers[careerId] = { level: 0, exp: 0 }; + } + } + break; + } + + case 'addExp': { + const careerId = effect.careerId; + if (careerId && typeof effect.value === 'number') { + if (!state.careers) state.careers = {}; + if (!state.careers[careerId]) { + state.careers[careerId] = { level: 0, exp: 0 }; + } + state.careers[careerId].exp += effect.value; + } + break; + } + + case 'addItem': { + const itemId = effect.itemId; + if (itemId) { + if (!state.shopItems) state.shopItems = {}; + state.shopItems[itemId] = (state.shopItems[itemId] || 0) + 1; + } + break; + } + + case 'addBuff': { + const buffId = effect.buffId; + if (buffId) { + if (!state.activeBuffs) state.activeBuffs = {}; + const duration = effect.duration; + const expiresDay = duration === -1 ? -1 : (state.day || 0) + duration; + state.activeBuffs[buffId] = { expiresDay }; + } + break; + } + + case 'upgradeArtifact': { + const artifactId = effect.artifactId; + if (artifactId) { + if (!state.artifacts) state.artifacts = {}; + state.artifacts[artifactId] = (state.artifacts[artifactId] || 0) + 1; + } + break; + } + + case 'addLog': { + if (effect.text) { + if (!state.logs) state.logs = []; + const entry = { + day: state.day, + year: state.year, + age: state.age, + text: effect.text, + type: effect.logType || 'normal', + timestamp: Date.now(), + }; + state.logs.unshift(entry); + if (state.logs.length > 500) { + state.logs.pop(); + } + } + break; + } + + case 'die': { + state.alive = false; + if (effect.reason) { + state.deathReason = effect.reason; + } + break; + } + + default: + // Unknown effect type, ignore + break; + } +} diff --git a/test/effectApplier.test.js b/test/effectApplier.test.js new file mode 100644 index 0000000..274b811 --- /dev/null +++ b/test/effectApplier.test.js @@ -0,0 +1,200 @@ +import { applyEffect } from '../src/engine/effectApplier.js'; + +function assert(cond, msg) { + if (!cond) throw new Error(msg); +} + +function createState() { + return { + day: 5, + year: 1, + age: 10, + alive: true, + money: 0, + stats: { body: 0, wisdom: 0, charm: 0, destiny: 0, business: 0, intelligence: 0 }, + careers: {}, + shopItems: {}, + activeBuffs: {}, + artifacts: {}, + worldFlags: {}, + logs: [], + }; +} + +// === addMoney === +{ + const state = createState(); + applyEffect({ type: 'addMoney', value: 100 }, state); + assert(state.money === 100, 'addMoney should add to money'); +} +{ + const state = createState(); + state.money = 50; + applyEffect({ type: 'addMoney', value: 25 }, state); + assert(state.money === 75, 'addMoney should add to existing money'); +} + +// === addStat === +{ + const state = createState(); + applyEffect({ type: 'addStat', stat: 'body', value: 5 }, state); + assert(state.stats.body === 5, 'addStat should add to stat'); +} +{ + const state = createState(); + state.stats.body = 10; + applyEffect({ type: 'addStat', stat: 'body', value: 3 }, state); + assert(state.stats.body === 13, 'addStat should add to existing stat'); +} + +// === setStat === +{ + const state = createState(); + applyEffect({ type: 'setStat', stat: 'wisdom', value: 20 }, state); + assert(state.stats.wisdom === 20, 'setStat should set stat to value'); +} +{ + const state = createState(); + state.stats.wisdom = 50; + applyEffect({ type: 'setStat', stat: 'wisdom', value: 10 }, state); + assert(state.stats.wisdom === 10, 'setStat should overwrite existing stat'); +} + +// === setFlag === +{ + const state = createState(); + applyEffect({ type: 'setFlag', flag: 'met_taoist', value: true }, state); + assert(state.worldFlags.met_taoist === true, 'setFlag should set flag'); +} +{ + const state = createState(); + state.worldFlags.met_taoist = true; + applyEffect({ type: 'setFlag', flag: 'met_taoist', value: false }, state); + assert(state.worldFlags.met_taoist === false, 'setFlag should overwrite flag'); +} + +// === unlockCareer === +{ + const state = createState(); + applyEffect({ type: 'unlockCareer', careerId: 'student' }, state); + assert(state.careers.student !== undefined, 'unlockCareer should create career'); + assert(state.careers.student.level === 0, 'unlockCareer should set level to 0'); + assert(state.careers.student.exp === 0, 'unlockCareer should set exp to 0'); +} +{ + const state = createState(); + state.careers.student = { level: 5, exp: 100 }; + applyEffect({ type: 'unlockCareer', careerId: 'student' }, state); + assert(state.careers.student.level === 5, 'unlockCareer should not overwrite existing career'); + assert(state.careers.student.exp === 100, 'unlockCareer should not overwrite existing career exp'); +} + +// === addExp === +{ + const state = createState(); + applyEffect({ type: 'addExp', careerId: 'student', value: 50 }, state); + assert(state.careers.student !== undefined, 'addExp should create career if missing'); + assert(state.careers.student.exp === 50, 'addExp should add exp'); +} +{ + const state = createState(); + state.careers.student = { level: 1, exp: 10 }; + applyEffect({ type: 'addExp', careerId: 'student', value: 20 }, state); + assert(state.careers.student.exp === 30, 'addExp should add to existing exp'); +} + +// === addItem === +{ + const state = createState(); + applyEffect({ type: 'addItem', itemId: 'taoist_license' }, state); + assert(state.shopItems.taoist_license === 1, 'addItem should add item'); +} +{ + const state = createState(); + state.shopItems.taoist_license = 2; + applyEffect({ type: 'addItem', itemId: 'taoist_license' }, state); + assert(state.shopItems.taoist_license === 3, 'addItem should increment existing item'); +} + +// === addBuff === +{ + const state = createState(); + applyEffect({ type: 'addBuff', buffId: 'blessing', duration: 10 }, state); + assert(state.activeBuffs.blessing !== undefined, 'addBuff should add buff'); + assert(state.activeBuffs.blessing.expiresDay === 15, 'addBuff should calculate expiresDay'); +} +{ + const state = createState(); + applyEffect({ type: 'addBuff', buffId: 'curse', duration: -1 }, state); + assert(state.activeBuffs.curse !== undefined, 'addBuff should add permanent buff'); + assert(state.activeBuffs.curse.expiresDay === -1, 'addBuff should set expiresDay to -1 for permanent'); +} + +// === upgradeArtifact === +{ + const state = createState(); + applyEffect({ type: 'upgradeArtifact', artifactId: 'immortal_sword' }, state); + assert(state.artifacts.immortal_sword === 1, 'upgradeArtifact should add artifact'); +} +{ + const state = createState(); + state.artifacts.immortal_sword = 2; + applyEffect({ type: 'upgradeArtifact', artifactId: 'immortal_sword' }, state); + assert(state.artifacts.immortal_sword === 3, 'upgradeArtifact should increment existing artifact'); +} + +// === addLog === +{ + const state = createState(); + applyEffect({ type: 'addLog', text: 'Test log entry' }, state); + assert(state.logs.length === 1, 'addLog should add log entry'); + assert(state.logs[0].text === 'Test log entry', 'addLog should set text'); + assert(state.logs[0].day === 5, 'addLog should set day'); + assert(state.logs[0].year === 1, 'addLog should set year'); + assert(state.logs[0].age === 10, 'addLog should set age'); + assert(state.logs[0].type === 'normal', 'addLog should default type to normal'); + assert(typeof state.logs[0].timestamp === 'number', 'addLog should set timestamp'); +} +{ + const state = createState(); + applyEffect({ type: 'addLog', text: 'First' }, state); + applyEffect({ type: 'addLog', text: 'Second' }, state); + assert(state.logs.length === 2, 'addLog should add multiple entries'); + assert(state.logs[0].text === 'Second', 'addLog should add newest to front'); + assert(state.logs[1].text === 'First', 'addLog should keep older entries at back'); +} +{ + const state = createState(); + for (let i = 0; i < 510; i++) { + applyEffect({ type: 'addLog', text: `Log ${i}` }, state); + } + assert(state.logs.length === 500, 'addLog should cap at 500 entries'); + assert(state.logs[0].text === 'Log 509', 'addLog should keep newest entries'); +} + +// === die === +{ + const state = createState(); + applyEffect({ type: 'die', reason: 'old_age' }, state); + assert(state.alive === false, 'die should set alive to false'); + assert(state.deathReason === 'old_age', 'die should set deathReason'); +} + +// === edge cases === +{ + const state = createState(); + applyEffect(null, state); + assert(state.money === 0, 'null effect should not modify state'); +} +{ + const state = createState(); + applyEffect({}, state); + assert(state.money === 0, 'empty effect should not modify state'); +} +{ + const state = createState(); + applyEffect({ type: 'unknownEffect' }, state); + assert(state.money === 0, 'unknown effect should not modify state'); +} + +console.log('All effectApplier tests PASS');