v0.8.1: 探险平衡 + 信标系统扩展(周挑战 + 信标链)

P1-a 探险 BOSS 胜率平衡 + 能量恢复(expedition.ts + config.ts + gameStore.ts + ExpeditionPanel.tsx):
- combatWinRate: scale 8→4, floor 0.25→0.35(基础力10对BOSS胜率 25%→35-45%)
- BOSS 难度 5-7→3-5(配合新公式基础胜率达 45-55%)
- computeEnergyRegenInterval(state): 动态恢复间隔
  - exp_2 解锁 -30%, exp_3 解锁 -20%, 探索力属性 -最高30%
  - 下限 12s(原固定 45s)
- computeEnergyRegen 接受 intervalSec 参数
- gameStore tick 传入动态间隔
- config.ts: exp_2/exp_3 描述加'能量恢复 +30%/+20%'
- ExpeditionPanel: 显示实际恢复速度 + '已加速'标记

P1-b 信标系统扩展(beacon.ts + BeaconPanel.tsx + gameStore.ts)[subagent 9-b]:
- 周挑战: getWeekKey ISO 8601 + FNV-1a 种子确定性生成
  - 难度强制 anomaly 60%/singular 40%, goal 日挑战×3-5倍
  - 完整进度/领奖/排行榜推送(isWeekly标记)
- 信标链: 4里程碑(3/7/14/30天) + grace续命机制(每链1次)
  - recordChainCompletion 核心断链/续命逻辑
  - 奖励 50→680洞见递增
