restore: worklog 调研记录 + 清理工作区

This commit is contained in:
2026-06-24 02:53:45 +00:00
parent 43c203ebd2
commit 3fdcd6797f
14 changed files with 468 additions and 10721 deletions
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