d9404a31-1970-4e79-ac9a-f4f9db60350b
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ message: "Hello, world!" });
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.13 0.02 290);
|
||||
--foreground: oklch(0.97 0.01 200);
|
||||
--card: oklch(0.19 0.025 290);
|
||||
--card-foreground: oklch(0.97 0.01 200);
|
||||
--popover: oklch(0.19 0.025 290);
|
||||
--popover-foreground: oklch(0.97 0.01 200);
|
||||
--primary: oklch(0.78 0.17 162);
|
||||
--primary-foreground: oklch(0.15 0.02 290);
|
||||
--secondary: oklch(0.28 0.03 290);
|
||||
--secondary-foreground: oklch(0.97 0.01 200);
|
||||
--muted: oklch(0.26 0.02 290);
|
||||
--muted-foreground: oklch(0.72 0.02 200);
|
||||
--accent: oklch(0.32 0.05 320);
|
||||
--accent-foreground: oklch(0.97 0.01 200);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.72 0.16 320);
|
||||
--chart-1: oklch(0.72 0.19 162);
|
||||
--chart-2: oklch(0.74 0.18 15);
|
||||
--chart-3: oklch(0.82 0.17 75);
|
||||
--chart-4: oklch(0.72 0.22 320);
|
||||
--chart-5: oklch(0.78 0.13 195);
|
||||
--sidebar: oklch(0.19 0.025 290);
|
||||
--sidebar-foreground: oklch(0.97 0.01 200);
|
||||
--sidebar-primary: oklch(0.78 0.17 162);
|
||||
--sidebar-primary-foreground: oklch(0.15 0.02 290);
|
||||
--sidebar-accent: oklch(0.32 0.05 320);
|
||||
--sidebar-accent-foreground: oklch(0.97 0.01 200);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.72 0.16 320);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
background-color: #050410;
|
||||
}
|
||||
}
|
||||
|
||||
/* 全局滚动条 */
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.14); border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.24); }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
|
||||
/* 玻璃态卡片 */
|
||||
.glass {
|
||||
background: rgba(20, 16, 40, 0.45);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
/* 渐变文字 */
|
||||
.text-gradient {
|
||||
background: linear-gradient(135deg, #34d399 0%, #e879f9 50%, #fb7185 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* 通用脉冲 */
|
||||
@keyframes echo-glow {
|
||||
0%, 100% { opacity: 0.6; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "回响星核 · Echo Nexus",
|
||||
description: "原创深空考古放置策略游戏 — 解码记忆晶体,拼凑以太文明的回响,跨越维度发起接触。",
|
||||
keywords: ["Echo Nexus", "回响星核", "idle game", "browser game", "放置游戏", "解谜", "深空考古"],
|
||||
authors: [{ name: "Super_Z" }],
|
||||
icons: {
|
||||
icon: "https://z-cdn.chatglm.cn/z-ai/static/logo.svg",
|
||||
},
|
||||
openGraph: {
|
||||
title: "回响星核 · Echo Nexus",
|
||||
description: "原创深空考古放置策略游戏",
|
||||
siteName: "Echo Nexus",
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "回响星核 · Echo Nexus",
|
||||
description: "原创深空考古放置策略游戏",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-CN" suppressHydrationWarning className="dark">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground`}
|
||||
>
|
||||
{children}
|
||||
<Toaster />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
"use client";
|
||||
// 回响星核 / Echo Nexus — 游戏主入口
|
||||
import { useState, useEffect } from "react";
|
||||
import { StarfieldCanvas } from "@/components/game/StarfieldCanvas";
|
||||
import { ResourceBar } from "@/components/game/ResourceBar";
|
||||
import { CrystalOrb } from "@/components/game/CrystalOrb";
|
||||
import { DecodePanel } from "@/components/game/DecodeArray";
|
||||
import { TechTree } from "@/components/game/TechTree";
|
||||
import { Codex } from "@/components/game/Codex";
|
||||
import { PrestigeDialog } from "@/components/game/PrestigeDialog";
|
||||
import { SettingsDialog } from "@/components/game/SettingsDialog";
|
||||
import { useGameLoop } from "@/hooks/useGameLoop";
|
||||
import { useGameStore } from "@/store/gameStore";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Settings,
|
||||
RotateCcw,
|
||||
Sparkles,
|
||||
Cpu,
|
||||
BookOpen,
|
||||
BarChart3,
|
||||
Github,
|
||||
} from "lucide-react";
|
||||
import { TECH_TREE, FRAGMENTS, formatNum } from "@/lib/game/config";
|
||||
|
||||
export default function Page() {
|
||||
useGameLoop();
|
||||
const [prestigeOpen, setPrestigeOpen] = useState(false);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const contact = useGameStore((s) => s.contact);
|
||||
const totalDecoded = useGameStore((s) => s.totalDecoded);
|
||||
const crystalsPerSec = useGameStore((s) => s.crystalsPerSec);
|
||||
const ascensions = useGameStore((s) => s.ascensions);
|
||||
const ownedTech = useGameStore((s) => s.tech);
|
||||
const ownedFragments = useGameStore((s) => s.fragments);
|
||||
const crystals = useGameStore((s) => s.crystals);
|
||||
|
||||
// 挂载检测:避免持久化 store 在 SSR/CSR 间产生 hydration 不一致
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// 防止 SSR/CSR 不一致
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-[#050410] text-muted-foreground">
|
||||
<div className="animate-pulse">唤醒回响中…</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ownedTechCount = Object.values(ownedTech).filter((v) => v > 0).length;
|
||||
const ownedFragCount = Object.values(ownedFragments).filter(Boolean).length;
|
||||
const canPrestige = contact >= 100;
|
||||
|
||||
// 目标提示
|
||||
let goal = "点击中央晶体发起脉冲,累积记忆晶体";
|
||||
if (totalDecoded === 0 && crystals >= 5) {
|
||||
goal = "右侧「待解码晶体」点击一颗晶体,开始解码";
|
||||
} else if (totalDecoded > 0 && ownedTechCount === 0) {
|
||||
goal = "用洞见解锁技术树,提升产能";
|
||||
} else if (totalDecoded > 0 && ownedFragCount < FRAGMENTS.length) {
|
||||
goal = `继续解码,拼凑记忆图谱(${ownedFragCount}/${FRAGMENTS.length})`;
|
||||
}
|
||||
if (canPrestige) goal = "✦ 接触进度已满,可发起飞升进入新周目";
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen flex flex-col bg-[#050410] text-foreground overflow-x-hidden">
|
||||
{/* 星空背景 */}
|
||||
<StarfieldCanvas className="fixed inset-0 w-full h-full -z-10" />
|
||||
|
||||
{/* 顶部 Header */}
|
||||
<header className="sticky top-0 z-30 px-3 sm:px-5 pt-3 pb-2">
|
||||
<div className="flex items-center gap-3 mb-2.5">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="relative h-9 w-9">
|
||||
<div className="absolute inset-0 rounded-full bg-gradient-to-br from-emerald-400 via-fuchsia-500 to-rose-400 blur-md opacity-60" />
|
||||
<div className="absolute inset-1 rounded-full bg-[#050410] flex items-center justify-center">
|
||||
<Sparkles className="h-4 w-4 text-fuchsia-300" />
|
||||
</div>
|
||||
</div>
|
||||
<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.1
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-1.5">
|
||||
{canPrestige && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setPrestigeOpen(true)}
|
||||
className="border-fuchsia-400/50 text-fuchsia-200 hover:bg-fuchsia-500/10 h-8 px-2.5"
|
||||
>
|
||||
<RotateCcw className="h-3.5 w-3.5 mr-1" />
|
||||
飞升
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => setSettingsOpen(true)}
|
||||
className="h-8 w-8"
|
||||
aria-label="设置"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ResourceBar onPrestige={() => setPrestigeOpen(true)} />
|
||||
</header>
|
||||
|
||||
{/* 主体 */}
|
||||
<main className="flex-1 px-3 sm:px-5 pb-3 min-h-0">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[1fr_minmax(360px,420px)] gap-3 h-full">
|
||||
{/* 左侧:脉冲晶体 */}
|
||||
<section className="glass rounded-2xl p-4 sm:p-6 flex flex-col items-center justify-center min-h-[420px] relative overflow-hidden">
|
||||
{/* 装饰光圈 */}
|
||||
<div className="pointer-events-none absolute -top-20 -left-20 h-60 w-60 rounded-full bg-emerald-500/10 blur-3xl" />
|
||||
<div className="pointer-events-none absolute -bottom-20 -right-20 h-60 w-60 rounded-full bg-fuchsia-500/10 blur-3xl" />
|
||||
<CrystalOrb />
|
||||
</section>
|
||||
|
||||
{/* 右侧:解码 + 标签面板 */}
|
||||
<section className="flex flex-col gap-3 min-h-0">
|
||||
{/* 解码面板 */}
|
||||
<div className="glass rounded-2xl p-4 flex-1 min-h-[360px] max-h-[560px]">
|
||||
<DecodePanel />
|
||||
</div>
|
||||
{/* 标签面板:技术 / 图谱 / 统计 */}
|
||||
<div className="glass rounded-2xl p-3 min-h-[280px] max-h-[380px]">
|
||||
<Tabs defaultValue="tech" className="h-full flex flex-col">
|
||||
<TabsList className="grid grid-cols-3 h-8 bg-black/30">
|
||||
<TabsTrigger value="tech" className="text-xs gap-1">
|
||||
<Cpu className="h-3 w-3" />
|
||||
技术
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="codex" className="text-xs gap-1">
|
||||
<BookOpen className="h-3 w-3" />
|
||||
图谱
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="stats" className="text-xs gap-1">
|
||||
<BarChart3 className="h-3 w-3" />
|
||||
统计
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="tech" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
|
||||
<TechTree />
|
||||
</TabsContent>
|
||||
<TabsContent value="codex" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
|
||||
<Codex />
|
||||
</TabsContent>
|
||||
<TabsContent value="stats" className="flex-1 mt-2 min-h-0">
|
||||
<StatsPanel />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 底部 Footer */}
|
||||
<footer className="sticky bottom-0 z-20 mt-auto px-3 sm:px-5 pb-2 pt-1">
|
||||
<div className="glass rounded-xl px-3 py-2 flex items-center gap-2 text-xs">
|
||||
<span className="text-fuchsia-300">▶</span>
|
||||
<span className="text-muted-foreground flex-1 truncate">{goal}</span>
|
||||
<span className="hidden sm:inline text-muted-foreground/60">|</span>
|
||||
<span className="hidden sm:inline text-muted-foreground/70">
|
||||
产能 {formatNum(crystalsPerSec)}/s
|
||||
</span>
|
||||
<a
|
||||
href="https://git.atdunbg.xyz/Super_Z/echo-nexus"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-muted-foreground/60 hover:text-foreground transition ml-1"
|
||||
aria-label="仓库"
|
||||
>
|
||||
<Github className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<PrestigeDialog open={prestigeOpen} onOpenChange={setPrestigeOpen} />
|
||||
<SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatsPanel() {
|
||||
const s = useGameStore();
|
||||
const rows = [
|
||||
{ label: "累计解码晶体", value: `${s.totalDecoded} 颗` },
|
||||
{ label: "飞升周目", value: `${s.ascensions}` },
|
||||
{ label: "持有蓝图", value: `${s.blueprints.length} / 6` },
|
||||
{ label: "已学技术", value: `${Object.values(s.tech).filter((v) => v > 0).length} / ${TECH_TREE.length}` },
|
||||
{ label: "已获碎片", value: `${Object.values(s.fragments).filter(Boolean).length} / ${FRAGMENTS.length}` },
|
||||
{ label: "接触进度", value: `${s.contact.toFixed(1)}%` },
|
||||
{ label: "晶体产能", value: `${s.crystalsPerSec.toFixed(2)} /s` },
|
||||
{ label: "仓库上限", value: `${formatNum(s.crystalCap)}` },
|
||||
{ label: "洞见倍率", value: `×${s.insightMult.toFixed(2)}` },
|
||||
];
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-1.5 text-xs">
|
||||
{rows.map((r) => (
|
||||
<div
|
||||
key={r.label}
|
||||
className="flex items-center justify-between rounded-md bg-black/25 border border-white/5 px-2 py-1.5"
|
||||
>
|
||||
<span className="text-muted-foreground">{r.label}</span>
|
||||
<span className="font-mono text-foreground">{r.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user