- BeaconPanel: 周挑战fuchsia主题 + 信标链amber→rose渐变里程碑节点
- gameStore: trackBeacon升级 + claimWeeklyBeacon/claimChainReward actions
- VLM 8/10, lint零错误, 5场景bun测试全PASS
This commit is contained in:
2026-06-23 22:34:52 +00:00
parent a3aa381656
commit 71ca5b48a9
14 changed files with 2446 additions and 51 deletions
+471 -19
View File
@@ -1,29 +1,54 @@
"use client";
// 回响星核 / Echo Nexus — 深空信标面板v0.5 每日挑战 + 本地排行榜)
// 回响星核 / Echo Nexus — 深空信标面板
// v0.5:每日挑战 + 本地排行榜
// v0.8:周挑战 + 信标链(连续完成奖励)
import { useState, useEffect, useCallback } from "react";
import { useGameStore } from "@/store/gameStore";
import { useToast } from "@/hooks/use-toast";
import { sfx } from "@/hooks/useAudio";
import {
generateDailyChallenge,
generateWeeklyChallenge,
loadDailyProgress,
loadWeeklyProgress,
loadLeaderboard,
loadChainState,
claimBeaconReward,
msUntilNextDay,
formatCountdown,
getTodayKey,
BEACON_DIFFICULTY,
BEACON_TYPE_META,
BEACON_CHAIN_MILESTONES,
BEACON_CHAIN_REWARDS,
getWeekKey,
getTodayKey,
getNextMilestone,
getChainProgress,
msUntilNextDay,
msUntilNextWeek,
formatCountdown,
type BeaconDailyChallenge,
type BeaconDailyProgress,
type BeaconWeeklyChallenge,
type BeaconWeeklyProgress,
type BeaconScoreEntry,
type BeaconChallengeType,
type BeaconDifficulty,
type BeaconChainState,
} from "@/lib/game/beacon";
import { formatNum } from "@/lib/game/config";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { Radio, Clock, Trophy, Sparkles, Award, Crown, Medal } from "lucide-react";
import {
Radio,
Clock,
Trophy,
Sparkles,
Award,
Crown,
Medal,
Link2,
Flame,
Zap,
} from "lucide-react";
const DIFFICULTY_ORDER: BeaconDifficulty[] = ["routine", "anomaly", "singular"];
@@ -35,34 +60,48 @@ function rankBadge(rank: number) {
}
export function BeaconPanel() {
const insights = useGameStore((s) => s.insights);
const grantBeaconReward = useGameStore((s) => s.grantBeaconReward);
const claimWeeklyBeacon = useGameStore((s) => s.claimWeeklyBeacon);
const claimChainReward = useGameStore((s) => s.claimChainReward);
const { toast } = useToast();
const [challenge, setChallenge] = useState<BeaconDailyChallenge | null>(null);
const [progress, setProgress] = useState<BeaconDailyProgress | null>(null);
const [weeklyChallenge, setWeeklyChallenge] =
useState<BeaconWeeklyChallenge | null>(null);
const [weeklyProgress, setWeeklyProgress] =
useState<BeaconWeeklyProgress | null>(null);
const [chainState, setChainState] = useState<BeaconChainState | null>(null);
const [leaderboard, setLeaderboard] = useState<BeaconScoreEntry[]>([]);
const [countdown, setCountdown] = useState("00:00:00");
const [dayCountdown, setDayCountdown] = useState("00:00:00");
const [weekCountdown, setWeekCountdown] = useState("00:00:00");
const [now, setNow] = useState(Date.now());
// 初始化 + 每秒刷新(进度 + 倒计时)
useEffect(() => {
setChallenge(generateDailyChallenge());
setProgress(loadDailyProgress());
setWeeklyChallenge(generateWeeklyChallenge());
setWeeklyProgress(loadWeeklyProgress());
setChainState(loadChainState());
setLeaderboard(loadLeaderboard());
const id = setInterval(() => {
setNow(Date.now());
setProgress(loadDailyProgress());
setWeeklyProgress(loadWeeklyProgress());
setChainState(loadChainState());
setChallenge((c) => c ?? generateDailyChallenge());
setWeeklyChallenge((c) => c ?? generateWeeklyChallenge());
}, 1000);
return () => clearInterval(id);
}, []);
useEffect(() => {
setCountdown(formatCountdown(msUntilNextDay(new Date(now))));
setDayCountdown(formatCountdown(msUntilNextDay(new Date(now))));
setWeekCountdown(formatCountdown(msUntilNextWeek(new Date(now))));
}, [now]);
const handleClaim = useCallback(() => {
const handleClaimDaily = useCallback(() => {
if (!challenge || !progress) return;
if (progress.completedAt === null || progress.claimed) return;
const res = claimBeaconReward(challenge, progress);
@@ -72,12 +111,51 @@ export function BeaconPanel() {
grantBeaconReward(res.rewardInsight, res.rewardContact);
sfx("achievement");
toast({
title: "✦ 信标奖励已领取",
description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(1)} 接触 · 得分 ${res.score}`,
title: "✦ 每日信标奖励已领取",
description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(
1
)} 接触 · 得分 ${res.score}`,
});
}, [challenge, progress, toast, grantBeaconReward]);
if (!challenge || !progress) {
const handleClaimWeekly = useCallback(() => {
if (!weeklyChallenge || !weeklyProgress) return;
if (weeklyProgress.completedAt === null || weeklyProgress.claimed) return;
const res = claimWeeklyBeacon();
setLeaderboard(loadLeaderboard());
setWeeklyProgress(loadWeeklyProgress());
if (res.rewardInsight > 0 || res.rewardContact > 0) {
sfx("achievement");
toast({
title: "✦ 周挑战奖励已领取",
description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(
1
)} 接触 · 得分 ${res.score}`,
});
}
}, [weeklyChallenge, weeklyProgress, toast, claimWeeklyBeacon]);
const handleClaimChain = useCallback(
(milestone: number) => {
const reward = BEACON_CHAIN_REWARDS.find(
(r) => r.milestone === milestone
);
const res = claimChainReward(milestone);
setChainState(loadChainState());
if (res.ok) {
sfx("achievement");
toast({
title: `${res.label || reward?.label || "里程碑"} 已领取`,
description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(
1
)} 接触`,
});
}
},
[toast, claimChainReward]
);
if (!challenge || !progress || !weeklyChallenge || !weeklyProgress || !chainState) {
return (
<div className="flex items-center justify-center h-full text-xs text-muted-foreground/60">
@@ -92,6 +170,20 @@ export function BeaconPanel() {
const isClaimed = progress.claimed;
const canClaim = isCompleted && !isClaimed;
// 周挑战派生量
const wDiffMeta = BEACON_DIFFICULTY[weeklyChallenge.difficulty];
const wTypeMeta = BEACON_TYPE_META[weeklyChallenge.type];
const wPct = Math.min(100, (weeklyProgress.progress / weeklyChallenge.goal) * 100);
const wCompleted = weeklyProgress.completedAt !== null;
const wClaimed = weeklyProgress.claimed;
const wCanClaim = wCompleted && !wClaimed;
// 信标链派生量
const chainProgress = getChainProgress(chainState.currentStreak);
const nextMilestone = getNextMilestone(chainState.currentStreak);
const todayKey = getTodayKey();
const completedToday = chainState.lastCompletedDateKey === todayKey;
return (
<div className="flex flex-col gap-2.5 h-full overflow-y-auto echo-scroll pr-0.5">
<style jsx global>{`
@@ -106,6 +198,29 @@ export function BeaconPanel() {
0%, 100% { box-shadow: 0 0 18px ${diffMeta.glow}, inset 0 0 12px ${diffMeta.glow}; }
50% { box-shadow: 0 0 32px ${diffMeta.glow}, inset 0 0 20px ${diffMeta.glow}; }
}
@keyframes weekly-glow {
0%, 100% { box-shadow: 0 0 18px rgba(232,121,249,0.35), inset 0 0 12px rgba(232,121,249,0.25); }
50% { box-shadow: 0 0 32px rgba(232,121,249,0.5), inset 0 0 20px rgba(232,121,249,0.35); }
}
@keyframes chain-milestone-pulse {
0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(251,113,133,0.55); }
50% { transform: scale(1.06); box-shadow: 0 0 0 8px rgba(251,113,133,0); }
}
.chain-milestone-reachable {
animation: chain-milestone-pulse 1.8s ease-in-out infinite;
}
@keyframes chain-streak-flux {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.chain-streak-text {
background: linear-gradient(90deg, #fbbf24, #fb7185, #fbbf24);
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: chain-streak-flux 4s ease-in-out infinite;
}
`}</style>
{/* 头部:信标 + 倒计时 */}
@@ -117,7 +232,7 @@ export function BeaconPanel() {
<div className="flex items-center gap-1 text-[10px] text-muted-foreground/70">
<Clock className="h-3 w-3" />
<span className="font-mono text-fuchsia-300/90">{countdown}</span>
<span className="font-mono text-fuchsia-300/90">{dayCountdown}</span>
</div>
</div>
@@ -205,14 +320,15 @@ export function BeaconPanel() {
<span className="text-fuchsia-300 font-mono">+{challenge.rewardContact.toFixed(1)}</span>
</div>
<Button
onClick={handleClaim}
onClick={handleClaimDaily}
disabled={!canClaim}
size="sm"
className="h-7 px-3 text-[11px] border-0"
style={{
background: canClaim
? `linear-gradient(90deg, ${diffMeta.color}, ${diffMeta.color}cc)`
: undefined,
: `${diffMeta.color}1a`,
color: canClaim ? "#022c22" : `${diffMeta.color}99`,
}}
>
{isClaimed ? "已领取" : canClaim ? "领取奖励" : isCompleted ? "已领取" : "进行中…"}
@@ -228,6 +344,333 @@ export function BeaconPanel() {
</div>
</div>
{/* 周挑战 + 信标链(桌面端并排,移动端单列) */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-2.5">
{/* ===== 周挑战卡片(fuchsia 主题) ===== */}
<div
className="relative rounded-xl border p-3 overflow-hidden"
style={{
borderColor: "rgba(232,121,249,0.4)",
background: `linear-gradient(135deg, rgba(232,121,249,0.12), rgba(0,0,0,0.45))`,
animation: wCompleted ? "none" : "weekly-glow 3.5s ease-in-out infinite",
}}
>
{/* 头部:标题 + weekKey + 倒计时 */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-1.5">
<Zap className="h-3.5 w-3.5 text-fuchsia-400" />
<span className="text-xs font-semibold text-fuchsia-200">
· WEEKLY
</span>
</div>
<div className="flex items-center gap-1 text-[9px] text-muted-foreground/70">
<span className="font-mono text-fuchsia-300/80">
{weeklyChallenge.weekKey}
</span>
<Clock className="h-2.5 w-2.5" />
<span className="font-mono text-fuchsia-300/90">{weekCountdown}</span>
</div>
</div>
{/* 难度 + 类型 标签 */}
<div className="flex items-center gap-1.5 mb-2">
<span
className="text-[10px] font-mono px-1.5 py-0.5 rounded flex items-center gap-1"
style={{ background: `${wDiffMeta.color}22`, color: wDiffMeta.color, border: `1px solid ${wDiffMeta.color}55` }}
>
<span>{wDiffMeta.icon}</span>
{wDiffMeta.label}
</span>
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-white/5 text-muted-foreground border border-white/10">
{wTypeMeta.icon} {wTypeMeta.label}
</span>
{wCompleted && (
<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">
<Sparkles className="h-2.5 w-2.5" />
</span>
)}
</div>
{/* 标题 */}
<h4
className="text-sm font-semibold mb-1"
style={{ color: wDiffMeta.color, textShadow: `0 0 10px ${wDiffMeta.glow}` }}
>
{weeklyChallenge.title}
</h4>
<p className="text-[10px] text-muted-foreground/80 leading-relaxed mb-2">
{weeklyChallenge.desc}
</p>
{/* 进度条 */}
<div className="mb-2">
<div className="flex items-center justify-between mb-1">
<span className="text-[10px] text-muted-foreground/70"></span>
<span className="text-[11px] font-mono font-semibold" style={{ color: wDiffMeta.color }}>
{Math.min(weeklyProgress.progress, weeklyChallenge.goal)} / {weeklyChallenge.goal} {wTypeMeta.unit}
</span>
</div>
<Progress
value={wPct}
className="h-2 bg-black/40"
style={{
["--progress-color" as string]: wDiffMeta.color,
}}
/>
</div>
{/* 奖励 + 领取按钮 */}
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-1.5 text-[10px]">
<span className="text-muted-foreground/60"></span>
{weeklyChallenge.rewardInsight > 0 && (
<span className="text-amber-300 font-mono">
+{formatNum(weeklyChallenge.rewardInsight)}
</span>
)}
<span className="text-fuchsia-300 font-mono">
+{weeklyChallenge.rewardContact.toFixed(1)}
</span>
</div>
<Button
onClick={handleClaimWeekly}
disabled={!wCanClaim}
size="sm"
className="h-7 px-3 text-[11px] border-0"
style={{
background: wCanClaim
? `linear-gradient(90deg, #34d399, #34d399cc)`
: "rgba(232,121,249,0.10)",
color: wCanClaim
? "#022c22"
: "rgba(232,121,249,0.55)",
}}
>
{wClaimed
? "已领取"
: wCanClaim
? "领取奖励"
: wCompleted
? "已领取"
: "进行中…"}
</Button>
</div>
{wCompleted && weeklyProgress.durationSec > 0 && (
<div className="mt-1.5 text-[10px] text-muted-foreground/50 text-center">
{Math.floor(weeklyProgress.durationSec / 60)}{weeklyProgress.durationSec % 60}
</div>
)}
</div>
{/* ===== 信标链卡片(amber→rose 渐变) ===== */}
<div
className="relative rounded-xl border p-3 overflow-hidden"
style={{
borderColor: "rgba(251,191,36,0.35)",
background: `linear-gradient(135deg, rgba(251,191,36,0.10), rgba(251,113,133,0.10), rgba(0,0,0,0.4))`,
}}
>
{/* 头部:标题 + 当前连续天数 */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-1.5">
<Link2 className="h-3.5 w-3.5 text-amber-400" />
<span className="text-xs font-semibold text-amber-200">
· CHAIN
</span>
</div>
<div className="flex items-baseline gap-1">
<Flame className="h-3 w-3 text-rose-400" />
<span className="text-[10px] text-muted-foreground/60"></span>
<span className="chain-streak-text text-2xl font-bold font-mono leading-none">
{chainState.currentStreak}
</span>
<span className="text-[10px] text-muted-foreground/60"></span>
</div>
</div>
{/* 今日完成状态 */}
<div className="mb-2 flex items-center justify-between">
<span className="text-[10px] text-muted-foreground/70">
{completedToday
? "今日已贡献"
: "今日尚未完成日挑战"}
</span>
<span
className={`text-[10px] font-mono px-1.5 py-0.5 rounded border ${
completedToday
? "bg-emerald-500/15 text-emerald-300 border-emerald-500/30"
: "bg-white/5 text-muted-foreground/70 border-white/10"
}`}
>
{completedToday ? "✓ 已记录" : "○ 待完成"}
</span>
</div>
{/* 4 个里程碑节点横向排列 */}
<div className="flex items-center justify-between mb-2 relative">
{/* 节点之间的连线(背景灰) */}
<div className="absolute top-6 left-[12.5%] right-[12.5%] h-[2px] bg-white/10" />
{/* 已达成部分高亮(基于 prev→next 插值) */}
{(() => {
// 节点圆心水平位置(百分比)
const MILESTONE_POS: Record<number, number> = {
0: 12.5,
3: 12.5,
7: 37.5,
14: 62.5,
30: 87.5,
};
const prevPos =
MILESTONE_POS[chainProgress.prev] ?? 12.5;
const nextPos =
chainProgress.next !== null
? MILESTONE_POS[chainProgress.next] ?? 87.5
: 87.5;
const pct = chainProgress.progressPct / 100;
const activeEndPos = prevPos + (nextPos - prevPos) * pct;
const widthPct = Math.max(0, activeEndPos - 12.5);
if (widthPct <= 0) return null;
return (
<div
className="absolute top-6 h-[2px]"
style={{
left: "12.5%",
width: `${widthPct}%`,
background:
"linear-gradient(90deg, #fbbf24, #fb7185)",
boxShadow: "0 0 8px rgba(251,113,133,0.5)",
}}
/>
);
})()}
{BEACON_CHAIN_MILESTONES.map((m) => {
const reward = BEACON_CHAIN_REWARDS.find((r) => r.milestone === m);
const claimed = chainState.milestonesClaimed.includes(m);
const reachable =
chainState.currentStreak >= m && !claimed;
const inProgress = chainState.currentStreak > 0 && nextMilestone === m;
// 节点配色
let nodeBg = "rgba(255,255,255,0.05)";
let nodeBorder = "rgba(255,255,255,0.15)";
let textColor = "rgba(255,255,255,0.4)";
if (claimed) {
nodeBg = "rgba(52,211,153,0.25)";
nodeBorder = "#34d399";
textColor = "#34d399";
} else if (reachable) {
nodeBg = "rgba(251,113,133,0.20)";
nodeBorder = "#fb7185";
textColor = "#fb7185";
} else if (inProgress) {
nodeBg = "rgba(251,191,36,0.20)";
nodeBorder = "#fbbf24";
textColor = "#fbbf24";
}
return (
<div
key={m}
className="flex flex-col items-center gap-1 z-10 flex-1"
>
<div
className={`w-12 h-12 rounded-full flex items-center justify-center font-mono font-bold text-sm border-2 ${
reachable ? "chain-milestone-reachable" : ""
}`}
style={{
background: nodeBg,
borderColor: nodeBorder,
color: textColor,
}}
>
{claimed ? (
<Sparkles className="h-4 w-4" />
) : (
m
)}
</div>
<span
className="text-[9px] text-center leading-tight"
style={{ color: textColor }}
>
{reward?.label}
</span>
{claimed ? (
<span className="text-[9px] text-emerald-400/80 font-mono">
</span>
) : reachable ? (
<Button
onClick={() => handleClaimChain(m)}
size="sm"
className="h-5 px-2 text-[9px] py-0 border-0"
style={{
background:
"linear-gradient(90deg, #fb7185, #f43f5e)",
color: "#1c0608",
}}
>
</Button>
) : (
<span className="text-[9px] text-muted-foreground/40 font-mono">
+{reward?.rewardInsight}
</span>
)}
</div>
);
})}
</div>
{/* 进度条 */}
<div className="mb-2">
<div className="flex items-center justify-between mb-1">
<span className="text-[10px] text-muted-foreground/70">
{chainProgress.next === null
? "已通关全部里程碑"
: `下一目标:${chainProgress.next}`}
</span>
<span className="text-[10px] font-mono text-amber-300">
{chainProgress.current}
{chainProgress.next !== null && ` / ${chainProgress.next}`}
</span>
</div>
<Progress
value={chainProgress.progressPct}
className="h-1.5 bg-black/40"
style={{
["--progress-color" as string]: "#fbbf24",
}}
/>
</div>
{/* 底部统计 */}
<div className="flex items-center justify-between gap-2 text-[10px]">
<div className="flex items-center gap-2">
<span className="text-muted-foreground/60"></span>
<span className="font-mono text-amber-300">
{chainState.longestStreak}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-muted-foreground/60"></span>
<span className="font-mono text-rose-300">
{chainState.totalCompletions}
</span>
</div>
<div className="flex items-center gap-1">
<span className="text-muted-foreground/60"></span>
{chainState.graceUsed >= 1 ? (
<span className="font-mono text-rose-400/80"></span>
) : (
<span className="font-mono text-emerald-400/80"></span>
)}
</div>
</div>
</div>
</div>
{/* 本地排行榜 */}
<div className="rounded-xl border border-white/10 bg-black/30 p-2.5">
<div className="flex items-center gap-1.5 mb-2">
@@ -239,7 +682,7 @@ export function BeaconPanel() {
{leaderboard.length === 0 ? (
<div className="text-center py-4 text-[11px] text-muted-foreground/40">
<Trophy className="h-6 w-6 mx-auto mb-1 opacity-30" />
</div>
) : (
<div className="space-y-0.5 max-h-[180px] overflow-y-auto echo-scroll">
@@ -247,12 +690,16 @@ export function BeaconPanel() {
const rb = rankBadge(i);
const eDiff = BEACON_DIFFICULTY[entry.difficulty];
const eType = BEACON_TYPE_META[entry.challenge as BeaconChallengeType];
const isMine = entry.dateKey === getTodayKey();
const isMine = entry.dateKey === getTodayKey() || entry.dateKey === getWeekKey();
return (
<div
key={`${entry.timestamp}-${i}`}
className={`flex items-center gap-2 px-2 py-1 rounded-lg text-[11px] ${
isMine ? "bg-fuchsia-500/10 border border-fuchsia-500/20" : "hover:bg-white/5"
entry.isWeekly
? "bg-fuchsia-500/10 border border-fuchsia-500/20"
: isMine
? "bg-fuchsia-500/10 border border-fuchsia-500/20"
: "hover:bg-white/5"
}`}
>
{/* 排名 */}
@@ -263,10 +710,15 @@ export function BeaconPanel() {
<span className="text-muted-foreground/50 font-mono">{i + 1}</span>
)}
</span>
{/* 类型 + 难度 */}
{/* 类型 + 难度 + 周挑战标记 */}
<span className="flex items-center gap-1 flex-1 min-w-0">
<span style={{ color: eDiff.color }} className="font-mono">{eDiff.icon}</span>
<span className="text-muted-foreground/80 truncate">{eType.label}</span>
{entry.isWeekly && (
<span className="text-[9px] font-mono px-1 rounded bg-fuchsia-500/20 text-fuchsia-300 border border-fuchsia-500/40">
WEEK
</span>
)}
{entry.progress >= 1 && <span className="text-emerald-400/70"></span>}
</span>
{/* 用时 */}
+9 -3
View File
@@ -4,7 +4,7 @@ import { useState } from "react";
import { useGameStore } from "@/store/gameStore";
import { useToast } from "@/hooks/use-toast";
import { sfx } from "@/hooks/useAudio";
import { EXPEDITION_CONFIG, combatWinRate } from "@/lib/game/expedition";
import { EXPEDITION_CONFIG, combatWinRate, computeEnergyRegenInterval } from "@/lib/game/expedition";
import { formatNum } from "@/lib/game/config";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
@@ -42,6 +42,8 @@ export function ExpeditionPanel() {
const lastEnergyTick = useGameStore((s) => s.lastEnergyTick);
const expeditionLog = useGameStore((s) => s.expeditionLog);
const totalExpeditions = useGameStore((s) => s.totalExpeditions);
// v0.8.1 动态能量恢复间隔(技术 + 探索力属性缩短)
const regenIntervalSec = useGameStore((s) => computeEnergyRegenInterval(s));
const startExpedition = useGameStore((s) => s.startExpedition);
const resolveCurrentNode = useGameStore((s) => s.resolveCurrentNode);
const advanceNode = useGameStore((s) => s.advanceNode);
@@ -49,10 +51,11 @@ export function ExpeditionPanel() {
const { toast } = useToast();
const [lastLog, setLastLog] = useState<string | null>(null);
// 能量恢复进度
// 能量恢复进度v0.8.1 动态间隔)
const now = Date.now();
const regenMs = EXPEDITION_CONFIG.energyRegenSec * 1000;
const regenMs = regenIntervalSec * 1000;
const regenProgress = energy >= energyMax ? 100 : Math.min(100, ((now - lastEnergyTick) / regenMs) * 100);
const regenBoosted = regenIntervalSec < EXPEDITION_CONFIG.energyRegenSec;
const handleStart = () => {
const res = startExpedition();
@@ -126,6 +129,9 @@ export function ExpeditionPanel() {
/>
<p className="text-[10px] text-muted-foreground/70 mt-1">
{Math.ceil((regenMs - (now - lastEnergyTick)) / 1000)}s
{regenBoosted && (
<span className="text-emerald-400/80 ml-1"> {regenIntervalSec.toFixed(0)}s/ ()</span>
)}
</p>
</>
) : (