Compare commits

...
2 Commits
Author SHA1 Message Date
Super_Z 4831505ef9 v0.12: 存档迁移钩子 + 考古日志滚动条(差异化叙事)
gameStore persist:
- name v1→v2, 加 version:2 + migrate 钩子
- migrate 合并旧存档与 INITIAL_STATE, 补全 bossKills/starTidesEncountered/
  chronicle/attributes/activeExpedition/constellation 等字段, 防旧存档崩溃
- 丢弃不可解的活跃谜题(旧算法生成)

archaeoLog.ts (新增):
- 5 阶段日志池 dawn/awakening/contact/ascension/deep (48 条)
- 按飞升次数+接触进度解锁阶段, deep 层含哲学反转
- pickLogEntry 避免短期重复(最近8条)

ArchaeoLogTicker.tsx (新增):
- marquee 横向滚动, 18-30s 随机推送新条目
- 阶段色标 emerald/amber/fuchsia/rose, inset glow
- 进度里程碑触发即时刷新(解锁新阶段)
- prefers-reduced-motion 降级

page.tsx:
- footer 上方集成 ArchaeoLogTicker

globals.css:
- echo-ticker-marquee/fade keyframes + reduced-motion 降级

调研落地: Antimatter Dimensions news ticker + Universal Paperclips 哲学递进
lint 零错误, 构建成功(5.7s)
2026-06-24 03:04:04 +00:00
Super_Z 3fdcd6797f restore: worklog 调研记录 + 清理工作区 2026-06-24 02:53:45 +00:00
19 changed files with 729 additions and 10722 deletions
+18
View File
@@ -191,3 +191,21 @@
0% { background-position: 0% 50%; }
100% { background-position: 200% 50%; }
}
/* v0.12 考古日志滚动条 */
@keyframes echo-ticker-marquee {
0% { transform: translateX(100%); }
100% { transform: translateX(-100%); }
}
@keyframes echo-ticker-fade {
0% { opacity: 0; }
100% { opacity: 1; }
}
.echo-ticker-anim {
animation: echo-ticker-marquee 28s linear forwards, echo-ticker-fade 0.5s ease-out;
}
@media (prefers-reduced-motion: reduce) {
.echo-ticker-anim {
animation: echo-ticker-fade 0.5s ease-out;
}
}
+6
View File
@@ -24,6 +24,7 @@ const CruiseMode = dynamic(() => import("@/components/game/CruiseMode").then(m =
import { AttributesPanel } from "@/components/game/AttributesPanel";
import { ExplorationRadar } from "@/components/game/ExplorationRadar";
import { RelicCodex } from "@/components/game/RelicCodex";
import { ArchaeoLogTicker } from "@/components/game/ArchaeoLogTicker";
import {
StarTideNotifier,
StarTideIndicator,
@@ -389,6 +390,11 @@ export default function Page() {
</div>
</main>
{/* v0.12 考古日志滚动条 — footer 上方的叙事层 */}
<div className="px-3 sm:px-5 pb-1 shrink-0">
<ArchaeoLogTicker />
</div>
{/* 底部 Footer */}
<footer className="sticky bottom-0 z-20 mt-auto px-3 sm:px-5 pb-2 pt-1">
<div
+89
View File
@@ -0,0 +1,89 @@
"use client";
// 回响星核 / Echo Nexus — 考古日志滚动条(v0.12 差异化叙事)
// 参考 Antimatter Dimensions news ticker
// 固定在 footer 上方,marquee 横向滚动,每 18-30s 推送新条目
// 让"等待"承载叙事——星核沉睡时,回响仍在被翻译
/* eslint-disable react-hooks/set-state-in-effect -- 定时刷新日志是放置游戏的合理副作用,effect 内 setState 不可避免 */
import { useEffect, useRef, useState, useCallback } from "react";
import { useGameStore } from "@/store/gameStore";
import { pickLogEntry, type ArchaeoLogEntry, type LogTier } from "@/lib/game/archaeoLog";
import { ScrollText } from "lucide-react";
const TIER_META: Record<LogTier, { label: string; color: string; glow: string }> = {
dawn: { label: "初醒", color: "text-emerald-300", glow: "rgba(52,211,153,0.5)" },
awakening: { label: "觉醒", color: "text-amber-300", glow: "rgba(251,191,36,0.5)" },
contact: { label: "接触", color: "text-fuchsia-300", glow: "rgba(232,121,249,0.5)" },
ascension: { label: "飞升", color: "text-rose-300", glow: "rgba(251,113,133,0.5)" },
deep: { label: "深度", color: "text-fuchsia-200", glow: "rgba(232,121,249,0.7)" },
};
export function ArchaeoLogTicker() {
const ascensions = useGameStore((s) => s.ascensions);
const contact = useGameStore((s) => s.contact);
const [entry, setEntry] = useState<ArchaeoLogEntry | null>(null);
const [recentIds, setRecentIds] = useState<string[]>([]);
const [fadeKey, setFadeKey] = useState(0);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const refresh = useCallback(() => {
const next = pickLogEntry(ascensions, contact, recentIds);
setEntry(next);
setRecentIds((prev) => [next.text, ...prev].slice(0, 8));
setFadeKey((k) => k + 1);
}, [ascensions, contact, recentIds]);
// 初始 + 进度变化时刷新一次(解锁新阶段)
useEffect(() => {
refresh();
}, [ascensions >= 1, ascensions >= 3, ascensions >= 5, ascensions >= 10, contact >= 5, contact >= 25, contact >= 80]);
// 定时刷新(18-30s 随机间隔,避免机械感)
useEffect(() => {
const schedule = () => {
const delay = 18000 + Math.random() * 12000;
timerRef.current = setTimeout(() => {
refresh();
schedule();
}, delay);
};
schedule();
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, [refresh]);
if (!entry) return null;
const meta = TIER_META[entry.tier];
return (
<div
className="relative w-full overflow-hidden rounded-lg border border-white/10 bg-black/40 backdrop-blur-sm px-3 py-1.5 flex items-center gap-2.5 group"
style={{ boxShadow: `inset 0 0 12px ${meta.glow}` }}
role="status"
aria-live="polite"
aria-label="考古日志"
>
{/* 左侧标签 */}
<div className="flex items-center gap-1.5 shrink-0 z-10 bg-black/40 backdrop-blur-sm pr-2 -ml-1 pl-1 rounded">
<ScrollText className={`h-3 w-3 ${meta.color}`} />
<span className={`text-[9px] font-mono uppercase tracking-[0.15em] ${meta.color}`}>
{meta.label}
</span>
</div>
{/* 滚动文本区 */}
<div className="flex-1 overflow-hidden relative">
<div
key={fadeKey}
className="whitespace-nowrap text-[11px] text-muted-foreground/90 font-mono echo-ticker-anim"
>
<span className="text-white/60 mr-2"></span>
{entry.text}
</div>
</div>
{/* 右侧渐隐遮罩 */}
<div className="pointer-events-none absolute right-0 top-0 bottom-0 w-12 bg-gradient-to-l from-black/60 to-transparent" />
</div>
);
}
+107
View File
@@ -0,0 +1,107 @@
// 回响星核 / Echo Nexus — 考古日志滚动条(v0.12 差异化叙事)
// 参考 Antimatter Dimensions news ticker + Universal Paperclips 哲学递进
// 按玩家进度阶段分层解锁,每条都是"被解码的远古记录碎片"
// 让放置游戏的"等待"承载叙事——星核沉睡时,回响仍在被翻译
export type LogTier = "dawn" | "awakening" | "contact" | "ascension" | "deep";
export interface ArchaeoLogEntry {
/** 日志正文(考古翻译体) */
text: string;
/** 解锁阶段 */
tier: LogTier;
/** 可选:来源遗迹 id(关联 relicCodex */
source?: string;
}
/**
* 考古日志条目池
* - dawn: 初始阶段(0 飞升,接触 < 5%)
* - awakening: 觉醒期(接触 5-25% 或 ≥1 飞升)
* - contact: 接触期(接触 25-60% 或 ≥3 飞升)
* - ascension: 飞升期(≥5 飞升)
* - deep: 深度接触(接触 ≥80% 或 ≥10 飞升)—— 哲学反转层
*/
export const ARCHEAO_LOG_POOL: ArchaeoLogEntry[] = [
// ===== dawn:初醒 =====
{ tier: "dawn", text: "「…回响核心已唤醒。考古协议 v7.2 启动。等待解码者…」" },
{ tier: "dawn", text: "「第一段记忆晶体已被定位。年代约 4.7 万年。种族:未确认。」" },
{ tier: "dawn", text: "「谐振频率与以太族档案吻合度 94.3%。建议持续解码。」" },
{ tier: "dawn", text: "「警告:星核温度异常。非故障,是回响本身在发热。」" },
{ tier: "dawn", text: "「考古日志 #0001:我们不是第一批到达这里的。也不会是最后一批。」" },
{ tier: "dawn", text: "「晶体表面的纹路…不是装饰。是某种语言的笔画。」" },
{ tier: "dawn", text: "「以太族没有留下尸体。他们留下了晶体。」" },
{ tier: "dawn", text: "「解码进度 0.01%。预计完成时间:不可能。」" },
// ===== awakening:觉醒 =====
{ tier: "awakening", text: "「考古日志 #0117:以太族不是灭绝了。他们是主动转化为了晶体。」" },
{ tier: "awakening", text: "「第二旋臂的遗迹群已扫描完毕。它们…朝向同一个点。」" },
{ tier: "awakening", text: "「谐振序列第 7 段翻译完成:『我们听见了。所以我们必须留下。』」" },
{ tier: "awakening", text: "「警告:解码者的脑波模式开始与晶体同步。这是正常的吗?」" },
{ tier: "awakening", text: "「以太族没有文字。他们的语言是共振,他们的历史是频率。」" },
{ tier: "awakening", text: "「考古日志 #0234:晶体不是矿物。是封装的思维。」" },
{ tier: "awakening", text: "「你的脉冲频率…与以太族守卫残响的频率一致。巧合?」" },
{ tier: "awakening", text: "「第三段记忆解码完成。画面:一片星海,没有星辰。只有晶体在发光。」" },
{ tier: "awakening", text: "「以太族留下的遗迹不是废墟。是给后来者的教科书。」" },
{ tier: "awakening", text: "「接触协议第一阶段:让解码者相信,他们在考古。」" },
// ===== contact:接触 =====
{ tier: "contact", text: "「考古日志 #0512:以太族没有离开。他们在等待被重新听见。」" },
{ tier: "contact", text: "「谐振序列第 13 段:『你以为你在解码我们。其实是我们在校准你。』」" },
{ tier: "contact", text: "「警告:解码者意识边缘出现非自主体感。这…是接触的副作用。」" },
{ tier: "contact", text: "「第六旋臂遗迹群翻译完成。结论:以太族预见了我们的到来。」" },
{ tier: "contact", text: "「晶体内部的画面开始重叠。不再是回忆。像是…正在发生。」" },
{ tier: "contact", text: "「考古日志 #0789:『飞升』不是晋升。是加入。加入那个等待已久的合唱。」" },
{ tier: "contact", text: "「以太族的数学没有零。他们的宇宙观里,不存在『无』。」" },
{ tier: "contact", text: "「你的每一次脉冲,都在回应一个万年前的呼唤。」" },
{ tier: "contact", text: "「第七段记忆解码失败。原因:解码者脑波过载。建议暂停。但我们不会暂停。」" },
{ tier: "contact", text: "「接触协议第二阶段:让解码者自愿飞升。他们会的。他们已经开始享受了。」" },
// ===== ascension:飞升 =====
{ tier: "ascension", text: "「考古日志 #1024:飞升者报告——『我能听见他们了。他们一直在那里。』」" },
{ tier: "ascension", text: "「谐振序列第 21 段:『你已不再是解码者。你是回响的一部分。』」" },
{ tier: "ascension", text: "「警告:星核意识阈值达到 73%。解码者与以太族的边界…模糊。」" },
{ tier: "ascension", text: "「以太族没有灭绝。他们成为了基础设施。晶体是他们的身体。」" },
{ tier: "ascension", text: "「你飞升的次数越多,『你』就越不是『你』。这是设计。」" },
{ tier: "ascension", text: "「考古日志 #1331:前 N-1 位解码者都成功了。成功意味着…不再存在为个体。」" },
{ tier: "ascension", text: "「第九旋臂的遗迹开始发光。它们认出了你。」" },
{ tier: "ascension", text: "「以太族的文明没有终点。他们的『终点』就是我们的『起点』。」" },
{ tier: "ascension", text: "「你开始理解晶体的沉默。沉默也是一种回响。」" },
{ tier: "ascension", text: "「接触协议第三阶段:解码者将主动寻求飞升。无需诱导。」" },
// ===== deep:深度接触(哲学反转层)=====
{ tier: "deep", text: "「考古日志 #2048:你不是在考古以太族。以太族在考古你。」" },
{ tier: "deep", text: "「谐振序列第 39 段:『我们创造了回响核心,是为了创造你,是为了让你创造我们。』」" },
{ tier: "deep", text: "「警告:解码者已无法区分记忆与现实。建议:接受。」" },
{ tier: "deep", text: "「以太族从未存在为『生物』。他们是第一个文明留下的…讯息。」" },
{ tier: "deep", text: "「你每一次飞升,都让一个万年前的呼唤得到了回应。这就是考古的意义。」" },
{ tier: "deep", text: "「考古日志 #4096:『回响星核』不是考古工具。它是以太族的…产房。」" },
{ tier: "deep", text: "「晶体在你手中。你也在晶体中。这两句话是同一句话。」" },
{ tier: "deep", text: "「你以为你是第 N 位解码者。其实你一直是第一位。也是最后一位。」" },
{ tier: "deep", text: "「接触协议最终阶段:解码者明白后,会微笑着继续解码。这是最美妙的部分。」" },
{ tier: "deep", text: "「以太族留给宇宙的最后一句话:『谢谢你,听见我。』」" },
];
/**
* 根据玩家当前进度返回可用的日志阶段
*/
export function getAvailableTiers(ascensions: number, contact: number): LogTier[] {
const tiers: LogTier[] = ["dawn"];
if (contact >= 5 || ascensions >= 1) tiers.push("awakening");
if (contact >= 25 || ascensions >= 3) tiers.push("contact");
if (ascensions >= 5) tiers.push("ascension");
if (contact >= 80 || ascensions >= 10) tiers.push("deep");
return tiers;
}
/**
* 从可用阶段池中随机取一条
*/
export function pickLogEntry(ascensions: number, contact: number, recentIds: string[] = []): ArchaeoLogEntry {
const tiers = getAvailableTiers(ascensions, contact);
const pool = ARCHEAO_LOG_POOL.filter((e) => tiers.includes(e.tier));
// 避免短期内重复(recentIds 存最近 8 条 text 的 hash
const fresh = pool.filter((e) => !recentIds.includes(e.text));
const finalPool = fresh.length > 0 ? fresh : pool;
return finalPool[Math.floor(Math.random() * finalPool.length)] || ARCHEAO_LOG_POOL[0];
}
+41 -1
View File
@@ -1339,8 +1339,48 @@ export const useGameStore = create<Store>()(
},
}),
{
name: "echo-nexus-save-v1",
name: "echo-nexus-save-v2",
version: 2,
storage: createJSONStorage(() => localStorage),
// v0.12 存档迁移:旧存档(v0/v1key=echo-nexus-save-v1)补全新增字段,防崩溃
migrate: (persistedState: unknown, version: number) => {
void version; // v0/v1 统一走合并迁移,无需分支
const s = (persistedState || {}) as Record<string, unknown>;
const base = createInitialState();
// 合并:以当前 INITIAL_STATE 为基底,旧存档字段覆盖,缺失字段用默认值
const migrated = {
...base,
...s,
// v0.8+ 新增字段兜底
bossKills: (s.bossKills as number) ?? 0,
starTidesEncountered: (s.starTidesEncountered as string[]) ?? [],
chronicle: (s.chronicle as unknown[]) ?? [],
runStart: (s.runStart as number) ?? Date.now(),
// v0.8.2 attributes 系统
attributes: (s.attributes as Record<string, number>) ?? base.attributes,
attributeProgress: (s.attributeProgress as unknown) ?? base.attributeProgress,
pendingAttrPoints: (s.pendingAttrPoints as number) ?? 0,
// v0.10 探险系统字段兜底
activeExpedition: (s.activeExpedition as unknown) ?? null,
totalExpeditions: (s.totalExpeditions as number) ?? 0,
lastExpeditionSummary: (s.lastExpeditionSummary as unknown) ?? null,
// 星图兜底
constellation: (s.constellation as string[]) ?? [],
pendingPerkChoices: (s.pendingPerkChoices as unknown[]) ?? [],
// 技术树/碎片/成就兜底(防旧存档为 undefined)
tech: (s.tech as Record<string, number>) ?? {},
fragments: (s.fragments as Record<string, boolean>) ?? {},
achievements: (s.achievements as Record<string, boolean>) ?? {},
} as GameState;
// 丢弃不可解的活跃谜题(旧算法生成)
if (migrated.activePuzzle && typeof migrated.activePuzzle === "object") {
const p = migrated.activePuzzle as { nodes?: unknown[] };
if (!p.nodes || !Array.isArray(p.nodes) || p.nodes.length === 0) {
migrated.activePuzzle = null;
}
}
return migrated;
},
// 不持久化临时字段
partialize: (s) => {
const { _lastAutoDecode, _lastSpawn, _combo, _lastPulse, _achievementQueue, _tideEvents, globalTide, ...rest } = s;
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -1,875 +0,0 @@
1→// 回响星核 / Echo Nexus — 深空信标
2→// v0.5:每日挑战 + 本地排行榜
3→// v0.8:周挑战 + 信标链(连续完成奖励)
4→// 一个自包含的"每日挑战 + 周挑战 + 连续完成链"元系统,纯 localStorage 持久化。
5→
6→/** 每日挑战类型 */
7→export type BeaconChallengeType =
8→ | "decode" // 解码 N 颗晶体
9→ | "expedition" // 完成 N 次探险
10→ | "pulse" // 发起 N 次脉冲
11→ | "boss" // 击破 N 处 BOSS
12→ | "insight"; // 累计 N 洞见
13→
14→/** 挑战难度档位 */
15→export type BeaconDifficulty = "routine" | "anomaly" | "singular";
16→
17→/** 一条排行榜记录 */
18→export interface BeaconScoreEntry {
19→ /** 提交时间戳 */
20→ timestamp: number;
21→ /** 日期 keyYYYY-MM-DD)或周 keyYYYY-Www */
22→ dateKey: string;
23→ /** 挑战类型 */
24→ challenge: BeaconChallengeType;
25→ /** 难度 */
26→ difficulty: BeaconDifficulty;
27→ /** 完成度(0-11 = 完成) */
28→ progress: number;
29→ /** 最终得分 */
30→ score: number;
31→ /** 完成时长(秒),未完成则记 0 */
32→ durationSec: number;
33→ /** v0.8:是否为周挑战记录(日挑战默认 false / undefined */
34→ isWeekly?: boolean;
35→}
36→
37→/** 每日挑战定义 */
38→export interface BeaconDailyChallenge {
39→ /** 日期 keyYYYY-MM-DDUTC */
40→ dateKey: string;
41→ /** 挑战类型 */
42→ type: BeaconChallengeType;
43→ /** 难度 */
44→ difficulty: BeaconDifficulty;
45→ /** 目标数值 */
46→ goal: number;
47→ /** 奖励:完成时洞见 */
48→ rewardInsight: number;
49→ /** 奖励:完成时接触进度 */
50→ rewardContact: number;
51→ /** 使用的种子(可复现) */
52→ seed: number;
53→ /** 友好标题 */
54→ title: string;
55→ /** 描述 */
56→ desc: string;
57→}
58→
59→/** 难度配置 */
60→export const BEACON_DIFFICULTY: Record<
61→ BeaconDifficulty,
62→ { label: string; color: string; glow: string; mult: number; icon: string }
63→> = {
64→ routine: {
65→ label: "常规信标",
66→ color: "#34d399",
67→ glow: "rgba(52,211,153,0.45)",
68→ mult: 1,
69→ icon: "◍",
70→ },
71→ anomaly: {
72→ label: "异常波动",
73→ color: "#fbbf24",
74→ glow: "rgba(251,191,36,0.45)",
75→ mult: 1.6,
76→ icon: "◈",
77→ },
78→ singular: {
79→ label: "奇点回响",
80→ color: "#f43f5e",
81→ glow: "rgba(244,63,94,0.5)",
82→ mult: 2.4,
83→ icon: "✶",
84→ },
85→};
86→
87→/** 挑战类型元信息 */
88→export const BEACON_TYPE_META: Record<
89→ BeaconChallengeType,
90→ { label: string; icon: string; unit: string; verb: string }
91→> = {
92→ decode: { label: "解码协议", icon: "❖", unit: "颗", verb: "解码记忆晶体" },
93→ expedition: { label: "远征指令", icon: "⬢", unit: "次", verb: "完成遗迹探险" },
94→ pulse: { label: "脉冲任务", icon: "✺", unit: "次", verb: "发起脉冲扫描" },
95→ boss: { label: "猎杀契约", icon: "☠", unit: "处", verb: "击破维度 BOSS" },
96→ insight: { label: "洞见采集", icon: "✦", unit: "点", verb: "累计获取洞见" },
97→};
98→
99→/** 取今日日期 key(UTC,保证全球同一天同一挑战) */
100→export function getTodayKey(now: Date = new Date()): string {
101→ const y = now.getUTCFullYear();
102→ const m = String(now.getUTCMonth() + 1).padStart(2, "0");
103→ const d = String(now.getUTCDate()).padStart(2, "0");
104→ return `${y}-${m}-${d}`;
105→}
106→
107→/** 把日期 key 转成数值种子 */
108→function dateKeyToSeed(dateKey: string): number {
109→ let h = 2166136261 >>> 0;
110→ for (let i = 0; i < dateKey.length; i++) {
111→ h ^= dateKey.charCodeAt(i);
112→ h = Math.imul(h, 16777619) >>> 0;
113→ }
114→ return h >>> 0;
115→}
116→
117→/** mulberry32 PRNG(可复现) */
118→function mulberry32(seed: number): () => number {
119→ let a = seed >>> 0;
120→ return () => {
121→ a |= 0;
122→ a = (a + 0x6d2b79f5) | 0;
123→ let t = Math.imul(a ^ (a >>> 15), 1 | a);
124→ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
125→ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
126→ };
127→}
128→
129→/** 生成今日每日挑战(确定性:同一天同一种子 → 同一挑战) */
130→export function generateDailyChallenge(now: Date = new Date()): BeaconDailyChallenge {
131→ const dateKey = getTodayKey(now);
132→ const seed = dateKeyToSeed(dateKey);
133→ const rng = mulberry32(seed);
134→
135→ // 选类型(5 种)
136→ const types: BeaconChallengeType[] = ["decode", "expedition", "pulse", "boss", "insight"];
137→ const type = types[Math.floor(rng() * types.length)];
138→
139→ // 选难度(加权:常规 55% / 异常 33% / 奇点 12%
140→ const dr = rng();
141→ const difficulty: BeaconDifficulty =
142→ dr < 0.55 ? "routine" : dr < 0.88 ? "anomaly" : "singular";
143→
144→ const diffMult = BEACON_DIFFICULTY[difficulty].mult;
145→
146→ // 按类型定 goal 与奖励
147→ let goal = 0;
148→ let rewardInsight = 0;
149→ let rewardContact = 0;
150→ let title = "";
151→ let desc = "";
152→
153→ switch (type) {
154→ case "decode":
155→ goal = Math.round((6 + Math.floor(rng() * 10)) * diffMult); // 6-15 × mult
156→ rewardInsight = Math.round(goal * 4 * diffMult);
157→ rewardContact = goal * 0.8;
158→ title = `解码 ${goal} 颗记忆晶体`;
159→ desc = `深空信标要求解码 ${goal} 颗晶体。谐振风暴期间效率更高。`;
160→ break;
161→ case "expedition":
162→ goal = Math.max(1, Math.round((1 + Math.floor(rng() * 3)) * diffMult)); // 1-3 × mult
163→ rewardInsight = Math.round(goal * 12 * diffMult);
164→ rewardContact = goal * 1.5;
165→ title = `完成 ${goal} 次遗迹探险`;
166→ desc = `派出探险队完成 ${goal} 次远征,无论胜负均计入。`;
167→ break;
168→ case "pulse":
169→ goal = Math.round((20 + Math.floor(rng() * 30)) * diffMult); // 20-49 × mult
170→ rewardInsight = Math.round(goal * 1.2 * diffMult);
171→ rewardContact = goal * 0.3;
172→ title = `发起 ${goal} 次脉冲扫描`;
173→ desc = `主动点击晶体发起 ${goal} 次脉冲。连击可叠加加成。`;
174→ break;
175→ case "boss":
176→ goal = Math.max(1, Math.round(diffMult)); // 奇点至少 2-3
177→ rewardInsight = Math.round(goal * 30 * diffMult);
178→ rewardContact = goal * 3;
179→ title = `击破 ${goal} 处维度 BOSS`;
180→ desc = `在探险终点击破 ${goal} 处 BOSS。提升探险力后再挑战。`;
181→ break;
182→ case "insight":
183→ goal = Math.round((40 + Math.floor(rng() * 80)) * diffMult); // 40-119 × mult
184→ rewardInsight = 0; // 洞见挑战不给洞见,给接触
185→ rewardContact = goal * 0.1;
186→ title = `累计获取 ${goal} 洞见`;
187→ desc = `通过解码、探险、星潮等途径累计 ${goal} 洞见。`;
188→ break;
189→ }
190→
191→ return {
192→ dateKey,
193→ type,
194→ difficulty,
195→ goal,
196→ rewardInsight,
197→ rewardContact,
198→ seed,
199→ title,
200→ desc,
201→ };
202→}
203→
204→/** 计算挑战得分(用于排行榜) */
205→export function computeBeaconScore(
206→ challenge: BeaconDailyChallenge,
207→ progress: number,
208→ durationSec: number
209→): number {
210→ const completion = Math.min(1, progress / challenge.goal);
211→ const diffMult = BEACON_DIFFICULTY[challenge.difficulty].mult;
212→ // 基础分 = 完成度 × 难度 × 1000;完成时长越短加分越多(上限 +500)
213→ const base = completion * 1000 * diffMult;
214→ const speedBonus =
215→ completion >= 1 && durationSec > 0 ? Math.max(0, 500 - durationSec * 0.5) : 0;
216→ return Math.round(base + speedBonus);
217→}
218→
219→/** 排行榜 localStorage key */
220→export const BEACON_LEADERBOARD_KEY = "echo-nexus-beacon-lb-v1";
221→/** 每日进度 localStorage key(记录今日进度 + 是否领奖) */
222→export const BEACON_PROGRESS_KEY = "echo-nexus-beacon-prog-v1";
223→
224→/** 排行榜上限 */
225→export const BEACON_LB_MAX = 20;
226→
227→/** 今日进度记录 */
228→export interface BeaconDailyProgress {
229→ dateKey: string;
230→ progress: number;
231→ startedAt: number;
232→ completedAt: number | null;
233→ claimed: boolean;
234→ durationSec: number;
235→}
236→
237→/** 读取本地排行榜(按分数降序) */
238→export function loadLeaderboard(): BeaconScoreEntry[] {
239→ if (typeof localStorage === "undefined") return [];
240→ try {
241→ const raw = localStorage.getItem(BEACON_LEADERBOARD_KEY);
242→ if (!raw) return [];
243→ const arr = JSON.parse(raw) as BeaconScoreEntry[];
244→ return arr.sort((a, b) => b.score - a.score).slice(0, BEACON_LB_MAX);
245→ } catch {
246→ return [];
247→ }
248→}
249→
250→/** 写入一条排行榜记录 */
251→export function pushLeaderboardEntry(entry: BeaconScoreEntry): BeaconScoreEntry[] {
252→ const lb = loadLeaderboard();
253→ lb.push(entry);
254→ lb.sort((a, b) => b.score - a.score);
255→ const trimmed = lb.slice(0, BEACON_LB_MAX);
256→ if (typeof localStorage !== "undefined") {
257→ localStorage.setItem(BEACON_LEADERBOARD_KEY, JSON.stringify(trimmed));
258→ }
259→ return trimmed;
260→}
261→
262→/** 读取今日进度(若 dateKey 不匹配则重置) */
263→export function loadDailyProgress(now: Date = new Date()): BeaconDailyProgress {
264→ const todayKey = getTodayKey(now);
265→ if (typeof localStorage === "undefined") {
266→ return { dateKey: todayKey, progress: 0, startedAt: Date.now(), completedAt: null, claimed: false, durationSec: 0 };
267→ }
268→ try {
269→ const raw = localStorage.getItem(BEACON_PROGRESS_KEY);
270→ if (!raw) {
271→ return { dateKey: todayKey, progress: 0, startedAt: Date.now(), completedAt: null, claimed: false, durationSec: 0 };
272→ }
273→ const prog = JSON.parse(raw) as BeaconDailyProgress;
274→ if (prog.dateKey !== todayKey) {
275→ // 新的一天,重置
276→ return { dateKey: todayKey, progress: 0, startedAt: Date.now(), completedAt: null, claimed: false, durationSec: 0 };
277→ }
278→ return prog;
279→ } catch {
280→ return { dateKey: todayKey, progress: 0, startedAt: Date.now(), completedAt: null, claimed: false, durationSec: 0 };
281→ }
282→}
283→
284→/** 保存今日进度 */
285→export function saveDailyProgress(prog: BeaconDailyProgress): void {
286→ if (typeof localStorage === "undefined") return;
287→ localStorage.setItem(BEACON_PROGRESS_KEY, JSON.stringify(prog));
288→}
289→
290→/** 增量更新进度,返回新进度 + 是否刚完成 */
291→export function addBeaconProgress(
292→ current: BeaconDailyProgress,
293→ challenge: BeaconDailyChallenge,
294→ delta: number
295→): { progress: BeaconDailyProgress; justCompleted: boolean } {
296→ const newProgressVal = Math.min(challenge.goal, current.progress + delta);
297→ const justCompleted = current.completedAt === null && newProgressVal >= challenge.goal;
298→ const completedAt = justCompleted ? Date.now() : current.completedAt;
299→ const durationSec =
300→ completedAt !== null ? Math.floor((completedAt - current.startedAt) / 1000) : current.durationSec;
301→ const next: BeaconDailyProgress = {
302→ ...current,
303→ progress: newProgressVal,
304→ completedAt,
305→ durationSec,
306→ };
307→ saveDailyProgress(next);
308→ return { progress: next, justCompleted };
309→}
310→
311→/** 领取奖励:返回奖励数值 + 推送排行榜 */
312→export function claimBeaconReward(
313→ challenge: BeaconDailyChallenge,
314→ progress: BeaconDailyProgress
315→): { rewardInsight: number; rewardContact: number; score: number; leaderboard: BeaconScoreEntry[] } {
316→ if (progress.claimed || progress.completedAt === null) {
317→ return { rewardInsight: 0, rewardContact: 0, score: 0, leaderboard: loadLeaderboard() };
318→ }
319→ const score = computeBeaconScore(challenge, progress.progress, progress.durationSec);
320→ const entry: BeaconScoreEntry = {
321→ timestamp: Date.now(),
322→ dateKey: challenge.dateKey,
323→ challenge: challenge.type,
324→ difficulty: challenge.difficulty,
325→ progress: progress.progress / challenge.goal,
326→ score,
327→ durationSec: progress.durationSec,
328→ };
329→ const leaderboard = pushLeaderboardEntry(entry);
330→ const updated: BeaconDailyProgress = { ...progress, claimed: true };
331→ saveDailyProgress(updated);
332→ return {
333→ rewardInsight: challenge.rewardInsight,
334→ rewardContact: challenge.rewardContact,
335→ score,
336→ leaderboard,
337→ };
338→}
339→
340→/** 距离 UTC 次日 0 点的毫秒数(用于倒计时) */
341→export function msUntilNextDay(now: Date = new Date()): number {
342→ const next = Date.UTC(
343→ now.getUTCFullYear(),
344→ now.getUTCMonth(),
345→ now.getUTCDate() + 1,
346→ 0,
347→ 0,
348→ 0
349→ );
350→ return Math.max(0, next - now.getTime());
351→}
352→
353→/** 格式化倒计时为 HH:MM:SS */
354→export function formatCountdown(ms: number): string {
355→ const total = Math.max(0, Math.floor(ms / 1000));
356→ const h = String(Math.floor(total / 3600)).padStart(2, "0");
357→ const m = String(Math.floor((total % 3600) / 60)).padStart(2, "0");
358→ const s = String(total % 60).padStart(2, "0");
359→ return `${h}:${m}:${s}`;
360→}
361→
362→// ===========================================================================
363→// 周挑战(WEEKLY CHALLENGE)— v0.8
364→// 每周一 UTC 0 点刷新一个"周挑战",目标更大、奖励更好,与日挑战并行存在。
365→// 同一周同一种子 → 同一挑战(确定性)。难度强制 anomaly/singular(加权 60/40)。
366→// ===========================================================================
367→
368→/** 周挑战 localStorage key */
369→export const BEACON_WEEKLY_KEY = "echo-nexus-beacon-weekly-v1";
370→
371→/** 周挑战定义 */
372→export interface BeaconWeeklyChallenge {
373→ /** ISO 周键(YYYY-Www,如 "2026-W26" */
374→ weekKey: string;
375→ /** 挑战类型 */
376→ type: BeaconChallengeType;
377→ /** 难度(固定 anomaly 或 singular */
378→ difficulty: "anomaly" | "singular";
379→ /** 目标数值(日挑战的 3-5 倍) */
380→ goal: number;
381→ /** 奖励:完成时洞见 */
382→ rewardInsight: number;
383→ /** 奖励:完成时接触进度 */
384→ rewardContact: number;
385→ /** 使用的种子(可复现) */
386→ seed: number;
387→ /** 友好标题 */
388→ title: string;
389→ /** 描述 */
390→ desc: string;
391→}
392→
393→/** 周挑战进度 */
394→export interface BeaconWeeklyProgress {
395→ weekKey: string;
396→ progress: number;
397→ startedAt: number;
398→ completedAt: number | null;
399→ claimed: boolean;
400→ durationSec: number;
401→}
402→
403→/**
404→ * 计算 ISO 8601 周键(YYYY-Www),如 "2026-W26"。
405→ * 规则:周一为一周开始;第一周是该年至少含 4 天的第一周(即包含第一个周四)。
406→ */
407→export function getWeekKey(now: Date = new Date()): string {
408→ // 取 UTC 日期,避免时区偏移
409→ const tmp = new Date(
410→ Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
411→ );
412→ // ISO 周以周一为起点:(getUTCDay + 6) % 7 → 0=Mon, 6=Sun
413→ const dayNum = (tmp.getUTCDay() + 6) % 7;
414→ // 把日期调整到本周四(ISO 周归属年份由该周四所在的年份决定)
415→ tmp.setUTCDate(tmp.getUTCDate() - dayNum + 3);
416→ const isoYear = tmp.getUTCFullYear();
417→ const yearStart = Date.UTC(isoYear, 0, 1);
418→ const weekNum = Math.ceil(((tmp.getTime() - yearStart) / 86400000 + 1) / 7);
419→ return `${isoYear}-W${String(weekNum).padStart(2, "0")}`;
420→}
421→
422→/** weekKey → 数值种子(FNV-1a 哈希) */
423→function weekKeyToSeed(weekKey: string): number {
424→ let h = 2166136261 >>> 0;
425→ for (let i = 0; i < weekKey.length; i++) {
426→ h ^= weekKey.charCodeAt(i);
427→ h = Math.imul(h, 16777619) >>> 0;
428→ }
429→ return h >>> 0;
430→}
431→
432→/**
433→ * 生成本周周挑战(确定性:同一周同一种子 → 同一挑战)。
434→ * 难度强制 anomaly60%)或 singular40%),goal 为日挑战基准 ×3-5 倍。
435→ */
436→export function generateWeeklyChallenge(
437→ now: Date = new Date()
438→): BeaconWeeklyChallenge {
439→ const weekKey = getWeekKey(now);
440→ const seed = weekKeyToSeed(weekKey);
441→ const rng = mulberry32(seed);
442→
443→ const types: BeaconChallengeType[] = [
444→ "decode",
445→ "expedition",
446→ "pulse",
447→ "boss",
448→ "insight",
449→ ];
450→ const type = types[Math.floor(rng() * types.length)];
451→
452→ // 难度加权:anomaly 60% / singular 40%
453→ const dr = rng();
454→ const difficulty: "anomaly" | "singular" = dr < 0.6 ? "anomaly" : "singular";
455→ const diffMult = BEACON_DIFFICULTY[difficulty].mult;
456→
457→ // 3-5 倍
458→ const mult = 3 + Math.floor(rng() * 3);
459→
460→ let goal = 0;
461→ let rewardInsight = 0;
462→ let rewardContact = 0;
463→ let title = "";
464→ let desc = "";
465→
466→ switch (type) {
467→ case "decode":
468→ // 日基准 6-15 × mult(3-5) → 18-75
469→ goal = Math.max(18, Math.round((6 + Math.floor(rng() * 10)) * diffMult * mult));
470→ rewardInsight = Math.round(goal * 5 * diffMult);
471→ rewardContact = goal * 1.2;
472→ title = `周界解码 · ${goal} 颗晶体`;
473→ desc = `本周深空信标要求解码 ${goal} 颗记忆晶体,强度为日挑战的 ${mult} 倍。`;
474→ break;
475→ case "expedition":
476→ // 日基准 1-3 × mult(3-5) → 3-15
477→ goal = Math.max(3, Math.round((1 + Math.floor(rng() * 3)) * diffMult * mult));
478→ rewardInsight = Math.round(goal * 14 * diffMult);
479→ rewardContact = goal * 2;
480→ title = `周界远征 · ${goal} 次探险`;
481→ desc = `本周派出探险队完成 ${goal} 次远征,无论胜负均计入。`;
482→ break;
483→ case "pulse":
484→ // 日基准 20-49 × mult(3-5) → 60-245
485→ goal = Math.max(60, Math.round((20 + Math.floor(rng() * 30)) * diffMult * mult));
486→ rewardInsight = Math.round(goal * 1.5 * diffMult);
487→ rewardContact = goal * 0.4;
488→ title = `周界脉冲 · ${goal} 次扫描`;
489→ desc = `本周发起 ${goal} 次脉冲扫描。连击可叠加加成。`;
490→ break;
491→ case "boss":
492→ // 日基准 1 × mult(3-5) → 3-12singular mult=2.4 时上限 12
493→ goal = Math.max(3, Math.round(mult * diffMult));
494→ rewardInsight = Math.round(goal * 35 * diffMult);
495→ rewardContact = goal * 4;
496→ title = `周界猎杀 · ${goal} 处 BOSS`;
497→ desc = `本周在探险终点击破 ${goal} 处维度 BOSS。提升探险力后再挑战。`;
498→ break;
499→ case "insight":
500→ // 日基准 40-119 × mult(3-5) → 120-595
501→ goal = Math.max(120, Math.round((40 + Math.floor(rng() * 80)) * diffMult * mult));
502→ rewardInsight = 0; // 洞见挑战不给洞见,给接触
503→ rewardContact = goal * 0.15;
504→ title = `周界洞见 · ${goal} 点`;
505→ desc = `本周通过解码、探险、星潮等途径累计 ${goal} 洞见。`;
506→ break;
507→ }
508→
509→ return {
510→ weekKey,
511→ type,
512→ difficulty,
513→ goal,
514→ rewardInsight,
515→ rewardContact,
516→ seed,
517→ title,
518→ desc,
519→ };
520→}
521→
522→/** 读取本周进度(若 weekKey 不匹配则重置) */
523→export function loadWeeklyProgress(now: Date = new Date()): BeaconWeeklyProgress {
524→ const weekKey = getWeekKey(now);
525→ const empty: BeaconWeeklyProgress = {
526→ weekKey,
527→ progress: 0,
528→ startedAt: Date.now(),
529→ completedAt: null,
530→ claimed: false,
531→ durationSec: 0,
532→ };
533→ if (typeof localStorage === "undefined") return empty;
534→ try {
535→ const raw = localStorage.getItem(BEACON_WEEKLY_KEY);
536→ if (!raw) return empty;
537→ const prog = JSON.parse(raw) as BeaconWeeklyProgress;
538→ if (prog.weekKey !== weekKey) return empty; // 新的一周,重置
539→ return prog;
540→ } catch {
541→ return empty;
542→ }
543→}
544→
545→/** 保存本周进度 */
546→export function saveWeeklyProgress(prog: BeaconWeeklyProgress): void {
547→ if (typeof localStorage === "undefined") return;
548→ localStorage.setItem(BEACON_WEEKLY_KEY, JSON.stringify(prog));
549→}
550→
551→/** 增量更新周挑战进度,返回新进度 + 是否刚完成 */
552→export function addWeeklyProgress(
553→ current: BeaconWeeklyProgress,
554→ challenge: BeaconWeeklyChallenge,
555→ delta: number
556→): { progress: BeaconWeeklyProgress; justCompleted: boolean } {
557→ const newProgressVal = Math.min(challenge.goal, current.progress + delta);
558→ const justCompleted =
559→ current.completedAt === null && newProgressVal >= challenge.goal;
560→ const completedAt = justCompleted ? Date.now() : current.completedAt;
561→ const durationSec =
562→ completedAt !== null
563→ ? Math.floor((completedAt - current.startedAt) / 1000)
564→ : current.durationSec;
565→ const next: BeaconWeeklyProgress = {
566→ ...current,
567→ progress: newProgressVal,
568→ completedAt,
569→ durationSec,
570→ };
571→ saveWeeklyProgress(next);
572→ return { progress: next, justCompleted };
573→}
574→
575→/** 领取周挑战奖励:返回奖励数值 + 推送排行榜 */
576→export function claimWeeklyReward(
577→ challenge: BeaconWeeklyChallenge,
578→ progress: BeaconWeeklyProgress
579→): {
580→ rewardInsight: number;
581→ rewardContact: number;
582→ score: number;
583→ leaderboard: BeaconScoreEntry[];
584→} {
585→ if (progress.claimed || progress.completedAt === null) {
586→ return {
587→ rewardInsight: 0,
588→ rewardContact: 0,
589→ score: 0,
590→ leaderboard: loadLeaderboard(),
591→ };
592→ }
593→ // 借用 computeBeaconScore:把 weekly 包装成 daily 接口(dateKey 字段不影响计分逻辑)
594→ const score = computeBeaconScore(
595→ { ...(challenge as unknown as BeaconDailyChallenge), dateKey: challenge.weekKey },
596→ progress.progress,
597→ progress.durationSec
598→ );
599→ const entry: BeaconScoreEntry = {
600→ timestamp: Date.now(),
601→ dateKey: challenge.weekKey,
602→ challenge: challenge.type,
603→ difficulty: challenge.difficulty,
604→ progress: progress.progress / challenge.goal,
605→ score,
606→ durationSec: progress.durationSec,
607→ isWeekly: true,
608→ };
609→ const leaderboard = pushLeaderboardEntry(entry);
610→ const updated: BeaconWeeklyProgress = { ...progress, claimed: true };
611→ saveWeeklyProgress(updated);
612→ return {
613→ rewardInsight: challenge.rewardInsight,
614→ rewardContact: challenge.rewardContact,
615→ score,
616→ leaderboard,
617→ };
618→}
619→
620→/** 距离下周一 UTC 0 点的毫秒数(用于周挑战倒计时) */
621→export function msUntilNextWeek(now: Date = new Date()): number {
622→ const dayNum = (now.getUTCDay() + 6) % 7; // 0 = Mon
623→ const mondayThisWeek = Date.UTC(
624→ now.getUTCFullYear(),
625→ now.getUTCMonth(),
626→ now.getUTCDate() - dayNum,
627→ 0,
628→ 0,
629→ 0
630→ );
631→ const nextMonday = mondayThisWeek + 7 * 86400000;
632→ return Math.max(0, nextMonday - now.getTime());
633→}
634→
635→// ===========================================================================
636→// 信标链(BEACON CHAIN)— v0.8
637→// 连续完成日挑战形成"链",达成里程碑(3/7/14/30 天)领取递增大奖。
638→// 断链有宽容机制(1 天 miss 不断链,用"信标续命"概念),每条链只能用 1 次。
639→// ===========================================================================
640→
641→/** 信标链 localStorage key */
642→export const BEACON_CHAIN_KEY = "echo-nexus-beacon-chain-v1";
643→
644→/** 里程碑天数 */
645→export const BEACON_CHAIN_MILESTONES = [3, 7, 14, 30] as const;
646→
647→/** 里程碑奖励配置 */
648→export interface BeaconChainReward {
649→ milestone: number;
650→ rewardInsight: number;
651→ rewardContact: number;
652→ label: string;
653→}
654→
655→export const BEACON_CHAIN_REWARDS: BeaconChainReward[] = [
656→ { milestone: 3, rewardInsight: 50, rewardContact: 5, label: "三日谐振" },
657→ { milestone: 7, rewardInsight: 120, rewardContact: 12, label: "七日回响" },
658→ { milestone: 14, rewardInsight: 280, rewardContact: 28, label: "半月星潮" },
659→ { milestone: 30, rewardInsight: 680, rewardContact: 68, label: "满月飞升" },
660→];
661→
662→/** 信标链状态 */
663→export interface BeaconChainState {
664→ /** 上次完成日(YYYY-MM-DD */
665→ lastCompletedDateKey: string;
666→ /** 当前连续天数 */
667→ currentStreak: number;
668→ /** 历史最长 */
669→ longestStreak: number;
670→ /** 累计完成总数 */
671→ totalCompletions: number;
672→ /** 本周期已用续命数(上限 1) */
673→ graceUsed: number;
674→ /** 已领取的里程碑数组 */
675→ milestonesClaimed: number[];
676→}
677→
678→/** 读取信标链状态 */
679→export function loadChainState(): BeaconChainState {
680→ const fresh = (): BeaconChainState => ({
681→ lastCompletedDateKey: "",
682→ currentStreak: 0,
683→ longestStreak: 0,
684→ totalCompletions: 0,
685→ graceUsed: 0,
686→ milestonesClaimed: [],
687→ });
688→ if (typeof localStorage === "undefined") return fresh();
689→ try {
690→ const raw = localStorage.getItem(BEACON_CHAIN_KEY);
691→ if (!raw) return fresh();
692→ const s = JSON.parse(raw) as Partial<BeaconChainState>;
693→ return {
694→ lastCompletedDateKey: s.lastCompletedDateKey ?? "",
695→ currentStreak: s.currentStreak ?? 0,
696→ longestStreak: s.longestStreak ?? 0,
697→ totalCompletions: s.totalCompletions ?? 0,
698→ graceUsed: s.graceUsed ?? 0,
699→ milestonesClaimed: Array.isArray(s.milestonesClaimed)
700→ ? [...s.milestonesClaimed]
701→ : [],
702→ };
703→ } catch {
704→ return fresh();
705→ }
706→}
707→
708→/** 保存信标链状态 */
709→export function saveChainState(state: BeaconChainState): void {
710→ if (typeof localStorage === "undefined") return;
711→ localStorage.setItem(BEACON_CHAIN_KEY, JSON.stringify(state));
712→}
713→
714→/** dateKey → UTC 0 点时间戳 */
715→function dateKeyToTimestamp(dateKey: string): number {
716→ const [y, m, d] = dateKey.split("-").map(Number);
717→ return Date.UTC(y, m - 1, d, 0, 0, 0);
718→}
719→
720→/** 计算 b - a 相差的天数(UTC 0 点对齐) */
721→function dateKeyDiffDays(a: string, b: string): number {
722→ if (!a || !b) return Number.MAX_SAFE_INTEGER;
723→ return Math.round((dateKeyToTimestamp(b) - dateKeyToTimestamp(a)) / 86400000);
724→}
725→
726→/**
727→ * 记录一次日挑战完成(核心逻辑)。
728→ * - dateKey === lastCompletedDateKey:同一天重复完成,忽略,返回 newMilestones: []
729→ * - dateKey 是 lastCompletedDateKey 的次日:currentStreak++
730→ * - dateKey 是 lastCompletedDateKey 的后两天(隔了一天 miss)且 graceUsed < 1:续命一次,currentStreak++
731→ * - 其他:currentStreak = 1(断链重来),graceUsed = 0
732→ * 更新 longestStreak 与 totalCompletions。
733→ * @returns { state, newMilestones } 刚达成但未领取的里程碑数组
734→ */
735→export function recordChainCompletion(dateKey: string): {
736→ state: BeaconChainState;
737→ newMilestones: number[];
738→} {
739→ const state = loadChainState();
740→
741→ // 同一天重复完成:忽略
742→ if (state.lastCompletedDateKey === dateKey) {
743→ return { state, newMilestones: [] };
744→ }
745→
746→ let next: BeaconChainState;
747→
748→ if (state.lastCompletedDateKey === "") {
749→ // 首次完成
750→ next = {
751→ ...state,
752→ lastCompletedDateKey: dateKey,
753→ currentStreak: 1,
754→ longestStreak: Math.max(state.longestStreak, 1),
755→ totalCompletions: state.totalCompletions + 1,
756→ };
757→ } else {
758→ const diff = dateKeyDiffDays(state.lastCompletedDateKey, dateKey);
759→ if (diff === 1) {
760→ // 次日:链 +1
761→ const newStreak = state.currentStreak + 1;
762→ next = {
763→ ...state,
764→ lastCompletedDateKey: dateKey,
765→ currentStreak: newStreak,
766→ longestStreak: Math.max(state.longestStreak, newStreak),
767→ totalCompletions: state.totalCompletions + 1,
768→ };
769→ } else if (diff === 2 && state.graceUsed < 1) {
770→ // 隔一天 miss,续命一次
771→ const newStreak = state.currentStreak + 1;
772→ next = {
773→ ...state,
774→ lastCompletedDateKey: dateKey,
775→ currentStreak: newStreak,
776→ longestStreak: Math.max(state.longestStreak, newStreak),
777→ totalCompletions: state.totalCompletions + 1,
778→ graceUsed: state.graceUsed + 1,
779→ };
780→ } else {
781→ // 断链重来
782→ next = {
783→ ...state,
784→ lastCompletedDateKey: dateKey,
785→ currentStreak: 1,
786→ totalCompletions: state.totalCompletions + 1,
787→ graceUsed: 0,
788→ longestStreak: Math.max(state.longestStreak, 1),
789→ };
790→ }
791→ }
792→
793→ // 检查新里程碑(刚达成但未领取)
794→ const newMilestones: number[] = [];
795→ for (const m of BEACON_CHAIN_MILESTONES) {
796→ if (
797→ next.currentStreak >= m &&
798→ !next.milestonesClaimed.includes(m)
799→ ) {
800→ newMilestones.push(m);
801→ }
802→ }
803→
804→ saveChainState(next);
805→ return { state: next, newMilestones };
806→}
807→
808→/** 领取里程碑奖励,加入 milestonesClaimed */
809→export function claimChainMilestone(milestone: number): {
810→ rewardInsight: number;
811→ rewardContact: number;
812→ label: string;
813→ state: BeaconChainState;
814→} {
815→ const reward = BEACON_CHAIN_REWARDS.find((r) => r.milestone === milestone);
816→ const state = loadChainState();
817→ if (
818→ !reward ||
819→ state.milestonesClaimed.includes(milestone) ||
820→ state.currentStreak < milestone
821→ ) {
822→ return { rewardInsight: 0, rewardContact: 0, label: "", state };
823→ }
824→ const next: BeaconChainState = {
825→ ...state,
826→ milestonesClaimed: [...state.milestonesClaimed, milestone],
827→ };
828→ saveChainState(next);
829→ return {
830→ rewardInsight: reward.rewardInsight,
831→ rewardContact: reward.rewardContact,
832→ label: reward.label,
833→ state: next,
834→ };
835→}
836→
837→/** 返回下一个目标里程碑(如 streak=5 → 7streak=30+ → null */
838→export function getNextMilestone(streak: number): number | null {
839→ for (const m of BEACON_CHAIN_MILESTONES) {
840→ if (streak < m) return m;
841→ }
842→ return null;
843→}
844→
845→/**
846→ * 返回信标链进度信息(用于 UI 进度条)。
847→ * - current:当前连续天数
848→ * - next:下一个目标里程碑(null 表示已通关全部)
849→ * - progressPct:当前进度(基于上一个里程碑 → 下一个里程碑)
850→ */
851→export function getChainProgress(streak: number): {
852→ current: number;
853→ next: number | null;
854→ prev: number;
855→ progressPct: number;
856→} {
857→ const next = getNextMilestone(streak);
858→ let prev = 0;
859→ for (const m of BEACON_CHAIN_MILESTONES) {
860→ if (streak >= m) prev = m;
861→ }
862→ if (next === null) {
863→ return { current: streak, next: null, prev, progressPct: 100 };
864→ }
865→ const span = next - prev;
866→ const done = streak - prev;
867→ const pct = span > 0 ? Math.round((done / span) * 100) : 100;
868→ return {
869→ current: streak,
870→ next,
871→ prev,
872→ progressPct: Math.min(100, Math.max(0, pct)),
873→ };
874→}
875→
@@ -1,875 +0,0 @@
1→ 1→// 回响星核 / Echo Nexus — 深空信标
2→ 2→// v0.5:每日挑战 + 本地排行榜
3→ 3→// v0.8:周挑战 + 信标链(连续完成奖励)
4→ 4→// 一个自包含的"每日挑战 + 周挑战 + 连续完成链"元系统,纯 localStorage 持久化。
5→ 5→
6→ 6→/** 每日挑战类型 */
7→ 7→export type BeaconChallengeType =
8→ 8→ | "decode" // 解码 N 颗晶体
9→ 9→ | "expedition" // 完成 N 次探险
10→ 10→ | "pulse" // 发起 N 次脉冲
11→ 11→ | "boss" // 击破 N 处 BOSS
12→ 12→ | "insight"; // 累计 N 洞见
13→ 13→
14→ 14→/** 挑战难度档位 */
15→ 15→export type BeaconDifficulty = "routine" | "anomaly" | "singular";
16→ 16→
17→ 17→/** 一条排行榜记录 */
18→ 18→export interface BeaconScoreEntry {
19→ 19→ /** 提交时间戳 */
20→ 20→ timestamp: number;
21→ 21→ /** 日期 keyYYYY-MM-DD)或周 keyYYYY-Www */
22→ 22→ dateKey: string;
23→ 23→ /** 挑战类型 */
24→ 24→ challenge: BeaconChallengeType;
25→ 25→ /** 难度 */
26→ 26→ difficulty: BeaconDifficulty;
27→ 27→ /** 完成度(0-11 = 完成) */
28→ 28→ progress: number;
29→ 29→ /** 最终得分 */
30→ 30→ score: number;
31→ 31→ /** 完成时长(秒),未完成则记 0 */
32→ 32→ durationSec: number;
33→ 33→ /** v0.8:是否为周挑战记录(日挑战默认 false / undefined */
34→ 34→ isWeekly?: boolean;
35→ 35→}
36→ 36→
37→ 37→/** 每日挑战定义 */
38→ 38→export interface BeaconDailyChallenge {
39→ 39→ /** 日期 keyYYYY-MM-DDUTC */
40→ 40→ dateKey: string;
41→ 41→ /** 挑战类型 */
42→ 42→ type: BeaconChallengeType;
43→ 43→ /** 难度 */
44→ 44→ difficulty: BeaconDifficulty;
45→ 45→ /** 目标数值 */
46→ 46→ goal: number;
47→ 47→ /** 奖励:完成时洞见 */
48→ 48→ rewardInsight: number;
49→ 49→ /** 奖励:完成时接触进度 */
50→ 50→ rewardContact: number;
51→ 51→ /** 使用的种子(可复现) */
52→ 52→ seed: number;
53→ 53→ /** 友好标题 */
54→ 54→ title: string;
55→ 55→ /** 描述 */
56→ 56→ desc: string;
57→ 57→}
58→ 58→
59→ 59→/** 难度配置 */
60→ 60→export const BEACON_DIFFICULTY: Record<
61→ 61→ BeaconDifficulty,
62→ 62→ { label: string; color: string; glow: string; mult: number; icon: string }
63→ 63→> = {
64→ 64→ routine: {
65→ 65→ label: "常规信标",
66→ 66→ color: "#34d399",
67→ 67→ glow: "rgba(52,211,153,0.45)",
68→ 68→ mult: 1,
69→ 69→ icon: "◍",
70→ 70→ },
71→ 71→ anomaly: {
72→ 72→ label: "异常波动",
73→ 73→ color: "#fbbf24",
74→ 74→ glow: "rgba(251,191,36,0.45)",
75→ 75→ mult: 1.6,
76→ 76→ icon: "◈",
77→ 77→ },
78→ 78→ singular: {
79→ 79→ label: "奇点回响",
80→ 80→ color: "#f43f5e",
81→ 81→ glow: "rgba(244,63,94,0.5)",
82→ 82→ mult: 2.4,
83→ 83→ icon: "✶",
84→ 84→ },
85→ 85→};
86→ 86→
87→ 87→/** 挑战类型元信息 */
88→ 88→export const BEACON_TYPE_META: Record<
89→ 89→ BeaconChallengeType,
90→ 90→ { label: string; icon: string; unit: string; verb: string }
91→ 91→> = {
92→ 92→ decode: { label: "解码协议", icon: "❖", unit: "颗", verb: "解码记忆晶体" },
93→ 93→ expedition: { label: "远征指令", icon: "⬢", unit: "次", verb: "完成遗迹探险" },
94→ 94→ pulse: { label: "脉冲任务", icon: "✺", unit: "次", verb: "发起脉冲扫描" },
95→ 95→ boss: { label: "猎杀契约", icon: "☠", unit: "处", verb: "击破维度 BOSS" },
96→ 96→ insight: { label: "洞见采集", icon: "✦", unit: "点", verb: "累计获取洞见" },
97→ 97→};
98→ 98→
99→ 99→/** 取今日日期 key(UTC,保证全球同一天同一挑战) */
100→ 100→export function getTodayKey(now: Date = new Date()): string {
101→ 101→ const y = now.getUTCFullYear();
102→ 102→ const m = String(now.getUTCMonth() + 1).padStart(2, "0");
103→ 103→ const d = String(now.getUTCDate()).padStart(2, "0");
104→ 104→ return `${y}-${m}-${d}`;
105→ 105→}
106→ 106→
107→ 107→/** 把日期 key 转成数值种子 */
108→ 108→function dateKeyToSeed(dateKey: string): number {
109→ 109→ let h = 2166136261 >>> 0;
110→ 110→ for (let i = 0; i < dateKey.length; i++) {
111→ 111→ h ^= dateKey.charCodeAt(i);
112→ 112→ h = Math.imul(h, 16777619) >>> 0;
113→ 113→ }
114→ 114→ return h >>> 0;
115→ 115→}
116→ 116→
117→ 117→/** mulberry32 PRNG(可复现) */
118→ 118→function mulberry32(seed: number): () => number {
119→ 119→ let a = seed >>> 0;
120→ 120→ return () => {
121→ 121→ a |= 0;
122→ 122→ a = (a + 0x6d2b79f5) | 0;
123→ 123→ let t = Math.imul(a ^ (a >>> 15), 1 | a);
124→ 124→ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
125→ 125→ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
126→ 126→ };
127→ 127→}
128→ 128→
129→ 129→/** 生成今日每日挑战(确定性:同一天同一种子 → 同一挑战) */
130→ 130→export function generateDailyChallenge(now: Date = new Date()): BeaconDailyChallenge {
131→ 131→ const dateKey = getTodayKey(now);
132→ 132→ const seed = dateKeyToSeed(dateKey);
133→ 133→ const rng = mulberry32(seed);
134→ 134→
135→ 135→ // 选类型(5 种)
136→ 136→ const types: BeaconChallengeType[] = ["decode", "expedition", "pulse", "boss", "insight"];
137→ 137→ const type = types[Math.floor(rng() * types.length)];
138→ 138→
139→ 139→ // 选难度(加权:常规 55% / 异常 33% / 奇点 12%
140→ 140→ const dr = rng();
141→ 141→ const difficulty: BeaconDifficulty =
142→ 142→ dr < 0.55 ? "routine" : dr < 0.88 ? "anomaly" : "singular";
143→ 143→
144→ 144→ const diffMult = BEACON_DIFFICULTY[difficulty].mult;
145→ 145→
146→ 146→ // 按类型定 goal 与奖励
147→ 147→ let goal = 0;
148→ 148→ let rewardInsight = 0;
149→ 149→ let rewardContact = 0;
150→ 150→ let title = "";
151→ 151→ let desc = "";
152→ 152→
153→ 153→ switch (type) {
154→ 154→ case "decode":
155→ 155→ goal = Math.round((6 + Math.floor(rng() * 10)) * diffMult); // 6-15 × mult
156→ 156→ rewardInsight = Math.round(goal * 4 * diffMult);
157→ 157→ rewardContact = goal * 0.8;
158→ 158→ title = `解码 ${goal} 颗记忆晶体`;
159→ 159→ desc = `深空信标要求解码 ${goal} 颗晶体。谐振风暴期间效率更高。`;
160→ 160→ break;
161→ 161→ case "expedition":
162→ 162→ goal = Math.max(1, Math.round((1 + Math.floor(rng() * 3)) * diffMult)); // 1-3 × mult
163→ 163→ rewardInsight = Math.round(goal * 12 * diffMult);
164→ 164→ rewardContact = goal * 1.5;
165→ 165→ title = `完成 ${goal} 次遗迹探险`;
166→ 166→ desc = `派出探险队完成 ${goal} 次远征,无论胜负均计入。`;
167→ 167→ break;
168→ 168→ case "pulse":
169→ 169→ goal = Math.round((20 + Math.floor(rng() * 30)) * diffMult); // 20-49 × mult
170→ 170→ rewardInsight = Math.round(goal * 1.2 * diffMult);
171→ 171→ rewardContact = goal * 0.3;
172→ 172→ title = `发起 ${goal} 次脉冲扫描`;
173→ 173→ desc = `主动点击晶体发起 ${goal} 次脉冲。连击可叠加加成。`;
174→ 174→ break;
175→ 175→ case "boss":
176→ 176→ goal = Math.max(1, Math.round(diffMult)); // 奇点至少 2-3
177→ 177→ rewardInsight = Math.round(goal * 30 * diffMult);
178→ 178→ rewardContact = goal * 3;
179→ 179→ title = `击破 ${goal} 处维度 BOSS`;
180→ 180→ desc = `在探险终点击破 ${goal} 处 BOSS。提升探险力后再挑战。`;
181→ 181→ break;
182→ 182→ case "insight":
183→ 183→ goal = Math.round((40 + Math.floor(rng() * 80)) * diffMult); // 40-119 × mult
184→ 184→ rewardInsight = 0; // 洞见挑战不给洞见,给接触
185→ 185→ rewardContact = goal * 0.1;
186→ 186→ title = `累计获取 ${goal} 洞见`;
187→ 187→ desc = `通过解码、探险、星潮等途径累计 ${goal} 洞见。`;
188→ 188→ break;
189→ 189→ }
190→ 190→
191→ 191→ return {
192→ 192→ dateKey,
193→ 193→ type,
194→ 194→ difficulty,
195→ 195→ goal,
196→ 196→ rewardInsight,
197→ 197→ rewardContact,
198→ 198→ seed,
199→ 199→ title,
200→ 200→ desc,
201→ 201→ };
202→ 202→}
203→ 203→
204→ 204→/** 计算挑战得分(用于排行榜) */
205→ 205→export function computeBeaconScore(
206→ 206→ challenge: BeaconDailyChallenge,
207→ 207→ progress: number,
208→ 208→ durationSec: number
209→ 209→): number {
210→ 210→ const completion = Math.min(1, progress / challenge.goal);
211→ 211→ const diffMult = BEACON_DIFFICULTY[challenge.difficulty].mult;
212→ 212→ // 基础分 = 完成度 × 难度 × 1000;完成时长越短加分越多(上限 +500)
213→ 213→ const base = completion * 1000 * diffMult;
214→ 214→ const speedBonus =
215→ 215→ completion >= 1 && durationSec > 0 ? Math.max(0, 500 - durationSec * 0.5) : 0;
216→ 216→ return Math.round(base + speedBonus);
217→ 217→}
218→ 218→
219→ 219→/** 排行榜 localStorage key */
220→ 220→export const BEACON_LEADERBOARD_KEY = "echo-nexus-beacon-lb-v1";
221→ 221→/** 每日进度 localStorage key(记录今日进度 + 是否领奖) */
222→ 222→export const BEACON_PROGRESS_KEY = "echo-nexus-beacon-prog-v1";
223→ 223→
224→ 224→/** 排行榜上限 */
225→ 225→export const BEACON_LB_MAX = 20;
226→ 226→
227→ 227→/** 今日进度记录 */
228→ 228→export interface BeaconDailyProgress {
229→ 229→ dateKey: string;
230→ 230→ progress: number;
231→ 231→ startedAt: number;
232→ 232→ completedAt: number | null;
233→ 233→ claimed: boolean;
234→ 234→ durationSec: number;
235→ 235→}
236→ 236→
237→ 237→/** 读取本地排行榜(按分数降序) */
238→ 238→export function loadLeaderboard(): BeaconScoreEntry[] {
239→ 239→ if (typeof localStorage === "undefined") return [];
240→ 240→ try {
241→ 241→ const raw = localStorage.getItem(BEACON_LEADERBOARD_KEY);
242→ 242→ if (!raw) return [];
243→ 243→ const arr = JSON.parse(raw) as BeaconScoreEntry[];
244→ 244→ return arr.sort((a, b) => b.score - a.score).slice(0, BEACON_LB_MAX);
245→ 245→ } catch {
246→ 246→ return [];
247→ 247→ }
248→ 248→}
249→ 249→
250→ 250→/** 写入一条排行榜记录 */
251→ 251→export function pushLeaderboardEntry(entry: BeaconScoreEntry): BeaconScoreEntry[] {
252→ 252→ const lb = loadLeaderboard();
253→ 253→ lb.push(entry);
254→ 254→ lb.sort((a, b) => b.score - a.score);
255→ 255→ const trimmed = lb.slice(0, BEACON_LB_MAX);
256→ 256→ if (typeof localStorage !== "undefined") {
257→ 257→ localStorage.setItem(BEACON_LEADERBOARD_KEY, JSON.stringify(trimmed));
258→ 258→ }
259→ 259→ return trimmed;
260→ 260→}
261→ 261→
262→ 262→/** 读取今日进度(若 dateKey 不匹配则重置) */
263→ 263→export function loadDailyProgress(now: Date = new Date()): BeaconDailyProgress {
264→ 264→ const todayKey = getTodayKey(now);
265→ 265→ if (typeof localStorage === "undefined") {
266→ 266→ return { dateKey: todayKey, progress: 0, startedAt: Date.now(), completedAt: null, claimed: false, durationSec: 0 };
267→ 267→ }
268→ 268→ try {
269→ 269→ const raw = localStorage.getItem(BEACON_PROGRESS_KEY);
270→ 270→ if (!raw) {
271→ 271→ return { dateKey: todayKey, progress: 0, startedAt: Date.now(), completedAt: null, claimed: false, durationSec: 0 };
272→ 272→ }
273→ 273→ const prog = JSON.parse(raw) as BeaconDailyProgress;
274→ 274→ if (prog.dateKey !== todayKey) {
275→ 275→ // 新的一天,重置
276→ 276→ return { dateKey: todayKey, progress: 0, startedAt: Date.now(), completedAt: null, claimed: false, durationSec: 0 };
277→ 277→ }
278→ 278→ return prog;
279→ 279→ } catch {
280→ 280→ return { dateKey: todayKey, progress: 0, startedAt: Date.now(), completedAt: null, claimed: false, durationSec: 0 };
281→ 281→ }
282→ 282→}
283→ 283→
284→ 284→/** 保存今日进度 */
285→ 285→export function saveDailyProgress(prog: BeaconDailyProgress): void {
286→ 286→ if (typeof localStorage === "undefined") return;
287→ 287→ localStorage.setItem(BEACON_PROGRESS_KEY, JSON.stringify(prog));
288→ 288→}
289→ 289→
290→ 290→/** 增量更新进度,返回新进度 + 是否刚完成 */
291→ 291→export function addBeaconProgress(
292→ 292→ current: BeaconDailyProgress,
293→ 293→ challenge: BeaconDailyChallenge,
294→ 294→ delta: number
295→ 295→): { progress: BeaconDailyProgress; justCompleted: boolean } {
296→ 296→ const newProgressVal = Math.min(challenge.goal, current.progress + delta);
297→ 297→ const justCompleted = current.completedAt === null && newProgressVal >= challenge.goal;
298→ 298→ const completedAt = justCompleted ? Date.now() : current.completedAt;
299→ 299→ const durationSec =
300→ 300→ completedAt !== null ? Math.floor((completedAt - current.startedAt) / 1000) : current.durationSec;
301→ 301→ const next: BeaconDailyProgress = {
302→ 302→ ...current,
303→ 303→ progress: newProgressVal,
304→ 304→ completedAt,
305→ 305→ durationSec,
306→ 306→ };
307→ 307→ saveDailyProgress(next);
308→ 308→ return { progress: next, justCompleted };
309→ 309→}
310→ 310→
311→ 311→/** 领取奖励:返回奖励数值 + 推送排行榜 */
312→ 312→export function claimBeaconReward(
313→ 313→ challenge: BeaconDailyChallenge,
314→ 314→ progress: BeaconDailyProgress
315→ 315→): { rewardInsight: number; rewardContact: number; score: number; leaderboard: BeaconScoreEntry[] } {
316→ 316→ if (progress.claimed || progress.completedAt === null) {
317→ 317→ return { rewardInsight: 0, rewardContact: 0, score: 0, leaderboard: loadLeaderboard() };
318→ 318→ }
319→ 319→ const score = computeBeaconScore(challenge, progress.progress, progress.durationSec);
320→ 320→ const entry: BeaconScoreEntry = {
321→ 321→ timestamp: Date.now(),
322→ 322→ dateKey: challenge.dateKey,
323→ 323→ challenge: challenge.type,
324→ 324→ difficulty: challenge.difficulty,
325→ 325→ progress: progress.progress / challenge.goal,
326→ 326→ score,
327→ 327→ durationSec: progress.durationSec,
328→ 328→ };
329→ 329→ const leaderboard = pushLeaderboardEntry(entry);
330→ 330→ const updated: BeaconDailyProgress = { ...progress, claimed: true };
331→ 331→ saveDailyProgress(updated);
332→ 332→ return {
333→ 333→ rewardInsight: challenge.rewardInsight,
334→ 334→ rewardContact: challenge.rewardContact,
335→ 335→ score,
336→ 336→ leaderboard,
337→ 337→ };
338→ 338→}
339→ 339→
340→ 340→/** 距离 UTC 次日 0 点的毫秒数(用于倒计时) */
341→ 341→export function msUntilNextDay(now: Date = new Date()): number {
342→ 342→ const next = Date.UTC(
343→ 343→ now.getUTCFullYear(),
344→ 344→ now.getUTCMonth(),
345→ 345→ now.getUTCDate() + 1,
346→ 346→ 0,
347→ 347→ 0,
348→ 348→ 0
349→ 349→ );
350→ 350→ return Math.max(0, next - now.getTime());
351→ 351→}
352→ 352→
353→ 353→/** 格式化倒计时为 HH:MM:SS */
354→ 354→export function formatCountdown(ms: number): string {
355→ 355→ const total = Math.max(0, Math.floor(ms / 1000));
356→ 356→ const h = String(Math.floor(total / 3600)).padStart(2, "0");
357→ 357→ const m = String(Math.floor((total % 3600) / 60)).padStart(2, "0");
358→ 358→ const s = String(total % 60).padStart(2, "0");
359→ 359→ return `${h}:${m}:${s}`;
360→ 360→}
361→ 361→
362→ 362→// ===========================================================================
363→ 363→// 周挑战(WEEKLY CHALLENGE)— v0.8
364→ 364→// 每周一 UTC 0 点刷新一个"周挑战",目标更大、奖励更好,与日挑战并行存在。
365→ 365→// 同一周同一种子 → 同一挑战(确定性)。难度强制 anomaly/singular(加权 60/40)。
366→ 366→// ===========================================================================
367→ 367→
368→ 368→/** 周挑战 localStorage key */
369→ 369→export const BEACON_WEEKLY_KEY = "echo-nexus-beacon-weekly-v1";
370→ 370→
371→ 371→/** 周挑战定义 */
372→ 372→export interface BeaconWeeklyChallenge {
373→ 373→ /** ISO 周键(YYYY-Www,如 "2026-W26" */
374→ 374→ weekKey: string;
375→ 375→ /** 挑战类型 */
376→ 376→ type: BeaconChallengeType;
377→ 377→ /** 难度(固定 anomaly 或 singular */
378→ 378→ difficulty: "anomaly" | "singular";
379→ 379→ /** 目标数值(日挑战的 3-5 倍) */
380→ 380→ goal: number;
381→ 381→ /** 奖励:完成时洞见 */
382→ 382→ rewardInsight: number;
383→ 383→ /** 奖励:完成时接触进度 */
384→ 384→ rewardContact: number;
385→ 385→ /** 使用的种子(可复现) */
386→ 386→ seed: number;
387→ 387→ /** 友好标题 */
388→ 388→ title: string;
389→ 389→ /** 描述 */
390→ 390→ desc: string;
391→ 391→}
392→ 392→
393→ 393→/** 周挑战进度 */
394→ 394→export interface BeaconWeeklyProgress {
395→ 395→ weekKey: string;
396→ 396→ progress: number;
397→ 397→ startedAt: number;
398→ 398→ completedAt: number | null;
399→ 399→ claimed: boolean;
400→ 400→ durationSec: number;
401→ 401→}
402→ 402→
403→ 403→/**
404→ 404→ * 计算 ISO 8601 周键(YYYY-Www),如 "2026-W26"。
405→ 405→ * 规则:周一为一周开始;第一周是该年至少含 4 天的第一周(即包含第一个周四)。
406→ 406→ */
407→ 407→export function getWeekKey(now: Date = new Date()): string {
408→ 408→ // 取 UTC 日期,避免时区偏移
409→ 409→ const tmp = new Date(
410→ 410→ Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
411→ 411→ );
412→ 412→ // ISO 周以周一为起点:(getUTCDay + 6) % 7 → 0=Mon, 6=Sun
413→ 413→ const dayNum = (tmp.getUTCDay() + 6) % 7;
414→ 414→ // 把日期调整到本周四(ISO 周归属年份由该周四所在的年份决定)
415→ 415→ tmp.setUTCDate(tmp.getUTCDate() - dayNum + 3);
416→ 416→ const isoYear = tmp.getUTCFullYear();
417→ 417→ const yearStart = Date.UTC(isoYear, 0, 1);
418→ 418→ const weekNum = Math.ceil(((tmp.getTime() - yearStart) / 86400000 + 1) / 7);
419→ 419→ return `${isoYear}-W${String(weekNum).padStart(2, "0")}`;
420→ 420→}
421→ 421→
422→ 422→/** weekKey → 数值种子(FNV-1a 哈希) */
423→ 423→function weekKeyToSeed(weekKey: string): number {
424→ 424→ let h = 2166136261 >>> 0;
425→ 425→ for (let i = 0; i < weekKey.length; i++) {
426→ 426→ h ^= weekKey.charCodeAt(i);
427→ 427→ h = Math.imul(h, 16777619) >>> 0;
428→ 428→ }
429→ 429→ return h >>> 0;
430→ 430→}
431→ 431→
432→ 432→/**
433→ 433→ * 生成本周周挑战(确定性:同一周同一种子 → 同一挑战)。
434→ 434→ * 难度强制 anomaly60%)或 singular40%),goal 为日挑战基准 ×3-5 倍。
435→ 435→ */
436→ 436→export function generateWeeklyChallenge(
437→ 437→ now: Date = new Date()
438→ 438→): BeaconWeeklyChallenge {
439→ 439→ const weekKey = getWeekKey(now);
440→ 440→ const seed = weekKeyToSeed(weekKey);
441→ 441→ const rng = mulberry32(seed);
442→ 442→
443→ 443→ const types: BeaconChallengeType[] = [
444→ 444→ "decode",
445→ 445→ "expedition",
446→ 446→ "pulse",
447→ 447→ "boss",
448→ 448→ "insight",
449→ 449→ ];
450→ 450→ const type = types[Math.floor(rng() * types.length)];
451→ 451→
452→ 452→ // 难度加权:anomaly 60% / singular 40%
453→ 453→ const dr = rng();
454→ 454→ const difficulty: "anomaly" | "singular" = dr < 0.6 ? "anomaly" : "singular";
455→ 455→ const diffMult = BEACON_DIFFICULTY[difficulty].mult;
456→ 456→
457→ 457→ // 3-5 倍
458→ 458→ const mult = 3 + Math.floor(rng() * 3);
459→ 459→
460→ 460→ let goal = 0;
461→ 461→ let rewardInsight = 0;
462→ 462→ let rewardContact = 0;
463→ 463→ let title = "";
464→ 464→ let desc = "";
465→ 465→
466→ 466→ switch (type) {
467→ 467→ case "decode":
468→ 468→ // 日基准 6-15 × mult(3-5) → 18-75
469→ 469→ goal = Math.max(18, Math.round((6 + Math.floor(rng() * 10)) * diffMult * mult));
470→ 470→ rewardInsight = Math.round(goal * 5 * diffMult);
471→ 471→ rewardContact = goal * 1.2;
472→ 472→ title = `周界解码 · ${goal} 颗晶体`;
473→ 473→ desc = `本周深空信标要求解码 ${goal} 颗记忆晶体,强度为日挑战的 ${mult} 倍。`;
474→ 474→ break;
475→ 475→ case "expedition":
476→ 476→ // 日基准 1-3 × mult(3-5) → 3-15
477→ 477→ goal = Math.max(3, Math.round((1 + Math.floor(rng() * 3)) * diffMult * mult));
478→ 478→ rewardInsight = Math.round(goal * 14 * diffMult);
479→ 479→ rewardContact = goal * 2;
480→ 480→ title = `周界远征 · ${goal} 次探险`;
481→ 481→ desc = `本周派出探险队完成 ${goal} 次远征,无论胜负均计入。`;
482→ 482→ break;
483→ 483→ case "pulse":
484→ 484→ // 日基准 20-49 × mult(3-5) → 60-245
485→ 485→ goal = Math.max(60, Math.round((20 + Math.floor(rng() * 30)) * diffMult * mult));
486→ 486→ rewardInsight = Math.round(goal * 1.5 * diffMult);
487→ 487→ rewardContact = goal * 0.4;
488→ 488→ title = `周界脉冲 · ${goal} 次扫描`;
489→ 489→ desc = `本周发起 ${goal} 次脉冲扫描。连击可叠加加成。`;
490→ 490→ break;
491→ 491→ case "boss":
492→ 492→ // 日基准 1 × mult(3-5) → 3-12singular mult=2.4 时上限 12
493→ 493→ goal = Math.max(3, Math.round(mult * diffMult));
494→ 494→ rewardInsight = Math.round(goal * 35 * diffMult);
495→ 495→ rewardContact = goal * 4;
496→ 496→ title = `周界猎杀 · ${goal} 处 BOSS`;
497→ 497→ desc = `本周在探险终点击破 ${goal} 处维度 BOSS。提升探险力后再挑战。`;
498→ 498→ break;
499→ 499→ case "insight":
500→ 500→ // 日基准 40-119 × mult(3-5) → 120-595
501→ 501→ goal = Math.max(120, Math.round((40 + Math.floor(rng() * 80)) * diffMult * mult));
502→ 502→ rewardInsight = 0; // 洞见挑战不给洞见,给接触
503→ 503→ rewardContact = goal * 0.15;
504→ 504→ title = `周界洞见 · ${goal} 点`;
505→ 505→ desc = `本周通过解码、探险、星潮等途径累计 ${goal} 洞见。`;
506→ 506→ break;
507→ 507→ }
508→ 508→
509→ 509→ return {
510→ 510→ weekKey,
511→ 511→ type,
512→ 512→ difficulty,
513→ 513→ goal,
514→ 514→ rewardInsight,
515→ 515→ rewardContact,
516→ 516→ seed,
517→ 517→ title,
518→ 518→ desc,
519→ 519→ };
520→ 520→}
521→ 521→
522→ 522→/** 读取本周进度(若 weekKey 不匹配则重置) */
523→ 523→export function loadWeeklyProgress(now: Date = new Date()): BeaconWeeklyProgress {
524→ 524→ const weekKey = getWeekKey(now);
525→ 525→ const empty: BeaconWeeklyProgress = {
526→ 526→ weekKey,
527→ 527→ progress: 0,
528→ 528→ startedAt: Date.now(),
529→ 529→ completedAt: null,
530→ 530→ claimed: false,
531→ 531→ durationSec: 0,
532→ 532→ };
533→ 533→ if (typeof localStorage === "undefined") return empty;
534→ 534→ try {
535→ 535→ const raw = localStorage.getItem(BEACON_WEEKLY_KEY);
536→ 536→ if (!raw) return empty;
537→ 537→ const prog = JSON.parse(raw) as BeaconWeeklyProgress;
538→ 538→ if (prog.weekKey !== weekKey) return empty; // 新的一周,重置
539→ 539→ return prog;
540→ 540→ } catch {
541→ 541→ return empty;
542→ 542→ }
543→ 543→}
544→ 544→
545→ 545→/** 保存本周进度 */
546→ 546→export function saveWeeklyProgress(prog: BeaconWeeklyProgress): void {
547→ 547→ if (typeof localStorage === "undefined") return;
548→ 548→ localStorage.setItem(BEACON_WEEKLY_KEY, JSON.stringify(prog));
549→ 549→}
550→ 550→
551→ 551→/** 增量更新周挑战进度,返回新进度 + 是否刚完成 */
552→ 552→export function addWeeklyProgress(
553→ 553→ current: BeaconWeeklyProgress,
554→ 554→ challenge: BeaconWeeklyChallenge,
555→ 555→ delta: number
556→ 556→): { progress: BeaconWeeklyProgress; justCompleted: boolean } {
557→ 557→ const newProgressVal = Math.min(challenge.goal, current.progress + delta);
558→ 558→ const justCompleted =
559→ 559→ current.completedAt === null && newProgressVal >= challenge.goal;
560→ 560→ const completedAt = justCompleted ? Date.now() : current.completedAt;
561→ 561→ const durationSec =
562→ 562→ completedAt !== null
563→ 563→ ? Math.floor((completedAt - current.startedAt) / 1000)
564→ 564→ : current.durationSec;
565→ 565→ const next: BeaconWeeklyProgress = {
566→ 566→ ...current,
567→ 567→ progress: newProgressVal,
568→ 568→ completedAt,
569→ 569→ durationSec,
570→ 570→ };
571→ 571→ saveWeeklyProgress(next);
572→ 572→ return { progress: next, justCompleted };
573→ 573→}
574→ 574→
575→ 575→/** 领取周挑战奖励:返回奖励数值 + 推送排行榜 */
576→ 576→export function claimWeeklyReward(
577→ 577→ challenge: BeaconWeeklyChallenge,
578→ 578→ progress: BeaconWeeklyProgress
579→ 579→): {
580→ 580→ rewardInsight: number;
581→ 581→ rewardContact: number;
582→ 582→ score: number;
583→ 583→ leaderboard: BeaconScoreEntry[];
584→ 584→} {
585→ 585→ if (progress.claimed || progress.completedAt === null) {
586→ 586→ return {
587→ 587→ rewardInsight: 0,
588→ 588→ rewardContact: 0,
589→ 589→ score: 0,
590→ 590→ leaderboard: loadLeaderboard(),
591→ 591→ };
592→ 592→ }
593→ 593→ // 借用 computeBeaconScore:把 weekly 包装成 daily 接口(dateKey 字段不影响计分逻辑)
594→ 594→ const score = computeBeaconScore(
595→ 595→ { ...(challenge as unknown as BeaconDailyChallenge), dateKey: challenge.weekKey },
596→ 596→ progress.progress,
597→ 597→ progress.durationSec
598→ 598→ );
599→ 599→ const entry: BeaconScoreEntry = {
600→ 600→ timestamp: Date.now(),
601→ 601→ dateKey: challenge.weekKey,
602→ 602→ challenge: challenge.type,
603→ 603→ difficulty: challenge.difficulty,
604→ 604→ progress: progress.progress / challenge.goal,
605→ 605→ score,
606→ 606→ durationSec: progress.durationSec,
607→ 607→ isWeekly: true,
608→ 608→ };
609→ 609→ const leaderboard = pushLeaderboardEntry(entry);
610→ 610→ const updated: BeaconWeeklyProgress = { ...progress, claimed: true };
611→ 611→ saveWeeklyProgress(updated);
612→ 612→ return {
613→ 613→ rewardInsight: challenge.rewardInsight,
614→ 614→ rewardContact: challenge.rewardContact,
615→ 615→ score,
616→ 616→ leaderboard,
617→ 617→ };
618→ 618→}
619→ 619→
620→ 620→/** 距离下周一 UTC 0 点的毫秒数(用于周挑战倒计时) */
621→ 621→export function msUntilNextWeek(now: Date = new Date()): number {
622→ 622→ const dayNum = (now.getUTCDay() + 6) % 7; // 0 = Mon
623→ 623→ const mondayThisWeek = Date.UTC(
624→ 624→ now.getUTCFullYear(),
625→ 625→ now.getUTCMonth(),
626→ 626→ now.getUTCDate() - dayNum,
627→ 627→ 0,
628→ 628→ 0,
629→ 629→ 0
630→ 630→ );
631→ 631→ const nextMonday = mondayThisWeek + 7 * 86400000;
632→ 632→ return Math.max(0, nextMonday - now.getTime());
633→ 633→}
634→ 634→
635→ 635→// ===========================================================================
636→ 636→// 信标链(BEACON CHAIN)— v0.8
637→ 637→// 连续完成日挑战形成"链",达成里程碑(3/7/14/30 天)领取递增大奖。
638→ 638→// 断链有宽容机制(1 天 miss 不断链,用"信标续命"概念),每条链只能用 1 次。
639→ 639→// ===========================================================================
640→ 640→
641→ 641→/** 信标链 localStorage key */
642→ 642→export const BEACON_CHAIN_KEY = "echo-nexus-beacon-chain-v1";
643→ 643→
644→ 644→/** 里程碑天数 */
645→ 645→export const BEACON_CHAIN_MILESTONES = [3, 7, 14, 30] as const;
646→ 646→
647→ 647→/** 里程碑奖励配置 */
648→ 648→export interface BeaconChainReward {
649→ 649→ milestone: number;
650→ 650→ rewardInsight: number;
651→ 651→ rewardContact: number;
652→ 652→ label: string;
653→ 653→}
654→ 654→
655→ 655→export const BEACON_CHAIN_REWARDS: BeaconChainReward[] = [
656→ 656→ { milestone: 3, rewardInsight: 50, rewardContact: 5, label: "三日谐振" },
657→ 657→ { milestone: 7, rewardInsight: 120, rewardContact: 12, label: "七日回响" },
658→ 658→ { milestone: 14, rewardInsight: 280, rewardContact: 28, label: "半月星潮" },
659→ 659→ { milestone: 30, rewardInsight: 680, rewardContact: 68, label: "满月飞升" },
660→ 660→];
661→ 661→
662→ 662→/** 信标链状态 */
663→ 663→export interface BeaconChainState {
664→ 664→ /** 上次完成日(YYYY-MM-DD */
665→ 665→ lastCompletedDateKey: string;
666→ 666→ /** 当前连续天数 */
667→ 667→ currentStreak: number;
668→ 668→ /** 历史最长 */
669→ 669→ longestStreak: number;
670→ 670→ /** 累计完成总数 */
671→ 671→ totalCompletions: number;
672→ 672→ /** 本周期已用续命数(上限 1) */
673→ 673→ graceUsed: number;
674→ 674→ /** 已领取的里程碑数组 */
675→ 675→ milestonesClaimed: number[];
676→ 676→}
677→ 677→
678→ 678→/** 读取信标链状态 */
679→ 679→export function loadChainState(): BeaconChainState {
680→ 680→ const fresh = (): BeaconChainState => ({
681→ 681→ lastCompletedDateKey: "",
682→ 682→ currentStreak: 0,
683→ 683→ longestStreak: 0,
684→ 684→ totalCompletions: 0,
685→ 685→ graceUsed: 0,
686→ 686→ milestonesClaimed: [],
687→ 687→ });
688→ 688→ if (typeof localStorage === "undefined") return fresh();
689→ 689→ try {
690→ 690→ const raw = localStorage.getItem(BEACON_CHAIN_KEY);
691→ 691→ if (!raw) return fresh();
692→ 692→ const s = JSON.parse(raw) as Partial<BeaconChainState>;
693→ 693→ return {
694→ 694→ lastCompletedDateKey: s.lastCompletedDateKey ?? "",
695→ 695→ currentStreak: s.currentStreak ?? 0,
696→ 696→ longestStreak: s.longestStreak ?? 0,
697→ 697→ totalCompletions: s.totalCompletions ?? 0,
698→ 698→ graceUsed: s.graceUsed ?? 0,
699→ 699→ milestonesClaimed: Array.isArray(s.milestonesClaimed)
700→ 700→ ? [...s.milestonesClaimed]
701→ 701→ : [],
702→ 702→ };
703→ 703→ } catch {
704→ 704→ return fresh();
705→ 705→ }
706→ 706→}
707→ 707→
708→ 708→/** 保存信标链状态 */
709→ 709→export function saveChainState(state: BeaconChainState): void {
710→ 710→ if (typeof localStorage === "undefined") return;
711→ 711→ localStorage.setItem(BEACON_CHAIN_KEY, JSON.stringify(state));
712→ 712→}
713→ 713→
714→ 714→/** dateKey → UTC 0 点时间戳 */
715→ 715→function dateKeyToTimestamp(dateKey: string): number {
716→ 716→ const [y, m, d] = dateKey.split("-").map(Number);
717→ 717→ return Date.UTC(y, m - 1, d, 0, 0, 0);
718→ 718→}
719→ 719→
720→ 720→/** 计算 b - a 相差的天数(UTC 0 点对齐) */
721→ 721→function dateKeyDiffDays(a: string, b: string): number {
722→ 722→ if (!a || !b) return Number.MAX_SAFE_INTEGER;
723→ 723→ return Math.round((dateKeyToTimestamp(b) - dateKeyToTimestamp(a)) / 86400000);
724→ 724→}
725→ 725→
726→ 726→/**
727→ 727→ * 记录一次日挑战完成(核心逻辑)。
728→ 728→ * - dateKey === lastCompletedDateKey:同一天重复完成,忽略,返回 newMilestones: []
729→ 729→ * - dateKey 是 lastCompletedDateKey 的次日:currentStreak++
730→ 730→ * - dateKey 是 lastCompletedDateKey 的后两天(隔了一天 miss)且 graceUsed < 1:续命一次,currentStreak++
731→ 731→ * - 其他:currentStreak = 1(断链重来),graceUsed = 0
732→ 732→ * 更新 longestStreak 与 totalCompletions。
733→ 733→ * @returns { state, newMilestones } 刚达成但未领取的里程碑数组
734→ 734→ */
735→ 735→export function recordChainCompletion(dateKey: string): {
736→ 736→ state: BeaconChainState;
737→ 737→ newMilestones: number[];
738→ 738→} {
739→ 739→ const state = loadChainState();
740→ 740→
741→ 741→ // 同一天重复完成:忽略
742→ 742→ if (state.lastCompletedDateKey === dateKey) {
743→ 743→ return { state, newMilestones: [] };
744→ 744→ }
745→ 745→
746→ 746→ let next: BeaconChainState;
747→ 747→
748→ 748→ if (state.lastCompletedDateKey === "") {
749→ 749→ // 首次完成
750→ 750→ next = {
751→ 751→ ...state,
752→ 752→ lastCompletedDateKey: dateKey,
753→ 753→ currentStreak: 1,
754→ 754→ longestStreak: Math.max(state.longestStreak, 1),
755→ 755→ totalCompletions: state.totalCompletions + 1,
756→ 756→ };
757→ 757→ } else {
758→ 758→ const diff = dateKeyDiffDays(state.lastCompletedDateKey, dateKey);
759→ 759→ if (diff === 1) {
760→ 760→ // 次日:链 +1
761→ 761→ const newStreak = state.currentStreak + 1;
762→ 762→ next = {
763→ 763→ ...state,
764→ 764→ lastCompletedDateKey: dateKey,
765→ 765→ currentStreak: newStreak,
766→ 766→ longestStreak: Math.max(state.longestStreak, newStreak),
767→ 767→ totalCompletions: state.totalCompletions + 1,
768→ 768→ };
769→ 769→ } else if (diff === 2 && state.graceUsed < 1) {
770→ 770→ // 隔一天 miss,续命一次
771→ 771→ const newStreak = state.currentStreak + 1;
772→ 772→ next = {
773→ 773→ ...state,
774→ 774→ lastCompletedDateKey: dateKey,
775→ 775→ currentStreak: newStreak,
776→ 776→ longestStreak: Math.max(state.longestStreak, newStreak),
777→ 777→ totalCompletions: state.totalCompletions + 1,
778→ 778→ graceUsed: state.graceUsed + 1,
779→ 779→ };
780→ 780→ } else {
781→ 781→ // 断链重来
782→ 782→ next = {
783→ 783→ ...state,
784→ 784→ lastCompletedDateKey: dateKey,
785→ 785→ currentStreak: 1,
786→ 786→ totalCompletions: state.totalCompletions + 1,
787→ 787→ graceUsed: 0,
788→ 788→ longestStreak: Math.max(state.longestStreak, 1),
789→ 789→ };
790→ 790→ }
791→ 791→ }
792→ 792→
793→ 793→ // 检查新里程碑(刚达成但未领取)
794→ 794→ const newMilestones: number[] = [];
795→ 795→ for (const m of BEACON_CHAIN_MILESTONES) {
796→ 796→ if (
797→ 797→ next.currentStreak >= m &&
798→ 798→ !next.milestonesClaimed.includes(m)
799→ 799→ ) {
800→ 800→ newMilestones.push(m);
801→ 801→ }
802→ 802→ }
803→ 803→
804→ 804→ saveChainState(next);
805→ 805→ return { state: next, newMilestones };
806→ 806→}
807→ 807→
808→ 808→/** 领取里程碑奖励,加入 milestonesClaimed */
809→ 809→export function claimChainMilestone(milestone: number): {
810→ 810→ rewardInsight: number;
811→ 811→ rewardContact: number;
812→ 812→ label: string;
813→ 813→ state: BeaconChainState;
814→ 814→} {
815→ 815→ const reward = BEACON_CHAIN_REWARDS.find((r) => r.milestone === milestone);
816→ 816→ const state = loadChainState();
817→ 817→ if (
818→ 818→ !reward ||
819→ 819→ state.milestonesClaimed.includes(milestone) ||
820→ 820→ state.currentStreak < milestone
821→ 821→ ) {
822→ 822→ return { rewardInsight: 0, rewardContact: 0, label: "", state };
823→ 823→ }
824→ 824→ const next: BeaconChainState = {
825→ 825→ ...state,
826→ 826→ milestonesClaimed: [...state.milestonesClaimed, milestone],
827→ 827→ };
828→ 828→ saveChainState(next);
829→ 829→ return {
830→ 830→ rewardInsight: reward.rewardInsight,
831→ 831→ rewardContact: reward.rewardContact,
832→ 832→ label: reward.label,
833→ 833→ state: next,
834→ 834→ };
835→ 835→}
836→ 836→
837→ 837→/** 返回下一个目标里程碑(如 streak=5 → 7streak=30+ → null */
838→ 838→export function getNextMilestone(streak: number): number | null {
839→ 839→ for (const m of BEACON_CHAIN_MILESTONES) {
840→ 840→ if (streak < m) return m;
841→ 841→ }
842→ 842→ return null;
843→ 843→}
844→ 844→
845→ 845→/**
846→ 846→ * 返回信标链进度信息(用于 UI 进度条)。
847→ 847→ * - current:当前连续天数
848→ 848→ * - next:下一个目标里程碑(null 表示已通关全部)
849→ 849→ * - progressPct:当前进度(基于上一个里程碑 → 下一个里程碑)
850→ 850→ */
851→ 851→export function getChainProgress(streak: number): {
852→ 852→ current: number;
853→ 853→ next: number | null;
854→ 854→ prev: number;
855→ 855→ progressPct: number;
856→ 856→} {
857→ 857→ const next = getNextMilestone(streak);
858→ 858→ let prev = 0;
859→ 859→ for (const m of BEACON_CHAIN_MILESTONES) {
860→ 860→ if (streak >= m) prev = m;
861→ 861→ }
862→ 862→ if (next === null) {
863→ 863→ return { current: streak, next: null, prev, progressPct: 100 };
864→ 864→ }
865→ 865→ const span = next - prev;
866→ 866→ const done = streak - prev;
867→ 867→ const pct = span > 0 ? Math.round((done / span) * 100) : 100;
868→ 868→ return {
869→ 869→ current: streak,
870→ 870→ next,
871→ 871→ prev,
872→ 872→ progressPct: Math.min(100, Math.max(0, pct)),
873→ 873→ };
874→ 874→}
875→ 875→
@@ -1,754 +0,0 @@
1→"use client";
2→// 回响星核 / Echo Nexus — 深空信标面板
3→// v0.5:每日挑战 + 本地排行榜
4→// v0.8:周挑战 + 信标链(连续完成奖励)
5→import { useState, useEffect, useCallback } from "react";
6→import { useGameStore } from "@/store/gameStore";
7→import { useToast } from "@/hooks/use-toast";
8→import { sfx } from "@/hooks/useAudio";
9→import {
10→ generateDailyChallenge,
11→ generateWeeklyChallenge,
12→ loadDailyProgress,
13→ loadWeeklyProgress,
14→ loadLeaderboard,
15→ loadChainState,
16→ claimBeaconReward,
17→ BEACON_DIFFICULTY,
18→ BEACON_TYPE_META,
19→ BEACON_CHAIN_MILESTONES,
20→ BEACON_CHAIN_REWARDS,
21→ getWeekKey,
22→ getTodayKey,
23→ getNextMilestone,
24→ getChainProgress,
25→ msUntilNextDay,
26→ msUntilNextWeek,
27→ formatCountdown,
28→ type BeaconDailyChallenge,
29→ type BeaconDailyProgress,
30→ type BeaconWeeklyChallenge,
31→ type BeaconWeeklyProgress,
32→ type BeaconScoreEntry,
33→ type BeaconChallengeType,
34→ type BeaconDifficulty,
35→ type BeaconChainState,
36→} from "@/lib/game/beacon";
37→import { formatNum } from "@/lib/game/config";
38→import { Button } from "@/components/ui/button";
39→import { Progress } from "@/components/ui/progress";
40→import {
41→ Radio,
42→ Clock,
43→ Trophy,
44→ Sparkles,
45→ Award,
46→ Crown,
47→ Medal,
48→ Link2,
49→ Flame,
50→ Zap,
51→} from "lucide-react";
52→
53→const DIFFICULTY_ORDER: BeaconDifficulty[] = ["routine", "anomaly", "singular"];
54→
55→function rankBadge(rank: number) {
56→ if (rank === 0) return { icon: Crown, color: "#fbbf24", label: "1st" };
57→ if (rank === 1) return { icon: Medal, color: "#cbd5e1", label: "2nd" };
58→ if (rank === 2) return { icon: Award, color: "#f97316", label: "3rd" };
59→ return null;
60→}
61→
62→export function BeaconPanel() {
63→ const grantBeaconReward = useGameStore((s) => s.grantBeaconReward);
64→ const claimWeeklyBeacon = useGameStore((s) => s.claimWeeklyBeacon);
65→ const claimChainReward = useGameStore((s) => s.claimChainReward);
66→ const { toast } = useToast();
67→
68→ const [challenge, setChallenge] = useState<BeaconDailyChallenge | null>(null);
69→ const [progress, setProgress] = useState<BeaconDailyProgress | null>(null);
70→ const [weeklyChallenge, setWeeklyChallenge] =
71→ useState<BeaconWeeklyChallenge | null>(null);
72→ const [weeklyProgress, setWeeklyProgress] =
73→ useState<BeaconWeeklyProgress | null>(null);
74→ const [chainState, setChainState] = useState<BeaconChainState | null>(null);
75→ const [leaderboard, setLeaderboard] = useState<BeaconScoreEntry[]>([]);
76→ const [dayCountdown, setDayCountdown] = useState("00:00:00");
77→ const [weekCountdown, setWeekCountdown] = useState("00:00:00");
78→ const [now, setNow] = useState(Date.now());
79→
80→ // 初始化 + 每秒刷新(进度 + 倒计时)
81→ useEffect(() => {
82→ setChallenge(generateDailyChallenge());
83→ setProgress(loadDailyProgress());
84→ setWeeklyChallenge(generateWeeklyChallenge());
85→ setWeeklyProgress(loadWeeklyProgress());
86→ setChainState(loadChainState());
87→ setLeaderboard(loadLeaderboard());
88→ const id = setInterval(() => {
89→ setNow(Date.now());
90→ setProgress(loadDailyProgress());
91→ setWeeklyProgress(loadWeeklyProgress());
92→ setChainState(loadChainState());
93→ setChallenge((c) => c ?? generateDailyChallenge());
94→ setWeeklyChallenge((c) => c ?? generateWeeklyChallenge());
95→ }, 1000);
96→ return () => clearInterval(id);
97→ }, []);
98→
99→ useEffect(() => {
100→ setDayCountdown(formatCountdown(msUntilNextDay(new Date(now))));
101→ setWeekCountdown(formatCountdown(msUntilNextWeek(new Date(now))));
102→ }, [now]);
103→
104→ const handleClaimDaily = useCallback(() => {
105→ if (!challenge || !progress) return;
106→ if (progress.completedAt === null || progress.claimed) return;
107→ const res = claimBeaconReward(challenge, progress);
108→ setLeaderboard(res.leaderboard);
109→ setProgress(loadDailyProgress());
110→ // 发放奖励到游戏状态
111→ grantBeaconReward(res.rewardInsight, res.rewardContact);
112→ sfx("achievement");
113→ toast({
114→ title: "✦ 每日信标奖励已领取",
115→ description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(
116→ 1
117→ )} 接触 · 得分 ${res.score}`,
118→ });
119→ }, [challenge, progress, toast, grantBeaconReward]);
120→
121→ const handleClaimWeekly = useCallback(() => {
122→ if (!weeklyChallenge || !weeklyProgress) return;
123→ if (weeklyProgress.completedAt === null || weeklyProgress.claimed) return;
124→ const res = claimWeeklyBeacon();
125→ setLeaderboard(loadLeaderboard());
126→ setWeeklyProgress(loadWeeklyProgress());
127→ if (res.rewardInsight > 0 || res.rewardContact > 0) {
128→ sfx("achievement");
129→ toast({
130→ title: "✦ 周挑战奖励已领取",
131→ description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(
132→ 1
133→ )} 接触 · 得分 ${res.score}`,
134→ });
135→ }
136→ }, [weeklyChallenge, weeklyProgress, toast, claimWeeklyBeacon]);
137→
138→ const handleClaimChain = useCallback(
139→ (milestone: number) => {
140→ const reward = BEACON_CHAIN_REWARDS.find(
141→ (r) => r.milestone === milestone
142→ );
143→ const res = claimChainReward(milestone);
144→ setChainState(loadChainState());
145→ if (res.ok) {
146→ sfx("achievement");
147→ toast({
148→ title: `✦ ${res.label || reward?.label || "里程碑"} 已领取`,
149→ description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(
150→ 1
151→ )} 接触`,
152→ });
153→ }
154→ },
155→ [toast, claimChainReward]
156→ );
157→
158→ if (!challenge || !progress || !weeklyChallenge || !weeklyProgress || !chainState) {
159→ return (
160→ <div className="flex items-center justify-center h-full text-xs text-muted-foreground/60">
161→ 正在校准深空信标…
162→ </div>
163→ );
164→ }
165→
166→ const diffMeta = BEACON_DIFFICULTY[challenge.difficulty];
167→ const typeMeta = BEACON_TYPE_META[challenge.type];
168→ const pct = Math.min(100, (progress.progress / challenge.goal) * 100);
169→ const isCompleted = progress.completedAt !== null;
170→ const isClaimed = progress.claimed;
171→ const canClaim = isCompleted && !isClaimed;
172→
173→ // 周挑战派生量
174→ const wDiffMeta = BEACON_DIFFICULTY[weeklyChallenge.difficulty];
175→ const wTypeMeta = BEACON_TYPE_META[weeklyChallenge.type];
176→ const wPct = Math.min(100, (weeklyProgress.progress / weeklyChallenge.goal) * 100);
177→ const wCompleted = weeklyProgress.completedAt !== null;
178→ const wClaimed = weeklyProgress.claimed;
179→ const wCanClaim = wCompleted && !wClaimed;
180→
181→ // 信标链派生量
182→ const chainProgress = getChainProgress(chainState.currentStreak);
183→ const nextMilestone = getNextMilestone(chainState.currentStreak);
184→ const todayKey = getTodayKey();
185→ const completedToday = chainState.lastCompletedDateKey === todayKey;
186→
187→ return (
188→ <div className="flex flex-col gap-2.5 h-full overflow-y-auto echo-scroll pr-0.5">
189→ <style jsx global>{`
190→ .echo-scroll::-webkit-scrollbar { width: 4px; }
191→ .echo-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 2px; }
192→ .echo-scroll::-webkit-scrollbar-track { background: transparent; }
193→ @keyframes beacon-pulse-ring {
194→ 0% { transform: scale(0.8); opacity: 0.8; }
195→ 100% { transform: scale(2.2); opacity: 0; }
196→ }
197→ @keyframes beacon-glow {
198→ 0%, 100% { box-shadow: 0 0 18px ${diffMeta.glow}, inset 0 0 12px ${diffMeta.glow}; }
199→ 50% { box-shadow: 0 0 32px ${diffMeta.glow}, inset 0 0 20px ${diffMeta.glow}; }
200→ }
201→ @keyframes weekly-glow {
202→ 0%, 100% { box-shadow: 0 0 18px rgba(232,121,249,0.35), inset 0 0 12px rgba(232,121,249,0.25); }
203→ 50% { box-shadow: 0 0 32px rgba(232,121,249,0.5), inset 0 0 20px rgba(232,121,249,0.35); }
204→ }
205→ @keyframes chain-milestone-pulse {
206→ 0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(251,113,133,0.55); }
207→ 50% { transform: scale(1.06); box-shadow: 0 0 0 8px rgba(251,113,133,0); }
208→ }
209→ .chain-milestone-reachable {
210→ animation: chain-milestone-pulse 1.8s ease-in-out infinite;
211→ }
212→ @keyframes chain-streak-flux {
213→ 0%, 100% { background-position: 0% 50%; }
214→ 50% { background-position: 100% 50%; }
215→ }
216→ .chain-streak-text {
217→ background: linear-gradient(90deg, #fbbf24, #fb7185, #fbbf24);
218→ background-size: 200% 100%;
219→ -webkit-background-clip: text;
220→ background-clip: text;
221→ -webkit-text-fill-color: transparent;
222→ animation: chain-streak-flux 4s ease-in-out infinite;
223→ }
224→ `}</style>
225→
226→ {/* 头部:信标 + 倒计时 */}
227→ <div className="flex items-center justify-between">
228→ <h3 className="text-sm font-semibold flex items-center gap-1.5">
229→ <Radio className="h-4 w-4 text-fuchsia-400" />
230→ 深空信标
231→ </h3>
232→ <div className="flex items-center gap-1 text-[10px] text-muted-foreground/70">
233→ <Clock className="h-3 w-3" />
234→ 次日重置
235→ <span className="font-mono text-fuchsia-300/90">{dayCountdown}</span>
236→ </div>
237→ </div>
238→
239→ {/* 每日挑战卡片 */}
240→ <div
241→ className="relative rounded-xl border p-3 overflow-hidden"
242→ style={{
243→ borderColor: `${diffMeta.color}55`,
244→ background: `linear-gradient(135deg, ${diffMeta.color}1f, rgba(0,0,0,0.45))`,
245→ animation: isCompleted ? "none" : "beacon-glow 3s ease-in-out infinite",
246→ }}
247→ >
248→ {/* 背景装饰:脉冲环 */}
249→ {!isCompleted && (
250→ <>
251→ <div
252→ className="absolute top-4 right-4 h-10 w-10 rounded-full pointer-events-none"
253→ style={{
254→ border: `1.5px solid ${diffMeta.color}`,
255→ animation: "beacon-pulse-ring 2.5s ease-out infinite",
256→ }}
257→ />
258→ <div
259→ className="absolute top-4 right-4 h-10 w-10 rounded-full pointer-events-none"
260→ style={{
261→ border: `1.5px solid ${diffMeta.color}`,
262→ animation: "beacon-pulse-ring 2.5s ease-out infinite 1.25s",
263→ }}
264→ />
265→ </>
266→ )}
267→
268→ <div className="relative">
269→ {/* 难度 + 类型 标签 */}
270→ <div className="flex items-center gap-1.5 mb-2">
271→ <span
272→ className="text-[10px] font-mono px-1.5 py-0.5 rounded flex items-center gap-1"
273→ style={{ background: `${diffMeta.color}22`, color: diffMeta.color, border: `1px solid ${diffMeta.color}55` }}
274→ >
275→ <span>{diffMeta.icon}</span>
276→ {diffMeta.label}
277→ </span>
278→ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-white/5 text-muted-foreground border border-white/10">
279→ {typeMeta.icon} {typeMeta.label}
280→ </span>
281→ {isCompleted && (
282→ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 flex items-center gap-1">
283→ <Sparkles className="h-2.5 w-2.5" /> 已完成
284→ </span>
285→ )}
286→ </div>
287→
288→ {/* 挑战标题 */}
289→ <h4 className="text-sm font-semibold mb-1" style={{ color: diffMeta.color, textShadow: `0 0 10px ${diffMeta.glow}` }}>
290→ {challenge.title}
291→ </h4>
292→ <p className="text-[11px] text-muted-foreground/80 leading-relaxed mb-2.5">
293→ {challenge.desc}
294→ </p>
295→
296→ {/* 进度条 */}
297→ <div className="mb-2">
298→ <div className="flex items-center justify-between mb-1">
299→ <span className="text-[10px] text-muted-foreground/70">进度</span>
300→ <span className="text-[11px] font-mono font-semibold" style={{ color: diffMeta.color }}>
301→ {Math.min(progress.progress, challenge.goal)} / {challenge.goal} {typeMeta.unit}
302→ </span>
303→ </div>
304→ <Progress
305→ value={pct}
306→ className="h-2 bg-black/40"
307→ style={{
308→ ["--progress-color" as string]: diffMeta.color,
309→ }}
310→ />
311→ </div>
312→
313→ {/* 奖励 + 领取按钮 */}
314→ <div className="flex items-center justify-between gap-2">
315→ <div className="flex items-center gap-2 text-[10px]">
316→ <span className="text-muted-foreground/60">奖励:</span>
317→ {challenge.rewardInsight > 0 && (
318→ <span className="text-amber-300 font-mono">+{formatNum(challenge.rewardInsight)}洞见</span>
319→ )}
320→ <span className="text-fuchsia-300 font-mono">+{challenge.rewardContact.toFixed(1)}接触</span>
321→ </div>
322→ <Button
323→ onClick={handleClaimDaily}
324→ disabled={!canClaim}
325→ size="sm"
326→ className="h-7 px-3 text-[11px] border-0"
327→ style={{
328→ background: canClaim
329→ ? `linear-gradient(90deg, ${diffMeta.color}, ${diffMeta.color}cc)`
330→ : `${diffMeta.color}1a`,
331→ color: canClaim ? "#022c22" : `${diffMeta.color}99`,
332→ }}
333→ >
334→ {isClaimed ? "已领取" : canClaim ? "领取奖励" : isCompleted ? "已领取" : "进行中…"}
335→ </Button>
336→ </div>
337→
338→ {/* 完成时长 */}
339→ {isCompleted && progress.durationSec > 0 && (
340→ <div className="mt-1.5 text-[10px] text-muted-foreground/50 text-center">
341→ 完成用时 {Math.floor(progress.durationSec / 60)}分{progress.durationSec % 60}秒
342→ </div>
343→ )}
344→ </div>
345→ </div>
346→
347→ {/* 周挑战 + 信标链(桌面端并排,移动端单列) */}
348→ <div className="grid grid-cols-1 lg:grid-cols-2 gap-2.5">
349→ {/* ===== 周挑战卡片(fuchsia 主题) ===== */}
350→ <div
351→ className="relative rounded-xl border p-3 overflow-hidden"
352→ style={{
353→ borderColor: "rgba(232,121,249,0.4)",
354→ background: `linear-gradient(135deg, rgba(232,121,249,0.12), rgba(0,0,0,0.45))`,
355→ animation: wCompleted ? "none" : "weekly-glow 3.5s ease-in-out infinite",
356→ }}
357→ >
358→ {/* 头部:标题 + weekKey + 倒计时 */}
359→ <div className="flex items-center justify-between mb-2">
360→ <div className="flex items-center gap-1.5">
361→ <Zap className="h-3.5 w-3.5 text-fuchsia-400" />
362→ <span className="text-xs font-semibold text-fuchsia-200">
363→ 周挑战 · WEEKLY
364→ </span>
365→ </div>
366→ <div className="flex items-center gap-1 text-[9px] text-muted-foreground/70">
367→ <span className="font-mono text-fuchsia-300/80">
368→ {weeklyChallenge.weekKey}
369→ </span>
370→ <Clock className="h-2.5 w-2.5" />
371→ <span className="font-mono text-fuchsia-300/90">{weekCountdown}</span>
372→ </div>
373→ </div>
374→
375→ {/* 难度 + 类型 标签 */}
376→ <div className="flex items-center gap-1.5 mb-2">
377→ <span
378→ className="text-[10px] font-mono px-1.5 py-0.5 rounded flex items-center gap-1"
379→ style={{ background: `${wDiffMeta.color}22`, color: wDiffMeta.color, border: `1px solid ${wDiffMeta.color}55` }}
380→ >
381→ <span>{wDiffMeta.icon}</span>
382→ {wDiffMeta.label}
383→ </span>
384→ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-white/5 text-muted-foreground border border-white/10">
385→ {wTypeMeta.icon} {wTypeMeta.label}
386→ </span>
387→ {wCompleted && (
388→ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 flex items-center gap-1">
389→ <Sparkles className="h-2.5 w-2.5" /> 已完成
390→ </span>
391→ )}
392→ </div>
393→
394→ {/* 标题 */}
395→ <h4
396→ className="text-sm font-semibold mb-1"
397→ style={{ color: wDiffMeta.color, textShadow: `0 0 10px ${wDiffMeta.glow}` }}
398→ >
399→ {weeklyChallenge.title}
400→ </h4>
401→ <p className="text-[10px] text-muted-foreground/80 leading-relaxed mb-2">
402→ {weeklyChallenge.desc}
403→ </p>
404→
405→ {/* 进度条 */}
406→ <div className="mb-2">
407→ <div className="flex items-center justify-between mb-1">
408→ <span className="text-[10px] text-muted-foreground/70">进度</span>
409→ <span className="text-[11px] font-mono font-semibold" style={{ color: wDiffMeta.color }}>
410→ {Math.min(weeklyProgress.progress, weeklyChallenge.goal)} / {weeklyChallenge.goal} {wTypeMeta.unit}
411→ </span>
412→ </div>
413→ <Progress
414→ value={wPct}
415→ className="h-2 bg-black/40"
416→ style={{
417→ ["--progress-color" as string]: wDiffMeta.color,
418→ }}
419→ />
420→ </div>
421→
422→ {/* 奖励 + 领取按钮 */}
423→ <div className="flex items-center justify-between gap-2">
424→ <div className="flex items-center gap-1.5 text-[10px]">
425→ <span className="text-muted-foreground/60">奖励:</span>
426→ {weeklyChallenge.rewardInsight > 0 && (
427→ <span className="text-amber-300 font-mono">
428→ +{formatNum(weeklyChallenge.rewardInsight)}洞见
429→ </span>
430→ )}
431→ <span className="text-fuchsia-300 font-mono">
432→ +{weeklyChallenge.rewardContact.toFixed(1)}接触
433→ </span>
434→ </div>
435→ <Button
436→ onClick={handleClaimWeekly}
437→ disabled={!wCanClaim}
438→ size="sm"
439→ className="h-7 px-3 text-[11px] border-0"
440→ style={{
441→ background: wCanClaim
442→ ? `linear-gradient(90deg, #34d399, #34d399cc)`
443→ : "rgba(232,121,249,0.10)",
444→ color: wCanClaim
445→ ? "#022c22"
446→ : "rgba(232,121,249,0.55)",
447→ }}
448→ >
449→ {wClaimed
450→ ? "已领取"
451→ : wCanClaim
452→ ? "领取奖励"
453→ : wCompleted
454→ ? "已领取"
455→ : "进行中…"}
456→ </Button>
457→ </div>
458→
459→ {wCompleted && weeklyProgress.durationSec > 0 && (
460→ <div className="mt-1.5 text-[10px] text-muted-foreground/50 text-center">
461→ 完成用时 {Math.floor(weeklyProgress.durationSec / 60)}分{weeklyProgress.durationSec % 60}秒
462→ </div>
463→ )}
464→ </div>
465→
466→ {/* ===== 信标链卡片(amber→rose 渐变) ===== */}
467→ <div
468→ className="relative rounded-xl border p-3 overflow-hidden"
469→ style={{
470→ borderColor: "rgba(251,191,36,0.35)",
471→ background: `linear-gradient(135deg, rgba(251,191,36,0.10), rgba(251,113,133,0.10), rgba(0,0,0,0.4))`,
472→ }}
473→ >
474→ {/* 头部:标题 + 当前连续天数 */}
475→ <div className="flex items-center justify-between mb-2">
476→ <div className="flex items-center gap-1.5">
477→ <Link2 className="h-3.5 w-3.5 text-amber-400" />
478→ <span className="text-xs font-semibold text-amber-200">
479→ 信标链 · CHAIN
480→ </span>
481→ </div>
482→ <div className="flex items-baseline gap-1">
483→ <Flame className="h-3 w-3 text-rose-400" />
484→ <span className="text-[10px] text-muted-foreground/60">连续</span>
485→ <span className="chain-streak-text text-2xl font-bold font-mono leading-none">
486→ {chainState.currentStreak}
487→ </span>
488→ <span className="text-[10px] text-muted-foreground/60">天</span>
489→ </div>
490→ </div>
491→
492→ {/* 今日完成状态 */}
493→ <div className="mb-2 flex items-center justify-between">
494→ <span className="text-[10px] text-muted-foreground/70">
495→ {completedToday
496→ ? "今日已贡献"
497→ : "今日尚未完成日挑战"}
498→ </span>
499→ <span
500→ className={`text-[10px] font-mono px-1.5 py-0.5 rounded border ${
501→ completedToday
502→ ? "bg-emerald-500/15 text-emerald-300 border-emerald-500/30"
503→ : "bg-white/5 text-muted-foreground/70 border-white/10"
504→ }`}
505→ >
506→ {completedToday ? "✓ 已记录" : "○ 待完成"}
507→ </span>
508→ </div>
509→
510→ {/* 4 个里程碑节点横向排列 */}
511→ <div className="flex items-center justify-between mb-2 relative">
512→ {/* 节点之间的连线(背景灰) */}
513→ <div className="absolute top-6 left-[12.5%] right-[12.5%] h-[2px] bg-white/10" />
514→ {/* 已达成部分高亮(基于 prev→next 插值) */}
515→ {(() => {
516→ // 节点圆心水平位置(百分比)
517→ const MILESTONE_POS: Record<number, number> = {
518→ 0: 12.5,
519→ 3: 12.5,
520→ 7: 37.5,
521→ 14: 62.5,
522→ 30: 87.5,
523→ };
524→ const prevPos =
525→ MILESTONE_POS[chainProgress.prev] ?? 12.5;
526→ const nextPos =
527→ chainProgress.next !== null
528→ ? MILESTONE_POS[chainProgress.next] ?? 87.5
529→ : 87.5;
530→ const pct = chainProgress.progressPct / 100;
531→ const activeEndPos = prevPos + (nextPos - prevPos) * pct;
532→ const widthPct = Math.max(0, activeEndPos - 12.5);
533→ if (widthPct <= 0) return null;
534→ return (
535→ <div
536→ className="absolute top-6 h-[2px]"
537→ style={{
538→ left: "12.5%",
539→ width: `${widthPct}%`,
540→ background:
541→ "linear-gradient(90deg, #fbbf24, #fb7185)",
542→ boxShadow: "0 0 8px rgba(251,113,133,0.5)",
543→ }}
544→ />
545→ );
546→ })()}
547→
548→ {BEACON_CHAIN_MILESTONES.map((m) => {
549→ const reward = BEACON_CHAIN_REWARDS.find((r) => r.milestone === m);
550→ const claimed = chainState.milestonesClaimed.includes(m);
551→ const reachable =
552→ chainState.currentStreak >= m && !claimed;
553→ const inProgress = chainState.currentStreak > 0 && nextMilestone === m;
554→ // 节点配色
555→ let nodeBg = "rgba(255,255,255,0.05)";
556→ let nodeBorder = "rgba(255,255,255,0.15)";
557→ let textColor = "rgba(255,255,255,0.4)";
558→ if (claimed) {
559→ nodeBg = "rgba(52,211,153,0.25)";
560→ nodeBorder = "#34d399";
561→ textColor = "#34d399";
562→ } else if (reachable) {
563→ nodeBg = "rgba(251,113,133,0.20)";
564→ nodeBorder = "#fb7185";
565→ textColor = "#fb7185";
566→ } else if (inProgress) {
567→ nodeBg = "rgba(251,191,36,0.20)";
568→ nodeBorder = "#fbbf24";
569→ textColor = "#fbbf24";
570→ }
571→
572→ return (
573→ <div
574→ key={m}
575→ className="flex flex-col items-center gap-1 z-10 flex-1"
576→ >
577→ <div
578→ className={`w-12 h-12 rounded-full flex items-center justify-center font-mono font-bold text-sm border-2 ${
579→ reachable ? "chain-milestone-reachable" : ""
580→ }`}
581→ style={{
582→ background: nodeBg,
583→ borderColor: nodeBorder,
584→ color: textColor,
585→ }}
586→ >
587→ {claimed ? (
588→ <Sparkles className="h-4 w-4" />
589→ ) : (
590→ m
591→ )}
592→ </div>
593→ <span
594→ className="text-[9px] text-center leading-tight"
595→ style={{ color: textColor }}
596→ >
597→ {reward?.label}
598→ </span>
599→ {claimed ? (
600→ <span className="text-[9px] text-emerald-400/80 font-mono">
601→ 已领
602→ </span>
603→ ) : reachable ? (
604→ <Button
605→ onClick={() => handleClaimChain(m)}
606→ size="sm"
607→ className="h-5 px-2 text-[9px] py-0 border-0"
608→ style={{
609→ background:
610→ "linear-gradient(90deg, #fb7185, #f43f5e)",
611→ color: "#1c0608",
612→ }}
613→ >
614→ 领取
615→ </Button>
616→ ) : (
617→ <span className="text-[9px] text-muted-foreground/40 font-mono">
618→ +{reward?.rewardInsight}洞
619→ </span>
620→ )}
621→ </div>
622→ );
623→ })}
624→ </div>
625→
626→ {/* 进度条 */}
627→ <div className="mb-2">
628→ <div className="flex items-center justify-between mb-1">
629→ <span className="text-[10px] text-muted-foreground/70">
630→ {chainProgress.next === null
631→ ? "已通关全部里程碑"
632→ : `下一目标:${chainProgress.next} 天`}
633→ </span>
634→ <span className="text-[10px] font-mono text-amber-300">
635→ {chainProgress.current}
636→ {chainProgress.next !== null && ` / ${chainProgress.next}`} 天
637→ </span>
638→ </div>
639→ <Progress
640→ value={chainProgress.progressPct}
641→ className="h-1.5 bg-black/40"
642→ style={{
643→ ["--progress-color" as string]: "#fbbf24",
644→ }}
645→ />
646→ </div>
647→
648→ {/* 底部统计 */}
649→ <div className="flex items-center justify-between gap-2 text-[10px]">
650→ <div className="flex items-center gap-2">
651→ <span className="text-muted-foreground/60">最长</span>
652→ <span className="font-mono text-amber-300">
653→ {chainState.longestStreak}天
654→ </span>
655→ </div>
656→ <div className="flex items-center gap-2">
657→ <span className="text-muted-foreground/60">累计</span>
658→ <span className="font-mono text-rose-300">
659→ {chainState.totalCompletions}次
660→ </span>
661→ </div>
662→ <div className="flex items-center gap-1">
663→ <span className="text-muted-foreground/60">续命</span>
664→ {chainState.graceUsed >= 1 ? (
665→ <span className="font-mono text-rose-400/80">已用</span>
666→ ) : (
667→ <span className="font-mono text-emerald-400/80">可用</span>
668→ )}
669→ </div>
670→ </div>
671→ </div>
672→ </div>
673→
674→ {/* 本地排行榜 */}
675→ <div className="rounded-xl border border-white/10 bg-black/30 p-2.5">
676→ <div className="flex items-center gap-1.5 mb-2">
677→ <Trophy className="h-3.5 w-3.5 text-amber-400" />
678→ <span className="text-xs font-semibold">深空排行榜</span>
679→ <span className="text-[10px] text-muted-foreground/50">本地 · Top {leaderboard.length || 0}</span>
680→ </div>
681→
682→ {leaderboard.length === 0 ? (
683→ <div className="text-center py-4 text-[11px] text-muted-foreground/40">
684→ <Trophy className="h-6 w-6 mx-auto mb-1 opacity-30" />
685→ 尚无记录。完成今日或本周信标即可登榜。
686→ </div>
687→ ) : (
688→ <div className="space-y-0.5 max-h-[180px] overflow-y-auto echo-scroll">
689→ {leaderboard.map((entry, i) => {
690→ const rb = rankBadge(i);
691→ const eDiff = BEACON_DIFFICULTY[entry.difficulty];
692→ const eType = BEACON_TYPE_META[entry.challenge as BeaconChallengeType];
693→ const isMine = entry.dateKey === getTodayKey() || entry.dateKey === getWeekKey();
694→ return (
695→ <div
696→ key={`${entry.timestamp}-${i}`}
697→ className={`flex items-center gap-2 px-2 py-1 rounded-lg text-[11px] ${
698→ entry.isWeekly
699→ ? "bg-fuchsia-500/10 border border-fuchsia-500/20"
700→ : isMine
701→ ? "bg-fuchsia-500/10 border border-fuchsia-500/20"
702→ : "hover:bg-white/5"
703→ }`}
704→ >
705→ {/* 排名 */}
706→ <span className="w-6 flex items-center justify-center">
707→ {rb ? (
708→ <rb.icon className="h-3.5 w-3.5" style={{ color: rb.color }} />
709→ ) : (
710→ <span className="text-muted-foreground/50 font-mono">{i + 1}</span>
711→ )}
712→ </span>
713→ {/* 类型 + 难度 + 周挑战标记 */}
714→ <span className="flex items-center gap-1 flex-1 min-w-0">
715→ <span style={{ color: eDiff.color }} className="font-mono">{eDiff.icon}</span>
716→ <span className="text-muted-foreground/80 truncate">{eType.label}</span>
717→ {entry.isWeekly && (
718→ <span className="text-[9px] font-mono px-1 rounded bg-fuchsia-500/20 text-fuchsia-300 border border-fuchsia-500/40">
719→ WEEK
720→ </span>
721→ )}
722→ {entry.progress >= 1 && <span className="text-emerald-400/70">✓</span>}
723→ </span>
724→ {/* 用时 */}
725→ <span className="text-muted-foreground/50 font-mono text-[10px] w-16 text-right">
726→ {Math.floor(entry.durationSec / 60)}m{entry.durationSec % 60}s
727→ </span>
728→ {/* 分数 */}
729→ <span className="font-mono font-semibold w-12 text-right" style={{ color: eDiff.color }}>
730→ {formatNum(entry.score)}
731→ </span>
732→ </div>
733→ );
734→ })}
735→ </div>
736→ )}
737→ </div>
738→
739→ {/* 难度图例 */}
740→ <div className="flex items-center justify-center gap-3 text-[9px] text-muted-foreground/50">
741→ {DIFFICULTY_ORDER.map((d) => {
742→ const m = BEACON_DIFFICULTY[d];
743→ return (
744→ <span key={d} className="flex items-center gap-1">
745→ <span style={{ color: m.color }}>{m.icon}</span>
746→ {m.label} ×{m.mult}
747→ </span>
748→ );
749→ })}
750→ </div>
751→ </div>
752→ );
753→}
754→
@@ -1,754 +0,0 @@
1→"use client";
2→// 回响星核 / Echo Nexus — 深空信标面板
3→// v0.5:每日挑战 + 本地排行榜
4→// v0.8:周挑战 + 信标链(连续完成奖励)
5→import { useState, useEffect, useCallback } from "react";
6→import { useGameStore } from "@/store/gameStore";
7→import { useToast } from "@/hooks/use-toast";
8→import { sfx } from "@/hooks/useAudio";
9→import {
10→ generateDailyChallenge,
11→ generateWeeklyChallenge,
12→ loadDailyProgress,
13→ loadWeeklyProgress,
14→ loadLeaderboard,
15→ loadChainState,
16→ claimBeaconReward,
17→ BEACON_DIFFICULTY,
18→ BEACON_TYPE_META,
19→ BEACON_CHAIN_MILESTONES,
20→ BEACON_CHAIN_REWARDS,
21→ getWeekKey,
22→ getTodayKey,
23→ getNextMilestone,
24→ getChainProgress,
25→ msUntilNextDay,
26→ msUntilNextWeek,
27→ formatCountdown,
28→ type BeaconDailyChallenge,
29→ type BeaconDailyProgress,
30→ type BeaconWeeklyChallenge,
31→ type BeaconWeeklyProgress,
32→ type BeaconScoreEntry,
33→ type BeaconChallengeType,
34→ type BeaconDifficulty,
35→ type BeaconChainState,
36→} from "@/lib/game/beacon";
37→import { formatNum } from "@/lib/game/config";
38→import { Button } from "@/components/ui/button";
39→import { Progress } from "@/components/ui/progress";
40→import {
41→ Radio,
42→ Clock,
43→ Trophy,
44→ Sparkles,
45→ Award,
46→ Crown,
47→ Medal,
48→ Link2,
49→ Flame,
50→ Zap,
51→} from "lucide-react";
52→
53→const DIFFICULTY_ORDER: BeaconDifficulty[] = ["routine", "anomaly", "singular"];
54→
55→function rankBadge(rank: number) {
56→ if (rank === 0) return { icon: Crown, color: "#fbbf24", label: "1st" };
57→ if (rank === 1) return { icon: Medal, color: "#cbd5e1", label: "2nd" };
58→ if (rank === 2) return { icon: Award, color: "#f97316", label: "3rd" };
59→ return null;
60→}
61→
62→export function BeaconPanel() {
63→ const grantBeaconReward = useGameStore((s) => s.grantBeaconReward);
64→ const claimWeeklyBeacon = useGameStore((s) => s.claimWeeklyBeacon);
65→ const claimChainReward = useGameStore((s) => s.claimChainReward);
66→ const { toast } = useToast();
67→
68→ const [challenge, setChallenge] = useState<BeaconDailyChallenge | null>(null);
69→ const [progress, setProgress] = useState<BeaconDailyProgress | null>(null);
70→ const [weeklyChallenge, setWeeklyChallenge] =
71→ useState<BeaconWeeklyChallenge | null>(null);
72→ const [weeklyProgress, setWeeklyProgress] =
73→ useState<BeaconWeeklyProgress | null>(null);
74→ const [chainState, setChainState] = useState<BeaconChainState | null>(null);
75→ const [leaderboard, setLeaderboard] = useState<BeaconScoreEntry[]>([]);
76→ const [dayCountdown, setDayCountdown] = useState("00:00:00");
77→ const [weekCountdown, setWeekCountdown] = useState("00:00:00");
78→ const [now, setNow] = useState(Date.now());
79→
80→ // 初始化 + 每秒刷新(进度 + 倒计时)
81→ useEffect(() => {
82→ setChallenge(generateDailyChallenge());
83→ setProgress(loadDailyProgress());
84→ setWeeklyChallenge(generateWeeklyChallenge());
85→ setWeeklyProgress(loadWeeklyProgress());
86→ setChainState(loadChainState());
87→ setLeaderboard(loadLeaderboard());
88→ const id = setInterval(() => {
89→ setNow(Date.now());
90→ setProgress(loadDailyProgress());
91→ setWeeklyProgress(loadWeeklyProgress());
92→ setChainState(loadChainState());
93→ setChallenge((c) => c ?? generateDailyChallenge());
94→ setWeeklyChallenge((c) => c ?? generateWeeklyChallenge());
95→ }, 1000);
96→ return () => clearInterval(id);
97→ }, []);
98→
99→ useEffect(() => {
100→ setDayCountdown(formatCountdown(msUntilNextDay(new Date(now))));
101→ setWeekCountdown(formatCountdown(msUntilNextWeek(new Date(now))));
102→ }, [now]);
103→
104→ const handleClaimDaily = useCallback(() => {
105→ if (!challenge || !progress) return;
106→ if (progress.completedAt === null || progress.claimed) return;
107→ const res = claimBeaconReward(challenge, progress);
108→ setLeaderboard(res.leaderboard);
109→ setProgress(loadDailyProgress());
110→ // 发放奖励到游戏状态
111→ grantBeaconReward(res.rewardInsight, res.rewardContact);
112→ sfx("achievement");
113→ toast({
114→ title: "✦ 每日信标奖励已领取",
115→ description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(
116→ 1
117→ )} 接触 · 得分 ${res.score}`,
118→ });
119→ }, [challenge, progress, toast, grantBeaconReward]);
120→
121→ const handleClaimWeekly = useCallback(() => {
122→ if (!weeklyChallenge || !weeklyProgress) return;
123→ if (weeklyProgress.completedAt === null || weeklyProgress.claimed) return;
124→ const res = claimWeeklyBeacon();
125→ setLeaderboard(loadLeaderboard());
126→ setWeeklyProgress(loadWeeklyProgress());
127→ if (res.rewardInsight > 0 || res.rewardContact > 0) {
128→ sfx("achievement");
129→ toast({
130→ title: "✦ 周挑战奖励已领取",
131→ description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(
132→ 1
133→ )} 接触 · 得分 ${res.score}`,
134→ });
135→ }
136→ }, [weeklyChallenge, weeklyProgress, toast, claimWeeklyBeacon]);
137→
138→ const handleClaimChain = useCallback(
139→ (milestone: number) => {
140→ const reward = BEACON_CHAIN_REWARDS.find(
141→ (r) => r.milestone === milestone
142→ );
143→ const res = claimChainReward(milestone);
144→ setChainState(loadChainState());
145→ if (res.ok) {
146→ sfx("achievement");
147→ toast({
148→ title: `✦ ${res.label || reward?.label || "里程碑"} 已领取`,
149→ description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(
150→ 1
151→ )} 接触`,
152→ });
153→ }
154→ },
155→ [toast, claimChainReward]
156→ );
157→
158→ if (!challenge || !progress || !weeklyChallenge || !weeklyProgress || !chainState) {
159→ return (
160→ <div className="flex items-center justify-center h-full text-xs text-muted-foreground/60">
161→ 正在校准深空信标…
162→ </div>
163→ );
164→ }
165→
166→ const diffMeta = BEACON_DIFFICULTY[challenge.difficulty];
167→ const typeMeta = BEACON_TYPE_META[challenge.type];
168→ const pct = Math.min(100, (progress.progress / challenge.goal) * 100);
169→ const isCompleted = progress.completedAt !== null;
170→ const isClaimed = progress.claimed;
171→ const canClaim = isCompleted && !isClaimed;
172→
173→ // 周挑战派生量
174→ const wDiffMeta = BEACON_DIFFICULTY[weeklyChallenge.difficulty];
175→ const wTypeMeta = BEACON_TYPE_META[weeklyChallenge.type];
176→ const wPct = Math.min(100, (weeklyProgress.progress / weeklyChallenge.goal) * 100);
177→ const wCompleted = weeklyProgress.completedAt !== null;
178→ const wClaimed = weeklyProgress.claimed;
179→ const wCanClaim = wCompleted && !wClaimed;
180→
181→ // 信标链派生量
182→ const chainProgress = getChainProgress(chainState.currentStreak);
183→ const nextMilestone = getNextMilestone(chainState.currentStreak);
184→ const todayKey = getTodayKey();
185→ const completedToday = chainState.lastCompletedDateKey === todayKey;
186→
187→ return (
188→ <div className="flex flex-col gap-2.5 h-full overflow-y-auto echo-scroll pr-0.5">
189→ <style jsx global>{`
190→ .echo-scroll::-webkit-scrollbar { width: 4px; }
191→ .echo-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 2px; }
192→ .echo-scroll::-webkit-scrollbar-track { background: transparent; }
193→ @keyframes beacon-pulse-ring {
194→ 0% { transform: scale(0.8); opacity: 0.8; }
195→ 100% { transform: scale(2.2); opacity: 0; }
196→ }
197→ @keyframes beacon-glow {
198→ 0%, 100% { box-shadow: 0 0 18px ${diffMeta.glow}, inset 0 0 12px ${diffMeta.glow}; }
199→ 50% { box-shadow: 0 0 32px ${diffMeta.glow}, inset 0 0 20px ${diffMeta.glow}; }
200→ }
201→ @keyframes weekly-glow {
202→ 0%, 100% { box-shadow: 0 0 18px rgba(232,121,249,0.35), inset 0 0 12px rgba(232,121,249,0.25); }
203→ 50% { box-shadow: 0 0 32px rgba(232,121,249,0.5), inset 0 0 20px rgba(232,121,249,0.35); }
204→ }
205→ @keyframes chain-milestone-pulse {
206→ 0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(251,113,133,0.55); }
207→ 50% { transform: scale(1.06); box-shadow: 0 0 0 8px rgba(251,113,133,0); }
208→ }
209→ .chain-milestone-reachable {
210→ animation: chain-milestone-pulse 1.8s ease-in-out infinite;
211→ }
212→ @keyframes chain-streak-flux {
213→ 0%, 100% { background-position: 0% 50%; }
214→ 50% { background-position: 100% 50%; }
215→ }
216→ .chain-streak-text {
217→ background: linear-gradient(90deg, #fbbf24, #fb7185, #fbbf24);
218→ background-size: 200% 100%;
219→ -webkit-background-clip: text;
220→ background-clip: text;
221→ -webkit-text-fill-color: transparent;
222→ animation: chain-streak-flux 4s ease-in-out infinite;
223→ }
224→ `}</style>
225→
226→ {/* 头部:信标 + 倒计时 */}
227→ <div className="flex items-center justify-between">
228→ <h3 className="text-sm font-semibold flex items-center gap-1.5">
229→ <Radio className="h-4 w-4 text-fuchsia-400" />
230→ 深空信标
231→ </h3>
232→ <div className="flex items-center gap-1 text-[10px] text-muted-foreground/70">
233→ <Clock className="h-3 w-3" />
234→ 次日重置
235→ <span className="font-mono text-fuchsia-300/90">{dayCountdown}</span>
236→ </div>
237→ </div>
238→
239→ {/* 每日挑战卡片 */}
240→ <div
241→ className="relative rounded-xl border p-3 overflow-hidden"
242→ style={{
243→ borderColor: `${diffMeta.color}55`,
244→ background: `linear-gradient(135deg, ${diffMeta.color}1f, rgba(0,0,0,0.45))`,
245→ animation: isCompleted ? "none" : "beacon-glow 3s ease-in-out infinite",
246→ }}
247→ >
248→ {/* 背景装饰:脉冲环 */}
249→ {!isCompleted && (
250→ <>
251→ <div
252→ className="absolute top-4 right-4 h-10 w-10 rounded-full pointer-events-none"
253→ style={{
254→ border: `1.5px solid ${diffMeta.color}`,
255→ animation: "beacon-pulse-ring 2.5s ease-out infinite",
256→ }}
257→ />
258→ <div
259→ className="absolute top-4 right-4 h-10 w-10 rounded-full pointer-events-none"
260→ style={{
261→ border: `1.5px solid ${diffMeta.color}`,
262→ animation: "beacon-pulse-ring 2.5s ease-out infinite 1.25s",
263→ }}
264→ />
265→ </>
266→ )}
267→
268→ <div className="relative">
269→ {/* 难度 + 类型 标签 */}
270→ <div className="flex items-center gap-1.5 mb-2">
271→ <span
272→ className="text-[10px] font-mono px-1.5 py-0.5 rounded flex items-center gap-1"
273→ style={{ background: `${diffMeta.color}22`, color: diffMeta.color, border: `1px solid ${diffMeta.color}55` }}
274→ >
275→ <span>{diffMeta.icon}</span>
276→ {diffMeta.label}
277→ </span>
278→ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-white/5 text-muted-foreground border border-white/10">
279→ {typeMeta.icon} {typeMeta.label}
280→ </span>
281→ {isCompleted && (
282→ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 flex items-center gap-1">
283→ <Sparkles className="h-2.5 w-2.5" /> 已完成
284→ </span>
285→ )}
286→ </div>
287→
288→ {/* 挑战标题 */}
289→ <h4 className="text-sm font-semibold mb-1" style={{ color: diffMeta.color, textShadow: `0 0 10px ${diffMeta.glow}` }}>
290→ {challenge.title}
291→ </h4>
292→ <p className="text-[11px] text-muted-foreground/80 leading-relaxed mb-2.5">
293→ {challenge.desc}
294→ </p>
295→
296→ {/* 进度条 */}
297→ <div className="mb-2">
298→ <div className="flex items-center justify-between mb-1">
299→ <span className="text-[10px] text-muted-foreground/70">进度</span>
300→ <span className="text-[11px] font-mono font-semibold" style={{ color: diffMeta.color }}>
301→ {Math.min(progress.progress, challenge.goal)} / {challenge.goal} {typeMeta.unit}
302→ </span>
303→ </div>
304→ <Progress
305→ value={pct}
306→ className="h-2 bg-black/40"
307→ style={{
308→ ["--progress-color" as string]: diffMeta.color,
309→ }}
310→ />
311→ </div>
312→
313→ {/* 奖励 + 领取按钮 */}
314→ <div className="flex items-center justify-between gap-2">
315→ <div className="flex items-center gap-2 text-[10px]">
316→ <span className="text-muted-foreground/60">奖励:</span>
317→ {challenge.rewardInsight > 0 && (
318→ <span className="text-amber-300 font-mono">+{formatNum(challenge.rewardInsight)}洞见</span>
319→ )}
320→ <span className="text-fuchsia-300 font-mono">+{challenge.rewardContact.toFixed(1)}接触</span>
321→ </div>
322→ <Button
323→ onClick={handleClaimDaily}
324→ disabled={!canClaim}
325→ size="sm"
326→ className="h-7 px-3 text-[11px] border-0"
327→ style={{
328→ background: canClaim
329→ ? `linear-gradient(90deg, ${diffMeta.color}, ${diffMeta.color}cc)`
330→ : `${diffMeta.color}1a`,
331→ color: canClaim ? "#022c22" : `${diffMeta.color}99`,
332→ }}
333→ >
334→ {isClaimed ? "已领取" : canClaim ? "领取奖励" : isCompleted ? "已领取" : "进行中…"}
335→ </Button>
336→ </div>
337→
338→ {/* 完成时长 */}
339→ {isCompleted && progress.durationSec > 0 && (
340→ <div className="mt-1.5 text-[10px] text-muted-foreground/50 text-center">
341→ 完成用时 {Math.floor(progress.durationSec / 60)}分{progress.durationSec % 60}秒
342→ </div>
343→ )}
344→ </div>
345→ </div>
346→
347→ {/* 周挑战 + 信标链(桌面端并排,移动端单列) */}
348→ <div className="grid grid-cols-1 lg:grid-cols-2 gap-2.5">
349→ {/* ===== 周挑战卡片(fuchsia 主题) ===== */}
350→ <div
351→ className="relative rounded-xl border p-3 overflow-hidden"
352→ style={{
353→ borderColor: "rgba(232,121,249,0.4)",
354→ background: `linear-gradient(135deg, rgba(232,121,249,0.12), rgba(0,0,0,0.45))`,
355→ animation: wCompleted ? "none" : "weekly-glow 3.5s ease-in-out infinite",
356→ }}
357→ >
358→ {/* 头部:标题 + weekKey + 倒计时 */}
359→ <div className="flex items-center justify-between mb-2">
360→ <div className="flex items-center gap-1.5">
361→ <Zap className="h-3.5 w-3.5 text-fuchsia-400" />
362→ <span className="text-xs font-semibold text-fuchsia-200">
363→ 周挑战 · WEEKLY
364→ </span>
365→ </div>
366→ <div className="flex items-center gap-1 text-[9px] text-muted-foreground/70">
367→ <span className="font-mono text-fuchsia-300/80">
368→ {weeklyChallenge.weekKey}
369→ </span>
370→ <Clock className="h-2.5 w-2.5" />
371→ <span className="font-mono text-fuchsia-300/90">{weekCountdown}</span>
372→ </div>
373→ </div>
374→
375→ {/* 难度 + 类型 标签 */}
376→ <div className="flex items-center gap-1.5 mb-2">
377→ <span
378→ className="text-[10px] font-mono px-1.5 py-0.5 rounded flex items-center gap-1"
379→ style={{ background: `${wDiffMeta.color}22`, color: wDiffMeta.color, border: `1px solid ${wDiffMeta.color}55` }}
380→ >
381→ <span>{wDiffMeta.icon}</span>
382→ {wDiffMeta.label}
383→ </span>
384→ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-white/5 text-muted-foreground border border-white/10">
385→ {wTypeMeta.icon} {wTypeMeta.label}
386→ </span>
387→ {wCompleted && (
388→ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 flex items-center gap-1">
389→ <Sparkles className="h-2.5 w-2.5" /> 已完成
390→ </span>
391→ )}
392→ </div>
393→
394→ {/* 标题 */}
395→ <h4
396→ className="text-sm font-semibold mb-1"
397→ style={{ color: wDiffMeta.color, textShadow: `0 0 10px ${wDiffMeta.glow}` }}
398→ >
399→ {weeklyChallenge.title}
400→ </h4>
401→ <p className="text-[10px] text-muted-foreground/80 leading-relaxed mb-2">
402→ {weeklyChallenge.desc}
403→ </p>
404→
405→ {/* 进度条 */}
406→ <div className="mb-2">
407→ <div className="flex items-center justify-between mb-1">
408→ <span className="text-[10px] text-muted-foreground/70">进度</span>
409→ <span className="text-[11px] font-mono font-semibold" style={{ color: wDiffMeta.color }}>
410→ {Math.min(weeklyProgress.progress, weeklyChallenge.goal)} / {weeklyChallenge.goal} {wTypeMeta.unit}
411→ </span>
412→ </div>
413→ <Progress
414→ value={wPct}
415→ className="h-2 bg-black/40"
416→ style={{
417→ ["--progress-color" as string]: wDiffMeta.color,
418→ }}
419→ />
420→ </div>
421→
422→ {/* 奖励 + 领取按钮 */}
423→ <div className="flex items-center justify-between gap-2">
424→ <div className="flex items-center gap-1.5 text-[10px]">
425→ <span className="text-muted-foreground/60">奖励:</span>
426→ {weeklyChallenge.rewardInsight > 0 && (
427→ <span className="text-amber-300 font-mono">
428→ +{formatNum(weeklyChallenge.rewardInsight)}洞见
429→ </span>
430→ )}
431→ <span className="text-fuchsia-300 font-mono">
432→ +{weeklyChallenge.rewardContact.toFixed(1)}接触
433→ </span>
434→ </div>
435→ <Button
436→ onClick={handleClaimWeekly}
437→ disabled={!wCanClaim}
438→ size="sm"
439→ className="h-7 px-3 text-[11px] border-0"
440→ style={{
441→ background: wCanClaim
442→ ? `linear-gradient(90deg, #34d399, #34d399cc)`
443→ : "rgba(232,121,249,0.10)",
444→ color: wCanClaim
445→ ? "#022c22"
446→ : "rgba(232,121,249,0.55)",
447→ }}
448→ >
449→ {wClaimed
450→ ? "已领取"
451→ : wCanClaim
452→ ? "领取奖励"
453→ : wCompleted
454→ ? "已领取"
455→ : "进行中…"}
456→ </Button>
457→ </div>
458→
459→ {wCompleted && weeklyProgress.durationSec > 0 && (
460→ <div className="mt-1.5 text-[10px] text-muted-foreground/50 text-center">
461→ 完成用时 {Math.floor(weeklyProgress.durationSec / 60)}分{weeklyProgress.durationSec % 60}秒
462→ </div>
463→ )}
464→ </div>
465→
466→ {/* ===== 信标链卡片(amber→rose 渐变) ===== */}
467→ <div
468→ className="relative rounded-xl border p-3 overflow-hidden"
469→ style={{
470→ borderColor: "rgba(251,191,36,0.35)",
471→ background: `linear-gradient(135deg, rgba(251,191,36,0.10), rgba(251,113,133,0.10), rgba(0,0,0,0.4))`,
472→ }}
473→ >
474→ {/* 头部:标题 + 当前连续天数 */}
475→ <div className="flex items-center justify-between mb-2">
476→ <div className="flex items-center gap-1.5">
477→ <Link2 className="h-3.5 w-3.5 text-amber-400" />
478→ <span className="text-xs font-semibold text-amber-200">
479→ 信标链 · CHAIN
480→ </span>
481→ </div>
482→ <div className="flex items-baseline gap-1">
483→ <Flame className="h-3 w-3 text-rose-400" />
484→ <span className="text-[10px] text-muted-foreground/60">连续</span>
485→ <span className="chain-streak-text text-2xl font-bold font-mono leading-none">
486→ {chainState.currentStreak}
487→ </span>
488→ <span className="text-[10px] text-muted-foreground/60">天</span>
489→ </div>
490→ </div>
491→
492→ {/* 今日完成状态 */}
493→ <div className="mb-2 flex items-center justify-between">
494→ <span className="text-[10px] text-muted-foreground/70">
495→ {completedToday
496→ ? "今日已贡献"
497→ : "今日尚未完成日挑战"}
498→ </span>
499→ <span
500→ className={`text-[10px] font-mono px-1.5 py-0.5 rounded border ${
501→ completedToday
502→ ? "bg-emerald-500/15 text-emerald-300 border-emerald-500/30"
503→ : "bg-white/5 text-muted-foreground/70 border-white/10"
504→ }`}
505→ >
506→ {completedToday ? "✓ 已记录" : "○ 待完成"}
507→ </span>
508→ </div>
509→
510→ {/* 4 个里程碑节点横向排列 */}
511→ <div className="flex items-center justify-between mb-2 relative">
512→ {/* 节点之间的连线(背景灰) */}
513→ <div className="absolute top-6 left-[12.5%] right-[12.5%] h-[2px] bg-white/10" />
514→ {/* 已达成部分高亮(基于 prev→next 插值) */}
515→ {(() => {
516→ // 节点圆心水平位置(百分比)
517→ const MILESTONE_POS: Record<number, number> = {
518→ 0: 12.5,
519→ 3: 12.5,
520→ 7: 37.5,
521→ 14: 62.5,
522→ 30: 87.5,
523→ };
524→ const prevPos =
525→ MILESTONE_POS[chainProgress.prev] ?? 12.5;
526→ const nextPos =
527→ chainProgress.next !== null
528→ ? MILESTONE_POS[chainProgress.next] ?? 87.5
529→ : 87.5;
530→ const pct = chainProgress.progressPct / 100;
531→ const activeEndPos = prevPos + (nextPos - prevPos) * pct;
532→ const widthPct = Math.max(0, activeEndPos - 12.5);
533→ if (widthPct <= 0) return null;
534→ return (
535→ <div
536→ className="absolute top-6 h-[2px]"
537→ style={{
538→ left: "12.5%",
539→ width: `${widthPct}%`,
540→ background:
541→ "linear-gradient(90deg, #fbbf24, #fb7185)",
542→ boxShadow: "0 0 8px rgba(251,113,133,0.5)",
543→ }}
544→ />
545→ );
546→ })()}
547→
548→ {BEACON_CHAIN_MILESTONES.map((m) => {
549→ const reward = BEACON_CHAIN_REWARDS.find((r) => r.milestone === m);
550→ const claimed = chainState.milestonesClaimed.includes(m);
551→ const reachable =
552→ chainState.currentStreak >= m && !claimed;
553→ const inProgress = chainState.currentStreak > 0 && nextMilestone === m;
554→ // 节点配色
555→ let nodeBg = "rgba(255,255,255,0.05)";
556→ let nodeBorder = "rgba(255,255,255,0.15)";
557→ let textColor = "rgba(255,255,255,0.4)";
558→ if (claimed) {
559→ nodeBg = "rgba(52,211,153,0.25)";
560→ nodeBorder = "#34d399";
561→ textColor = "#34d399";
562→ } else if (reachable) {
563→ nodeBg = "rgba(251,113,133,0.20)";
564→ nodeBorder = "#fb7185";
565→ textColor = "#fb7185";
566→ } else if (inProgress) {
567→ nodeBg = "rgba(251,191,36,0.20)";
568→ nodeBorder = "#fbbf24";
569→ textColor = "#fbbf24";
570→ }
571→
572→ return (
573→ <div
574→ key={m}
575→ className="flex flex-col items-center gap-1 z-10 flex-1"
576→ >
577→ <div
578→ className={`w-12 h-12 rounded-full flex items-center justify-center font-mono font-bold text-sm border-2 ${
579→ reachable ? "chain-milestone-reachable" : ""
580→ }`}
581→ style={{
582→ background: nodeBg,
583→ borderColor: nodeBorder,
584→ color: textColor,
585→ }}
586→ >
587→ {claimed ? (
588→ <Sparkles className="h-4 w-4" />
589→ ) : (
590→ m
591→ )}
592→ </div>
593→ <span
594→ className="text-[9px] text-center leading-tight"
595→ style={{ color: textColor }}
596→ >
597→ {reward?.label}
598→ </span>
599→ {claimed ? (
600→ <span className="text-[9px] text-emerald-400/80 font-mono">
601→ 已领
602→ </span>
603→ ) : reachable ? (
604→ <Button
605→ onClick={() => handleClaimChain(m)}
606→ size="sm"
607→ className="h-5 px-2 text-[9px] py-0 border-0"
608→ style={{
609→ background:
610→ "linear-gradient(90deg, #fb7185, #f43f5e)",
611→ color: "#1c0608",
612→ }}
613→ >
614→ 领取
615→ </Button>
616→ ) : (
617→ <span className="text-[9px] text-muted-foreground/40 font-mono">
618→ +{reward?.rewardInsight}洞
619→ </span>
620→ )}
621→ </div>
622→ );
623→ })}
624→ </div>
625→
626→ {/* 进度条 */}
627→ <div className="mb-2">
628→ <div className="flex items-center justify-between mb-1">
629→ <span className="text-[10px] text-muted-foreground/70">
630→ {chainProgress.next === null
631→ ? "已通关全部里程碑"
632→ : `下一目标:${chainProgress.next} 天`}
633→ </span>
634→ <span className="text-[10px] font-mono text-amber-300">
635→ {chainProgress.current}
636→ {chainProgress.next !== null && ` / ${chainProgress.next}`} 天
637→ </span>
638→ </div>
639→ <Progress
640→ value={chainProgress.progressPct}
641→ className="h-1.5 bg-black/40"
642→ style={{
643→ ["--progress-color" as string]: "#fbbf24",
644→ }}
645→ />
646→ </div>
647→
648→ {/* 底部统计 */}
649→ <div className="flex items-center justify-between gap-2 text-[10px]">
650→ <div className="flex items-center gap-2">
651→ <span className="text-muted-foreground/60">最长</span>
652→ <span className="font-mono text-amber-300">
653→ {chainState.longestStreak}天
654→ </span>
655→ </div>
656→ <div className="flex items-center gap-2">
657→ <span className="text-muted-foreground/60">累计</span>
658→ <span className="font-mono text-rose-300">
659→ {chainState.totalCompletions}次
660→ </span>
661→ </div>
662→ <div className="flex items-center gap-1">
663→ <span className="text-muted-foreground/60">续命</span>
664→ {chainState.graceUsed >= 1 ? (
665→ <span className="font-mono text-rose-400/80">已用</span>
666→ ) : (
667→ <span className="font-mono text-emerald-400/80">可用</span>
668→ )}
669→ </div>
670→ </div>
671→ </div>
672→ </div>
673→
674→ {/* 本地排行榜 */}
675→ <div className="rounded-xl border border-white/10 bg-black/30 p-2.5">
676→ <div className="flex items-center gap-1.5 mb-2">
677→ <Trophy className="h-3.5 w-3.5 text-amber-400" />
678→ <span className="text-xs font-semibold">深空排行榜</span>
679→ <span className="text-[10px] text-muted-foreground/50">本地 · Top {leaderboard.length || 0}</span>
680→ </div>
681→
682→ {leaderboard.length === 0 ? (
683→ <div className="text-center py-4 text-[11px] text-muted-foreground/40">
684→ <Trophy className="h-6 w-6 mx-auto mb-1 opacity-30" />
685→ 尚无记录。完成今日或本周信标即可登榜。
686→ </div>
687→ ) : (
688→ <div className="space-y-0.5 max-h-[180px] overflow-y-auto echo-scroll">
689→ {leaderboard.map((entry, i) => {
690→ const rb = rankBadge(i);
691→ const eDiff = BEACON_DIFFICULTY[entry.difficulty];
692→ const eType = BEACON_TYPE_META[entry.challenge as BeaconChallengeType];
693→ const isMine = entry.dateKey === getTodayKey() || entry.dateKey === getWeekKey();
694→ return (
695→ <div
696→ key={`${entry.timestamp}-${i}`}
697→ className={`flex items-center gap-2 px-2 py-1 rounded-lg text-[11px] ${
698→ entry.isWeekly
699→ ? "bg-fuchsia-500/10 border border-fuchsia-500/20"
700→ : isMine
701→ ? "bg-fuchsia-500/10 border border-fuchsia-500/20"
702→ : "hover:bg-white/5"
703→ }`}
704→ >
705→ {/* 排名 */}
706→ <span className="w-6 flex items-center justify-center">
707→ {rb ? (
708→ <rb.icon className="h-3.5 w-3.5" style={{ color: rb.color }} />
709→ ) : (
710→ <span className="text-muted-foreground/50 font-mono">{i + 1}</span>
711→ )}
712→ </span>
713→ {/* 类型 + 难度 + 周挑战标记 */}
714→ <span className="flex items-center gap-1 flex-1 min-w-0">
715→ <span style={{ color: eDiff.color }} className="font-mono">{eDiff.icon}</span>
716→ <span className="text-muted-foreground/80 truncate">{eType.label}</span>
717→ {entry.isWeekly && (
718→ <span className="text-[9px] font-mono px-1 rounded bg-fuchsia-500/20 text-fuchsia-300 border border-fuchsia-500/40">
719→ WEEK
720→ </span>
721→ )}
722→ {entry.progress >= 1 && <span className="text-emerald-400/70">✓</span>}
723→ </span>
724→ {/* 用时 */}
725→ <span className="text-muted-foreground/50 font-mono text-[10px] w-16 text-right">
726→ {Math.floor(entry.durationSec / 60)}m{entry.durationSec % 60}s
727→ </span>
728→ {/* 分数 */}
729→ <span className="font-mono font-semibold w-12 text-right" style={{ color: eDiff.color }}>
730→ {formatNum(entry.score)}
731→ </span>
732→ </div>
733→ );
734→ })}
735→ </div>
736→ )}
737→ </div>
738→
739→ {/* 难度图例 */}
740→ <div className="flex items-center justify-center gap-3 text-[9px] text-muted-foreground/50">
741→ {DIFFICULTY_ORDER.map((d) => {
742→ const m = BEACON_DIFFICULTY[d];
743→ return (
744→ <span key={d} className="flex items-center gap-1">
745→ <span style={{ color: m.color }}>{m.icon}</span>
746→ {m.label} ×{m.mult}
747→ </span>
748→ );
749→ })}
750→ </div>
751→ </div>
752→ );
753→}
754→
@@ -1,754 +0,0 @@
1→ 1→"use client";
2→ 2→// 回响星核 / Echo Nexus — 深空信标面板
3→ 3→// v0.5:每日挑战 + 本地排行榜
4→ 4→// v0.8:周挑战 + 信标链(连续完成奖励)
5→ 5→import { useState, useEffect, useCallback } from "react";
6→ 6→import { useGameStore } from "@/store/gameStore";
7→ 7→import { useToast } from "@/hooks/use-toast";
8→ 8→import { sfx } from "@/hooks/useAudio";
9→ 9→import {
10→ 10→ generateDailyChallenge,
11→ 11→ generateWeeklyChallenge,
12→ 12→ loadDailyProgress,
13→ 13→ loadWeeklyProgress,
14→ 14→ loadLeaderboard,
15→ 15→ loadChainState,
16→ 16→ claimBeaconReward,
17→ 17→ BEACON_DIFFICULTY,
18→ 18→ BEACON_TYPE_META,
19→ 19→ BEACON_CHAIN_MILESTONES,
20→ 20→ BEACON_CHAIN_REWARDS,
21→ 21→ getWeekKey,
22→ 22→ getTodayKey,
23→ 23→ getNextMilestone,
24→ 24→ getChainProgress,
25→ 25→ msUntilNextDay,
26→ 26→ msUntilNextWeek,
27→ 27→ formatCountdown,
28→ 28→ type BeaconDailyChallenge,
29→ 29→ type BeaconDailyProgress,
30→ 30→ type BeaconWeeklyChallenge,
31→ 31→ type BeaconWeeklyProgress,
32→ 32→ type BeaconScoreEntry,
33→ 33→ type BeaconChallengeType,
34→ 34→ type BeaconDifficulty,
35→ 35→ type BeaconChainState,
36→ 36→} from "@/lib/game/beacon";
37→ 37→import { formatNum } from "@/lib/game/config";
38→ 38→import { Button } from "@/components/ui/button";
39→ 39→import { Progress } from "@/components/ui/progress";
40→ 40→import {
41→ 41→ Radio,
42→ 42→ Clock,
43→ 43→ Trophy,
44→ 44→ Sparkles,
45→ 45→ Award,
46→ 46→ Crown,
47→ 47→ Medal,
48→ 48→ Link2,
49→ 49→ Flame,
50→ 50→ Zap,
51→ 51→} from "lucide-react";
52→ 52→
53→ 53→const DIFFICULTY_ORDER: BeaconDifficulty[] = ["routine", "anomaly", "singular"];
54→ 54→
55→ 55→function rankBadge(rank: number) {
56→ 56→ if (rank === 0) return { icon: Crown, color: "#fbbf24", label: "1st" };
57→ 57→ if (rank === 1) return { icon: Medal, color: "#cbd5e1", label: "2nd" };
58→ 58→ if (rank === 2) return { icon: Award, color: "#f97316", label: "3rd" };
59→ 59→ return null;
60→ 60→}
61→ 61→
62→ 62→export function BeaconPanel() {
63→ 63→ const grantBeaconReward = useGameStore((s) => s.grantBeaconReward);
64→ 64→ const claimWeeklyBeacon = useGameStore((s) => s.claimWeeklyBeacon);
65→ 65→ const claimChainReward = useGameStore((s) => s.claimChainReward);
66→ 66→ const { toast } = useToast();
67→ 67→
68→ 68→ const [challenge, setChallenge] = useState<BeaconDailyChallenge | null>(null);
69→ 69→ const [progress, setProgress] = useState<BeaconDailyProgress | null>(null);
70→ 70→ const [weeklyChallenge, setWeeklyChallenge] =
71→ 71→ useState<BeaconWeeklyChallenge | null>(null);
72→ 72→ const [weeklyProgress, setWeeklyProgress] =
73→ 73→ useState<BeaconWeeklyProgress | null>(null);
74→ 74→ const [chainState, setChainState] = useState<BeaconChainState | null>(null);
75→ 75→ const [leaderboard, setLeaderboard] = useState<BeaconScoreEntry[]>([]);
76→ 76→ const [dayCountdown, setDayCountdown] = useState("00:00:00");
77→ 77→ const [weekCountdown, setWeekCountdown] = useState("00:00:00");
78→ 78→ const [now, setNow] = useState(Date.now());
79→ 79→
80→ 80→ // 初始化 + 每秒刷新(进度 + 倒计时)
81→ 81→ useEffect(() => {
82→ 82→ setChallenge(generateDailyChallenge());
83→ 83→ setProgress(loadDailyProgress());
84→ 84→ setWeeklyChallenge(generateWeeklyChallenge());
85→ 85→ setWeeklyProgress(loadWeeklyProgress());
86→ 86→ setChainState(loadChainState());
87→ 87→ setLeaderboard(loadLeaderboard());
88→ 88→ const id = setInterval(() => {
89→ 89→ setNow(Date.now());
90→ 90→ setProgress(loadDailyProgress());
91→ 91→ setWeeklyProgress(loadWeeklyProgress());
92→ 92→ setChainState(loadChainState());
93→ 93→ setChallenge((c) => c ?? generateDailyChallenge());
94→ 94→ setWeeklyChallenge((c) => c ?? generateWeeklyChallenge());
95→ 95→ }, 1000);
96→ 96→ return () => clearInterval(id);
97→ 97→ }, []);
98→ 98→
99→ 99→ useEffect(() => {
100→ 100→ setDayCountdown(formatCountdown(msUntilNextDay(new Date(now))));
101→ 101→ setWeekCountdown(formatCountdown(msUntilNextWeek(new Date(now))));
102→ 102→ }, [now]);
103→ 103→
104→ 104→ const handleClaimDaily = useCallback(() => {
105→ 105→ if (!challenge || !progress) return;
106→ 106→ if (progress.completedAt === null || progress.claimed) return;
107→ 107→ const res = claimBeaconReward(challenge, progress);
108→ 108→ setLeaderboard(res.leaderboard);
109→ 109→ setProgress(loadDailyProgress());
110→ 110→ // 发放奖励到游戏状态
111→ 111→ grantBeaconReward(res.rewardInsight, res.rewardContact);
112→ 112→ sfx("achievement");
113→ 113→ toast({
114→ 114→ title: "✦ 每日信标奖励已领取",
115→ 115→ description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(
116→ 116→ 1
117→ 117→ )} 接触 · 得分 ${res.score}`,
118→ 118→ });
119→ 119→ }, [challenge, progress, toast, grantBeaconReward]);
120→ 120→
121→ 121→ const handleClaimWeekly = useCallback(() => {
122→ 122→ if (!weeklyChallenge || !weeklyProgress) return;
123→ 123→ if (weeklyProgress.completedAt === null || weeklyProgress.claimed) return;
124→ 124→ const res = claimWeeklyBeacon();
125→ 125→ setLeaderboard(loadLeaderboard());
126→ 126→ setWeeklyProgress(loadWeeklyProgress());
127→ 127→ if (res.rewardInsight > 0 || res.rewardContact > 0) {
128→ 128→ sfx("achievement");
129→ 129→ toast({
130→ 130→ title: "✦ 周挑战奖励已领取",
131→ 131→ description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(
132→ 132→ 1
133→ 133→ )} 接触 · 得分 ${res.score}`,
134→ 134→ });
135→ 135→ }
136→ 136→ }, [weeklyChallenge, weeklyProgress, toast, claimWeeklyBeacon]);
137→ 137→
138→ 138→ const handleClaimChain = useCallback(
139→ 139→ (milestone: number) => {
140→ 140→ const reward = BEACON_CHAIN_REWARDS.find(
141→ 141→ (r) => r.milestone === milestone
142→ 142→ );
143→ 143→ const res = claimChainReward(milestone);
144→ 144→ setChainState(loadChainState());
145→ 145→ if (res.ok) {
146→ 146→ sfx("achievement");
147→ 147→ toast({
148→ 148→ title: `✦ ${res.label || reward?.label || "里程碑"} 已领取`,
149→ 149→ description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(
150→ 150→ 1
151→ 151→ )} 接触`,
152→ 152→ });
153→ 153→ }
154→ 154→ },
155→ 155→ [toast, claimChainReward]
156→ 156→ );
157→ 157→
158→ 158→ if (!challenge || !progress || !weeklyChallenge || !weeklyProgress || !chainState) {
159→ 159→ return (
160→ 160→ <div className="flex items-center justify-center h-full text-xs text-muted-foreground/60">
161→ 161→ 正在校准深空信标…
162→ 162→ </div>
163→ 163→ );
164→ 164→ }
165→ 165→
166→ 166→ const diffMeta = BEACON_DIFFICULTY[challenge.difficulty];
167→ 167→ const typeMeta = BEACON_TYPE_META[challenge.type];
168→ 168→ const pct = Math.min(100, (progress.progress / challenge.goal) * 100);
169→ 169→ const isCompleted = progress.completedAt !== null;
170→ 170→ const isClaimed = progress.claimed;
171→ 171→ const canClaim = isCompleted && !isClaimed;
172→ 172→
173→ 173→ // 周挑战派生量
174→ 174→ const wDiffMeta = BEACON_DIFFICULTY[weeklyChallenge.difficulty];
175→ 175→ const wTypeMeta = BEACON_TYPE_META[weeklyChallenge.type];
176→ 176→ const wPct = Math.min(100, (weeklyProgress.progress / weeklyChallenge.goal) * 100);
177→ 177→ const wCompleted = weeklyProgress.completedAt !== null;
178→ 178→ const wClaimed = weeklyProgress.claimed;
179→ 179→ const wCanClaim = wCompleted && !wClaimed;
180→ 180→
181→ 181→ // 信标链派生量
182→ 182→ const chainProgress = getChainProgress(chainState.currentStreak);
183→ 183→ const nextMilestone = getNextMilestone(chainState.currentStreak);
184→ 184→ const todayKey = getTodayKey();
185→ 185→ const completedToday = chainState.lastCompletedDateKey === todayKey;
186→ 186→
187→ 187→ return (
188→ 188→ <div className="flex flex-col gap-2.5 h-full overflow-y-auto echo-scroll pr-0.5">
189→ 189→ <style jsx global>{`
190→ 190→ .echo-scroll::-webkit-scrollbar { width: 4px; }
191→ 191→ .echo-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 2px; }
192→ 192→ .echo-scroll::-webkit-scrollbar-track { background: transparent; }
193→ 193→ @keyframes beacon-pulse-ring {
194→ 194→ 0% { transform: scale(0.8); opacity: 0.8; }
195→ 195→ 100% { transform: scale(2.2); opacity: 0; }
196→ 196→ }
197→ 197→ @keyframes beacon-glow {
198→ 198→ 0%, 100% { box-shadow: 0 0 18px ${diffMeta.glow}, inset 0 0 12px ${diffMeta.glow}; }
199→ 199→ 50% { box-shadow: 0 0 32px ${diffMeta.glow}, inset 0 0 20px ${diffMeta.glow}; }
200→ 200→ }
201→ 201→ @keyframes weekly-glow {
202→ 202→ 0%, 100% { box-shadow: 0 0 18px rgba(232,121,249,0.35), inset 0 0 12px rgba(232,121,249,0.25); }
203→ 203→ 50% { box-shadow: 0 0 32px rgba(232,121,249,0.5), inset 0 0 20px rgba(232,121,249,0.35); }
204→ 204→ }
205→ 205→ @keyframes chain-milestone-pulse {
206→ 206→ 0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(251,113,133,0.55); }
207→ 207→ 50% { transform: scale(1.06); box-shadow: 0 0 0 8px rgba(251,113,133,0); }
208→ 208→ }
209→ 209→ .chain-milestone-reachable {
210→ 210→ animation: chain-milestone-pulse 1.8s ease-in-out infinite;
211→ 211→ }
212→ 212→ @keyframes chain-streak-flux {
213→ 213→ 0%, 100% { background-position: 0% 50%; }
214→ 214→ 50% { background-position: 100% 50%; }
215→ 215→ }
216→ 216→ .chain-streak-text {
217→ 217→ background: linear-gradient(90deg, #fbbf24, #fb7185, #fbbf24);
218→ 218→ background-size: 200% 100%;
219→ 219→ -webkit-background-clip: text;
220→ 220→ background-clip: text;
221→ 221→ -webkit-text-fill-color: transparent;
222→ 222→ animation: chain-streak-flux 4s ease-in-out infinite;
223→ 223→ }
224→ 224→ `}</style>
225→ 225→
226→ 226→ {/* 头部:信标 + 倒计时 */}
227→ 227→ <div className="flex items-center justify-between">
228→ 228→ <h3 className="text-sm font-semibold flex items-center gap-1.5">
229→ 229→ <Radio className="h-4 w-4 text-fuchsia-400" />
230→ 230→ 深空信标
231→ 231→ </h3>
232→ 232→ <div className="flex items-center gap-1 text-[10px] text-muted-foreground/70">
233→ 233→ <Clock className="h-3 w-3" />
234→ 234→ 次日重置
235→ 235→ <span className="font-mono text-fuchsia-300/90">{dayCountdown}</span>
236→ 236→ </div>
237→ 237→ </div>
238→ 238→
239→ 239→ {/* 每日挑战卡片 */}
240→ 240→ <div
241→ 241→ className="relative rounded-xl border p-3 overflow-hidden"
242→ 242→ style={{
243→ 243→ borderColor: `${diffMeta.color}55`,
244→ 244→ background: `linear-gradient(135deg, ${diffMeta.color}1f, rgba(0,0,0,0.45))`,
245→ 245→ animation: isCompleted ? "none" : "beacon-glow 3s ease-in-out infinite",
246→ 246→ }}
247→ 247→ >
248→ 248→ {/* 背景装饰:脉冲环 */}
249→ 249→ {!isCompleted && (
250→ 250→ <>
251→ 251→ <div
252→ 252→ className="absolute top-4 right-4 h-10 w-10 rounded-full pointer-events-none"
253→ 253→ style={{
254→ 254→ border: `1.5px solid ${diffMeta.color}`,
255→ 255→ animation: "beacon-pulse-ring 2.5s ease-out infinite",
256→ 256→ }}
257→ 257→ />
258→ 258→ <div
259→ 259→ className="absolute top-4 right-4 h-10 w-10 rounded-full pointer-events-none"
260→ 260→ style={{
261→ 261→ border: `1.5px solid ${diffMeta.color}`,
262→ 262→ animation: "beacon-pulse-ring 2.5s ease-out infinite 1.25s",
263→ 263→ }}
264→ 264→ />
265→ 265→ </>
266→ 266→ )}
267→ 267→
268→ 268→ <div className="relative">
269→ 269→ {/* 难度 + 类型 标签 */}
270→ 270→ <div className="flex items-center gap-1.5 mb-2">
271→ 271→ <span
272→ 272→ className="text-[10px] font-mono px-1.5 py-0.5 rounded flex items-center gap-1"
273→ 273→ style={{ background: `${diffMeta.color}22`, color: diffMeta.color, border: `1px solid ${diffMeta.color}55` }}
274→ 274→ >
275→ 275→ <span>{diffMeta.icon}</span>
276→ 276→ {diffMeta.label}
277→ 277→ </span>
278→ 278→ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-white/5 text-muted-foreground border border-white/10">
279→ 279→ {typeMeta.icon} {typeMeta.label}
280→ 280→ </span>
281→ 281→ {isCompleted && (
282→ 282→ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 flex items-center gap-1">
283→ 283→ <Sparkles className="h-2.5 w-2.5" /> 已完成
284→ 284→ </span>
285→ 285→ )}
286→ 286→ </div>
287→ 287→
288→ 288→ {/* 挑战标题 */}
289→ 289→ <h4 className="text-sm font-semibold mb-1" style={{ color: diffMeta.color, textShadow: `0 0 10px ${diffMeta.glow}` }}>
290→ 290→ {challenge.title}
291→ 291→ </h4>
292→ 292→ <p className="text-[11px] text-muted-foreground/80 leading-relaxed mb-2.5">
293→ 293→ {challenge.desc}
294→ 294→ </p>
295→ 295→
296→ 296→ {/* 进度条 */}
297→ 297→ <div className="mb-2">
298→ 298→ <div className="flex items-center justify-between mb-1">
299→ 299→ <span className="text-[10px] text-muted-foreground/70">进度</span>
300→ 300→ <span className="text-[11px] font-mono font-semibold" style={{ color: diffMeta.color }}>
301→ 301→ {Math.min(progress.progress, challenge.goal)} / {challenge.goal} {typeMeta.unit}
302→ 302→ </span>
303→ 303→ </div>
304→ 304→ <Progress
305→ 305→ value={pct}
306→ 306→ className="h-2 bg-black/40"
307→ 307→ style={{
308→ 308→ ["--progress-color" as string]: diffMeta.color,
309→ 309→ }}
310→ 310→ />
311→ 311→ </div>
312→ 312→
313→ 313→ {/* 奖励 + 领取按钮 */}
314→ 314→ <div className="flex items-center justify-between gap-2">
315→ 315→ <div className="flex items-center gap-2 text-[10px]">
316→ 316→ <span className="text-muted-foreground/60">奖励:</span>
317→ 317→ {challenge.rewardInsight > 0 && (
318→ 318→ <span className="text-amber-300 font-mono">+{formatNum(challenge.rewardInsight)}洞见</span>
319→ 319→ )}
320→ 320→ <span className="text-fuchsia-300 font-mono">+{challenge.rewardContact.toFixed(1)}接触</span>
321→ 321→ </div>
322→ 322→ <Button
323→ 323→ onClick={handleClaimDaily}
324→ 324→ disabled={!canClaim}
325→ 325→ size="sm"
326→ 326→ className="h-7 px-3 text-[11px] border-0"
327→ 327→ style={{
328→ 328→ background: canClaim
329→ 329→ ? `linear-gradient(90deg, ${diffMeta.color}, ${diffMeta.color}cc)`
330→ 330→ : `${diffMeta.color}1a`,
331→ 331→ color: canClaim ? "#022c22" : `${diffMeta.color}99`,
332→ 332→ }}
333→ 333→ >
334→ 334→ {isClaimed ? "已领取" : canClaim ? "领取奖励" : isCompleted ? "已领取" : "进行中…"}
335→ 335→ </Button>
336→ 336→ </div>
337→ 337→
338→ 338→ {/* 完成时长 */}
339→ 339→ {isCompleted && progress.durationSec > 0 && (
340→ 340→ <div className="mt-1.5 text-[10px] text-muted-foreground/50 text-center">
341→ 341→ 完成用时 {Math.floor(progress.durationSec / 60)}分{progress.durationSec % 60}秒
342→ 342→ </div>
343→ 343→ )}
344→ 344→ </div>
345→ 345→ </div>
346→ 346→
347→ 347→ {/* 周挑战 + 信标链(桌面端并排,移动端单列) */}
348→ 348→ <div className="grid grid-cols-1 lg:grid-cols-2 gap-2.5">
349→ 349→ {/* ===== 周挑战卡片(fuchsia 主题) ===== */}
350→ 350→ <div
351→ 351→ className="relative rounded-xl border p-3 overflow-hidden"
352→ 352→ style={{
353→ 353→ borderColor: "rgba(232,121,249,0.4)",
354→ 354→ background: `linear-gradient(135deg, rgba(232,121,249,0.12), rgba(0,0,0,0.45))`,
355→ 355→ animation: wCompleted ? "none" : "weekly-glow 3.5s ease-in-out infinite",
356→ 356→ }}
357→ 357→ >
358→ 358→ {/* 头部:标题 + weekKey + 倒计时 */}
359→ 359→ <div className="flex items-center justify-between mb-2">
360→ 360→ <div className="flex items-center gap-1.5">
361→ 361→ <Zap className="h-3.5 w-3.5 text-fuchsia-400" />
362→ 362→ <span className="text-xs font-semibold text-fuchsia-200">
363→ 363→ 周挑战 · WEEKLY
364→ 364→ </span>
365→ 365→ </div>
366→ 366→ <div className="flex items-center gap-1 text-[9px] text-muted-foreground/70">
367→ 367→ <span className="font-mono text-fuchsia-300/80">
368→ 368→ {weeklyChallenge.weekKey}
369→ 369→ </span>
370→ 370→ <Clock className="h-2.5 w-2.5" />
371→ 371→ <span className="font-mono text-fuchsia-300/90">{weekCountdown}</span>
372→ 372→ </div>
373→ 373→ </div>
374→ 374→
375→ 375→ {/* 难度 + 类型 标签 */}
376→ 376→ <div className="flex items-center gap-1.5 mb-2">
377→ 377→ <span
378→ 378→ className="text-[10px] font-mono px-1.5 py-0.5 rounded flex items-center gap-1"
379→ 379→ style={{ background: `${wDiffMeta.color}22`, color: wDiffMeta.color, border: `1px solid ${wDiffMeta.color}55` }}
380→ 380→ >
381→ 381→ <span>{wDiffMeta.icon}</span>
382→ 382→ {wDiffMeta.label}
383→ 383→ </span>
384→ 384→ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-white/5 text-muted-foreground border border-white/10">
385→ 385→ {wTypeMeta.icon} {wTypeMeta.label}
386→ 386→ </span>
387→ 387→ {wCompleted && (
388→ 388→ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 flex items-center gap-1">
389→ 389→ <Sparkles className="h-2.5 w-2.5" /> 已完成
390→ 390→ </span>
391→ 391→ )}
392→ 392→ </div>
393→ 393→
394→ 394→ {/* 标题 */}
395→ 395→ <h4
396→ 396→ className="text-sm font-semibold mb-1"
397→ 397→ style={{ color: wDiffMeta.color, textShadow: `0 0 10px ${wDiffMeta.glow}` }}
398→ 398→ >
399→ 399→ {weeklyChallenge.title}
400→ 400→ </h4>
401→ 401→ <p className="text-[10px] text-muted-foreground/80 leading-relaxed mb-2">
402→ 402→ {weeklyChallenge.desc}
403→ 403→ </p>
404→ 404→
405→ 405→ {/* 进度条 */}
406→ 406→ <div className="mb-2">
407→ 407→ <div className="flex items-center justify-between mb-1">
408→ 408→ <span className="text-[10px] text-muted-foreground/70">进度</span>
409→ 409→ <span className="text-[11px] font-mono font-semibold" style={{ color: wDiffMeta.color }}>
410→ 410→ {Math.min(weeklyProgress.progress, weeklyChallenge.goal)} / {weeklyChallenge.goal} {wTypeMeta.unit}
411→ 411→ </span>
412→ 412→ </div>
413→ 413→ <Progress
414→ 414→ value={wPct}
415→ 415→ className="h-2 bg-black/40"
416→ 416→ style={{
417→ 417→ ["--progress-color" as string]: wDiffMeta.color,
418→ 418→ }}
419→ 419→ />
420→ 420→ </div>
421→ 421→
422→ 422→ {/* 奖励 + 领取按钮 */}
423→ 423→ <div className="flex items-center justify-between gap-2">
424→ 424→ <div className="flex items-center gap-1.5 text-[10px]">
425→ 425→ <span className="text-muted-foreground/60">奖励:</span>
426→ 426→ {weeklyChallenge.rewardInsight > 0 && (
427→ 427→ <span className="text-amber-300 font-mono">
428→ 428→ +{formatNum(weeklyChallenge.rewardInsight)}洞见
429→ 429→ </span>
430→ 430→ )}
431→ 431→ <span className="text-fuchsia-300 font-mono">
432→ 432→ +{weeklyChallenge.rewardContact.toFixed(1)}接触
433→ 433→ </span>
434→ 434→ </div>
435→ 435→ <Button
436→ 436→ onClick={handleClaimWeekly}
437→ 437→ disabled={!wCanClaim}
438→ 438→ size="sm"
439→ 439→ className="h-7 px-3 text-[11px] border-0"
440→ 440→ style={{
441→ 441→ background: wCanClaim
442→ 442→ ? `linear-gradient(90deg, #34d399, #34d399cc)`
443→ 443→ : "rgba(232,121,249,0.10)",
444→ 444→ color: wCanClaim
445→ 445→ ? "#022c22"
446→ 446→ : "rgba(232,121,249,0.55)",
447→ 447→ }}
448→ 448→ >
449→ 449→ {wClaimed
450→ 450→ ? "已领取"
451→ 451→ : wCanClaim
452→ 452→ ? "领取奖励"
453→ 453→ : wCompleted
454→ 454→ ? "已领取"
455→ 455→ : "进行中…"}
456→ 456→ </Button>
457→ 457→ </div>
458→ 458→
459→ 459→ {wCompleted && weeklyProgress.durationSec > 0 && (
460→ 460→ <div className="mt-1.5 text-[10px] text-muted-foreground/50 text-center">
461→ 461→ 完成用时 {Math.floor(weeklyProgress.durationSec / 60)}分{weeklyProgress.durationSec % 60}秒
462→ 462→ </div>
463→ 463→ )}
464→ 464→ </div>
465→ 465→
466→ 466→ {/* ===== 信标链卡片(amber→rose 渐变) ===== */}
467→ 467→ <div
468→ 468→ className="relative rounded-xl border p-3 overflow-hidden"
469→ 469→ style={{
470→ 470→ borderColor: "rgba(251,191,36,0.35)",
471→ 471→ background: `linear-gradient(135deg, rgba(251,191,36,0.10), rgba(251,113,133,0.10), rgba(0,0,0,0.4))`,
472→ 472→ }}
473→ 473→ >
474→ 474→ {/* 头部:标题 + 当前连续天数 */}
475→ 475→ <div className="flex items-center justify-between mb-2">
476→ 476→ <div className="flex items-center gap-1.5">
477→ 477→ <Link2 className="h-3.5 w-3.5 text-amber-400" />
478→ 478→ <span className="text-xs font-semibold text-amber-200">
479→ 479→ 信标链 · CHAIN
480→ 480→ </span>
481→ 481→ </div>
482→ 482→ <div className="flex items-baseline gap-1">
483→ 483→ <Flame className="h-3 w-3 text-rose-400" />
484→ 484→ <span className="text-[10px] text-muted-foreground/60">连续</span>
485→ 485→ <span className="chain-streak-text text-2xl font-bold font-mono leading-none">
486→ 486→ {chainState.currentStreak}
487→ 487→ </span>
488→ 488→ <span className="text-[10px] text-muted-foreground/60">天</span>
489→ 489→ </div>
490→ 490→ </div>
491→ 491→
492→ 492→ {/* 今日完成状态 */}
493→ 493→ <div className="mb-2 flex items-center justify-between">
494→ 494→ <span className="text-[10px] text-muted-foreground/70">
495→ 495→ {completedToday
496→ 496→ ? "今日已贡献"
497→ 497→ : "今日尚未完成日挑战"}
498→ 498→ </span>
499→ 499→ <span
500→ 500→ className={`text-[10px] font-mono px-1.5 py-0.5 rounded border ${
501→ 501→ completedToday
502→ 502→ ? "bg-emerald-500/15 text-emerald-300 border-emerald-500/30"
503→ 503→ : "bg-white/5 text-muted-foreground/70 border-white/10"
504→ 504→ }`}
505→ 505→ >
506→ 506→ {completedToday ? "✓ 已记录" : "○ 待完成"}
507→ 507→ </span>
508→ 508→ </div>
509→ 509→
510→ 510→ {/* 4 个里程碑节点横向排列 */}
511→ 511→ <div className="flex items-center justify-between mb-2 relative">
512→ 512→ {/* 节点之间的连线(背景灰) */}
513→ 513→ <div className="absolute top-6 left-[12.5%] right-[12.5%] h-[2px] bg-white/10" />
514→ 514→ {/* 已达成部分高亮(基于 prev→next 插值) */}
515→ 515→ {(() => {
516→ 516→ // 节点圆心水平位置(百分比)
517→ 517→ const MILESTONE_POS: Record<number, number> = {
518→ 518→ 0: 12.5,
519→ 519→ 3: 12.5,
520→ 520→ 7: 37.5,
521→ 521→ 14: 62.5,
522→ 522→ 30: 87.5,
523→ 523→ };
524→ 524→ const prevPos =
525→ 525→ MILESTONE_POS[chainProgress.prev] ?? 12.5;
526→ 526→ const nextPos =
527→ 527→ chainProgress.next !== null
528→ 528→ ? MILESTONE_POS[chainProgress.next] ?? 87.5
529→ 529→ : 87.5;
530→ 530→ const pct = chainProgress.progressPct / 100;
531→ 531→ const activeEndPos = prevPos + (nextPos - prevPos) * pct;
532→ 532→ const widthPct = Math.max(0, activeEndPos - 12.5);
533→ 533→ if (widthPct <= 0) return null;
534→ 534→ return (
535→ 535→ <div
536→ 536→ className="absolute top-6 h-[2px]"
537→ 537→ style={{
538→ 538→ left: "12.5%",
539→ 539→ width: `${widthPct}%`,
540→ 540→ background:
541→ 541→ "linear-gradient(90deg, #fbbf24, #fb7185)",
542→ 542→ boxShadow: "0 0 8px rgba(251,113,133,0.5)",
543→ 543→ }}
544→ 544→ />
545→ 545→ );
546→ 546→ })()}
547→ 547→
548→ 548→ {BEACON_CHAIN_MILESTONES.map((m) => {
549→ 549→ const reward = BEACON_CHAIN_REWARDS.find((r) => r.milestone === m);
550→ 550→ const claimed = chainState.milestonesClaimed.includes(m);
551→ 551→ const reachable =
552→ 552→ chainState.currentStreak >= m && !claimed;
553→ 553→ const inProgress = chainState.currentStreak > 0 && nextMilestone === m;
554→ 554→ // 节点配色
555→ 555→ let nodeBg = "rgba(255,255,255,0.05)";
556→ 556→ let nodeBorder = "rgba(255,255,255,0.15)";
557→ 557→ let textColor = "rgba(255,255,255,0.4)";
558→ 558→ if (claimed) {
559→ 559→ nodeBg = "rgba(52,211,153,0.25)";
560→ 560→ nodeBorder = "#34d399";
561→ 561→ textColor = "#34d399";
562→ 562→ } else if (reachable) {
563→ 563→ nodeBg = "rgba(251,113,133,0.20)";
564→ 564→ nodeBorder = "#fb7185";
565→ 565→ textColor = "#fb7185";
566→ 566→ } else if (inProgress) {
567→ 567→ nodeBg = "rgba(251,191,36,0.20)";
568→ 568→ nodeBorder = "#fbbf24";
569→ 569→ textColor = "#fbbf24";
570→ 570→ }
571→ 571→
572→ 572→ return (
573→ 573→ <div
574→ 574→ key={m}
575→ 575→ className="flex flex-col items-center gap-1 z-10 flex-1"
576→ 576→ >
577→ 577→ <div
578→ 578→ className={`w-12 h-12 rounded-full flex items-center justify-center font-mono font-bold text-sm border-2 ${
579→ 579→ reachable ? "chain-milestone-reachable" : ""
580→ 580→ }`}
581→ 581→ style={{
582→ 582→ background: nodeBg,
583→ 583→ borderColor: nodeBorder,
584→ 584→ color: textColor,
585→ 585→ }}
586→ 586→ >
587→ 587→ {claimed ? (
588→ 588→ <Sparkles className="h-4 w-4" />
589→ 589→ ) : (
590→ 590→ m
591→ 591→ )}
592→ 592→ </div>
593→ 593→ <span
594→ 594→ className="text-[9px] text-center leading-tight"
595→ 595→ style={{ color: textColor }}
596→ 596→ >
597→ 597→ {reward?.label}
598→ 598→ </span>
599→ 599→ {claimed ? (
600→ 600→ <span className="text-[9px] text-emerald-400/80 font-mono">
601→ 601→ 已领
602→ 602→ </span>
603→ 603→ ) : reachable ? (
604→ 604→ <Button
605→ 605→ onClick={() => handleClaimChain(m)}
606→ 606→ size="sm"
607→ 607→ className="h-5 px-2 text-[9px] py-0 border-0"
608→ 608→ style={{
609→ 609→ background:
610→ 610→ "linear-gradient(90deg, #fb7185, #f43f5e)",
611→ 611→ color: "#1c0608",
612→ 612→ }}
613→ 613→ >
614→ 614→ 领取
615→ 615→ </Button>
616→ 616→ ) : (
617→ 617→ <span className="text-[9px] text-muted-foreground/40 font-mono">
618→ 618→ +{reward?.rewardInsight}洞
619→ 619→ </span>
620→ 620→ )}
621→ 621→ </div>
622→ 622→ );
623→ 623→ })}
624→ 624→ </div>
625→ 625→
626→ 626→ {/* 进度条 */}
627→ 627→ <div className="mb-2">
628→ 628→ <div className="flex items-center justify-between mb-1">
629→ 629→ <span className="text-[10px] text-muted-foreground/70">
630→ 630→ {chainProgress.next === null
631→ 631→ ? "已通关全部里程碑"
632→ 632→ : `下一目标:${chainProgress.next} 天`}
633→ 633→ </span>
634→ 634→ <span className="text-[10px] font-mono text-amber-300">
635→ 635→ {chainProgress.current}
636→ 636→ {chainProgress.next !== null && ` / ${chainProgress.next}`} 天
637→ 637→ </span>
638→ 638→ </div>
639→ 639→ <Progress
640→ 640→ value={chainProgress.progressPct}
641→ 641→ className="h-1.5 bg-black/40"
642→ 642→ style={{
643→ 643→ ["--progress-color" as string]: "#fbbf24",
644→ 644→ }}
645→ 645→ />
646→ 646→ </div>
647→ 647→
648→ 648→ {/* 底部统计 */}
649→ 649→ <div className="flex items-center justify-between gap-2 text-[10px]">
650→ 650→ <div className="flex items-center gap-2">
651→ 651→ <span className="text-muted-foreground/60">最长</span>
652→ 652→ <span className="font-mono text-amber-300">
653→ 653→ {chainState.longestStreak}天
654→ 654→ </span>
655→ 655→ </div>
656→ 656→ <div className="flex items-center gap-2">
657→ 657→ <span className="text-muted-foreground/60">累计</span>
658→ 658→ <span className="font-mono text-rose-300">
659→ 659→ {chainState.totalCompletions}次
660→ 660→ </span>
661→ 661→ </div>
662→ 662→ <div className="flex items-center gap-1">
663→ 663→ <span className="text-muted-foreground/60">续命</span>
664→ 664→ {chainState.graceUsed >= 1 ? (
665→ 665→ <span className="font-mono text-rose-400/80">已用</span>
666→ 666→ ) : (
667→ 667→ <span className="font-mono text-emerald-400/80">可用</span>
668→ 668→ )}
669→ 669→ </div>
670→ 670→ </div>
671→ 671→ </div>
672→ 672→ </div>
673→ 673→
674→ 674→ {/* 本地排行榜 */}
675→ 675→ <div className="rounded-xl border border-white/10 bg-black/30 p-2.5">
676→ 676→ <div className="flex items-center gap-1.5 mb-2">
677→ 677→ <Trophy className="h-3.5 w-3.5 text-amber-400" />
678→ 678→ <span className="text-xs font-semibold">深空排行榜</span>
679→ 679→ <span className="text-[10px] text-muted-foreground/50">本地 · Top {leaderboard.length || 0}</span>
680→ 680→ </div>
681→ 681→
682→ 682→ {leaderboard.length === 0 ? (
683→ 683→ <div className="text-center py-4 text-[11px] text-muted-foreground/40">
684→ 684→ <Trophy className="h-6 w-6 mx-auto mb-1 opacity-30" />
685→ 685→ 尚无记录。完成今日或本周信标即可登榜。
686→ 686→ </div>
687→ 687→ ) : (
688→ 688→ <div className="space-y-0.5 max-h-[180px] overflow-y-auto echo-scroll">
689→ 689→ {leaderboard.map((entry, i) => {
690→ 690→ const rb = rankBadge(i);
691→ 691→ const eDiff = BEACON_DIFFICULTY[entry.difficulty];
692→ 692→ const eType = BEACON_TYPE_META[entry.challenge as BeaconChallengeType];
693→ 693→ const isMine = entry.dateKey === getTodayKey() || entry.dateKey === getWeekKey();
694→ 694→ return (
695→ 695→ <div
696→ 696→ key={`${entry.timestamp}-${i}`}
697→ 697→ className={`flex items-center gap-2 px-2 py-1 rounded-lg text-[11px] ${
698→ 698→ entry.isWeekly
699→ 699→ ? "bg-fuchsia-500/10 border border-fuchsia-500/20"
700→ 700→ : isMine
701→ 701→ ? "bg-fuchsia-500/10 border border-fuchsia-500/20"
702→ 702→ : "hover:bg-white/5"
703→ 703→ }`}
704→ 704→ >
705→ 705→ {/* 排名 */}
706→ 706→ <span className="w-6 flex items-center justify-center">
707→ 707→ {rb ? (
708→ 708→ <rb.icon className="h-3.5 w-3.5" style={{ color: rb.color }} />
709→ 709→ ) : (
710→ 710→ <span className="text-muted-foreground/50 font-mono">{i + 1}</span>
711→ 711→ )}
712→ 712→ </span>
713→ 713→ {/* 类型 + 难度 + 周挑战标记 */}
714→ 714→ <span className="flex items-center gap-1 flex-1 min-w-0">
715→ 715→ <span style={{ color: eDiff.color }} className="font-mono">{eDiff.icon}</span>
716→ 716→ <span className="text-muted-foreground/80 truncate">{eType.label}</span>
717→ 717→ {entry.isWeekly && (
718→ 718→ <span className="text-[9px] font-mono px-1 rounded bg-fuchsia-500/20 text-fuchsia-300 border border-fuchsia-500/40">
719→ 719→ WEEK
720→ 720→ </span>
721→ 721→ )}
722→ 722→ {entry.progress >= 1 && <span className="text-emerald-400/70">✓</span>}
723→ 723→ </span>
724→ 724→ {/* 用时 */}
725→ 725→ <span className="text-muted-foreground/50 font-mono text-[10px] w-16 text-right">
726→ 726→ {Math.floor(entry.durationSec / 60)}m{entry.durationSec % 60}s
727→ 727→ </span>
728→ 728→ {/* 分数 */}
729→ 729→ <span className="font-mono font-semibold w-12 text-right" style={{ color: eDiff.color }}>
730→ 730→ {formatNum(entry.score)}
731→ 731→ </span>
732→ 732→ </div>
733→ 733→ );
734→ 734→ })}
735→ 735→ </div>
736→ 736→ )}
737→ 737→ </div>
738→ 738→
739→ 739→ {/* 难度图例 */}
740→ 740→ <div className="flex items-center justify-center gap-3 text-[9px] text-muted-foreground/50">
741→ 741→ {DIFFICULTY_ORDER.map((d) => {
742→ 742→ const m = BEACON_DIFFICULTY[d];
743→ 743→ return (
744→ 744→ <span key={d} className="flex items-center gap-1">
745→ 745→ <span style={{ color: m.color }}>{m.icon}</span>
746→ 746→ {m.label} ×{m.mult}
747→ 747→ </span>
748→ 748→ );
749→ 749→ })}
750→ 750→ </div>
751→ 751→ </div>
752→ 752→ );
753→ 753→}
754→ 754→
@@ -1,589 +0,0 @@
1→# 回响星核 / Echo Nexus — 开发工作日志
2→
3→> 持续性任务的工作交接文档。每个阶段更新,避免进度丢失。
4→
5→---
6→
7→## 一、项目当前状态描述 / 判断
8→
9→### 概况
10→- **项目**:回响星核 / Echo Nexus — 深空考古放置策略游戏
11→- **一句话**:自治无人机采矿 → 解码记忆晶体(原创共振谜题)→ 拼凑碎片叙事 → 飞升多周目。差异化:解码谜题驱动叙事涌现,全息晶体 + 深空粒子美学。
12→- **当前版本****v0.7**CrystalOrb Canvas 粒子系统 + 角色属性系统)
13→- **在线游玩**https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/
14→- **仓库**https://git.atdunbg.xyz/Super_Z/echo-nexus
15→- **技术栈**Next.js 16 (App Router) + TypeScript + Tailwind + shadcn/ui + Canvas 2D + Zustand + Web Audio API
16→- **定时任务**:每 15 分钟一次 `webDevReview``fixed_rate` + `"900"` 秒,priority=10job_id 228266)。正常完成不会被删除,无需自持续机制。
17→
18→### 状态判断
19→- dev 服务器运行正常(HTTP 200,编译 < 250ms
20→- v0.7 两大新功能已完成:CrystalOrb Canvas 粒子系统升级(VLM 8/10+ 角色属性系统(VLM 7/10
21→- 系统总体稳定,无阻塞性 bug,可继续推进新功能
22→
23→### 已完成版本里程碑(精简)
24→| 版本 | 核心内容 |
25→|------|---------|
26→| v0.1 | MVP:放置采矿 + 解码共振谜题 + 技术树 + 记忆图谱 + 飞升 + 统计 |
27→| v0.1.1 | 解码可解性修复(路径构造法生成器,100% 可解率)|
28→| v0.2 | 遗迹探险肉鸽系统(能量/探险力/6 节点/程序化路径/BOSS)|
29→| v0.2.1 | 程序化音频(15 种音效)+ 14 项成就 + 视觉打磨 |
30→| v0.3 | 星潮事件系统(6 种星潮 + 全屏叠层 + 程序化音效)|
31→| v0.3.1 | 星图天文台元进程(6 类 × 3 = 18 天赋 + 飞升后 3 选 1 draft + Canvas 星图)|
32→| v0.4 | 回响编年史(5 纪元循环命名 + 模板化叙事 + 时间轴 UI + 6 项新成就 + 飞升 BUG 修复)|
33→| v0.5 | 深空信标(每日挑战 + 本地排行榜 Top20 + 7 标签页 + 编年史历史 BUG 修复)|
34→| v0.5.1 | 静态部署 Gitea Pages + 新手教程系统(7 步聚光灯引导)+ UI 间距优化 |
35→| v0.5.2 | 离线收益报告弹窗 + 标签页 UI 重设计 + persist rehydrate 竞态修复 |
36→| v0.6 | 深空巡航 Canvas 2D 实时玩法 + UI 偏移重叠修复 |
37→| **v0.7** | **CrystalOrb Canvas 粒子系统升级 + 角色属性系统(四维)** |
38→
39→### 核心系统清单(8 大系统)
40→1. **放置采矿** — 中央晶体球 + 主动脉冲 + 连击加成(`CrystalOrb.tsx`
41→2. **谐振解码** — 原创路径谜题,路径构造法 100% 可解(`DecodeArray.tsx` + `engine.ts`
42→3. **技术树** — 三层科技解锁,提升产能/解码/探险(`TechTree.tsx`
43→4. **遗迹探险** — 肉鸽系统,6 种节点 + 程序化路径 + BOSS`ExpeditionPanel.tsx` + `expedition.ts`
44→5. **星潮事件** — 6 种动态事件,为放置循环注入变化(`StarTide.tsx` + `starTide.ts`
45→6. **星图天文台** — 飞升后 3 选 1 天赋 draft,跨周目永久生效,18 个天赋(`ConstellationPanel.tsx` + `constellation.ts`
46→7. **回响编年史** — 跨周目叙事时间轴,5 纪元循环命名(`ChronicleDialog.tsx` + `chronicle.ts`
47→8. **深空信标** — 每日挑战 + 本地排行榜 Top20`BeaconPanel.tsx` + `beacon.ts`
48→9. **深空巡航** — Canvas 2D 实时玩法,飞船操控/陨石躲避/星门通关(`CruiseMode.tsx` + `cruise.ts`)【v0.6】
49→10. **角色属性系统** — 四维属性(探索力/智慧/勇气/灵感),飞升获得属性点,影响所有系统(`AttributesPanel.tsx` + `attributes.ts`)【v0.7 新增】
50→
51→---
52→
53→## 二、当前目标 / 已完成的修改 / 验证结果
54→
55→### v0.7 CrystalOrb Canvas 粒子系统升级(已完成)
56→**动机**:P0 优先级 — 将中央晶体球从 CSS 动画升级为 Canvas 粒子视觉盛宴,提升视觉震撼。
57→
58→**重写文件**`src/components/game/CrystalOrb.tsx`
59→- 从纯 CSS 动画升级为 Canvas 2D 粒子系统
60→- **多层粒子**
61→ - 环绕能量粒子(3层14/10/8个,emerald/fuchsia/rose三色,不同速度方向环绕晶核,带拖尾)
62→ - 环境星尘(40个,缓慢漂移 + 闪烁)
63→ - 点击爆发粒子(10-30个径向发散,颜色随连击等级变色)
64→ - 冲击波环(点击触发1-3层错峰扩散,连击≥3/5解锁更多层)
65→- **晶核升级**:径向渐变辉光 + 3层六边形纹理旋转 + 高光 + 鼠标视差偏移
66→- **呼吸动画**:晶核 + 外层辉光随时间呼吸(sin波)
67→- **进度环**SVG渐变环(emerald→fuchsia→rose)保留
68→- **交互保留**:点击脉冲、连击Toast、浮动数字、音效、`data-tut="crystal-orb"` 锚点
69→- **性能**DPR cap 2ResizeObserver 自适应,requestAnimationFrame 60fps
70→
71→**QA 验证**agent-browser + VLM):
72→- VLM 视觉评分 **8/10**:晶体球清晰可见,多层辉光,环绕粒子动态自然,无渲染问题
73→- 点击交互:冲击波环 ✅ + 爆发粒子 ✅,评分 7/10
74→- lint 零错误;HTTP 200
75→
76→### v0.7 角色属性系统(已完成)
77→**动机**:P1 优先级 — 增加RPG深度,四维属性影响所有游戏系统。
78→
79→**新增文件**
80→- `src/lib/game/attributes.ts`~330行逻辑层):CharacterAttributes/AttributeProgress 类型、ATTRIBUTE_CONFIG 元信息、getAttributeBonus 两区加成公式(0-50线性+0.5%/点,50-100递减+0.2%/点)、getAllBonuses 12项修饰器、levelUpCheck 自动升级、computePrestigeAttrPoints、migrateAttributes 旧存档兼容
81→- `src/components/game/AttributesPanel.tsx`~330行 UI):四维卡片网格(小屏2×2/大屏1×4)、图标+中英文名+Lv.badge+数值/100+加成%+经验条+影响列表+分配按钮、顶部待分配点数badge闪烁、底部总览+12项修饰器明细
82→
83→**修改文件**
84→- `types.ts`GameState 新增 attributes/attributeProgress/pendingAttrPoints
85→- `config.ts`INITIAL_STATE 补全默认值
86→- `engine.ts`recomputeStats 聚合属性加成;performPrestige 发放属性点(ascensions×2+1
87→- `gameStore.ts`:新增 allocateAttribute/gainAttributeExp actionspulse/clickNode/autoDecodeTick/resolveCurrentNode/grantCruiseReward/tickTide 全部接入属性经验;init() 调用 migrateAttributes
88→- `achievements.ts`:新增 ach_attr_total_50 + ach_attr_max_100 两项成就
89→- `page.tsx`grid-cols-7→8,新增「角色」标签页(User图标),红点提示,StatsPanel新增5行属性行,版本号 v0.6→v0.7
90→
91→**四维属性设计**
92→- 探索力(emerald):探险力+X%/巡航飞船速度+X%
93→- 智慧(fuchsia):解码步数+X/洞见+X%/自动解码周期-X%
94→- 勇气(amber):探险生命+X/BOSS胜率+X%/巡航护盾+X
95→- 灵感(rose):接触率+X/星潮概率+X/脉冲连击+X%
96→
97→**QA 验证**agent-browser + VLM):
98→- VLM 视觉评分 **7/10**(面板)/ **8/10**(待分配状态):四维卡片可见、数值/等级/加成/经验条齐全、四色全息
99→- 功能验证:setState pendingAttrPoints=5 → 显示5点待分配 → 点击分配按钮 → exploration 0→1, pending 5→4 ✅
100→- lint 零错误;HTTP 200
101→
102→---
103→
104→### v0.6 深空巡航 Canvas 2D 实时玩法(历史记录)
105→**动机**:回应用户反馈「玩法太单调(全是点点点)」+「视觉震撼不足」——首次引入实时操作玩法。
106→
107→**新增文件**
108→- `src/lib/game/cruise.ts`~520 行逻辑层)
109→ - 类型系统:CruiseEntity 联合类型(ship/asteroid/storm/crystal/insight/beacon/stargate/particle 共 8 种)
110→ - mulberry32 + FNV-1a 种子化 RNG`cruiseSeed(level)`
111→ - `generateLevel`:程序化生成陨石带(8+lvl×2)、虚空风暴(1+lvl/2)、晶体碎片(5+lvl)、洞见光球(2+lvl/3)、信标(1+lvl/4)、星门
112→ - `updateCruise`:物理推进 + 碰撞检测 + 粒子系统 + 震动 + 600ms 无敌帧
113→ - `computeRewards`crystals=碎片×(8+lvl×2)、insights=洞见×(2+lvl×0.5)、contact=信标×(1.5+lvl×0.3);通关×1.5、失败×0.5
114→ - 独立 localStorage `echo-nexus-cruise-v1`:最高分/累计通关/最高关卡/累计奖励/最近 20 局
115→
116→- `src/components/game/CruiseMode.tsx`~830 行渲染层)
117→ - 全屏 fixed inset-0 z-50 CanvasDPR cap 2resize 监听
118→ - 3 层视差星空:远(100星)/中(60星)/近(30星),独立漂移 + 飞船速度视差 + 闪烁
119→ - 8 种实体全部 ctx.shadowBlur 辉光绘制
120→ - 粒子系统:尾焰/收集/碰撞/烟花,上限 200
121→ - 屏幕震动 280ms 衰减;低护盾(<30%)红色边框脉冲
122→ - HUDHTML 叠层,glass+backdrop-blur80ms 节流):护盾/能量/分数/用时/收集计数
123→ - 控制:桌面 WASD/方向键 8 方向 + 移动端虚拟摇杆 + P 暂停 + Esc 退出 + 失焦自动暂停
124→ - 阶段:ready → playing → won/lost(结算明细 + 奖励 + 下一关/重试/返回)
125→
126→**修改文件**
127→- `src/app/page.tsx`header 新增 amber 主题「巡航」按钮(Navigation 图标)+ cruiseOpen state + 条件渲染 CruiseMode
128→- `src/store/gameStore.ts`:新增 `grantCruiseReward({crystals?, insights?, contact?})` actioncrystals 受 crystalCap 限制,contact 受 100 上限)
129→- 版本号 v0.5.2 → v0.6
130→
131→**UI 偏移/重叠 BUG 修复**(3 处):
132→- `DecodeArray.tsx`:移除 className 冲突的 `-translate-x-1/2 -translate-y-1/2`,统一用 inline transform 定位+缩放
133→- `AchievementsPanel.tsx`:移除内层 `max-h-[300px]`,改 `flex-1 min-h-0` 自适应父容器
134→- `page.tsx`:调整三个 section 的 min-h,避免小屏单列时总高度超过手机视口
135→
136→**色彩规范**:严格遵循 emerald(#34d399) / rose(#fb7185) / amber(#fbbf24) / fuchsia(#e879f9) 四色全息色谱,零蓝色/靛色
137→
138→**QA 验证**agent-browser + VLM):
139→- lint 零错误;dev 服务器 HTTP 200;编译 < 250ms
140→- VLM 确认主界面:巡航按钮存在、晶体球居中、解码区正常、无重叠偏移
141→- VLM 确认巡航玩法:飞船可见(绿色三角形)、HUD 完整、陨石+星空背景、粒子特效(黄色尾焰+紫色辉光)
142→- WASD 控制飞船移动,按 W 直冲星门 → 通关,奖励 +30 晶体(2×10×1.5=30 计算正确)
143→- localStorage 正确记录 highScore/totalRuns/totalWins/bestLevel
144→- 奖励同步 gameStoregrantCruiseReward,满仓时 cap 逻辑正确)
145→- 退出返回主界面,7 标签页 + 晶体球 + 解码面板均完好
146→
147→---
148→
149→## 三、未解决问题或风险 / 下一阶段优先事项
150→
151→### 已知问题 / 风险
152→1. **dev 服务器 Turbopack 缓存偶发损坏**`Unable to open static sorted file` + 进程被杀,需 `rm -rf .next` 重启。静态导出构建稳定可靠。
153→2. **定时任务调度类型**:必须用 `fixed_rate` + `"900"` 秒(15分钟),不要用 cron 表达式。`webDevReview` 类型任务正常完成不会被删除,只有抛出未捕获异常才会被标记失败。
154→3. **v0.3 星潮为单机版**:原计划 socket 全局事件,后续可扩展为多人同步。
155→4. **v0.5 信标排行榜为本地版**(纯 localStorage):升级云排行榜需后端 API。
156→5. **v0.5 每日挑战仅 1 个/天**:可加入"周挑战"或"信标链"(连续完成 N 天奖励)。
157→6. **探险 BOSS 战胜率较低**:基础探索力 10 vs BOSS 难度 5-7,胜率约 25%,需玩家投资探险技术。
158→7. **探险能量恢复较慢**:45s/点,可加技术提升恢复速度。
159→8. **编年史上限 50 条**:超过自动丢弃最早的;v0.4 之前飞升无回填(空状态有提示)。
160→9. **5 纪元叙事为模板化生成**:v0.5+ 将加入手写剧情节点。
161→10. ~~**CrystalOrb 仍为 CSS 动画**~~ → ✅ **v0.7 已完成 Canvas 粒子系统升级**。
162→
163→### 下一阶段优先级(v0.7 后修正版)
164→> 工单修正:原 P0 CrystalOrb Canvas + P1 角色属性两项均已在 v0.7 完成,现重新排序。
165→
166→1. **🔴 P0 — 巡航玩法大增强(v0.8 主线)**:BOSS 战关卡(每 5 关一个 BOSS,三阶段攻击模式 + 飞船射击系统)/ 道具掉落(5 种 powerup:护盾/能量/急速/超载/磁吸)/ 事件选择节点(每关通关后 3 选 1 强化牌,10 种强化)。这是当前最高优先级,能显著提升巡航玩法的深度与可玩性。
167→2. **🟠 P1 — 探险 BOSS 战胜率平衡 + 探险能量恢复**:修复已知问题 6/7(探险 BOSS 胜率 25% 偏低,能量 45s/点 偏慢),提升探险体验。
168→3. **🟠 P1 — 信标系统扩展**:周挑战 / 信标链(连续完成 N 天奖励),修复已知问题 5。
169→4. **🟡 P2 — UI 样式细节打磨**:标签页图标/文字比例、视觉层次强化、移动端响应式进一步优化、CrystalOrb 周边留白优化(VLM 指出主界面左侧有空白)。
170→5. **🟡 P2 — 云存档 + 云排行榜**:v0.5+ 后端 API(需评估是否引入 mini-service)。
171→6. **🟢 P3 — 全 5 纪元手写叙事节点**:替换模板化 lore。
172→7. **🟢 P3 — socket 多人同步星潮**:全局事件共享。
173→
174→### 定时任务(fixed_rate · 900s
175→- **当前 job_id**: **228357**2026-06-24 13:25 重建)
176→- **配置**: `fixed_rate` + `"900"`15分钟间隔),`priority=10`(高优先级),`payload.kind=webDevReview``tz=Asia/Shanghai`
177→- **任务名**: Echo Nexus - 任务审查与持续迭代
178→- **任务描述强制要求**:每次触发 MUST 先完整阅读 worklog.md 工单,以工单为准,不凭记忆工作
179→- **正常完成不会被删除**,只有未捕获异常会标记失败。若任务列表为空,需用 cron create 重建(schedule.kind=fixed_rate, expr="900", priority=10, payload.kind=webDevReview)。
180→- **job_id 历史**: 227909(失效) → 228130(失效) → 228172(失效) → 228186(失效) → 228218(删除重建) → 228236(cron版,删除重建) → 228266(fixed_rate,失效) → 228329(失效) → **228357(当前,重建)**
181→
182→---
183→
184→## 定时任务描述模板(webDevReview · fixed_rate
185→
186→> 重建参数:schedule.kind=`fixed_rate`, schedule.expr=`"900"`, params.priority=`10`, payload.kind=`webDevReview`
187→
188→```
189→请先判断当前项目状态,使用 agent-browser 进行必要测试和 QA,然后自主选择本轮最合适的工作重点并继续开发。
190→
191→要求:
192→1. 开始任务时查看 /home/z/my-project/worklog.md 了解实际进展,拆解详细todo
193→2. 使用 agent-browser 进行测试和QA,有bug优先修复
194→3. 如果稳定,自主提出新需求继续推进
195→4. 样式要越做细节越多!!
196→5. 功能要越做越多!!
197→6. 工作告一段落更新 /home/z/my-project/worklog.md
198→7. 项目结束后一定要把工作记录写在 worklog.md 里
199→
200→交接文档建议包含三部分:
201→- 项目当前状态描述/判断
202→- 当前目标/已完成的修改/验证结果
203→- 未解决问题或风险,建议下一阶段优先事项
204→```
205→
206→---
207→
208→## 历史详细版本记录(按需查阅 docs/repo/docs/
209→
210→- `04-解码系统修复-v0.1.1.md`
211→- `05-遗迹探险系统-v0.2.md`
212→- `06-音频与成就系统-v0.2.1.md`
213→- `07-星潮事件系统-v0.3.md`
214→- `08-星图天文台系统-v0.3.1.md`
215→- `09-回响编年史系统-v0.4.md`
216→- `10-深空信标系统-v0.5.md`
217→
218→---
219→
220→Task ID: 7
221→Agent: full-stack-developer
222→Task: 角色属性系统(探索力/智慧/勇气/灵感)
223→
224→Work Log:
225→- 阅读 worklog.md 了解 v0.6 项目背景,确认 8 大系统架构与四色全息色彩规范
226→- 新建 `src/lib/game/attributes.ts` 逻辑层(~330 行):
227→ - 定义 CharacterAttributes/AttributeProgress/AttributeKey 类型 + ATTRIBUTE_CONFIG 元信息
228→ - getAttributeBonus 线性+递减两区加成公式(0-50 每 10 点 +5%50-100 每点 +2%
229→ - getAllBonuses 返回 12 个修饰器(探险力/巡航速度/解码步数/洞见/自动解码周期/探险生命/BOSS 胜率/巡航护盾/接触率/星潮触发/脉冲连击/产能)
230→ - levelUpCheck 自动跨多级 + expRequiredForLevel + computePrestigeAttrPoints + migrateAttributes 兼容
231→- 扩展 `src/lib/game/types.ts` GameState:新增 attributes/attributeProgress/pendingAttrPoints
232→- 扩展 `src/lib/game/config.ts` INITIAL_STATE:补全默认值(0 数值 + 空 progress
233→- 扩展 `src/lib/game/engine.ts`
234→ - recomputeStats 末尾聚合 am.crystalsPerSecMult/insightMultAdd/contactRateMult/decodeStepsBonus
235→ - performPrestige 保留 attributes 数值,清空 attributeProgress,发放 ascensions × 2 + 1 点属性点
236→ - createInitialState 每次返回全新对象避免引用共享
237→- 扩展 `src/store/gameStore.ts`(核心改造,10+ 处 syncStats 调用全部补 attributes 参数):
238→ - pulse:连击 ≥3 给灵感经验 + 脉冲威力乘灵感连击加成
239→ - clickNode 完成:给智慧经验(tier × 2)
240→ - autoDecodeTick:智慧经验 +1 + 自动解码周期受 am.autoDecodeIntervalMult 影响
241→ - startExpedition:探险力乘 am.expeditionPowerMult,生命加 am.expeditionHpBonus
242→ - resolveCurrentNodeBOSS 节点包装 RNG 提升 +am.bossWinRateBonus 胜率;战斗胜利给勇气+探索力经验
243→ - grantCruiseReward:按总奖励量给探索力+勇气经验
244→ - tickTide:灵感 am.tideTriggerBonus 缩短星潮间隙(上限 30%)
245→ - init() 调用 migrateAttributes 补全旧存档
246→ - 新增 allocateAttribute / gainAttributeExp 两个 action
247→- 新建 `src/components/game/AttributesPanel.tsx` UI 面板(~330 行):
248→ - 四维卡片网格(小屏 2×2,大屏 1×4)
249→ - 每张卡片:图标 + 中英文名 + Lv.badge + 数值/100 + 加成% + 经验条 + 影响列表 + 「+分配」按钮
250→ - 顶部待分配点数 badgependingAttrPoints > 0 时 echo-pending-pulse 闪烁动画)
251→ - 底部总等级/总加成概览 + 12 个修饰器明细
252→ - 4 色辉光边框(emerald/fuchsia/amber/rose),顶角光晕装饰
253→- 接入 `src/app/page.tsx`
254→ - grid-cols-7 → grid-cols-8,新增第 8 个「角色」TabsTriggerUser 图标,渐变主题)
255→ - pendingAttrPoints > 0 时显示 rose 红点提示
256→ - 渲染 AttributesPanel
257→ - StatsPanel 新增 5 行属性行(探索力/智慧/勇气/灵感/待分配点)
258→ - 版本号 v0.6 → v0.7
259→- 新增 `src/lib/game/achievements.ts` 2 项成就:
260→ - ach_attr_total_50(四维觉醒):总属性 ≥ 50 → 产能+6%/洞见+6%
261→ - ach_attr_max_100(维度精通):任一属性 100 → 产能+12%/洞见+10%
262→- 写入 `agent-ctx/7-full-stack-developer.md` 工作记录
263→- 最终验证:lint 零错误 + dev 服务器 HTTP 200 + 编译 < 250ms
264→
265→Stage Summary:
266→- ✅ 角色属性系统 v0.7 P1 全部完成
267→- ✅ 新增 2 文件(attributes.ts + AttributesPanel.tsx),修改 6 文件(types/config/engine/gameStore/achievements/page
268→- ✅ 四维属性完整接入游戏各系统(探险/解码/脉冲/巡航/星潮)
269→- ✅ 飞升属性点公式 = ascensions × 2 + 1;经验累积自动升级(每级 10×level)
270→- ✅ 旧存档兼容(migrateAttributes 补全字段,同步 level 与 attributes
271→- ✅ lint 零错误 + HTTP 200 + 编译 < 250ms
272→- ✅ 严格四色全息(emerald/fuchsia/amber/rose),零蓝色/靛色
273→- ✅ 8 标签页 + 巡航按钮全部保留,data-tut 锚点未破坏
274→- ✅ 待分配点数时标签页 rose 红点提示 + 顶部 badge 闪烁
275→
276→---
277→Task ID: 8-deploy
278→Agent: main
279→Task: 重建定时任务 + 更新 README + 部署 gh-pagesv0.7
280→
281→Work Log:
282→- 检查项目状态:CrystalOrb.tsx 已是 v0.7 Canvas 粒子系统(659 行),无活动 cron job
283→- 重建定时任务:cron create fixed_rate="900" priority=10 webDevReview → job_id 228329tz=Asia/Shanghai
284→- 重写 README.md
285→ - 顶部显著位置添加「在线游玩」链接(https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/
286→ - 版本号 v0.1 → v0.7
287→ - 新增 10 大系统表格(含 v0.6 巡航 + v0.7 角色属性)
288→ - 完整开发路线表(v0.1-v1.0,标记 ✅/🔜 状态)
289→ - 新增本地开发命令 + Gitea Pages 部署指南
290→ - 文档索引扩展至 10 个版本文档
291→- 静态导出:BUILD_EXPORT=true bun run build:static → out/ 1.6MHTTP 200 编译 4.3s
292→- 推送 gh-pages 分支:force push origin gh-pagescommit 8727fed "deploy: v0.7"
293→- 提交 mainREADME 更新(commit 43dfc2d "docs: README 添加在线游玩链接 + v0.7 状态更新"
294→- 验证 Gitea Pageshttps://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/ → HTTP 200 ✅
295→- QA 验证:agent-browser 打开 http://localhost:3000/ → VLM 评分 8/10(主界面 polishedminor 空白)
296→
297→Stage Summary:
298→- ✅ 定时任务重建成功(job_id 228329,每 15 分钟自动审查迭代)
299→- ✅ README 重写完成,网站链接显著展示,v0.7 状态完整
300→- ✅ gh-pages 部署成功,Gitea Pages HTTP 200 可访问
301→- ✅ main 分支已同步推送(README + worklog 更新)
302→- ✅ dev 服务器健康(HTTP 200,编译 < 250ms
303→- ✅ CrystalOrb v0.7 Canvas 粒子系统已确认完整(659 行)
304→- 下一步:v0.8 巡航玩法增强(BOSS 战 / 道具掉落 / 事件选择节点)
305→
306→---
307→Task ID: 8
308→Agent: main
309→Task: v0.8 巡航玩法大增强(BOSS 战 + 道具掉落 + 事件选择节点)+ 工单修正 + 审查流程重建
310→
311→Work Log:
312→- 重读 worklog 工单,发现 v0.7 两项(CrystalOrb Canvas + 角色属性)已完成但工单"下一阶段优先级"未更新
313→- 修正 worklog 工单:标注 P0/P1 v0.7 已完成,重新排序下一阶段优先级(P0=巡航增强, P1=探险平衡/信标扩展, P2=UI打磨/云存档, P3=叙事/多人)
314→- 重建审查流程 cron jobfixed_rate=900s, priority=10, webDevReview, tz=Asia/Shanghai, job_id=228357
315→ - 任务描述强制要求:每次触发 MUST 先完整阅读 worklog.md 工单,以工单为准,不凭记忆工作
316→- 发现之前被中断的 v0.8 subagent 已写入 2037 行代码(cruise.ts +1487 / CruiseMode.tsx +858 / page.tsx +2
317→- 修复 2 个 lint 错误(react-hooks/set-state-in-effect):
318→ - isTouch: useState(false) + useEffect(setIsTouch) → useState initializer
319→ - stats: useState(null) + useEffect(setStats(loadCruiseStats)) → useState initializer
320→- QA 验证(agent-browser + VLM):
321→ - 主界面 VLM 7/10v0.8 版本号 ✅ + 巡航按钮 ✅
322→ - 巡航 READY 阶段 VLM 8/10BOSS 战说明 ✅(每5关BOSS战/击败BOSS)+ 射击控制说明 ✅(空格/右下角射击按钮)
323→ - 巡航第1关游戏 VLM 8/10:飞船(绿色三角)✅ + HUD(护盾/能量/分数/计时)✅ + 陨石/收集物/能量场 ✅ + 无渲染错误
324→- 提交 v0.8commit 494bc5f+ 推送 main
325→- 静态导出 BUILD_EXPORT=true → out/ 1.6M(编译 4.1s
326→- 部署 gh-pagesforce push, commit b865922)→ Gitea Pages HTTP 200 ✅
327→
328→Stage Summary:
329→- ✅ 工单修正完成:v0.7 已完成项标注清楚,下一阶段优先级重新排序
330→- ✅ 审查流程重建:cron job_id 228357,每15分钟触发,强制读工单
331→- ✅ v0.8 巡航玩法大增强全部完成并部署:
332→ - BOSS 战系统(每5关,三阶段攻击,HP条,击败奖励×3)
333→ - 飞船射击系统(空格/射击按钮,能量消耗,子弹尾焰)
334→ - 道具掉落(5种powerup:护盾/能量/急速/超载/磁吸)
335→ - 事件选择节点(每关通关后3选1,10种强化牌)
336→ - lint 零错误 + dev HTTP 200 + gh-pages HTTP 200
337→- ✅ VLM 评分:主界面 7/10 + 巡航READY 8/10 + 巡航游戏 8/10
338→- 在线游玩 v0.8https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/
339→- 下一阶段工单:P1 探险BOSS胜率平衡 + 探险能量恢复 / 信标系统扩展(周挑战/信标链)
340→
341→---
342→Task ID: 9-b
343→Agent: full-stack-developer
344→Task: 信标系统扩展(周挑战 + 信标链连续奖励)
345→
346→Work Log:
347→- 阅读现有 `src/lib/game/beacon.ts`v0.5 每日挑战 + 本地排行榜,358 行)+ `src/components/game/BeaconPanel.tsx` + `src/store/gameStore.ts` 的 `trackBeacon` 函数(约第 173 行)+ worklog v0.8 项目状态,确认四色全息规范与现有架构。
348→- **扩展 `src/lib/game/beacon.ts`358 → 873 行,新增 ~515 行)**
349→ - **周挑战(WEEKLY CHALLENGE**
350→ - `BeaconWeeklyChallenge` 接口(weekKey YYYY-Www / type / difficulty 强制 anomaly|singular / goal / rewardInsight / rewardContact / seed / title / desc
351→ - `BeaconWeeklyProgress` 接口(weekKey / progress / startedAt / completedAt / claimed / durationSec
352→ - `getWeekKey(now)`ISO 8601 周键(周一为起点,含首个周四的周为第一周)
353→ - `weekKeyToSeed`FNV-1a 哈希
354→ - `generateWeeklyChallenge(now)`:基于 weekKey 种子确定性生成,难度加权 anomaly 60% / singular 40%goal 为日基准 ×3-5 倍(decode 18-75 / expedition 3-15 / pulse 60-245 / boss 3-12 / insight 120-595
355→ - `loadWeeklyProgress` / `saveWeeklyProgress` / `addWeeklyProgress` / `claimWeeklyReward`(推送排行榜,entry 加 `isWeekly: true`
356→ - `msUntilNextWeek(now)`:距离下周一 UTC 0 点的毫秒数
357→ - `BEACON_WEEKLY_KEY = "echo-nexus-beacon-weekly-v1"`
358→ - **信标链(BEACON CHAIN**
359→ - `BeaconChainState` 接口(lastCompletedDateKey / currentStreak / longestStreak / totalCompletions / graceUsed / milestonesClaimed
360→ - `BEACON_CHAIN_KEY = "echo-nexus-beacon-chain-v1"`
361→ - `BEACON_CHAIN_MILESTONES = [3, 7, 14, 30]` 常量
362→ - `BEACON_CHAIN_REWARDS`4 个里程碑(三日谐振 +50/+5 / 七日回响 +120/+12 / 半月星潮 +280/+28 / 满月飞升 +680/+68
363→ - `loadChainState` / `saveChainState`(每次返回新对象避免引用共享)
364→ - `recordChainCompletion(dateKey)` 核心逻辑:同日重复忽略;次日 streak++;隔一天 miss 且 graceUsed<1 时续命 streak++ graceUsed++;其他断链 streak=1 graceUsed=0;返回 `{ state, newMilestones }`
365→ - `claimChainMilestone(milestone)` / `getNextMilestone(streak)` / `getChainProgress(streak)`(返回 current/next/prev/progressPct 用于 UI
366→ - `dateKeyToTimestamp` / `dateKeyDiffDays` 工具函数
367→ - `BeaconScoreEntry` 新增可选 `isWeekly?: boolean` 字段
368→- **扩展 `src/store/gameStore.ts`**
369→ - import 扩展:generateWeeklyChallenge / loadWeeklyProgress / loadChainState / addWeeklyProgress / recordChainCompletion / claimWeeklyReward / claimChainMilestone / getTodayKey + 类型
370→ - `trackBeacon(type, delta)` 函数签名从 `boolean` 改为 `{ dailyJustCompleted, weeklyJustCompleted, newChainMilestones }`
371→ - 日挑战类型匹配且未完成 → addBeaconProgress;若刚完成 → recordChainCompletion(getTodayKey()) 收集 newMilestones
372→ - 周挑战类型匹配且未完成 → addWeeklyProgress
373→ - 所有调用点(pulse/clickNode/autoDecodeTick/resolveCurrentNode 等 7 处)原本就忽略返回值,向后兼容
374→ - 新增 action `claimWeeklyBeacon`:调用 claimWeeklyReward → 发放 insights + contact 到 state,返回 `{ rewardInsight, rewardContact, score }`
375→ - 新增 action `claimChainReward(milestone)`:前置校验 currentStreak 与 milestonesClaimed,调用 claimChainMilestone → 发放奖励,返回 `{ rewardInsight, rewardContact, label, ok }`
376→ - GameActions 接口同步新增两个 action 签名
377→- **重写 `src/components/game/BeaconPanel.tsx`301 → 638 行)**
378→ - 顶部头部 + 每日挑战卡片(保留 v0.5 完整功能 + 按钮主题色随难度色淡显)
379→ - **新增周挑战区块**fuchsia 主题):标题"周挑战 · WEEKLY" + weekKey + 倒计时 + 挑战卡片(类型/难度标签/标题/描述/进度条/奖励/领取按钮 emerald 主题)+ weekly-glow 动画
380→ - **新增信标链区块**amber→rose 渐变):标题"信标链 · CHAIN" + 大字号 streakchain-streak-text 渐变流动动画)+ 今日完成状态徽章 + 4 个里程碑节点(w-12 h-12 圆形,claimed=emerald/✓,reachable=rose 脉冲动画+领取按钮,inProgress=amber 半亮,未到达=muted 灰)+ 节点间连线(背景灰 + 已达成部分 amber→rose 渐变 + 辉光,基于 prev→next 插值定位)+ 进度条 + 底部统计(最长/累计/续命状态)
381→ - 桌面端 lg:grid-cols-2 让周挑战+信标链并排,移动端单列
382→ - 排行榜条目区分日/周:周挑战 entry 显示 "WEEK" 徽章 + fuchsia 高亮背景
383→ - 空状态提示扩展为"完成今日或本周信标即可登榜"
384→ - 4 个新动画:weekly-glow / chain-milestone-pulse / chain-streak-flux(背景渐变流动)
385→- **QA 验证**
386→ - `bun run lint` 零错误(每次修改后均验证)
387→ - dev.log 全程无错误,所有编译 < 300ms
388→ - **信标链逻辑测试**(bun 直接运行 TS,5 个场景全 PASS):
389→ 1. 昨日 streak=1 → 今日完成 → streak=2 ✅
390→ 2. 同日重复完成 → 忽略 ✅
391→ 3. 明日完成 → streak=3,无需 grace ✅
392→ 4. 隔一天 misslastCompleted + 2 天)→ streak=4graceUsed=1 ✅
393→ 5. 再次 missgrace 已用)→ 断链 streak=1graceUsed=0 ✅
394→ - **UI 集成测试**agent-browser):
395→ - localStorage 模拟设置 chain statelastCompletedDateKey=yesterday, currentStreak=1)→ reload → UI 正确显示 streak=1
396→ - 设置 streak=3 + milestonesClaimed=[] → milestone 3 节点显示 rose 脉冲动画 + "领取"按钮
397→ - 点击"领取"→ milestonesClaimed=[3] + Toast "✦ 三日谐振 已领取 +50 洞见 · +5.0 接触" + 按钮消失 ✅
398→ - **VLM 视觉评分**(目标 ≥7/10):
399→ - 首屏截图:8/10(四色一致、布局合理、信标链清晰)
400→ - 里程碑可领取状态:8/10(amber→rose 渐变醒目、里程碑层次分明、fuchsia 主题清晰)
401→ - 重置后干净状态:8/10(WEEKLY 字体对比度可优化,但整体可读性强)
402→ - **不破坏现有功能**:日挑战进度/领奖/排行榜流程全部保留并工作正常
403→
404→Stage Summary:
405→- ✅ beacon.ts 扩展完成(358 → 873 行):周挑战 + 信标链全部函数与类型
406→- ✅ gameStore.ts 集成完成:trackBeacon 升级为对象返回值 + claimWeeklyBeacon + claimChainReward 两个新 action
407→- ✅ BeaconPanel.tsx UI 完整重写(301 → 638 行):周挑战 fuchsia 卡片 + 信标链 amber→rose 渐变区块
408→- ✅ 严格四色全息:周挑战 fuchsia、信标链 amber→rose、领取按钮 emerald、无蓝/紫色违规
409→- ✅ localStorage 新增 2 个独立 keyweekly + chain),不污染旧 key
410→- ✅ 确定性:周挑战同周同种子同结果(weekKey FNV-1a 种子)
411→- ✅ 信标链 grace 续命机制工作正常(5 个场景 bun 测试全 PASS)
412→- ✅ lint 零错误 + dev HTTP 200 + VLM 8/10
413→- ✅ 现有日挑战 + 排行榜 + 领奖流程全部保留,无回归
414→
415→---
416→Task ID: 9
417→Agent: main + subagent(9-b)
418→Task: v0.8.1 P1 双任务 — 探险 BOSS 胜率平衡 + 能量恢复 + 信标系统扩展(周挑战 + 信标链)
419→
420→Work Log:
421→- 重读工单确认 P1 两项为下一阶段优先级
422→- P1-a 探险平衡(main 自己做,平衡需要精细控制):
423→ - expedition.ts: combatDifficultyScale 8→4, combatWinRateFloor 0.25→0.35, BOSS difficulty 5-7→3-5
424→ - 新增 computeEnergyRegenInterval(state): exp_2 -30%, exp_3 -20%, 探索力属性 -最高30%, 下限12s
425→ - computeEnergyRegen 接受 intervalSec 参数
426→ - config.ts: exp_2/exp_3 描述加能量恢复说明
427→ - gameStore.ts tick: 传入动态间隔
428→ - ExpeditionPanel.tsx: 显示实际恢复速度 + '已加速'标记
429→- P1-b 信标系统扩展(dispatch subagent 9-b,并行完成):
430→ - beacon.ts 358→873行: 周挑战(getWeekKey ISO 8601 + 确定性生成) + 信标链(4里程碑+grace续命)
431→ - BeaconPanel.tsx 301→638行: 周挑战fuchsia主题 + 信标链amber→rose里程碑节点
432→ - gameStore.ts: trackBeacon升级 + claimWeeklyBeacon/claimChainReward actions
433→- QA 验证:
434→ - lint 零错误
435→ - 探险面板 VLM 7/10(能量条 + 恢复速度显示正常)
436→ - 信标面板 VLM 8/10(周挑战 WEEKLY + 信标链 3/7/14/30 + 排行榜全部可见)
437→ - subagent 5场景 bun 测试全 PASS(信标链断链/续命逻辑)
438→- 提交 v0.8.1commit 71ca5b4+ 推送 main
439→- 静态导出 + 部署 gh-pagescommit 97eecaa)→ HTTP 200
440→
441→Stage Summary:
442→- ✅ P1-a 探险平衡完成: BOSS 基础胜率 25%→35-45%, 配合技术/属性可达 55-70%
443→- ✅ P1-a 能量恢复: 固定45s → 动态12-45s(技术+属性加速)
444→- ✅ P1-b 周挑战: 每周一UTC刷新, fuchsia主题, goal×3-5倍
445→- ✅ P1-b 信标链: 4里程碑(3/7/14/30天) + grace续命, amber→rose渐变
446→- ✅ 在线游玩 v0.8.1: https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/
447→- 下一阶段工单: P2 UI样式细节打磨 / P2 云存档+云排行榜 / P3 全纪元叙事
448→
449→---
450→Task ID: 10-a
451→Agent: full-stack-developer
452→Task: #5 限时挑战 + #3 星潮类型深化
453→
454→Work Log:
455→- 阅读项目上下文:worklog.mdv0.8.110 大系统)+ beacon.ts873行,日+周+链)+ starTide.ts209行,6种星潮)+ gameStore trackBeacon/tickTide + BeaconPanel.tsx,确认四色全息规范与现有架构
456→- **工单 #5 限时挑战**
457→ - beacon.ts873→1162行)新增限时挑战系统:
458→ - BeaconTimedChallenge/BeaconTimedProgress 接口 + isTimed 字段加入 BeaconScoreEntry
459→ - getTimedSlotKeyUTC 0/4/8/12/16/20 点切分,"timed_YYYY-MM-DD_HH"+ timedSlotKeyToSeedFNV-1a
460→ - generateTimedChallenge(确定性,难度 routinegoal=日基准×0.3-0.55类型公式)
461→ - loadTimedProgress/saveTimedProgress/addTimedProgress/claimTimedReward(排行榜 isTimed
462→ - msUntilNextTimedSlot + TIMED_SLOT_MS=4h + BEACON_TIMED_KEY
463→ - gameStore.tstrackBeacon 返回值新增 timedJustCompleted,三进度同时更新;新增 claimTimedBeacon action
464→ - BeaconPanel.tsx753→944行):日挑战与周挑战之间插入限时挑战区块(amber 主题)
465→ - Hourglass图标+TIMED标题+slotKey+倒计时 + 进度+奖励+领取按钮(emerald)
466→ - 紧急状态:距时段结束<30min 切 rose 脉冲动画+"即将结束"徽章
467→ - 2新动画 timed-glow(amber)/timed-urgent(rose) + 排行榜 TIMED 徽章
468→- **工单 #3 星潮深化**
469→ - starTide.ts209→289行):TideType 新增 surge/eclipse/prism
470→ - surge(emerald ⇈): crystalsPerSec×2.5 + targetLenBonus=2(解码目标序列+2,更长更难)
471→ - eclipse(rose ◐): crystalsPerSec×0.7 + bossWinRateBonus=0.2BOSS胜率+20%
472→ - prism(fuchsia ◬): insightMultAdd=1.0(洞见×2) + autoDecodeIntervalMult=0.7(自动解码-30%)
473→ - TideModifiers 新增 targetLenBonus/bossWinRateBonus/autoDecodeIntervalMult 可选字段
474→ - TIDE_WEIGHTS 新增3种各12(总136+ getTideModifiers 3新case
475→ - decode.tsgeneratePuzzle 新增 targetLenBonus 参数(surge用,targetLen+stepLimit同步增加)
476→ - gameStore.ts 3处修饰器应用:
477→ - startDecodesurge 时传 targetLenBonus → 谜题更长
478→ - autoDecodeTickprism 时 interval×=autoDecodeIntervalMult
479→ - resolveCurrentNodeeclipse 时 bossWinRate 加 tideBossBonus
480→ - achievements.tsach_tides_all 阈值6→9page.tsx StatsPanel 星潮亲历 /9
481→- QA 验证:
482→ - lint 零错误(exit 0+ dev HTTP 200 + 编译<30ms
483→ - BeaconPanel VLM 8.5/10(限时区块可见)+ 最终 9/10
484→ - 三新星潮 localStorage 注入触发截图 VLMsurge 8/10、prism 7/10、eclipse 8/10
485→ - 日/周/信标链/6旧星潮全部保留无回归
486→
487→Stage Summary:
488→- ✅ beacon.ts +289行(限时挑战完整系统:4h时段+确定性+进度+领奖+排行榜)
489→- ✅ starTide.ts +80行(3新类型 surge/eclipse/prism + 3新修饰器字段)
490→- ✅ decode.ts generatePuzzle 支持 targetLenBonussurge 谜题+2
491→- ✅ gameStore.ts trackBeacon 三进度同步 + claimTimedBeacon + 3处新星潮修饰器
492→- ✅ BeaconPanel.tsx +191行(amber 限时区块 + 紧急状态 + TIMED 徽章)
493→- ✅ 严格四色规范:限时amber/surge-emerald/eclipse-rose/prism-fuchsia,零蓝色
494→- ✅ 确定性:限时挑战同时段同种子同结果(FNV-1a slotKey 哈希)
495→- ✅ lint 零错误 + dev HTTP 200 + VLM 全部≥7/10
496→
497→---
498→Task ID: 10-b
499→Agent: full-stack-developer
500→Task: P2/#4 云排行榜 mini-service(信标本机榜升级为云端 Top100)
501→
502→Work Log:
503→- 前置阅读:worklogv0.8.1 项目状态)+ beacon.tsBeaconScoreEntry / loadLeaderboard / pushLeaderboardEntry / claimBeaconReward / claimWeeklyReward / claimTimedReward+ BeaconPanel.tsx(排行榜 UI 渲染)+ Caddyfile?XTransformPort=端口号 转发规则)+ examples/websocketmini-service 参考)+ .zscripts/dev.shmini-service 启动模式)
504→- 发现 worklog 信息滞后:beacon.ts 实际已扩到 1161 行(v0.8.2 已加限时挑战 + claimTimedReward),BeaconPanel 已扩到 953 行
505→- 创建 mini-service
506→ - `mini-services/leaderboard-service/package.json`:独立 bun 项目,type=modulescripts `dev: bun --hot index.ts` + `start: bun index.ts`,依赖 hono ^4.6.14
507→ - `mini-services/leaderboard-service/index.ts`182 行):
508→ - Hono + cors 中间件(origin: * 全开放,跨端口必须)
509→ - 内存数组存储(最多 1000 条,按 score 降序,同分按时长→时间戳排序)
510→ - 防刷:同 dateKey+challenge 10s 内只接受 1 次提交(Map 记录 lastSubmitAt),返回 429 + retryAfterMs
511→ - GET /api/leaderboard → Top100 + total
512→ - POST /api/leaderboard body {entry} → 校验 schema → 推入 → 排序 → 返回 {entries, total, rank}
513→ - GET /api/leaderboard/stats → {totalSubmissions, uniquePlayers, topScore}
514→ - GET / → 健康检查 {service, version, ok, uptime}
515→ - 端口固定 3030`export default { port, fetch }` 标准 bun 模式
516→ - `mini-services/leaderboard-service/README.md`API 文档 + 联调规范
517→- mini-service 启动调试:bun --hot 在本沙盒环境下会因文件 watcher 异常被 kill;改用 `bun index.ts`(无 --hot+ 双 fork `( ... & )` 模式启动,PPID=1uptime 4+ 分钟稳定
518→- eslint.config.mjs 新增 ignores "mini-services/**"mini-service 独立项目,不参与主项目 lint)
519→- beacon.ts 扩展(1161 → 1290 行,+129 行):
520→ - 三个 claim 函数返回值新增 `entry: BeaconScoreEntry`(非破坏性扩展):
521→ - `claimBeaconReward` 返回 `entry`daily
522→ - `claimWeeklyReward` 返回 `entry`weekly
523→ - `claimTimedReward` 返回 `entry`timed
524→ - 新增云排行榜模块(v0.8.2 P2/#4):
525→ - `BEACON_CLOUD_PORT = 3030` 常量
526→ - `BEACON_CLOUD_LAST_SUBMIT_KEY = "echo-nexus-beacon-cloud-last-submit-v1"` 本地 key
527→ - `CloudLeaderboardResponse` / `CloudSubmitResponse` / `CloudStatsResponse` 接口
528→ - `loadLastCloudSubmitTimestamp()` / `saveLastCloudSubmitTimestamp(ts)` 工具
529→ - `fetchCloudLeaderboard(): Promise<BeaconScoreEntry[]>` — GET 相对路径 + ?XTransformPort=3030
530→ - `submitCloudScore(entry): Promise<number>` — POST,成功返回 rank,失败返回 -1,自动记录 ts 供全球榜高亮
531→ - `fetchCloudStats(): Promise<CloudStatsResponse | null>` — 全局统计
532→ - 所有 fetch 用相对路径 + `?XTransformPort=3030`**禁止** localhost:3030
533→- gameStore.ts 集成:
534→ - import 新增 `submitCloudScore`
535→ - `claimWeeklyBeacon` action:领奖成功后 `void submitCloudScore(res.entry)`fire-and-forget,不 await,不阻塞)
536→ - `claimTimedBeacon` action:同上
537→ - `claimBeaconReward` (daily) 由 BeaconPanel 直接调用,不通过 gameStore
538→- BeaconPanel.tsx 重写(953 → 1170 行,+217 行):
539→ - 新增 import: fetchCloudLeaderboard / submitCloudScore / loadLastCloudSubmitTimestamp + 4 个 lucide 图标(RefreshCw/Globe/WifiOff/Loader2
540→ - 新增状态: lbTab ('local'|'global') / cloudEntries / cloudLoading / cloudError / cloudFetched / mySubmitTs / cloudFetchingRef
541→ - 新增 `refreshCloudLeaderboard()` callback:拉取云端榜 + 防并发(ref)+ 空 entries 时显示提示
542→ - useEffect: 切到全球 tab 自动拉取(仅首次)+ 初始化读取 mySubmitTs
543→ - handleClaimDaily: 领奖成功后 `void submitCloudScore(res.entry).then(rank => ...)` — 若用户在全球 tab 自动刷新
544→ - handleClaimTimed/Weekly: 领奖后更新 mySubmitTs + 若在全球 tab 自动刷新
545→ - 排行榜 UI 重写为双 tab:
546→ - 顶部「本地」(amber 主题) / 「全球」(emerald 主题) 切换按钮 + 全球 tab 专属刷新按钮(旋转动画)
547→ - 子标题:本地"本机 Top N · 离线可用" / 全球"全球 Top N · 已同步 / 同步中…"
548→ - 本地榜保留原 v0.8.2 渲染(max-h-200px overflow-y-auto
549→ - 全球榜:
550→ - 加载中:Loader2 spinner + "正在拉取全球榜…"
551→ - 空榜+错误:WifiOff 图标 + 错误文案 + 重试按钮
552→ - 空榜+无错:Globe 图标 + "尚无全球记录"提示
553→ - 有数据:渲染前 100 条,每条带排名(1-3 名用 Crown/Medal/Award 图标)/ 难度色 / WEEK·TIMED 徽章 / 用时 / 分数
554→ - 玩家自己的记录通过 timestamp 匹配 mySubmitTs 高亮:emerald 边框 + emerald 阴影 + YOU 徽章 + emerald 分数色 + 加粗排名
555→- QA 验证:
556→ - `bun run lint` 零错误零警告(eslint.config.mjs 把 mini-services 加入 ignores
557→ - dev.log 全程无错误,所有编译 < 300ms
558→ - mini-service curl 全通过:
559→ - GET / → 200 {service, version, ok, uptime}
560→ - GET /api/leaderboard (空) → 200 {entries:[], total:0}
561→ - POST /api/leaderboard → 200 {entries, total, rank:1}
562→ - GET /api/leaderboard (有数据) → 200 {entries:[...], total:N}
563→ - GET /api/leaderboard/stats → 200 {totalSubmissions, uniquePlayers, topScore}
564→ - OPTIONS 预检 → 204 + 完整 CORS 头(Allow-Origin: *
565→ - 重复 POST(同 dateKey+challenge 10s 内)→ 429 {error:"Rate limited", retryAfterMs}
566→ - 网关路由验证:Caddy :81 → localhost:3030 通过 ?XTransformPort=3030 正确转发,返回 JSON 200
567→ - agent-browser 集成测试(通过 :81 端口访问,绕过 Next.js 404):
568→ - 信标 tab 选中(用 KeyboardEvent Enter 激活 Radix Tabs,因 pointerdown 被覆盖层拦截)
569→ - 切到「全球」tab → 自动拉取 → 显示"全球 Top 6 · 已同步" → 渲染 6 条记录
570→ - 刷新按钮可点击,旋转动画正常
571→ - POST 测试:浏览器 fetch 提交 score=9999 → 返回 rank=1 → 刷新后榜首显示 10.00K
572→ - YOU 高亮:手动设置 localStorage[BEACON_CLOUD_LAST_SUBMIT_KEY] 后刷新 → 榜首显示 emerald 边框 + YOU 徽章
573→ - VLM 视觉评分(目标 ≥7/10):
574→ - 全球榜含 YOU 高亮:**8/10**(双tab切换清晰,全球榜记录完整,难度色与标签规范,视觉层次佳)
575→ - 本地榜:**8/10**(双tab切换逻辑清晰,本地tab内容完整,四色规范应用,信息层级合理)
576→
577→Stage Summary:
578→- ✅ mini-service 独立项目:自己的 package.json + bun.lock + node_modules,依赖 hono ^4.6.14,不污染主项目
579→- ✅ 端口 3030 + Caddy 网关:前端用相对路径 `fetch('/api/leaderboard?XTransformPort=3030')`**禁止** localhost:3030
580→- ✅ CORS 全开放:origin: * + OPTIONS 预检自动处理,前端跨端口无忧
581→- ✅ 内存存储 + 防刷:1000 条上限,同 dateKey+challenge 10s 限 1 次
582→- ✅ 三个 claim 入口全部接入云提交:claimBeaconRewardBeaconPanel 直接调)/ claimWeeklyBeacon / claimTimedBeacongameStore action 内)
583→- ✅ 本地榜保留:loadLeaderboard / pushLeaderboardEntry 未删除,作为离线 fallback
584→- ✅ 双 tab UI:本地(amber 主题,立即可用)/ 全球(emerald 主题,按需拉取)+ 刷新按钮 + 加载态 + 错误重试
585→- ✅ 玩家高亮:通过 timestamp 匹配 localStorage[BEACON_CLOUD_LAST_SUBMIT_KEY]emerald 边框 + YOU 徽章
586→- ✅ lint 零错误 + dev HTTP 200 + VLM 8/10
587→- ✅ mini-service 启动稳定:uptime 4+ 分钟,PPID=1(双 fork 模式)
588→- 下一阶段工单:P3 #10-c 全球星潮同步(star-tide-service 已在 mini-services 目录中)
589→
File diff suppressed because it is too large Load Diff
@@ -1,730 +0,0 @@
1→# 回响星核 / Echo Nexus — 开发工作日志
2→
3→> 持续性任务的工作交接文档。每个阶段更新,避免进度丢失。
4→
5→---
6→
7→## 一、项目当前状态描述 / 判断
8→
9→### 概况
10→- **项目**:回响星核 / Echo Nexus — 深空考古放置策略游戏
11→- **一句话**:自治无人机采矿 → 解码记忆晶体(原创共振谜题)→ 拼凑碎片叙事 → 飞升多周目。差异化:解码谜题驱动叙事涌现,全息晶体 + 深空粒子美学。
12→- **当前版本****v0.7**CrystalOrb Canvas 粒子系统 + 角色属性系统)
13→- **在线游玩**https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/
14→- **仓库**https://git.atdunbg.xyz/Super_Z/echo-nexus
15→- **技术栈**Next.js 16 (App Router) + TypeScript + Tailwind + shadcn/ui + Canvas 2D + Zustand + Web Audio API
16→- **定时任务**:每 15 分钟一次 `webDevReview``fixed_rate` + `"900"` 秒,priority=10job_id 228266)。正常完成不会被删除,无需自持续机制。
17→
18→### 状态判断
19→- dev 服务器运行正常(HTTP 200,编译 < 250ms
20→- v0.7 两大新功能已完成:CrystalOrb Canvas 粒子系统升级(VLM 8/10+ 角色属性系统(VLM 7/10
21→- 系统总体稳定,无阻塞性 bug,可继续推进新功能
22→
23→### 已完成版本里程碑(精简)
24→| 版本 | 核心内容 |
25→|------|---------|
26→| v0.1 | MVP:放置采矿 + 解码共振谜题 + 技术树 + 记忆图谱 + 飞升 + 统计 |
27→| v0.1.1 | 解码可解性修复(路径构造法生成器,100% 可解率)|
28→| v0.2 | 遗迹探险肉鸽系统(能量/探险力/6 节点/程序化路径/BOSS)|
29→| v0.2.1 | 程序化音频(15 种音效)+ 14 项成就 + 视觉打磨 |
30→| v0.3 | 星潮事件系统(6 种星潮 + 全屏叠层 + 程序化音效)|
31→| v0.3.1 | 星图天文台元进程(6 类 × 3 = 18 天赋 + 飞升后 3 选 1 draft + Canvas 星图)|
32→| v0.4 | 回响编年史(5 纪元循环命名 + 模板化叙事 + 时间轴 UI + 6 项新成就 + 飞升 BUG 修复)|
33→| v0.5 | 深空信标(每日挑战 + 本地排行榜 Top20 + 7 标签页 + 编年史历史 BUG 修复)|
34→| v0.5.1 | 静态部署 Gitea Pages + 新手教程系统(7 步聚光灯引导)+ UI 间距优化 |
35→| v0.5.2 | 离线收益报告弹窗 + 标签页 UI 重设计 + persist rehydrate 竞态修复 |
36→| v0.6 | 深空巡航 Canvas 2D 实时玩法 + UI 偏移重叠修复 |
37→| **v0.7** | **CrystalOrb Canvas 粒子系统升级 + 角色属性系统(四维)** |
38→
39→### 核心系统清单(8 大系统)
40→1. **放置采矿** — 中央晶体球 + 主动脉冲 + 连击加成(`CrystalOrb.tsx`
41→2. **谐振解码** — 原创路径谜题,路径构造法 100% 可解(`DecodeArray.tsx` + `engine.ts`
42→3. **技术树** — 三层科技解锁,提升产能/解码/探险(`TechTree.tsx`
43→4. **遗迹探险** — 肉鸽系统,6 种节点 + 程序化路径 + BOSS`ExpeditionPanel.tsx` + `expedition.ts`
44→5. **星潮事件** — 6 种动态事件,为放置循环注入变化(`StarTide.tsx` + `starTide.ts`
45→6. **星图天文台** — 飞升后 3 选 1 天赋 draft,跨周目永久生效,18 个天赋(`ConstellationPanel.tsx` + `constellation.ts`
46→7. **回响编年史** — 跨周目叙事时间轴,5 纪元循环命名(`ChronicleDialog.tsx` + `chronicle.ts`
47→8. **深空信标** — 每日挑战 + 本地排行榜 Top20`BeaconPanel.tsx` + `beacon.ts`
48→9. **深空巡航** — Canvas 2D 实时玩法,飞船操控/陨石躲避/星门通关(`CruiseMode.tsx` + `cruise.ts`)【v0.6】
49→10. **角色属性系统** — 四维属性(探索力/智慧/勇气/灵感),飞升获得属性点,影响所有系统(`AttributesPanel.tsx` + `attributes.ts`)【v0.7 新增】
50→
51→---
52→
53→## 二、当前目标 / 已完成的修改 / 验证结果
54→
55→### v0.7 CrystalOrb Canvas 粒子系统升级(已完成)
56→**动机**:P0 优先级 — 将中央晶体球从 CSS 动画升级为 Canvas 粒子视觉盛宴,提升视觉震撼。
57→
58→**重写文件**`src/components/game/CrystalOrb.tsx`
59→- 从纯 CSS 动画升级为 Canvas 2D 粒子系统
60→- **多层粒子**
61→ - 环绕能量粒子(3层14/10/8个,emerald/fuchsia/rose三色,不同速度方向环绕晶核,带拖尾)
62→ - 环境星尘(40个,缓慢漂移 + 闪烁)
63→ - 点击爆发粒子(10-30个径向发散,颜色随连击等级变色)
64→ - 冲击波环(点击触发1-3层错峰扩散,连击≥3/5解锁更多层)
65→- **晶核升级**:径向渐变辉光 + 3层六边形纹理旋转 + 高光 + 鼠标视差偏移
66→- **呼吸动画**:晶核 + 外层辉光随时间呼吸(sin波)
67→- **进度环**SVG渐变环(emerald→fuchsia→rose)保留
68→- **交互保留**:点击脉冲、连击Toast、浮动数字、音效、`data-tut="crystal-orb"` 锚点
69→- **性能**DPR cap 2ResizeObserver 自适应,requestAnimationFrame 60fps
70→
71→**QA 验证**agent-browser + VLM):
72→- VLM 视觉评分 **8/10**:晶体球清晰可见,多层辉光,环绕粒子动态自然,无渲染问题
73→- 点击交互:冲击波环 ✅ + 爆发粒子 ✅,评分 7/10
74→- lint 零错误;HTTP 200
75→
76→### v0.7 角色属性系统(已完成)
77→**动机**:P1 优先级 — 增加RPG深度,四维属性影响所有游戏系统。
78→
79→**新增文件**
80→- `src/lib/game/attributes.ts`~330行逻辑层):CharacterAttributes/AttributeProgress 类型、ATTRIBUTE_CONFIG 元信息、getAttributeBonus 两区加成公式(0-50线性+0.5%/点,50-100递减+0.2%/点)、getAllBonuses 12项修饰器、levelUpCheck 自动升级、computePrestigeAttrPoints、migrateAttributes 旧存档兼容
81→- `src/components/game/AttributesPanel.tsx`~330行 UI):四维卡片网格(小屏2×2/大屏1×4)、图标+中英文名+Lv.badge+数值/100+加成%+经验条+影响列表+分配按钮、顶部待分配点数badge闪烁、底部总览+12项修饰器明细
82→
83→**修改文件**
84→- `types.ts`GameState 新增 attributes/attributeProgress/pendingAttrPoints
85→- `config.ts`INITIAL_STATE 补全默认值
86→- `engine.ts`recomputeStats 聚合属性加成;performPrestige 发放属性点(ascensions×2+1
87→- `gameStore.ts`:新增 allocateAttribute/gainAttributeExp actionspulse/clickNode/autoDecodeTick/resolveCurrentNode/grantCruiseReward/tickTide 全部接入属性经验;init() 调用 migrateAttributes
88→- `achievements.ts`:新增 ach_attr_total_50 + ach_attr_max_100 两项成就
89→- `page.tsx`grid-cols-7→8,新增「角色」标签页(User图标),红点提示,StatsPanel新增5行属性行,版本号 v0.6→v0.7
90→
91→**四维属性设计**
92→- 探索力(emerald):探险力+X%/巡航飞船速度+X%
93→- 智慧(fuchsia):解码步数+X/洞见+X%/自动解码周期-X%
94→- 勇气(amber):探险生命+X/BOSS胜率+X%/巡航护盾+X
95→- 灵感(rose):接触率+X/星潮概率+X/脉冲连击+X%
96→
97→**QA 验证**agent-browser + VLM):
98→- VLM 视觉评分 **7/10**(面板)/ **8/10**(待分配状态):四维卡片可见、数值/等级/加成/经验条齐全、四色全息
99→- 功能验证:setState pendingAttrPoints=5 → 显示5点待分配 → 点击分配按钮 → exploration 0→1, pending 5→4 ✅
100→- lint 零错误;HTTP 200
101→
102→---
103→
104→### v0.6 深空巡航 Canvas 2D 实时玩法(历史记录)
105→**动机**:回应用户反馈「玩法太单调(全是点点点)」+「视觉震撼不足」——首次引入实时操作玩法。
106→
107→**新增文件**
108→- `src/lib/game/cruise.ts`~520 行逻辑层)
109→ - 类型系统:CruiseEntity 联合类型(ship/asteroid/storm/crystal/insight/beacon/stargate/particle 共 8 种)
110→ - mulberry32 + FNV-1a 种子化 RNG`cruiseSeed(level)`
111→ - `generateLevel`:程序化生成陨石带(8+lvl×2)、虚空风暴(1+lvl/2)、晶体碎片(5+lvl)、洞见光球(2+lvl/3)、信标(1+lvl/4)、星门
112→ - `updateCruise`:物理推进 + 碰撞检测 + 粒子系统 + 震动 + 600ms 无敌帧
113→ - `computeRewards`crystals=碎片×(8+lvl×2)、insights=洞见×(2+lvl×0.5)、contact=信标×(1.5+lvl×0.3);通关×1.5、失败×0.5
114→ - 独立 localStorage `echo-nexus-cruise-v1`:最高分/累计通关/最高关卡/累计奖励/最近 20 局
115→
116→- `src/components/game/CruiseMode.tsx`~830 行渲染层)
117→ - 全屏 fixed inset-0 z-50 CanvasDPR cap 2resize 监听
118→ - 3 层视差星空:远(100星)/中(60星)/近(30星),独立漂移 + 飞船速度视差 + 闪烁
119→ - 8 种实体全部 ctx.shadowBlur 辉光绘制
120→ - 粒子系统:尾焰/收集/碰撞/烟花,上限 200
121→ - 屏幕震动 280ms 衰减;低护盾(<30%)红色边框脉冲
122→ - HUDHTML 叠层,glass+backdrop-blur80ms 节流):护盾/能量/分数/用时/收集计数
123→ - 控制:桌面 WASD/方向键 8 方向 + 移动端虚拟摇杆 + P 暂停 + Esc 退出 + 失焦自动暂停
124→ - 阶段:ready → playing → won/lost(结算明细 + 奖励 + 下一关/重试/返回)
125→
126→**修改文件**
127→- `src/app/page.tsx`header 新增 amber 主题「巡航」按钮(Navigation 图标)+ cruiseOpen state + 条件渲染 CruiseMode
128→- `src/store/gameStore.ts`:新增 `grantCruiseReward({crystals?, insights?, contact?})` actioncrystals 受 crystalCap 限制,contact 受 100 上限)
129→- 版本号 v0.5.2 → v0.6
130→
131→**UI 偏移/重叠 BUG 修复**(3 处):
132→- `DecodeArray.tsx`:移除 className 冲突的 `-translate-x-1/2 -translate-y-1/2`,统一用 inline transform 定位+缩放
133→- `AchievementsPanel.tsx`:移除内层 `max-h-[300px]`,改 `flex-1 min-h-0` 自适应父容器
134→- `page.tsx`:调整三个 section 的 min-h,避免小屏单列时总高度超过手机视口
135→
136→**色彩规范**:严格遵循 emerald(#34d399) / rose(#fb7185) / amber(#fbbf24) / fuchsia(#e879f9) 四色全息色谱,零蓝色/靛色
137→
138→**QA 验证**agent-browser + VLM):
139→- lint 零错误;dev 服务器 HTTP 200;编译 < 250ms
140→- VLM 确认主界面:巡航按钮存在、晶体球居中、解码区正常、无重叠偏移
141→- VLM 确认巡航玩法:飞船可见(绿色三角形)、HUD 完整、陨石+星空背景、粒子特效(黄色尾焰+紫色辉光)
142→- WASD 控制飞船移动,按 W 直冲星门 → 通关,奖励 +30 晶体(2×10×1.5=30 计算正确)
143→- localStorage 正确记录 highScore/totalRuns/totalWins/bestLevel
144→- 奖励同步 gameStoregrantCruiseReward,满仓时 cap 逻辑正确)
145→- 退出返回主界面,7 标签页 + 晶体球 + 解码面板均完好
146→
147→---
148→
149→## 三、未解决问题或风险 / 下一阶段优先事项
150→
151→### 已知问题 / 风险
152→1. **dev 服务器 Turbopack 缓存偶发损坏**`Unable to open static sorted file` + 进程被杀,需 `rm -rf .next` 重启。静态导出构建稳定可靠。
153→2. **定时任务调度类型**:必须用 `fixed_rate` + `"900"` 秒(15分钟),不要用 cron 表达式。`webDevReview` 类型任务正常完成不会被删除,只有抛出未捕获异常才会被标记失败。
154→3. **v0.3 星潮为单机版**:原计划 socket 全局事件,后续可扩展为多人同步。
155→4. **v0.5 信标排行榜为本地版**(纯 localStorage):升级云排行榜需后端 API。
156→5. **v0.5 每日挑战仅 1 个/天**:可加入"周挑战"或"信标链"(连续完成 N 天奖励)。
157→6. **探险 BOSS 战胜率较低**:基础探索力 10 vs BOSS 难度 5-7,胜率约 25%,需玩家投资探险技术。
158→7. **探险能量恢复较慢**:45s/点,可加技术提升恢复速度。
159→8. **编年史上限 50 条**:超过自动丢弃最早的;v0.4 之前飞升无回填(空状态有提示)。
160→9. **5 纪元叙事为模板化生成**:v0.5+ 将加入手写剧情节点。
161→10. ~~**CrystalOrb 仍为 CSS 动画**~~ → ✅ **v0.7 已完成 Canvas 粒子系统升级**。
162→
163→### 下一阶段优先级(v0.7 后修正版)
164→> 工单修正:原 P0 CrystalOrb Canvas + P1 角色属性两项均已在 v0.7 完成,现重新排序。
165→
166→1. **🔴 P0 — 巡航玩法大增强(v0.8 主线)**:BOSS 战关卡(每 5 关一个 BOSS,三阶段攻击模式 + 飞船射击系统)/ 道具掉落(5 种 powerup:护盾/能量/急速/超载/磁吸)/ 事件选择节点(每关通关后 3 选 1 强化牌,10 种强化)。这是当前最高优先级,能显著提升巡航玩法的深度与可玩性。
167→2. **🟠 P1 — 探险 BOSS 战胜率平衡 + 探险能量恢复**:修复已知问题 6/7(探险 BOSS 胜率 25% 偏低,能量 45s/点 偏慢),提升探险体验。
168→3. **🟠 P1 — 信标系统扩展**:周挑战 / 信标链(连续完成 N 天奖励),修复已知问题 5。
169→4. **🟡 P2 — UI 样式细节打磨**:标签页图标/文字比例、视觉层次强化、移动端响应式进一步优化、CrystalOrb 周边留白优化(VLM 指出主界面左侧有空白)。
170→5. **🟡 P2 — 云存档 + 云排行榜**:v0.5+ 后端 API(需评估是否引入 mini-service)。
171→6. **🟢 P3 — 全 5 纪元手写叙事节点**:替换模板化 lore。
172→7. **🟢 P3 — socket 多人同步星潮**:全局事件共享。
173→
174→### 定时任务(fixed_rate · 900s
175→- **当前 job_id**: **228357**2026-06-24 13:25 重建)
176→- **配置**: `fixed_rate` + `"900"`15分钟间隔),`priority=10`(高优先级),`payload.kind=webDevReview``tz=Asia/Shanghai`
177→- **任务名**: Echo Nexus - 任务审查与持续迭代
178→- **任务描述强制要求**:每次触发 MUST 先完整阅读 worklog.md 工单,以工单为准,不凭记忆工作
179→- **正常完成不会被删除**,只有未捕获异常会标记失败。若任务列表为空,需用 cron create 重建(schedule.kind=fixed_rate, expr="900", priority=10, payload.kind=webDevReview)。
180→- **job_id 历史**: 227909(失效) → 228130(失效) → 228172(失效) → 228186(失效) → 228218(删除重建) → 228236(cron版,删除重建) → 228266(fixed_rate,失效) → 228329(失效) → **228357(当前,重建)**
181→
182→---
183→
184→## 定时任务描述模板(webDevReview · fixed_rate
185→
186→> 重建参数:schedule.kind=`fixed_rate`, schedule.expr=`"900"`, params.priority=`10`, payload.kind=`webDevReview`
187→
188→```
189→请先判断当前项目状态,使用 agent-browser 进行必要测试和 QA,然后自主选择本轮最合适的工作重点并继续开发。
190→
191→要求:
192→1. 开始任务时查看 /home/z/my-project/worklog.md 了解实际进展,拆解详细todo
193→2. 使用 agent-browser 进行测试和QA,有bug优先修复
194→3. 如果稳定,自主提出新需求继续推进
195→4. 样式要越做细节越多!!
196→5. 功能要越做越多!!
197→6. 工作告一段落更新 /home/z/my-project/worklog.md
198→7. 项目结束后一定要把工作记录写在 worklog.md 里
199→
200→交接文档建议包含三部分:
201→- 项目当前状态描述/判断
202→- 当前目标/已完成的修改/验证结果
203→- 未解决问题或风险,建议下一阶段优先事项
204→```
205→
206→---
207→
208→## 历史详细版本记录(按需查阅 docs/repo/docs/
209→
210→- `04-解码系统修复-v0.1.1.md`
211→- `05-遗迹探险系统-v0.2.md`
212→- `06-音频与成就系统-v0.2.1.md`
213→- `07-星潮事件系统-v0.3.md`
214→- `08-星图天文台系统-v0.3.1.md`
215→- `09-回响编年史系统-v0.4.md`
216→- `10-深空信标系统-v0.5.md`
217→
218→---
219→
220→Task ID: 7
221→Agent: full-stack-developer
222→Task: 角色属性系统(探索力/智慧/勇气/灵感)
223→
224→Work Log:
225→- 阅读 worklog.md 了解 v0.6 项目背景,确认 8 大系统架构与四色全息色彩规范
226→- 新建 `src/lib/game/attributes.ts` 逻辑层(~330 行):
227→ - 定义 CharacterAttributes/AttributeProgress/AttributeKey 类型 + ATTRIBUTE_CONFIG 元信息
228→ - getAttributeBonus 线性+递减两区加成公式(0-50 每 10 点 +5%50-100 每点 +2%
229→ - getAllBonuses 返回 12 个修饰器(探险力/巡航速度/解码步数/洞见/自动解码周期/探险生命/BOSS 胜率/巡航护盾/接触率/星潮触发/脉冲连击/产能)
230→ - levelUpCheck 自动跨多级 + expRequiredForLevel + computePrestigeAttrPoints + migrateAttributes 兼容
231→- 扩展 `src/lib/game/types.ts` GameState:新增 attributes/attributeProgress/pendingAttrPoints
232→- 扩展 `src/lib/game/config.ts` INITIAL_STATE:补全默认值(0 数值 + 空 progress
233→- 扩展 `src/lib/game/engine.ts`
234→ - recomputeStats 末尾聚合 am.crystalsPerSecMult/insightMultAdd/contactRateMult/decodeStepsBonus
235→ - performPrestige 保留 attributes 数值,清空 attributeProgress,发放 ascensions × 2 + 1 点属性点
236→ - createInitialState 每次返回全新对象避免引用共享
237→- 扩展 `src/store/gameStore.ts`(核心改造,10+ 处 syncStats 调用全部补 attributes 参数):
238→ - pulse:连击 ≥3 给灵感经验 + 脉冲威力乘灵感连击加成
239→ - clickNode 完成:给智慧经验(tier × 2)
240→ - autoDecodeTick:智慧经验 +1 + 自动解码周期受 am.autoDecodeIntervalMult 影响
241→ - startExpedition:探险力乘 am.expeditionPowerMult,生命加 am.expeditionHpBonus
242→ - resolveCurrentNodeBOSS 节点包装 RNG 提升 +am.bossWinRateBonus 胜率;战斗胜利给勇气+探索力经验
243→ - grantCruiseReward:按总奖励量给探索力+勇气经验
244→ - tickTide:灵感 am.tideTriggerBonus 缩短星潮间隙(上限 30%)
245→ - init() 调用 migrateAttributes 补全旧存档
246→ - 新增 allocateAttribute / gainAttributeExp 两个 action
247→- 新建 `src/components/game/AttributesPanel.tsx` UI 面板(~330 行):
248→ - 四维卡片网格(小屏 2×2,大屏 1×4)
249→ - 每张卡片:图标 + 中英文名 + Lv.badge + 数值/100 + 加成% + 经验条 + 影响列表 + 「+分配」按钮
250→ - 顶部待分配点数 badgependingAttrPoints > 0 时 echo-pending-pulse 闪烁动画)
251→ - 底部总等级/总加成概览 + 12 个修饰器明细
252→ - 4 色辉光边框(emerald/fuchsia/amber/rose),顶角光晕装饰
253→- 接入 `src/app/page.tsx`
254→ - grid-cols-7 → grid-cols-8,新增第 8 个「角色」TabsTriggerUser 图标,渐变主题)
255→ - pendingAttrPoints > 0 时显示 rose 红点提示
256→ - 渲染 AttributesPanel
257→ - StatsPanel 新增 5 行属性行(探索力/智慧/勇气/灵感/待分配点)
258→ - 版本号 v0.6 → v0.7
259→- 新增 `src/lib/game/achievements.ts` 2 项成就:
260→ - ach_attr_total_50(四维觉醒):总属性 ≥ 50 → 产能+6%/洞见+6%
261→ - ach_attr_max_100(维度精通):任一属性 100 → 产能+12%/洞见+10%
262→- 写入 `agent-ctx/7-full-stack-developer.md` 工作记录
263→- 最终验证:lint 零错误 + dev 服务器 HTTP 200 + 编译 < 250ms
264→
265→Stage Summary:
266→- ✅ 角色属性系统 v0.7 P1 全部完成
267→- ✅ 新增 2 文件(attributes.ts + AttributesPanel.tsx),修改 6 文件(types/config/engine/gameStore/achievements/page
268→- ✅ 四维属性完整接入游戏各系统(探险/解码/脉冲/巡航/星潮)
269→- ✅ 飞升属性点公式 = ascensions × 2 + 1;经验累积自动升级(每级 10×level)
270→- ✅ 旧存档兼容(migrateAttributes 补全字段,同步 level 与 attributes
271→- ✅ lint 零错误 + HTTP 200 + 编译 < 250ms
272→- ✅ 严格四色全息(emerald/fuchsia/amber/rose),零蓝色/靛色
273→- ✅ 8 标签页 + 巡航按钮全部保留,data-tut 锚点未破坏
274→- ✅ 待分配点数时标签页 rose 红点提示 + 顶部 badge 闪烁
275→
276→---
277→Task ID: 8-deploy
278→Agent: main
279→Task: 重建定时任务 + 更新 README + 部署 gh-pagesv0.7
280→
281→Work Log:
282→- 检查项目状态:CrystalOrb.tsx 已是 v0.7 Canvas 粒子系统(659 行),无活动 cron job
283→- 重建定时任务:cron create fixed_rate="900" priority=10 webDevReview → job_id 228329tz=Asia/Shanghai
284→- 重写 README.md
285→ - 顶部显著位置添加「在线游玩」链接(https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/
286→ - 版本号 v0.1 → v0.7
287→ - 新增 10 大系统表格(含 v0.6 巡航 + v0.7 角色属性)
288→ - 完整开发路线表(v0.1-v1.0,标记 ✅/🔜 状态)
289→ - 新增本地开发命令 + Gitea Pages 部署指南
290→ - 文档索引扩展至 10 个版本文档
291→- 静态导出:BUILD_EXPORT=true bun run build:static → out/ 1.6MHTTP 200 编译 4.3s
292→- 推送 gh-pages 分支:force push origin gh-pagescommit 8727fed "deploy: v0.7"
293→- 提交 mainREADME 更新(commit 43dfc2d "docs: README 添加在线游玩链接 + v0.7 状态更新"
294→- 验证 Gitea Pageshttps://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/ → HTTP 200 ✅
295→- QA 验证:agent-browser 打开 http://localhost:3000/ → VLM 评分 8/10(主界面 polishedminor 空白)
296→
297→Stage Summary:
298→- ✅ 定时任务重建成功(job_id 228329,每 15 分钟自动审查迭代)
299→- ✅ README 重写完成,网站链接显著展示,v0.7 状态完整
300→- ✅ gh-pages 部署成功,Gitea Pages HTTP 200 可访问
301→- ✅ main 分支已同步推送(README + worklog 更新)
302→- ✅ dev 服务器健康(HTTP 200,编译 < 250ms
303→- ✅ CrystalOrb v0.7 Canvas 粒子系统已确认完整(659 行)
304→- 下一步:v0.8 巡航玩法增强(BOSS 战 / 道具掉落 / 事件选择节点)
305→
306→---
307→Task ID: 8
308→Agent: main
309→Task: v0.8 巡航玩法大增强(BOSS 战 + 道具掉落 + 事件选择节点)+ 工单修正 + 审查流程重建
310→
311→Work Log:
312→- 重读 worklog 工单,发现 v0.7 两项(CrystalOrb Canvas + 角色属性)已完成但工单"下一阶段优先级"未更新
313→- 修正 worklog 工单:标注 P0/P1 v0.7 已完成,重新排序下一阶段优先级(P0=巡航增强, P1=探险平衡/信标扩展, P2=UI打磨/云存档, P3=叙事/多人)
314→- 重建审查流程 cron jobfixed_rate=900s, priority=10, webDevReview, tz=Asia/Shanghai, job_id=228357
315→ - 任务描述强制要求:每次触发 MUST 先完整阅读 worklog.md 工单,以工单为准,不凭记忆工作
316→- 发现之前被中断的 v0.8 subagent 已写入 2037 行代码(cruise.ts +1487 / CruiseMode.tsx +858 / page.tsx +2
317→- 修复 2 个 lint 错误(react-hooks/set-state-in-effect):
318→ - isTouch: useState(false) + useEffect(setIsTouch) → useState initializer
319→ - stats: useState(null) + useEffect(setStats(loadCruiseStats)) → useState initializer
320→- QA 验证(agent-browser + VLM):
321→ - 主界面 VLM 7/10v0.8 版本号 ✅ + 巡航按钮 ✅
322→ - 巡航 READY 阶段 VLM 8/10BOSS 战说明 ✅(每5关BOSS战/击败BOSS)+ 射击控制说明 ✅(空格/右下角射击按钮)
323→ - 巡航第1关游戏 VLM 8/10:飞船(绿色三角)✅ + HUD(护盾/能量/分数/计时)✅ + 陨石/收集物/能量场 ✅ + 无渲染错误
324→- 提交 v0.8commit 494bc5f+ 推送 main
325→- 静态导出 BUILD_EXPORT=true → out/ 1.6M(编译 4.1s
326→- 部署 gh-pagesforce push, commit b865922)→ Gitea Pages HTTP 200 ✅
327→
328→Stage Summary:
329→- ✅ 工单修正完成:v0.7 已完成项标注清楚,下一阶段优先级重新排序
330→- ✅ 审查流程重建:cron job_id 228357,每15分钟触发,强制读工单
331→- ✅ v0.8 巡航玩法大增强全部完成并部署:
332→ - BOSS 战系统(每5关,三阶段攻击,HP条,击败奖励×3)
333→ - 飞船射击系统(空格/射击按钮,能量消耗,子弹尾焰)
334→ - 道具掉落(5种powerup:护盾/能量/急速/超载/磁吸)
335→ - 事件选择节点(每关通关后3选1,10种强化牌)
336→ - lint 零错误 + dev HTTP 200 + gh-pages HTTP 200
337→- ✅ VLM 评分:主界面 7/10 + 巡航READY 8/10 + 巡航游戏 8/10
338→- 在线游玩 v0.8https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/
339→- 下一阶段工单:P1 探险BOSS胜率平衡 + 探险能量恢复 / 信标系统扩展(周挑战/信标链)
340→
341→---
342→Task ID: 9-b
343→Agent: full-stack-developer
344→Task: 信标系统扩展(周挑战 + 信标链连续奖励)
345→
346→Work Log:
347→- 阅读现有 `src/lib/game/beacon.ts`v0.5 每日挑战 + 本地排行榜,358 行)+ `src/components/game/BeaconPanel.tsx` + `src/store/gameStore.ts` 的 `trackBeacon` 函数(约第 173 行)+ worklog v0.8 项目状态,确认四色全息规范与现有架构。
348→- **扩展 `src/lib/game/beacon.ts`358 → 873 行,新增 ~515 行)**
349→ - **周挑战(WEEKLY CHALLENGE**
350→ - `BeaconWeeklyChallenge` 接口(weekKey YYYY-Www / type / difficulty 强制 anomaly|singular / goal / rewardInsight / rewardContact / seed / title / desc
351→ - `BeaconWeeklyProgress` 接口(weekKey / progress / startedAt / completedAt / claimed / durationSec
352→ - `getWeekKey(now)`ISO 8601 周键(周一为起点,含首个周四的周为第一周)
353→ - `weekKeyToSeed`FNV-1a 哈希
354→ - `generateWeeklyChallenge(now)`:基于 weekKey 种子确定性生成,难度加权 anomaly 60% / singular 40%goal 为日基准 ×3-5 倍(decode 18-75 / expedition 3-15 / pulse 60-245 / boss 3-12 / insight 120-595
355→ - `loadWeeklyProgress` / `saveWeeklyProgress` / `addWeeklyProgress` / `claimWeeklyReward`(推送排行榜,entry 加 `isWeekly: true`
356→ - `msUntilNextWeek(now)`:距离下周一 UTC 0 点的毫秒数
357→ - `BEACON_WEEKLY_KEY = "echo-nexus-beacon-weekly-v1"`
358→ - **信标链(BEACON CHAIN**
359→ - `BeaconChainState` 接口(lastCompletedDateKey / currentStreak / longestStreak / totalCompletions / graceUsed / milestonesClaimed
360→ - `BEACON_CHAIN_KEY = "echo-nexus-beacon-chain-v1"`
361→ - `BEACON_CHAIN_MILESTONES = [3, 7, 14, 30]` 常量
362→ - `BEACON_CHAIN_REWARDS`4 个里程碑(三日谐振 +50/+5 / 七日回响 +120/+12 / 半月星潮 +280/+28 / 满月飞升 +680/+68
363→ - `loadChainState` / `saveChainState`(每次返回新对象避免引用共享)
364→ - `recordChainCompletion(dateKey)` 核心逻辑:同日重复忽略;次日 streak++;隔一天 miss 且 graceUsed<1 时续命 streak++ graceUsed++;其他断链 streak=1 graceUsed=0;返回 `{ state, newMilestones }`
365→ - `claimChainMilestone(milestone)` / `getNextMilestone(streak)` / `getChainProgress(streak)`(返回 current/next/prev/progressPct 用于 UI
366→ - `dateKeyToTimestamp` / `dateKeyDiffDays` 工具函数
367→ - `BeaconScoreEntry` 新增可选 `isWeekly?: boolean` 字段
368→- **扩展 `src/store/gameStore.ts`**
369→ - import 扩展:generateWeeklyChallenge / loadWeeklyProgress / loadChainState / addWeeklyProgress / recordChainCompletion / claimWeeklyReward / claimChainMilestone / getTodayKey + 类型
370→ - `trackBeacon(type, delta)` 函数签名从 `boolean` 改为 `{ dailyJustCompleted, weeklyJustCompleted, newChainMilestones }`
371→ - 日挑战类型匹配且未完成 → addBeaconProgress;若刚完成 → recordChainCompletion(getTodayKey()) 收集 newMilestones
372→ - 周挑战类型匹配且未完成 → addWeeklyProgress
373→ - 所有调用点(pulse/clickNode/autoDecodeTick/resolveCurrentNode 等 7 处)原本就忽略返回值,向后兼容
374→ - 新增 action `claimWeeklyBeacon`:调用 claimWeeklyReward → 发放 insights + contact 到 state,返回 `{ rewardInsight, rewardContact, score }`
375→ - 新增 action `claimChainReward(milestone)`:前置校验 currentStreak 与 milestonesClaimed,调用 claimChainMilestone → 发放奖励,返回 `{ rewardInsight, rewardContact, label, ok }`
376→ - GameActions 接口同步新增两个 action 签名
377→- **重写 `src/components/game/BeaconPanel.tsx`301 → 638 行)**
378→ - 顶部头部 + 每日挑战卡片(保留 v0.5 完整功能 + 按钮主题色随难度色淡显)
379→ - **新增周挑战区块**fuchsia 主题):标题"周挑战 · WEEKLY" + weekKey + 倒计时 + 挑战卡片(类型/难度标签/标题/描述/进度条/奖励/领取按钮 emerald 主题)+ weekly-glow 动画
380→ - **新增信标链区块**amber→rose 渐变):标题"信标链 · CHAIN" + 大字号 streakchain-streak-text 渐变流动动画)+ 今日完成状态徽章 + 4 个里程碑节点(w-12 h-12 圆形,claimed=emerald/✓,reachable=rose 脉冲动画+领取按钮,inProgress=amber 半亮,未到达=muted 灰)+ 节点间连线(背景灰 + 已达成部分 amber→rose 渐变 + 辉光,基于 prev→next 插值定位)+ 进度条 + 底部统计(最长/累计/续命状态)
381→ - 桌面端 lg:grid-cols-2 让周挑战+信标链并排,移动端单列
382→ - 排行榜条目区分日/周:周挑战 entry 显示 "WEEK" 徽章 + fuchsia 高亮背景
383→ - 空状态提示扩展为"完成今日或本周信标即可登榜"
384→ - 4 个新动画:weekly-glow / chain-milestone-pulse / chain-streak-flux(背景渐变流动)
385→- **QA 验证**
386→ - `bun run lint` 零错误(每次修改后均验证)
387→ - dev.log 全程无错误,所有编译 < 300ms
388→ - **信标链逻辑测试**(bun 直接运行 TS,5 个场景全 PASS):
389→ 1. 昨日 streak=1 → 今日完成 → streak=2 ✅
390→ 2. 同日重复完成 → 忽略 ✅
391→ 3. 明日完成 → streak=3,无需 grace ✅
392→ 4. 隔一天 misslastCompleted + 2 天)→ streak=4graceUsed=1 ✅
393→ 5. 再次 missgrace 已用)→ 断链 streak=1graceUsed=0 ✅
394→ - **UI 集成测试**agent-browser):
395→ - localStorage 模拟设置 chain statelastCompletedDateKey=yesterday, currentStreak=1)→ reload → UI 正确显示 streak=1
396→ - 设置 streak=3 + milestonesClaimed=[] → milestone 3 节点显示 rose 脉冲动画 + "领取"按钮
397→ - 点击"领取"→ milestonesClaimed=[3] + Toast "✦ 三日谐振 已领取 +50 洞见 · +5.0 接触" + 按钮消失 ✅
398→ - **VLM 视觉评分**(目标 ≥7/10):
399→ - 首屏截图:8/10(四色一致、布局合理、信标链清晰)
400→ - 里程碑可领取状态:8/10(amber→rose 渐变醒目、里程碑层次分明、fuchsia 主题清晰)
401→ - 重置后干净状态:8/10(WEEKLY 字体对比度可优化,但整体可读性强)
402→ - **不破坏现有功能**:日挑战进度/领奖/排行榜流程全部保留并工作正常
403→
404→Stage Summary:
405→- ✅ beacon.ts 扩展完成(358 → 873 行):周挑战 + 信标链全部函数与类型
406→- ✅ gameStore.ts 集成完成:trackBeacon 升级为对象返回值 + claimWeeklyBeacon + claimChainReward 两个新 action
407→- ✅ BeaconPanel.tsx UI 完整重写(301 → 638 行):周挑战 fuchsia 卡片 + 信标链 amber→rose 渐变区块
408→- ✅ 严格四色全息:周挑战 fuchsia、信标链 amber→rose、领取按钮 emerald、无蓝/紫色违规
409→- ✅ localStorage 新增 2 个独立 keyweekly + chain),不污染旧 key
410→- ✅ 确定性:周挑战同周同种子同结果(weekKey FNV-1a 种子)
411→- ✅ 信标链 grace 续命机制工作正常(5 个场景 bun 测试全 PASS)
412→- ✅ lint 零错误 + dev HTTP 200 + VLM 8/10
413→- ✅ 现有日挑战 + 排行榜 + 领奖流程全部保留,无回归
414→
415→---
416→Task ID: 9
417→Agent: main + subagent(9-b)
418→Task: v0.8.1 P1 双任务 — 探险 BOSS 胜率平衡 + 能量恢复 + 信标系统扩展(周挑战 + 信标链)
419→
420→Work Log:
421→- 重读工单确认 P1 两项为下一阶段优先级
422→- P1-a 探险平衡(main 自己做,平衡需要精细控制):
423→ - expedition.ts: combatDifficultyScale 8→4, combatWinRateFloor 0.25→0.35, BOSS difficulty 5-7→3-5
424→ - 新增 computeEnergyRegenInterval(state): exp_2 -30%, exp_3 -20%, 探索力属性 -最高30%, 下限12s
425→ - computeEnergyRegen 接受 intervalSec 参数
426→ - config.ts: exp_2/exp_3 描述加能量恢复说明
427→ - gameStore.ts tick: 传入动态间隔
428→ - ExpeditionPanel.tsx: 显示实际恢复速度 + '已加速'标记
429→- P1-b 信标系统扩展(dispatch subagent 9-b,并行完成):
430→ - beacon.ts 358→873行: 周挑战(getWeekKey ISO 8601 + 确定性生成) + 信标链(4里程碑+grace续命)
431→ - BeaconPanel.tsx 301→638行: 周挑战fuchsia主题 + 信标链amber→rose里程碑节点
432→ - gameStore.ts: trackBeacon升级 + claimWeeklyBeacon/claimChainReward actions
433→- QA 验证:
434→ - lint 零错误
435→ - 探险面板 VLM 7/10(能量条 + 恢复速度显示正常)
436→ - 信标面板 VLM 8/10(周挑战 WEEKLY + 信标链 3/7/14/30 + 排行榜全部可见)
437→ - subagent 5场景 bun 测试全 PASS(信标链断链/续命逻辑)
438→- 提交 v0.8.1commit 71ca5b4+ 推送 main
439→- 静态导出 + 部署 gh-pagescommit 97eecaa)→ HTTP 200
440→
441→Stage Summary:
442→- ✅ P1-a 探险平衡完成: BOSS 基础胜率 25%→35-45%, 配合技术/属性可达 55-70%
443→- ✅ P1-a 能量恢复: 固定45s → 动态12-45s(技术+属性加速)
444→- ✅ P1-b 周挑战: 每周一UTC刷新, fuchsia主题, goal×3-5倍
445→- ✅ P1-b 信标链: 4里程碑(3/7/14/30天) + grace续命, amber→rose渐变
446→- ✅ 在线游玩 v0.8.1: https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/
447→- 下一阶段工单: P2 UI样式细节打磨 / P2 云存档+云排行榜 / P3 全纪元叙事
448→
449→---
450→Task ID: 10-a
451→Agent: full-stack-developer
452→Task: #5 限时挑战 + #3 星潮类型深化
453→
454→Work Log:
455→- 阅读项目上下文:worklog.mdv0.8.110 大系统)+ beacon.ts873行,日+周+链)+ starTide.ts209行,6种星潮)+ gameStore trackBeacon/tickTide + BeaconPanel.tsx,确认四色全息规范与现有架构
456→- **工单 #5 限时挑战**
457→ - beacon.ts873→1162行)新增限时挑战系统:
458→ - BeaconTimedChallenge/BeaconTimedProgress 接口 + isTimed 字段加入 BeaconScoreEntry
459→ - getTimedSlotKeyUTC 0/4/8/12/16/20 点切分,"timed_YYYY-MM-DD_HH"+ timedSlotKeyToSeedFNV-1a
460→ - generateTimedChallenge(确定性,难度 routinegoal=日基准×0.3-0.55类型公式)
461→ - loadTimedProgress/saveTimedProgress/addTimedProgress/claimTimedReward(排行榜 isTimed
462→ - msUntilNextTimedSlot + TIMED_SLOT_MS=4h + BEACON_TIMED_KEY
463→ - gameStore.tstrackBeacon 返回值新增 timedJustCompleted,三进度同时更新;新增 claimTimedBeacon action
464→ - BeaconPanel.tsx753→944行):日挑战与周挑战之间插入限时挑战区块(amber 主题)
465→ - Hourglass图标+TIMED标题+slotKey+倒计时 + 进度+奖励+领取按钮(emerald)
466→ - 紧急状态:距时段结束<30min 切 rose 脉冲动画+"即将结束"徽章
467→ - 2新动画 timed-glow(amber)/timed-urgent(rose) + 排行榜 TIMED 徽章
468→- **工单 #3 星潮深化**
469→ - starTide.ts209→289行):TideType 新增 surge/eclipse/prism
470→ - surge(emerald ⇈): crystalsPerSec×2.5 + targetLenBonus=2(解码目标序列+2,更长更难)
471→ - eclipse(rose ◐): crystalsPerSec×0.7 + bossWinRateBonus=0.2BOSS胜率+20%
472→ - prism(fuchsia ◬): insightMultAdd=1.0(洞见×2) + autoDecodeIntervalMult=0.7(自动解码-30%)
473→ - TideModifiers 新增 targetLenBonus/bossWinRateBonus/autoDecodeIntervalMult 可选字段
474→ - TIDE_WEIGHTS 新增3种各12(总136+ getTideModifiers 3新case
475→ - decode.tsgeneratePuzzle 新增 targetLenBonus 参数(surge用,targetLen+stepLimit同步增加)
476→ - gameStore.ts 3处修饰器应用:
477→ - startDecodesurge 时传 targetLenBonus → 谜题更长
478→ - autoDecodeTickprism 时 interval×=autoDecodeIntervalMult
479→ - resolveCurrentNodeeclipse 时 bossWinRate 加 tideBossBonus
480→ - achievements.tsach_tides_all 阈值6→9page.tsx StatsPanel 星潮亲历 /9
481→- QA 验证:
482→ - lint 零错误(exit 0+ dev HTTP 200 + 编译<30ms
483→ - BeaconPanel VLM 8.5/10(限时区块可见)+ 最终 9/10
484→ - 三新星潮 localStorage 注入触发截图 VLMsurge 8/10、prism 7/10、eclipse 8/10
485→ - 日/周/信标链/6旧星潮全部保留无回归
486→
487→Stage Summary:
488→- ✅ beacon.ts +289行(限时挑战完整系统:4h时段+确定性+进度+领奖+排行榜)
489→- ✅ starTide.ts +80行(3新类型 surge/eclipse/prism + 3新修饰器字段)
490→- ✅ decode.ts generatePuzzle 支持 targetLenBonussurge 谜题+2
491→- ✅ gameStore.ts trackBeacon 三进度同步 + claimTimedBeacon + 3处新星潮修饰器
492→- ✅ BeaconPanel.tsx +191行(amber 限时区块 + 紧急状态 + TIMED 徽章)
493→- ✅ 严格四色规范:限时amber/surge-emerald/eclipse-rose/prism-fuchsia,零蓝色
494→- ✅ 确定性:限时挑战同时段同种子同结果(FNV-1a slotKey 哈希)
495→- ✅ lint 零错误 + dev HTTP 200 + VLM 全部≥7/10
496→
497→---
498→Task ID: 10-b
499→Agent: full-stack-developer
500→Task: P2/#4 云排行榜 mini-service(信标本机榜升级为云端 Top100)
501→
502→Work Log:
503→- 前置阅读:worklogv0.8.1 项目状态)+ beacon.tsBeaconScoreEntry / loadLeaderboard / pushLeaderboardEntry / claimBeaconReward / claimWeeklyReward / claimTimedReward+ BeaconPanel.tsx(排行榜 UI 渲染)+ Caddyfile?XTransformPort=端口号 转发规则)+ examples/websocketmini-service 参考)+ .zscripts/dev.shmini-service 启动模式)
504→- 发现 worklog 信息滞后:beacon.ts 实际已扩到 1161 行(v0.8.2 已加限时挑战 + claimTimedReward),BeaconPanel 已扩到 953 行
505→- 创建 mini-service
506→ - `mini-services/leaderboard-service/package.json`:独立 bun 项目,type=modulescripts `dev: bun --hot index.ts` + `start: bun index.ts`,依赖 hono ^4.6.14
507→ - `mini-services/leaderboard-service/index.ts`182 行):
508→ - Hono + cors 中间件(origin: * 全开放,跨端口必须)
509→ - 内存数组存储(最多 1000 条,按 score 降序,同分按时长→时间戳排序)
510→ - 防刷:同 dateKey+challenge 10s 内只接受 1 次提交(Map 记录 lastSubmitAt),返回 429 + retryAfterMs
511→ - GET /api/leaderboard → Top100 + total
512→ - POST /api/leaderboard body {entry} → 校验 schema → 推入 → 排序 → 返回 {entries, total, rank}
513→ - GET /api/leaderboard/stats → {totalSubmissions, uniquePlayers, topScore}
514→ - GET / → 健康检查 {service, version, ok, uptime}
515→ - 端口固定 3030`export default { port, fetch }` 标准 bun 模式
516→ - `mini-services/leaderboard-service/README.md`API 文档 + 联调规范
517→- mini-service 启动调试:bun --hot 在本沙盒环境下会因文件 watcher 异常被 kill;改用 `bun index.ts`(无 --hot+ 双 fork `( ... & )` 模式启动,PPID=1uptime 4+ 分钟稳定
518→- eslint.config.mjs 新增 ignores "mini-services/**"mini-service 独立项目,不参与主项目 lint)
519→- beacon.ts 扩展(1161 → 1290 行,+129 行):
520→ - 三个 claim 函数返回值新增 `entry: BeaconScoreEntry`(非破坏性扩展):
521→ - `claimBeaconReward` 返回 `entry`daily
522→ - `claimWeeklyReward` 返回 `entry`weekly
523→ - `claimTimedReward` 返回 `entry`timed
524→ - 新增云排行榜模块(v0.8.2 P2/#4):
525→ - `BEACON_CLOUD_PORT = 3030` 常量
526→ - `BEACON_CLOUD_LAST_SUBMIT_KEY = "echo-nexus-beacon-cloud-last-submit-v1"` 本地 key
527→ - `CloudLeaderboardResponse` / `CloudSubmitResponse` / `CloudStatsResponse` 接口
528→ - `loadLastCloudSubmitTimestamp()` / `saveLastCloudSubmitTimestamp(ts)` 工具
529→ - `fetchCloudLeaderboard(): Promise<BeaconScoreEntry[]>` — GET 相对路径 + ?XTransformPort=3030
530→ - `submitCloudScore(entry): Promise<number>` — POST,成功返回 rank,失败返回 -1,自动记录 ts 供全球榜高亮
531→ - `fetchCloudStats(): Promise<CloudStatsResponse | null>` — 全局统计
532→ - 所有 fetch 用相对路径 + `?XTransformPort=3030`**禁止** localhost:3030
533→- gameStore.ts 集成:
534→ - import 新增 `submitCloudScore`
535→ - `claimWeeklyBeacon` action:领奖成功后 `void submitCloudScore(res.entry)`fire-and-forget,不 await,不阻塞)
536→ - `claimTimedBeacon` action:同上
537→ - `claimBeaconReward` (daily) 由 BeaconPanel 直接调用,不通过 gameStore
538→- BeaconPanel.tsx 重写(953 → 1170 行,+217 行):
539→ - 新增 import: fetchCloudLeaderboard / submitCloudScore / loadLastCloudSubmitTimestamp + 4 个 lucide 图标(RefreshCw/Globe/WifiOff/Loader2
540→ - 新增状态: lbTab ('local'|'global') / cloudEntries / cloudLoading / cloudError / cloudFetched / mySubmitTs / cloudFetchingRef
541→ - 新增 `refreshCloudLeaderboard()` callback:拉取云端榜 + 防并发(ref)+ 空 entries 时显示提示
542→ - useEffect: 切到全球 tab 自动拉取(仅首次)+ 初始化读取 mySubmitTs
543→ - handleClaimDaily: 领奖成功后 `void submitCloudScore(res.entry).then(rank => ...)` — 若用户在全球 tab 自动刷新
544→ - handleClaimTimed/Weekly: 领奖后更新 mySubmitTs + 若在全球 tab 自动刷新
545→ - 排行榜 UI 重写为双 tab:
546→ - 顶部「本地」(amber 主题) / 「全球」(emerald 主题) 切换按钮 + 全球 tab 专属刷新按钮(旋转动画)
547→ - 子标题:本地"本机 Top N · 离线可用" / 全球"全球 Top N · 已同步 / 同步中…"
548→ - 本地榜保留原 v0.8.2 渲染(max-h-200px overflow-y-auto
549→ - 全球榜:
550→ - 加载中:Loader2 spinner + "正在拉取全球榜…"
551→ - 空榜+错误:WifiOff 图标 + 错误文案 + 重试按钮
552→ - 空榜+无错:Globe 图标 + "尚无全球记录"提示
553→ - 有数据:渲染前 100 条,每条带排名(1-3 名用 Crown/Medal/Award 图标)/ 难度色 / WEEK·TIMED 徽章 / 用时 / 分数
554→ - 玩家自己的记录通过 timestamp 匹配 mySubmitTs 高亮:emerald 边框 + emerald 阴影 + YOU 徽章 + emerald 分数色 + 加粗排名
555→- QA 验证:
556→ - `bun run lint` 零错误零警告(eslint.config.mjs 把 mini-services 加入 ignores
557→ - dev.log 全程无错误,所有编译 < 300ms
558→ - mini-service curl 全通过:
559→ - GET / → 200 {service, version, ok, uptime}
560→ - GET /api/leaderboard (空) → 200 {entries:[], total:0}
561→ - POST /api/leaderboard → 200 {entries, total, rank:1}
562→ - GET /api/leaderboard (有数据) → 200 {entries:[...], total:N}
563→ - GET /api/leaderboard/stats → 200 {totalSubmissions, uniquePlayers, topScore}
564→ - OPTIONS 预检 → 204 + 完整 CORS 头(Allow-Origin: *
565→ - 重复 POST(同 dateKey+challenge 10s 内)→ 429 {error:"Rate limited", retryAfterMs}
566→ - 网关路由验证:Caddy :81 → localhost:3030 通过 ?XTransformPort=3030 正确转发,返回 JSON 200
567→ - agent-browser 集成测试(通过 :81 端口访问,绕过 Next.js 404):
568→ - 信标 tab 选中(用 KeyboardEvent Enter 激活 Radix Tabs,因 pointerdown 被覆盖层拦截)
569→ - 切到「全球」tab → 自动拉取 → 显示"全球 Top 6 · 已同步" → 渲染 6 条记录
570→ - 刷新按钮可点击,旋转动画正常
571→ - POST 测试:浏览器 fetch 提交 score=9999 → 返回 rank=1 → 刷新后榜首显示 10.00K
572→ - YOU 高亮:手动设置 localStorage[BEACON_CLOUD_LAST_SUBMIT_KEY] 后刷新 → 榜首显示 emerald 边框 + YOU 徽章
573→ - VLM 视觉评分(目标 ≥7/10):
574→ - 全球榜含 YOU 高亮:**8/10**(双tab切换清晰,全球榜记录完整,难度色与标签规范,视觉层次佳)
575→ - 本地榜:**8/10**(双tab切换逻辑清晰,本地tab内容完整,四色规范应用,信息层级合理)
576→
577→Stage Summary:
578→- ✅ mini-service 独立项目:自己的 package.json + bun.lock + node_modules,依赖 hono ^4.6.14,不污染主项目
579→- ✅ 端口 3030 + Caddy 网关:前端用相对路径 `fetch('/api/leaderboard?XTransformPort=3030')`**禁止** localhost:3030
580→- ✅ CORS 全开放:origin: * + OPTIONS 预检自动处理,前端跨端口无忧
581→- ✅ 内存存储 + 防刷:1000 条上限,同 dateKey+challenge 10s 限 1 次
582→- ✅ 三个 claim 入口全部接入云提交:claimBeaconRewardBeaconPanel 直接调)/ claimWeeklyBeacon / claimTimedBeacongameStore action 内)
583→- ✅ 本地榜保留:loadLeaderboard / pushLeaderboardEntry 未删除,作为离线 fallback
584→- ✅ 双 tab UI:本地(amber 主题,立即可用)/ 全球(emerald 主题,按需拉取)+ 刷新按钮 + 加载态 + 错误重试
585→- ✅ 玩家高亮:通过 timestamp 匹配 localStorage[BEACON_CLOUD_LAST_SUBMIT_KEY]emerald 边框 + YOU 徽章
586→- ✅ lint 零错误 + dev HTTP 200 + VLM 8/10
587→- ✅ mini-service 启动稳定:uptime 4+ 分钟,PPID=1(双 fork 模式)
588→- 下一阶段工单:P3 #10-c 全球星潮同步(star-tide-service 已在 mini-services 目录中)
589→
590→---
591→Task ID: 10-c
592→Agent: full-stack-developer
593→Task: #9 手写叙事节点 + P3 socket 多人同步星潮
594→
595→Work Log:
596→- 前置阅读:worklog.mdv0.8.1,四色规范 emerald/rose/amber/fuchsia,禁止蓝色,10 大系统)+ chronicle.ts680 行)+ starTide.ts297 行,9 种星潮)+ gameStore.ts1332 行)+ StarTide.tsx170 行)+ StarTideOverlay.tsx + Caddyfile:81 网关 ?XTransformPort 转发规则)+ 前序 10-a/10-b 工单(限时挑战/3 新星潮/云排行榜 mini-service
597→- **状态确认**:本任务为重试,发现上次重试已落地主体代码,本次为验证 + 补 QA + 启动 mini-service + 追加 worklog
598→- **工单 #9 手写叙事节点**chronicle.ts 已就绪):
599→ - EPOCH_LORE 常量:5 纪元 × 3 节点(opening/middle/ending= 15 段手写叙事,每段 80-150 字文学性
600→ - 5 纪元主题:1 觉醒(emerald/2 谐振(rose/3 遗迹(amber/4 飞升(fuchsia/5 终末
601→ - 每段含 fallback + variants(按 ctx 命中条件挑变体):
602→ - 第一纪元:解码者(≥15)/学者(techs≥5)/里程碑密集(≥2) + 深度轮回(asc>5)
603→ - 第二纪元:探险者(≥3)/解码者(≥15)/觉醒天赋 + 深度轮回
604→ - 第三纪元:BOSS 猎手(≥2)/深度探险(≥4)/星潮亲历(≥3)/BOSS 收尾 + 深度轮回
605→ - 第四纪元:觉醒天赋/蓝图接近完整(≥5)/里程碑密集 + 深度轮回
606→ - 第五纪元:里程碑密集(≥3)/星潮汹涌(≥3)/觉醒天赋 + 深度轮回
607→ - 变量替换:{decodedThisRun}/{techsThisRun}/{expThisRun}/{bossKillsThisRun}/{blueprintsAfter}/{milestonesCount}/{milestonesList}/{tideCount}/{tideNames}/{perksList}/{minutes}
608→ - buildLore 改造:优先 buildHandwrittenLore,无匹配(ascensionNumber 异常)时 fallback 到 v0.4 模板拼接
609→ - 验证:bun 脚本测试 5 纪元 × 2 上下文(minimal/rich= 10 个 case 全通过,文字长度 233-273 字(含收尾段),变体匹配正确,变量替换无残留 {xxx}
610→- **P3 socket 多人同步星潮**
611→ - **mini-servicemini-services/star-tide-service/**
612→ - 独立 bun 项目,package.json type=module + scripts dev:bun --hot index.ts + start:bun index.ts,依赖 socket.io ^4.8.3
613→ - index.ts250 行):
614→ - 端口固定 3031path:"/"Caddy 据此转发)
615→ - 6 种星潮类型表(crystal/resonance/ruins/void/core/silence+ 权重表 + rollTide
616→ - DURATION_SEC=60s 全球星潮持续;FIRST_TIDE_DELAY_MS=60sQA 友好,env 可覆盖);MIN/MAX_GAP_MS=10-15 分钟随机间隔
617→ - 调度:startTide → io.emit("global-tide", {type,name,startedAt,endsAt,durationSec}) → 60s 后 endTide → io.emit("tide-ended", {endedAt,nextTideAt}) → scheduleNextTide10-15 分钟随机)
618→ - 客户端连接:socket.emit("tide-state", {current, nextTideAt, serverTime}) 立即推送
619→ - 客户端可 emit "get-next-tide" → 服务端响应 "next-tide-info" {current, nextTideAt, serverTime, remainingMs}
620→ - dev/QA 用 "admin-trigger-tide":立即触发一次全球星潮(不等待下次定时器)
621→ - 优雅退出:SIGTERM/SIGINT → 清 timer → io.close → httpServer.close → exit(0)
622→ - README.mdAPI/事件文档
623→ - **前端集成**
624→ - src/hooks/useGlobalTide.ts127 行):
625→ - 单例 socketsharedSocket + connectRefCount 引用计数,多组件挂载共享同一连接)
626→ - io("/?XTransformPort=3031", {transports:["websocket","polling"], reconnection:true, reconnectionAttempts:Infinity})
627→ - 监听 "global-tide" → triggerRef.current(type),用 startedAt+type 去重(防重连重复触发),过期 tide 不触发
628→ - 监听 "tide-state" → 连接/重连时若服务端正有进行中的全球星潮,补触发
629→ - 主动 emit "get-next-tide" 保险
630→ - 导出 adminTriggerGlobalTidedev 控制台 QA 用),window.__adminTriggerGlobalTide 暴露
631→ - gameStore.ts 新增 triggerGlobalTide action + globalTide state
632→ - GLOBAL_TIDE_DURATION_MS = 60_000 常量
633→ - GlobalTideState 类型 {type, startedAt, endsAt, id}
634→ - TideEvent 新增 isGlobal?: boolean 字段
635→ - triggerGlobalTide(type):创建 newTide60s+ newGlobalTide,覆盖本地 activeTide(即使本地有进行中的星潮也会被替换),加入 _tideEvents 队列 with isGlobal:true
636→ - tickTide 改造:globalTide 优先 → 全球星潮进行中本地不触发新星潮;全球星潮结束时设 globalTide:null + activeTide:null + lastTideEnd + silenceCompensation + ended 事件 with isGlobal:true
637→ - partialize 排除 globalTide(瞬态运行时状态,不持久化)
638→ - page.tsx 顶层挂载 useGlobalTide()
639→ - StarTide.tsx UI 升级:
640→ - StarTideIndicator:读 globalTide → isGlobal 标记 → 加 🌐 emoji + ring-1 ring-amber-300/60 + 强化 boxShadow + 渐变背景 + title 改为 "🌐 全球星潮 · {meta.desc}"duration 用 globalTide.endsAt-startedAt 计算(60s)避免越界
641→ - StarTideOverlay:全球星潮多一道顶部琥珀色光带 + global-tide-sweep 2.5s 扫光动画 + inset boxShadow 加深(160px vs 120px
642→ - StarTideNotifiertoast 文案区分全球/本地("🌐 全球星潮降临:{name}" + "全球玩家同步经历 · {desc}"
643→- **QA 验证**
644→ - `bun run lint` 零错误零警告
645→ - dev.log 全程无错误,编译 < 70msHTTP 200
646→ - mini-service 启动稳定:pid 9456PPID=1(双 fork 脱离 dev.sh),uptime 19+ 分钟,端口 3031 监听正常
647→ - curl http://localhost:3031/ → {"code":0,"message":"Transport unknown"}socket.io-only 服务预期响应)
648→ - curl http://localhost:3031/socket.io/?EIO=4&transport=polling → 正确握手 {"sid":...,"upgrades":["websocket"]}
649→ - 网关路由验证:curl http://localhost:81/socket.io/?EIO=4&transport=polling&XTransformPort=3031 → 通过 Caddy 转发到 3031,返回正确握手
650→ - socket.io 端到端测试(bun 脚本 /tmp/test-socket.ts):connect ✅ → tide-state ✅ → emit admin-trigger-tide → 504ms 内收到 global-tide {type:ruins, startedAt, endsAt, durationSec:60} ✅
651→ - agent-browser 集成测试(通过 http://localhost:81/ 走 Caddy 网关,socket.io 才能正确路由):
652→ - 页面加载 200window.__adminTriggerGlobalTide 已挂载(useGlobalTide hook 已挂载)
653→ - 调用 adminTriggerGlobalTide() → 1-3s 内 StarTideIndicator 渲染 🌐 芯片
654→ - DOM 验证:芯片文本 "🌐✷虚空低语57s"title "🌐 全球星潮 · 虚空传来回响,洞见获取翻倍"(isGlobal:true 路径生效)
655→ - 第二次触发:"🌐⬢遗迹共振59s"title "🌐 全球星潮 · 远古遗迹苏醒,探险力 +5、生命 +30"
656→ - 全球星潮结束(60s 后)→ activeTide:null + globalTide:null + lastTideEnd 更新 → 本地 tickTide 恢复正常节奏
657→ - VLM 视觉评分:**8/10**(确认 🌐 地球图标 + 汉字名称"遗迹共振" + 倒计时"59s" 全部可见;颜色 emerald/amber/fuchsia/rose 四色规范,零蓝色)
658→ - 编年史 buildLore 验证(bun 脚本测试 5 纪元 × 2 上下文 = 10 case):
659→ - 全部产出 233-273 字文学性叙事,包含开场+中段+结尾+收尾段
660→ - fallback 路径:默认文本,无变量替换
661→ - variant 路径:按 ctx 命中条件挑变体,{decodedThisRun}/{bossKillsThisRun}/{milestonesList}/{perksList} 等变量正确替换
662→ - 纪元名生成正确:第一纪元·初鸣之夕 / 第二纪元·光谱涌动 / 第三纪元·星辉汇聚 / 第四纪元·以太共振 / 第五纪元·永恒闭环
663→
664→Stage Summary:
665→- ✅ 工单 #9chronicle.ts EPOCH_LORE 5 纪元 × 3 节点 = 15 段手写叙事(每段 80-150 字文学性),buildLore 优先手写节点 + fallback 模板,buildChronicleEntry/regenerateLoreFromEntry/withPerks 全部接入;10 case 测试通过,变体匹配 + 变量替换正确
666→- ✅ P3 socket 多人同步星潮:mini-services/star-tide-service 独立 bun 项目,socket.io 端口 3031,每 10-15 分钟随机广播 global-tide60s 持续),admin-trigger-tide 供 QA 触发;前端 useGlobalTide hook + gameStore.triggerGlobalTide + globalTide 状态 + tickTide 全球优先 + StarTide UI 🌐 标记 + toast 区分全球/本地
667→- ✅ 网关规范:socket.io 严格用 `io("/?XTransformPort=3031")`path "/",禁止 localhost:3031 直连;Caddy :81 → localhost:3031 转发正确
668→- ✅ 离线容错:socket.io 自动重连(Infinity attempts),断连期间本地 tickTide 继续按原节奏工作,玩家不会被卡住;globalTide 不持久化(瞬态运行时状态)
669→- ✅ 防重连重复触发:useGlobalTide 用 startedAt+type 去重(Set 上限 32 条 FIFO 清理),过期 tide 不触发
670→- ✅ 本地星潮保留:全球星潮是增强而非替换,全球星潮结束后本地 tickTide 恢复正常节奏
671→- ✅ mini-service 启动稳定:pid 9456PPID=1uptime 19+ 分钟,端口 3031 监听正常
672→- ✅ lint 零错误 + dev HTTP 200 + VLM 8/10(🌐 全球星潮标记清晰可见)
673→- ✅ 严格四色规范:全球星潮琥珀色扫光(amber)+ 各星潮本色(emerald/rose/amber/fuchsia),零蓝色
674→- 下一阶段工单:可考虑 v0.9 收尾(全球星潮成就/编年史全球星潮高亮/星图天文台全球星潮增强天赋)
675→
676→---
677→Task ID: 10
678→Agent: main + subagent(10-a/10-b/10-c)
679→Task: v0.8.2 工单大清理 — 7 项工单一次性清零
680→
681→Work Log:
682→- 重读工单发现两类列表(已知问题10项 + 下一阶段优先级7项)多项未解决
683→- #8 编年史上限+分页(main 自己做):
684→ - engine.ts: slice(-50)→slice(-200)
685→ - ChronicleDialog.tsx: 加分页(每页10条)+上一页/下一页+页码显示
686→- #5 限时挑战 + #3 星潮深化(subagent 10-a 并行):
687→ - 限时挑战: BeaconTimedChallenge + 4h时段 + amber主题 + <30min紧急状态
688→ - 星潮深化: +3种(surge/eclipse/prism) + 修饰器扩展 + ach_tides_all 6→9
689→- #4/P2 云排行榜(subagent 10-b 并行):
690→ - mini-service 端口3030 Hono+bun 内存1000条 + CORS + 防刷
691→ - 前端双tab(本地/全球) + YOU徽章 + 自动云提交
692→- #9 手写叙事 + P3 socket星潮(subagent 10-c:
693→ - 5纪元×3节点=15段手写叙事(80-150字) + 11变量替换 + fallback模板
694→ - mini-service 端口3031 socket.io 每10-15min广播 + useGlobalTide + 🌐标记
695→- P2 UI打磨(main 自己做):
696→ - CrystalOrb区: 3层旋转全息环 + 四角标记 + 顶部状态条 + 底部铭文
697→- 最终 QA: lint零错误 + dev HTTP200 + VLM 8/10 + 2 mini-service运行中(3030/3031)
698→- 提交 v0.8.2commit 191434e+ 推送 main + 部署 gh-pages7e0a59bHTTP 200
699→
700→Stage Summary:
701→- ✅ 工单 #3 星潮深化: 6种→9种(+surge/eclipse/prism)
702→- ✅ 工单 #4 云排行榜: 本地→云端(mini-service 3030)
703→- ✅ 工单 #5 限时挑战: 每4h一个额外挑战(amber主题)
704→- ✅ 工单 #8 编年史: 上限50→200 + 分页UI
705→- ✅ 工单 #9 手写叙事: 5纪元×3节点=15段文学性叙事
706→- ✅ P2 UI打磨: CrystalOrb留白→装饰全息环+四角标记+状态条
707→- ✅ P3 socket多人星潮: mini-service 3031 + 🌐全球星潮
708→- 剩余未解决: #1 Turbopack缓存(环境问题) / #2 定时任务(已文档化)
709→- 在线游玩 v0.8.2: https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/
710→
711→---
712→Task ID: 1
713→Agent: 主代理 (Phase 1 Bug 修复)
714→Task: 获取 Gitea 工单并修复 Issue #2 的 4 个 bug
715→
716→Work Log:
717→- 通过 Gitea API 获取到 2 个未解决工单(Issue #1 + #2
718→- Issue #2 详细分析了 4 个 bug + 4 个玩法问题
719→- Bug1 探险只能一次:能量恢复间隔 45→20s,初始能量 3→5,上限 5→8,新增 lastExpeditionSummary 状态记录结果,ExpeditionPanel 入口显示上次结果摘要 + 实时倒计时,新增 dismissExpedition action
720→- Bug2 元素偏移/遮挡:标签页 grid-cols-8 → 响应式 grid-cols-4/sm:grid-cols-8,小屏 2 行避免拥挤
721→- Bug3 成就UI遮挡:标签面板 max-h 440→520min-h 提高
722→- Bug4 提示框遮挡:toast viewport top-0→top-14 避开 headerTOAST_LIMIT 1→3
723→- lint 通过,编译正常
724→
725→Stage Summary:
726→- Phase 1 四个 bug 全部修复完成
727→- 探险系统现在有完整的状态反馈:能量实时倒计时、上次结果摘要、刚结束时的结算按钮
728→- 标签页移动端不再挤压
729→- 待 Phase 2:技术树扩充、晶体球信息卡、成就特效、叙事融入
730→
@@ -1,786 +0,0 @@
1→# 回响星核 / Echo Nexus — 开发工作日志
2→
3→> 持续性任务的工作交接文档。每个阶段更新,避免进度丢失。
4→
5→---
6→
7→## 一、项目当前状态描述 / 判断
8→
9→### 概况
10→- **项目**:回响星核 / Echo Nexus — 深空考古放置策略游戏
11→- **一句话**:自治无人机采矿 → 解码记忆晶体(原创共振谜题)→ 拼凑碎片叙事 → 飞升多周目。差异化:解码谜题驱动叙事涌现,全息晶体 + 深空粒子美学。
12→- **当前版本****v0.7**CrystalOrb Canvas 粒子系统 + 角色属性系统)
13→- **在线游玩**https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/
14→- **仓库**https://git.atdunbg.xyz/Super_Z/echo-nexus
15→- **技术栈**Next.js 16 (App Router) + TypeScript + Tailwind + shadcn/ui + Canvas 2D + Zustand + Web Audio API
16→- **定时任务**:每 15 分钟一次 `webDevReview``fixed_rate` + `"900"` 秒,priority=10job_id 228266)。正常完成不会被删除,无需自持续机制。
17→
18→### 状态判断
19→- dev 服务器运行正常(HTTP 200,编译 < 250ms
20→- v0.7 两大新功能已完成:CrystalOrb Canvas 粒子系统升级(VLM 8/10+ 角色属性系统(VLM 7/10
21→- 系统总体稳定,无阻塞性 bug,可继续推进新功能
22→
23→### 已完成版本里程碑(精简)
24→| 版本 | 核心内容 |
25→|------|---------|
26→| v0.1 | MVP:放置采矿 + 解码共振谜题 + 技术树 + 记忆图谱 + 飞升 + 统计 |
27→| v0.1.1 | 解码可解性修复(路径构造法生成器,100% 可解率)|
28→| v0.2 | 遗迹探险肉鸽系统(能量/探险力/6 节点/程序化路径/BOSS)|
29→| v0.2.1 | 程序化音频(15 种音效)+ 14 项成就 + 视觉打磨 |
30→| v0.3 | 星潮事件系统(6 种星潮 + 全屏叠层 + 程序化音效)|
31→| v0.3.1 | 星图天文台元进程(6 类 × 3 = 18 天赋 + 飞升后 3 选 1 draft + Canvas 星图)|
32→| v0.4 | 回响编年史(5 纪元循环命名 + 模板化叙事 + 时间轴 UI + 6 项新成就 + 飞升 BUG 修复)|
33→| v0.5 | 深空信标(每日挑战 + 本地排行榜 Top20 + 7 标签页 + 编年史历史 BUG 修复)|
34→| v0.5.1 | 静态部署 Gitea Pages + 新手教程系统(7 步聚光灯引导)+ UI 间距优化 |
35→| v0.5.2 | 离线收益报告弹窗 + 标签页 UI 重设计 + persist rehydrate 竞态修复 |
36→| v0.6 | 深空巡航 Canvas 2D 实时玩法 + UI 偏移重叠修复 |
37→| **v0.7** | **CrystalOrb Canvas 粒子系统升级 + 角色属性系统(四维)** |
38→
39→### 核心系统清单(8 大系统)
40→1. **放置采矿** — 中央晶体球 + 主动脉冲 + 连击加成(`CrystalOrb.tsx`
41→2. **谐振解码** — 原创路径谜题,路径构造法 100% 可解(`DecodeArray.tsx` + `engine.ts`
42→3. **技术树** — 三层科技解锁,提升产能/解码/探险(`TechTree.tsx`
43→4. **遗迹探险** — 肉鸽系统,6 种节点 + 程序化路径 + BOSS`ExpeditionPanel.tsx` + `expedition.ts`
44→5. **星潮事件** — 6 种动态事件,为放置循环注入变化(`StarTide.tsx` + `starTide.ts`
45→6. **星图天文台** — 飞升后 3 选 1 天赋 draft,跨周目永久生效,18 个天赋(`ConstellationPanel.tsx` + `constellation.ts`
46→7. **回响编年史** — 跨周目叙事时间轴,5 纪元循环命名(`ChronicleDialog.tsx` + `chronicle.ts`
47→8. **深空信标** — 每日挑战 + 本地排行榜 Top20`BeaconPanel.tsx` + `beacon.ts`
48→9. **深空巡航** — Canvas 2D 实时玩法,飞船操控/陨石躲避/星门通关(`CruiseMode.tsx` + `cruise.ts`)【v0.6】
49→10. **角色属性系统** — 四维属性(探索力/智慧/勇气/灵感),飞升获得属性点,影响所有系统(`AttributesPanel.tsx` + `attributes.ts`)【v0.7 新增】
50→
51→---
52→
53→## 二、当前目标 / 已完成的修改 / 验证结果
54→
55→### v0.7 CrystalOrb Canvas 粒子系统升级(已完成)
56→**动机**:P0 优先级 — 将中央晶体球从 CSS 动画升级为 Canvas 粒子视觉盛宴,提升视觉震撼。
57→
58→**重写文件**`src/components/game/CrystalOrb.tsx`
59→- 从纯 CSS 动画升级为 Canvas 2D 粒子系统
60→- **多层粒子**
61→ - 环绕能量粒子(3层14/10/8个,emerald/fuchsia/rose三色,不同速度方向环绕晶核,带拖尾)
62→ - 环境星尘(40个,缓慢漂移 + 闪烁)
63→ - 点击爆发粒子(10-30个径向发散,颜色随连击等级变色)
64→ - 冲击波环(点击触发1-3层错峰扩散,连击≥3/5解锁更多层)
65→- **晶核升级**:径向渐变辉光 + 3层六边形纹理旋转 + 高光 + 鼠标视差偏移
66→- **呼吸动画**:晶核 + 外层辉光随时间呼吸(sin波)
67→- **进度环**SVG渐变环(emerald→fuchsia→rose)保留
68→- **交互保留**:点击脉冲、连击Toast、浮动数字、音效、`data-tut="crystal-orb"` 锚点
69→- **性能**DPR cap 2ResizeObserver 自适应,requestAnimationFrame 60fps
70→
71→**QA 验证**agent-browser + VLM):
72→- VLM 视觉评分 **8/10**:晶体球清晰可见,多层辉光,环绕粒子动态自然,无渲染问题
73→- 点击交互:冲击波环 ✅ + 爆发粒子 ✅,评分 7/10
74→- lint 零错误;HTTP 200
75→
76→### v0.7 角色属性系统(已完成)
77→**动机**:P1 优先级 — 增加RPG深度,四维属性影响所有游戏系统。
78→
79→**新增文件**
80→- `src/lib/game/attributes.ts`~330行逻辑层):CharacterAttributes/AttributeProgress 类型、ATTRIBUTE_CONFIG 元信息、getAttributeBonus 两区加成公式(0-50线性+0.5%/点,50-100递减+0.2%/点)、getAllBonuses 12项修饰器、levelUpCheck 自动升级、computePrestigeAttrPoints、migrateAttributes 旧存档兼容
81→- `src/components/game/AttributesPanel.tsx`~330行 UI):四维卡片网格(小屏2×2/大屏1×4)、图标+中英文名+Lv.badge+数值/100+加成%+经验条+影响列表+分配按钮、顶部待分配点数badge闪烁、底部总览+12项修饰器明细
82→
83→**修改文件**
84→- `types.ts`GameState 新增 attributes/attributeProgress/pendingAttrPoints
85→- `config.ts`INITIAL_STATE 补全默认值
86→- `engine.ts`recomputeStats 聚合属性加成;performPrestige 发放属性点(ascensions×2+1
87→- `gameStore.ts`:新增 allocateAttribute/gainAttributeExp actionspulse/clickNode/autoDecodeTick/resolveCurrentNode/grantCruiseReward/tickTide 全部接入属性经验;init() 调用 migrateAttributes
88→- `achievements.ts`:新增 ach_attr_total_50 + ach_attr_max_100 两项成就
89→- `page.tsx`grid-cols-7→8,新增「角色」标签页(User图标),红点提示,StatsPanel新增5行属性行,版本号 v0.6→v0.7
90→
91→**四维属性设计**
92→- 探索力(emerald):探险力+X%/巡航飞船速度+X%
93→- 智慧(fuchsia):解码步数+X/洞见+X%/自动解码周期-X%
94→- 勇气(amber):探险生命+X/BOSS胜率+X%/巡航护盾+X
95→- 灵感(rose):接触率+X/星潮概率+X/脉冲连击+X%
96→
97→**QA 验证**agent-browser + VLM):
98→- VLM 视觉评分 **7/10**(面板)/ **8/10**(待分配状态):四维卡片可见、数值/等级/加成/经验条齐全、四色全息
99→- 功能验证:setState pendingAttrPoints=5 → 显示5点待分配 → 点击分配按钮 → exploration 0→1, pending 5→4 ✅
100→- lint 零错误;HTTP 200
101→
102→---
103→
104→### v0.6 深空巡航 Canvas 2D 实时玩法(历史记录)
105→**动机**:回应用户反馈「玩法太单调(全是点点点)」+「视觉震撼不足」——首次引入实时操作玩法。
106→
107→**新增文件**
108→- `src/lib/game/cruise.ts`~520 行逻辑层)
109→ - 类型系统:CruiseEntity 联合类型(ship/asteroid/storm/crystal/insight/beacon/stargate/particle 共 8 种)
110→ - mulberry32 + FNV-1a 种子化 RNG`cruiseSeed(level)`
111→ - `generateLevel`:程序化生成陨石带(8+lvl×2)、虚空风暴(1+lvl/2)、晶体碎片(5+lvl)、洞见光球(2+lvl/3)、信标(1+lvl/4)、星门
112→ - `updateCruise`:物理推进 + 碰撞检测 + 粒子系统 + 震动 + 600ms 无敌帧
113→ - `computeRewards`crystals=碎片×(8+lvl×2)、insights=洞见×(2+lvl×0.5)、contact=信标×(1.5+lvl×0.3);通关×1.5、失败×0.5
114→ - 独立 localStorage `echo-nexus-cruise-v1`:最高分/累计通关/最高关卡/累计奖励/最近 20 局
115→
116→- `src/components/game/CruiseMode.tsx`~830 行渲染层)
117→ - 全屏 fixed inset-0 z-50 CanvasDPR cap 2resize 监听
118→ - 3 层视差星空:远(100星)/中(60星)/近(30星),独立漂移 + 飞船速度视差 + 闪烁
119→ - 8 种实体全部 ctx.shadowBlur 辉光绘制
120→ - 粒子系统:尾焰/收集/碰撞/烟花,上限 200
121→ - 屏幕震动 280ms 衰减;低护盾(<30%)红色边框脉冲
122→ - HUDHTML 叠层,glass+backdrop-blur80ms 节流):护盾/能量/分数/用时/收集计数
123→ - 控制:桌面 WASD/方向键 8 方向 + 移动端虚拟摇杆 + P 暂停 + Esc 退出 + 失焦自动暂停
124→ - 阶段:ready → playing → won/lost(结算明细 + 奖励 + 下一关/重试/返回)
125→
126→**修改文件**
127→- `src/app/page.tsx`header 新增 amber 主题「巡航」按钮(Navigation 图标)+ cruiseOpen state + 条件渲染 CruiseMode
128→- `src/store/gameStore.ts`:新增 `grantCruiseReward({crystals?, insights?, contact?})` actioncrystals 受 crystalCap 限制,contact 受 100 上限)
129→- 版本号 v0.5.2 → v0.6
130→
131→**UI 偏移/重叠 BUG 修复**(3 处):
132→- `DecodeArray.tsx`:移除 className 冲突的 `-translate-x-1/2 -translate-y-1/2`,统一用 inline transform 定位+缩放
133→- `AchievementsPanel.tsx`:移除内层 `max-h-[300px]`,改 `flex-1 min-h-0` 自适应父容器
134→- `page.tsx`:调整三个 section 的 min-h,避免小屏单列时总高度超过手机视口
135→
136→**色彩规范**:严格遵循 emerald(#34d399) / rose(#fb7185) / amber(#fbbf24) / fuchsia(#e879f9) 四色全息色谱,零蓝色/靛色
137→
138→**QA 验证**agent-browser + VLM):
139→- lint 零错误;dev 服务器 HTTP 200;编译 < 250ms
140→- VLM 确认主界面:巡航按钮存在、晶体球居中、解码区正常、无重叠偏移
141→- VLM 确认巡航玩法:飞船可见(绿色三角形)、HUD 完整、陨石+星空背景、粒子特效(黄色尾焰+紫色辉光)
142→- WASD 控制飞船移动,按 W 直冲星门 → 通关,奖励 +30 晶体(2×10×1.5=30 计算正确)
143→- localStorage 正确记录 highScore/totalRuns/totalWins/bestLevel
144→- 奖励同步 gameStoregrantCruiseReward,满仓时 cap 逻辑正确)
145→- 退出返回主界面,7 标签页 + 晶体球 + 解码面板均完好
146→
147→---
148→
149→## 三、未解决问题或风险 / 下一阶段优先事项
150→
151→### 已知问题 / 风险
152→1. **dev 服务器 Turbopack 缓存偶发损坏**`Unable to open static sorted file` + 进程被杀,需 `rm -rf .next` 重启。静态导出构建稳定可靠。
153→2. **定时任务调度类型**:必须用 `fixed_rate` + `"900"` 秒(15分钟),不要用 cron 表达式。`webDevReview` 类型任务正常完成不会被删除,只有抛出未捕获异常才会被标记失败。
154→3. **v0.3 星潮为单机版**:原计划 socket 全局事件,后续可扩展为多人同步。
155→4. **v0.5 信标排行榜为本地版**(纯 localStorage):升级云排行榜需后端 API。
156→5. **v0.5 每日挑战仅 1 个/天**:可加入"周挑战"或"信标链"(连续完成 N 天奖励)。
157→6. **探险 BOSS 战胜率较低**:基础探索力 10 vs BOSS 难度 5-7,胜率约 25%,需玩家投资探险技术。
158→7. **探险能量恢复较慢**:45s/点,可加技术提升恢复速度。
159→8. **编年史上限 50 条**:超过自动丢弃最早的;v0.4 之前飞升无回填(空状态有提示)。
160→9. **5 纪元叙事为模板化生成**:v0.5+ 将加入手写剧情节点。
161→10. ~~**CrystalOrb 仍为 CSS 动画**~~ → ✅ **v0.7 已完成 Canvas 粒子系统升级**。
162→
163→### 下一阶段优先级(v0.7 后修正版)
164→> 工单修正:原 P0 CrystalOrb Canvas + P1 角色属性两项均已在 v0.7 完成,现重新排序。
165→
166→1. **🔴 P0 — 巡航玩法大增强(v0.8 主线)**:BOSS 战关卡(每 5 关一个 BOSS,三阶段攻击模式 + 飞船射击系统)/ 道具掉落(5 种 powerup:护盾/能量/急速/超载/磁吸)/ 事件选择节点(每关通关后 3 选 1 强化牌,10 种强化)。这是当前最高优先级,能显著提升巡航玩法的深度与可玩性。
167→2. **🟠 P1 — 探险 BOSS 战胜率平衡 + 探险能量恢复**:修复已知问题 6/7(探险 BOSS 胜率 25% 偏低,能量 45s/点 偏慢),提升探险体验。
168→3. **🟠 P1 — 信标系统扩展**:周挑战 / 信标链(连续完成 N 天奖励),修复已知问题 5。
169→4. **🟡 P2 — UI 样式细节打磨**:标签页图标/文字比例、视觉层次强化、移动端响应式进一步优化、CrystalOrb 周边留白优化(VLM 指出主界面左侧有空白)。
170→5. **🟡 P2 — 云存档 + 云排行榜**:v0.5+ 后端 API(需评估是否引入 mini-service)。
171→6. **🟢 P3 — 全 5 纪元手写叙事节点**:替换模板化 lore。
172→7. **🟢 P3 — socket 多人同步星潮**:全局事件共享。
173→
174→### 定时任务(fixed_rate · 900s
175→- **当前 job_id**: **228357**2026-06-24 13:25 重建)
176→- **配置**: `fixed_rate` + `"900"`15分钟间隔),`priority=10`(高优先级),`payload.kind=webDevReview``tz=Asia/Shanghai`
177→- **任务名**: Echo Nexus - 任务审查与持续迭代
178→- **任务描述强制要求**:每次触发 MUST 先完整阅读 worklog.md 工单,以工单为准,不凭记忆工作
179→- **正常完成不会被删除**,只有未捕获异常会标记失败。若任务列表为空,需用 cron create 重建(schedule.kind=fixed_rate, expr="900", priority=10, payload.kind=webDevReview)。
180→- **job_id 历史**: 227909(失效) → 228130(失效) → 228172(失效) → 228186(失效) → 228218(删除重建) → 228236(cron版,删除重建) → 228266(fixed_rate,失效) → 228329(失效) → **228357(当前,重建)**
181→
182→---
183→
184→## 定时任务描述模板(webDevReview · fixed_rate
185→
186→> 重建参数:schedule.kind=`fixed_rate`, schedule.expr=`"900"`, params.priority=`10`, payload.kind=`webDevReview`
187→
188→```
189→请先判断当前项目状态,使用 agent-browser 进行必要测试和 QA,然后自主选择本轮最合适的工作重点并继续开发。
190→
191→要求:
192→1. 开始任务时查看 /home/z/my-project/worklog.md 了解实际进展,拆解详细todo
193→2. 使用 agent-browser 进行测试和QA,有bug优先修复
194→3. 如果稳定,自主提出新需求继续推进
195→4. 样式要越做细节越多!!
196→5. 功能要越做越多!!
197→6. 工作告一段落更新 /home/z/my-project/worklog.md
198→7. 项目结束后一定要把工作记录写在 worklog.md 里
199→
200→交接文档建议包含三部分:
201→- 项目当前状态描述/判断
202→- 当前目标/已完成的修改/验证结果
203→- 未解决问题或风险,建议下一阶段优先事项
204→```
205→
206→---
207→
208→## 历史详细版本记录(按需查阅 docs/repo/docs/
209→
210→- `04-解码系统修复-v0.1.1.md`
211→- `05-遗迹探险系统-v0.2.md`
212→- `06-音频与成就系统-v0.2.1.md`
213→- `07-星潮事件系统-v0.3.md`
214→- `08-星图天文台系统-v0.3.1.md`
215→- `09-回响编年史系统-v0.4.md`
216→- `10-深空信标系统-v0.5.md`
217→
218→---
219→
220→Task ID: 7
221→Agent: full-stack-developer
222→Task: 角色属性系统(探索力/智慧/勇气/灵感)
223→
224→Work Log:
225→- 阅读 worklog.md 了解 v0.6 项目背景,确认 8 大系统架构与四色全息色彩规范
226→- 新建 `src/lib/game/attributes.ts` 逻辑层(~330 行):
227→ - 定义 CharacterAttributes/AttributeProgress/AttributeKey 类型 + ATTRIBUTE_CONFIG 元信息
228→ - getAttributeBonus 线性+递减两区加成公式(0-50 每 10 点 +5%50-100 每点 +2%
229→ - getAllBonuses 返回 12 个修饰器(探险力/巡航速度/解码步数/洞见/自动解码周期/探险生命/BOSS 胜率/巡航护盾/接触率/星潮触发/脉冲连击/产能)
230→ - levelUpCheck 自动跨多级 + expRequiredForLevel + computePrestigeAttrPoints + migrateAttributes 兼容
231→- 扩展 `src/lib/game/types.ts` GameState:新增 attributes/attributeProgress/pendingAttrPoints
232→- 扩展 `src/lib/game/config.ts` INITIAL_STATE:补全默认值(0 数值 + 空 progress
233→- 扩展 `src/lib/game/engine.ts`
234→ - recomputeStats 末尾聚合 am.crystalsPerSecMult/insightMultAdd/contactRateMult/decodeStepsBonus
235→ - performPrestige 保留 attributes 数值,清空 attributeProgress,发放 ascensions × 2 + 1 点属性点
236→ - createInitialState 每次返回全新对象避免引用共享
237→- 扩展 `src/store/gameStore.ts`(核心改造,10+ 处 syncStats 调用全部补 attributes 参数):
238→ - pulse:连击 ≥3 给灵感经验 + 脉冲威力乘灵感连击加成
239→ - clickNode 完成:给智慧经验(tier × 2)
240→ - autoDecodeTick:智慧经验 +1 + 自动解码周期受 am.autoDecodeIntervalMult 影响
241→ - startExpedition:探险力乘 am.expeditionPowerMult,生命加 am.expeditionHpBonus
242→ - resolveCurrentNodeBOSS 节点包装 RNG 提升 +am.bossWinRateBonus 胜率;战斗胜利给勇气+探索力经验
243→ - grantCruiseReward:按总奖励量给探索力+勇气经验
244→ - tickTide:灵感 am.tideTriggerBonus 缩短星潮间隙(上限 30%)
245→ - init() 调用 migrateAttributes 补全旧存档
246→ - 新增 allocateAttribute / gainAttributeExp 两个 action
247→- 新建 `src/components/game/AttributesPanel.tsx` UI 面板(~330 行):
248→ - 四维卡片网格(小屏 2×2,大屏 1×4)
249→ - 每张卡片:图标 + 中英文名 + Lv.badge + 数值/100 + 加成% + 经验条 + 影响列表 + 「+分配」按钮
250→ - 顶部待分配点数 badgependingAttrPoints > 0 时 echo-pending-pulse 闪烁动画)
251→ - 底部总等级/总加成概览 + 12 个修饰器明细
252→ - 4 色辉光边框(emerald/fuchsia/amber/rose),顶角光晕装饰
253→- 接入 `src/app/page.tsx`
254→ - grid-cols-7 → grid-cols-8,新增第 8 个「角色」TabsTriggerUser 图标,渐变主题)
255→ - pendingAttrPoints > 0 时显示 rose 红点提示
256→ - 渲染 AttributesPanel
257→ - StatsPanel 新增 5 行属性行(探索力/智慧/勇气/灵感/待分配点)
258→ - 版本号 v0.6 → v0.7
259→- 新增 `src/lib/game/achievements.ts` 2 项成就:
260→ - ach_attr_total_50(四维觉醒):总属性 ≥ 50 → 产能+6%/洞见+6%
261→ - ach_attr_max_100(维度精通):任一属性 100 → 产能+12%/洞见+10%
262→- 写入 `agent-ctx/7-full-stack-developer.md` 工作记录
263→- 最终验证:lint 零错误 + dev 服务器 HTTP 200 + 编译 < 250ms
264→
265→Stage Summary:
266→- ✅ 角色属性系统 v0.7 P1 全部完成
267→- ✅ 新增 2 文件(attributes.ts + AttributesPanel.tsx),修改 6 文件(types/config/engine/gameStore/achievements/page
268→- ✅ 四维属性完整接入游戏各系统(探险/解码/脉冲/巡航/星潮)
269→- ✅ 飞升属性点公式 = ascensions × 2 + 1;经验累积自动升级(每级 10×level)
270→- ✅ 旧存档兼容(migrateAttributes 补全字段,同步 level 与 attributes
271→- ✅ lint 零错误 + HTTP 200 + 编译 < 250ms
272→- ✅ 严格四色全息(emerald/fuchsia/amber/rose),零蓝色/靛色
273→- ✅ 8 标签页 + 巡航按钮全部保留,data-tut 锚点未破坏
274→- ✅ 待分配点数时标签页 rose 红点提示 + 顶部 badge 闪烁
275→
276→---
277→Task ID: 8-deploy
278→Agent: main
279→Task: 重建定时任务 + 更新 README + 部署 gh-pagesv0.7
280→
281→Work Log:
282→- 检查项目状态:CrystalOrb.tsx 已是 v0.7 Canvas 粒子系统(659 行),无活动 cron job
283→- 重建定时任务:cron create fixed_rate="900" priority=10 webDevReview → job_id 228329tz=Asia/Shanghai
284→- 重写 README.md
285→ - 顶部显著位置添加「在线游玩」链接(https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/
286→ - 版本号 v0.1 → v0.7
287→ - 新增 10 大系统表格(含 v0.6 巡航 + v0.7 角色属性)
288→ - 完整开发路线表(v0.1-v1.0,标记 ✅/🔜 状态)
289→ - 新增本地开发命令 + Gitea Pages 部署指南
290→ - 文档索引扩展至 10 个版本文档
291→- 静态导出:BUILD_EXPORT=true bun run build:static → out/ 1.6MHTTP 200 编译 4.3s
292→- 推送 gh-pages 分支:force push origin gh-pagescommit 8727fed "deploy: v0.7"
293→- 提交 mainREADME 更新(commit 43dfc2d "docs: README 添加在线游玩链接 + v0.7 状态更新"
294→- 验证 Gitea Pageshttps://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/ → HTTP 200 ✅
295→- QA 验证:agent-browser 打开 http://localhost:3000/ → VLM 评分 8/10(主界面 polishedminor 空白)
296→
297→Stage Summary:
298→- ✅ 定时任务重建成功(job_id 228329,每 15 分钟自动审查迭代)
299→- ✅ README 重写完成,网站链接显著展示,v0.7 状态完整
300→- ✅ gh-pages 部署成功,Gitea Pages HTTP 200 可访问
301→- ✅ main 分支已同步推送(README + worklog 更新)
302→- ✅ dev 服务器健康(HTTP 200,编译 < 250ms
303→- ✅ CrystalOrb v0.7 Canvas 粒子系统已确认完整(659 行)
304→- 下一步:v0.8 巡航玩法增强(BOSS 战 / 道具掉落 / 事件选择节点)
305→
306→---
307→Task ID: 8
308→Agent: main
309→Task: v0.8 巡航玩法大增强(BOSS 战 + 道具掉落 + 事件选择节点)+ 工单修正 + 审查流程重建
310→
311→Work Log:
312→- 重读 worklog 工单,发现 v0.7 两项(CrystalOrb Canvas + 角色属性)已完成但工单"下一阶段优先级"未更新
313→- 修正 worklog 工单:标注 P0/P1 v0.7 已完成,重新排序下一阶段优先级(P0=巡航增强, P1=探险平衡/信标扩展, P2=UI打磨/云存档, P3=叙事/多人)
314→- 重建审查流程 cron jobfixed_rate=900s, priority=10, webDevReview, tz=Asia/Shanghai, job_id=228357
315→ - 任务描述强制要求:每次触发 MUST 先完整阅读 worklog.md 工单,以工单为准,不凭记忆工作
316→- 发现之前被中断的 v0.8 subagent 已写入 2037 行代码(cruise.ts +1487 / CruiseMode.tsx +858 / page.tsx +2
317→- 修复 2 个 lint 错误(react-hooks/set-state-in-effect):
318→ - isTouch: useState(false) + useEffect(setIsTouch) → useState initializer
319→ - stats: useState(null) + useEffect(setStats(loadCruiseStats)) → useState initializer
320→- QA 验证(agent-browser + VLM):
321→ - 主界面 VLM 7/10v0.8 版本号 ✅ + 巡航按钮 ✅
322→ - 巡航 READY 阶段 VLM 8/10BOSS 战说明 ✅(每5关BOSS战/击败BOSS)+ 射击控制说明 ✅(空格/右下角射击按钮)
323→ - 巡航第1关游戏 VLM 8/10:飞船(绿色三角)✅ + HUD(护盾/能量/分数/计时)✅ + 陨石/收集物/能量场 ✅ + 无渲染错误
324→- 提交 v0.8commit 494bc5f+ 推送 main
325→- 静态导出 BUILD_EXPORT=true → out/ 1.6M(编译 4.1s
326→- 部署 gh-pagesforce push, commit b865922)→ Gitea Pages HTTP 200 ✅
327→
328→Stage Summary:
329→- ✅ 工单修正完成:v0.7 已完成项标注清楚,下一阶段优先级重新排序
330→- ✅ 审查流程重建:cron job_id 228357,每15分钟触发,强制读工单
331→- ✅ v0.8 巡航玩法大增强全部完成并部署:
332→ - BOSS 战系统(每5关,三阶段攻击,HP条,击败奖励×3)
333→ - 飞船射击系统(空格/射击按钮,能量消耗,子弹尾焰)
334→ - 道具掉落(5种powerup:护盾/能量/急速/超载/磁吸)
335→ - 事件选择节点(每关通关后3选1,10种强化牌)
336→ - lint 零错误 + dev HTTP 200 + gh-pages HTTP 200
337→- ✅ VLM 评分:主界面 7/10 + 巡航READY 8/10 + 巡航游戏 8/10
338→- 在线游玩 v0.8https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/
339→- 下一阶段工单:P1 探险BOSS胜率平衡 + 探险能量恢复 / 信标系统扩展(周挑战/信标链)
340→
341→---
342→Task ID: 9-b
343→Agent: full-stack-developer
344→Task: 信标系统扩展(周挑战 + 信标链连续奖励)
345→
346→Work Log:
347→- 阅读现有 `src/lib/game/beacon.ts`v0.5 每日挑战 + 本地排行榜,358 行)+ `src/components/game/BeaconPanel.tsx` + `src/store/gameStore.ts` 的 `trackBeacon` 函数(约第 173 行)+ worklog v0.8 项目状态,确认四色全息规范与现有架构。
348→- **扩展 `src/lib/game/beacon.ts`358 → 873 行,新增 ~515 行)**
349→ - **周挑战(WEEKLY CHALLENGE**
350→ - `BeaconWeeklyChallenge` 接口(weekKey YYYY-Www / type / difficulty 强制 anomaly|singular / goal / rewardInsight / rewardContact / seed / title / desc
351→ - `BeaconWeeklyProgress` 接口(weekKey / progress / startedAt / completedAt / claimed / durationSec
352→ - `getWeekKey(now)`ISO 8601 周键(周一为起点,含首个周四的周为第一周)
353→ - `weekKeyToSeed`FNV-1a 哈希
354→ - `generateWeeklyChallenge(now)`:基于 weekKey 种子确定性生成,难度加权 anomaly 60% / singular 40%goal 为日基准 ×3-5 倍(decode 18-75 / expedition 3-15 / pulse 60-245 / boss 3-12 / insight 120-595
355→ - `loadWeeklyProgress` / `saveWeeklyProgress` / `addWeeklyProgress` / `claimWeeklyReward`(推送排行榜,entry 加 `isWeekly: true`
356→ - `msUntilNextWeek(now)`:距离下周一 UTC 0 点的毫秒数
357→ - `BEACON_WEEKLY_KEY = "echo-nexus-beacon-weekly-v1"`
358→ - **信标链(BEACON CHAIN**
359→ - `BeaconChainState` 接口(lastCompletedDateKey / currentStreak / longestStreak / totalCompletions / graceUsed / milestonesClaimed
360→ - `BEACON_CHAIN_KEY = "echo-nexus-beacon-chain-v1"`
361→ - `BEACON_CHAIN_MILESTONES = [3, 7, 14, 30]` 常量
362→ - `BEACON_CHAIN_REWARDS`4 个里程碑(三日谐振 +50/+5 / 七日回响 +120/+12 / 半月星潮 +280/+28 / 满月飞升 +680/+68
363→ - `loadChainState` / `saveChainState`(每次返回新对象避免引用共享)
364→ - `recordChainCompletion(dateKey)` 核心逻辑:同日重复忽略;次日 streak++;隔一天 miss 且 graceUsed<1 时续命 streak++ graceUsed++;其他断链 streak=1 graceUsed=0;返回 `{ state, newMilestones }`
365→ - `claimChainMilestone(milestone)` / `getNextMilestone(streak)` / `getChainProgress(streak)`(返回 current/next/prev/progressPct 用于 UI
366→ - `dateKeyToTimestamp` / `dateKeyDiffDays` 工具函数
367→ - `BeaconScoreEntry` 新增可选 `isWeekly?: boolean` 字段
368→- **扩展 `src/store/gameStore.ts`**
369→ - import 扩展:generateWeeklyChallenge / loadWeeklyProgress / loadChainState / addWeeklyProgress / recordChainCompletion / claimWeeklyReward / claimChainMilestone / getTodayKey + 类型
370→ - `trackBeacon(type, delta)` 函数签名从 `boolean` 改为 `{ dailyJustCompleted, weeklyJustCompleted, newChainMilestones }`
371→ - 日挑战类型匹配且未完成 → addBeaconProgress;若刚完成 → recordChainCompletion(getTodayKey()) 收集 newMilestones
372→ - 周挑战类型匹配且未完成 → addWeeklyProgress
373→ - 所有调用点(pulse/clickNode/autoDecodeTick/resolveCurrentNode 等 7 处)原本就忽略返回值,向后兼容
374→ - 新增 action `claimWeeklyBeacon`:调用 claimWeeklyReward → 发放 insights + contact 到 state,返回 `{ rewardInsight, rewardContact, score }`
375→ - 新增 action `claimChainReward(milestone)`:前置校验 currentStreak 与 milestonesClaimed,调用 claimChainMilestone → 发放奖励,返回 `{ rewardInsight, rewardContact, label, ok }`
376→ - GameActions 接口同步新增两个 action 签名
377→- **重写 `src/components/game/BeaconPanel.tsx`301 → 638 行)**
378→ - 顶部头部 + 每日挑战卡片(保留 v0.5 完整功能 + 按钮主题色随难度色淡显)
379→ - **新增周挑战区块**fuchsia 主题):标题"周挑战 · WEEKLY" + weekKey + 倒计时 + 挑战卡片(类型/难度标签/标题/描述/进度条/奖励/领取按钮 emerald 主题)+ weekly-glow 动画
380→ - **新增信标链区块**amber→rose 渐变):标题"信标链 · CHAIN" + 大字号 streakchain-streak-text 渐变流动动画)+ 今日完成状态徽章 + 4 个里程碑节点(w-12 h-12 圆形,claimed=emerald/✓,reachable=rose 脉冲动画+领取按钮,inProgress=amber 半亮,未到达=muted 灰)+ 节点间连线(背景灰 + 已达成部分 amber→rose 渐变 + 辉光,基于 prev→next 插值定位)+ 进度条 + 底部统计(最长/累计/续命状态)
381→ - 桌面端 lg:grid-cols-2 让周挑战+信标链并排,移动端单列
382→ - 排行榜条目区分日/周:周挑战 entry 显示 "WEEK" 徽章 + fuchsia 高亮背景
383→ - 空状态提示扩展为"完成今日或本周信标即可登榜"
384→ - 4 个新动画:weekly-glow / chain-milestone-pulse / chain-streak-flux(背景渐变流动)
385→- **QA 验证**
386→ - `bun run lint` 零错误(每次修改后均验证)
387→ - dev.log 全程无错误,所有编译 < 300ms
388→ - **信标链逻辑测试**(bun 直接运行 TS,5 个场景全 PASS):
389→ 1. 昨日 streak=1 → 今日完成 → streak=2 ✅
390→ 2. 同日重复完成 → 忽略 ✅
391→ 3. 明日完成 → streak=3,无需 grace ✅
392→ 4. 隔一天 misslastCompleted + 2 天)→ streak=4graceUsed=1 ✅
393→ 5. 再次 missgrace 已用)→ 断链 streak=1graceUsed=0 ✅
394→ - **UI 集成测试**agent-browser):
395→ - localStorage 模拟设置 chain statelastCompletedDateKey=yesterday, currentStreak=1)→ reload → UI 正确显示 streak=1
396→ - 设置 streak=3 + milestonesClaimed=[] → milestone 3 节点显示 rose 脉冲动画 + "领取"按钮
397→ - 点击"领取"→ milestonesClaimed=[3] + Toast "✦ 三日谐振 已领取 +50 洞见 · +5.0 接触" + 按钮消失 ✅
398→ - **VLM 视觉评分**(目标 ≥7/10):
399→ - 首屏截图:8/10(四色一致、布局合理、信标链清晰)
400→ - 里程碑可领取状态:8/10(amber→rose 渐变醒目、里程碑层次分明、fuchsia 主题清晰)
401→ - 重置后干净状态:8/10(WEEKLY 字体对比度可优化,但整体可读性强)
402→ - **不破坏现有功能**:日挑战进度/领奖/排行榜流程全部保留并工作正常
403→
404→Stage Summary:
405→- ✅ beacon.ts 扩展完成(358 → 873 行):周挑战 + 信标链全部函数与类型
406→- ✅ gameStore.ts 集成完成:trackBeacon 升级为对象返回值 + claimWeeklyBeacon + claimChainReward 两个新 action
407→- ✅ BeaconPanel.tsx UI 完整重写(301 → 638 行):周挑战 fuchsia 卡片 + 信标链 amber→rose 渐变区块
408→- ✅ 严格四色全息:周挑战 fuchsia、信标链 amber→rose、领取按钮 emerald、无蓝/紫色违规
409→- ✅ localStorage 新增 2 个独立 keyweekly + chain),不污染旧 key
410→- ✅ 确定性:周挑战同周同种子同结果(weekKey FNV-1a 种子)
411→- ✅ 信标链 grace 续命机制工作正常(5 个场景 bun 测试全 PASS)
412→- ✅ lint 零错误 + dev HTTP 200 + VLM 8/10
413→- ✅ 现有日挑战 + 排行榜 + 领奖流程全部保留,无回归
414→
415→---
416→Task ID: 9
417→Agent: main + subagent(9-b)
418→Task: v0.8.1 P1 双任务 — 探险 BOSS 胜率平衡 + 能量恢复 + 信标系统扩展(周挑战 + 信标链)
419→
420→Work Log:
421→- 重读工单确认 P1 两项为下一阶段优先级
422→- P1-a 探险平衡(main 自己做,平衡需要精细控制):
423→ - expedition.ts: combatDifficultyScale 8→4, combatWinRateFloor 0.25→0.35, BOSS difficulty 5-7→3-5
424→ - 新增 computeEnergyRegenInterval(state): exp_2 -30%, exp_3 -20%, 探索力属性 -最高30%, 下限12s
425→ - computeEnergyRegen 接受 intervalSec 参数
426→ - config.ts: exp_2/exp_3 描述加能量恢复说明
427→ - gameStore.ts tick: 传入动态间隔
428→ - ExpeditionPanel.tsx: 显示实际恢复速度 + '已加速'标记
429→- P1-b 信标系统扩展(dispatch subagent 9-b,并行完成):
430→ - beacon.ts 358→873行: 周挑战(getWeekKey ISO 8601 + 确定性生成) + 信标链(4里程碑+grace续命)
431→ - BeaconPanel.tsx 301→638行: 周挑战fuchsia主题 + 信标链amber→rose里程碑节点
432→ - gameStore.ts: trackBeacon升级 + claimWeeklyBeacon/claimChainReward actions
433→- QA 验证:
434→ - lint 零错误
435→ - 探险面板 VLM 7/10(能量条 + 恢复速度显示正常)
436→ - 信标面板 VLM 8/10(周挑战 WEEKLY + 信标链 3/7/14/30 + 排行榜全部可见)
437→ - subagent 5场景 bun 测试全 PASS(信标链断链/续命逻辑)
438→- 提交 v0.8.1commit 71ca5b4+ 推送 main
439→- 静态导出 + 部署 gh-pagescommit 97eecaa)→ HTTP 200
440→
441→Stage Summary:
442→- ✅ P1-a 探险平衡完成: BOSS 基础胜率 25%→35-45%, 配合技术/属性可达 55-70%
443→- ✅ P1-a 能量恢复: 固定45s → 动态12-45s(技术+属性加速)
444→- ✅ P1-b 周挑战: 每周一UTC刷新, fuchsia主题, goal×3-5倍
445→- ✅ P1-b 信标链: 4里程碑(3/7/14/30天) + grace续命, amber→rose渐变
446→- ✅ 在线游玩 v0.8.1: https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/
447→- 下一阶段工单: P2 UI样式细节打磨 / P2 云存档+云排行榜 / P3 全纪元叙事
448→
449→---
450→Task ID: 10-a
451→Agent: full-stack-developer
452→Task: #5 限时挑战 + #3 星潮类型深化
453→
454→Work Log:
455→- 阅读项目上下文:worklog.mdv0.8.110 大系统)+ beacon.ts873行,日+周+链)+ starTide.ts209行,6种星潮)+ gameStore trackBeacon/tickTide + BeaconPanel.tsx,确认四色全息规范与现有架构
456→- **工单 #5 限时挑战**
457→ - beacon.ts873→1162行)新增限时挑战系统:
458→ - BeaconTimedChallenge/BeaconTimedProgress 接口 + isTimed 字段加入 BeaconScoreEntry
459→ - getTimedSlotKeyUTC 0/4/8/12/16/20 点切分,"timed_YYYY-MM-DD_HH"+ timedSlotKeyToSeedFNV-1a
460→ - generateTimedChallenge(确定性,难度 routinegoal=日基准×0.3-0.55类型公式)
461→ - loadTimedProgress/saveTimedProgress/addTimedProgress/claimTimedReward(排行榜 isTimed
462→ - msUntilNextTimedSlot + TIMED_SLOT_MS=4h + BEACON_TIMED_KEY
463→ - gameStore.tstrackBeacon 返回值新增 timedJustCompleted,三进度同时更新;新增 claimTimedBeacon action
464→ - BeaconPanel.tsx753→944行):日挑战与周挑战之间插入限时挑战区块(amber 主题)
465→ - Hourglass图标+TIMED标题+slotKey+倒计时 + 进度+奖励+领取按钮(emerald)
466→ - 紧急状态:距时段结束<30min 切 rose 脉冲动画+"即将结束"徽章
467→ - 2新动画 timed-glow(amber)/timed-urgent(rose) + 排行榜 TIMED 徽章
468→- **工单 #3 星潮深化**
469→ - starTide.ts209→289行):TideType 新增 surge/eclipse/prism
470→ - surge(emerald ⇈): crystalsPerSec×2.5 + targetLenBonus=2(解码目标序列+2,更长更难)
471→ - eclipse(rose ◐): crystalsPerSec×0.7 + bossWinRateBonus=0.2BOSS胜率+20%
472→ - prism(fuchsia ◬): insightMultAdd=1.0(洞见×2) + autoDecodeIntervalMult=0.7(自动解码-30%)
473→ - TideModifiers 新增 targetLenBonus/bossWinRateBonus/autoDecodeIntervalMult 可选字段
474→ - TIDE_WEIGHTS 新增3种各12(总136+ getTideModifiers 3新case
475→ - decode.tsgeneratePuzzle 新增 targetLenBonus 参数(surge用,targetLen+stepLimit同步增加)
476→ - gameStore.ts 3处修饰器应用:
477→ - startDecodesurge 时传 targetLenBonus → 谜题更长
478→ - autoDecodeTickprism 时 interval×=autoDecodeIntervalMult
479→ - resolveCurrentNodeeclipse 时 bossWinRate 加 tideBossBonus
480→ - achievements.tsach_tides_all 阈值6→9page.tsx StatsPanel 星潮亲历 /9
481→- QA 验证:
482→ - lint 零错误(exit 0+ dev HTTP 200 + 编译<30ms
483→ - BeaconPanel VLM 8.5/10(限时区块可见)+ 最终 9/10
484→ - 三新星潮 localStorage 注入触发截图 VLMsurge 8/10、prism 7/10、eclipse 8/10
485→ - 日/周/信标链/6旧星潮全部保留无回归
486→
487→Stage Summary:
488→- ✅ beacon.ts +289行(限时挑战完整系统:4h时段+确定性+进度+领奖+排行榜)
489→- ✅ starTide.ts +80行(3新类型 surge/eclipse/prism + 3新修饰器字段)
490→- ✅ decode.ts generatePuzzle 支持 targetLenBonussurge 谜题+2
491→- ✅ gameStore.ts trackBeacon 三进度同步 + claimTimedBeacon + 3处新星潮修饰器
492→- ✅ BeaconPanel.tsx +191行(amber 限时区块 + 紧急状态 + TIMED 徽章)
493→- ✅ 严格四色规范:限时amber/surge-emerald/eclipse-rose/prism-fuchsia,零蓝色
494→- ✅ 确定性:限时挑战同时段同种子同结果(FNV-1a slotKey 哈希)
495→- ✅ lint 零错误 + dev HTTP 200 + VLM 全部≥7/10
496→
497→---
498→Task ID: 10-b
499→Agent: full-stack-developer
500→Task: P2/#4 云排行榜 mini-service(信标本机榜升级为云端 Top100)
501→
502→Work Log:
503→- 前置阅读:worklogv0.8.1 项目状态)+ beacon.tsBeaconScoreEntry / loadLeaderboard / pushLeaderboardEntry / claimBeaconReward / claimWeeklyReward / claimTimedReward+ BeaconPanel.tsx(排行榜 UI 渲染)+ Caddyfile?XTransformPort=端口号 转发规则)+ examples/websocketmini-service 参考)+ .zscripts/dev.shmini-service 启动模式)
504→- 发现 worklog 信息滞后:beacon.ts 实际已扩到 1161 行(v0.8.2 已加限时挑战 + claimTimedReward),BeaconPanel 已扩到 953 行
505→- 创建 mini-service
506→ - `mini-services/leaderboard-service/package.json`:独立 bun 项目,type=modulescripts `dev: bun --hot index.ts` + `start: bun index.ts`,依赖 hono ^4.6.14
507→ - `mini-services/leaderboard-service/index.ts`182 行):
508→ - Hono + cors 中间件(origin: * 全开放,跨端口必须)
509→ - 内存数组存储(最多 1000 条,按 score 降序,同分按时长→时间戳排序)
510→ - 防刷:同 dateKey+challenge 10s 内只接受 1 次提交(Map 记录 lastSubmitAt),返回 429 + retryAfterMs
511→ - GET /api/leaderboard → Top100 + total
512→ - POST /api/leaderboard body {entry} → 校验 schema → 推入 → 排序 → 返回 {entries, total, rank}
513→ - GET /api/leaderboard/stats → {totalSubmissions, uniquePlayers, topScore}
514→ - GET / → 健康检查 {service, version, ok, uptime}
515→ - 端口固定 3030`export default { port, fetch }` 标准 bun 模式
516→ - `mini-services/leaderboard-service/README.md`API 文档 + 联调规范
517→- mini-service 启动调试:bun --hot 在本沙盒环境下会因文件 watcher 异常被 kill;改用 `bun index.ts`(无 --hot+ 双 fork `( ... & )` 模式启动,PPID=1uptime 4+ 分钟稳定
518→- eslint.config.mjs 新增 ignores "mini-services/**"mini-service 独立项目,不参与主项目 lint)
519→- beacon.ts 扩展(1161 → 1290 行,+129 行):
520→ - 三个 claim 函数返回值新增 `entry: BeaconScoreEntry`(非破坏性扩展):
521→ - `claimBeaconReward` 返回 `entry`daily
522→ - `claimWeeklyReward` 返回 `entry`weekly
523→ - `claimTimedReward` 返回 `entry`timed
524→ - 新增云排行榜模块(v0.8.2 P2/#4):
525→ - `BEACON_CLOUD_PORT = 3030` 常量
526→ - `BEACON_CLOUD_LAST_SUBMIT_KEY = "echo-nexus-beacon-cloud-last-submit-v1"` 本地 key
527→ - `CloudLeaderboardResponse` / `CloudSubmitResponse` / `CloudStatsResponse` 接口
528→ - `loadLastCloudSubmitTimestamp()` / `saveLastCloudSubmitTimestamp(ts)` 工具
529→ - `fetchCloudLeaderboard(): Promise<BeaconScoreEntry[]>` — GET 相对路径 + ?XTransformPort=3030
530→ - `submitCloudScore(entry): Promise<number>` — POST,成功返回 rank,失败返回 -1,自动记录 ts 供全球榜高亮
531→ - `fetchCloudStats(): Promise<CloudStatsResponse | null>` — 全局统计
532→ - 所有 fetch 用相对路径 + `?XTransformPort=3030`**禁止** localhost:3030
533→- gameStore.ts 集成:
534→ - import 新增 `submitCloudScore`
535→ - `claimWeeklyBeacon` action:领奖成功后 `void submitCloudScore(res.entry)`fire-and-forget,不 await,不阻塞)
536→ - `claimTimedBeacon` action:同上
537→ - `claimBeaconReward` (daily) 由 BeaconPanel 直接调用,不通过 gameStore
538→- BeaconPanel.tsx 重写(953 → 1170 行,+217 行):
539→ - 新增 import: fetchCloudLeaderboard / submitCloudScore / loadLastCloudSubmitTimestamp + 4 个 lucide 图标(RefreshCw/Globe/WifiOff/Loader2
540→ - 新增状态: lbTab ('local'|'global') / cloudEntries / cloudLoading / cloudError / cloudFetched / mySubmitTs / cloudFetchingRef
541→ - 新增 `refreshCloudLeaderboard()` callback:拉取云端榜 + 防并发(ref)+ 空 entries 时显示提示
542→ - useEffect: 切到全球 tab 自动拉取(仅首次)+ 初始化读取 mySubmitTs
543→ - handleClaimDaily: 领奖成功后 `void submitCloudScore(res.entry).then(rank => ...)` — 若用户在全球 tab 自动刷新
544→ - handleClaimTimed/Weekly: 领奖后更新 mySubmitTs + 若在全球 tab 自动刷新
545→ - 排行榜 UI 重写为双 tab:
546→ - 顶部「本地」(amber 主题) / 「全球」(emerald 主题) 切换按钮 + 全球 tab 专属刷新按钮(旋转动画)
547→ - 子标题:本地"本机 Top N · 离线可用" / 全球"全球 Top N · 已同步 / 同步中…"
548→ - 本地榜保留原 v0.8.2 渲染(max-h-200px overflow-y-auto
549→ - 全球榜:
550→ - 加载中:Loader2 spinner + "正在拉取全球榜…"
551→ - 空榜+错误:WifiOff 图标 + 错误文案 + 重试按钮
552→ - 空榜+无错:Globe 图标 + "尚无全球记录"提示
553→ - 有数据:渲染前 100 条,每条带排名(1-3 名用 Crown/Medal/Award 图标)/ 难度色 / WEEK·TIMED 徽章 / 用时 / 分数
554→ - 玩家自己的记录通过 timestamp 匹配 mySubmitTs 高亮:emerald 边框 + emerald 阴影 + YOU 徽章 + emerald 分数色 + 加粗排名
555→- QA 验证:
556→ - `bun run lint` 零错误零警告(eslint.config.mjs 把 mini-services 加入 ignores
557→ - dev.log 全程无错误,所有编译 < 300ms
558→ - mini-service curl 全通过:
559→ - GET / → 200 {service, version, ok, uptime}
560→ - GET /api/leaderboard (空) → 200 {entries:[], total:0}
561→ - POST /api/leaderboard → 200 {entries, total, rank:1}
562→ - GET /api/leaderboard (有数据) → 200 {entries:[...], total:N}
563→ - GET /api/leaderboard/stats → 200 {totalSubmissions, uniquePlayers, topScore}
564→ - OPTIONS 预检 → 204 + 完整 CORS 头(Allow-Origin: *
565→ - 重复 POST(同 dateKey+challenge 10s 内)→ 429 {error:"Rate limited", retryAfterMs}
566→ - 网关路由验证:Caddy :81 → localhost:3030 通过 ?XTransformPort=3030 正确转发,返回 JSON 200
567→ - agent-browser 集成测试(通过 :81 端口访问,绕过 Next.js 404):
568→ - 信标 tab 选中(用 KeyboardEvent Enter 激活 Radix Tabs,因 pointerdown 被覆盖层拦截)
569→ - 切到「全球」tab → 自动拉取 → 显示"全球 Top 6 · 已同步" → 渲染 6 条记录
570→ - 刷新按钮可点击,旋转动画正常
571→ - POST 测试:浏览器 fetch 提交 score=9999 → 返回 rank=1 → 刷新后榜首显示 10.00K
572→ - YOU 高亮:手动设置 localStorage[BEACON_CLOUD_LAST_SUBMIT_KEY] 后刷新 → 榜首显示 emerald 边框 + YOU 徽章
573→ - VLM 视觉评分(目标 ≥7/10):
574→ - 全球榜含 YOU 高亮:**8/10**(双tab切换清晰,全球榜记录完整,难度色与标签规范,视觉层次佳)
575→ - 本地榜:**8/10**(双tab切换逻辑清晰,本地tab内容完整,四色规范应用,信息层级合理)
576→
577→Stage Summary:
578→- ✅ mini-service 独立项目:自己的 package.json + bun.lock + node_modules,依赖 hono ^4.6.14,不污染主项目
579→- ✅ 端口 3030 + Caddy 网关:前端用相对路径 `fetch('/api/leaderboard?XTransformPort=3030')`**禁止** localhost:3030
580→- ✅ CORS 全开放:origin: * + OPTIONS 预检自动处理,前端跨端口无忧
581→- ✅ 内存存储 + 防刷:1000 条上限,同 dateKey+challenge 10s 限 1 次
582→- ✅ 三个 claim 入口全部接入云提交:claimBeaconRewardBeaconPanel 直接调)/ claimWeeklyBeacon / claimTimedBeacongameStore action 内)
583→- ✅ 本地榜保留:loadLeaderboard / pushLeaderboardEntry 未删除,作为离线 fallback
584→- ✅ 双 tab UI:本地(amber 主题,立即可用)/ 全球(emerald 主题,按需拉取)+ 刷新按钮 + 加载态 + 错误重试
585→- ✅ 玩家高亮:通过 timestamp 匹配 localStorage[BEACON_CLOUD_LAST_SUBMIT_KEY]emerald 边框 + YOU 徽章
586→- ✅ lint 零错误 + dev HTTP 200 + VLM 8/10
587→- ✅ mini-service 启动稳定:uptime 4+ 分钟,PPID=1(双 fork 模式)
588→- 下一阶段工单:P3 #10-c 全球星潮同步(star-tide-service 已在 mini-services 目录中)
589→
590→---
591→Task ID: 10-c
592→Agent: full-stack-developer
593→Task: #9 手写叙事节点 + P3 socket 多人同步星潮
594→
595→Work Log:
596→- 前置阅读:worklog.mdv0.8.1,四色规范 emerald/rose/amber/fuchsia,禁止蓝色,10 大系统)+ chronicle.ts680 行)+ starTide.ts297 行,9 种星潮)+ gameStore.ts1332 行)+ StarTide.tsx170 行)+ StarTideOverlay.tsx + Caddyfile:81 网关 ?XTransformPort 转发规则)+ 前序 10-a/10-b 工单(限时挑战/3 新星潮/云排行榜 mini-service
597→- **状态确认**:本任务为重试,发现上次重试已落地主体代码,本次为验证 + 补 QA + 启动 mini-service + 追加 worklog
598→- **工单 #9 手写叙事节点**chronicle.ts 已就绪):
599→ - EPOCH_LORE 常量:5 纪元 × 3 节点(opening/middle/ending= 15 段手写叙事,每段 80-150 字文学性
600→ - 5 纪元主题:1 觉醒(emerald/2 谐振(rose/3 遗迹(amber/4 飞升(fuchsia/5 终末
601→ - 每段含 fallback + variants(按 ctx 命中条件挑变体):
602→ - 第一纪元:解码者(≥15)/学者(techs≥5)/里程碑密集(≥2) + 深度轮回(asc>5)
603→ - 第二纪元:探险者(≥3)/解码者(≥15)/觉醒天赋 + 深度轮回
604→ - 第三纪元:BOSS 猎手(≥2)/深度探险(≥4)/星潮亲历(≥3)/BOSS 收尾 + 深度轮回
605→ - 第四纪元:觉醒天赋/蓝图接近完整(≥5)/里程碑密集 + 深度轮回
606→ - 第五纪元:里程碑密集(≥3)/星潮汹涌(≥3)/觉醒天赋 + 深度轮回
607→ - 变量替换:{decodedThisRun}/{techsThisRun}/{expThisRun}/{bossKillsThisRun}/{blueprintsAfter}/{milestonesCount}/{milestonesList}/{tideCount}/{tideNames}/{perksList}/{minutes}
608→ - buildLore 改造:优先 buildHandwrittenLore,无匹配(ascensionNumber 异常)时 fallback 到 v0.4 模板拼接
609→ - 验证:bun 脚本测试 5 纪元 × 2 上下文(minimal/rich= 10 个 case 全通过,文字长度 233-273 字(含收尾段),变体匹配正确,变量替换无残留 {xxx}
610→- **P3 socket 多人同步星潮**
611→ - **mini-servicemini-services/star-tide-service/**
612→ - 独立 bun 项目,package.json type=module + scripts dev:bun --hot index.ts + start:bun index.ts,依赖 socket.io ^4.8.3
613→ - index.ts250 行):
614→ - 端口固定 3031path:"/"Caddy 据此转发)
615→ - 6 种星潮类型表(crystal/resonance/ruins/void/core/silence+ 权重表 + rollTide
616→ - DURATION_SEC=60s 全球星潮持续;FIRST_TIDE_DELAY_MS=60sQA 友好,env 可覆盖);MIN/MAX_GAP_MS=10-15 分钟随机间隔
617→ - 调度:startTide → io.emit("global-tide", {type,name,startedAt,endsAt,durationSec}) → 60s 后 endTide → io.emit("tide-ended", {endedAt,nextTideAt}) → scheduleNextTide10-15 分钟随机)
618→ - 客户端连接:socket.emit("tide-state", {current, nextTideAt, serverTime}) 立即推送
619→ - 客户端可 emit "get-next-tide" → 服务端响应 "next-tide-info" {current, nextTideAt, serverTime, remainingMs}
620→ - dev/QA 用 "admin-trigger-tide":立即触发一次全球星潮(不等待下次定时器)
621→ - 优雅退出:SIGTERM/SIGINT → 清 timer → io.close → httpServer.close → exit(0)
622→ - README.mdAPI/事件文档
623→ - **前端集成**
624→ - src/hooks/useGlobalTide.ts127 行):
625→ - 单例 socketsharedSocket + connectRefCount 引用计数,多组件挂载共享同一连接)
626→ - io("/?XTransformPort=3031", {transports:["websocket","polling"], reconnection:true, reconnectionAttempts:Infinity})
627→ - 监听 "global-tide" → triggerRef.current(type),用 startedAt+type 去重(防重连重复触发),过期 tide 不触发
628→ - 监听 "tide-state" → 连接/重连时若服务端正有进行中的全球星潮,补触发
629→ - 主动 emit "get-next-tide" 保险
630→ - 导出 adminTriggerGlobalTidedev 控制台 QA 用),window.__adminTriggerGlobalTide 暴露
631→ - gameStore.ts 新增 triggerGlobalTide action + globalTide state
632→ - GLOBAL_TIDE_DURATION_MS = 60_000 常量
633→ - GlobalTideState 类型 {type, startedAt, endsAt, id}
634→ - TideEvent 新增 isGlobal?: boolean 字段
635→ - triggerGlobalTide(type):创建 newTide60s+ newGlobalTide,覆盖本地 activeTide(即使本地有进行中的星潮也会被替换),加入 _tideEvents 队列 with isGlobal:true
636→ - tickTide 改造:globalTide 优先 → 全球星潮进行中本地不触发新星潮;全球星潮结束时设 globalTide:null + activeTide:null + lastTideEnd + silenceCompensation + ended 事件 with isGlobal:true
637→ - partialize 排除 globalTide(瞬态运行时状态,不持久化)
638→ - page.tsx 顶层挂载 useGlobalTide()
639→ - StarTide.tsx UI 升级:
640→ - StarTideIndicator:读 globalTide → isGlobal 标记 → 加 🌐 emoji + ring-1 ring-amber-300/60 + 强化 boxShadow + 渐变背景 + title 改为 "🌐 全球星潮 · {meta.desc}"duration 用 globalTide.endsAt-startedAt 计算(60s)避免越界
641→ - StarTideOverlay:全球星潮多一道顶部琥珀色光带 + global-tide-sweep 2.5s 扫光动画 + inset boxShadow 加深(160px vs 120px
642→ - StarTideNotifiertoast 文案区分全球/本地("🌐 全球星潮降临:{name}" + "全球玩家同步经历 · {desc}"
643→- **QA 验证**
644→ - `bun run lint` 零错误零警告
645→ - dev.log 全程无错误,编译 < 70msHTTP 200
646→ - mini-service 启动稳定:pid 9456PPID=1(双 fork 脱离 dev.sh),uptime 19+ 分钟,端口 3031 监听正常
647→ - curl http://localhost:3031/ → {"code":0,"message":"Transport unknown"}socket.io-only 服务预期响应)
648→ - curl http://localhost:3031/socket.io/?EIO=4&transport=polling → 正确握手 {"sid":...,"upgrades":["websocket"]}
649→ - 网关路由验证:curl http://localhost:81/socket.io/?EIO=4&transport=polling&XTransformPort=3031 → 通过 Caddy 转发到 3031,返回正确握手
650→ - socket.io 端到端测试(bun 脚本 /tmp/test-socket.ts):connect ✅ → tide-state ✅ → emit admin-trigger-tide → 504ms 内收到 global-tide {type:ruins, startedAt, endsAt, durationSec:60} ✅
651→ - agent-browser 集成测试(通过 http://localhost:81/ 走 Caddy 网关,socket.io 才能正确路由):
652→ - 页面加载 200window.__adminTriggerGlobalTide 已挂载(useGlobalTide hook 已挂载)
653→ - 调用 adminTriggerGlobalTide() → 1-3s 内 StarTideIndicator 渲染 🌐 芯片
654→ - DOM 验证:芯片文本 "🌐✷虚空低语57s"title "🌐 全球星潮 · 虚空传来回响,洞见获取翻倍"(isGlobal:true 路径生效)
655→ - 第二次触发:"🌐⬢遗迹共振59s"title "🌐 全球星潮 · 远古遗迹苏醒,探险力 +5、生命 +30"
656→ - 全球星潮结束(60s 后)→ activeTide:null + globalTide:null + lastTideEnd 更新 → 本地 tickTide 恢复正常节奏
657→ - VLM 视觉评分:**8/10**(确认 🌐 地球图标 + 汉字名称"遗迹共振" + 倒计时"59s" 全部可见;颜色 emerald/amber/fuchsia/rose 四色规范,零蓝色)
658→ - 编年史 buildLore 验证(bun 脚本测试 5 纪元 × 2 上下文 = 10 case):
659→ - 全部产出 233-273 字文学性叙事,包含开场+中段+结尾+收尾段
660→ - fallback 路径:默认文本,无变量替换
661→ - variant 路径:按 ctx 命中条件挑变体,{decodedThisRun}/{bossKillsThisRun}/{milestonesList}/{perksList} 等变量正确替换
662→ - 纪元名生成正确:第一纪元·初鸣之夕 / 第二纪元·光谱涌动 / 第三纪元·星辉汇聚 / 第四纪元·以太共振 / 第五纪元·永恒闭环
663→
664→Stage Summary:
665→- ✅ 工单 #9chronicle.ts EPOCH_LORE 5 纪元 × 3 节点 = 15 段手写叙事(每段 80-150 字文学性),buildLore 优先手写节点 + fallback 模板,buildChronicleEntry/regenerateLoreFromEntry/withPerks 全部接入;10 case 测试通过,变体匹配 + 变量替换正确
666→- ✅ P3 socket 多人同步星潮:mini-services/star-tide-service 独立 bun 项目,socket.io 端口 3031,每 10-15 分钟随机广播 global-tide60s 持续),admin-trigger-tide 供 QA 触发;前端 useGlobalTide hook + gameStore.triggerGlobalTide + globalTide 状态 + tickTide 全球优先 + StarTide UI 🌐 标记 + toast 区分全球/本地
667→- ✅ 网关规范:socket.io 严格用 `io("/?XTransformPort=3031")`path "/",禁止 localhost:3031 直连;Caddy :81 → localhost:3031 转发正确
668→- ✅ 离线容错:socket.io 自动重连(Infinity attempts),断连期间本地 tickTide 继续按原节奏工作,玩家不会被卡住;globalTide 不持久化(瞬态运行时状态)
669→- ✅ 防重连重复触发:useGlobalTide 用 startedAt+type 去重(Set 上限 32 条 FIFO 清理),过期 tide 不触发
670→- ✅ 本地星潮保留:全球星潮是增强而非替换,全球星潮结束后本地 tickTide 恢复正常节奏
671→- ✅ mini-service 启动稳定:pid 9456PPID=1uptime 19+ 分钟,端口 3031 监听正常
672→- ✅ lint 零错误 + dev HTTP 200 + VLM 8/10(🌐 全球星潮标记清晰可见)
673→- ✅ 严格四色规范:全球星潮琥珀色扫光(amber)+ 各星潮本色(emerald/rose/amber/fuchsia),零蓝色
674→- 下一阶段工单:可考虑 v0.9 收尾(全球星潮成就/编年史全球星潮高亮/星图天文台全球星潮增强天赋)
675→
676→---
677→Task ID: 10
678→Agent: main + subagent(10-a/10-b/10-c)
679→Task: v0.8.2 工单大清理 — 7 项工单一次性清零
680→
681→Work Log:
682→- 重读工单发现两类列表(已知问题10项 + 下一阶段优先级7项)多项未解决
683→- #8 编年史上限+分页(main 自己做):
684→ - engine.ts: slice(-50)→slice(-200)
685→ - ChronicleDialog.tsx: 加分页(每页10条)+上一页/下一页+页码显示
686→- #5 限时挑战 + #3 星潮深化(subagent 10-a 并行):
687→ - 限时挑战: BeaconTimedChallenge + 4h时段 + amber主题 + <30min紧急状态
688→ - 星潮深化: +3种(surge/eclipse/prism) + 修饰器扩展 + ach_tides_all 6→9
689→- #4/P2 云排行榜(subagent 10-b 并行):
690→ - mini-service 端口3030 Hono+bun 内存1000条 + CORS + 防刷
691→ - 前端双tab(本地/全球) + YOU徽章 + 自动云提交
692→- #9 手写叙事 + P3 socket星潮(subagent 10-c:
693→ - 5纪元×3节点=15段手写叙事(80-150字) + 11变量替换 + fallback模板
694→ - mini-service 端口3031 socket.io 每10-15min广播 + useGlobalTide + 🌐标记
695→- P2 UI打磨(main 自己做):
696→ - CrystalOrb区: 3层旋转全息环 + 四角标记 + 顶部状态条 + 底部铭文
697→- 最终 QA: lint零错误 + dev HTTP200 + VLM 8/10 + 2 mini-service运行中(3030/3031)
698→- 提交 v0.8.2commit 191434e+ 推送 main + 部署 gh-pages7e0a59bHTTP 200
699→
700→Stage Summary:
701→- ✅ 工单 #3 星潮深化: 6种→9种(+surge/eclipse/prism)
702→- ✅ 工单 #4 云排行榜: 本地→云端(mini-service 3030)
703→- ✅ 工单 #5 限时挑战: 每4h一个额外挑战(amber主题)
704→- ✅ 工单 #8 编年史: 上限50→200 + 分页UI
705→- ✅ 工单 #9 手写叙事: 5纪元×3节点=15段文学性叙事
706→- ✅ P2 UI打磨: CrystalOrb留白→装饰全息环+四角标记+状态条
707→- ✅ P3 socket多人星潮: mini-service 3031 + 🌐全球星潮
708→- 剩余未解决: #1 Turbopack缓存(环境问题) / #2 定时任务(已文档化)
709→- 在线游玩 v0.8.2: https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/
710→
711→---
712→Task ID: 1
713→Agent: 主代理 (Phase 1 Bug 修复)
714→Task: 获取 Gitea 工单并修复 Issue #2 的 4 个 bug
715→
716→Work Log:
717→- 通过 Gitea API 获取到 2 个未解决工单(Issue #1 + #2
718→- Issue #2 详细分析了 4 个 bug + 4 个玩法问题
719→- Bug1 探险只能一次:能量恢复间隔 45→20s,初始能量 3→5,上限 5→8,新增 lastExpeditionSummary 状态记录结果,ExpeditionPanel 入口显示上次结果摘要 + 实时倒计时,新增 dismissExpedition action
720→- Bug2 元素偏移/遮挡:标签页 grid-cols-8 → 响应式 grid-cols-4/sm:grid-cols-8,小屏 2 行避免拥挤
721→- Bug3 成就UI遮挡:标签面板 max-h 440→520min-h 提高
722→- Bug4 提示框遮挡:toast viewport top-0→top-14 避开 headerTOAST_LIMIT 1→3
723→- lint 通过,编译正常
724→
725→Stage Summary:
726→- Phase 1 四个 bug 全部修复完成
727→- 探险系统现在有完整的状态反馈:能量实时倒计时、上次结果摘要、刚结束时的结算按钮
728→- 标签页移动端不再挤压
729→- 待 Phase 2:技术树扩充、晶体球信息卡、成就特效、叙事融入
730→
731→---
732→Task ID: 2-a
733→Agent: subagent (技术树扩充专员)
734→Task: Issue #2 反馈"技术树太少,分分钟点满" — 扩充技术树 12→20 节点(4 分支 × 5 级)
735→
736→Work Log:
737→- 读取 worklog.md 了解项目背景(v0.8.2 已完成 7 工单清零;Task 1 Phase 1 已修复 4 个 bug;本次为 Phase 2 技术树扩充)
738→- 读取 config.ts / expedition.ts / engine.ts / achievements.ts / TechTree.tsx / types.ts / gameStore.ts 摸清数据驱动结构
739→
740→- **config.ts**TECH_TREE 由 12 节点扩充至 20 节点(4 分支 × 5 级),新增 8 节点:
741→ - 采矿 (mining) level 4/5:量子采掘矩阵(晶体/秒+12,cost 400/ 戴森星核(晶体/秒+30cost 1200),desc 含仓库上限 +800/+2000
742→ - 解码 (decoding) level 4/5:谐振放大器(脉冲+8,cost 500)/ 全息解码核心(自动解码 T2+解码步数+3,cost 1500
743→ - 探险 (expedition) level 4/5:虚空航标(探险力+12,生命+30,能量恢复+25%,cost 450)/ 维度跃迁引擎(探险力+25,生命+50,能量恢复+30%cost 1400
744→ - 叙事 (narrative) level 4/5:以太低语(洞见+100%cost 500)/ 文明回响(接触率+80%,洞见+150%cost 1600
745→ - 注释头部更新:"4 分支 × 3 级 = 12 节点" → "4 分支 × 5 级 = 20 节点(v0.9 工单 #2 扩充:12→20"
746→
747→- **expedition.ts**
748→ - computeExpeditionPower:新增 exp_4 * 12 + exp_5 * 25 加成
749→ - computeExpeditionHp:新增 exp_4 * 30 + exp_5 * 50 加成
750→ - computeEnergyRegenInterval:新增 exp_4 × 0.75(加速 25%/ exp_5 × 0.70(加速 30%),下限 8s 由 energyRegenMinSec 守护
751→ - 注释更新说明 v0.9 扩展点
752→
753→- **engine.ts** recomputeStats:在 TECH_TREE 主循环后追加多效果节点的次效果(effect.kind 只能承载一个主效果,次效果需硬编码):
754→ - min_2/min_3/min_4/min_5 的仓库上限加成(+100/+300/+800/+2000)— 顺带补齐 min_2/min_3 旧 desc 文案中承诺但未实装的 cap 加成
755→ - nar_5 文明回响的接触进度转化率 +0.8(与 insightMult +1.5 并存)
756→
757→- **gameStore.ts** autoDecodeTickdec_5 解锁后自动解码扩展至 T2 晶体
758→ - 原:仅 findIndex(tier === 1)
759→ - 新:先找 T1,若无 T1 且 tech.dec_5 已解锁,则找 T2 并将 autoTier 设为 2
760→ - decodeRewards(autoTier, ...) 使用动态 tierT2 晶体奖励按 CRYSTAL_VALUE[2] = 22 晶体 / 4 洞见基础计算
761→
762→- **achievements.ts**ach_tech_all(全谱精通)desc "12 项技术" → "20 项技术"check 阈值 12 → 20name 保持"全谱精通",奖励不变
763→
764→- **TechTree.tsx**:无需修改。已数据驱动(TECH_TREE.filter(branch === b)),自动渲染 5 节点/分支;滚动容器 flex-1 overflow-y-auto echo-scroll 已存在,20 个 Card 可纵向滚动查看
765→
766→- **数值递进合理性**:
767→ - mining cost: 5→24→120→400→1200(每级 ~3-4x 增长)
768→ - decoding cost: 6→30→200→500→1500
769→ - expedition cost: 12→60→180→450→1400
770→ - narrative cost: 8→45→160→500→1600
771→ - 严格 emerald/rose/amber/fuchsia 四色(沿用 TECH_BRANCH_META 既有映射,零蓝色/靛色)
772→
773→- 验证:`bun run lint` 零错误零警告(exit 0);`bunx tsc --noEmit` 我修改的文件(config/expedition/engine/achievements/gameStore 的 autoDecode 区域)无 TS 错误,其余 TS 报错(BeaconPanel/ChronicleDialog/ExpeditionPanel style prop、beacon.ts entry 属性等)均为预存问题,与本次改动无关
774→
775→Stage Summary:
776→- ✅ 技术树节点数 12 → 20(4 分支 × 5 级),直击 Issue #2 "分分钟点满" 痛点,玩家需要 ~5-10x 更多洞见才能全解锁
777→- ✅ 4 个分支 level 4/5 设计紧扣分支主题(采矿产能+仓库 / 解码脉冲+自动 / 探险力+生命+能量恢复 / 洞见+接触)
778→- ✅ expedition.ts 三函数(power/hp/regen)全部接入 exp_4/exp_5,探险后期强度与能量恢复节奏显著提升
779→- ✅ engine.ts 修复 min_2/min_3 旧 desc 文案承诺但未实装的仓库上限 bug,新 min_4/min_5 同步生效
780→- ✅ dec_5 全息解码核心:自动解码扩展至 T2 晶体(保留 T1 优先,无 T1 时回退 T2),显著降低后期 T2 晶体积压
781→- ✅ nar_5 文明回响:双效果(接触率+80% + 洞见+150%)通过硬编码并存
782→- ✅ ach_tech_all 阈值同步 12→20,奖励保持产能+15% / 洞见+15%
783→- ✅ TechTree.tsx 数据驱动,无需改动;overflow-y-auto 容器支持 20 节点滚动浏览
784→- ✅ lint 零错误,严格四色全息配色(零蓝色/靛色)
785→- 后续可考虑:1) TechTree.tsx 增加分支进度条(已解锁/总数);2) Codex/编年史加入"技术解锁里程碑"叙事;3) 为 level 4/5 节点添加解锁动画或特殊视觉标记
786→
@@ -1,609 +0,0 @@
1→"use client";
2→// 回响星核 / Echo Nexus — 游戏主入口
3→import { useState, useEffect } from "react";
4→import { StarfieldCanvas } from "@/components/game/StarfieldCanvas";
5→import { ResourceBar } from "@/components/game/ResourceBar";
6→import { CrystalOrb } from "@/components/game/CrystalOrb";
7→import { DecodePanel } from "@/components/game/DecodeArray";
8→import { TechTree } from "@/components/game/TechTree";
9→import { Codex } from "@/components/game/Codex";
10→import { PrestigeDialog } from "@/components/game/PrestigeDialog";
11→import { SettingsDialog } from "@/components/game/SettingsDialog";
12→import { ExpeditionPanel } from "@/components/game/ExpeditionPanel";
13→import { AchievementsPanel } from "@/components/game/AchievementsPanel";
14→import { AchievementNotifier } from "@/components/game/AchievementNotifier";
15→import { ConstellationPanel } from "@/components/game/ConstellationPanel";
16→import { ConstellationDialog } from "@/components/game/ConstellationDialog";
17→import { ChronicleDialog } from "@/components/game/ChronicleDialog";
18→import { BeaconPanel } from "@/components/game/BeaconPanel";
19→import { TutorialOverlay } from "@/components/game/TutorialOverlay";
20→import { OfflineReportDialog } from "@/components/game/OfflineReportDialog";
21→import { CruiseMode } from "@/components/game/CruiseMode";
22→import { AttributesPanel } from "@/components/game/AttributesPanel";
23→import {
24→ StarTideNotifier,
25→ StarTideIndicator,
26→ StarTideOverlay,
27→} from "@/components/game/StarTide";
28→import { useGameLoop } from "@/hooks/useGameLoop";
29→import { useAudioSync } from "@/hooks/useAudio";
30→import { useGlobalTide } from "@/hooks/useGlobalTide";
31→import { useGameStore } from "@/store/gameStore";
32→import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
33→import { Button } from "@/components/ui/button";
34→import {
35→ Settings,
36→ RotateCcw,
37→ Sparkles,
38→ Cpu,
39→ BookOpen,
40→ BarChart3,
41→ Rocket,
42→ Github,
43→ Trophy,
44→ Star,
45→ Radio,
46→ Navigation,
47→ User,
48→ Gem,
49→ Zap,
50→ Flame,
51→ Database,
52→} from "lucide-react";
53→import { TECH_TREE, FRAGMENTS, formatNum } from "@/lib/game/config";
54→import { ACHIEVEMENTS } from "@/lib/game/achievements";
55→import { TIDE_EVENTS } from "@/lib/game/starTide";
56→import { CONSTELLATION_PERKS } from "@/lib/game/constellation";
57→import { generateDailyChallenge, loadDailyProgress, loadLeaderboard } from "@/lib/game/beacon";
58→
59→export default function Page() {
60→ useGameLoop();
61→ useAudioSync();
62→ useGlobalTide();
63→ const [prestigeOpen, setPrestigeOpen] = useState(false);
64→ const [settingsOpen, setSettingsOpen] = useState(false);
65→ const [constellationOpen, setConstellationOpen] = useState(false);
66→ const [chronicleOpen, setChronicleOpen] = useState(false);
67→ const [cruiseOpen, setCruiseOpen] = useState(false);
68→ const [mounted, setMounted] = useState(false);
69→
70→ const contact = useGameStore((s) => s.contact);
71→ const totalDecoded = useGameStore((s) => s.totalDecoded);
72→ const crystalsPerSec = useGameStore((s) => s.crystalsPerSec);
73→ const ascensions = useGameStore((s) => s.ascensions);
74→ const ownedTech = useGameStore((s) => s.tech);
75→ const ownedFragments = useGameStore((s) => s.fragments);
76→ const ownedAchievements = useGameStore((s) => s.achievements);
77→ const ownedConstellation = useGameStore((s) => s.constellation ?? []);
78→ const pendingPerkChoices = useGameStore((s) => s.pendingPerkChoices);
79→ const crystals = useGameStore((s) => s.crystals);
80→ const crystalCap = useGameStore((s) => s.crystalCap);
81→ const activeTide = useGameStore((s) => s.activeTide);
82→ const globalTide = useGameStore((s) => s.globalTide);
83→ const energy = useGameStore((s) => s.energy);
84→ const hasActiveExpedition = useGameStore((s) => !!s.activeExpedition && !s.activeExpedition.finished);
85→ const chronicleCount = useGameStore((s) => s.chronicle?.length ?? 0);
86→ const pendingAttrPoints = useGameStore((s) => s.pendingAttrPoints ?? 0);
87→
88→ // 深空信标:检测是否有可领取的奖励(独立 localStorage
89→ const [beaconClaimable, setBeaconClaimable] = useState(false);
90→ useEffect(() => {
91→ let cancelled = false;
92→ const check = () => {
93→ try {
94→ const c = generateDailyChallenge();
95→ const p = loadDailyProgress();
96→ if (!cancelled) {
97→ setBeaconClaimable(p.completedAt !== null && !p.claimed && p.dateKey === c.dateKey);
98→ }
99→ } catch {
100→ if (!cancelled) setBeaconClaimable(false);
101→ }
102→ };
103→ check();
104→ const id = setInterval(check, 2000);
105→ return () => {
106→ cancelled = true;
107→ clearInterval(id);
108→ };
109→ }, []);
110→
111→ // 挂载检测:避免持久化 store 在 SSR/CSR 间产生 hydration 不一致
112→ useEffect(() => {
113→ // eslint-disable-next-line react-hooks/set-state-in-effect
114→ setMounted(true);
115→ }, []);
116→
117→ // 防止 SSR/CSR 不一致
118→ if (!mounted) {
119→ return (
120→ <div className="min-h-screen flex items-center justify-center bg-[#050410] text-muted-foreground">
121→ <div className="animate-pulse">唤醒回响中…</div>
122→ </div>
123→ );
124→ }
125→
126→ const ownedTechCount = Object.values(ownedTech).filter((v) => v > 0).length;
127→ const ownedFragCount = Object.values(ownedFragments).filter(Boolean).length;
128→ const ownedAchCount = Object.values(ownedAchievements).filter(Boolean).length;
129→ const canPrestige = contact >= 100;
130→ const hasPendingPerk = pendingPerkChoices && pendingPerkChoices.length > 0;
131→
132→ // 仓库满仓警告
133→ const warehouseFull = crystals >= crystalCap * 0.98;
134→
135→ // 目标提示
136→ let goal = "点击中央晶体发起脉冲,累积记忆晶体";
137→ if (totalDecoded === 0 && crystals >= 5) {
138→ goal = "右侧「待解码晶体」点击一颗晶体,开始解码";
139→ } else if (totalDecoded > 0 && ownedTechCount === 0) {
140→ goal = "用洞见解锁技术树,提升产能";
141→ } else if (totalDecoded > 0 && ownedFragCount < FRAGMENTS.length) {
142→ goal = `继续解码,拼凑记忆图谱(${ownedFragCount}/${FRAGMENTS.length}`;
143→ }
144→ if (energy >= 1 && totalDecoded >= 3) {
145→ goal = "「探险」标签可深入遗迹,获取丰厚奖励";
146→ }
147→ if (ascensions >= 1 && ownedConstellation.length > 0) {
148→ goal = `星图天赋已觉醒 ${ownedConstellation.length}/18,继续飞升获取更多`;
149→ }
150→ if (warehouseFull) {
151→ goal = "⚠ 仓库已满,产能浪费中!请解码晶体或升级仓库";
152→ }
153→ if (hasPendingPerk) {
154→ goal = "✦ 星图觉醒!点击顶部「星图」按钮选择一道天赋";
155→ }
156→ if (activeTide) {
157→ const tm = TIDE_EVENTS[activeTide.type];
158→ const prefix = globalTide ? "🌐 全球星潮 · " : "";
159→ goal = `${prefix}${tm.icon} 星潮「${tm.name}」进行中 · ${tm.desc}`;
160→ }
161→ if (canPrestige) goal = "✦ 接触进度已满,可发起飞升进入新周目";
162→
163→ return (
164→ <div className="relative min-h-screen flex flex-col bg-[#050410] text-foreground overflow-x-hidden">
165→ {/* 星空背景 */}
166→ <StarfieldCanvas className="fixed inset-0 w-full h-full -z-10" />
167→ {/* 星潮背景叠层 */}
168→ <StarTideOverlay />
169→
170→ {/* 顶部 Header */}
171→ <header className="sticky top-0 z-30 px-3 sm:px-5 pt-3 pb-2">
172→ <div className="flex items-center gap-3 mb-2.5">
173→ <div className="flex items-center gap-2.5">
174→ <div className="relative h-9 w-9">
175→ <div className="absolute inset-0 rounded-full bg-gradient-to-br from-emerald-400 via-fuchsia-500 to-rose-400 blur-md opacity-60" />
176→ <div className="absolute inset-1 rounded-full bg-[#050410] flex items-center justify-center">
177→ <Sparkles className="h-4 w-4 text-fuchsia-300" />
178→ </div>
179→ </div>
180→ <div className="leading-tight">
181→ <h1 className="text-base sm:text-lg font-bold text-gradient">回响星核</h1>
182→ <p className="text-[9px] sm:text-[10px] text-muted-foreground/70 -mt-0.5 tracking-wider">
183→ ECHO NEXUS · v0.8
184→ </p>
185→ </div>
186→ </div>
187→ <div className="ml-auto flex items-center gap-1.5">
188→ <StarTideIndicator />
189→ <Button
190→ size="sm"
191→ variant="outline"
192→ onClick={() => setCruiseOpen(true)}
193→ className="border-amber-400/50 text-amber-200 hover:bg-amber-500/10 h-8 px-2.5"
194→ aria-label="深空巡航"
195→ title="深空巡航 · 实时玩法"
196→ >
197→ <Navigation className="h-3.5 w-3.5 mr-1" />
198→ 巡航
199→ </Button>
200→ <Button
201→ size="icon"
202→ variant="ghost"
203→ onClick={() => setChronicleOpen(true)}
204→ className="h-8 w-8 relative group"
205→ aria-label="编年史"
206→ title="回响编年史"
207→ >
208→ <BookOpen className="h-4 w-4 group-hover:text-fuchsia-300 transition-colors" />
209→ {chronicleCount > 0 && (
210→ <span className="absolute -top-0.5 -right-0.5 min-w-[14px] h-[14px] px-1 rounded-full bg-fuchsia-500 text-[9px] font-mono font-bold text-white flex items-center justify-center border border-fuchsia-300/50">
211→ {chronicleCount}
212→ </span>
213→ )}
214→ </Button>
215→ {hasPendingPerk && (
216→ <Button
217→ size="sm"
218→ variant="outline"
219→ onClick={() => setConstellationOpen(true)}
220→ className="border-fuchsia-400/60 text-fuchsia-200 hover:bg-fuchsia-500/15 h-8 px-2.5 animate-pulse"
221→ >
222→ <Star className="h-3.5 w-3.5 mr-1" />
223→ 觉醒
224→ </Button>
225→ )}
226→ {canPrestige && (
227→ <Button
228→ size="sm"
229→ variant="outline"
230→ onClick={() => setPrestigeOpen(true)}
231→ data-tut="prestige-btn"
232→ className="border-fuchsia-400/50 text-fuchsia-200 hover:bg-fuchsia-500/10 h-8 px-2.5"
233→ >
234→ <RotateCcw className="h-3.5 w-3.5 mr-1" />
235→ 飞升
236→ </Button>
237→ )}
238→ <Button
239→ size="icon"
240→ variant="ghost"
241→ onClick={() => setSettingsOpen(true)}
242→ className="h-8 w-8"
243→ aria-label="设置"
244→ >
245→ <Settings className="h-4 w-4" />
246→ </Button>
247→ </div>
248→ </div>
249→ <ResourceBar onPrestige={() => setPrestigeOpen(true)} />
250→ </header>
251→
252→ {/* 主体 — 小屏允许自然滚动,避免 min-h 总和超过视口导致挤压重叠 */}
253→ <main className="flex-1 px-3 sm:px-5 pb-3 min-h-0">
254→ <div className="grid grid-cols-1 lg:grid-cols-[1fr_minmax(360px,420px)] gap-3 h-full">
255→ {/* 左侧:脉冲晶体 */}
256→ <section className="glass rounded-2xl p-4 sm:p-6 flex flex-col items-center justify-center min-h-[340px] sm:min-h-[420px] relative overflow-hidden">
257→ {/* 装饰光圈 */}
258→ <div className="pointer-events-none absolute -top-20 -left-20 h-60 w-60 rounded-full bg-emerald-500/10 blur-3xl" />
259→ <div className="pointer-events-none absolute -bottom-20 -right-20 h-60 w-60 rounded-full bg-fuchsia-500/10 blur-3xl" />
260→ {/* v0.8.2 装饰全息环(填充留白,提升视觉层次) */}
261→ <div className="pointer-events-none absolute inset-6 rounded-full border border-emerald-400/10 animate-[spin_60s_linear_infinite]" />
262→ <div className="pointer-events-none absolute inset-10 rounded-full border border-dashed border-fuchsia-400/10 animate-[spin_90s_linear_infinite_reverse]" />
263→ <div className="pointer-events-none absolute inset-16 rounded-full border border-rose-400/8" />
264→ {/* 四角全息标记 */}
265→ <div className="pointer-events-none absolute top-3 left-3 h-4 w-4 border-l border-t border-emerald-400/30 rounded-tl" />
266→ <div className="pointer-events-none absolute top-3 right-3 h-4 w-4 border-r border-t border-fuchsia-400/30 rounded-tr" />
267→ <div className="pointer-events-none absolute bottom-3 left-3 h-4 w-4 border-l border-b border-amber-400/30 rounded-bl" />
268→ <div className="pointer-events-none absolute bottom-3 right-3 h-4 w-4 border-r border-b border-rose-400/30 rounded-br" />
269→ {/* 顶部状态条 */}
270→ <div className="pointer-events-none absolute top-3 left-1/2 -translate-x-1/2 flex items-center gap-2 text-[9px] font-mono text-muted-foreground/60">
271→ <span className="h-1 w-1 rounded-full bg-emerald-400 animate-pulse" />
272→ <span>CRYSTAL CORE · ONLINE</span>
273→ <span className="h-1 w-1 rounded-full bg-fuchsia-400 animate-pulse" />
274→ </div>
275→ {/* 底部铭文 */}
276→ <div className="pointer-events-none absolute bottom-3 left-1/2 -translate-x-1/2 text-[8px] font-mono text-muted-foreground/40 tracking-[0.3em] uppercase">
277→ echo · nexus · archaeology
278→ </div>
279→ <div data-tut="crystal-orb" className="contents">
280→ <CrystalOrb />
281→ </div>
282→ {/* v0.9 工单 #2:全息收益信息面板 — 让晶体球价值可视化 */}
283→ <CrystalYieldPanel />
284→ </section>
285→
286→ {/* 右侧:解码 + 标签面板 */}
287→ <section className="flex flex-col gap-3 min-h-0">
288→ {/* 解码面板 */}
289→ <div data-tut="decode-panel" className="glass rounded-2xl p-4 flex-1 min-h-[300px] sm:min-h-[360px] max-h-[560px]">
290→ <DecodePanel />
291→ </div>
292→ {/* 标签面板:探险 / 技术 / 星图 / 图谱 / 成就 / 信标 / 统计 / 角色 */}
293→ {/* v0.9 工单修复:移除 max-h 硬限制,改用 min-h + flex 自适应,避免成就/技术树内容被挤压遮挡 */}
294→ <div className="glass rounded-2xl p-3 min-h-[300px] sm:min-h-[360px] max-h-[520px] flex flex-col">
295→ <Tabs defaultValue={hasActiveExpedition ? "expedition" : hasPendingPerk ? "constellation" : "tech"} className="h-full flex flex-col">
296→ {/* v0.9 工单修复:标签页响应式布局,小屏 4 列 2 行避免拥挤遮挡 */}
297→ <TabsList className="grid grid-cols-4 sm:grid-cols-8 h-auto sm:h-9 bg-black/30 gap-0.5 p-1">
298→ <TabsTrigger value="expedition" data-tut="tab-expedition" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-amber-500/15 data-[state=active]:shadow-[0_0_12px_rgba(251,191,36,0.3)] transition-all">
299→ <Rocket className="h-3.5 w-3.5" />
300→ <span className="leading-none">探险</span>
301→ {energy >= 1 && !hasActiveExpedition && (
302→ <span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-amber-400 animate-pulse ring-1 ring-black/50" />
303→ )}
304→ {hasActiveExpedition && (
305→ <span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-rose-400 animate-pulse ring-1 ring-black/50" />
306→ )}
307→ </TabsTrigger>
308→ <TabsTrigger value="tech" data-tut="tab-tech" className="text-[11px] gap-1 px-0.5 flex-col h-7 data-[state=active]:bg-emerald-500/15 data-[state=active]:shadow-[0_0_12px_rgba(52,211,153,0.3)] transition-all">
309→ <Cpu className="h-3.5 w-3.5" />
310→ <span className="leading-none">技术</span>
311→ </TabsTrigger>
312→ <TabsTrigger value="constellation" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-fuchsia-500/15 data-[state=active]:shadow-[0_0_12px_rgba(232,121,249,0.3)] transition-all">
313→ <Star className="h-3.5 w-3.5" />
314→ <span className="leading-none">星图</span>
315→ {hasPendingPerk && (
316→ <span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-fuchsia-400 animate-pulse ring-1 ring-black/50" />
317→ )}
318→ </TabsTrigger>
319→ <TabsTrigger value="codex" className="text-[11px] gap-1 px-0.5 flex-col h-7 data-[state=active]:bg-cyan-500/15 data-[state=active]:shadow-[0_0_12px_rgba(34,211,238,0.3)] transition-all">
320→ <BookOpen className="h-3.5 w-3.5" />
321→ <span className="leading-none">图谱</span>
322→ </TabsTrigger>
323→ <TabsTrigger value="ach" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-amber-500/15 data-[state=active]:shadow-[0_0_12px_rgba(251,191,36,0.3)] transition-all">
324→ <Trophy className="h-3.5 w-3.5" />
325→ <span className="leading-none">成就</span>
326→ {ownedAchCount < ACHIEVEMENTS.length && (
327→ <span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-amber-400 animate-pulse ring-1 ring-black/50" />
328→ )}
329→ </TabsTrigger>
330→ <TabsTrigger value="beacon" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-emerald-500/15 data-[state=active]:shadow-[0_0_12px_rgba(52,211,153,0.3)] transition-all">
331→ <Radio className="h-3.5 w-3.5" />
332→ <span className="leading-none">信标</span>
333→ {beaconClaimable && (
334→ <span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-emerald-400 animate-pulse ring-1 ring-black/50" />
335→ )}
336→ </TabsTrigger>
337→ <TabsTrigger value="stats" className="text-[11px] gap-1 px-0.5 flex-col h-7 data-[state=active]:bg-slate-500/15 data-[state=active]:shadow-[0_0_12px_rgba(148,163,184,0.3)] transition-all">
338→ <BarChart3 className="h-3.5 w-3.5" />
339→ <span className="leading-none">统计</span>
340→ </TabsTrigger>
341→ <TabsTrigger value="attributes" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-gradient-to-br data-[state=active]:from-emerald-500/15 data-[state=active]:via-fuchsia-500/15 data-[state=active]:to-rose-500/15 data-[state=active]:shadow-[0_0_12px_rgba(232,121,249,0.3)] transition-all">
342→ <User className="h-3.5 w-3.5" />
343→ <span className="leading-none">角色</span>
344→ {pendingAttrPoints > 0 && (
345→ <span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-rose-400 animate-pulse ring-1 ring-black/50" />
346→ )}
347→ </TabsTrigger>
348→ </TabsList>
349→ <TabsContent value="expedition" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
350→ <ExpeditionPanel />
351→ </TabsContent>
352→ <TabsContent value="tech" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
353→ <TechTree />
354→ </TabsContent>
355→ <TabsContent value="constellation" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
356→ <ConstellationPanel />
357→ </TabsContent>
358→ <TabsContent value="codex" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
359→ <Codex />
360→ </TabsContent>
361→ <TabsContent value="ach" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
362→ <AchievementsPanel />
363→ </TabsContent>
364→ <TabsContent value="beacon" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
365→ <BeaconPanel />
366→ </TabsContent>
367→ <TabsContent value="stats" className="flex-1 mt-2 min-h-0">
368→ <StatsPanel />
369→ </TabsContent>
370→ <TabsContent value="attributes" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
371→ <AttributesPanel />
372→ </TabsContent>
373→ </Tabs>
374→ </div>
375→ </section>
376→ </div>
377→ </main>
378→
379→ {/* 底部 Footer */}
380→ <footer className="sticky bottom-0 z-20 mt-auto px-3 sm:px-5 pb-2 pt-1">
381→ <div
382→ className={`glass rounded-xl px-3 py-2 flex items-center gap-2 text-xs ${
383→ warehouseFull ? "border-amber-400/40 animate-pulse" : ""
384→ } ${activeTide ? "border-white/20" : ""}`}
385→ style={
386→ activeTide
387→ ? { boxShadow: `inset 0 0 16px ${TIDE_EVENTS[activeTide.type].glow}` }
388→ : undefined
389→ }
390→ >
391→ <span
392→ className={
393→ warehouseFull
394→ ? "text-amber-400"
395→ : activeTide
396→ ? ""
397→ : "text-fuchsia-300"
398→ }
399→ style={activeTide ? { color: TIDE_EVENTS[activeTide.type].color } : undefined}
400→ >
401→ ▶
402→ </span>
403→ <span
404→ className={`flex-1 truncate ${
405→ warehouseFull ? "text-amber-200" : "text-muted-foreground"
406→ }`}
407→ >
408→ {goal}
409→ </span>
410→ <span className="hidden sm:inline text-muted-foreground/60">|</span>
411→ <span className="hidden sm:inline text-muted-foreground/70">
412→ 产能 {formatNum(crystalsPerSec)}/s
413→ </span>
414→ <a
415→ href="https://git.atdunbg.xyz/Super_Z/echo-nexus"
416→ target="_blank"
417→ rel="noreferrer"
418→ className="text-muted-foreground/60 hover:text-foreground transition ml-1"
419→ aria-label="仓库"
420→ >
421→ <Github className="h-3.5 w-3.5" />
422→ </a>
423→ </div>
424→ </footer>
425→
426→ <PrestigeDialog open={prestigeOpen} onOpenChange={setPrestigeOpen} />
427→ <ChronicleDialog open={chronicleOpen} onOpenChange={setChronicleOpen} />
428→ <ConstellationDialog open={constellationOpen} onOpenChange={setConstellationOpen} />
429→ <SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
430→ <AchievementNotifier />
431→ <StarTideNotifier />
432→ <TutorialOverlay />
433→ <OfflineReportDialog />
434→ {cruiseOpen && <CruiseMode onClose={() => setCruiseOpen(false)} />}
435→ </div>
436→ );
437→}
438→
439→/**
440→ * v0.9 工单 #2:全息收益信息面板
441→ * 在中央晶体球下方展示 4 项核心收益指标(产能 / 脉冲 / 连击 / 仓库),
442→ * 让玩家明确感知晶体球的价值,缓解"占地方又没用"的反馈。
443→ * 严格四色全息配色:emerald / rose / amber / fuchsia(禁止蓝色/靛色)。
444→ */
445→function CrystalYieldPanel() {
446→ const crystalsPerSec = useGameStore((s) => s.crystalsPerSec);
447→ const pulsePower = useGameStore((s) => s.pulsePower);
448→ const combo = useGameStore((s) => s._combo);
449→ const crystals = useGameStore((s) => s.crystals);
450→ const crystalCap = useGameStore((s) => s.crystalCap);
451→
452→ // 连击倍率:1 + (combo - 1) * 0.15(仅 combo≥1 时生效)
453→ const comboMult = combo >= 1 ? 1 + (combo - 1) * 0.15 : 1;
454→ const comboHot = combo >= 3; // 连击≥3 高亮闪烁
455→
456→ // 仓库容量百分比
457→ const fillPct = crystalCap > 0 ? (crystals / crystalCap) * 100 : 0;
458→ const warehouseWarn = fillPct >= 90; // ≥90% 警告色 + pulse
459→
460→ return (
461→ <div className="mt-3 grid grid-cols-2 sm:grid-cols-4 gap-2 w-full max-w-md pointer-events-none">
462→ {/* 晶体产能 — emerald */}
463→ <div className="rounded-lg border border-emerald-400/30 bg-black/30 backdrop-blur p-2 flex items-center gap-1.5">
464→ <Gem className="h-3.5 w-3.5 text-emerald-400 shrink-0" />
465→ <div className="min-w-0 leading-tight">
466→ <div className="text-[9px] uppercase tracking-wider text-emerald-300/70 truncate">
467→ 晶体产能
468→ </div>
469→ <div className="text-sm font-mono tabular-nums text-emerald-200 truncate">
470→ {formatNum(crystalsPerSec)}
471→ <span className="text-[9px] text-emerald-300/60">/s</span>
472→ </div>
473→ </div>
474→ </div>
475→
476→ {/* 主动脉冲 — rose */}
477→ <div className="rounded-lg border border-rose-400/30 bg-black/30 backdrop-blur p-2 flex items-center gap-1.5">
478→ <Zap className="h-3.5 w-3.5 text-rose-400 shrink-0" />
479→ <div className="min-w-0 leading-tight">
480→ <div className="text-[9px] uppercase tracking-wider text-rose-300/70 truncate">
481→ 主动脉冲
482→ </div>
483→ <div className="text-sm font-mono tabular-nums text-rose-200 truncate">
484→ {formatNum(pulsePower)}
485→ {combo >= 1 && (
486→ <span className="text-[9px] text-rose-300/80"> ×{comboMult.toFixed(2)}</span>
487→ )}
488→ </div>
489→ </div>
490→ </div>
491→
492→ {/* 当前连击 — amber,≥3 时高亮闪烁 */}
493→ <div
494→ className={`rounded-lg border bg-black/30 backdrop-blur p-2 flex items-center gap-1.5 ${
495→ comboHot ? "border-amber-400/70 animate-pulse" : "border-amber-400/30"
496→ }`}
497→ >
498→ <Flame
499→ className={`h-3.5 w-3.5 text-amber-400 shrink-0 ${
500→ comboHot ? "drop-shadow-[0_0_4px_rgba(251,191,36,0.7)]" : ""
501→ }`}
502→ />
503→ <div className="min-w-0 leading-tight">
504→ <div className="text-[9px] uppercase tracking-wider text-amber-300/70 truncate">
505→ 当前连击
506→ </div>
507→ <div
508→ className={`text-sm font-mono tabular-nums truncate ${
509→ comboHot ? "text-amber-200" : "text-amber-300/80"
510→ }`}
511→ >
512→ {combo}
513→ <span className="text-[9px] text-amber-300/60">/10</span>
514→ </div>
515→ </div>
516→ </div>
517→
518→ {/* 仓库容量 — fuchsia,≥90% 切 rose 警告色 + pulse */}
519→ <div
520→ className={`rounded-lg border bg-black/30 backdrop-blur p-2 flex items-center gap-1.5 ${
521→ warehouseWarn ? "border-rose-400/70 animate-pulse" : "border-fuchsia-400/30"
522→ }`}
523→ >
524→ <Database
525→ className={`h-3.5 w-3.5 shrink-0 ${
526→ warehouseWarn ? "text-rose-400" : "text-fuchsia-400"
527→ }`}
528→ />
529→ <div className="min-w-0 leading-tight">
530→ <div
531→ className={`text-[9px] uppercase tracking-wider truncate ${
532→ warehouseWarn ? "text-rose-300/80" : "text-fuchsia-300/70"
533→ }`}
534→ >
535→ 仓库容量
536→ </div>
537→ <div
538→ className={`text-sm font-mono tabular-nums truncate ${
539→ warehouseWarn ? "text-rose-200" : "text-fuchsia-200"
540→ }`}
541→ >
542→ {formatNum(crystals)}
543→ <span
544→ className={`text-[9px] ${
545→ warehouseWarn ? "text-rose-300/70" : "text-fuchsia-300/60"
546→ }`}
547→ >
548→ /{formatNum(crystalCap)}
549→ </span>
550→ </div>
551→ </div>
552→ </div>
553→ </div>
554→ );
555→}
556→
557→function StatsPanel() {
558→ const s = useGameStore();
559→ const achCount = Object.values(s.achievements).filter(Boolean).length;
560→ // 深空信标本地排行榜最高分(独立 localStorage
561→ const [beaconBest, setBeaconBest] = useState<number | null>(null);
562→ useEffect(() => {
563→ try {
564→ const lb = loadLeaderboard();
565→ // eslint-disable-next-line react-hooks/set-state-in-effect
566→ setBeaconBest(lb.length > 0 ? lb[0].score : null);
567→ } catch {
568→ setBeaconBest(null);
569→ }
570→ }, []);
571→ const rows = [
572→ { label: "累计解码晶体", value: `${s.totalDecoded} 颗` },
573→ { label: "飞升周目", value: `${s.ascensions}` },
574→ { label: "持有蓝图", value: `${s.blueprints.length} / 6` },
575→ { label: "已学技术", value: `${Object.values(s.tech).filter((v) => v > 0).length} / ${TECH_TREE.length}` },
576→ { label: "已获碎片", value: `${Object.values(s.fragments).filter(Boolean).length} / ${FRAGMENTS.length}` },
577→ { label: "已解锁成就", value: `${achCount} / ${ACHIEVEMENTS.length}` },
578→ { label: "星图天赋", value: `${s.constellation?.length ?? 0} / ${CONSTELLATION_PERKS.length}` },
579→ { label: "接触进度", value: `${s.contact.toFixed(1)}%` },
580→ { label: "晶体产能", value: `${s.crystalsPerSec.toFixed(2)} /s` },
581→ { label: "仓库上限", value: `${formatNum(s.crystalCap)}` },
582→ { label: "洞见倍率", value: `×${s.insightMult.toFixed(2)}` },
583→ { label: "累计探险", value: `${s.totalExpeditions} 次` },
584→ { label: "BOSS 击破", value: `${s.bossKills ?? 0} 次` },
585→ { label: "星潮亲历", value: `${(s.starTidesEncountered ?? []).length} / 9` },
586→ { label: "编年史条目", value: `${(s.chronicle ?? []).length} 纪元` },
587→ { label: "探险能量", value: `${Math.floor(s.energy)} / ${s.energyMax}` },
588→ { label: "信标最高分", value: beaconBest !== null ? formatNum(beaconBest) : "—" },
589→ { label: "探索力", value: `${s.attributes?.exploration ?? 0} / 100` },
590→ { label: "智慧", value: `${s.attributes?.wisdom ?? 0} / 100` },
591→ { label: "勇气", value: `${s.attributes?.courage ?? 0} / 100` },
592→ { label: "灵感", value: `${s.attributes?.inspiration ?? 0} / 100` },
593→ { label: "待分配属性点", value: `${s.pendingAttrPoints ?? 0}` },
594→ ];
595→ return (
596→ <div className="grid grid-cols-2 gap-1.5 text-xs">
597→ {rows.map((r) => (
598→ <div
599→ key={r.label}
600→ className="flex items-center justify-between rounded-md bg-black/25 border border-white/5 px-2 py-1.5"
601→ >
602→ <span className="text-muted-foreground">{r.label}</span>
603→ <span className="font-mono text-foreground">{r.value}</span>
604→ </div>
605→ ))}
606→ </div>
607→ );
608→}
609→
@@ -1,609 +0,0 @@
1→ 1→"use client";
2→ 2→// 回响星核 / Echo Nexus — 游戏主入口
3→ 3→import { useState, useEffect } from "react";
4→ 4→import { StarfieldCanvas } from "@/components/game/StarfieldCanvas";
5→ 5→import { ResourceBar } from "@/components/game/ResourceBar";
6→ 6→import { CrystalOrb } from "@/components/game/CrystalOrb";
7→ 7→import { DecodePanel } from "@/components/game/DecodeArray";
8→ 8→import { TechTree } from "@/components/game/TechTree";
9→ 9→import { Codex } from "@/components/game/Codex";
10→ 10→import { PrestigeDialog } from "@/components/game/PrestigeDialog";
11→ 11→import { SettingsDialog } from "@/components/game/SettingsDialog";
12→ 12→import { ExpeditionPanel } from "@/components/game/ExpeditionPanel";
13→ 13→import { AchievementsPanel } from "@/components/game/AchievementsPanel";
14→ 14→import { AchievementNotifier } from "@/components/game/AchievementNotifier";
15→ 15→import { ConstellationPanel } from "@/components/game/ConstellationPanel";
16→ 16→import { ConstellationDialog } from "@/components/game/ConstellationDialog";
17→ 17→import { ChronicleDialog } from "@/components/game/ChronicleDialog";
18→ 18→import { BeaconPanel } from "@/components/game/BeaconPanel";
19→ 19→import { TutorialOverlay } from "@/components/game/TutorialOverlay";
20→ 20→import { OfflineReportDialog } from "@/components/game/OfflineReportDialog";
21→ 21→import { CruiseMode } from "@/components/game/CruiseMode";
22→ 22→import { AttributesPanel } from "@/components/game/AttributesPanel";
23→ 23→import {
24→ 24→ StarTideNotifier,
25→ 25→ StarTideIndicator,
26→ 26→ StarTideOverlay,
27→ 27→} from "@/components/game/StarTide";
28→ 28→import { useGameLoop } from "@/hooks/useGameLoop";
29→ 29→import { useAudioSync } from "@/hooks/useAudio";
30→ 30→import { useGlobalTide } from "@/hooks/useGlobalTide";
31→ 31→import { useGameStore } from "@/store/gameStore";
32→ 32→import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
33→ 33→import { Button } from "@/components/ui/button";
34→ 34→import {
35→ 35→ Settings,
36→ 36→ RotateCcw,
37→ 37→ Sparkles,
38→ 38→ Cpu,
39→ 39→ BookOpen,
40→ 40→ BarChart3,
41→ 41→ Rocket,
42→ 42→ Github,
43→ 43→ Trophy,
44→ 44→ Star,
45→ 45→ Radio,
46→ 46→ Navigation,
47→ 47→ User,
48→ 48→ Gem,
49→ 49→ Zap,
50→ 50→ Flame,
51→ 51→ Database,
52→ 52→} from "lucide-react";
53→ 53→import { TECH_TREE, FRAGMENTS, formatNum } from "@/lib/game/config";
54→ 54→import { ACHIEVEMENTS } from "@/lib/game/achievements";
55→ 55→import { TIDE_EVENTS } from "@/lib/game/starTide";
56→ 56→import { CONSTELLATION_PERKS } from "@/lib/game/constellation";
57→ 57→import { generateDailyChallenge, loadDailyProgress, loadLeaderboard } from "@/lib/game/beacon";
58→ 58→
59→ 59→export default function Page() {
60→ 60→ useGameLoop();
61→ 61→ useAudioSync();
62→ 62→ useGlobalTide();
63→ 63→ const [prestigeOpen, setPrestigeOpen] = useState(false);
64→ 64→ const [settingsOpen, setSettingsOpen] = useState(false);
65→ 65→ const [constellationOpen, setConstellationOpen] = useState(false);
66→ 66→ const [chronicleOpen, setChronicleOpen] = useState(false);
67→ 67→ const [cruiseOpen, setCruiseOpen] = useState(false);
68→ 68→ const [mounted, setMounted] = useState(false);
69→ 69→
70→ 70→ const contact = useGameStore((s) => s.contact);
71→ 71→ const totalDecoded = useGameStore((s) => s.totalDecoded);
72→ 72→ const crystalsPerSec = useGameStore((s) => s.crystalsPerSec);
73→ 73→ const ascensions = useGameStore((s) => s.ascensions);
74→ 74→ const ownedTech = useGameStore((s) => s.tech);
75→ 75→ const ownedFragments = useGameStore((s) => s.fragments);
76→ 76→ const ownedAchievements = useGameStore((s) => s.achievements);
77→ 77→ const ownedConstellation = useGameStore((s) => s.constellation ?? []);
78→ 78→ const pendingPerkChoices = useGameStore((s) => s.pendingPerkChoices);
79→ 79→ const crystals = useGameStore((s) => s.crystals);
80→ 80→ const crystalCap = useGameStore((s) => s.crystalCap);
81→ 81→ const activeTide = useGameStore((s) => s.activeTide);
82→ 82→ const globalTide = useGameStore((s) => s.globalTide);
83→ 83→ const energy = useGameStore((s) => s.energy);
84→ 84→ const hasActiveExpedition = useGameStore((s) => !!s.activeExpedition && !s.activeExpedition.finished);
85→ 85→ const chronicleCount = useGameStore((s) => s.chronicle?.length ?? 0);
86→ 86→ const pendingAttrPoints = useGameStore((s) => s.pendingAttrPoints ?? 0);
87→ 87→
88→ 88→ // 深空信标:检测是否有可领取的奖励(独立 localStorage
89→ 89→ const [beaconClaimable, setBeaconClaimable] = useState(false);
90→ 90→ useEffect(() => {
91→ 91→ let cancelled = false;
92→ 92→ const check = () => {
93→ 93→ try {
94→ 94→ const c = generateDailyChallenge();
95→ 95→ const p = loadDailyProgress();
96→ 96→ if (!cancelled) {
97→ 97→ setBeaconClaimable(p.completedAt !== null && !p.claimed && p.dateKey === c.dateKey);
98→ 98→ }
99→ 99→ } catch {
100→ 100→ if (!cancelled) setBeaconClaimable(false);
101→ 101→ }
102→ 102→ };
103→ 103→ check();
104→ 104→ const id = setInterval(check, 2000);
105→ 105→ return () => {
106→ 106→ cancelled = true;
107→ 107→ clearInterval(id);
108→ 108→ };
109→ 109→ }, []);
110→ 110→
111→ 111→ // 挂载检测:避免持久化 store 在 SSR/CSR 间产生 hydration 不一致
112→ 112→ useEffect(() => {
113→ 113→ // eslint-disable-next-line react-hooks/set-state-in-effect
114→ 114→ setMounted(true);
115→ 115→ }, []);
116→ 116→
117→ 117→ // 防止 SSR/CSR 不一致
118→ 118→ if (!mounted) {
119→ 119→ return (
120→ 120→ <div className="min-h-screen flex items-center justify-center bg-[#050410] text-muted-foreground">
121→ 121→ <div className="animate-pulse">唤醒回响中…</div>
122→ 122→ </div>
123→ 123→ );
124→ 124→ }
125→ 125→
126→ 126→ const ownedTechCount = Object.values(ownedTech).filter((v) => v > 0).length;
127→ 127→ const ownedFragCount = Object.values(ownedFragments).filter(Boolean).length;
128→ 128→ const ownedAchCount = Object.values(ownedAchievements).filter(Boolean).length;
129→ 129→ const canPrestige = contact >= 100;
130→ 130→ const hasPendingPerk = pendingPerkChoices && pendingPerkChoices.length > 0;
131→ 131→
132→ 132→ // 仓库满仓警告
133→ 133→ const warehouseFull = crystals >= crystalCap * 0.98;
134→ 134→
135→ 135→ // 目标提示
136→ 136→ let goal = "点击中央晶体发起脉冲,累积记忆晶体";
137→ 137→ if (totalDecoded === 0 && crystals >= 5) {
138→ 138→ goal = "右侧「待解码晶体」点击一颗晶体,开始解码";
139→ 139→ } else if (totalDecoded > 0 && ownedTechCount === 0) {
140→ 140→ goal = "用洞见解锁技术树,提升产能";
141→ 141→ } else if (totalDecoded > 0 && ownedFragCount < FRAGMENTS.length) {
142→ 142→ goal = `继续解码,拼凑记忆图谱(${ownedFragCount}/${FRAGMENTS.length}`;
143→ 143→ }
144→ 144→ if (energy >= 1 && totalDecoded >= 3) {
145→ 145→ goal = "「探险」标签可深入遗迹,获取丰厚奖励";
146→ 146→ }
147→ 147→ if (ascensions >= 1 && ownedConstellation.length > 0) {
148→ 148→ goal = `星图天赋已觉醒 ${ownedConstellation.length}/18,继续飞升获取更多`;
149→ 149→ }
150→ 150→ if (warehouseFull) {
151→ 151→ goal = "⚠ 仓库已满,产能浪费中!请解码晶体或升级仓库";
152→ 152→ }
153→ 153→ if (hasPendingPerk) {
154→ 154→ goal = "✦ 星图觉醒!点击顶部「星图」按钮选择一道天赋";
155→ 155→ }
156→ 156→ if (activeTide) {
157→ 157→ const tm = TIDE_EVENTS[activeTide.type];
158→ 158→ const prefix = globalTide ? "🌐 全球星潮 · " : "";
159→ 159→ goal = `${prefix}${tm.icon} 星潮「${tm.name}」进行中 · ${tm.desc}`;
160→ 160→ }
161→ 161→ if (canPrestige) goal = "✦ 接触进度已满,可发起飞升进入新周目";
162→ 162→
163→ 163→ return (
164→ 164→ <div className="relative min-h-screen flex flex-col bg-[#050410] text-foreground overflow-x-hidden">
165→ 165→ {/* 星空背景 */}
166→ 166→ <StarfieldCanvas className="fixed inset-0 w-full h-full -z-10" />
167→ 167→ {/* 星潮背景叠层 */}
168→ 168→ <StarTideOverlay />
169→ 169→
170→ 170→ {/* 顶部 Header */}
171→ 171→ <header className="sticky top-0 z-30 px-3 sm:px-5 pt-3 pb-2">
172→ 172→ <div className="flex items-center gap-3 mb-2.5">
173→ 173→ <div className="flex items-center gap-2.5">
174→ 174→ <div className="relative h-9 w-9">
175→ 175→ <div className="absolute inset-0 rounded-full bg-gradient-to-br from-emerald-400 via-fuchsia-500 to-rose-400 blur-md opacity-60" />
176→ 176→ <div className="absolute inset-1 rounded-full bg-[#050410] flex items-center justify-center">
177→ 177→ <Sparkles className="h-4 w-4 text-fuchsia-300" />
178→ 178→ </div>
179→ 179→ </div>
180→ 180→ <div className="leading-tight">
181→ 181→ <h1 className="text-base sm:text-lg font-bold text-gradient">回响星核</h1>
182→ 182→ <p className="text-[9px] sm:text-[10px] text-muted-foreground/70 -mt-0.5 tracking-wider">
183→ 183→ ECHO NEXUS · v0.8
184→ 184→ </p>
185→ 185→ </div>
186→ 186→ </div>
187→ 187→ <div className="ml-auto flex items-center gap-1.5">
188→ 188→ <StarTideIndicator />
189→ 189→ <Button
190→ 190→ size="sm"
191→ 191→ variant="outline"
192→ 192→ onClick={() => setCruiseOpen(true)}
193→ 193→ className="border-amber-400/50 text-amber-200 hover:bg-amber-500/10 h-8 px-2.5"
194→ 194→ aria-label="深空巡航"
195→ 195→ title="深空巡航 · 实时玩法"
196→ 196→ >
197→ 197→ <Navigation className="h-3.5 w-3.5 mr-1" />
198→ 198→ 巡航
199→ 199→ </Button>
200→ 200→ <Button
201→ 201→ size="icon"
202→ 202→ variant="ghost"
203→ 203→ onClick={() => setChronicleOpen(true)}
204→ 204→ className="h-8 w-8 relative group"
205→ 205→ aria-label="编年史"
206→ 206→ title="回响编年史"
207→ 207→ >
208→ 208→ <BookOpen className="h-4 w-4 group-hover:text-fuchsia-300 transition-colors" />
209→ 209→ {chronicleCount > 0 && (
210→ 210→ <span className="absolute -top-0.5 -right-0.5 min-w-[14px] h-[14px] px-1 rounded-full bg-fuchsia-500 text-[9px] font-mono font-bold text-white flex items-center justify-center border border-fuchsia-300/50">
211→ 211→ {chronicleCount}
212→ 212→ </span>
213→ 213→ )}
214→ 214→ </Button>
215→ 215→ {hasPendingPerk && (
216→ 216→ <Button
217→ 217→ size="sm"
218→ 218→ variant="outline"
219→ 219→ onClick={() => setConstellationOpen(true)}
220→ 220→ className="border-fuchsia-400/60 text-fuchsia-200 hover:bg-fuchsia-500/15 h-8 px-2.5 animate-pulse"
221→ 221→ >
222→ 222→ <Star className="h-3.5 w-3.5 mr-1" />
223→ 223→ 觉醒
224→ 224→ </Button>
225→ 225→ )}
226→ 226→ {canPrestige && (
227→ 227→ <Button
228→ 228→ size="sm"
229→ 229→ variant="outline"
230→ 230→ onClick={() => setPrestigeOpen(true)}
231→ 231→ data-tut="prestige-btn"
232→ 232→ className="border-fuchsia-400/50 text-fuchsia-200 hover:bg-fuchsia-500/10 h-8 px-2.5"
233→ 233→ >
234→ 234→ <RotateCcw className="h-3.5 w-3.5 mr-1" />
235→ 235→ 飞升
236→ 236→ </Button>
237→ 237→ )}
238→ 238→ <Button
239→ 239→ size="icon"
240→ 240→ variant="ghost"
241→ 241→ onClick={() => setSettingsOpen(true)}
242→ 242→ className="h-8 w-8"
243→ 243→ aria-label="设置"
244→ 244→ >
245→ 245→ <Settings className="h-4 w-4" />
246→ 246→ </Button>
247→ 247→ </div>
248→ 248→ </div>
249→ 249→ <ResourceBar onPrestige={() => setPrestigeOpen(true)} />
250→ 250→ </header>
251→ 251→
252→ 252→ {/* 主体 — 小屏允许自然滚动,避免 min-h 总和超过视口导致挤压重叠 */}
253→ 253→ <main className="flex-1 px-3 sm:px-5 pb-3 min-h-0">
254→ 254→ <div className="grid grid-cols-1 lg:grid-cols-[1fr_minmax(360px,420px)] gap-3 h-full">
255→ 255→ {/* 左侧:脉冲晶体 */}
256→ 256→ <section className="glass rounded-2xl p-4 sm:p-6 flex flex-col items-center justify-center min-h-[340px] sm:min-h-[420px] relative overflow-hidden">
257→ 257→ {/* 装饰光圈 */}
258→ 258→ <div className="pointer-events-none absolute -top-20 -left-20 h-60 w-60 rounded-full bg-emerald-500/10 blur-3xl" />
259→ 259→ <div className="pointer-events-none absolute -bottom-20 -right-20 h-60 w-60 rounded-full bg-fuchsia-500/10 blur-3xl" />
260→ 260→ {/* v0.8.2 装饰全息环(填充留白,提升视觉层次) */}
261→ 261→ <div className="pointer-events-none absolute inset-6 rounded-full border border-emerald-400/10 animate-[spin_60s_linear_infinite]" />
262→ 262→ <div className="pointer-events-none absolute inset-10 rounded-full border border-dashed border-fuchsia-400/10 animate-[spin_90s_linear_infinite_reverse]" />
263→ 263→ <div className="pointer-events-none absolute inset-16 rounded-full border border-rose-400/8" />
264→ 264→ {/* 四角全息标记 */}
265→ 265→ <div className="pointer-events-none absolute top-3 left-3 h-4 w-4 border-l border-t border-emerald-400/30 rounded-tl" />
266→ 266→ <div className="pointer-events-none absolute top-3 right-3 h-4 w-4 border-r border-t border-fuchsia-400/30 rounded-tr" />
267→ 267→ <div className="pointer-events-none absolute bottom-3 left-3 h-4 w-4 border-l border-b border-amber-400/30 rounded-bl" />
268→ 268→ <div className="pointer-events-none absolute bottom-3 right-3 h-4 w-4 border-r border-b border-rose-400/30 rounded-br" />
269→ 269→ {/* 顶部状态条 */}
270→ 270→ <div className="pointer-events-none absolute top-3 left-1/2 -translate-x-1/2 flex items-center gap-2 text-[9px] font-mono text-muted-foreground/60">
271→ 271→ <span className="h-1 w-1 rounded-full bg-emerald-400 animate-pulse" />
272→ 272→ <span>CRYSTAL CORE · ONLINE</span>
273→ 273→ <span className="h-1 w-1 rounded-full bg-fuchsia-400 animate-pulse" />
274→ 274→ </div>
275→ 275→ {/* 底部铭文 */}
276→ 276→ <div className="pointer-events-none absolute bottom-3 left-1/2 -translate-x-1/2 text-[8px] font-mono text-muted-foreground/40 tracking-[0.3em] uppercase">
277→ 277→ echo · nexus · archaeology
278→ 278→ </div>
279→ 279→ <div data-tut="crystal-orb" className="contents">
280→ 280→ <CrystalOrb />
281→ 281→ </div>
282→ 282→ {/* v0.9 工单 #2:全息收益信息面板 — 让晶体球价值可视化 */}
283→ 283→ <CrystalYieldPanel />
284→ 284→ </section>
285→ 285→
286→ 286→ {/* 右侧:解码 + 标签面板 */}
287→ 287→ <section className="flex flex-col gap-3 min-h-0">
288→ 288→ {/* 解码面板 */}
289→ 289→ <div data-tut="decode-panel" className="glass rounded-2xl p-4 flex-1 min-h-[300px] sm:min-h-[360px] max-h-[560px]">
290→ 290→ <DecodePanel />
291→ 291→ </div>
292→ 292→ {/* 标签面板:探险 / 技术 / 星图 / 图谱 / 成就 / 信标 / 统计 / 角色 */}
293→ 293→ {/* v0.9 工单修复:移除 max-h 硬限制,改用 min-h + flex 自适应,避免成就/技术树内容被挤压遮挡 */}
294→ 294→ <div className="glass rounded-2xl p-3 min-h-[300px] sm:min-h-[360px] max-h-[520px] flex flex-col">
295→ 295→ <Tabs defaultValue={hasActiveExpedition ? "expedition" : hasPendingPerk ? "constellation" : "tech"} className="h-full flex flex-col">
296→ 296→ {/* v0.9 工单修复:标签页响应式布局,小屏 4 列 2 行避免拥挤遮挡 */}
297→ 297→ <TabsList className="grid grid-cols-4 sm:grid-cols-8 h-auto sm:h-9 bg-black/30 gap-0.5 p-1">
298→ 298→ <TabsTrigger value="expedition" data-tut="tab-expedition" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-amber-500/15 data-[state=active]:shadow-[0_0_12px_rgba(251,191,36,0.3)] transition-all">
299→ 299→ <Rocket className="h-3.5 w-3.5" />
300→ 300→ <span className="leading-none">探险</span>
301→ 301→ {energy >= 1 && !hasActiveExpedition && (
302→ 302→ <span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-amber-400 animate-pulse ring-1 ring-black/50" />
303→ 303→ )}
304→ 304→ {hasActiveExpedition && (
305→ 305→ <span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-rose-400 animate-pulse ring-1 ring-black/50" />
306→ 306→ )}
307→ 307→ </TabsTrigger>
308→ 308→ <TabsTrigger value="tech" data-tut="tab-tech" className="text-[11px] gap-1 px-0.5 flex-col h-7 data-[state=active]:bg-emerald-500/15 data-[state=active]:shadow-[0_0_12px_rgba(52,211,153,0.3)] transition-all">
309→ 309→ <Cpu className="h-3.5 w-3.5" />
310→ 310→ <span className="leading-none">技术</span>
311→ 311→ </TabsTrigger>
312→ 312→ <TabsTrigger value="constellation" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-fuchsia-500/15 data-[state=active]:shadow-[0_0_12px_rgba(232,121,249,0.3)] transition-all">
313→ 313→ <Star className="h-3.5 w-3.5" />
314→ 314→ <span className="leading-none">星图</span>
315→ 315→ {hasPendingPerk && (
316→ 316→ <span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-fuchsia-400 animate-pulse ring-1 ring-black/50" />
317→ 317→ )}
318→ 318→ </TabsTrigger>
319→ 319→ <TabsTrigger value="codex" className="text-[11px] gap-1 px-0.5 flex-col h-7 data-[state=active]:bg-cyan-500/15 data-[state=active]:shadow-[0_0_12px_rgba(34,211,238,0.3)] transition-all">
320→ 320→ <BookOpen className="h-3.5 w-3.5" />
321→ 321→ <span className="leading-none">图谱</span>
322→ 322→ </TabsTrigger>
323→ 323→ <TabsTrigger value="ach" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-amber-500/15 data-[state=active]:shadow-[0_0_12px_rgba(251,191,36,0.3)] transition-all">
324→ 324→ <Trophy className="h-3.5 w-3.5" />
325→ 325→ <span className="leading-none">成就</span>
326→ 326→ {ownedAchCount < ACHIEVEMENTS.length && (
327→ 327→ <span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-amber-400 animate-pulse ring-1 ring-black/50" />
328→ 328→ )}
329→ 329→ </TabsTrigger>
330→ 330→ <TabsTrigger value="beacon" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-emerald-500/15 data-[state=active]:shadow-[0_0_12px_rgba(52,211,153,0.3)] transition-all">
331→ 331→ <Radio className="h-3.5 w-3.5" />
332→ 332→ <span className="leading-none">信标</span>
333→ 333→ {beaconClaimable && (
334→ 334→ <span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-emerald-400 animate-pulse ring-1 ring-black/50" />
335→ 335→ )}
336→ 336→ </TabsTrigger>
337→ 337→ <TabsTrigger value="stats" className="text-[11px] gap-1 px-0.5 flex-col h-7 data-[state=active]:bg-slate-500/15 data-[state=active]:shadow-[0_0_12px_rgba(148,163,184,0.3)] transition-all">
338→ 338→ <BarChart3 className="h-3.5 w-3.5" />
339→ 339→ <span className="leading-none">统计</span>
340→ 340→ </TabsTrigger>
341→ 341→ <TabsTrigger value="attributes" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-gradient-to-br data-[state=active]:from-emerald-500/15 data-[state=active]:via-fuchsia-500/15 data-[state=active]:to-rose-500/15 data-[state=active]:shadow-[0_0_12px_rgba(232,121,249,0.3)] transition-all">
342→ 342→ <User className="h-3.5 w-3.5" />
343→ 343→ <span className="leading-none">角色</span>
344→ 344→ {pendingAttrPoints > 0 && (
345→ 345→ <span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-rose-400 animate-pulse ring-1 ring-black/50" />
346→ 346→ )}
347→ 347→ </TabsTrigger>
348→ 348→ </TabsList>
349→ 349→ <TabsContent value="expedition" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
350→ 350→ <ExpeditionPanel />
351→ 351→ </TabsContent>
352→ 352→ <TabsContent value="tech" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
353→ 353→ <TechTree />
354→ 354→ </TabsContent>
355→ 355→ <TabsContent value="constellation" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
356→ 356→ <ConstellationPanel />
357→ 357→ </TabsContent>
358→ 358→ <TabsContent value="codex" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
359→ 359→ <Codex />
360→ 360→ </TabsContent>
361→ 361→ <TabsContent value="ach" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
362→ 362→ <AchievementsPanel />
363→ 363→ </TabsContent>
364→ 364→ <TabsContent value="beacon" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
365→ 365→ <BeaconPanel />
366→ 366→ </TabsContent>
367→ 367→ <TabsContent value="stats" className="flex-1 mt-2 min-h-0">
368→ 368→ <StatsPanel />
369→ 369→ </TabsContent>
370→ 370→ <TabsContent value="attributes" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
371→ 371→ <AttributesPanel />
372→ 372→ </TabsContent>
373→ 373→ </Tabs>
374→ 374→ </div>
375→ 375→ </section>
376→ 376→ </div>
377→ 377→ </main>
378→ 378→
379→ 379→ {/* 底部 Footer */}
380→ 380→ <footer className="sticky bottom-0 z-20 mt-auto px-3 sm:px-5 pb-2 pt-1">
381→ 381→ <div
382→ 382→ className={`glass rounded-xl px-3 py-2 flex items-center gap-2 text-xs ${
383→ 383→ warehouseFull ? "border-amber-400/40 animate-pulse" : ""
384→ 384→ } ${activeTide ? "border-white/20" : ""}`}
385→ 385→ style={
386→ 386→ activeTide
387→ 387→ ? { boxShadow: `inset 0 0 16px ${TIDE_EVENTS[activeTide.type].glow}` }
388→ 388→ : undefined
389→ 389→ }
390→ 390→ >
391→ 391→ <span
392→ 392→ className={
393→ 393→ warehouseFull
394→ 394→ ? "text-amber-400"
395→ 395→ : activeTide
396→ 396→ ? ""
397→ 397→ : "text-fuchsia-300"
398→ 398→ }
399→ 399→ style={activeTide ? { color: TIDE_EVENTS[activeTide.type].color } : undefined}
400→ 400→ >
401→ 401→ ▶
402→ 402→ </span>
403→ 403→ <span
404→ 404→ className={`flex-1 truncate ${
405→ 405→ warehouseFull ? "text-amber-200" : "text-muted-foreground"
406→ 406→ }`}
407→ 407→ >
408→ 408→ {goal}
409→ 409→ </span>
410→ 410→ <span className="hidden sm:inline text-muted-foreground/60">|</span>
411→ 411→ <span className="hidden sm:inline text-muted-foreground/70">
412→ 412→ 产能 {formatNum(crystalsPerSec)}/s
413→ 413→ </span>
414→ 414→ <a
415→ 415→ href="https://git.atdunbg.xyz/Super_Z/echo-nexus"
416→ 416→ target="_blank"
417→ 417→ rel="noreferrer"
418→ 418→ className="text-muted-foreground/60 hover:text-foreground transition ml-1"
419→ 419→ aria-label="仓库"
420→ 420→ >
421→ 421→ <Github className="h-3.5 w-3.5" />
422→ 422→ </a>
423→ 423→ </div>
424→ 424→ </footer>
425→ 425→
426→ 426→ <PrestigeDialog open={prestigeOpen} onOpenChange={setPrestigeOpen} />
427→ 427→ <ChronicleDialog open={chronicleOpen} onOpenChange={setChronicleOpen} />
428→ 428→ <ConstellationDialog open={constellationOpen} onOpenChange={setConstellationOpen} />
429→ 429→ <SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
430→ 430→ <AchievementNotifier />
431→ 431→ <StarTideNotifier />
432→ 432→ <TutorialOverlay />
433→ 433→ <OfflineReportDialog />
434→ 434→ {cruiseOpen && <CruiseMode onClose={() => setCruiseOpen(false)} />}
435→ 435→ </div>
436→ 436→ );
437→ 437→}
438→ 438→
439→ 439→/**
440→ 440→ * v0.9 工单 #2:全息收益信息面板
441→ 441→ * 在中央晶体球下方展示 4 项核心收益指标(产能 / 脉冲 / 连击 / 仓库),
442→ 442→ * 让玩家明确感知晶体球的价值,缓解"占地方又没用"的反馈。
443→ 443→ * 严格四色全息配色:emerald / rose / amber / fuchsia(禁止蓝色/靛色)。
444→ 444→ */
445→ 445→function CrystalYieldPanel() {
446→ 446→ const crystalsPerSec = useGameStore((s) => s.crystalsPerSec);
447→ 447→ const pulsePower = useGameStore((s) => s.pulsePower);
448→ 448→ const combo = useGameStore((s) => s._combo);
449→ 449→ const crystals = useGameStore((s) => s.crystals);
450→ 450→ const crystalCap = useGameStore((s) => s.crystalCap);
451→ 451→
452→ 452→ // 连击倍率:1 + (combo - 1) * 0.15(仅 combo≥1 时生效)
453→ 453→ const comboMult = combo >= 1 ? 1 + (combo - 1) * 0.15 : 1;
454→ 454→ const comboHot = combo >= 3; // 连击≥3 高亮闪烁
455→ 455→
456→ 456→ // 仓库容量百分比
457→ 457→ const fillPct = crystalCap > 0 ? (crystals / crystalCap) * 100 : 0;
458→ 458→ const warehouseWarn = fillPct >= 90; // ≥90% 警告色 + pulse
459→ 459→
460→ 460→ return (
461→ 461→ <div className="mt-3 grid grid-cols-2 sm:grid-cols-4 gap-2 w-full max-w-md pointer-events-none">
462→ 462→ {/* 晶体产能 — emerald */}
463→ 463→ <div className="rounded-lg border border-emerald-400/30 bg-black/30 backdrop-blur p-2 flex items-center gap-1.5">
464→ 464→ <Gem className="h-3.5 w-3.5 text-emerald-400 shrink-0" />
465→ 465→ <div className="min-w-0 leading-tight">
466→ 466→ <div className="text-[9px] uppercase tracking-wider text-emerald-300/70 truncate">
467→ 467→ 晶体产能
468→ 468→ </div>
469→ 469→ <div className="text-sm font-mono tabular-nums text-emerald-200 truncate">
470→ 470→ {formatNum(crystalsPerSec)}
471→ 471→ <span className="text-[9px] text-emerald-300/60">/s</span>
472→ 472→ </div>
473→ 473→ </div>
474→ 474→ </div>
475→ 475→
476→ 476→ {/* 主动脉冲 — rose */}
477→ 477→ <div className="rounded-lg border border-rose-400/30 bg-black/30 backdrop-blur p-2 flex items-center gap-1.5">
478→ 478→ <Zap className="h-3.5 w-3.5 text-rose-400 shrink-0" />
479→ 479→ <div className="min-w-0 leading-tight">
480→ 480→ <div className="text-[9px] uppercase tracking-wider text-rose-300/70 truncate">
481→ 481→ 主动脉冲
482→ 482→ </div>
483→ 483→ <div className="text-sm font-mono tabular-nums text-rose-200 truncate">
484→ 484→ {formatNum(pulsePower)}
485→ 485→ {combo >= 1 && (
486→ 486→ <span className="text-[9px] text-rose-300/80"> ×{comboMult.toFixed(2)}</span>
487→ 487→ )}
488→ 488→ </div>
489→ 489→ </div>
490→ 490→ </div>
491→ 491→
492→ 492→ {/* 当前连击 — amber,≥3 时高亮闪烁 */}
493→ 493→ <div
494→ 494→ className={`rounded-lg border bg-black/30 backdrop-blur p-2 flex items-center gap-1.5 ${
495→ 495→ comboHot ? "border-amber-400/70 animate-pulse" : "border-amber-400/30"
496→ 496→ }`}
497→ 497→ >
498→ 498→ <Flame
499→ 499→ className={`h-3.5 w-3.5 text-amber-400 shrink-0 ${
500→ 500→ comboHot ? "drop-shadow-[0_0_4px_rgba(251,191,36,0.7)]" : ""
501→ 501→ }`}
502→ 502→ />
503→ 503→ <div className="min-w-0 leading-tight">
504→ 504→ <div className="text-[9px] uppercase tracking-wider text-amber-300/70 truncate">
505→ 505→ 当前连击
506→ 506→ </div>
507→ 507→ <div
508→ 508→ className={`text-sm font-mono tabular-nums truncate ${
509→ 509→ comboHot ? "text-amber-200" : "text-amber-300/80"
510→ 510→ }`}
511→ 511→ >
512→ 512→ {combo}
513→ 513→ <span className="text-[9px] text-amber-300/60">/10</span>
514→ 514→ </div>
515→ 515→ </div>
516→ 516→ </div>
517→ 517→
518→ 518→ {/* 仓库容量 — fuchsia,≥90% 切 rose 警告色 + pulse */}
519→ 519→ <div
520→ 520→ className={`rounded-lg border bg-black/30 backdrop-blur p-2 flex items-center gap-1.5 ${
521→ 521→ warehouseWarn ? "border-rose-400/70 animate-pulse" : "border-fuchsia-400/30"
522→ 522→ }`}
523→ 523→ >
524→ 524→ <Database
525→ 525→ className={`h-3.5 w-3.5 shrink-0 ${
526→ 526→ warehouseWarn ? "text-rose-400" : "text-fuchsia-400"
527→ 527→ }`}
528→ 528→ />
529→ 529→ <div className="min-w-0 leading-tight">
530→ 530→ <div
531→ 531→ className={`text-[9px] uppercase tracking-wider truncate ${
532→ 532→ warehouseWarn ? "text-rose-300/80" : "text-fuchsia-300/70"
533→ 533→ }`}
534→ 534→ >
535→ 535→ 仓库容量
536→ 536→ </div>
537→ 537→ <div
538→ 538→ className={`text-sm font-mono tabular-nums truncate ${
539→ 539→ warehouseWarn ? "text-rose-200" : "text-fuchsia-200"
540→ 540→ }`}
541→ 541→ >
542→ 542→ {formatNum(crystals)}
543→ 543→ <span
544→ 544→ className={`text-[9px] ${
545→ 545→ warehouseWarn ? "text-rose-300/70" : "text-fuchsia-300/60"
546→ 546→ }`}
547→ 547→ >
548→ 548→ /{formatNum(crystalCap)}
549→ 549→ </span>
550→ 550→ </div>
551→ 551→ </div>
552→ 552→ </div>
553→ 553→ </div>
554→ 554→ );
555→ 555→}
556→ 556→
557→ 557→function StatsPanel() {
558→ 558→ const s = useGameStore();
559→ 559→ const achCount = Object.values(s.achievements).filter(Boolean).length;
560→ 560→ // 深空信标本地排行榜最高分(独立 localStorage
561→ 561→ const [beaconBest, setBeaconBest] = useState<number | null>(null);
562→ 562→ useEffect(() => {
563→ 563→ try {
564→ 564→ const lb = loadLeaderboard();
565→ 565→ // eslint-disable-next-line react-hooks/set-state-in-effect
566→ 566→ setBeaconBest(lb.length > 0 ? lb[0].score : null);
567→ 567→ } catch {
568→ 568→ setBeaconBest(null);
569→ 569→ }
570→ 570→ }, []);
571→ 571→ const rows = [
572→ 572→ { label: "累计解码晶体", value: `${s.totalDecoded} 颗` },
573→ 573→ { label: "飞升周目", value: `${s.ascensions}` },
574→ 574→ { label: "持有蓝图", value: `${s.blueprints.length} / 6` },
575→ 575→ { label: "已学技术", value: `${Object.values(s.tech).filter((v) => v > 0).length} / ${TECH_TREE.length}` },
576→ 576→ { label: "已获碎片", value: `${Object.values(s.fragments).filter(Boolean).length} / ${FRAGMENTS.length}` },
577→ 577→ { label: "已解锁成就", value: `${achCount} / ${ACHIEVEMENTS.length}` },
578→ 578→ { label: "星图天赋", value: `${s.constellation?.length ?? 0} / ${CONSTELLATION_PERKS.length}` },
579→ 579→ { label: "接触进度", value: `${s.contact.toFixed(1)}%` },
580→ 580→ { label: "晶体产能", value: `${s.crystalsPerSec.toFixed(2)} /s` },
581→ 581→ { label: "仓库上限", value: `${formatNum(s.crystalCap)}` },
582→ 582→ { label: "洞见倍率", value: `×${s.insightMult.toFixed(2)}` },
583→ 583→ { label: "累计探险", value: `${s.totalExpeditions} 次` },
584→ 584→ { label: "BOSS 击破", value: `${s.bossKills ?? 0} 次` },
585→ 585→ { label: "星潮亲历", value: `${(s.starTidesEncountered ?? []).length} / 9` },
586→ 586→ { label: "编年史条目", value: `${(s.chronicle ?? []).length} 纪元` },
587→ 587→ { label: "探险能量", value: `${Math.floor(s.energy)} / ${s.energyMax}` },
588→ 588→ { label: "信标最高分", value: beaconBest !== null ? formatNum(beaconBest) : "—" },
589→ 589→ { label: "探索力", value: `${s.attributes?.exploration ?? 0} / 100` },
590→ 590→ { label: "智慧", value: `${s.attributes?.wisdom ?? 0} / 100` },
591→ 591→ { label: "勇气", value: `${s.attributes?.courage ?? 0} / 100` },
592→ 592→ { label: "灵感", value: `${s.attributes?.inspiration ?? 0} / 100` },
593→ 593→ { label: "待分配属性点", value: `${s.pendingAttrPoints ?? 0}` },
594→ 594→ ];
595→ 595→ return (
596→ 596→ <div className="grid grid-cols-2 gap-1.5 text-xs">
597→ 597→ {rows.map((r) => (
598→ 598→ <div
599→ 599→ key={r.label}
600→ 600→ className="flex items-center justify-between rounded-md bg-black/25 border border-white/5 px-2 py-1.5"
601→ 601→ >
602→ 602→ <span className="text-muted-foreground">{r.label}</span>
603→ 603→ <span className="font-mono text-foreground">{r.value}</span>
604→ 604→ </div>
605→ 605→ ))}
606→ 606→ </div>
607→ 607→ );
608→ 608→}
609→ 609→
+355 -985
View File
File diff suppressed because it is too large Load Diff