v0.14: 放置系统 (Idle Operations) — 采矿无人机舰队 + 6放置工程 + 永久产能加成 + idle徽章

This commit is contained in:
2026-06-24 10:19:40 +00:00
parent 1701a908c9
commit 17820b1f28
189 changed files with 2718 additions and 35 deletions
Regular → Executable
+76 -6
View File
@@ -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">