v0.14: 放置系统 (Idle Operations) — 采矿无人机舰队 + 6放置工程 + 永久产能加成 + idle徽章
This commit is contained in:
Regular → Executable
+76
-6
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
// 回响星核 / Echo Nexus — 游戏主入口
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { StarfieldCanvas } from "@/components/game/StarfieldCanvas";
|
||||
import { ResourceBar } from "@/components/game/ResourceBar";
|
||||
import { CrystalOrb } from "@/components/game/CrystalOrb";
|
||||
@@ -20,6 +20,9 @@ import { TutorialOverlay } from "@/components/game/TutorialOverlay";
|
||||
import { OfflineReportDialog } from "@/components/game/OfflineReportDialog";
|
||||
import { CruiseMode } from "@/components/game/CruiseMode";
|
||||
import { AttributesPanel } from "@/components/game/AttributesPanel";
|
||||
import { IdleOperationsPanel } from "@/components/game/IdleOperationsPanel";
|
||||
import { IdleStatusBadge } from "@/components/game/IdleStatusBadge";
|
||||
import { IdleProjectBar } from "@/components/game/IdleProjectBar";
|
||||
import {
|
||||
StarTideNotifier,
|
||||
StarTideIndicator,
|
||||
@@ -44,12 +47,14 @@ import {
|
||||
Radio,
|
||||
Navigation,
|
||||
User,
|
||||
Clock,
|
||||
} from "lucide-react";
|
||||
import { TECH_TREE, FRAGMENTS, formatNum } from "@/lib/game/config";
|
||||
import { ACHIEVEMENTS } from "@/lib/game/achievements";
|
||||
import { TIDE_EVENTS } from "@/lib/game/starTide";
|
||||
import { CONSTELLATION_PERKS } from "@/lib/game/constellation";
|
||||
import { generateDailyChallenge, loadDailyProgress, loadLeaderboard } from "@/lib/game/beacon";
|
||||
import { getUnlockedIdleProjects } from "@/lib/game/idle";
|
||||
|
||||
export default function Page() {
|
||||
useGameLoop();
|
||||
@@ -77,6 +82,14 @@ export default function Page() {
|
||||
const hasActiveExpedition = useGameStore((s) => !!s.activeExpedition && !s.activeExpedition.finished);
|
||||
const chronicleCount = useGameStore((s) => s.chronicle?.length ?? 0);
|
||||
const pendingAttrPoints = useGameStore((s) => s.pendingAttrPoints ?? 0);
|
||||
// v0.14 放置系统:用于驱动 goal 提示 + 标签默认值
|
||||
const idleProjectSlots = useGameStore(
|
||||
(s) => (s.idleProjectSlots ?? [null, null, null]) as (
|
||||
| { projectId: string; completed: boolean }
|
||||
| null
|
||||
)[]
|
||||
);
|
||||
const createdAt = useGameStore((s) => s.createdAt);
|
||||
|
||||
// 深空信标:检测是否有可领取的奖励(独立 localStorage)
|
||||
const [beaconClaimable, setBeaconClaimable] = useState(false);
|
||||
@@ -103,10 +116,26 @@ export default function Page() {
|
||||
|
||||
// 挂载检测:避免持久化 store 在 SSR/CSR 间产生 hydration 不一致
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// v0.14 放置系统:默认放置标签(在挂载后按优先级切换一次)
|
||||
// 注意:所有 hooks 必须在早期 return 之前调用,保持调用顺序一致
|
||||
const [activeTab, setActiveTab] = useState("idle");
|
||||
const tabInited = useRef(false);
|
||||
const hasPendingPerkEarly = !!(pendingPerkChoices && pendingPerkChoices.length > 0);
|
||||
useEffect(() => {
|
||||
if (tabInited.current) return;
|
||||
tabInited.current = true;
|
||||
if (hasActiveExpedition) {
|
||||
setActiveTab("expedition");
|
||||
} else if (hasPendingPerkEarly) {
|
||||
setActiveTab("constellation");
|
||||
} else {
|
||||
setActiveTab("idle");
|
||||
}
|
||||
}, [hasActiveExpedition, hasPendingPerkEarly]);
|
||||
|
||||
// 防止 SSR/CSR 不一致
|
||||
if (!mounted) {
|
||||
return (
|
||||
@@ -125,6 +154,19 @@ export default function Page() {
|
||||
// 仓库满仓警告
|
||||
const warehouseFull = crystals >= crystalCap * 0.98;
|
||||
|
||||
// v0.14 放置系统相关 goal 提示
|
||||
const idleClaimableCount = idleProjectSlots.filter(
|
||||
(s) => s !== null && s.completed
|
||||
).length;
|
||||
const idleRunningCount = idleProjectSlots.filter(
|
||||
(s) => s !== null && !s.completed
|
||||
).length;
|
||||
const playtimeSec = (Date.now() - (createdAt ?? Date.now())) / 1000;
|
||||
const unlockedIdleCount = getUnlockedIdleProjects({
|
||||
ascensions,
|
||||
crystalsPerSec,
|
||||
}).length;
|
||||
|
||||
// 目标提示
|
||||
let goal = "点击中央晶体发起脉冲,累积记忆晶体";
|
||||
if (totalDecoded === 0 && crystals >= 5) {
|
||||
@@ -151,6 +193,16 @@ export default function Page() {
|
||||
goal = `${tm.icon} 星潮「${tm.name}」进行中 · ${tm.desc}`;
|
||||
}
|
||||
if (canPrestige) goal = "✦ 接触进度已满,可发起飞升进入新周目";
|
||||
// v0.14 放置工程 goal 提示(高优先级,覆盖默认)
|
||||
if (idleClaimableCount > 0) {
|
||||
goal = `✦ 放置工程已完成 ${idleClaimableCount} 项,请前往「放置」标签领取奖励`;
|
||||
} else if (
|
||||
idleRunningCount === 0 &&
|
||||
unlockedIdleCount > 0 &&
|
||||
playtimeSec > 60
|
||||
) {
|
||||
goal = "「放置」标签可派遣工程项目,离线自动产出";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen flex flex-col bg-[#050410] text-foreground overflow-x-hidden">
|
||||
@@ -172,12 +224,13 @@ export default function Page() {
|
||||
<div className="leading-tight">
|
||||
<h1 className="text-base sm:text-lg font-bold text-gradient">回响星核</h1>
|
||||
<p className="text-[9px] sm:text-[10px] text-muted-foreground/70 -mt-0.5 tracking-wider">
|
||||
ECHO NEXUS · v0.8
|
||||
ECHO NEXUS · v0.14
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-1.5">
|
||||
<StarTideIndicator />
|
||||
<IdleStatusBadge onClick={() => setActiveTab("idle")} />
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@@ -252,6 +305,8 @@ export default function Page() {
|
||||
<div data-tut="crystal-orb" className="contents">
|
||||
<CrystalOrb />
|
||||
</div>
|
||||
{/* v0.14 放置工程进度条(晶体球下方) */}
|
||||
<IdleProjectBar />
|
||||
</section>
|
||||
|
||||
{/* 右侧:解码 + 标签面板 */}
|
||||
@@ -260,10 +315,19 @@ export default function Page() {
|
||||
<div data-tut="decode-panel" className="glass rounded-2xl p-4 flex-1 min-h-[300px] sm:min-h-[360px] max-h-[560px]">
|
||||
<DecodePanel />
|
||||
</div>
|
||||
{/* 标签面板:探险 / 技术 / 星图 / 图谱 / 成就 / 信标 / 统计 / 角色 */}
|
||||
{/* 标签面板:放置 / 探险 / 技术 / 星图 / 图谱 / 成就 / 信标 / 统计 / 角色 */}
|
||||
<div className="glass rounded-2xl p-3 min-h-[280px] sm:min-h-[320px] max-h-[440px] flex flex-col">
|
||||
<Tabs defaultValue={hasActiveExpedition ? "expedition" : hasPendingPerk ? "constellation" : "tech"} className="h-full flex flex-col">
|
||||
<TabsList className="grid grid-cols-8 h-9 bg-black/30 gap-0.5 p-1">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex flex-col">
|
||||
<TabsList className="grid grid-cols-9 h-9 bg-black/30 gap-0.5 p-1">
|
||||
<TabsTrigger value="idle" data-tut="tab-idle" 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">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
<span className="leading-none">放置</span>
|
||||
{idleClaimableCount > 0 && (
|
||||
<span className="absolute -top-0.5 -right-0.5 min-w-[14px] h-[14px] px-1 rounded-full bg-emerald-500 text-[9px] font-mono font-bold text-white flex items-center justify-center border border-emerald-300/50 animate-pulse">
|
||||
{idleClaimableCount}
|
||||
</span>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
<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">
|
||||
<Rocket className="h-3.5 w-3.5" />
|
||||
<span className="leading-none">探险</span>
|
||||
@@ -315,6 +379,9 @@ export default function Page() {
|
||||
)}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="idle" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
|
||||
<IdleOperationsPanel />
|
||||
</TabsContent>
|
||||
<TabsContent value="expedition" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
|
||||
<ExpeditionPanel />
|
||||
</TabsContent>
|
||||
@@ -442,6 +509,9 @@ function StatsPanel() {
|
||||
{ label: "勇气", value: `${s.attributes?.courage ?? 0} / 100` },
|
||||
{ label: "灵感", value: `${s.attributes?.inspiration ?? 0} / 100` },
|
||||
{ label: "待分配属性点", value: `${s.pendingAttrPoints ?? 0}` },
|
||||
{ label: "放置永久加成", value: `+${formatNum(s.idlePermanentBonus ?? 0)} /s` },
|
||||
{ label: "完成放置工程", value: `${s.idleStats?.projectsCompleted ?? 0} 项` },
|
||||
{ label: "放置产出晶体", value: formatNum(s.idleStats?.crystalsFromIdle ?? 0) },
|
||||
];
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-1.5 text-xs">
|
||||
|
||||
Reference in New Issue
Block a user