restore: worklog 调研记录 + 清理工作区
This commit is contained in:
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→ /** 日期 key(YYYY-MM-DD)或周 key(YYYY-Www) */
|
||||
22→ dateKey: string;
|
||||
23→ /** 挑战类型 */
|
||||
24→ challenge: BeaconChallengeType;
|
||||
25→ /** 难度 */
|
||||
26→ difficulty: BeaconDifficulty;
|
||||
27→ /** 完成度(0-1,1 = 完成) */
|
||||
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→ /** 日期 key(YYYY-MM-DD,UTC) */
|
||||
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→ * 难度强制 anomaly(60%)或 singular(40%),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-12(singular 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 → 7;streak=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→ /** 日期 key(YYYY-MM-DD)或周 key(YYYY-Www) */
|
||||
22→ 22→ dateKey: string;
|
||||
23→ 23→ /** 挑战类型 */
|
||||
24→ 24→ challenge: BeaconChallengeType;
|
||||
25→ 25→ /** 难度 */
|
||||
26→ 26→ difficulty: BeaconDifficulty;
|
||||
27→ 27→ /** 完成度(0-1,1 = 完成) */
|
||||
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→ /** 日期 key(YYYY-MM-DD,UTC) */
|
||||
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→ * 难度强制 anomaly(60%)或 singular(40%),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-12(singular 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 → 7;streak=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=10,job_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 2,ResizeObserver 自适应,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 actions;pulse/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 Canvas,DPR cap 2,resize 监听
|
||||
118→ - 3 层视差星空:远(100星)/中(60星)/近(30星),独立漂移 + 飞船速度视差 + 闪烁
|
||||
119→ - 8 种实体全部 ctx.shadowBlur 辉光绘制
|
||||
120→ - 粒子系统:尾焰/收集/碰撞/烟花,上限 200
|
||||
121→ - 屏幕震动 280ms 衰减;低护盾(<30%)红色边框脉冲
|
||||
122→ - HUD(HTML 叠层,glass+backdrop-blur,80ms 节流):护盾/能量/分数/用时/收集计数
|
||||
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?})` action(crystals 受 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→- 奖励同步 gameStore(grantCruiseReward,满仓时 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→ - resolveCurrentNode:BOSS 节点包装 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→ - 顶部待分配点数 badge(pendingAttrPoints > 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 个「角色」TabsTrigger(User 图标,渐变主题)
|
||||
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-pages(v0.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 228329(tz=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.6M(HTTP 200 编译 4.3s)
|
||||
292→- 推送 gh-pages 分支:force push origin gh-pages(commit 8727fed "deploy: v0.7")
|
||||
293→- 提交 main:README 更新(commit 43dfc2d "docs: README 添加在线游玩链接 + v0.7 状态更新")
|
||||
294→- 验证 Gitea Pages:https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/ → HTTP 200 ✅
|
||||
295→- QA 验证:agent-browser 打开 http://localhost:3000/ → VLM 评分 8/10(主界面 polished,minor 空白)
|
||||
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 job:fixed_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/10:v0.8 版本号 ✅ + 巡航按钮 ✅
|
||||
322→ - 巡航 READY 阶段 VLM 8/10:BOSS 战说明 ✅(每5关BOSS战/击败BOSS)+ 射击控制说明 ✅(空格/右下角射击按钮)
|
||||
323→ - 巡航第1关游戏 VLM 8/10:飞船(绿色三角)✅ + HUD(护盾/能量/分数/计时)✅ + 陨石/收集物/能量场 ✅ + 无渲染错误
|
||||
324→- 提交 v0.8(commit 494bc5f)+ 推送 main
|
||||
325→- 静态导出 BUILD_EXPORT=true → out/ 1.6M(编译 4.1s)
|
||||
326→- 部署 gh-pages(force 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.8:https://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" + 大字号 streak(chain-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. 隔一天 miss(lastCompleted + 2 天)→ streak=4,graceUsed=1 ✅
|
||||
393→ 5. 再次 miss(grace 已用)→ 断链 streak=1,graceUsed=0 ✅
|
||||
394→ - **UI 集成测试**(agent-browser):
|
||||
395→ - localStorage 模拟设置 chain state(lastCompletedDateKey=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 个独立 key(weekly + 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.1(commit 71ca5b4)+ 推送 main
|
||||
439→- 静态导出 + 部署 gh-pages(commit 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.md(v0.8.1,10 大系统)+ beacon.ts(873行,日+周+链)+ starTide.ts(209行,6种星潮)+ gameStore trackBeacon/tickTide + BeaconPanel.tsx,确认四色全息规范与现有架构
|
||||
456→- **工单 #5 限时挑战**:
|
||||
457→ - beacon.ts(873→1162行)新增限时挑战系统:
|
||||
458→ - BeaconTimedChallenge/BeaconTimedProgress 接口 + isTimed 字段加入 BeaconScoreEntry
|
||||
459→ - getTimedSlotKey(UTC 0/4/8/12/16/20 点切分,"timed_YYYY-MM-DD_HH")+ timedSlotKeyToSeed(FNV-1a)
|
||||
460→ - generateTimedChallenge(确定性,难度 routine,goal=日基准×0.3-0.5,5类型公式)
|
||||
461→ - loadTimedProgress/saveTimedProgress/addTimedProgress/claimTimedReward(排行榜 isTimed)
|
||||
462→ - msUntilNextTimedSlot + TIMED_SLOT_MS=4h + BEACON_TIMED_KEY
|
||||
463→ - gameStore.ts:trackBeacon 返回值新增 timedJustCompleted,三进度同时更新;新增 claimTimedBeacon action
|
||||
464→ - BeaconPanel.tsx(753→944行):日挑战与周挑战之间插入限时挑战区块(amber 主题)
|
||||
465→ - Hourglass图标+TIMED标题+slotKey+倒计时 + 进度+奖励+领取按钮(emerald)
|
||||
466→ - 紧急状态:距时段结束<30min 切 rose 脉冲动画+"即将结束"徽章
|
||||
467→ - 2新动画 timed-glow(amber)/timed-urgent(rose) + 排行榜 TIMED 徽章
|
||||
468→- **工单 #3 星潮深化**:
|
||||
469→ - starTide.ts(209→289行):TideType 新增 surge/eclipse/prism
|
||||
470→ - surge(emerald ⇈): crystalsPerSec×2.5 + targetLenBonus=2(解码目标序列+2,更长更难)
|
||||
471→ - eclipse(rose ◐): crystalsPerSec×0.7 + bossWinRateBonus=0.2(BOSS胜率+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.ts:generatePuzzle 新增 targetLenBonus 参数(surge用,targetLen+stepLimit同步增加)
|
||||
476→ - gameStore.ts 3处修饰器应用:
|
||||
477→ - startDecode:surge 时传 targetLenBonus → 谜题更长
|
||||
478→ - autoDecodeTick:prism 时 interval×=autoDecodeIntervalMult
|
||||
479→ - resolveCurrentNode:eclipse 时 bossWinRate 加 tideBossBonus
|
||||
480→ - achievements.ts:ach_tides_all 阈值6→9;page.tsx StatsPanel 星潮亲历 /9
|
||||
481→- QA 验证:
|
||||
482→ - lint 零错误(exit 0)+ dev HTTP 200 + 编译<30ms
|
||||
483→ - BeaconPanel VLM 8.5/10(限时区块可见)+ 最终 9/10
|
||||
484→ - 三新星潮 localStorage 注入触发截图 VLM:surge 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 支持 targetLenBonus(surge 谜题+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→- 前置阅读:worklog(v0.8.1 项目状态)+ beacon.ts(BeaconScoreEntry / loadLeaderboard / pushLeaderboardEntry / claimBeaconReward / claimWeeklyReward / claimTimedReward)+ BeaconPanel.tsx(排行榜 UI 渲染)+ Caddyfile(?XTransformPort=端口号 转发规则)+ examples/websocket(mini-service 参考)+ .zscripts/dev.sh(mini-service 启动模式)
|
||||
504→- 发现 worklog 信息滞后:beacon.ts 实际已扩到 1161 行(v0.8.2 已加限时挑战 + claimTimedReward),BeaconPanel 已扩到 953 行
|
||||
505→- 创建 mini-service:
|
||||
506→ - `mini-services/leaderboard-service/package.json`:独立 bun 项目,type=module,scripts `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=1,uptime 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 入口全部接入云提交:claimBeaconReward(BeaconPanel 直接调)/ claimWeeklyBeacon / claimTimedBeacon(gameStore 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=10,job_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 2,ResizeObserver 自适应,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 actions;pulse/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 Canvas,DPR cap 2,resize 监听
|
||||
118→ - 3 层视差星空:远(100星)/中(60星)/近(30星),独立漂移 + 飞船速度视差 + 闪烁
|
||||
119→ - 8 种实体全部 ctx.shadowBlur 辉光绘制
|
||||
120→ - 粒子系统:尾焰/收集/碰撞/烟花,上限 200
|
||||
121→ - 屏幕震动 280ms 衰减;低护盾(<30%)红色边框脉冲
|
||||
122→ - HUD(HTML 叠层,glass+backdrop-blur,80ms 节流):护盾/能量/分数/用时/收集计数
|
||||
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?})` action(crystals 受 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→- 奖励同步 gameStore(grantCruiseReward,满仓时 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→ - resolveCurrentNode:BOSS 节点包装 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→ - 顶部待分配点数 badge(pendingAttrPoints > 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 个「角色」TabsTrigger(User 图标,渐变主题)
|
||||
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-pages(v0.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 228329(tz=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.6M(HTTP 200 编译 4.3s)
|
||||
292→- 推送 gh-pages 分支:force push origin gh-pages(commit 8727fed "deploy: v0.7")
|
||||
293→- 提交 main:README 更新(commit 43dfc2d "docs: README 添加在线游玩链接 + v0.7 状态更新")
|
||||
294→- 验证 Gitea Pages:https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/ → HTTP 200 ✅
|
||||
295→- QA 验证:agent-browser 打开 http://localhost:3000/ → VLM 评分 8/10(主界面 polished,minor 空白)
|
||||
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 job:fixed_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/10:v0.8 版本号 ✅ + 巡航按钮 ✅
|
||||
322→ - 巡航 READY 阶段 VLM 8/10:BOSS 战说明 ✅(每5关BOSS战/击败BOSS)+ 射击控制说明 ✅(空格/右下角射击按钮)
|
||||
323→ - 巡航第1关游戏 VLM 8/10:飞船(绿色三角)✅ + HUD(护盾/能量/分数/计时)✅ + 陨石/收集物/能量场 ✅ + 无渲染错误
|
||||
324→- 提交 v0.8(commit 494bc5f)+ 推送 main
|
||||
325→- 静态导出 BUILD_EXPORT=true → out/ 1.6M(编译 4.1s)
|
||||
326→- 部署 gh-pages(force 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.8:https://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" + 大字号 streak(chain-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. 隔一天 miss(lastCompleted + 2 天)→ streak=4,graceUsed=1 ✅
|
||||
393→ 5. 再次 miss(grace 已用)→ 断链 streak=1,graceUsed=0 ✅
|
||||
394→ - **UI 集成测试**(agent-browser):
|
||||
395→ - localStorage 模拟设置 chain state(lastCompletedDateKey=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 个独立 key(weekly + 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.1(commit 71ca5b4)+ 推送 main
|
||||
439→- 静态导出 + 部署 gh-pages(commit 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.md(v0.8.1,10 大系统)+ beacon.ts(873行,日+周+链)+ starTide.ts(209行,6种星潮)+ gameStore trackBeacon/tickTide + BeaconPanel.tsx,确认四色全息规范与现有架构
|
||||
456→- **工单 #5 限时挑战**:
|
||||
457→ - beacon.ts(873→1162行)新增限时挑战系统:
|
||||
458→ - BeaconTimedChallenge/BeaconTimedProgress 接口 + isTimed 字段加入 BeaconScoreEntry
|
||||
459→ - getTimedSlotKey(UTC 0/4/8/12/16/20 点切分,"timed_YYYY-MM-DD_HH")+ timedSlotKeyToSeed(FNV-1a)
|
||||
460→ - generateTimedChallenge(确定性,难度 routine,goal=日基准×0.3-0.5,5类型公式)
|
||||
461→ - loadTimedProgress/saveTimedProgress/addTimedProgress/claimTimedReward(排行榜 isTimed)
|
||||
462→ - msUntilNextTimedSlot + TIMED_SLOT_MS=4h + BEACON_TIMED_KEY
|
||||
463→ - gameStore.ts:trackBeacon 返回值新增 timedJustCompleted,三进度同时更新;新增 claimTimedBeacon action
|
||||
464→ - BeaconPanel.tsx(753→944行):日挑战与周挑战之间插入限时挑战区块(amber 主题)
|
||||
465→ - Hourglass图标+TIMED标题+slotKey+倒计时 + 进度+奖励+领取按钮(emerald)
|
||||
466→ - 紧急状态:距时段结束<30min 切 rose 脉冲动画+"即将结束"徽章
|
||||
467→ - 2新动画 timed-glow(amber)/timed-urgent(rose) + 排行榜 TIMED 徽章
|
||||
468→- **工单 #3 星潮深化**:
|
||||
469→ - starTide.ts(209→289行):TideType 新增 surge/eclipse/prism
|
||||
470→ - surge(emerald ⇈): crystalsPerSec×2.5 + targetLenBonus=2(解码目标序列+2,更长更难)
|
||||
471→ - eclipse(rose ◐): crystalsPerSec×0.7 + bossWinRateBonus=0.2(BOSS胜率+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.ts:generatePuzzle 新增 targetLenBonus 参数(surge用,targetLen+stepLimit同步增加)
|
||||
476→ - gameStore.ts 3处修饰器应用:
|
||||
477→ - startDecode:surge 时传 targetLenBonus → 谜题更长
|
||||
478→ - autoDecodeTick:prism 时 interval×=autoDecodeIntervalMult
|
||||
479→ - resolveCurrentNode:eclipse 时 bossWinRate 加 tideBossBonus
|
||||
480→ - achievements.ts:ach_tides_all 阈值6→9;page.tsx StatsPanel 星潮亲历 /9
|
||||
481→- QA 验证:
|
||||
482→ - lint 零错误(exit 0)+ dev HTTP 200 + 编译<30ms
|
||||
483→ - BeaconPanel VLM 8.5/10(限时区块可见)+ 最终 9/10
|
||||
484→ - 三新星潮 localStorage 注入触发截图 VLM:surge 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 支持 targetLenBonus(surge 谜题+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→- 前置阅读:worklog(v0.8.1 项目状态)+ beacon.ts(BeaconScoreEntry / loadLeaderboard / pushLeaderboardEntry / claimBeaconReward / claimWeeklyReward / claimTimedReward)+ BeaconPanel.tsx(排行榜 UI 渲染)+ Caddyfile(?XTransformPort=端口号 转发规则)+ examples/websocket(mini-service 参考)+ .zscripts/dev.sh(mini-service 启动模式)
|
||||
504→- 发现 worklog 信息滞后:beacon.ts 实际已扩到 1161 行(v0.8.2 已加限时挑战 + claimTimedReward),BeaconPanel 已扩到 953 行
|
||||
505→- 创建 mini-service:
|
||||
506→ - `mini-services/leaderboard-service/package.json`:独立 bun 项目,type=module,scripts `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=1,uptime 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 入口全部接入云提交:claimBeaconReward(BeaconPanel 直接调)/ claimWeeklyBeacon / claimTimedBeacon(gameStore 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.md(v0.8.1,四色规范 emerald/rose/amber/fuchsia,禁止蓝色,10 大系统)+ chronicle.ts(680 行)+ starTide.ts(297 行,9 种星潮)+ gameStore.ts(1332 行)+ StarTide.tsx(170 行)+ 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-service(mini-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.ts(250 行):
|
||||
614→ - 端口固定 3031,path:"/"(Caddy 据此转发)
|
||||
615→ - 6 种星潮类型表(crystal/resonance/ruins/void/core/silence)+ 权重表 + rollTide
|
||||
616→ - DURATION_SEC=60s 全球星潮持续;FIRST_TIDE_DELAY_MS=60s(QA 友好,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}) → scheduleNextTide(10-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.md:API/事件文档
|
||||
623→ - **前端集成**:
|
||||
624→ - src/hooks/useGlobalTide.ts(127 行):
|
||||
625→ - 单例 socket(sharedSocket + 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→ - 导出 adminTriggerGlobalTide(dev 控制台 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):创建 newTide(60s)+ 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→ - StarTideNotifier:toast 文案区分全球/本地("🌐 全球星潮降临:{name}" + "全球玩家同步经历 · {desc}")
|
||||
643→- **QA 验证**:
|
||||
644→ - `bun run lint` 零错误零警告
|
||||
645→ - dev.log 全程无错误,编译 < 70ms,HTTP 200
|
||||
646→ - mini-service 启动稳定:pid 9456,PPID=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→ - 页面加载 200,window.__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→- ✅ 工单 #9:chronicle.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-tide(60s 持续),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 9456,PPID=1,uptime 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.2(commit 191434e)+ 推送 main + 部署 gh-pages(7e0a59b)HTTP 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→520,min-h 提高
|
||||
722→- Bug4 提示框遮挡:toast viewport top-0→top-14 避开 header,TOAST_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=10,job_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 2,ResizeObserver 自适应,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 actions;pulse/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 Canvas,DPR cap 2,resize 监听
|
||||
118→ - 3 层视差星空:远(100星)/中(60星)/近(30星),独立漂移 + 飞船速度视差 + 闪烁
|
||||
119→ - 8 种实体全部 ctx.shadowBlur 辉光绘制
|
||||
120→ - 粒子系统:尾焰/收集/碰撞/烟花,上限 200
|
||||
121→ - 屏幕震动 280ms 衰减;低护盾(<30%)红色边框脉冲
|
||||
122→ - HUD(HTML 叠层,glass+backdrop-blur,80ms 节流):护盾/能量/分数/用时/收集计数
|
||||
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?})` action(crystals 受 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→- 奖励同步 gameStore(grantCruiseReward,满仓时 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→ - resolveCurrentNode:BOSS 节点包装 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→ - 顶部待分配点数 badge(pendingAttrPoints > 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 个「角色」TabsTrigger(User 图标,渐变主题)
|
||||
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-pages(v0.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 228329(tz=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.6M(HTTP 200 编译 4.3s)
|
||||
292→- 推送 gh-pages 分支:force push origin gh-pages(commit 8727fed "deploy: v0.7")
|
||||
293→- 提交 main:README 更新(commit 43dfc2d "docs: README 添加在线游玩链接 + v0.7 状态更新")
|
||||
294→- 验证 Gitea Pages:https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/ → HTTP 200 ✅
|
||||
295→- QA 验证:agent-browser 打开 http://localhost:3000/ → VLM 评分 8/10(主界面 polished,minor 空白)
|
||||
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 job:fixed_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/10:v0.8 版本号 ✅ + 巡航按钮 ✅
|
||||
322→ - 巡航 READY 阶段 VLM 8/10:BOSS 战说明 ✅(每5关BOSS战/击败BOSS)+ 射击控制说明 ✅(空格/右下角射击按钮)
|
||||
323→ - 巡航第1关游戏 VLM 8/10:飞船(绿色三角)✅ + HUD(护盾/能量/分数/计时)✅ + 陨石/收集物/能量场 ✅ + 无渲染错误
|
||||
324→- 提交 v0.8(commit 494bc5f)+ 推送 main
|
||||
325→- 静态导出 BUILD_EXPORT=true → out/ 1.6M(编译 4.1s)
|
||||
326→- 部署 gh-pages(force 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.8:https://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" + 大字号 streak(chain-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. 隔一天 miss(lastCompleted + 2 天)→ streak=4,graceUsed=1 ✅
|
||||
393→ 5. 再次 miss(grace 已用)→ 断链 streak=1,graceUsed=0 ✅
|
||||
394→ - **UI 集成测试**(agent-browser):
|
||||
395→ - localStorage 模拟设置 chain state(lastCompletedDateKey=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 个独立 key(weekly + 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.1(commit 71ca5b4)+ 推送 main
|
||||
439→- 静态导出 + 部署 gh-pages(commit 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.md(v0.8.1,10 大系统)+ beacon.ts(873行,日+周+链)+ starTide.ts(209行,6种星潮)+ gameStore trackBeacon/tickTide + BeaconPanel.tsx,确认四色全息规范与现有架构
|
||||
456→- **工单 #5 限时挑战**:
|
||||
457→ - beacon.ts(873→1162行)新增限时挑战系统:
|
||||
458→ - BeaconTimedChallenge/BeaconTimedProgress 接口 + isTimed 字段加入 BeaconScoreEntry
|
||||
459→ - getTimedSlotKey(UTC 0/4/8/12/16/20 点切分,"timed_YYYY-MM-DD_HH")+ timedSlotKeyToSeed(FNV-1a)
|
||||
460→ - generateTimedChallenge(确定性,难度 routine,goal=日基准×0.3-0.5,5类型公式)
|
||||
461→ - loadTimedProgress/saveTimedProgress/addTimedProgress/claimTimedReward(排行榜 isTimed)
|
||||
462→ - msUntilNextTimedSlot + TIMED_SLOT_MS=4h + BEACON_TIMED_KEY
|
||||
463→ - gameStore.ts:trackBeacon 返回值新增 timedJustCompleted,三进度同时更新;新增 claimTimedBeacon action
|
||||
464→ - BeaconPanel.tsx(753→944行):日挑战与周挑战之间插入限时挑战区块(amber 主题)
|
||||
465→ - Hourglass图标+TIMED标题+slotKey+倒计时 + 进度+奖励+领取按钮(emerald)
|
||||
466→ - 紧急状态:距时段结束<30min 切 rose 脉冲动画+"即将结束"徽章
|
||||
467→ - 2新动画 timed-glow(amber)/timed-urgent(rose) + 排行榜 TIMED 徽章
|
||||
468→- **工单 #3 星潮深化**:
|
||||
469→ - starTide.ts(209→289行):TideType 新增 surge/eclipse/prism
|
||||
470→ - surge(emerald ⇈): crystalsPerSec×2.5 + targetLenBonus=2(解码目标序列+2,更长更难)
|
||||
471→ - eclipse(rose ◐): crystalsPerSec×0.7 + bossWinRateBonus=0.2(BOSS胜率+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.ts:generatePuzzle 新增 targetLenBonus 参数(surge用,targetLen+stepLimit同步增加)
|
||||
476→ - gameStore.ts 3处修饰器应用:
|
||||
477→ - startDecode:surge 时传 targetLenBonus → 谜题更长
|
||||
478→ - autoDecodeTick:prism 时 interval×=autoDecodeIntervalMult
|
||||
479→ - resolveCurrentNode:eclipse 时 bossWinRate 加 tideBossBonus
|
||||
480→ - achievements.ts:ach_tides_all 阈值6→9;page.tsx StatsPanel 星潮亲历 /9
|
||||
481→- QA 验证:
|
||||
482→ - lint 零错误(exit 0)+ dev HTTP 200 + 编译<30ms
|
||||
483→ - BeaconPanel VLM 8.5/10(限时区块可见)+ 最终 9/10
|
||||
484→ - 三新星潮 localStorage 注入触发截图 VLM:surge 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 支持 targetLenBonus(surge 谜题+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→- 前置阅读:worklog(v0.8.1 项目状态)+ beacon.ts(BeaconScoreEntry / loadLeaderboard / pushLeaderboardEntry / claimBeaconReward / claimWeeklyReward / claimTimedReward)+ BeaconPanel.tsx(排行榜 UI 渲染)+ Caddyfile(?XTransformPort=端口号 转发规则)+ examples/websocket(mini-service 参考)+ .zscripts/dev.sh(mini-service 启动模式)
|
||||
504→- 发现 worklog 信息滞后:beacon.ts 实际已扩到 1161 行(v0.8.2 已加限时挑战 + claimTimedReward),BeaconPanel 已扩到 953 行
|
||||
505→- 创建 mini-service:
|
||||
506→ - `mini-services/leaderboard-service/package.json`:独立 bun 项目,type=module,scripts `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=1,uptime 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 入口全部接入云提交:claimBeaconReward(BeaconPanel 直接调)/ claimWeeklyBeacon / claimTimedBeacon(gameStore 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.md(v0.8.1,四色规范 emerald/rose/amber/fuchsia,禁止蓝色,10 大系统)+ chronicle.ts(680 行)+ starTide.ts(297 行,9 种星潮)+ gameStore.ts(1332 行)+ StarTide.tsx(170 行)+ 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-service(mini-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.ts(250 行):
|
||||
614→ - 端口固定 3031,path:"/"(Caddy 据此转发)
|
||||
615→ - 6 种星潮类型表(crystal/resonance/ruins/void/core/silence)+ 权重表 + rollTide
|
||||
616→ - DURATION_SEC=60s 全球星潮持续;FIRST_TIDE_DELAY_MS=60s(QA 友好,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}) → scheduleNextTide(10-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.md:API/事件文档
|
||||
623→ - **前端集成**:
|
||||
624→ - src/hooks/useGlobalTide.ts(127 行):
|
||||
625→ - 单例 socket(sharedSocket + 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→ - 导出 adminTriggerGlobalTide(dev 控制台 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):创建 newTide(60s)+ 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→ - StarTideNotifier:toast 文案区分全球/本地("🌐 全球星潮降临:{name}" + "全球玩家同步经历 · {desc}")
|
||||
643→- **QA 验证**:
|
||||
644→ - `bun run lint` 零错误零警告
|
||||
645→ - dev.log 全程无错误,编译 < 70ms,HTTP 200
|
||||
646→ - mini-service 启动稳定:pid 9456,PPID=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→ - 页面加载 200,window.__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→- ✅ 工单 #9:chronicle.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-tide(60s 持续),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 9456,PPID=1,uptime 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.2(commit 191434e)+ 推送 main + 部署 gh-pages(7e0a59b)HTTP 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→520,min-h 提高
|
||||
722→- Bug4 提示框遮挡:toast viewport top-0→top-14 避开 header,TOAST_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)/ 戴森星核(晶体/秒+30,cost 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** autoDecodeTick:dec_5 解锁后自动解码扩展至 T2 晶体
|
||||
758→ - 原:仅 findIndex(tier === 1)
|
||||
759→ - 新:先找 T1,若无 T1 且 tech.dec_5 已解锁,则找 T2 并将 autoTier 设为 2
|
||||
760→ - decodeRewards(autoTier, ...) 使用动态 tier,T2 晶体奖励按 CRYSTAL_VALUE[2] = 22 晶体 / 4 洞见基础计算
|
||||
761→
|
||||
762→- **achievements.ts**:ach_tech_all(全谱精通)desc "12 项技术" → "20 项技术",check 阈值 12 → 20;name 保持"全谱精通",奖励不变
|
||||
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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user