From e69c48ba6921f8d8fcb0d81b6db1e6eb7fdeacb7 Mon Sep 17 00:00:00 2001
From: Super_Z <1401203083@qq.com>
Date: Tue, 23 Jun 2026 14:45:38 +0000
Subject: [PATCH] =?UTF-8?q?feat(v0.4):=20=E5=9B=9E=E5=93=8D=E7=BC=96?=
=?UTF-8?q?=E5=B9=B4=E5=8F=B2=20=E2=80=94=20=E8=B7=A8=E5=91=A8=E7=9B=AE?=
=?UTF-8?q?=E5=8F=99=E4=BA=8B=E6=97=B6=E9=97=B4=E8=BD=B4=E7=B3=BB=E7=BB=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 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
---
docs/repo | 2 +-
src/app/page.tsx | 24 +-
src/components/game/ChronicleDialog.tsx | 464 ++++++++++++++++++++++++
src/components/game/PrestigeDialog.tsx | 29 +-
src/lib/game/achievements.ts | 60 +++
src/lib/game/audio.ts | 18 +
src/lib/game/chronicle.ts | 293 +++++++++++++++
src/lib/game/config.ts | 11 +
src/lib/game/engine.ts | 15 +
src/lib/game/types.ts | 47 +++
src/store/gameStore.ts | 37 +-
11 files changed, 993 insertions(+), 7 deletions(-)
create mode 100644 src/components/game/ChronicleDialog.tsx
create mode 100644 src/lib/game/chronicle.ts
diff --git a/docs/repo b/docs/repo
index 433c19cc7..805e7b8b5 160000
--- a/docs/repo
+++ b/docs/repo
@@ -1 +1 @@
-Subproject commit 433c19cc7350885278263f8a2a11ab251e86a455
+Subproject commit 805e7b8b5339d3006a3143bf18c673bd80e5853e
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 6b537fc33..abebfd79f 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -14,6 +14,7 @@ import { AchievementsPanel } from "@/components/game/AchievementsPanel";
import { AchievementNotifier } from "@/components/game/AchievementNotifier";
import { ConstellationPanel } from "@/components/game/ConstellationPanel";
import { ConstellationDialog } from "@/components/game/ConstellationDialog";
+import { ChronicleDialog } from "@/components/game/ChronicleDialog";
import {
StarTideNotifier,
StarTideIndicator,
@@ -47,6 +48,7 @@ export default function Page() {
const [prestigeOpen, setPrestigeOpen] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
const [constellationOpen, setConstellationOpen] = useState(false);
+ const [chronicleOpen, setChronicleOpen] = useState(false);
const [mounted, setMounted] = useState(false);
const contact = useGameStore((s) => s.contact);
@@ -63,6 +65,7 @@ export default function Page() {
const activeTide = useGameStore((s) => s.activeTide);
const energy = useGameStore((s) => s.energy);
const hasActiveExpedition = useGameStore((s) => !!s.activeExpedition && !s.activeExpedition.finished);
+ const chronicleCount = useGameStore((s) => s.chronicle?.length ?? 0);
// 挂载检测:避免持久化 store 在 SSR/CSR 间产生 hydration 不一致
useEffect(() => {
@@ -135,12 +138,27 @@ export default function Page() {
回响星核
- ECHO NEXUS · v0.3.1
+ ECHO NEXUS · v0.4
+
{hasPendingPerk && (
+
+ {/* 编年史铭刻提示(v0.4 新增) */}
+
+
+
+ 本次飞升将铭刻入回响编年史,
+ 记录本周目的解码、探险、星潮与觉醒天赋,形成永恒叙事时间轴。
+
+
@@ -118,26 +128,37 @@ export function PrestigeDialog({
取消
diff --git a/src/lib/game/achievements.ts b/src/lib/game/achievements.ts
index edc759917..1ce147819 100644
--- a/src/lib/game/achievements.ts
+++ b/src/lib/game/achievements.ts
@@ -191,6 +191,66 @@ export const ACHIEVEMENTS: Achievement[] = [
reward: { crystalsPerSecPct: 12, insightPct: 12 },
rewardText: "产能 +12% · 洞见 +12%",
},
+ {
+ id: "ach_chronicle_1",
+ name: "首部编年",
+ desc: "完成首次飞升,开启编年史第一章",
+ icon: "📖",
+ color: "#e879f9",
+ check: (s) => (s.chronicle?.length ?? 0) >= 1,
+ reward: { insights: 40, crystalsPerSecPct: 6 },
+ rewardText: "+40 洞见 · 产能 +6%",
+ },
+ {
+ id: "ach_chronicle_3",
+ name: "三纪元回响",
+ desc: "完成 3 次飞升,编年史铭刻三章",
+ icon: "📜",
+ color: "#fbbf24",
+ check: (s) => (s.chronicle?.length ?? 0) >= 3,
+ reward: { crystalsPerSecPct: 10, insightPct: 10 },
+ rewardText: "产能 +10% · 洞见 +10%",
+ },
+ {
+ id: "ach_chronicle_5",
+ name: "五纪元闭环",
+ desc: "完成 5 次飞升,走完一轮纪元循环",
+ icon: "♾",
+ color: "#fcd34d",
+ check: (s) => (s.chronicle?.length ?? 0) >= 5,
+ reward: { crystalsPerSecPct: 15, insightPct: 15 },
+ rewardText: "产能 +15% · 洞见 +15%",
+ },
+ {
+ id: "ach_boss_1",
+ name: "首杀维度",
+ desc: "在遗迹探险中击破首个 BOSS",
+ icon: "⚔",
+ color: "#fb7185",
+ check: (s) => (s.bossKills ?? 0) >= 1,
+ reward: { insights: 25, crystalsPerSecPct: 4 },
+ rewardText: "+25 洞见 · 产能 +4%",
+ },
+ {
+ id: "ach_boss_5",
+ name: "维度猎手",
+ desc: "累计击破 5 个 BOSS",
+ icon: "⛧",
+ color: "#e879f9",
+ check: (s) => (s.bossKills ?? 0) >= 5,
+ reward: { crystalsPerSecPct: 10, insightPct: 8 },
+ rewardText: "产能 +10% · 洞见 +8%",
+ },
+ {
+ id: "ach_tides_all",
+ name: "星潮亲历者",
+ desc: "经历全部 6 种星潮事件",
+ icon: "🌊",
+ color: "#34d399",
+ check: (s) => (s.starTidesEncountered?.length ?? 0) >= 6,
+ reward: { crystalsPerSecPct: 8, insightPct: 8 },
+ rewardText: "产能 +8% · 洞见 +8%",
+ },
];
/** 计算成就提供的永久加成(跨周目保留) */
diff --git a/src/lib/game/audio.ts b/src/lib/game/audio.ts
index 76d9bf2e3..d6bb5284e 100644
--- a/src/lib/game/audio.ts
+++ b/src/lib/game/audio.ts
@@ -20,6 +20,8 @@ type SfxName =
| "tideStart" // 星潮降临(神秘扫频)
| "tideEnd" // 星潮结束(柔和消退)
| "constellation" // 星图觉醒(空灵琶音 + 高频闪光)
+ | "chronicle" // 编年史铭刻(深沉钟声 + 回响泛音)
+ | "chronicleOpen" // 打开编年史(轻柔翻页声)
| "uiHover" // 界面悬停(极轻)
| "uiClick"; // 界面点击(轻确认)
@@ -285,6 +287,22 @@ class AudioEngine {
this.tone(660, 0.07, "sine", 0.1);
break;
}
+ case "chronicle": {
+ // 编年史铭刻:深沉钟声 + 多层回响泛音 + 低频稳固
+ this.tone(196, 0.6, "sine", 0.18, 0); // G3 基音
+ this.tone(261.63, 0.55, "sine", 0.14, 0.05); // C4
+ this.tone(392, 0.5, "sine", 0.1, 0.1); // G4
+ this.tone(523.25, 0.4, "triangle", 0.08, 0.18); // C5
+ this.tone(783.99, 0.3, "sine", 0.05, 0.25); // G5 泛音
+ break;
+ }
+ case "chronicleOpen": {
+ // 翻页声:轻柔上升 + 短促气流
+ this.tone(440, 0.08, "sine", 0.06, 0);
+ this.tone(587.33, 0.1, "sine", 0.05, 0.04);
+ this.tone(880, 0.06, "triangle", 0.04, 0.1);
+ break;
+ }
}
}
}
diff --git a/src/lib/game/chronicle.ts b/src/lib/game/chronicle.ts
new file mode 100644
index 000000000..8e8f2f516
--- /dev/null
+++ b/src/lib/game/chronicle.ts
@@ -0,0 +1,293 @@
+// 回响星核 / Echo Nexus — 回响编年史(v0.4 跨周目叙事时间轴)
+import type { ChronicleEntry, GameState, RunStartSnapshot } from "./types";
+import { CONSTELLATION_PERKS, getPerk, CONSTELLATION_CATEGORY_META } from "./constellation";
+import { TIDE_EVENTS } from "./starTide";
+
+/** ID → 名称 速查表 */
+const PERK_NAME_MAP: Record = Object.fromEntries(
+ CONSTELLATION_PERKS.map((p) => [p.id, p.name])
+);
+
+/** 星潮 ID → 叙事用名(从 TIDE_EVENTS 复用,避免硬编码错位) */
+const TIDE_LORE_NAMES: Record = Object.fromEntries(
+ Object.entries(TIDE_EVENTS).map(([type, ev]) => [`tide_${type}`, ev.name])
+);
+
+/**
+ * 纪元名生成 —— 5 纪元循环 + 形容词池,每次飞升从池中按 ascensionNumber 抽取
+ * 对应 v0.5 路线图「全 5 纪元叙事」的早期骨架
+ */
+const EPOCH_NAMES: { prefix: string; suffixes: string[] }[] = [
+ { prefix: "第一纪元", suffixes: ["觉醒之晨", "初鸣之夕", "晶体降诞", "回响初啼"] },
+ { prefix: "第二纪元", suffixes: ["谐振潮汐", "光谱涌动", "深空低语", "残响交织"] },
+ { prefix: "第三纪元", suffixes: ["遗迹苏醒", "星辉汇聚", "虚空回望", "接触前夜"] },
+ { prefix: "第四纪元", suffixes: ["维度折叠", "以太共振", "星核悸动", "飞升之歌"] },
+ { prefix: "第五纪元", suffixes: ["终末回响", "永恒闭环", "超越之境", "起源重述"] },
+];
+
+/** 程序化生成纪元名(按 ascensionNumber 循环 5 纪元) */
+export function generateEpochName(ascensionNumber: number, seed: number): string {
+ const epochIdx = (ascensionNumber - 1) % EPOCH_NAMES.length;
+ const epoch = EPOCH_NAMES[epochIdx];
+ // 用 seed 选 suffix,保证可复现
+ const suffixIdx = seed % epoch.suffixes.length;
+ return `${epoch.prefix} · ${epoch.suffixes[suffixIdx]}`;
+}
+
+/** 叙事模板片段 —— 根据本周目数据动态拼装 */
+function buildLore(entry: {
+ ascensionNumber: number;
+ epochName: string;
+ durationSec: number;
+ techsThisRun: number;
+ expThisRun: number;
+ bossKillsThisRun: number;
+ decodedThisRun: number;
+ newTides: string[];
+ perksThisAscension: string[];
+ blueprintsAfter: number;
+ milestones: string[];
+}): string {
+ const minutes = Math.max(1, Math.round(entry.durationSec / 60));
+ const parts: string[] = [];
+
+ // 开场:纪元定调
+ parts.push(
+ `「${entry.epochName}」——无人机群在第 ${entry.ascensionNumber} 次跨越维度的余烬中重新校准。这一周目持续了约 ${minutes} 分钟。`
+ );
+
+ // 中段:核心活动
+ const activities: string[] = [];
+ if (entry.decodedThisRun > 0) {
+ activities.push(`解码了 ${entry.decodedThisRun} 颗记忆晶体`);
+ }
+ if (entry.techsThisRun > 0) {
+ activities.push(`激活了 ${entry.techsThisRun} 项新技术`);
+ }
+ if (entry.expThisRun > 0) {
+ activities.push(`完成了 ${entry.expThisRun} 次遗迹探险`);
+ }
+ if (entry.bossKillsThisRun > 0) {
+ activities.push(`击破 ${entry.bossKillsThisRun} 处维度 BOSS`);
+ }
+ if (activities.length > 0) {
+ parts.push(`自治机群${activities.join(",")},回响在虚空中沉淀。`);
+ } else {
+ parts.push(`自治机群在静默中等待,仅完成最基本的脉冲扫描。`);
+ }
+
+ // 星潮段落
+ if (entry.newTides.length > 0) {
+ const tideNames = entry.newTides.map((t) => TIDE_LORE_NAMES[t] || t).join("、");
+ parts.push(`本周目经历了 ${entry.newTides.length} 次星潮事件(${tideNames}),深空的呼吸塑造了每一个决策。`);
+ }
+
+ // 觉醒段落
+ if (entry.perksThisAscension.length > 0) {
+ const perkNames = entry.perksThisAscension
+ .map((id) => PERK_NAME_MAP[id] || id)
+ .join("、");
+ parts.push(`飞升之际,星图觉醒「${perkNames}」,铭刻于永恒回响。`);
+ }
+
+ // 里程碑段落
+ if (entry.milestones.length > 0) {
+ parts.push(`✦ 里程碑:${entry.milestones.join(";")}。`);
+ }
+
+ // 收尾
+ parts.push(
+ `蓝图累计 ${entry.blueprintsAfter} 张,新的周目在等深处的回声。`
+ );
+
+ return parts.join(" ");
+}
+
+/** 星潮 ID → 叙事用名(已在文件顶部从 TIDE_EVENTS 派生 TIDE_LORE_NAMES) */
+
+/** 检测本周目首次达成的里程碑 */
+function detectMilestones(state: GameState, deltas: {
+ techsThisRun: number;
+ expThisRun: number;
+ bossKillsThisRun: number;
+ decodedThisRun: number;
+ newTides: string[];
+}): string[] {
+ const m: string[] = [];
+ const ascN = state.ascensions + 1; // 即将变成的飞升次数
+ if (ascN === 1) m.push("首次飞升");
+ if (deltas.expThisRun > 0 && state.runStart.expeditionsCompleted === 0) {
+ m.push("首次完成遗迹探险");
+ }
+ if (deltas.bossKillsThisRun > 0) {
+ m.push("首杀维度 BOSS");
+ }
+ if (deltas.decodedThisRun >= 10) m.push(`单周目解码 ${deltas.decodedThisRun} 颗`);
+ if (deltas.decodedThisRun >= 30) m.push(`单周目解码 ${deltas.decodedThisRun} 颗`);
+ if (deltas.techsThisRun >= 5) m.push("技术狂人");
+ if (deltas.newTides.length >= 3) m.push("星潮亲历者");
+ if ((state.constellation?.length || 0) >= 6) m.push("星图六分");
+ if ((state.constellation?.length || 0) >= 12) m.push("星图十二宫");
+ return m;
+}
+
+/** 计算本周目 delta */
+export function computeRunDeltas(state: GameState): {
+ durationSec: number;
+ techsThisRun: number;
+ expThisRun: number;
+ bossKillsThisRun: number;
+ decodedThisRun: number;
+ newTides: string[];
+} {
+ const now = Date.now();
+ const rs = state.runStart;
+ return {
+ durationSec: Math.max(1, Math.floor((now - rs.timestamp) / 1000)),
+ techsThisRun: Math.max(0, Object.keys(state.tech || {}).length - rs.techsUnlocked),
+ expThisRun: Math.max(0, state.totalExpeditions - rs.expeditionsCompleted),
+ bossKillsThisRun: Math.max(0, state.bossKills - rs.bossKills),
+ decodedThisRun: Math.max(0, state.totalDecoded - rs.crystalsDecoded),
+ newTides: (state.starTidesEncountered || []).filter(
+ (t) => !(rs.starTidesEncountered || []).includes(t)
+ ),
+ };
+}
+
+/** 构建一条编年史记录(在 performPrestige 内调用,perksThisAscension 暂为空,由 chooseConstellationPerk 后回填) */
+export function buildChronicleEntry(
+ state: GameState,
+ perksThisAscension: string[] = []
+): ChronicleEntry {
+ const ascensionNumber = state.ascensions + 1;
+ const deltas = computeRunDeltas(state);
+ const seed = (state.createdAt + ascensionNumber * 7919) >>> 0;
+ const epochName = generateEpochName(ascensionNumber, seed);
+ const milestones = detectMilestones(state, deltas);
+ const blueprintsAfter = Math.min(
+ state.blueprints.length + 1, // 即将获得的 1 张
+ 6 // PRESTIGE.maxBlueprints
+ );
+
+ const lore = buildLore({
+ ascensionNumber,
+ epochName,
+ durationSec: deltas.durationSec,
+ techsThisRun: deltas.techsThisRun,
+ expThisRun: deltas.expThisRun,
+ bossKillsThisRun: deltas.bossKillsThisRun,
+ decodedThisRun: deltas.decodedThisRun,
+ newTides: deltas.newTides,
+ perksThisAscension,
+ blueprintsAfter,
+ milestones,
+ });
+
+ return {
+ ascensionNumber,
+ epochName,
+ timestamp: Date.now(),
+ durationSec: deltas.durationSec,
+ summary: {
+ techsUnlocked: Object.keys(state.tech || {}).length,
+ constellationsTotal: state.constellation?.length || 0,
+ expeditionsCompleted: state.totalExpeditions,
+ bossKills: state.bossKills,
+ starTidesEncountered: state.starTidesEncountered?.length || 0,
+ blueprintsAfter,
+ crystalsDecoded: state.totalDecoded,
+ crystalsDecodedThisRun: deltas.decodedThisRun,
+ },
+ perksThisAscension,
+ tidesThisRun: deltas.newTides,
+ lore,
+ milestones,
+ };
+}
+
+/** 创建新一轮 runStart 快照(飞升后调用) */
+export function createRunStartSnapshot(state: GameState): RunStartSnapshot {
+ return {
+ timestamp: Date.now(),
+ techsUnlocked: Object.keys(state.tech || {}).length,
+ expeditionsCompleted: state.totalExpeditions,
+ bossKills: state.bossKills,
+ starTidesEncountered: [...(state.starTidesEncountered || [])],
+ crystalsDecoded: state.totalDecoded,
+ };
+}
+
+/** 旧存档兼容:补全缺失字段 */
+export function migrateChronicleFields(state: Partial): {
+ chronicle: ChronicleEntry[];
+ runStart: RunStartSnapshot;
+ bossKills: number;
+ starTidesEncountered: string[];
+} {
+ return {
+ chronicle: Array.isArray(state.chronicle) ? state.chronicle : [],
+ runStart: state.runStart || {
+ timestamp: state.createdAt || Date.now(),
+ techsUnlocked: Object.keys(state.tech || {}).length,
+ expeditionsCompleted: state.totalExpeditions || 0,
+ bossKills: state.bossKills || 0,
+ starTidesEncountered: [...(state.starTidesEncountered || [])],
+ crystalsDecoded: state.totalDecoded || 0,
+ },
+ bossKills: typeof state.bossKills === "number" ? state.bossKills : 0,
+ starTidesEncountered: Array.isArray(state.starTidesEncountered)
+ ? state.starTidesEncountered
+ : [],
+ };
+}
+
+/** 将指定 entry 标记 perksThisAscension(飞升后选择天赋时回填) */
+export function withPerks(entry: ChronicleEntry, perks: string[]): ChronicleEntry {
+ // 若 perks 与原一致,直接返回
+ if (
+ entry.perksThisAscension.length === perks.length &&
+ entry.perksThisAscension.every((p, i) => p === perks[i])
+ ) {
+ return entry;
+ }
+ // 重新生成 lore 以包含天赋名
+ const perksNames = perks
+ .map((id) => PERK_NAME_MAP[id] || id)
+ .join("、");
+ const appendText =
+ perks.length > 0
+ ? ` 飞升之际,星图觉醒「${perksNames}」,铭刻于永恒回响。`
+ : "";
+ // 去重:原 lore 若已含「飞升之际」段则不再追加
+ const hasAwakenSeg = entry.lore.includes("飞升之际");
+ const lore = hasAwakenSeg ? entry.lore : entry.lore + appendText;
+ return {
+ ...entry,
+ perksThisAscension: perks,
+ lore,
+ };
+}
+
+/** 获取已点亮天赋的类别统计(用于编年史展示) */
+export function getPerkCategoryBreakdown(perks: string[]): Record {
+ const breakdown: Record = {};
+ for (const id of perks) {
+ const cat = getPerk(id)?.category;
+ if (cat) {
+ breakdown[cat] = (breakdown[cat] || 0) + 1;
+ }
+ }
+ return breakdown;
+}
+
+/** 类别 → 中文名(用于编年史展示) */
+export function getCategoryName(cat: string): string {
+ return CONSTELLATION_CATEGORY_META[cat as keyof typeof CONSTELLATION_CATEGORY_META]?.name || cat;
+}
+
+/** 类别 → 颜色 hex(用于编年史展示) */
+export function getCategoryColor(cat: string): string {
+ return CONSTELLATION_CATEGORY_META[cat as keyof typeof CONSTELLATION_CATEGORY_META]?.hex || "#94a3b8";
+}
+
+export { PERK_NAME_MAP };
diff --git a/src/lib/game/config.ts b/src/lib/game/config.ts
index 7f5b8f00e..2822ff5a2 100644
--- a/src/lib/game/config.ts
+++ b/src/lib/game/config.ts
@@ -29,11 +29,22 @@ export const INITIAL_STATE = {
energyMax: 5,
lastEnergyTick: Date.now(),
totalExpeditions: 0,
+ bossKills: 0,
achievements: {},
activeTide: null,
lastTideEnd: 0,
+ starTidesEncountered: [] as string[],
constellation: [] as string[],
pendingPerkChoices: null as string[] | null,
+ chronicle: [] as import("./types").ChronicleEntry[],
+ runStart: {
+ timestamp: Date.now(),
+ techsUnlocked: 0,
+ expeditionsCompleted: 0,
+ bossKills: 0,
+ starTidesEncountered: [] as string[],
+ crystalsDecoded: 0,
+ },
theme: "dark" as const,
soundOn: true,
};
diff --git a/src/lib/game/engine.ts b/src/lib/game/engine.ts
index fabadc703..5f5597563 100644
--- a/src/lib/game/engine.ts
+++ b/src/lib/game/engine.ts
@@ -10,6 +10,10 @@ import {
import { achievementBonuses } from "./achievements";
import { getTideModifiers, type StarTide } from "./starTide";
import { constellationBonuses, rollPerkChoices } from "./constellation";
+import {
+ buildChronicleEntry,
+ createRunStartSnapshot,
+} from "./chronicle";
/** 由技术树 + 飞升蓝图 + 成就 + 星图天赋 + 星潮聚合计算产能字段 */
export function recomputeStats(state: Partial): {
@@ -133,7 +137,13 @@ export function performPrestige(state: GameState): GameState {
// 星图「飞升礼包」天赋的初始资源补偿
const cm = constellationBonuses(state.constellation ?? []);
+ // === 编年史快照:在重置前构建本周目条目,perksThisAscension 暂为空(用户选择后回填) ===
+ const chronicleEntry = buildChronicleEntry(state, []);
+ const newChronicle = [...(state.chronicle || []), chronicleEntry].slice(-50); // 上限 50 条
+ const freshRunStart = createRunStartSnapshot(state);
+
// 保留:fragments, totalDecoded(累计), ascensions+1, blueprints, achievements, constellation, theme/sound, expeditionLog
+ // chronicle(新增), bossKills(累计), starTidesEncountered(累计), runStart(重置为新周目)
// 重置:资源、技术、产能、pendingCrystals、activePuzzle、activeExpedition、contact、lastTick、energy、星潮
const fresh = createInitialState();
const stats = recomputeStats({ tech: {}, blueprints, achievements: state.achievements, constellation: state.constellation });
@@ -156,6 +166,11 @@ export function performPrestige(state: GameState): GameState {
lastEnergyTick: Date.now(),
expeditionLog: state.expeditionLog,
totalExpeditions: state.totalExpeditions,
+ bossKills: state.bossKills, // 累计保留
+ starTidesEncountered: state.starTidesEncountered, // 累计保留
+ // 编年史:追加新条目,重置 runStart 为新周目
+ chronicle: newChronicle,
+ runStart: freshRunStart,
// 星潮:飞升后清空,lastTideEnd 设为现在,使首次星潮在 firstDelay 后触发
activeTide: null,
lastTideEnd: Date.now(),
diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts
index d8a13d46c..ad969d39d 100644
--- a/src/lib/game/types.ts
+++ b/src/lib/game/types.ts
@@ -124,6 +124,7 @@ export interface GameState {
energyMax: number;
lastEnergyTick: number;
totalExpeditions: number;
+ bossKills: number; // 累计击败 BOSS 数(跨周目)
// 成就
achievements: Record; // achievementId -> unlocked
@@ -131,11 +132,16 @@ export interface GameState {
// 星潮事件
activeTide: import("./starTide").StarTide | null;
lastTideEnd: number;
+ starTidesEncountered: string[]; // 累计遇到的星潮 ID 集合(跨周目,去重)
// 星图天文台(v0.3.1 元进程)
constellation: string[]; // 已解锁天赋 ID 列表
pendingPerkChoices: string[] | null; // 飞升后待选择(3 选 1)
+ // 回响编年史(v0.4 跨周目叙事时间轴)
+ chronicle: ChronicleEntry[];
+ runStart: RunStartSnapshot;
+
// 元
lastTick: number;
createdAt: number;
@@ -143,6 +149,47 @@ export interface GameState {
soundOn: boolean;
}
+/** 编年史单条记录(每次飞升生成一条) */
+export interface ChronicleEntry {
+ /** 第几次飞升(1-based) */
+ ascensionNumber: number;
+ /** 程序化生成的纪元名 */
+ epochName: string;
+ /** 飞升完成时间戳 */
+ timestamp: number;
+ /** 本周目持续时间(秒) */
+ durationSec: number;
+ /** 本周目快照统计 */
+ summary: {
+ techsUnlocked: number;
+ constellationsTotal: number;
+ expeditionsCompleted: number;
+ bossKills: number;
+ starTidesEncountered: number;
+ blueprintsAfter: number;
+ crystalsDecoded: number;
+ crystalsDecodedThisRun: number;
+ };
+ /** 本次飞升获得的天赋 ID(来自星图觉醒) */
+ perksThisAscension: string[];
+ /** 本周目遇到的星潮 ID 集合 */
+ tidesThisRun: string[];
+ /** 程序化生成的叙事文本(1-2 段) */
+ lore: string;
+ /** 标志位:本周目首次达成的里程碑 */
+ milestones: string[];
+}
+
+/** 周目起始快照(用于计算 delta) */
+export interface RunStartSnapshot {
+ timestamp: number;
+ techsUnlocked: number;
+ expeditionsCompleted: number;
+ bossKills: number;
+ starTidesEncountered: string[];
+ crystalsDecoded: number;
+}
+
// ============ 遗迹探险 (Expedition) 系统 ============
/** 探险节点类型 */
diff --git a/src/store/gameStore.ts b/src/store/gameStore.ts
index bfd8aca3c..c2a203ca4 100644
--- a/src/store/gameStore.ts
+++ b/src/store/gameStore.ts
@@ -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()(
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()(
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()(
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()(
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()(
contact: newContact,
fragments: newFragments,
expeditionLog: newLog,
+ bossKills,
});
return result;
},
@@ -626,10 +652,19 @@ export const useGameStore = create()(
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;