6f5491c0-6c94-4f8e-8ad3-136d04ab862f
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
"use client";
|
||||
// 回响星核 / Echo Nexus — 遗迹探险面板(v0.2 肉鸽系统)
|
||||
import { useState } from "react";
|
||||
import { useGameStore } from "@/store/gameStore";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { EXPEDITION_CONFIG, combatWinRate } from "@/lib/game/expedition";
|
||||
import { formatNum } from "@/lib/game/config";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import {
|
||||
Swords,
|
||||
Gem,
|
||||
HelpCircle,
|
||||
Puzzle,
|
||||
Skull,
|
||||
Heart,
|
||||
Zap,
|
||||
ChevronRight,
|
||||
LogOut,
|
||||
Rocket,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import type { ExpeditionNodeType } from "@/lib/game/types";
|
||||
|
||||
const NODE_META: Record<
|
||||
ExpeditionNodeType,
|
||||
{ icon: React.ComponentType<{ className?: string }>; color: string; label: string; bg: string }
|
||||
> = {
|
||||
combat: { icon: Swords, color: "#fb7185", label: "战斗", bg: "rgba(251,113,133,0.15)" },
|
||||
treasure: { icon: Gem, color: "#fbbf24", label: "宝藏", bg: "rgba(251,191,36,0.15)" },
|
||||
choice: { icon: HelpCircle, color: "#a5f3fc", label: "抉择", bg: "rgba(165,243,252,0.15)" },
|
||||
puzzle: { icon: Puzzle, color: "#e879f9", label: "解谜", bg: "rgba(232,121,249,0.15)" },
|
||||
rest: { icon: Heart, color: "#34d399", label: "休整", bg: "rgba(52,211,153,0.15)" },
|
||||
boss: { icon: Skull, color: "#f43f5e", label: "BOSS", bg: "rgba(244,63,94,0.2)" },
|
||||
};
|
||||
|
||||
export function ExpeditionPanel() {
|
||||
const activeExpedition = useGameStore((s) => s.activeExpedition);
|
||||
const energy = useGameStore((s) => s.energy);
|
||||
const energyMax = useGameStore((s) => s.energyMax);
|
||||
const lastEnergyTick = useGameStore((s) => s.lastEnergyTick);
|
||||
const expeditionLog = useGameStore((s) => s.expeditionLog);
|
||||
const totalExpeditions = useGameStore((s) => s.totalExpeditions);
|
||||
const startExpedition = useGameStore((s) => s.startExpedition);
|
||||
const resolveCurrentNode = useGameStore((s) => s.resolveCurrentNode);
|
||||
const advanceNode = useGameStore((s) => s.advanceNode);
|
||||
const abortExpedition = useGameStore((s) => s.abortExpedition);
|
||||
const { toast } = useToast();
|
||||
const [lastLog, setLastLog] = useState<string | null>(null);
|
||||
|
||||
// 能量恢复进度
|
||||
const now = Date.now();
|
||||
const regenMs = EXPEDITION_CONFIG.energyRegenSec * 1000;
|
||||
const regenProgress = energy >= energyMax ? 100 : Math.min(100, ((now - lastEnergyTick) / regenMs) * 100);
|
||||
|
||||
const handleStart = () => {
|
||||
const res = startExpedition();
|
||||
if (!res.ok) {
|
||||
toast({ title: "无法出发", description: res.reason, variant: "destructive" });
|
||||
} else {
|
||||
toast({ title: "探险队出发", description: "深入遗迹,谨慎前行。" });
|
||||
setLastLog(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResolve = () => {
|
||||
const result = resolveCurrentNode();
|
||||
if (!result) return;
|
||||
setLastLog(result.log);
|
||||
toast({
|
||||
title: result.ended
|
||||
? result.endReason === "victory" ? "✦ 探险胜利!" : "探险失败"
|
||||
: "节点结算",
|
||||
description: result.log,
|
||||
variant: result.endReason === "defeat" ? "destructive" : "default",
|
||||
});
|
||||
};
|
||||
|
||||
const handleAdvance = () => {
|
||||
advanceNode();
|
||||
setLastLog(null);
|
||||
};
|
||||
|
||||
const handleAbort = () => {
|
||||
abortExpedition();
|
||||
toast({ title: "探险队撤退", description: "保留已获奖励,安全返回。" });
|
||||
setLastLog(null);
|
||||
};
|
||||
|
||||
// ===== 无活跃探险:展示入口 =====
|
||||
if (!activeExpedition || activeExpedition.finished) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 h-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold flex items-center gap-1.5">
|
||||
<Rocket className="h-4 w-4 text-amber-400" />
|
||||
遗迹探险
|
||||
</h3>
|
||||
<span className="text-[10px] text-muted-foreground">已探索 {totalExpeditions} 次</span>
|
||||
</div>
|
||||
|
||||
{/* 能量条 */}
|
||||
<div className="rounded-xl border border-amber-400/20 bg-amber-950/20 p-3">
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Zap className="h-3.5 w-3.5 text-amber-400" />
|
||||
<span className="text-xs text-amber-200">探险能量</span>
|
||||
</div>
|
||||
<span className="text-xs font-mono text-amber-100">
|
||||
{energy} / {energyMax}
|
||||
</span>
|
||||
</div>
|
||||
{energy < energyMax ? (
|
||||
<>
|
||||
<Progress
|
||||
value={regenProgress}
|
||||
className="h-1.5 bg-black/40 [&>div]:bg-gradient-to-r [&>div]:from-amber-500 [&>div]:to-yellow-300"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground/70 mt-1">
|
||||
下一点能量约 {Math.ceil((regenMs - (now - lastEnergyTick)) / 1000)}s 后恢复
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-[10px] text-amber-300/80 mt-0.5">能量已满,可立即出发</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 出发按钮 */}
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-4 rounded-2xl border border-dashed border-amber-400/20 bg-black/30 p-6 relative overflow-hidden">
|
||||
<div className="absolute -top-10 -right-10 h-32 w-32 rounded-full bg-amber-500/10 blur-2xl" />
|
||||
<div className="relative">
|
||||
<div className="h-20 w-20 rounded-full bg-gradient-to-br from-amber-500/30 to-rose-500/20 flex items-center justify-center mx-auto" style={{ boxShadow: "0 0 30px rgba(251,191,36,0.3)" }}>
|
||||
<Rocket className="h-9 w-9 text-amber-300" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center space-y-1">
|
||||
<p className="text-sm font-medium text-amber-100">深入以太遗迹</p>
|
||||
<p className="text-[11px] text-muted-foreground/80 max-w-[220px]">
|
||||
派出探险队进入程序化生成的遗迹路径,沿途遭遇战斗、宝藏、抉择与解谜,击败终点 BOSS 获取丰厚奖励。
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleStart}
|
||||
disabled={energy < EXPEDITION_CONFIG.energyCost}
|
||||
className="bg-gradient-to-r from-amber-600 to-rose-500 hover:from-amber-500 hover:to-rose-400 text-white border-0"
|
||||
>
|
||||
<Rocket className="h-4 w-4 mr-1.5" />
|
||||
出发(消耗 {EXPEDITION_CONFIG.energyCost} 能量)
|
||||
</Button>
|
||||
{energy < EXPEDITION_CONFIG.energyCost && (
|
||||
<p className="text-[10px] text-rose-300/80">能量不足,请等待恢复</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 探险日志 */}
|
||||
{expeditionLog.length > 0 && (
|
||||
<div className="rounded-xl border border-white/5 bg-black/20 p-2 max-h-32 overflow-y-auto echo-scroll">
|
||||
<div className="text-[10px] text-muted-foreground/60 mb-1 px-1">最近探险记录</div>
|
||||
{expeditionLog.slice(0, 4).map((entry, i) => (
|
||||
<div key={i} className="text-[10px] text-muted-foreground/70 px-1 py-0.5 border-b border-white/5 last:border-0">
|
||||
<span className="text-amber-300/60">›</span> {entry.result}
|
||||
{entry.rewards && <span className="text-emerald-300/70 ml-1">{entry.rewards}</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 活跃探险:展示地图 + 事件 =====
|
||||
const exp = activeExpedition;
|
||||
const currentNode = exp.nodes[exp.currentNode];
|
||||
const nodeMeta = NODE_META[currentNode.type];
|
||||
const NodeIcon = nodeMeta.icon;
|
||||
const hpPct = (exp.hp / exp.maxHp) * 100;
|
||||
const pathProgress = ((exp.currentNode + (currentNode.cleared ? 1 : 0)) / exp.nodes.length) * 100;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2.5 h-full">
|
||||
{/* 头部:生命 + 路径进度 */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Rocket className="h-3.5 w-3.5 text-amber-400" />
|
||||
<span className="text-xs font-semibold">遗迹探险</span>
|
||||
<span className="text-[10px] text-muted-foreground">第 {exp.currentNode + 1}/{exp.nodes.length} 节点</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[10px] text-muted-foreground">探险力</span>
|
||||
<span className="text-xs font-mono font-semibold text-amber-300">{exp.power}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 生命条 */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-0.5">
|
||||
<div className="flex items-center gap-1">
|
||||
<Heart className="h-3 w-3 text-rose-400" />
|
||||
<span className="text-[10px] text-muted-foreground">探险队生命</span>
|
||||
</div>
|
||||
<span className={`text-[10px] font-mono ${hpPct < 30 ? "text-rose-400" : "text-foreground"}`}>
|
||||
{exp.hp} / {exp.maxHp}
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={hpPct}
|
||||
className="h-1.5 bg-black/40 [&>div]:bg-gradient-to-r [&>div]:from-rose-500 [&>div]:to-emerald-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 节点路径图 */}
|
||||
<div className="rounded-xl border border-white/10 bg-black/30 p-2.5">
|
||||
<div className="flex items-center gap-1 overflow-x-auto echo-scroll pb-1">
|
||||
{exp.nodes.map((node, i) => {
|
||||
const meta = NODE_META[node.type];
|
||||
const Icon = meta.icon;
|
||||
const isCurrent = i === exp.currentNode;
|
||||
const isCleared = node.cleared;
|
||||
const isPast = i < exp.currentNode;
|
||||
return (
|
||||
<div key={node.id} className="flex items-center shrink-0">
|
||||
<div
|
||||
className={`relative h-9 w-9 rounded-lg flex items-center justify-center transition-all duration-300 ${
|
||||
isCurrent ? "scale-125" : ""
|
||||
}`}
|
||||
style={{
|
||||
background: isCleared || isPast ? "rgba(255,255,255,0.04)" : meta.bg,
|
||||
border: isCurrent ? `1.5px solid ${meta.color}` : "1px solid rgba(255,255,255,0.08)",
|
||||
boxShadow: isCurrent ? `0 0 12px ${meta.color}66` : "none",
|
||||
}}
|
||||
title={`${meta.label} · ${node.title}`}
|
||||
>
|
||||
<Icon className={`h-4 w-4 ${isCleared || isPast ? "opacity-30" : ""}`} style={{ color: isCleared || isPast ? "#666" : meta.color }} />
|
||||
{isCleared && (
|
||||
<div className="absolute -top-1 -right-1 h-3 w-3 rounded-full bg-emerald-500/80 flex items-center justify-center">
|
||||
<span className="text-[8px] text-white">✓</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{i < exp.nodes.length - 1 && (
|
||||
<ChevronRight className={`h-3 w-3 mx-0.5 ${isPast || (isCurrent && isCleared) ? "text-amber-400/60" : "text-white/15"}`} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-1.5">
|
||||
<Progress value={pathProgress} className="h-1 bg-black/40 [&>div]:bg-gradient-to-r [&>div]:from-amber-500 [&>div]:to-rose-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 当前节点事件卡 */}
|
||||
<div
|
||||
className="rounded-xl border p-3 relative overflow-hidden"
|
||||
style={{
|
||||
borderColor: `${nodeMeta.color}44`,
|
||||
background: `linear-gradient(135deg, ${nodeMeta.color}11, rgba(0,0,0,0.4))`,
|
||||
boxShadow: `0 0 20px ${nodeMeta.color}22`,
|
||||
}}
|
||||
>
|
||||
<div className="absolute -top-8 -right-8 h-24 w-24 rounded-full blur-2xl" style={{ background: `${nodeMeta.color}22` }} />
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<div
|
||||
className="h-8 w-8 rounded-lg flex items-center justify-center"
|
||||
style={{ background: nodeMeta.bg, border: `1px solid ${nodeMeta.color}44` }}
|
||||
>
|
||||
<NodeIcon className="h-4 w-4" style={{ color: nodeMeta.color }} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded" style={{ background: nodeMeta.bg, color: nodeMeta.color }}>
|
||||
{nodeMeta.label}
|
||||
</span>
|
||||
{currentNode.type === "combat" && (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
胜率 {Math.round(combatWinRate(exp.power, currentNode.difficulty) * 100)}%
|
||||
</span>
|
||||
)}
|
||||
{currentNode.difficulty > 0 && (
|
||||
<span className="text-[10px] text-muted-foreground/60">难度 {currentNode.difficulty}</span>
|
||||
)}
|
||||
</div>
|
||||
<h4 className="text-sm font-semibold mt-0.5" style={{ color: nodeMeta.color }}>
|
||||
{currentNode.title}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground/90 leading-relaxed">
|
||||
{currentNode.desc}
|
||||
</p>
|
||||
|
||||
{/* 上次结算结果 */}
|
||||
{lastLog && (
|
||||
<div className="mt-2 px-2 py-1.5 rounded-lg bg-black/40 border border-white/10">
|
||||
<p className="text-[11px] text-foreground/90">{lastLog}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
{!currentNode.cleared ? (
|
||||
<Button
|
||||
onClick={handleResolve}
|
||||
className="flex-1 bg-gradient-to-r from-amber-600 to-rose-500 hover:from-amber-500 hover:to-rose-400 text-white border-0 h-8"
|
||||
>
|
||||
<Sparkles className="h-3.5 w-3.5 mr-1" />
|
||||
探索节点
|
||||
</Button>
|
||||
) : exp.currentNode < exp.nodes.length - 1 ? (
|
||||
<Button
|
||||
onClick={handleAdvance}
|
||||
className="flex-1 h-8"
|
||||
variant="outline"
|
||||
>
|
||||
<ChevronRight className="h-3.5 w-3.5 mr-1" />
|
||||
前进
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex-1 text-center text-xs text-emerald-300 py-1.5">探险已完成 ✓</div>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleAbort}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2 text-muted-foreground hover:text-rose-300"
|
||||
>
|
||||
<LogOut className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 本次奖励累计 */}
|
||||
<div className="flex items-center gap-2 text-[10px] px-1">
|
||||
<span className="text-muted-foreground/60">本次已获:</span>
|
||||
{exp.rewards.crystals > 0 && <span className="text-emerald-300">{formatNum(exp.rewards.crystals)}晶体</span>}
|
||||
{exp.rewards.insights > 0 && <span className="text-amber-300">{exp.rewards.insights}洞见</span>}
|
||||
{exp.rewards.contact > 0 && <span className="text-fuchsia-300">{exp.rewards.contact.toFixed(1)}接触</span>}
|
||||
{(exp.rewards.crystals === 0 && exp.rewards.insights === 0) && (
|
||||
<span className="text-muted-foreground/40">暂无</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style jsx global>{`
|
||||
.echo-scroll::-webkit-scrollbar { width: 4px; height: 4px; }
|
||||
.echo-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 2px; }
|
||||
.echo-scroll::-webkit-scrollbar-track { background: transparent; }
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user