// 回响星核 / Echo Nexus — 成就系统(数据驱动) import type { GameState } from "./types"; /** 成就奖励类型 */ export interface AchievementReward { crystals?: number; insights?: number; contact?: number; /** 永久产能加成(百分比,叠加到所有周目) */ crystalsPerSecPct?: number; insightPct?: number; } export interface Achievement { id: string; name: string; desc: string; /** 图标 emoji(轻量,无需图标库) */ icon: string; /** 颜色(用于徽章) */ color: string; /** 判定函数:返回 true 表示达成 */ check: (s: GameState) => boolean; reward: AchievementReward; /** 奖励文本(展示用) */ rewardText: string; } export const ACHIEVEMENTS: Achievement[] = [ { id: "ach_first_pulse", name: "初次触碰", desc: "发起第一次脉冲扫描", icon: "✦", color: "#34d399", check: (s) => s.totalDecoded >= 0 || s.crystals > 0, reward: { crystals: 5 }, rewardText: "+5 晶体", }, { id: "ach_first_decode", name: "谐振初鸣", desc: "成功解码第一颗记忆晶体", icon: "◈", color: "#fb7185", check: (s) => s.totalDecoded >= 1, reward: { insights: 3 }, rewardText: "+3 洞见", }, { id: "ach_decoded_10", name: "回响解码者", desc: "累计解码 10 颗晶体", icon: "❖", color: "#fbbf24", check: (s) => s.totalDecoded >= 10, reward: { insights: 15, crystalsPerSecPct: 5 }, rewardText: "+15 洞见 · 产能 +5%", }, { id: "ach_decoded_25", name: "记忆织匠", desc: "累计解码 25 颗晶体", icon: "✺", color: "#e879f9", check: (s) => s.totalDecoded >= 25, reward: { insights: 40, crystalsPerSecPct: 8 }, rewardText: "+40 洞见 · 产能 +8%", }, { id: "ach_decoded_50", name: "星核解密师", desc: "累计解码 50 颗晶体", icon: "✷", color: "#34d399", check: (s) => s.totalDecoded >= 50, reward: { insights: 100, crystalsPerSecPct: 12, insightPct: 10 }, rewardText: "+100 洞见 · 产能 +12% · 洞见 +10%", }, { id: "ach_tech_3", name: "初窥门径", desc: "解锁 3 项技术", icon: "⚙", color: "#fbbf24", check: (s) => Object.values(s.tech).filter((v) => v > 0).length >= 3, reward: { insights: 20 }, rewardText: "+20 洞见", }, { id: "ach_tech_all", name: "全谱精通", desc: "解锁全部 20 项技术", icon: "⬡", color: "#e879f9", check: (s) => Object.values(s.tech).filter((v) => v > 0).length >= 20, reward: { crystalsPerSecPct: 15, insightPct: 15 }, rewardText: "产能 +15% · 洞见 +15%", }, { id: "ach_frag_4", name: "残篇拾遗", desc: "拼凑 4 段记忆碎片", icon: "▤", color: "#fb7185", check: (s) => Object.values(s.fragments).filter(Boolean).length >= 4, reward: { contact: 10, insights: 25 }, rewardText: "+10 接触 · +25 洞见", }, { id: "ach_frag_all", name: "回响全谱", desc: "拼凑全部首纪元记忆碎片", icon: "▦", color: "#34d399", check: (s) => Object.values(s.fragments).filter(Boolean).length >= 8, reward: { contact: 25, crystalsPerSecPct: 10 }, rewardText: "+25 接触 · 产能 +10%", }, { id: "ach_exp_1", name: "初探遗迹", desc: "完成第一次遗迹探险", icon: "▲", color: "#fbbf24", check: (s) => s.totalExpeditions >= 1, reward: { insights: 10 }, rewardText: "+10 洞见", }, { id: "ach_exp_5", name: "遗迹猎手", desc: "累计出发 5 次探险", icon: "⬢", color: "#fb7185", check: (s) => s.totalExpeditions >= 5, reward: { insights: 30, crystalsPerSecPct: 5 }, rewardText: "+30 洞见 · 产能 +5%", }, { id: "ach_prestige_1", name: "初次接触", desc: "完成第一次飞升", icon: "✧", color: "#e879f9", check: (s) => s.ascensions >= 1, reward: { crystalsPerSecPct: 10, insightPct: 10 }, rewardText: "产能 +10% · 洞见 +10%", }, { id: "ach_prestige_3", name: "维度行者", desc: "累计飞升 3 次", icon: "✶", color: "#34d399", check: (s) => s.ascensions >= 3, reward: { crystalsPerSecPct: 20, insightPct: 20 }, rewardText: "产能 +20% · 洞见 +20%", }, { id: "ach_warehouse", name: "满仓时刻", desc: "晶体储量达到仓库上限", icon: "▣", color: "#fbbf24", check: (s) => s.crystals >= s.crystalCap, reward: { insights: 8 }, rewardText: "+8 洞见", }, { id: "ach_constellation_1", name: "星图初绘", desc: "觉醒第 1 道星图天赋", icon: "✨", color: "#e879f9", check: (s) => (s.constellation?.length ?? 0) >= 1, reward: { insights: 30, crystalsPerSecPct: 5 }, rewardText: "+30 洞见 · 产能 +5%", }, { id: "ach_constellation_6", name: "六分星辉", desc: "觉醒 6 道星图天赋(每类各 1 道)", icon: "✺", color: "#34d399", check: (s) => { const c = s.constellation ?? []; const cats = new Set(c.map((id) => id.split("_")[1])); return cats.size >= 6 && c.length >= 6; }, 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: "经历全部 9 种星潮事件", icon: "🌊", color: "#34d399", check: (s) => (s.starTidesEncountered?.length ?? 0) >= 9, reward: { crystalsPerSecPct: 8, insightPct: 8 }, rewardText: "产能 +8% · 洞见 +8%", }, { id: "ach_attr_total_50", name: "四维觉醒", desc: "角色四维属性总和达到 50", icon: "🧭", color: "#e879f9", check: (s) => { const a = s.attributes; if (!a) return false; return ( (a.exploration || 0) + (a.wisdom || 0) + (a.courage || 0) + (a.inspiration || 0) >= 50 ); }, reward: { crystalsPerSecPct: 6, insightPct: 6 }, rewardText: "产能 +6% · 洞见 +6%", }, { id: "ach_attr_max_100", name: "维度精通", desc: "任一角色属性达到满级 100", icon: "💫", color: "#fbbf24", check: (s) => { const a = s.attributes; if (!a) return false; return ( a.exploration >= 100 || a.wisdom >= 100 || a.courage >= 100 || a.inspiration >= 100 ); }, reward: { crystalsPerSecPct: 12, insightPct: 10 }, rewardText: "产能 +12% · 洞见 +10%", }, ]; /** 计算成就提供的永久加成(跨周目保留) */ export function achievementBonuses(unlocked: Record): { crystalsPerSecPct: number; insightPct: number; } { let crystalsPerSecPct = 0; let insightPct = 0; for (const a of ACHIEVEMENTS) { if (!unlocked[a.id]) continue; crystalsPerSecPct += a.reward.crystalsPerSecPct ?? 0; insightPct += a.reward.insightPct ?? 0; } return { crystalsPerSecPct, insightPct }; }