feat(v0.4): 回响编年史 — 跨周目叙事时间轴系统

- 新增 ChronicleEntry / RunStartSnapshot 类型与 GameState 字段
- 新建 chronicle.ts: 5 纪元名生成、模板化叙事、里程碑检测、run delta 计算
- engine.ts performPrestige 构建编年史条目并重置 runStart
- gameStore: init 兼容旧存档 + tickTide 记录星潮 + resolveCurrentNode 计 BOSS 击杀
- chooseConstellationPerk 回填最近一条 entry 的 perksThisAscension
- 新建 ChronicleDialog.tsx: 时间轴 UI + 5 纪元循环色 + 入场动画
- 新增 6 项成就: 首部编年/三纪元回响/五纪元闭环/首杀维度/维度猎手/星潮亲历者
- 新增 chronicle / chronicleOpen 程序化音效
- 修复关键 BUG: PrestigeDialog disabled 逻辑错误导致玩家永远无法飞升
- 修复 UX: newBp=0 时不再永久禁用飞升按钮(v0.3.1 星图+v0.4 编年史已提供动机)
- 统计面板新增 BOSS 击破/星潮亲历/编年史条目 3 行
- 版本号升至 v0.4
This commit is contained in:
2026-06-23 14:45:38 +00:00
parent 98fbbf1634
commit e69c48ba69
11 changed files with 993 additions and 7 deletions
+36 -1
View File
@@ -55,6 +55,10 @@ import {
constellationBonuses,
rollPerkChoices,
} from "@/lib/game/constellation";
import {
migrateChronicleFields,
withPerks,
} from "@/lib/game/chronicle";
interface GameActions {
// 生命周期
@@ -177,6 +181,8 @@ export const useGameStore = create<Store>()(
const activeTide = s.activeTide ?? null;
const constellation = s.constellation ?? [];
const pendingPerkChoices = s.pendingPerkChoices ?? null;
// v0.4 编年史兼容:补全 chronicle / runStart / bossKills / starTidesEncountered
const migrated = migrateChronicleFields(s);
// 星图「能量共振」天赋 +1 能量上限
const cm = constellationBonuses(constellation);
const energyMax = (INITIAL_STATE.energyMax) + cm.energyMaxBonus;
@@ -200,10 +206,14 @@ export const useGameStore = create<Store>()(
constellation,
pendingPerkChoices,
energyMax,
chronicle: migrated.chronicle,
runStart: migrated.runStart,
bossKills: migrated.bossKills,
starTidesEncountered: migrated.starTidesEncountered,
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements, activeTide: tide, constellation }),
});
} else {
set({ lastTick: now, activePuzzle, achievements, activeTide: tide, lastTideEnd: tide ? lastTideEndRaw : lastTideEndRaw, constellation, pendingPerkChoices, energyMax, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements, activeTide: tide, constellation }) });
set({ lastTick: now, activePuzzle, achievements, activeTide: tide, lastTideEnd: tide ? lastTideEndRaw : lastTideEndRaw, constellation, pendingPerkChoices, energyMax, chronicle: migrated.chronicle, runStart: migrated.runStart, bossKills: migrated.bossKills, starTidesEncountered: migrated.starTidesEncountered, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements, activeTide: tide, constellation }) });
}
},
@@ -258,8 +268,13 @@ export const useGameStore = create<Store>()(
endsAt: now + TIDE_CONFIG.duration,
};
const event: { started?: TideType; ended?: TideType; silenceCompensation?: number } = { started: type };
// v0.4 编年史:累计遇到的星潮 ID(去重)
const tidesAll = s.starTidesEncountered ?? [];
const tideId = `tide_${type}`;
const newTidesAll = tidesAll.includes(tideId) ? tidesAll : [...tidesAll, tideId];
set({
activeTide: newTide,
starTidesEncountered: newTidesAll,
// 星潮开始后重算 stats(应用 contactRate/insight 修饰)
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: newTide, constellation: s.constellation }),
_tideEvents: [...s._tideEvents, event],
@@ -556,6 +571,16 @@ export const useGameStore = create<Store>()(
exp.finished = true;
}
// v0.4 编年史:击破 BOSS 时累计计数
let bossKills = s.bossKills ?? 0;
if (
result.ended &&
result.endReason === "victory" &&
exp.nodes[exp.currentNode]?.type === "boss"
) {
bossKills = bossKills + 1;
}
set({
activeExpedition: exp,
crystals: newCrystals,
@@ -563,6 +588,7 @@ export const useGameStore = create<Store>()(
contact: newContact,
fragments: newFragments,
expeditionLog: newLog,
bossKills,
});
return result;
},
@@ -626,10 +652,19 @@ export const useGameStore = create<Store>()(
const newConstellation = [...(s.constellation ?? []), perkId];
const cm = constellationBonuses(newConstellation);
const energyMax = INITIAL_STATE.energyMax + cm.energyMaxBonus;
// v0.4 编年史:回填最近一条 entry 的 perksThisAscension
const chronicle = s.chronicle ?? [];
let newChronicle = chronicle;
if (chronicle.length > 0) {
const lastEntry = chronicle[chronicle.length - 1];
const updatedLast = withPerks(lastEntry, [perkId]);
newChronicle = [...chronicle.slice(0, -1), updatedLast];
}
set({
constellation: newConstellation,
pendingPerkChoices: null,
energyMax,
chronicle: newChronicle,
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: newConstellation }),
});
return true;