feat: add effect applier with all core effect types

This commit is contained in:
2026-05-13 03:29:32 +00:00
parent 8ff8e8f522
commit b09683e7f4
2 changed files with 326 additions and 0 deletions
+126
View File
@@ -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;
}
}
+200
View File
@@ -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');