From c78e4bba0b6ef0cd904837ba2f1fe9e575f4043c Mon Sep 17 00:00:00 2001 From: congsh <2452821485@qq.com> Date: Wed, 13 May 2026 03:31:49 +0000 Subject: [PATCH] feat: add state management with full serialization and rebirth reset --- src/engine/state.js | 125 ++++++++++++++++++++++++++++++++++++ test/state.test.js | 151 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 src/engine/state.js create mode 100644 test/state.test.js diff --git a/src/engine/state.js b/src/engine/state.js new file mode 100644 index 0000000..54440f4 --- /dev/null +++ b/src/engine/state.js @@ -0,0 +1,125 @@ +// ==================== 游戏状态管理 ==================== + +export function createInitialState() { + return { + // 时间 + day: 0, + year: 0, + age: 0, + reincarnation: 0, + + // 玩家状态 + alive: true, + identity: null, + stats: { body: 0, wisdom: 0, charm: 0, destiny: 0, business: 0, intelligence: 0 }, + talents: [], + careers: {}, + money: 0, + + // 商店与增益 + shopItems: {}, + activeBuffs: {}, + + // 神器与世界 + artifacts: {}, + worldFlags: {}, + npcs: {}, + + // 入侵事件 + invasionsTriggered: { military_40: false, spiritual_45: false, political_50: false, final_60: false }, + + // 突破路线 + breakthroughRoute: null, + + // 运行状态 + speed: 0, + paused: false, + logs: [], + triggeredEvents: new Set(), + + // 跨轮传承 + metaExp: {}, + memories: [], + unlockedEntries: new Set(), + history: [], + unlockedShopPool: new Set(), + }; +} + +export function resetForRebirth(state) { + // 保存本轮记录 + const record = { + reincarnation: state.reincarnation, + age: state.age, + day: state.day, + careers: { ...state.careers }, + identity: state.identity, + stats: { ...state.stats }, + money: state.money, + }; + state.history.push(record); + if (state.history.length > 50) { + state.history.shift(); + } + + // 保留跨轮数据 + const preserved = { + reincarnation: state.reincarnation + 1, + metaExp: state.metaExp, + memories: state.memories, + unlockedEntries: state.unlockedEntries, + history: state.history, + artifacts: state.artifacts, + unlockedShopPool: state.unlockedShopPool, + }; + + // 重置本轮数据 + state.day = 0; + state.year = 0; + state.age = 0; + state.alive = true; + state.careers = {}; + state.money = 0; + state.shopItems = {}; + state.activeBuffs = {}; + state.worldFlags = {}; + state.npcs = {}; + state.logs = []; + state.triggeredEvents = new Set(); + state.identity = null; + state.stats = { body: 0, wisdom: 0, charm: 0, destiny: 0, business: 0, intelligence: 0 }; + state.talents = []; + state.invasionsTriggered = { military_40: false, spiritual_45: false, political_50: false, final_60: false }; + state.breakthroughRoute = null; + state.speed = 0; + state.paused = false; + + // 恢复跨轮数据 + state.reincarnation = preserved.reincarnation; + state.metaExp = preserved.metaExp; + state.memories = preserved.memories; + state.unlockedEntries = preserved.unlockedEntries; + state.history = preserved.history; + state.artifacts = preserved.artifacts; + state.unlockedShopPool = preserved.unlockedShopPool; + + return state; +} + +export function serializeState(state) { + return { + ...state, + triggeredEvents: Array.from(state.triggeredEvents), + unlockedEntries: Array.from(state.unlockedEntries), + unlockedShopPool: Array.from(state.unlockedShopPool), + }; +} + +export function deserializeState(data, target) { + Object.assign(target, data, { + triggeredEvents: new Set(data.triggeredEvents || []), + unlockedEntries: new Set(data.unlockedEntries || []), + unlockedShopPool: new Set(data.unlockedShopPool || []), + }); + return target; +} diff --git a/test/state.test.js b/test/state.test.js new file mode 100644 index 0000000..04f1cb6 --- /dev/null +++ b/test/state.test.js @@ -0,0 +1,151 @@ +// ==================== 状态管理测试 ==================== + +import { createInitialState, resetForRebirth, serializeState, deserializeState } from '../src/engine/state.js'; + +let passCount = 0; +let failCount = 0; + +function assert(condition, message) { + if (condition) { + passCount++; + } else { + failCount++; + console.error('FAIL:', message); + } +} + +function testCreateInitialState() { + const state = createInitialState(); + + assert(state.day === 0, 'day should be 0'); + assert(state.year === 0, 'year should be 0'); + assert(state.age === 0, 'age should be 0'); + assert(state.reincarnation === 0, 'reincarnation should be 0'); + assert(state.alive === true, 'alive should be true'); + assert(state.identity === null, 'identity should be null'); + + assert(state.stats.body === 0, 'stats.body should be 0'); + assert(state.stats.wisdom === 0, 'stats.wisdom should be 0'); + assert(state.stats.charm === 0, 'stats.charm should be 0'); + assert(state.stats.destiny === 0, 'stats.destiny should be 0'); + assert(state.stats.business === 0, 'stats.business should be 0'); + assert(state.stats.intelligence === 0, 'stats.intelligence should be 0'); + + assert(Array.isArray(state.talents) && state.talents.length === 0, 'talents should be empty array'); + assert(Object.keys(state.careers).length === 0, 'careers should be empty object'); + assert(state.money === 0, 'money should be 0'); + assert(Object.keys(state.shopItems).length === 0, 'shopItems should be empty object'); + assert(Object.keys(state.activeBuffs).length === 0, 'activeBuffs should be empty object'); + assert(Object.keys(state.artifacts).length === 0, 'artifacts should be empty object'); + assert(Object.keys(state.worldFlags).length === 0, 'worldFlags should be empty object'); + assert(Object.keys(state.npcs).length === 0, 'npcs should be empty object'); + + assert(state.invasionsTriggered.military_40 === false, 'invasionsTriggered.military_40 should be false'); + assert(state.invasionsTriggered.spiritual_45 === false, 'invasionsTriggered.spiritual_45 should be false'); + assert(state.invasionsTriggered.political_50 === false, 'invasionsTriggered.political_50 should be false'); + assert(state.invasionsTriggered.final_60 === false, 'invasionsTriggered.final_60 should be false'); + + assert(state.breakthroughRoute === null, 'breakthroughRoute should be null'); + assert(state.speed === 0, 'speed should be 0'); + assert(state.paused === false, 'paused should be false'); + assert(Array.isArray(state.logs) && state.logs.length === 0, 'logs should be empty array'); + assert(state.triggeredEvents instanceof Set, 'triggeredEvents should be a Set'); + assert(Object.keys(state.metaExp).length === 0, 'metaExp should be empty object'); + assert(Array.isArray(state.memories) && state.memories.length === 0, 'memories should be empty array'); + assert(state.unlockedEntries instanceof Set, 'unlockedEntries should be a Set'); + assert(Array.isArray(state.history) && state.history.length === 0, 'history should be empty array'); + assert(state.unlockedShopPool instanceof Set, 'unlockedShopPool should be a Set'); +} + +function testResetForRebirth() { + const state = createInitialState(); + + // 模拟一轮游戏数据 + state.reincarnation = 2; + state.age = 30; + state.day = 100; + state.money = 5000; + state.careers = { merchant: { level: 3 } }; + state.identity = 'merchant'; + state.stats = { body: 10, wisdom: 15, charm: 8, destiny: 5, business: 20, intelligence: 12 }; + state.artifacts = { ringOfLuck: true }; + state.metaExp = { merchant: 100 }; + state.memories = ['memory1']; + state.unlockedEntries.add('entry1'); + state.unlockedShopPool.add('item1'); + state.history = [{ reincarnation: 0, age: 20 }]; + + resetForRebirth(state); + + // 验证重置 + assert(state.age === 0, 'age should reset to 0'); + assert(state.day === 0, 'day should reset to 0'); + assert(state.year === 0, 'year should reset to 0'); + assert(state.alive === true, 'alive should reset to true'); + assert(state.money === 0, 'money should reset to 0'); + assert(Object.keys(state.careers).length === 0, 'careers should reset to empty'); + assert(state.identity === null, 'identity should reset to null'); + assert(state.stats.body === 0, 'stats.body should reset to 0'); + assert(state.reincarnation === 3, 'reincarnation should increment by 1'); + + // 验证保留 + assert(state.artifacts.ringOfLuck === true, 'artifacts should be preserved'); + assert(state.metaExp.merchant === 100, 'metaExp should be preserved'); + assert(state.memories.length === 1, 'memories should be preserved'); + assert(state.unlockedEntries.has('entry1'), 'unlockedEntries should be preserved'); + assert(state.unlockedShopPool.has('item1'), 'unlockedShopPool should be preserved'); + assert(state.history.length === 2, 'history should have new record appended'); + assert(state.history[state.history.length - 1].reincarnation === 2, 'new history record should have correct reincarnation'); + assert(state.history[state.history.length - 1].age === 30, 'new history record should have correct age'); + assert(state.history[state.history.length - 1].money === 5000, 'new history record should have correct money'); +} + +function testSerializeDeserialize() { + const state = createInitialState(); + state.triggeredEvents.add('event1'); + state.triggeredEvents.add('event2'); + state.unlockedEntries.add('entry1'); + state.unlockedShopPool.add('item1'); + state.unlockedShopPool.add('item2'); + state.age = 25; + state.money = 1000; + + const serialized = serializeState(state); + + // 验证序列化后的 Set 变为 Array + assert(Array.isArray(serialized.triggeredEvents), 'serialized triggeredEvents should be Array'); + assert(serialized.triggeredEvents.includes('event1'), 'serialized triggeredEvents should contain event1'); + assert(Array.isArray(serialized.unlockedEntries), 'serialized unlockedEntries should be Array'); + assert(Array.isArray(serialized.unlockedShopPool), 'serialized unlockedShopPool should be Array'); + assert(serialized.unlockedShopPool.includes('item2'), 'serialized unlockedShopPool should contain item2'); + + // 验证 JSON 可序列化 + const jsonStr = JSON.stringify(serialized); + const parsed = JSON.parse(jsonStr); + + // 反序列化到新对象 + const target = createInitialState(); + deserializeState(parsed, target); + + assert(target.triggeredEvents instanceof Set, 'deserialized triggeredEvents should be Set'); + assert(target.triggeredEvents.has('event1'), 'deserialized triggeredEvents should have event1'); + assert(target.triggeredEvents.has('event2'), 'deserialized triggeredEvents should have event2'); + assert(target.unlockedEntries instanceof Set, 'deserialized unlockedEntries should be Set'); + assert(target.unlockedEntries.has('entry1'), 'deserialized unlockedEntries should have entry1'); + assert(target.unlockedShopPool instanceof Set, 'deserialized unlockedShopPool should be Set'); + assert(target.unlockedShopPool.has('item1'), 'deserialized unlockedShopPool should have item1'); + assert(target.age === 25, 'deserialized age should be 25'); + assert(target.money === 1000, 'deserialized money should be 1000'); +} + +// 运行测试 +testCreateInitialState(); +testResetForRebirth(); +testSerializeDeserialize(); + +if (failCount === 0) { + console.log('All state tests PASS'); +} else { + console.log(`${passCount} passed, ${failCount} failed`); + process.exit(1); +}