v0.8.2: 工单大清理 - 编年史分页+限时挑战+星潮深化+云排行榜+手写叙事+socket多人星潮+UI打磨
工单 #8 编年史上限+分页: - engine.ts: slice(-50)→slice(-200) - ChronicleDialog.tsx: 加分页(每页10条)+上一页/下一页+页码显示 工单 #5 限时挑战 (subagent 10-a): - beacon.ts: BeaconTimedChallenge + getTimedSlotKey(4h时段) + generateTimedChallenge - BeaconPanel.tsx: amber主题限时区块 + 倒计时 + <30min紧急状态 - gameStore: trackBeacon 加 timedJustCompleted + claimTimedBeacon action 工单 #3 星潮类型深化 (subagent 10-a): - starTide.ts: +3种星潮 surge(emerald)/eclipse(rose)/prism(fuchsia) - TideModifiers: +targetLenBonus/bossWinRateBonus/autoDecodeIntervalMult - decode.ts: generatePuzzle 加 targetLenBonus 参数 - achievements: ach_tides_all 阈值 6→9 工单 #4/P2 云排行榜 (subagent 10-b): - mini-services/leaderboard-service/ (端口3030, Hono+bun, 内存1000条) - API: GET/POST /api/leaderboard + /stats + CORS + 防刷 - beacon.ts: fetchCloudLeaderboard/submitCloudScore - BeaconPanel: 本地Top20/全球Top100 双tab + YOU徽章高亮 工单 #9 手写叙事 (subagent 10-c): - chronicle.ts: EPOCH_LORE 5纪元×3节点=15段手写叙事(80-150字/段) - buildLore 优先手写节点, fallback 模板, 11个变量替换 P3 socket 多人星潮 (subagent 10-c): - mini-services/star-tide-service/ (端口3031, socket.io) - 每10-15min广播global-tide, 60s持续, 6种类型权重 - useGlobalTide hook + triggerGlobalTide action - StarTideIndicator 加 🌐 全球星潮标记 P2 UI打磨: - page.tsx: CrystalOrb 区加装饰全息环(3层旋转) + 四角标记 + 顶部状态条 + 底部铭文 QA: lint零错误 + dev HTTP200 + VLM 8/10 + 2个mini-service运行中(3030/3031)
This commit is contained in:
+170
-18
@@ -63,19 +63,26 @@ import {
|
||||
import {
|
||||
generateDailyChallenge,
|
||||
generateWeeklyChallenge,
|
||||
generateTimedChallenge,
|
||||
loadDailyProgress,
|
||||
loadWeeklyProgress,
|
||||
loadTimedProgress,
|
||||
loadChainState,
|
||||
addBeaconProgress,
|
||||
addWeeklyProgress,
|
||||
addTimedProgress,
|
||||
recordChainCompletion,
|
||||
claimWeeklyReward,
|
||||
claimTimedReward,
|
||||
claimChainMilestone,
|
||||
submitCloudScore,
|
||||
getTodayKey,
|
||||
type BeaconDailyChallenge,
|
||||
type BeaconDailyProgress,
|
||||
type BeaconWeeklyChallenge,
|
||||
type BeaconWeeklyProgress,
|
||||
type BeaconTimedChallenge,
|
||||
type BeaconTimedProgress,
|
||||
} from "@/lib/game/beacon";
|
||||
import { setPendingOfflineReport } from "@/lib/game/offlineReport";
|
||||
import {
|
||||
@@ -101,8 +108,10 @@ interface GameActions {
|
||||
pulse: () => { gain: number; combo: number } | null;
|
||||
|
||||
// 星潮
|
||||
tickTide: (now: number) => { started?: TideType; ended?: TideType; silenceCompensation?: number } | null;
|
||||
consumeTideEvents: () => { started?: TideType; ended?: TideType; silenceCompensation?: number }[];
|
||||
tickTide: (now: number) => TideEvent | null;
|
||||
consumeTideEvents: () => TideEvent[];
|
||||
/** P3 socket 多人同步:强制触发一道全球星潮(覆盖本地冷却,持续 60s) */
|
||||
triggerGlobalTide: (type: TideType) => void;
|
||||
|
||||
// 解码
|
||||
startDecode: (crystalId: string) => void;
|
||||
@@ -140,12 +149,17 @@ interface GameActions {
|
||||
// 深空信标奖励发放(v0.5)
|
||||
grantBeaconReward: (insights: number, contact: number) => void;
|
||||
|
||||
// 深空信标 · 周挑战领取 + 信标链里程碑领取(v0.8)
|
||||
// 深空信标 · 周挑战领取 + 信标链里程碑领取 + 限时挑战领取(v0.8 / v0.8.2)
|
||||
claimWeeklyBeacon: () => {
|
||||
rewardInsight: number;
|
||||
rewardContact: number;
|
||||
score: number;
|
||||
};
|
||||
claimTimedBeacon: () => {
|
||||
rewardInsight: number;
|
||||
rewardContact: number;
|
||||
score: number;
|
||||
};
|
||||
claimChainReward: (milestone: number) => {
|
||||
rewardInsight: number;
|
||||
rewardContact: number;
|
||||
@@ -170,9 +184,31 @@ type Store = GameState & GameActions & {
|
||||
_combo: number;
|
||||
_lastPulse: number;
|
||||
_achievementQueue: Achievement[];
|
||||
_tideEvents: { started?: TideType; ended?: TideType; silenceCompensation?: number }[];
|
||||
_tideEvents: TideEvent[];
|
||||
/** P3 全球星潮状态(ephemeral,不持久化) */
|
||||
globalTide: GlobalTideState | null;
|
||||
};
|
||||
|
||||
/** 星潮事件(UI 消费用) */
|
||||
type TideEvent = {
|
||||
started?: TideType;
|
||||
ended?: TideType;
|
||||
silenceCompensation?: number;
|
||||
/** 是否来自全球星潮(P3 socket 同步) */
|
||||
isGlobal?: boolean;
|
||||
};
|
||||
|
||||
/** 全球星潮状态(与 StarTide 类似,但用于区分本地 vs 全球) */
|
||||
type GlobalTideState = {
|
||||
type: TideType;
|
||||
startedAt: number;
|
||||
endsAt: number;
|
||||
id: string;
|
||||
};
|
||||
|
||||
/** 全球星潮持续毫秒数(与 mini-service DURATION_SEC * 1000 一致) */
|
||||
const GLOBAL_TIDE_DURATION_MS = 60_000;
|
||||
|
||||
/** 计算并写回产能字段 */
|
||||
function syncStats(state: Partial<GameState>) {
|
||||
const s = recomputeStats(state);
|
||||
@@ -189,24 +225,27 @@ function syncStats(state: Partial<GameState>) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 深空信标进度追踪(v0.5 → v0.8 扩展)。
|
||||
* 深空信标进度追踪(v0.5 → v0.8 → v0.8.2 扩展)。
|
||||
* 在游戏关键动作(脉冲/解码/探险/BOSS/洞见)后调用,同时更新:
|
||||
* 1. 日挑战进度(按今日挑战类型增量)
|
||||
* 2. 周挑战进度(按本周挑战类型增量)
|
||||
* 3. 信标链:日挑战刚完成时记录一次完成(含 grace 续命逻辑)
|
||||
* 2. 限时挑战进度(按当前 4 小时时段挑战类型增量)
|
||||
* 3. 周挑战进度(按本周挑战类型增量)
|
||||
* 4. 信标链:日挑战刚完成时记录一次完成(含 grace 续命逻辑)
|
||||
* 进度独立存储于 localStorage,不污染 GameState。
|
||||
* @returns 三类状态变更供 UI 触发通知
|
||||
* @returns 四类状态变更供 UI 触发通知
|
||||
*/
|
||||
function trackBeacon(
|
||||
type: "pulse" | "decode" | "expedition" | "boss" | "insight",
|
||||
delta: number
|
||||
): {
|
||||
dailyJustCompleted: boolean;
|
||||
timedJustCompleted: boolean;
|
||||
weeklyJustCompleted: boolean;
|
||||
newChainMilestones: number[];
|
||||
} {
|
||||
const result = {
|
||||
dailyJustCompleted: false,
|
||||
timedJustCompleted: false,
|
||||
weeklyJustCompleted: false,
|
||||
newChainMilestones: [] as number[],
|
||||
};
|
||||
@@ -227,6 +266,20 @@ function trackBeacon(
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 限时挑战(v0.8.2)----
|
||||
const tChallenge: BeaconTimedChallenge = generateTimedChallenge();
|
||||
if (tChallenge.type === type) {
|
||||
const tCurrent: BeaconTimedProgress = loadTimedProgress();
|
||||
if (tCurrent.completedAt === null) {
|
||||
const { justCompleted } = addTimedProgress(
|
||||
tCurrent,
|
||||
tChallenge,
|
||||
delta
|
||||
);
|
||||
result.timedJustCompleted = justCompleted;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 周挑战 ----
|
||||
const wChallenge: BeaconWeeklyChallenge = generateWeeklyChallenge();
|
||||
if (wChallenge.type === type) {
|
||||
@@ -269,6 +322,7 @@ export const useGameStore = create<Store>()(
|
||||
_lastPulse: 0,
|
||||
_achievementQueue: [],
|
||||
_tideEvents: [],
|
||||
globalTide: null,
|
||||
|
||||
init: () => {
|
||||
const s = get();
|
||||
@@ -354,11 +408,42 @@ export const useGameStore = create<Store>()(
|
||||
},
|
||||
|
||||
hardReset: () => {
|
||||
set({ ...createInitialState(), _lastAutoDecode: Date.now(), _lastSpawn: Date.now(), _combo: 0, _lastPulse: 0, _achievementQueue: [], _tideEvents: [] });
|
||||
set({ ...createInitialState(), _lastAutoDecode: Date.now(), _lastSpawn: Date.now(), _combo: 0, _lastPulse: 0, _achievementQueue: [], _tideEvents: [], globalTide: null });
|
||||
},
|
||||
|
||||
tickTide: (now) => {
|
||||
const s = get();
|
||||
// === P3 全球星潮优先:若全球星潮进行中,本地不触发新的星潮 ===
|
||||
if (s.globalTide) {
|
||||
// 1a) 检查全球星潮是否结束
|
||||
if (now >= s.globalTide.endsAt) {
|
||||
const endedType = s.globalTide.type;
|
||||
let silenceCompensation = 0;
|
||||
if (endedType === "silence") {
|
||||
silenceCompensation = computeSilenceCompensation({
|
||||
type: endedType,
|
||||
startedAt: s.globalTide.startedAt,
|
||||
endsAt: s.globalTide.endsAt,
|
||||
id: s.globalTide.id,
|
||||
});
|
||||
}
|
||||
const event: TideEvent = { ended: endedType, isGlobal: true };
|
||||
if (silenceCompensation > 0) event.silenceCompensation = silenceCompensation;
|
||||
set({
|
||||
globalTide: null,
|
||||
activeTide: null,
|
||||
lastTideEnd: now,
|
||||
insights: s.insights + silenceCompensation,
|
||||
// 星潮结束后重算 stats
|
||||
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: null, constellation: s.constellation, attributes: s.attributes }),
|
||||
_tideEvents: [...s._tideEvents, event],
|
||||
});
|
||||
return event;
|
||||
}
|
||||
// 1b) 全球星潮仍在进行 — 不做本地处理(避免冲突)
|
||||
return null;
|
||||
}
|
||||
|
||||
const tide = s.activeTide;
|
||||
// 星图「星潮引导」减少间隙
|
||||
const cm = constellationBonuses(s.constellation ?? []);
|
||||
@@ -366,7 +451,7 @@ export const useGameStore = create<Store>()(
|
||||
const am = getAllBonuses(s.attributes ?? {});
|
||||
const tideGapReduction = Math.min(0.3, am.tideTriggerBonus);
|
||||
const gap = Math.max(15000, (TIDE_CONFIG.gap + cm.tideGapDeltaSec * 1000) * (1 - tideGapReduction));
|
||||
// 1) 检查当前星潮是否结束
|
||||
// 2) 检查当前本地星潮是否结束
|
||||
if (tide && now >= tide.endsAt) {
|
||||
const endedType = tide.type;
|
||||
// 寂静期补偿洞见
|
||||
@@ -375,7 +460,7 @@ export const useGameStore = create<Store>()(
|
||||
silenceCompensation = computeSilenceCompensation(tide);
|
||||
}
|
||||
const newInsights = s.insights + silenceCompensation;
|
||||
const event: { started?: TideType; ended?: TideType; silenceCompensation?: number } = { ended: endedType };
|
||||
const event: TideEvent = { ended: endedType };
|
||||
if (silenceCompensation > 0) event.silenceCompensation = silenceCompensation;
|
||||
set({
|
||||
activeTide: null,
|
||||
@@ -387,7 +472,7 @@ export const useGameStore = create<Store>()(
|
||||
});
|
||||
return event;
|
||||
}
|
||||
// 2) 检查是否该触发新星潮(间隙已过)
|
||||
// 3) 检查是否该触发新星潮(间隙已过)
|
||||
if (!tide) {
|
||||
const since = now - s.lastTideEnd;
|
||||
// 首次:以 createdAt 起算 firstDelay;之后以 lastTideEnd 起算 gap
|
||||
@@ -401,7 +486,7 @@ export const useGameStore = create<Store>()(
|
||||
startedAt: now,
|
||||
endsAt: now + TIDE_CONFIG.duration,
|
||||
};
|
||||
const event: { started?: TideType; ended?: TideType; silenceCompensation?: number } = { started: type };
|
||||
const event: TideEvent = { started: type };
|
||||
// v0.4 编年史:累计遇到的星潮 ID(去重)
|
||||
const tidesAll = s.starTidesEncountered ?? [];
|
||||
const tideId = `tide_${type}`;
|
||||
@@ -427,6 +512,37 @@ export const useGameStore = create<Store>()(
|
||||
return items;
|
||||
},
|
||||
|
||||
// P3 socket 多人同步:服务端广播全球星潮时调用,强制覆盖本地冷却
|
||||
triggerGlobalTide: (type) => {
|
||||
const s = get();
|
||||
const now = Date.now();
|
||||
const id = `global_tide_${now}_${Math.random().toString(36).slice(2, 7)}`;
|
||||
const newTide: StarTide = {
|
||||
id,
|
||||
type,
|
||||
startedAt: now,
|
||||
endsAt: now + GLOBAL_TIDE_DURATION_MS,
|
||||
};
|
||||
const newGlobalTide: GlobalTideState = {
|
||||
type,
|
||||
startedAt: now,
|
||||
endsAt: now + GLOBAL_TIDE_DURATION_MS,
|
||||
id,
|
||||
};
|
||||
// 编年史:累计遇到的星潮 ID(去重)
|
||||
const tidesAll = s.starTidesEncountered ?? [];
|
||||
const tideId = `tide_${type}`;
|
||||
const newTidesAll = tidesAll.includes(tideId) ? tidesAll : [...tidesAll, tideId];
|
||||
const event: TideEvent = { started: type, isGlobal: true };
|
||||
set({
|
||||
globalTide: newGlobalTide,
|
||||
activeTide: newTide, // 覆盖本地 activeTide(即使本地有进行中的星潮也会被替换)
|
||||
starTidesEncountered: newTidesAll,
|
||||
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: newTide, constellation: s.constellation, attributes: s.attributes }),
|
||||
_tideEvents: [...s._tideEvents, event],
|
||||
});
|
||||
},
|
||||
|
||||
tick: (now) => {
|
||||
const s = get();
|
||||
const dt = Math.max(0, (now - s.lastTick) / 1000);
|
||||
@@ -538,7 +654,10 @@ export const useGameStore = create<Store>()(
|
||||
const s = get();
|
||||
const crystal = s.pendingCrystals.find((c) => c.id === crystalId);
|
||||
if (!crystal) return;
|
||||
const puzzle = generatePuzzle(crystal.tier);
|
||||
// v0.8.2 涌动星潮:解码目标序列 +2(更长谜题)
|
||||
const tideMod = getTideModifiers(s.activeTide);
|
||||
const targetLenBonus = tideMod.targetLenBonus ?? 0;
|
||||
const puzzle = generatePuzzle(crystal.tier, undefined, targetLenBonus);
|
||||
set({
|
||||
activePuzzle: puzzle,
|
||||
pendingCrystals: s.pendingCrystals.filter((c) => c.id !== crystalId),
|
||||
@@ -654,7 +773,10 @@ export const useGameStore = create<Store>()(
|
||||
// v0.7 智慧:自动解码周期 -X%
|
||||
const am = getAllBonuses(s.attributes ?? {});
|
||||
const baseInterval = 12000 + cm.autoDecodeIntervalDeltaSec * 1000;
|
||||
const interval = Math.max(5000, baseInterval * am.autoDecodeIntervalMult);
|
||||
// v0.8.2 棱镜星潮:自动解码周期 ×0.7(-30%)
|
||||
const tideModEarly = getTideModifiers(s.activeTide);
|
||||
const tideIntervalMult = tideModEarly.autoDecodeIntervalMult ?? 1;
|
||||
const interval = Math.max(5000, baseInterval * am.autoDecodeIntervalMult * tideIntervalMult);
|
||||
if (now - s._lastAutoDecode < interval) return;
|
||||
// 找一颗 T1 晶体自动解码
|
||||
const idx = s.pendingCrystals.findIndex((c) => c.tier === 1);
|
||||
@@ -752,14 +874,17 @@ export const useGameStore = create<Store>()(
|
||||
// 深拷贝
|
||||
const exp = JSON.parse(JSON.stringify(s.activeExpedition));
|
||||
// v0.7 勇气:BOSS 战胜率 +X%(动态提高 RNG 阈值)
|
||||
// v0.8.2 蚀相星潮:BOSS 胜率 +20%
|
||||
const nodeBefore = exp.nodes[exp.currentNode];
|
||||
const isBossNode = nodeBefore?.type === "boss";
|
||||
const am = getAllBonuses(s.attributes ?? {});
|
||||
const tideMod = getTideModifiers(s.activeTide);
|
||||
const tideBossBonus = tideMod.bossWinRateBonus ?? 0;
|
||||
const result = isBossNode
|
||||
? resolveNode(exp, () => {
|
||||
// 单次 rng() 调用:B% 概率返回 0(必胜),其余情况返回 r-B(保持均匀分布)
|
||||
const r = Math.random();
|
||||
const b = Math.min(0.95, am.bossWinRateBonus);
|
||||
const b = Math.min(0.95, am.bossWinRateBonus + tideBossBonus);
|
||||
return r < b ? 0 : Math.min(1, r - b);
|
||||
})
|
||||
: resolveNode(exp);
|
||||
@@ -1020,6 +1145,33 @@ export const useGameStore = create<Store>()(
|
||||
insights: s.insights + Math.round(res.rewardInsight),
|
||||
contact: Math.min(100, s.contact + res.rewardContact),
|
||||
});
|
||||
// 异步提交到云排行榜(失败静默,不影响本地)
|
||||
if (res.entry) {
|
||||
void submitCloudScore(res.entry);
|
||||
}
|
||||
}
|
||||
return {
|
||||
rewardInsight: res.rewardInsight,
|
||||
rewardContact: res.rewardContact,
|
||||
score: res.score,
|
||||
};
|
||||
},
|
||||
|
||||
// 深空信标:领取限时挑战奖励(v0.8.2)
|
||||
claimTimedBeacon: () => {
|
||||
const challenge = generateTimedChallenge();
|
||||
const progress = loadTimedProgress();
|
||||
const res = claimTimedReward(challenge, progress);
|
||||
if (res.rewardInsight > 0 || res.rewardContact > 0) {
|
||||
const s = get();
|
||||
set({
|
||||
insights: s.insights + Math.round(res.rewardInsight),
|
||||
contact: Math.min(100, s.contact + res.rewardContact),
|
||||
});
|
||||
// 异步提交到云排行榜(失败静默,不影响本地)
|
||||
if (res.entry) {
|
||||
void submitCloudScore(res.entry);
|
||||
}
|
||||
}
|
||||
return {
|
||||
rewardInsight: res.rewardInsight,
|
||||
@@ -1155,8 +1307,8 @@ export const useGameStore = create<Store>()(
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
// 不持久化临时字段
|
||||
partialize: (s) => {
|
||||
const { _lastAutoDecode, _lastSpawn, _combo, _lastPulse, _achievementQueue, _tideEvents, ...rest } = s;
|
||||
void _lastAutoDecode; void _lastSpawn; void _combo; void _lastPulse; void _achievementQueue; void _tideEvents;
|
||||
const { _lastAutoDecode, _lastSpawn, _combo, _lastPulse, _achievementQueue, _tideEvents, globalTide, ...rest } = s;
|
||||
void _lastAutoDecode; void _lastSpawn; void _combo; void _lastPulse; void _achievementQueue; void _tideEvents; void globalTide;
|
||||
return rest as GameState;
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user