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 && (
@@ -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;