Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18b0ed110c |
@@ -1,149 +0,0 @@
|
||||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
|
||||
# vitepress cache directory
|
||||
**/.vitepress/cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
|
||||
# Echo Nexus 本地文件
|
||||
local-*
|
||||
.claude
|
||||
.z-ai-config
|
||||
dev.log
|
||||
dev.out.log
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
.vercel
|
||||
skills/
|
||||
@@ -1,122 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 将 stderr 重定向到 stdout,避免 execute_command 因为 stderr 输出而报错
|
||||
exec 2>&1
|
||||
|
||||
set -e
|
||||
|
||||
# 获取脚本所在目录(.zscripts 目录,即 workspace-agent/.zscripts)
|
||||
# 使用 $0 获取脚本路径(兼容 sh 和 bash)
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Next.js 项目路径
|
||||
NEXTJS_PROJECT_DIR="/home/z/my-project"
|
||||
|
||||
# 检查 Next.js 项目目录是否存在
|
||||
if [ ! -d "$NEXTJS_PROJECT_DIR" ]; then
|
||||
echo "❌ 错误: Next.js 项目目录不存在: $NEXTJS_PROJECT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🚀 开始构建 Next.js 应用和 mini-services..."
|
||||
echo "📁 Next.js 项目路径: $NEXTJS_PROJECT_DIR"
|
||||
|
||||
# 切换到 Next.js 项目目录
|
||||
cd "$NEXTJS_PROJECT_DIR" || exit 1
|
||||
|
||||
# 设置环境变量
|
||||
export NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
BUILD_DIR="/tmp/build_fullstack_$BUILD_ID"
|
||||
echo "📁 清理并创建构建目录: $BUILD_DIR"
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
# 安装依赖
|
||||
echo "📦 安装依赖..."
|
||||
bun install
|
||||
|
||||
# 构建 Next.js 应用
|
||||
echo "🔨 构建 Next.js 应用..."
|
||||
bun run build
|
||||
|
||||
# 构建 mini-services
|
||||
# 检查 Next.js 项目目录下是否有 mini-services 目录
|
||||
if [ -d "$NEXTJS_PROJECT_DIR/mini-services" ]; then
|
||||
echo "🔨 构建 mini-services..."
|
||||
# 使用 workspace-agent 目录下的 mini-services 脚本
|
||||
sh "$SCRIPT_DIR/mini-services-install.sh"
|
||||
sh "$SCRIPT_DIR/mini-services-build.sh"
|
||||
|
||||
# 复制 mini-services-start.sh 到 mini-services-dist 目录
|
||||
echo " - 复制 mini-services-start.sh 到 $BUILD_DIR"
|
||||
cp "$SCRIPT_DIR/mini-services-start.sh" "$BUILD_DIR/mini-services-start.sh"
|
||||
chmod +x "$BUILD_DIR/mini-services-start.sh"
|
||||
else
|
||||
echo "ℹ️ mini-services 目录不存在,跳过"
|
||||
fi
|
||||
|
||||
# 将所有构建产物复制到临时构建目录
|
||||
echo "📦 收集构建产物到 $BUILD_DIR..."
|
||||
|
||||
# 复制 Next.js standalone 构建输出
|
||||
if [ -d ".next/standalone" ]; then
|
||||
echo " - 复制 .next/standalone"
|
||||
cp -r .next/standalone "$BUILD_DIR/next-service-dist/"
|
||||
fi
|
||||
|
||||
# 复制 Next.js 静态文件
|
||||
if [ -d ".next/static" ]; then
|
||||
echo " - 复制 .next/static"
|
||||
mkdir -p "$BUILD_DIR/next-service-dist/.next"
|
||||
cp -r .next/static "$BUILD_DIR/next-service-dist/.next/"
|
||||
fi
|
||||
|
||||
# 复制 public 目录
|
||||
if [ -d "public" ]; then
|
||||
echo " - 复制 public"
|
||||
cp -r public "$BUILD_DIR/next-service-dist/"
|
||||
fi
|
||||
|
||||
# 将测试环境数据库复制到构建产物中,生产环境直接使用这份数据库
|
||||
if [ -f "./db/custom.db" ]; then
|
||||
echo "🗄️ 复制测试环境数据库到构建产物..."
|
||||
mkdir -p "$BUILD_DIR/db"
|
||||
cp -r ./db/. "$BUILD_DIR/db/"
|
||||
|
||||
echo "🗄️ 同步构建产物中的数据库结构..."
|
||||
DATABASE_URL="file:$BUILD_DIR/db/custom.db" bun run db:push
|
||||
echo "✅ 构建产物数据库已准备完成"
|
||||
ls -lah "$BUILD_DIR/db"
|
||||
else
|
||||
echo "❌ 未找到测试环境数据库文件 ./db/custom.db,无法继续构建生产包"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 复制 Caddyfile(如果存在)
|
||||
if [ -f "Caddyfile" ]; then
|
||||
echo " - 复制 Caddyfile"
|
||||
cp Caddyfile "$BUILD_DIR/"
|
||||
else
|
||||
echo "ℹ️ Caddyfile 不存在,跳过"
|
||||
fi
|
||||
|
||||
# 复制 start.sh 脚本
|
||||
echo " - 复制 start.sh 到 $BUILD_DIR"
|
||||
cp "$SCRIPT_DIR/start.sh" "$BUILD_DIR/start.sh"
|
||||
chmod +x "$BUILD_DIR/start.sh"
|
||||
|
||||
# 打包到 $BUILD_DIR.tar.gz
|
||||
PACKAGE_FILE="${BUILD_DIR}.tar.gz"
|
||||
echo ""
|
||||
echo "📦 打包构建产物到 $PACKAGE_FILE..."
|
||||
cd "$BUILD_DIR" || exit 1
|
||||
tar -czf "$PACKAGE_FILE" .
|
||||
cd - > /dev/null || exit 1
|
||||
|
||||
# # 清理临时目录
|
||||
# rm -rf "$BUILD_DIR"
|
||||
|
||||
echo ""
|
||||
echo "✅ 构建完成!所有产物已打包到 $PACKAGE_FILE"
|
||||
echo "📊 打包文件大小:"
|
||||
ls -lh "$PACKAGE_FILE"
|
||||
@@ -1 +0,0 @@
|
||||
959
|
||||
@@ -1,154 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# 获取脚本所在目录(.zscripts)
|
||||
# 使用 $0 获取脚本路径(与 build.sh 保持一致)
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
log_step_start() {
|
||||
local step_name="$1"
|
||||
echo "=========================================="
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting: $step_name"
|
||||
echo "=========================================="
|
||||
export STEP_START_TIME
|
||||
STEP_START_TIME=$(date +%s)
|
||||
}
|
||||
|
||||
log_step_end() {
|
||||
local step_name="${1:-Unknown step}"
|
||||
local end_time
|
||||
end_time=$(date +%s)
|
||||
local duration=$((end_time - STEP_START_TIME))
|
||||
echo "=========================================="
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Completed: $step_name"
|
||||
echo "[LOG] Step: $step_name | Duration: ${duration}s"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
}
|
||||
|
||||
start_mini_services() {
|
||||
local mini_services_dir="$PROJECT_DIR/mini-services"
|
||||
local started_count=0
|
||||
|
||||
log_step_start "Starting mini-services"
|
||||
if [ ! -d "$mini_services_dir" ]; then
|
||||
echo "Mini-services directory not found, skipping..."
|
||||
log_step_end "Starting mini-services"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Found mini-services directory, scanning for sub-services..."
|
||||
|
||||
for service_dir in "$mini_services_dir"/*; do
|
||||
if [ ! -d "$service_dir" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
local service_name
|
||||
service_name=$(basename "$service_dir")
|
||||
echo "Checking service: $service_name"
|
||||
|
||||
if [ ! -f "$service_dir/package.json" ]; then
|
||||
echo "[$service_name] No package.json found, skipping..."
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! grep -q '"dev"' "$service_dir/package.json"; then
|
||||
echo "[$service_name] No dev script found, skipping..."
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Starting $service_name in background..."
|
||||
(
|
||||
cd "$service_dir"
|
||||
echo "[$service_name] Installing dependencies..."
|
||||
bun install
|
||||
echo "[$service_name] Running bun run dev..."
|
||||
exec bun run dev
|
||||
) >"$PROJECT_DIR/.zscripts/mini-service-${service_name}.log" 2>&1 &
|
||||
|
||||
local service_pid=$!
|
||||
echo "[$service_name] Started in background (PID: $service_pid)"
|
||||
echo "[$service_name] Log: $PROJECT_DIR/.zscripts/mini-service-${service_name}.log"
|
||||
disown "$service_pid" 2>/dev/null || true
|
||||
started_count=$((started_count + 1))
|
||||
done
|
||||
|
||||
echo "Mini-services startup completed. Started $started_count service(s)."
|
||||
log_step_end "Starting mini-services"
|
||||
}
|
||||
|
||||
wait_for_service() {
|
||||
local host="$1"
|
||||
local port="$2"
|
||||
local service_name="$3"
|
||||
local max_attempts="${4:-60}"
|
||||
local attempt=1
|
||||
|
||||
echo "Waiting for $service_name to be ready on $host:$port..."
|
||||
|
||||
while [ "$attempt" -le "$max_attempts" ]; do
|
||||
if curl -s --connect-timeout 2 --max-time 5 "http://$host:$port" >/dev/null 2>&1; then
|
||||
echo "$service_name is ready!"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Attempt $attempt/$max_attempts: $service_name not ready yet, waiting..."
|
||||
sleep 1
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
echo "ERROR: $service_name failed to start within $max_attempts seconds"
|
||||
return 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [ -n "${DEV_PID:-}" ] && kill -0 "$DEV_PID" >/dev/null 2>&1; then
|
||||
echo "Stopping Next.js dev server (PID: $DEV_PID)..."
|
||||
kill "$DEV_PID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
if ! command -v bun >/dev/null 2>&1; then
|
||||
echo "ERROR: bun is not installed or not in PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_step_start "bun install"
|
||||
echo "[BUN] Installing dependencies..."
|
||||
bun install
|
||||
log_step_end "bun install"
|
||||
|
||||
log_step_start "bun run db:push"
|
||||
echo "[BUN] Setting up database..."
|
||||
bun run db:push
|
||||
log_step_end "bun run db:push"
|
||||
|
||||
log_step_start "Starting Next.js dev server"
|
||||
echo "[BUN] Starting development server..."
|
||||
bun run dev &
|
||||
DEV_PID=$!
|
||||
log_step_end "Starting Next.js dev server"
|
||||
|
||||
log_step_start "Waiting for Next.js dev server"
|
||||
wait_for_service "localhost" "3000" "Next.js dev server"
|
||||
log_step_end "Waiting for Next.js dev server"
|
||||
|
||||
log_step_start "Health check"
|
||||
echo "[BUN] Performing health check..."
|
||||
curl -fsS localhost:3000 >/dev/null
|
||||
echo "[BUN] Health check passed"
|
||||
log_step_end "Health check"
|
||||
|
||||
start_mini_services
|
||||
|
||||
echo "Next.js dev server is running in background (PID: $DEV_PID)."
|
||||
echo "Use 'kill $DEV_PID' to stop it."
|
||||
disown "$DEV_PID" 2>/dev/null || true
|
||||
unset DEV_PID
|
||||
@@ -1,78 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 配置项
|
||||
ROOT_DIR="/home/z/my-project/mini-services"
|
||||
DIST_DIR="/tmp/build_fullstack_$BUILD_ID/mini-services-dist"
|
||||
|
||||
main() {
|
||||
echo "🚀 开始批量构建..."
|
||||
|
||||
# 检查 rootdir 是否存在
|
||||
if [ ! -d "$ROOT_DIR" ]; then
|
||||
echo "ℹ️ 目录 $ROOT_DIR 不存在,跳过构建"
|
||||
return
|
||||
fi
|
||||
|
||||
# 创建输出目录(如果不存在)
|
||||
mkdir -p "$DIST_DIR"
|
||||
|
||||
# 统计变量
|
||||
success_count=0
|
||||
fail_count=0
|
||||
|
||||
# 遍历 mini-services 目录下的所有文件夹
|
||||
for dir in "$ROOT_DIR"/*; do
|
||||
# 检查是否是目录且包含 package.json
|
||||
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
|
||||
project_name=$(basename "$dir")
|
||||
|
||||
# 智能查找入口文件 (按优先级查找)
|
||||
entry_path=""
|
||||
for entry in "src/index.ts" "index.ts" "src/index.js" "index.js"; do
|
||||
if [ -f "$dir/$entry" ]; then
|
||||
entry_path="$dir/$entry"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$entry_path" ]; then
|
||||
echo "⚠️ 跳过 $project_name: 未找到入口文件 (index.ts/js)"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📦 正在构建: $project_name..."
|
||||
|
||||
# 使用 bun build CLI 构建
|
||||
output_file="$DIST_DIR/mini-service-$project_name.js"
|
||||
|
||||
if bun build "$entry_path" \
|
||||
--outfile "$output_file" \
|
||||
--target bun \
|
||||
--minify; then
|
||||
echo "✅ $project_name 构建成功 -> $output_file"
|
||||
success_count=$((success_count + 1))
|
||||
else
|
||||
echo "❌ $project_name 构建失败"
|
||||
fail_count=$((fail_count + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -f ./.zscripts/mini-services-start.sh ]; then
|
||||
cp ./.zscripts/mini-services-start.sh "$DIST_DIR/mini-services-start.sh"
|
||||
chmod +x "$DIST_DIR/mini-services-start.sh"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🎉 所有任务完成!"
|
||||
if [ $success_count -gt 0 ] || [ $fail_count -gt 0 ]; then
|
||||
echo "✅ 成功: $success_count 个"
|
||||
if [ $fail_count -gt 0 ]; then
|
||||
echo "❌ 失败: $fail_count 个"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
main
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 配置项
|
||||
ROOT_DIR="/home/z/my-project/mini-services"
|
||||
|
||||
main() {
|
||||
echo "🚀 开始批量安装依赖..."
|
||||
|
||||
# 检查 rootdir 是否存在
|
||||
if [ ! -d "$ROOT_DIR" ]; then
|
||||
echo "ℹ️ 目录 $ROOT_DIR 不存在,跳过安装"
|
||||
return
|
||||
fi
|
||||
|
||||
# 统计变量
|
||||
success_count=0
|
||||
fail_count=0
|
||||
failed_projects=""
|
||||
|
||||
# 遍历 mini-services 目录下的所有文件夹
|
||||
for dir in "$ROOT_DIR"/*; do
|
||||
# 检查是否是目录且包含 package.json
|
||||
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
|
||||
project_name=$(basename "$dir")
|
||||
echo ""
|
||||
echo "📦 正在安装依赖: $project_name..."
|
||||
|
||||
# 进入项目目录并执行 bun install
|
||||
if (cd "$dir" && bun install); then
|
||||
echo "✅ $project_name 依赖安装成功"
|
||||
success_count=$((success_count + 1))
|
||||
else
|
||||
echo "❌ $project_name 依赖安装失败"
|
||||
fail_count=$((fail_count + 1))
|
||||
if [ -z "$failed_projects" ]; then
|
||||
failed_projects="$project_name"
|
||||
else
|
||||
failed_projects="$failed_projects $project_name"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# 汇总结果
|
||||
echo ""
|
||||
echo "=================================================="
|
||||
if [ $success_count -gt 0 ] || [ $fail_count -gt 0 ]; then
|
||||
echo "🎉 安装完成!"
|
||||
echo "✅ 成功: $success_count 个"
|
||||
if [ $fail_count -gt 0 ]; then
|
||||
echo "❌ 失败: $fail_count 个"
|
||||
echo ""
|
||||
echo "失败的项目:"
|
||||
for project in $failed_projects; do
|
||||
echo " - $project"
|
||||
done
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ 未找到任何包含 package.json 的项目"
|
||||
fi
|
||||
echo "=================================================="
|
||||
}
|
||||
|
||||
main
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# 配置项
|
||||
DIST_DIR="./mini-services-dist"
|
||||
|
||||
# 存储所有子进程的 PID
|
||||
pids=""
|
||||
|
||||
# 清理函数:优雅关闭所有服务
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "🛑 正在关闭所有服务..."
|
||||
|
||||
# 发送 SIGTERM 信号给所有子进程
|
||||
for pid in $pids; do
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
service_name=$(ps -p "$pid" -o comm= 2>/dev/null || echo "unknown")
|
||||
echo " 关闭进程 $pid ($service_name)..."
|
||||
kill -TERM "$pid" 2>/dev/null
|
||||
fi
|
||||
done
|
||||
|
||||
# 等待所有进程退出(最多等待 5 秒)
|
||||
sleep 1
|
||||
for pid in $pids; do
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
# 如果还在运行,等待最多 4 秒
|
||||
timeout=4
|
||||
while [ $timeout -gt 0 ] && kill -0 "$pid" 2>/dev/null; do
|
||||
sleep 1
|
||||
timeout=$((timeout - 1))
|
||||
done
|
||||
# 如果仍然在运行,强制关闭
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo " 强制关闭进程 $pid..."
|
||||
kill -KILL "$pid" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✅ 所有服务已关闭"
|
||||
}
|
||||
|
||||
main() {
|
||||
echo "🚀 开始启动所有 mini services..."
|
||||
|
||||
# 检查 dist 目录是否存在
|
||||
if [ ! -d "$DIST_DIR" ]; then
|
||||
echo "ℹ️ 目录 $DIST_DIR 不存在"
|
||||
return
|
||||
fi
|
||||
|
||||
# 查找所有 mini-service-*.js 文件
|
||||
service_files=""
|
||||
for file in "$DIST_DIR"/mini-service-*.js; do
|
||||
if [ -f "$file" ]; then
|
||||
if [ -z "$service_files" ]; then
|
||||
service_files="$file"
|
||||
else
|
||||
service_files="$service_files $file"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# 计算服务文件数量
|
||||
service_count=0
|
||||
for file in $service_files; do
|
||||
service_count=$((service_count + 1))
|
||||
done
|
||||
|
||||
if [ $service_count -eq 0 ]; then
|
||||
echo "ℹ️ 未找到任何 mini service 文件"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "📦 找到 $service_count 个服务,开始启动..."
|
||||
echo ""
|
||||
|
||||
# 启动每个服务
|
||||
for file in $service_files; do
|
||||
service_name=$(basename "$file" .js | sed 's/mini-service-//')
|
||||
echo "▶️ 启动服务: $service_name..."
|
||||
|
||||
# 使用 bun 运行服务(后台运行)
|
||||
bun "$file" &
|
||||
pid=$!
|
||||
if [ -z "$pids" ]; then
|
||||
pids="$pid"
|
||||
else
|
||||
pids="$pids $pid"
|
||||
fi
|
||||
|
||||
# 等待一小段时间检查进程是否成功启动
|
||||
sleep 0.5
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
echo "❌ $service_name 启动失败"
|
||||
# 从字符串中移除失败的 PID
|
||||
pids=$(echo "$pids" | sed "s/\b$pid\b//" | sed 's/ */ /g' | sed 's/^ *//' | sed 's/ *$//')
|
||||
else
|
||||
echo "✅ $service_name 已启动 (PID: $pid)"
|
||||
fi
|
||||
done
|
||||
|
||||
# 计算运行中的服务数量
|
||||
running_count=0
|
||||
for pid in $pids; do
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
running_count=$((running_count + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "🎉 所有服务已启动!共 $running_count 个服务正在运行"
|
||||
echo ""
|
||||
echo "💡 按 Ctrl+C 停止所有服务"
|
||||
echo ""
|
||||
|
||||
# 等待所有后台进程
|
||||
wait
|
||||
}
|
||||
|
||||
main
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# 获取脚本所在目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
BUILD_DIR="$SCRIPT_DIR"
|
||||
|
||||
# 存储所有子进程的 PID
|
||||
pids=""
|
||||
|
||||
# 清理函数:优雅关闭所有服务
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "🛑 正在关闭所有服务..."
|
||||
|
||||
# 发送 SIGTERM 信号给所有子进程
|
||||
for pid in $pids; do
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
service_name=$(ps -p "$pid" -o comm= 2>/dev/null || echo "unknown")
|
||||
echo " 关闭进程 $pid ($service_name)..."
|
||||
kill -TERM "$pid" 2>/dev/null
|
||||
fi
|
||||
done
|
||||
|
||||
# 等待所有进程退出(最多等待 5 秒)
|
||||
sleep 1
|
||||
for pid in $pids; do
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
# 如果还在运行,等待最多 4 秒
|
||||
timeout=4
|
||||
while [ $timeout -gt 0 ] && kill -0 "$pid" 2>/dev/null; do
|
||||
sleep 1
|
||||
timeout=$((timeout - 1))
|
||||
done
|
||||
# 如果仍然在运行,强制关闭
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo " 强制关闭进程 $pid..."
|
||||
kill -KILL "$pid" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✅ 所有服务已关闭"
|
||||
exit 0
|
||||
}
|
||||
|
||||
echo "🚀 开始启动所有服务..."
|
||||
echo ""
|
||||
|
||||
# 切换到构建目录
|
||||
cd "$BUILD_DIR" || exit 1
|
||||
|
||||
ls -lah
|
||||
|
||||
DEFAULT_PACKAGED_DB_PATH="/app/db/custom.db"
|
||||
DEFAULT_PACKAGED_DATABASE_URL="file:$DEFAULT_PACKAGED_DB_PATH"
|
||||
|
||||
# 启动 Next.js 服务器
|
||||
if [ -f "./next-service-dist/server.js" ]; then
|
||||
echo "🚀 启动 Next.js 服务器..."
|
||||
cd next-service-dist/ || exit 1
|
||||
|
||||
# 设置环境变量
|
||||
export NODE_ENV=production
|
||||
export PORT="${PORT:-3000}"
|
||||
export HOSTNAME="${HOSTNAME:-0.0.0.0}"
|
||||
export DATABASE_URL="${DATABASE_URL:-$DEFAULT_PACKAGED_DATABASE_URL}"
|
||||
|
||||
if [ "$DATABASE_URL" = "$DEFAULT_PACKAGED_DATABASE_URL" ]; then
|
||||
if [ ! -f "$DEFAULT_PACKAGED_DB_PATH" ]; then
|
||||
echo "❌ 未找到打包后的数据库文件 $DEFAULT_PACKAGED_DB_PATH"
|
||||
echo " 为避免生产环境启动到空数据库,启动已终止"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🗄️ 当前使用打包数据库: $DEFAULT_PACKAGED_DB_PATH"
|
||||
else
|
||||
echo "🗄️ 当前使用外部指定数据库: $DATABASE_URL"
|
||||
fi
|
||||
|
||||
# 后台启动 Next.js
|
||||
bun server.js &
|
||||
NEXT_PID=$!
|
||||
pids="$NEXT_PID"
|
||||
|
||||
# 等待一小段时间检查进程是否成功启动
|
||||
sleep 1
|
||||
if ! kill -0 "$NEXT_PID" 2>/dev/null; then
|
||||
echo "❌ Next.js 服务器启动失败"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ Next.js 服务器已启动 (PID: $NEXT_PID, Port: $PORT)"
|
||||
fi
|
||||
|
||||
cd ../
|
||||
else
|
||||
echo "⚠️ 未找到 Next.js 服务器文件: ./next-service-dist/server.js"
|
||||
fi
|
||||
|
||||
# 启动 mini-services
|
||||
if [ -f "./mini-services-start.sh" ]; then
|
||||
echo "🚀 启动 mini-services..."
|
||||
|
||||
# 运行启动脚本(从根目录运行,脚本内部会处理 mini-services-dist 目录)
|
||||
sh ./mini-services-start.sh &
|
||||
MINI_PID=$!
|
||||
pids="$pids $MINI_PID"
|
||||
|
||||
# 等待一小段时间检查进程是否成功启动
|
||||
sleep 1
|
||||
if ! kill -0 "$MINI_PID" 2>/dev/null; then
|
||||
echo "⚠️ mini-services 可能启动失败,但继续运行..."
|
||||
else
|
||||
echo "✅ mini-services 已启动 (PID: $MINI_PID)"
|
||||
fi
|
||||
elif [ -d "./mini-services-dist" ]; then
|
||||
echo "⚠️ 未找到 mini-services 启动脚本,但目录存在"
|
||||
else
|
||||
echo "ℹ️ mini-services 目录不存在,跳过"
|
||||
fi
|
||||
|
||||
# 启动 Caddy(如果存在 Caddyfile)
|
||||
echo "🚀 启动 Caddy..."
|
||||
|
||||
# Caddy 作为前台进程运行(主进程)
|
||||
echo "✅ Caddy 已启动(前台运行)"
|
||||
echo ""
|
||||
echo "🎉 所有服务已启动!"
|
||||
echo ""
|
||||
echo "💡 按 Ctrl+C 停止所有服务"
|
||||
echo ""
|
||||
|
||||
# Caddy 作为主进程运行
|
||||
exec caddy run --config Caddyfile --adapter caddyfile
|
||||
@@ -1,23 +0,0 @@
|
||||
:81 {
|
||||
@transform_port_query {
|
||||
query XTransformPort=*
|
||||
}
|
||||
|
||||
handle @transform_port_query {
|
||||
reverse_proxy localhost:{query.XTransformPort} {
|
||||
header_up Host {host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
handle {
|
||||
reverse_proxy localhost:3000 {
|
||||
header_up Host {host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
# 回响星核 / Echo Nexus
|
||||
|
||||
> 原创深空考古放置策略游戏 · Original deep-space archaeology idle strategy game
|
||||
|
||||
你是一名深空考古指挥官,驾驶自治勘探舰回收已「飞升」的以太文明遗留的记忆晶体,解码碎片拼凑出他们的故事,并最终跨越维度发起「接触」。
|
||||
|
||||
## 项目状态
|
||||
|
||||
- **版本**:v0.1(MVP 开发中)
|
||||
- **栈**:Next.js 16 · TypeScript · Tailwind · shadcn/ui · Canvas · Zustand · Prisma
|
||||
- **仓库**:https://git.atdunbg.xyz/Super_Z/echo-nexus
|
||||
|
||||
## 文档
|
||||
|
||||
- [`docs/01-调研报告.md`](docs/01-调研报告.md) — 游戏市场调研
|
||||
- [`docs/02-游戏设计文档.md`](docs/02-游戏设计文档.md) — GDD 策划设计
|
||||
- [`docs/03-技术架构.md`](docs/03-技术架构.md) — 技术架构
|
||||
|
||||
## 核心特色
|
||||
|
||||
- **解码共振谜题**:把放置游戏的「等待」变成有节奏的主动小谜题
|
||||
- **碎片涌现叙事**:故事从解码中自然浮现,不打断玩法
|
||||
- **深空全息美学**:晶体 + 粒子 + 共振波纹的差异化视觉
|
||||
- **飞升多周目**:终局「接触」后保留蓝图开启新周目
|
||||
|
||||
## 开发路线
|
||||
|
||||
- v0.1 放置 + 解码 + 技术树 + 图谱 + 飞升(当前)
|
||||
- v0.2 遗迹探险肉鸽
|
||||
- v0.3 音频 + 全局星潮 socket 事件
|
||||
- v0.4 云存档 + 排行榜
|
||||
- v0.5 全 5 纪元叙事 + 隐藏内容
|
||||
|
||||
---
|
||||
© Super_Z · 持续开发中
|
||||
@@ -0,0 +1,9 @@
|
||||
1:"$Sreact.fragment"
|
||||
2:I[47257,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"ClientPageRoot"]
|
||||
3:I[52683,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/efcc875da619c164.js"],"default"]
|
||||
6:I[97367,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"OutletBoundary"]
|
||||
7:"$Sreact.suspense"
|
||||
0:{"buildId":"oyCsV_kYxc3pie8zrYRGs","rsc":["$","$1","c",{"children":[["$","$L2",null,{"Component":"$3","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@4","$@5"]}}],[["$","script","script-0",{"src":"/Super_Z/echo-nexus/_next/static/chunks/efcc875da619c164.js","async":true}]],["$","$L6",null,{"children":["$","$7",null,{"name":"Next.MetadataOutlet","children":"$@8"}]}]]}],"loading":null,"isPartial":false}
|
||||
4:{}
|
||||
5:"$0:rsc:props:children:0:props:serverProvidedParams:params"
|
||||
8:null
|
||||
@@ -0,0 +1,22 @@
|
||||
1:"$Sreact.fragment"
|
||||
2:I[39756,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"default"]
|
||||
3:I[37457,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"default"]
|
||||
4:I[77855,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js"],"Toaster"]
|
||||
5:I[47257,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"ClientPageRoot"]
|
||||
6:I[52683,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/efcc875da619c164.js"],"default"]
|
||||
9:I[97367,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"OutletBoundary"]
|
||||
a:"$Sreact.suspense"
|
||||
c:I[97367,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"ViewportBoundary"]
|
||||
e:I[97367,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"MetadataBoundary"]
|
||||
10:I[68027,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"default"]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/chunks/34d933785a17edf3.css","style"]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/chunks/f4a1ac847b63088d.css","style"]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/media/797e433ab948586e-s.p.29207c2f.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/media/caa3a2e1cccd8315-s.p.3b6cae6d.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
||||
0:{"P":null,"b":"oyCsV_kYxc3pie8zrYRGs","c":["",""],"q":"","i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/Super_Z/echo-nexus/_next/static/chunks/34d933785a17edf3.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/Super_Z/echo-nexus/_next/static/chunks/f4a1ac847b63088d.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"zh-CN","suppressHydrationWarning":true,"className":"dark","children":["$","body",null,{"className":"geist_a71539c9-module__T19VSG__variable geist_mono_8d43a2aa-module__8Li5zG__variable antialiased bg-background text-foreground","children":[["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}],["$","$L4",null,{}]]}]}]]}],{"children":[["$","$1","c",{"children":[["$","$L5",null,{"Component":"$6","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@7","$@8"]}}],[["$","script","script-0",{"src":"/Super_Z/echo-nexus/_next/static/chunks/efcc875da619c164.js","async":true,"nonce":"$undefined"}]],["$","$L9",null,{"children":["$","$a",null,{"name":"Next.MetadataOutlet","children":"$@b"}]}]]}],{},null,false,false]},null,false,false],["$","$1","h",{"children":[null,["$","$Lc",null,{"children":"$Ld"}],["$","div",null,{"hidden":true,"children":["$","$Le",null,{"children":["$","$a",null,{"name":"Next.Metadata","children":"$Lf"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$10",[]],"S":true}
|
||||
7:{}
|
||||
8:"$0:f:0:1:1:children:0:props:children:0:props:serverProvidedParams:params"
|
||||
d:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
|
||||
11:I[27201,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"IconMark"]
|
||||
b:null
|
||||
f:[["$","title","0",{"children":"回响星核 · Echo Nexus"}],["$","meta","1",{"name":"description","content":"原创深空考古放置策略游戏 — 解码记忆晶体,拼凑以太文明的回响,跨越维度发起接触。"}],["$","meta","2",{"name":"author","content":"Super_Z"}],["$","meta","3",{"name":"keywords","content":"Echo Nexus,回响星核,idle game,browser game,放置游戏,解谜,深空考古"}],["$","meta","4",{"property":"og:title","content":"回响星核 · Echo Nexus"}],["$","meta","5",{"property":"og:description","content":"原创深空考古放置策略游戏"}],["$","meta","6",{"property":"og:site_name","content":"Echo Nexus"}],["$","meta","7",{"property":"og:type","content":"website"}],["$","meta","8",{"name":"twitter:card","content":"summary_large_image"}],["$","meta","9",{"name":"twitter:title","content":"回响星核 · Echo Nexus"}],["$","meta","10",{"name":"twitter:description","content":"原创深空考古放置策略游戏"}],["$","link","11",{"rel":"icon","href":"https://z-cdn.chatglm.cn/z-ai/static/logo.svg"}],["$","$L11","12",{}]]
|
||||
@@ -0,0 +1,6 @@
|
||||
1:"$Sreact.fragment"
|
||||
2:I[97367,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"ViewportBoundary"]
|
||||
3:I[97367,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"MetadataBoundary"]
|
||||
4:"$Sreact.suspense"
|
||||
5:I[27201,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"IconMark"]
|
||||
0:{"buildId":"oyCsV_kYxc3pie8zrYRGs","rsc":["$","$1","h",{"children":[null,["$","$L2",null,{"children":[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]}],["$","div",null,{"hidden":true,"children":["$","$L3",null,{"children":["$","$4",null,{"name":"Next.Metadata","children":[["$","title","0",{"children":"回响星核 · Echo Nexus"}],["$","meta","1",{"name":"description","content":"原创深空考古放置策略游戏 — 解码记忆晶体,拼凑以太文明的回响,跨越维度发起接触。"}],["$","meta","2",{"name":"author","content":"Super_Z"}],["$","meta","3",{"name":"keywords","content":"Echo Nexus,回响星核,idle game,browser game,放置游戏,解谜,深空考古"}],["$","meta","4",{"property":"og:title","content":"回响星核 · Echo Nexus"}],["$","meta","5",{"property":"og:description","content":"原创深空考古放置策略游戏"}],["$","meta","6",{"property":"og:site_name","content":"Echo Nexus"}],["$","meta","7",{"property":"og:type","content":"website"}],["$","meta","8",{"name":"twitter:card","content":"summary_large_image"}],["$","meta","9",{"name":"twitter:title","content":"回响星核 · Echo Nexus"}],["$","meta","10",{"name":"twitter:description","content":"原创深空考古放置策略游戏"}],["$","link","11",{"rel":"icon","href":"https://z-cdn.chatglm.cn/z-ai/static/logo.svg"}],["$","$L5","12",{}]]}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],"loading":null,"isPartial":false}
|
||||
@@ -0,0 +1,7 @@
|
||||
1:"$Sreact.fragment"
|
||||
2:I[39756,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"default"]
|
||||
3:I[37457,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"default"]
|
||||
4:I[77855,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js"],"Toaster"]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/chunks/34d933785a17edf3.css","style"]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/chunks/f4a1ac847b63088d.css","style"]
|
||||
0:{"buildId":"oyCsV_kYxc3pie8zrYRGs","rsc":["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/Super_Z/echo-nexus/_next/static/chunks/34d933785a17edf3.css","precedence":"next"}],["$","link","1",{"rel":"stylesheet","href":"/Super_Z/echo-nexus/_next/static/chunks/f4a1ac847b63088d.css","precedence":"next"}],["$","script","script-0",{"src":"/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","async":true}],["$","script","script-1",{"src":"/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","async":true}]],["$","html",null,{"lang":"zh-CN","suppressHydrationWarning":true,"className":"dark","children":["$","body",null,{"className":"geist_a71539c9-module__T19VSG__variable geist_mono_8d43a2aa-module__8Li5zG__variable antialiased bg-background text-foreground","children":[["$","$L2",null,{"parallelRouterKey":"children","template":["$","$L3",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}],["$","$L4",null,{}]]}]}]]}],"loading":null,"isPartial":false}
|
||||
@@ -0,0 +1,5 @@
|
||||
:HL["/Super_Z/echo-nexus/_next/static/chunks/34d933785a17edf3.css","style"]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/chunks/f4a1ac847b63088d.css","style"]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/media/797e433ab948586e-s.p.29207c2f.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/media/caa3a2e1cccd8315-s.p.3b6cae6d.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
||||
0:{"buildId":"oyCsV_kYxc3pie8zrYRGs","tree":{"name":"","paramType":null,"paramKey":"","hasRuntimePrefetch":false,"slots":{"children":{"name":"__PAGE__","paramType":null,"paramKey":"__PAGE__","hasRuntimePrefetch":false,"slots":null,"isRootLayout":false}},"isRootLayout":true},"staleTime":300}
|
||||
@@ -0,0 +1,2 @@
|
||||
@font-face{font-family:Geist;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/fef07dbb0973bf53-s.518e079e.woff2)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Geist;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/8a480f0b521d4e75-s.ea323500.woff2)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Geist;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/53b9e256198e5412-s.853d50a3.woff2)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Geist;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/7178b3e590c64307-s.55554cd0.woff2)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Geist;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/caa3a2e1cccd8315-s.p.3b6cae6d.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Geist Fallback;src:local(Arial);ascent-override:95.94%;descent-override:28.16%;line-gap-override:0.0%;size-adjust:104.76%}.geist_a71539c9-module__T19VSG__className{font-family:Geist,Geist Fallback;font-style:normal}.geist_a71539c9-module__T19VSG__variable{--font-geist-sans:"Geist","Geist Fallback"}
|
||||
@font-face{font-family:Geist Mono;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/5ce348bf30bf5439-s.56c1f21e.woff2)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Geist Mono;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/4fa387ec64143e14-s.3b336396.woff2)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Geist Mono;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/6306c77e7c8268e4-s.e3369375.woff2)format("woff2");unicode-range:U+2000-2001,U+2004-2008,U+200A,U+23B8-23BD,U+2500-259F}@font-face{font-family:Geist Mono;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/7d817b4c03b0c5f1-s.a40b9a8b.woff2)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Geist Mono;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/bbc41e54d2fcbd21-s.fe42ddf4.woff2)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Geist Mono;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/797e433ab948586e-s.p.29207c2f.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Geist Mono Fallback;src:local(Arial);ascent-override:74.67%;descent-override:21.92%;line-gap-override:0.0%;size-adjust:134.59%}.geist_mono_8d43a2aa-module__8Li5zG__className{font-family:Geist Mono,Geist Mono Fallback;font-style:normal}.geist_mono_8d43a2aa-module__8Li5zG__variable{--font-geist-mono:"Geist Mono","Geist Mono Fallback"}
|
||||
@@ -0,0 +1,11 @@
|
||||
self.__BUILD_MANIFEST = {
|
||||
"__rewrites": {
|
||||
"afterFiles": [],
|
||||
"beforeFiles": [],
|
||||
"fallback": []
|
||||
},
|
||||
"sortedPages": [
|
||||
"/_app",
|
||||
"/_error"
|
||||
]
|
||||
};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
self.__SSG_MANIFEST=new Set([]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()
|
||||
@@ -0,0 +1,16 @@
|
||||
1:"$Sreact.fragment"
|
||||
2:I[39756,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"default"]
|
||||
3:I[37457,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"default"]
|
||||
4:I[77855,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"Toaster"]
|
||||
5:I[97367,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"OutletBoundary"]
|
||||
6:"$Sreact.suspense"
|
||||
8:I[97367,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"ViewportBoundary"]
|
||||
a:I[97367,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"MetadataBoundary"]
|
||||
c:I[68027,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"default"]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/chunks/34d933785a17edf3.css","style"]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/chunks/f4a1ac847b63088d.css","style"]
|
||||
0:{"P":null,"b":"oyCsV_kYxc3pie8zrYRGs","c":["","_not-found",""],"q":"","i":false,"f":[[["",{"children":["/_not-found",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/Super_Z/echo-nexus/_next/static/chunks/34d933785a17edf3.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/Super_Z/echo-nexus/_next/static/chunks/f4a1ac847b63088d.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"zh-CN","suppressHydrationWarning":true,"className":"dark","children":["$","body",null,{"className":"geist_a71539c9-module__T19VSG__variable geist_mono_8d43a2aa-module__8Li5zG__variable antialiased bg-background text-foreground","children":[["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}],["$","$L4",null,{}]]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:0:props:notFound:0:1:props:style","children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":"$0:f:0:1:0:props:children:1:props:children:props:children:0:props:notFound:0:1:props:children:props:children:1:props:style","children":404}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:0:props:notFound:0:1:props:children:props:children:2:props:style","children":["$","h2",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:0:props:notFound:0:1:props:children:props:children:2:props:children:props:style","children":"This page could not be found."}]}]]}]}]],null,["$","$L5",null,{"children":["$","$6",null,{"name":"Next.MetadataOutlet","children":"$@7"}]}]]}],{},null,false,false]},null,false,false]},null,false,false],["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$L8",null,{"children":"$L9"}],["$","div",null,{"hidden":true,"children":["$","$La",null,{"children":["$","$6",null,{"name":"Next.Metadata","children":"$Lb"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$c","$undefined"],"S":true}
|
||||
9:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
|
||||
d:I[27201,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"IconMark"]
|
||||
7:null
|
||||
b:[["$","title","0",{"children":"回响星核 · Echo Nexus"}],["$","meta","1",{"name":"description","content":"原创深空考古放置策略游戏 — 解码记忆晶体,拼凑以太文明的回响,跨越维度发起接触。"}],["$","meta","2",{"name":"author","content":"Super_Z"}],["$","meta","3",{"name":"keywords","content":"Echo Nexus,回响星核,idle game,browser game,放置游戏,解谜,深空考古"}],["$","meta","4",{"property":"og:title","content":"回响星核 · Echo Nexus"}],["$","meta","5",{"property":"og:description","content":"原创深空考古放置策略游戏"}],["$","meta","6",{"property":"og:site_name","content":"Echo Nexus"}],["$","meta","7",{"property":"og:type","content":"website"}],["$","meta","8",{"name":"twitter:card","content":"summary_large_image"}],["$","meta","9",{"name":"twitter:title","content":"回响星核 · Echo Nexus"}],["$","meta","10",{"name":"twitter:description","content":"原创深空考古放置策略游戏"}],["$","link","11",{"rel":"icon","href":"https://z-cdn.chatglm.cn/z-ai/static/logo.svg"}],["$","$Ld","12",{}]]
|
||||
@@ -0,0 +1,6 @@
|
||||
1:"$Sreact.fragment"
|
||||
2:I[97367,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"ViewportBoundary"]
|
||||
3:I[97367,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"MetadataBoundary"]
|
||||
4:"$Sreact.suspense"
|
||||
5:I[27201,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"IconMark"]
|
||||
0:{"buildId":"oyCsV_kYxc3pie8zrYRGs","rsc":["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$L2",null,{"children":[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]}],["$","div",null,{"hidden":true,"children":["$","$L3",null,{"children":["$","$4",null,{"name":"Next.Metadata","children":[["$","title","0",{"children":"回响星核 · Echo Nexus"}],["$","meta","1",{"name":"description","content":"原创深空考古放置策略游戏 — 解码记忆晶体,拼凑以太文明的回响,跨越维度发起接触。"}],["$","meta","2",{"name":"author","content":"Super_Z"}],["$","meta","3",{"name":"keywords","content":"Echo Nexus,回响星核,idle game,browser game,放置游戏,解谜,深空考古"}],["$","meta","4",{"property":"og:title","content":"回响星核 · Echo Nexus"}],["$","meta","5",{"property":"og:description","content":"原创深空考古放置策略游戏"}],["$","meta","6",{"property":"og:site_name","content":"Echo Nexus"}],["$","meta","7",{"property":"og:type","content":"website"}],["$","meta","8",{"name":"twitter:card","content":"summary_large_image"}],["$","meta","9",{"name":"twitter:title","content":"回响星核 · Echo Nexus"}],["$","meta","10",{"name":"twitter:description","content":"原创深空考古放置策略游戏"}],["$","link","11",{"rel":"icon","href":"https://z-cdn.chatglm.cn/z-ai/static/logo.svg"}],["$","$L5","12",{}]]}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],"loading":null,"isPartial":false}
|
||||
@@ -0,0 +1,7 @@
|
||||
1:"$Sreact.fragment"
|
||||
2:I[39756,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"default"]
|
||||
3:I[37457,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"default"]
|
||||
4:I[77855,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"Toaster"]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/chunks/34d933785a17edf3.css","style"]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/chunks/f4a1ac847b63088d.css","style"]
|
||||
0:{"buildId":"oyCsV_kYxc3pie8zrYRGs","rsc":["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/Super_Z/echo-nexus/_next/static/chunks/34d933785a17edf3.css","precedence":"next"}],["$","link","1",{"rel":"stylesheet","href":"/Super_Z/echo-nexus/_next/static/chunks/f4a1ac847b63088d.css","precedence":"next"}],["$","script","script-0",{"src":"/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","async":true}],["$","script","script-1",{"src":"/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","async":true}],["$","script","script-2",{"src":"/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js","async":true}]],["$","html",null,{"lang":"zh-CN","suppressHydrationWarning":true,"className":"dark","children":["$","body",null,{"className":"geist_a71539c9-module__T19VSG__variable geist_mono_8d43a2aa-module__8Li5zG__variable antialiased bg-background text-foreground","children":[["$","$L2",null,{"parallelRouterKey":"children","template":["$","$L3",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}],["$","$L4",null,{}]]}]}]]}],"loading":null,"isPartial":false}
|
||||
@@ -0,0 +1,5 @@
|
||||
1:"$Sreact.fragment"
|
||||
2:I[97367,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"OutletBoundary"]
|
||||
3:"$Sreact.suspense"
|
||||
0:{"buildId":"oyCsV_kYxc3pie8zrYRGs","rsc":["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],null,["$","$L2",null,{"children":["$","$3",null,{"name":"Next.MetadataOutlet","children":"$@4"}]}]]}],"loading":null,"isPartial":false}
|
||||
4:null
|
||||
@@ -0,0 +1,4 @@
|
||||
1:"$Sreact.fragment"
|
||||
2:I[39756,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"default"]
|
||||
3:I[37457,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"default"]
|
||||
0:{"buildId":"oyCsV_kYxc3pie8zrYRGs","rsc":["$","$1","c",{"children":[null,["$","$L2",null,{"parallelRouterKey":"children","template":["$","$L3",null,{}]}]]}],"loading":null,"isPartial":false}
|
||||
@@ -0,0 +1,3 @@
|
||||
:HL["/Super_Z/echo-nexus/_next/static/chunks/34d933785a17edf3.css","style"]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/chunks/f4a1ac847b63088d.css","style"]
|
||||
0:{"buildId":"oyCsV_kYxc3pie8zrYRGs","tree":{"name":"","paramType":null,"paramKey":"","hasRuntimePrefetch":false,"slots":{"children":{"name":"/_not-found","paramType":null,"paramKey":"/_not-found","hasRuntimePrefetch":false,"slots":{"children":{"name":"__PAGE__","paramType":null,"paramKey":"__PAGE__","hasRuntimePrefetch":false,"slots":null,"isRootLayout":false}},"isRootLayout":false}},"isRootLayout":true},"staleTime":300}
|
||||
@@ -0,0 +1,16 @@
|
||||
1:"$Sreact.fragment"
|
||||
2:I[39756,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"default"]
|
||||
3:I[37457,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"default"]
|
||||
4:I[77855,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"Toaster"]
|
||||
5:I[97367,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"OutletBoundary"]
|
||||
6:"$Sreact.suspense"
|
||||
8:I[97367,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"ViewportBoundary"]
|
||||
a:I[97367,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"MetadataBoundary"]
|
||||
c:I[68027,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"default"]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/chunks/34d933785a17edf3.css","style"]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/chunks/f4a1ac847b63088d.css","style"]
|
||||
0:{"P":null,"b":"oyCsV_kYxc3pie8zrYRGs","c":["","_not-found",""],"q":"","i":false,"f":[[["",{"children":["/_not-found",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/Super_Z/echo-nexus/_next/static/chunks/34d933785a17edf3.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/Super_Z/echo-nexus/_next/static/chunks/f4a1ac847b63088d.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"zh-CN","suppressHydrationWarning":true,"className":"dark","children":["$","body",null,{"className":"geist_a71539c9-module__T19VSG__variable geist_mono_8d43a2aa-module__8Li5zG__variable antialiased bg-background text-foreground","children":[["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}],["$","$L4",null,{}]]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:0:props:notFound:0:1:props:style","children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":"$0:f:0:1:0:props:children:1:props:children:props:children:0:props:notFound:0:1:props:children:props:children:1:props:style","children":404}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:0:props:notFound:0:1:props:children:props:children:2:props:style","children":["$","h2",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:0:props:notFound:0:1:props:children:props:children:2:props:children:props:style","children":"This page could not be found."}]}]]}]}]],null,["$","$L5",null,{"children":["$","$6",null,{"name":"Next.MetadataOutlet","children":"$@7"}]}]]}],{},null,false,false]},null,false,false]},null,false,false],["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$L8",null,{"children":"$L9"}],["$","div",null,{"hidden":true,"children":["$","$La",null,{"children":["$","$6",null,{"name":"Next.Metadata","children":"$Lb"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$c","$undefined"],"S":true}
|
||||
9:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
|
||||
d:I[27201,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"IconMark"]
|
||||
7:null
|
||||
b:[["$","title","0",{"children":"回响星核 · Echo Nexus"}],["$","meta","1",{"name":"description","content":"原创深空考古放置策略游戏 — 解码记忆晶体,拼凑以太文明的回响,跨越维度发起接触。"}],["$","meta","2",{"name":"author","content":"Super_Z"}],["$","meta","3",{"name":"keywords","content":"Echo Nexus,回响星核,idle game,browser game,放置游戏,解谜,深空考古"}],["$","meta","4",{"property":"og:title","content":"回响星核 · Echo Nexus"}],["$","meta","5",{"property":"og:description","content":"原创深空考古放置策略游戏"}],["$","meta","6",{"property":"og:site_name","content":"Echo Nexus"}],["$","meta","7",{"property":"og:type","content":"website"}],["$","meta","8",{"name":"twitter:card","content":"summary_large_image"}],["$","meta","9",{"name":"twitter:title","content":"回响星核 · Echo Nexus"}],["$","meta","10",{"name":"twitter:description","content":"原创深空考古放置策略游戏"}],["$","link","11",{"rel":"icon","href":"https://z-cdn.chatglm.cn/z-ai/static/logo.svg"}],["$","$Ld","12",{}]]
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
# 游戏市场调研报告
|
||||
|
||||
> 调研日期:2025 年(持续更新)
|
||||
> 调研者:Super_Z
|
||||
> 目的:为原创游戏《回响星核 / Echo Nexus》的设计提供数据支撑,避免落入「1:1 复刻」陷阱。
|
||||
|
||||
---
|
||||
|
||||
## 一、市场大盘(数据时效性已核实,多源交叉验证)
|
||||
|
||||
### 1.1 浏览器 / HTML5 游戏市场
|
||||
- **市场规模**:2025 年浏览器游戏市场约 **78 亿 ~ 118 亿美元**(不同机构口径略有差异),预计 2033-2034 年达到 **92 亿 ~ 163 亿美元**,CAGR 约 3.5%。
|
||||
- 来源:Research and Markets(78.1 亿→80.1 亿)、Dataintelo(118 亿→163 亿)、Business Research Insights。
|
||||
- **HTML5 游戏细分**:2024 年约 53.2 亿美元,2033 年预计 92.2 亿美元,CAGR 约 7.7%(LinkedIn / Metastat)。
|
||||
- **供给爆发**:2025 年有 **15,000+** 款 HTML5 新作上线,同比增长 **2.7 倍**(Business Research Insights)。
|
||||
- **技术趋势**:WebAssembly 让浏览器游戏性能逼近原生;WebGL/WebGPU 渲染能力持续提升。
|
||||
|
||||
**结论**:浏览器游戏是增长赛道,**可及性高、潜在受众巨大(尤其国际市场)**,但供给端竞争激烈,**必须靠差异化创新突围**。
|
||||
|
||||
### 1.2 独立游戏品类格局(Steam 视角)
|
||||
- **2025 年叙事游戏**成为 Steam 达到 1000 评价数量最多的品类(#1),超越传统品类(HowToMarketAGame 年度报告)。
|
||||
- **新一代独立原生品类**:开放世界生存、卡牌构筑(deckbuilder)、Roguelike 持续走强(SuperJump)。
|
||||
- **红海/饱和品类**(Reddit 独开者共识,建议避开正面对撞):Roguelike Deckbuilder、视觉小说、本地合作、4X。
|
||||
- **2025 独立代表作**:Hades II、Deltarune、Ways of Alchemy、Cluckmech Oasis 等。
|
||||
|
||||
**结论**:纯复刻热门品类(如又一款肉鸽卡牌)会被淹没;**叙事 + 创新机制组合**是突破口。
|
||||
|
||||
### 1.3 放置 / 增量(Idle / Incremental)品类
|
||||
- **极强留存**:Idle 类天然契合浏览器场景,代表作 Cookie Clicker、Universal Paperclips、Melvor Idle 长盛不衰。
|
||||
- **2025 热度持续**:GameSpot、Bits N' Pixels 年度榜单均大量收录 idle/clicker 游戏。
|
||||
- **典型玩法**:资源自动累积 + 主动操作 + 解锁/重置(prestige)循环。
|
||||
- **优势**:开发成本可控、留存曲线平缓、适合持续迭代。
|
||||
|
||||
### 1.4 玩家留存关键洞察
|
||||
- **核心循环(core loop)**是留存的根基;D1/D7/D30 是关键基准。
|
||||
- **渐进式功能发现**:让玩家逐步解锁新机制,避免一次性信息过载。
|
||||
- **社交连接 + 流畅运营**:长期留存的两大支柱。
|
||||
- **动机分层**:成就型、探索型、社交型玩家需要差异化设计。
|
||||
|
||||
---
|
||||
|
||||
## 二、竞品分析与差异化机会
|
||||
|
||||
| 维度 | 主流放置游戏 | 主流叙事游戏 | 主流肉鸽 | 《回响星核》定位 |
|
||||
|---|---|---|---|---|
|
||||
| 核心 | 数值增长 | 剧情推进 | 战斗随机 | **解码谜题 + 放置 + 探险** |
|
||||
| 叙事 | 弱/无 | 强但线性 | 弱 | **碎片化叙事,由玩法涌现** |
|
||||
| 美术 | 简笔画/像素 | 插画 | 像素 | **全息晶体 + 深空粒子美学** |
|
||||
| 留存钩子 | prestige | 章节 | 随机性 | **「接触」终局 + 多周目** |
|
||||
| 单局 | 长 | 中 | 短 | **可长可短(放置 + 主动)** |
|
||||
|
||||
**差异化结论**:
|
||||
1. 没有任何一款主流游戏同时是「深空考古 + 记忆解码谜题 + 放置 + 肉鸽探险 + 碎片叙事」。
|
||||
2. 叙事由解码行为**涌现**而非过场灌输,符合 2025 叙事品类崛起趋势。
|
||||
3. 放置核心保证留存,解码谜题提供主动乐趣,探险提供随机性重玩价值。
|
||||
|
||||
---
|
||||
|
||||
## 三、技术选型调研结论
|
||||
|
||||
- **框架**:Next.js 16(App Router)+ TypeScript —— 本项目既有栈,SSR/路由/API 一体化,适合 web 游戏。
|
||||
- **渲染**:2D 场景用 **HTML5 Canvas**(星空/粒子/晶体动画),UI 用 **React + shadcn/ui**。3D 暂不需要,控制复杂度。
|
||||
- **状态**:Zustand(客户端)+ localStorage 持久化;后续可接 API 同步存档。
|
||||
- **后端**:Next.js API Routes + Prisma(SQLite),承载存档同步、全局统计、(后续)排行榜。
|
||||
- **实时**:socket.io 小服务,用于全局「星潮」事件(后续阶段)。
|
||||
|
||||
**结论**:在既有 Next.js 栈内即可实现,无需引入重框架,性能与可维护性平衡良好。
|
||||
|
||||
---
|
||||
|
||||
## 四、设计原则(源自调研)
|
||||
|
||||
1. **原创优先**:不做任何现有游戏的 1:1 复刻,机制组合与世界观均自创。
|
||||
2. **核心循环扎实**:放置产出 → 解码消耗 → 解锁成长 → 探险扩张 → 终局「接触」。
|
||||
3. **叙事涌现**:故事从解码碎片中拼出,不打断玩法。
|
||||
4. **渐进披露**:功能随进度解锁,符合留存最佳实践。
|
||||
5. **视觉差异化**:全息晶体 + 深空粒子,第一眼即与同类区分。
|
||||
6. **可持续迭代**:模块化架构,便于后续通过工单需求持续扩展。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据来源清单(真实性/时效性核验)
|
||||
|
||||
1. Business Research Insights — Browser Games Market Report
|
||||
2. Research and Markets — Browser Games Market 2026
|
||||
3. Dataintelo — Browser Game Market Research 2034
|
||||
4. Metastat Insights — HTML5 Games Market 2030
|
||||
5. LinkedIn (行业分析) — Casual HTML5 Games 2024-2033
|
||||
6. HowToMarketAGame — 2025 Steam 品类年度报告
|
||||
7. SuperJump — State of Indie Games 2025
|
||||
8. GameSpot / Bits N' Pixels — 2025 Idle 游戏榜单
|
||||
9. GameDesignSkills / GameDeveloper — 留存策略 17 条
|
||||
10. Reddit r/incremental_games、r/gamedev 社区共识
|
||||
|
||||
所有数据均为 2024-2026 年发布,时效性满足设计决策需要。
|
||||
@@ -1,141 +0,0 @@
|
||||
# 《回响星核 / Echo Nexus》游戏设计文档 (GDD)
|
||||
|
||||
> 版本:v0.1(首发概念版)
|
||||
> 作者:Super_Z
|
||||
> 状态:设计已定稿,进入实现阶段
|
||||
|
||||
---
|
||||
|
||||
## 一、一句话概念
|
||||
|
||||
**你是一名深空考古指挥官,驾驶自治勘探舰回收已「飞升」的以太文明遗留的记忆晶体,解码碎片拼凑出他们的故事,并最终跨越维度发起「接触」。**
|
||||
|
||||
## 二、游戏定位
|
||||
|
||||
- **品类**:深空考古放置策略 + 记忆解码谜题 + 肉鸽探险 + 涌现叙事
|
||||
- **平台**:现代浏览器(桌面优先,移动适配)
|
||||
- **单局时长**:可长可短(放置 30 秒也有产出;主动游玩 10-30 分钟一轮探险)
|
||||
- **目标留存**:D1 > 40%,D7 > 15%(idle 品类基准之上)
|
||||
- **差异化**:业界首个「解码谜题驱动叙事涌现」的放置游戏
|
||||
|
||||
## 三、世界观
|
||||
|
||||
数万年前,银河系存在一个高度发达的文明——**以太族(Aetherians)**。他们没有灭亡,而是集体「飞升」到了更高维度。临走前,他们将文明的知识与记忆封存在遍布星系的**记忆晶体(Memory Crystals)**中,作为留给后来者的「面包屑」——只待某个文明能解读足够多的晶体,便有资格发起「接触」。
|
||||
|
||||
玩家扮演人类深空考古局的指挥官,舰载 AI「回响(Echo)」辅助你勘探遗迹、回收晶体、解码记忆。
|
||||
|
||||
## 四、核心循环
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
▼ │
|
||||
[自治无人机采矿] ──产出──> [记忆晶体库存] │
|
||||
│ │
|
||||
[解码谜题] <── 主动操作 │
|
||||
│ │
|
||||
┌──────────────┼──────────────┐ │
|
||||
▼ ▼ ▼ │
|
||||
[记忆碎片] [技术洞见] [剧情片段] │
|
||||
(叙事资源) (解锁升级) (拼凑故事) │
|
||||
│ │ │ │
|
||||
└──────────────┴──────────────┘ │
|
||||
│ │
|
||||
[发起遗迹探险] ─肉鸽─> 新晶体类型
|
||||
│ │
|
||||
[「接触」进度条 ↑] │
|
||||
│ │
|
||||
────── 达成 100% ───── 终局 / 新周目
|
||||
```
|
||||
|
||||
**核心循环一句话**:采矿→解码→成长→探险→填满接触条→新周目。
|
||||
|
||||
## 五、核心系统
|
||||
|
||||
### 5.1 记忆晶体采集(Idle 核心)
|
||||
- 自治无人机以 **晶体/秒** 速率自动产出记忆晶体。
|
||||
- 玩家可主动「脉冲扫描」(点击/按键)获得瞬时增量 + 连击加成。
|
||||
- 离线时按 50% 效率持续产出(最多累积 8 小时)。
|
||||
|
||||
### 5.2 解码系统(主动玩法核心 ⭐)
|
||||
每颗晶体需经「解码」才能释放其中内容。解码是一个**原创的图案共振谜题**:
|
||||
- 屏幕呈现一个由发光节点构成的**共振阵列**(类似星图)。
|
||||
- 玩家需在限定步数内,按特定**共振顺序**点亮节点,使能量流闭合形成回路。
|
||||
- 成功 → 释放记忆碎片 + 技术洞见 + 剧情片段。
|
||||
- 不同晶体等级 / 类型 → 阵列规模与共振规则不同(渐进难度)。
|
||||
- **设计意图**:把「解码」从被动等待变成有节奏的主动小谜题,避免放置游戏「只挂机不动手」的枯燥。
|
||||
|
||||
### 5.3 技术树(成长系统)
|
||||
四个分支,由「技术洞见」解锁:
|
||||
- **采矿**:提升晶体/秒、仓库上限、离线效率。
|
||||
- **解码**:降低解码步数消耗、解锁阵列提示、自动解码低级晶体。
|
||||
- **探险**:提升探险成功率、降低风险、增加稀有晶体掉落。
|
||||
- **叙事**:解锁更深记忆层、揭示隐藏剧情、提升「接触」进度转化率。
|
||||
|
||||
### 5.4 遗迹探险(Roguelike 层)
|
||||
- 消耗「能量」向随机生成的遗迹节点图派出探险队。
|
||||
- 每个节点有事件(战斗/解谜/宝藏/抉择),路径分支。
|
||||
- 失败损失探险队,成功带回稀有晶体 + 大量洞见 + 独有剧情。
|
||||
- 单次探险 10-30 分钟,提供短期目标与随机性重玩价值。
|
||||
|
||||
### 5.5 碎片叙事(叙事层)
|
||||
- 解码与探险产出的「记忆碎片」自动归档到**记忆图谱(Memory Codex)**。
|
||||
- 图谱以星图形式呈现,碎片拼合后揭示以太族编年史(5 个纪元)。
|
||||
- 关键碎片触发**「回响回放」**——一段沉浸式全息短叙(文字 + 视觉),但**绝不打断玩法**(可随时关闭)。
|
||||
|
||||
### 5.6 「接触」终局与多周目
|
||||
- 全部行为累积「接触进度」。
|
||||
- 达 100% → 触发终局「接触」事件(戏剧性叙事高潮)。
|
||||
- 之后可选择**飞升(Prestige)**进入新周目:保留叙事图谱与技术蓝图的一部分,重置数值,开启更高难度与隐藏内容。
|
||||
|
||||
## 六、美术与音频方向
|
||||
|
||||
- **视觉**:深空黑紫底色 + 全息青/品虹光晶体 + 流动星尘粒子。整体「冷峻神秘 + 科技仪式感」。
|
||||
- **字体**:标题用未来感无衬线,正文清晰可读。
|
||||
- **动效**:晶体生长、解码共振波纹、粒子涌动、飞升白光。
|
||||
- **音频**:环境氛围 pad + 解码成功清脆音 + 关键叙事轻旋律(后续阶段)。
|
||||
|
||||
## 七、UI 信息架构
|
||||
|
||||
```
|
||||
顶栏:晶体库存 | 接触进度 | 能量 | 设置
|
||||
左侧:主视图(Canvas 星空 + 晶体集群 + 解码阵列)
|
||||
右侧:标签面板
|
||||
- 采矿 (Mining)
|
||||
- 解码 (Decode) ⭐默认
|
||||
- 技术 (Tech)
|
||||
- 探险 (Expedition)
|
||||
- 记忆图谱 (Codex)
|
||||
底栏:当前目标提示 | 在线/离线收益 | 版本号
|
||||
```
|
||||
|
||||
## 八、首版(v0.1)MVP 范围
|
||||
|
||||
为快速验证核心乐趣,首版聚焦:
|
||||
- [x] 放置采矿(晶体/秒 + 主动脉冲)
|
||||
- [x] 解码共振谜题(3 种阵列规模)
|
||||
- [x] 技术树 4 分支 × 3 级 = 12 个升级
|
||||
- [x] 记忆图谱(首纪元 8 个碎片)
|
||||
- [x] 接触进度 + 飞升(Prestige)
|
||||
- [x] localStorage 存档 + 离线收益
|
||||
- [x] 精美 Canvas 星空背景 + 晶体动画
|
||||
- [x] 响应式布局(桌面/移动)
|
||||
|
||||
**v0.1 暂不做**:遗迹探险肉鸽、socket 全局事件、音频、多语言切换、账号云存档(留待后续版本/工单驱动)。
|
||||
|
||||
## 九、后续路线图
|
||||
|
||||
- **v0.2**:遗迹探险肉鸽系统
|
||||
- **v0.3**:音频系统 + 全局「星潮」socket 事件
|
||||
- **v0.4**:账号云存档 + 排行榜
|
||||
- **v0.5**:多周目隐藏内容 + 全 5 纪元叙事
|
||||
- **持续**:根据工单需求迭代
|
||||
|
||||
## 十、设计风险与对策
|
||||
|
||||
| 风险 | 对策 |
|
||||
|---|---|
|
||||
| 解码谜题单调 | 多种阵列规则 + 自动解码低级晶体 |
|
||||
| 放置枯燥 | 主动脉冲 + 连击 + 探险提供短期目标 |
|
||||
| 叙事打断玩法 | 所有叙事可跳过,图谱自愿浏览 |
|
||||
| 数值膨胀 | 飞升重置 + 指数曲线校准 |
|
||||
| 移动端操作 | 解码阵列适配触控,UI 自适应 |
|
||||
@@ -1,109 +0,0 @@
|
||||
# 技术架构设计
|
||||
|
||||
## 一、技术栈
|
||||
|
||||
| 层 | 技术 | 说明 |
|
||||
|---|---|---|
|
||||
| 框架 | Next.js 16 (App Router) | 既有栈,SSR + API 一体 |
|
||||
| 语言 | TypeScript 5 | 全量类型 |
|
||||
| 样式 | Tailwind CSS 4 + shadcn/ui | 既有 UI 组件库 |
|
||||
| 渲染 | HTML5 Canvas 2D | 星空/粒子/晶体/解码阵列 |
|
||||
| 客户端状态 | Zustand + persist | 游戏状态 + localStorage 持久化 |
|
||||
| 服务端状态 | TanStack Query(按需) | 存档同步、统计 |
|
||||
| 后端 | Next.js API Routes | 存档同步、全局统计 |
|
||||
| 数据库 | Prisma + SQLite | 存档、统计(v0.4+) |
|
||||
| 实时 | socket.io mini-service | 全局「星潮」事件(v0.3+) |
|
||||
|
||||
## 二、目录结构
|
||||
|
||||
```
|
||||
src/
|
||||
app/
|
||||
page.tsx # 游戏主入口(唯一用户可见路由)
|
||||
layout.tsx
|
||||
api/
|
||||
save/route.ts # 存档同步(v0.4+)
|
||||
stats/route.ts # 全局统计
|
||||
components/
|
||||
game/ # 游戏专属组件
|
||||
StarfieldCanvas.tsx # 星空背景
|
||||
CrystalCluster.tsx # 晶体集群
|
||||
DecodeArray.tsx # 解码共振阵列 ⭐
|
||||
ResourceBar.tsx # 顶栏资源
|
||||
MiningPanel.tsx
|
||||
TechTree.tsx
|
||||
Codex.tsx # 记忆图谱
|
||||
PrestigeDialog.tsx
|
||||
ui/ # shadcn/ui 既有
|
||||
lib/
|
||||
game/ # 游戏逻辑(纯函数,可测)
|
||||
config.ts # 数值配置
|
||||
types.ts # 类型
|
||||
engine.ts # 核心 tick 引擎
|
||||
decode.ts # 解码谜题逻辑
|
||||
tech.ts # 技术树
|
||||
codex.ts # 叙事数据
|
||||
prestige.ts # 飞升
|
||||
db.ts # Prisma
|
||||
store/
|
||||
gameStore.ts # Zustand 游戏状态
|
||||
hooks/
|
||||
useGameLoop.ts # 主循环
|
||||
use-toast.ts # 既有
|
||||
```
|
||||
|
||||
## 三、状态模型(Zustand)
|
||||
|
||||
```ts
|
||||
interface GameState {
|
||||
// 资源
|
||||
crystals: number; // 记忆晶体(主货币)
|
||||
insights: number; // 技术洞见
|
||||
fragments: Record<FragmentId, boolean>; // 已获碎片
|
||||
contact: number; // 接触进度 0-100
|
||||
|
||||
// 产能
|
||||
crystalsPerSec: number; // 由技术树计算
|
||||
pulsePower: number; // 主动脉冲产出
|
||||
|
||||
// 技术
|
||||
tech: Record<TechId, number>; // 各技术等级
|
||||
|
||||
// 飞升
|
||||
ascensions: number;
|
||||
blueprints: string[]; // 保留的蓝图
|
||||
|
||||
// 解码
|
||||
currentCrystal: Crystal | null;
|
||||
|
||||
// 元
|
||||
lastTick: number;
|
||||
prestige: PrestigeBonus;
|
||||
}
|
||||
```
|
||||
|
||||
## 四、核心循环引擎
|
||||
|
||||
- 客户端 `setInterval` 每 100ms tick 一次。
|
||||
- 每次 tick:`crystals += crystalsPerSec * dt`。
|
||||
- 离线收益:加载时计算 `now - lastTick`,按 50% 效率补发(上限 8h)。
|
||||
- 状态自动持久化到 localStorage(Zustand persist,节流 1s)。
|
||||
|
||||
## 五、解码谜题实现
|
||||
|
||||
- 阵列 = 节点图(邻接表),每节点有 `resonance` 值。
|
||||
- 共振规则:玩家依序点击节点,相邻点击须满足 `resonance` 差值 ≤ 阈值,且最终形成闭合回路。
|
||||
- 步数限制:规模 3×3=9 节点 / 4×4=16 / 5×5=25,步数上限分别为 6/10/15。
|
||||
- 成功 → 结算奖励 + 动画;失败 → 晶体不损耗,可重试。
|
||||
|
||||
## 六、Canvas 渲染策略
|
||||
|
||||
- 单一全屏 Canvas 作为背景层(星空 + 星尘 + 远景星云)。
|
||||
- 晶体集群与解码阵列用**独立 Canvas 或 SVG**叠加,便于交互。
|
||||
- requestAnimationFrame 驱动,设备像素比适配,低性能设备降帧。
|
||||
|
||||
## 七、可扩展性
|
||||
|
||||
- 数值配置集中在 `config.ts`,便于平衡调整与工单需求响应。
|
||||
- 叙事数据集中在 `codex.ts`,新增碎片只需追加数据。
|
||||
- 技术树/解码规则/探险地图均数据驱动,支持后续模块化扩展。
|
||||
@@ -1,57 +0,0 @@
|
||||
# 解码系统修复说明 (v0.1.1)
|
||||
|
||||
## 问题
|
||||
用户反馈「解码大量的无法通过」。经分析,根因是 `generatePuzzle` **没有保证生成的局面有解**:随机填色后可能根本不存在一条满足「按目标顺序、四邻接、不重复」的路径,导致大量死局。
|
||||
|
||||
## 修复方案
|
||||
|
||||
### 1. 路径构造法生成器(核心修复)
|
||||
重写 `generatePuzzle`,采用**路径构造法**:
|
||||
1. 先随机生成目标序列 target。
|
||||
2. 在网格上**主动构造一条合法路径**:随机起点,每步走向一个未使用的四邻接节点,长度 = target.length。
|
||||
3. 把路径上第 i 个节点颜色**强制设为 target[i]**。
|
||||
4. 其余非路径节点随机填色(60% 偏向目标色,提升多解性)。
|
||||
|
||||
这样**至少存在一条解**(即构造的路径),玩家不会遇到「怎么都过不了」的死局。
|
||||
|
||||
### 2. 精确可解性检测 `isSolvable`
|
||||
新增 DFS 函数,从当前路径末端出发,精确判断是否存在一条按 target 剩余顺序、四邻接、不重复的完成路径。用于:
|
||||
- 玩家点击后检测「此路不通」,提示撤销。
|
||||
- `isStuck` 复用此判定。
|
||||
|
||||
### 3. 撤销功能 `undoStep`
|
||||
新增 store action,支持单步回退(恢复节点 used 状态)。走错时无需整个重排。
|
||||
|
||||
### 4. 卡死提示横幅
|
||||
当局面不可解时,顶部显示「谐振受阻,此路已无解」横幅 + 快捷撤销按钮。
|
||||
|
||||
### 5. 可行起点高亮(体验增强)
|
||||
选起点阶段,用 `canStartFrom` 检测每个目标色节点「选了之后是否仍可解」:
|
||||
- **可行起点**:强光圈 + 2px 边框 + 快脉冲,引导玩家。
|
||||
- **死起点**:淡化(opacity 0.5),避免玩家踩坑。
|
||||
|
||||
### 6. 旧存档兼容
|
||||
`init` 时检测存档中的活跃谜题,若不可解(旧算法生成)则自动放回晶体队列,避免老玩家卡死。
|
||||
|
||||
## 验证结果
|
||||
|
||||
### 算法验证(1000 次/档位)
|
||||
- Tier 1: 1000/1000 可解 (100.0%)
|
||||
- Tier 2: 1000/1000 可解 (100.0%)
|
||||
- Tier 3: 1000/1000 可解 (100.0%)
|
||||
- 总计: 3000/3000 (100.00%)
|
||||
|
||||
### 模拟游玩(带可解性筛选的贪心玩家)
|
||||
- 500 局: 100% 解出,平均撤销 0 次
|
||||
|
||||
### agent-browser 端到端验证
|
||||
- T1 (3×3): 通过 ✓
|
||||
- T2 (4×4): 通过 ✓
|
||||
- 死路检测: 触发「谐振受阻」横幅 ✓
|
||||
- 撤销: 单步回退正常 ✓
|
||||
- 可行起点高亮: 准确标记(4 个玫色节点中仅 1 个为可行起点,与手算一致)✓
|
||||
|
||||
## 影响文件
|
||||
- `src/lib/game/decode.ts` — 重写生成器 + 新增 isSolvable/canStartFrom
|
||||
- `src/store/gameStore.ts` — clickNode 可解性检测 + undoStep + init 兼容
|
||||
- `src/components/game/DecodeArray.tsx` — 撤销按钮 + 卡死横幅 + 可行起点高亮
|
||||
@@ -1,107 +0,0 @@
|
||||
# v0.2 遗迹探险肉鸽系统
|
||||
|
||||
> 版本:v0.2.0
|
||||
> 日期:2025-06-23
|
||||
> 作者:Super_Z
|
||||
|
||||
## 概述
|
||||
|
||||
实现路线图中的 v0.2 旗舰功能——**遗迹探险肉鸽系统**。玩家消耗能量派出探险队,进入程序化生成的遗迹节点路径,沿途遭遇战斗、宝藏、抉择、解谜与休整事件,击败终点 BOSS 获取丰厚奖励。
|
||||
|
||||
## 新增系统
|
||||
|
||||
### 1. 能量系统
|
||||
- 新增「探险能量」资源,初始 3 点,上限 5 点。
|
||||
- 每 45 秒自动恢复 1 点能量(tick 驱动,离线也恢复)。
|
||||
- 每次探险消耗 1 点能量。
|
||||
|
||||
### 2. 探险力与生命
|
||||
- **探险力**:由基础值 10 + 探险分支技术加成 + 飞升蓝图加成 + 飞升周目加成计算。
|
||||
- **探险生命**:基础 100 + 技术/飞升加成。归零则探险失败结束。
|
||||
- **战斗胜率**:`clamp(power / (power + difficulty × 8), 0.25, 0.95)`。
|
||||
|
||||
### 3. 节点路径
|
||||
- 每次探险生成 5-7 个节点的线性路径。
|
||||
- 末位固定为 BOSS 节点。
|
||||
- 首节点强制为友好类型(宝藏/休整),保证开局体验。
|
||||
- 6 种节点类型:
|
||||
|
||||
| 类型 | 图标 | 颜色 | 机制 |
|
||||
|---|---|---|---|
|
||||
| 战斗 combat | 剑 | 玫红 | 按胜率决定胜负,胜获奖、败扣血或全灭 |
|
||||
| 宝藏 treasure | 宝石 | 琥珀 | 直接获得晶体+洞见 |
|
||||
| 抉择 choice | 问号 | 青 | 55% 好结果(奖励)/45% 坏结果(扣血) |
|
||||
| 解谜 puzzle | 拼图 | 紫 | 探险力越高越可能解出,成功获大奖 |
|
||||
| 休整 rest | 心 | 翠 | 恢复生命 |
|
||||
| BOSS boss | 骷髅 | 红 | 必定战斗,高难度,胜则大量奖励+探险胜利 |
|
||||
|
||||
### 4. 奖励机制
|
||||
- **实时入账**:每个节点结算后奖励立即加入玩家资源。
|
||||
- **晶体绕过仓库上限**:探险奖励晶体不受 idle 产能仓库上限限制(仅闲置产能受限)。
|
||||
- **失败保留**:探险失败(HP 归零)仍保留已获得的全部奖励。
|
||||
- **BOSS 胜利**:额外给予大量晶体、洞见、接触进度。
|
||||
- **撤退**:玩家可随时主动撤退,保留已获奖励。
|
||||
|
||||
### 5. 探险日志
|
||||
- 记录最近 30 条探险事件,含节点类型、结果描述、奖励明细。
|
||||
- 飞升后保留日志。
|
||||
|
||||
## UI 设计
|
||||
|
||||
### 探险标签页(主页面第 4 个标签)
|
||||
- **入口态**(无活跃探险):
|
||||
- 能量条(含恢复倒计时)
|
||||
- 出发按钮(消耗 1 能量)
|
||||
- 探险说明文案
|
||||
- 最近探险记录(最近 4 条)
|
||||
- **进行态**(有活跃探险):
|
||||
- 探险力 + 当前节点位置
|
||||
- 生命条(HP/maxHP,<30% 变红)
|
||||
- 节点路径图(横向,已通过/当前/未知,含类型图标与颜色)
|
||||
- 路径进度条
|
||||
- 当前节点事件卡(类型标签、难度、胜率、标题、描述、结算结果)
|
||||
- 操作按钮(探索节点 / 前进 / 撤退)
|
||||
- 本次累计奖励
|
||||
|
||||
### 视觉细节
|
||||
- 每种节点类型有专属颜色与图标
|
||||
- 当前节点放大 1.25 倍 + 光晕
|
||||
- 已通过节点显示绿色对勾
|
||||
- 事件卡使用对应节点类型的渐变背景与边框光晕
|
||||
- 能量条渐变(琥珀→黄)
|
||||
- 生命条渐变(玫红→翠绿)
|
||||
|
||||
## 配套修复与增强
|
||||
|
||||
### 仓库满仓警告
|
||||
- 晶体达上限 98% 时,底部状态栏变琥珀色 + 脉冲动画 + 警告文案。
|
||||
- 引导玩家解码或升级仓库。
|
||||
|
||||
### 闲置产能上限优化
|
||||
- 仓库满时不再自动产出晶体(而非产出后截断)。
|
||||
- 探险奖励晶体可超出上限,不被浪费。
|
||||
|
||||
### 技术树探险分支增强
|
||||
- 远征推进器:探险力 +3
|
||||
- 遗迹图谱:探险力 +5,探险生命 +20
|
||||
- 维度信标:探险力 +8,接触进度 +50%
|
||||
|
||||
## 验证结果
|
||||
|
||||
- ✅ 探险出发/节点结算/前进/BOSS/撤退全流程通过
|
||||
- ✅ HP clamp 修复(休整不超上限)
|
||||
- ✅ 探险奖励绕过仓库上限(101 晶体 > 50 cap)
|
||||
- ✅ 探险失败保留奖励
|
||||
- ✅ 能量恢复计时
|
||||
- ✅ 探险日志记录
|
||||
- ✅ 探险标签红点提示(有能量/有进行中探险)
|
||||
|
||||
## 影响文件
|
||||
|
||||
- `src/lib/game/types.ts` — 新增探险类型定义
|
||||
- `src/lib/game/expedition.ts` — **新建**,探险逻辑(生成/结算/能量恢复)
|
||||
- `src/lib/game/config.ts` — 初始状态增加探险字段,技术树描述更新
|
||||
- `src/lib/game/engine.ts` — createInitialState/performPrestige 包含探险字段
|
||||
- `src/store/gameStore.ts` — 探险 actions + 能量恢复 tick + HP clamp + 产能上限优化
|
||||
- `src/components/game/ExpeditionPanel.tsx` — **新建**,探险 UI
|
||||
- `src/app/page.tsx` — 新增探险标签页 + 仓库满仓警告 + 能量红点
|
||||
@@ -1,161 +0,0 @@
|
||||
# 回响星核 v0.2.1 — 音频系统 + 成就系统 + 视觉打磨
|
||||
|
||||
> 版本:v0.2.1
|
||||
> 日期:2026-06-23
|
||||
> 范围:程序化音频引擎、14 项成就系统、脉冲粒子爆发、UI 打磨
|
||||
|
||||
---
|
||||
|
||||
## 一、程序化音频系统(Web Audio API,零资源文件)
|
||||
|
||||
### 设计目标
|
||||
- 无需任何外部音频文件,全部由振荡器 + 噪声 + 包络合成
|
||||
- 音色契合"深空考古 / 全息晶体"美学:谐振、水晶、低频脉冲
|
||||
- 受 `soundOn` 开关控制,支持浏览器自动播放策略(首次用户交互后初始化 AudioContext)
|
||||
|
||||
### 实现
|
||||
- **文件**:`src/lib/game/audio.ts`(AudioEngine 单例 + 15 种音效)
|
||||
- **Hook**:`src/hooks/useAudio.ts`(`useAudioSync` 同步开关,`sfx()` 便捷播放)
|
||||
|
||||
### 音效清单
|
||||
| 音效名 | 触发场景 | 合成方式 |
|
||||
|--------|---------|---------|
|
||||
| pulse | 主动脉冲 | 上升正弦短音 + 三角波泛音 |
|
||||
| pulseCombo | 连击≥3 脉冲 | 更高基频 + 多泛音 |
|
||||
| decodeClick | 解码点击节点 | 按颜色变调(C5/D5/E5/F5)+ 泛音 |
|
||||
| decodeFail | 解码点击错误 | 低频锯齿 + 噪声脉冲 |
|
||||
| decodeSuccess | 解码成功 | 大三和弦琶音上扬 |
|
||||
| fragmentUnlock | 记忆碎片浮现 | 空灵长音 + 扫频 |
|
||||
| techBuy | 购买技术 | 双音确认 |
|
||||
| expeditionStart | 探险出发 | 低频锯齿扫频 + 噪声(引擎启动感) |
|
||||
| expeditionNode | 探险节点结算 | 三角波中频 |
|
||||
| expeditionVictory | 探险胜利 | 上行凯旋和弦 |
|
||||
| expeditionDefeat | 探险失败 | 下行低音 |
|
||||
| prestige | 飞升 | 宏大扫频 + 和弦堆叠 |
|
||||
| achievement | 成就解锁 | 亮丽四音琶音 |
|
||||
| uiHover | 界面悬停 | 极轻正弦 |
|
||||
| uiClick | 界面点击 | 轻确认 |
|
||||
|
||||
### 接入点
|
||||
- `CrystalOrb`:pulse / pulseCombo
|
||||
- `DecodeArray`:decodeClick(按颜色变调)/ decodeFail / decodeSuccess / fragmentUnlock
|
||||
- `ExpeditionPanel`:expeditionStart / expeditionNode / expeditionVictory / expeditionDefeat
|
||||
- `TechTree`:techBuy
|
||||
- `PrestigeDialog`:prestige
|
||||
- `AchievementNotifier`:achievement
|
||||
- `SettingsDialog`:新增「试听」按钮(播放 decodeSuccess)
|
||||
|
||||
---
|
||||
|
||||
## 二、成就系统(14 项,跨周目永久加成)
|
||||
|
||||
### 设计目标
|
||||
- 给玩家长期目标与即时正反馈
|
||||
- 成就奖励包含即时资源 + 永久产能/洞见百分比加成(跨周目保留,与蓝图叠加)
|
||||
- 解锁时 Toast 通知 + 专属音效
|
||||
|
||||
### 实现
|
||||
- **定义**:`src/lib/game/achievements.ts`(14 项成就 + `achievementBonuses` 聚合)
|
||||
- **状态**:`GameState.achievements: Record<string, boolean>`
|
||||
- **检测**:`gameStore.checkAchievements()` 每秒由主循环调用;页面可见性恢复时立即检测
|
||||
- **通知**:`AchievementNotifier` 组件消费 `_achievementQueue`,弹 Toast + 播音效
|
||||
- **UI**:`AchievementsPanel` 组件,作为第 4 个标签页
|
||||
|
||||
### 成就列表
|
||||
| ID | 名称 | 条件 | 奖励 |
|
||||
|----|------|------|------|
|
||||
| ach_first_pulse | 初次触碰 | 发起第一次脉冲 | +5 晶体 |
|
||||
| ach_first_decode | 谐振初鸣 | 解码第一颗晶体 | +3 洞见 |
|
||||
| ach_decoded_10 | 回响解码者 | 累计解码 10 颗 | +15 洞见 · 产能 +5% |
|
||||
| ach_decoded_25 | 记忆织匠 | 累计解码 25 颗 | +40 洞见 · 产能 +8% |
|
||||
| ach_decoded_50 | 星核解密师 | 累计解码 50 颗 | +100 洞见 · 产能 +12% · 洞见 +10% |
|
||||
| ach_tech_3 | 初窥门径 | 解锁 3 项技术 | +20 洞见 |
|
||||
| ach_tech_all | 全谱精通 | 解锁全部 12 项技术 | 产能 +15% · 洞见 +15% |
|
||||
| ach_frag_4 | 残篇拾遗 | 拼凑 4 段碎片 | +10 接触 · +25 洞见 |
|
||||
| ach_frag_all | 回响全谱 | 拼凑全部 8 段碎片 | +25 接触 · 产能 +10% |
|
||||
| ach_exp_1 | 初探遗迹 | 完成 1 次探险 | +10 洞见 |
|
||||
| ach_exp_5 | 遗迹猎手 | 出发 5 次探险 | +30 洞见 · 产能 +5% |
|
||||
| ach_prestige_1 | 初次接触 | 飞升 1 次 | 产能 +10% · 洞见 +10% |
|
||||
| ach_prestige_3 | 维度行者 | 飞升 3 次 | 产能 +20% · 洞见 +20% |
|
||||
| ach_warehouse | 满仓时刻 | 晶体达仓库上限 | +8 洞见 |
|
||||
|
||||
### 加成计算
|
||||
`recomputeStats()` 现聚合三层数值来源:
|
||||
1. 技术树节点效果
|
||||
2. 飞升蓝图加成(每张 +5% 产能 / +3% 洞见 / +4% 接触)
|
||||
3. **成就永久加成**(跨周目保留,飞升后仍生效)
|
||||
|
||||
### 旧存档兼容
|
||||
- `init()` 补全缺失的 `achievements` 字段为 `{}`
|
||||
- `partialize` 排除 `_achievementQueue`(运行时临时队列不持久化)
|
||||
|
||||
---
|
||||
|
||||
## 三、视觉打磨
|
||||
|
||||
### CrystalOrb 增强
|
||||
- **粒子爆发**:每次脉冲生成 6–12 个粒子,沿径向发散(连击越高粒子越多)
|
||||
- **脉冲扩散环**:点击时从晶体向外扩散的绿色光环
|
||||
- **浮动数字变色**:连击≥3 紫色、≥5 琥珀色,带对应颜色光晕
|
||||
- 新增 `echo-ring` / `echo-burst` 关键帧动画
|
||||
|
||||
### 标签栏
|
||||
- 从 4 列扩展为 5 列(新增「成就」)
|
||||
- 成就标签有未解锁时显示琥珀色脉冲点
|
||||
- 标签字号微调以适配 5 列
|
||||
|
||||
### StatsPanel
|
||||
- 新增「已解锁成就」「累计探险」「探险能量」三行
|
||||
|
||||
---
|
||||
|
||||
## 四、QA 验证
|
||||
|
||||
### agent-browser 自动化测试
|
||||
- ✅ 页面加载 HTTP 200,无运行时错误
|
||||
- ✅ 解码谜题:生成 → 求解(按目标色序点击相邻同色节点)→ 奖励入账(绕过仓库上限)
|
||||
- ✅ 探险系统:出发 → 探索节点 → 获得奖励 → 前进 → 日志记录
|
||||
- ✅ 成就面板:14 项渲染,3 项已解锁(初次触碰/谐振初鸣/满仓时刻),永久加成区域正确显示
|
||||
- ✅ 统计面板:成就计数 3/14,探险次数,能量条
|
||||
- ✅ 设置面板:音效试听按钮可点击,开关可用
|
||||
|
||||
### VLM 视觉验证
|
||||
- 成就面板:深色背景 + 科技光效 + 锁/解锁状态分明,色彩对比鲜明
|
||||
- 晶体集群:渐变晶核 + 旋转环 + 粒子爆发正常
|
||||
|
||||
### Lint
|
||||
- `bun run lint` 零错误
|
||||
|
||||
---
|
||||
|
||||
## 五、文件变更清单
|
||||
|
||||
**新增**:
|
||||
- `src/lib/game/audio.ts` — 程序化音频引擎
|
||||
- `src/hooks/useAudio.ts` — 音频 React 适配
|
||||
- `src/lib/game/achievements.ts` — 成就定义与聚合
|
||||
- `src/components/game/AchievementsPanel.tsx` — 成就面板
|
||||
- `src/components/game/AchievementNotifier.tsx` — 成就 Toast 消费者
|
||||
|
||||
**修改**:
|
||||
- `src/lib/game/types.ts` — GameState 新增 `achievements` 字段
|
||||
- `src/lib/game/config.ts` — INITIAL_STATE 新增 `achievements: {}`
|
||||
- `src/lib/game/engine.ts` — recomputeStats 聚合成就加成;performPrestige/createInitialState 保留成就
|
||||
- `src/store/gameStore.ts` — checkAchievements / consumeAchievementQueue;init/hardReset/partialize 兼容
|
||||
- `src/hooks/useGameLoop.ts` — 每秒检测成就 + 可见性恢复检测
|
||||
- `src/app/page.tsx` — 5 列标签 + 成就页 + AchievementNotifier + useAudioSync
|
||||
- `src/components/game/CrystalOrb.tsx` — 粒子爆发 + 扩散环 + 浮动数字变色 + pulse 音效
|
||||
- `src/components/game/DecodeArray.tsx` — decodeClick/fail/success/fragment 音效
|
||||
- `src/components/game/ExpeditionPanel.tsx` — start/node/victory/defeat 音效
|
||||
- `src/components/game/TechTree.tsx` — techBuy 音效
|
||||
- `src/components/game/PrestigeDialog.tsx` — prestige 音效
|
||||
- `src/components/game/SettingsDialog.tsx` — 音效试听按钮 + 文案更新
|
||||
|
||||
---
|
||||
|
||||
## 六、下一步规划
|
||||
|
||||
- **v0.3 剩余**:socket 全局「星潮」实时事件(需 mini-service)
|
||||
- **v0.4**:云存档 + 排行榜
|
||||
- **v0.5**:全 5 纪元叙事扩展
|
||||
- 持续关注 Gitea 工单
|
||||
@@ -1,113 +0,0 @@
|
||||
# 回响星核 v0.3 — 星潮事件系统(周期性全局玩法修饰)
|
||||
|
||||
> 版本:v0.3
|
||||
> 日期:2026-06-23
|
||||
> 范围:6 种星潮事件、周期触发、全屏视觉叠层、程序化音效、寂静期补偿机制
|
||||
|
||||
---
|
||||
|
||||
## 一、设计动机
|
||||
|
||||
回应 Gitea Issue #1「就一直连连看?」的玩法单一反馈。星潮事件为放置循环注入**动态变化**:深空中偶发的「星潮」会短暂改变物理法则,让玩家在 75 秒内体验产能翻倍、解码加成、探险增强等不同玩法侧重,打破单调感。
|
||||
|
||||
## 二、6 种星潮事件
|
||||
|
||||
| 类型 | 名称 | 效果 | 色调 | 性质 |
|
||||
|------|------|------|------|------|
|
||||
| crystal | 晶体潮 | 采矿产能 ×2 | 翠绿 | 正面 |
|
||||
| resonance | 谐振风暴 | 解码奖励 ×1.6 | 玫红 | 正面 |
|
||||
| ruins | 遗迹共振 | 探险力 +5、生命 +30 | 琥珀 | 正面 |
|
||||
| void | 虚空低语 | 洞见获取 ×2 | 紫红 | 正面 |
|
||||
| core | 星核悸动 | 接触率 ×3、脉冲威力 ×2 | 青色 | 正面 |
|
||||
| silence | 寂静期 | 产能 ×0.5;结束时按时长补偿洞见 | 灰蓝 | 负面(有补偿) |
|
||||
|
||||
## 三、触发机制
|
||||
|
||||
- **首次星潮**:游戏开始 30 秒后触发(`TIDE_CONFIG.firstDelay`)
|
||||
- **常规间隙**:星潮结束后 40 秒触发下一次(`TIDE_CONFIG.gap`)
|
||||
- **持续时间**:75 秒(`TIDE_CONFIG.duration`)
|
||||
- **权重抽取**:6 种事件按权重随机(正面事件权重高,寂静期权重最低 10%)
|
||||
- **飞升重置**:飞升后清空当前星潮,重置计时,30 秒后重新开始
|
||||
|
||||
## 四、修饰器应用层
|
||||
|
||||
| 修饰器 | 应用点 |
|
||||
|--------|--------|
|
||||
| crystalsPerSecMult | `tick()` 即时乘(显示基础值,实际翻倍积累) |
|
||||
| pulsePowerMult | `pulse()` 即时乘 |
|
||||
| decodeRewardMult | `clickNode()` / `autoDecodeTick()` 结算时乘 |
|
||||
| insightMultAdd | 进 `recomputeStats` 缓存(星潮起止时重算) |
|
||||
| contactRateMult | 进 `recomputeStats` 缓存 |
|
||||
| expeditionPowerBonus / hpBonus | `startExpedition()` 即时加 |
|
||||
| silenceInsightPerSec | 星潮结束时 `computeSilenceCompensation` 结算 |
|
||||
|
||||
## 五、UI 表现
|
||||
|
||||
### 顶部指示器(`StarTideIndicator`)
|
||||
- 活跃时在 header 右侧显示彩色芯片:图标 + 名称 + 倒计时秒数
|
||||
- 芯片背景为对应色半透明,带光晕,进度条随时间推进
|
||||
|
||||
### 全屏背景叠层(`StarTideOverlay`)
|
||||
- 顶部 + 底部对应色渐变色带
|
||||
- 边缘 `inset box-shadow` 呼吸光效(3 秒周期)
|
||||
- 活跃时整页笼罩对应色调氛围
|
||||
|
||||
### 通知(`StarTideNotifier`)
|
||||
- 星潮降临时 Toast + `tideStart` 音效(神秘扫频)
|
||||
- 星潮结束时 Toast + `tideEnd` 音效(柔和消退)
|
||||
- 寂静期结束时显示「虚空回赠 N 洞见」+ `techBuy` 音效
|
||||
|
||||
### Footer 联动
|
||||
- 活跃时 footer 提示文字变为「✺ 星潮「晶体潮」进行中 · 深空晶体涌动…」
|
||||
- footer 边框点亮对应色光晕
|
||||
|
||||
## 六、程序化音效
|
||||
|
||||
新增 2 种音效(`audio.ts`):
|
||||
- `tideStart`:330→660Hz 正弦扫频 + 440→880Hz 三角波 + 523/784Hz 长音(神秘降临感)
|
||||
- `tideEnd`:659→523→392Hz 下行三音(柔和消退)
|
||||
|
||||
## 七、状态与兼容
|
||||
|
||||
- `GameState` 新增 `activeTide: StarTide | null`、`lastTideEnd: number`
|
||||
- 旧存档兼容:`init()` 补全字段为 `null/0`
|
||||
- `partialize` 排除运行时队列 `_tideEvents`
|
||||
- `performPrestige` 重置星潮(清空 activeTide,lastTideEnd 设为 now)
|
||||
|
||||
## 八、QA 验证
|
||||
|
||||
### agent-browser 自动化
|
||||
- ✅ 强制触发星潮:`lastTideEnd=0` → 重载 → 30s 内自动触发,状态显示 `activeTide: crystal`
|
||||
- ✅ 顶部指示器渲染:「✺ 晶体潮 64s」绿色芯片
|
||||
- ✅ 背景叠层:VLM 确认绿色调背景光晕 + 顶部/底部色带 + 边缘呼吸光
|
||||
- ✅ 寂静期补偿:设置 `type=silence, endsAt=now+3s` → 等待结束 → insights 从 50 增至 81(补偿 30)
|
||||
- ✅ Footer 联动:活跃时显示「✺ 星潮「晶体潮」进行中 · 深空晶体涌动…」
|
||||
- ✅ 版本号 v0.3 显示
|
||||
- ✅ lint 零错误,HTTP 200,无运行时错误
|
||||
|
||||
### VLM 视觉确认
|
||||
- 指示器芯片:绿色文字「晶体潮 11s」
|
||||
- 背景光晕:深绿色,顶部/底部色带 + 边缘呼吸光
|
||||
- 视觉完成度:界面清晰、风格统一、细节良好
|
||||
|
||||
## 九、文件变更清单
|
||||
|
||||
**新增**:
|
||||
- `src/lib/game/starTide.ts` — 星潮定义、修饰器、触发逻辑
|
||||
- `src/components/game/StarTide.tsx` — 指示器 + 叠层 + 通知三组件
|
||||
|
||||
**修改**:
|
||||
- `src/lib/game/types.ts` — GameState 新增 `activeTide` / `lastTideEnd`
|
||||
- `src/lib/game/config.ts` — INITIAL_STATE 新增潮汐字段
|
||||
- `src/lib/game/engine.ts` — `recomputeStats` 应用 insightMultAdd/contactRateMult;`performPrestige`/`createInitialState` 处理潮汐
|
||||
- `src/lib/game/audio.ts` — 新增 `tideStart` / `tideEnd` 音效
|
||||
- `src/store/gameStore.ts` — `tickTide` / `consumeTideEvents`;tick/pulse/clickNode/autoDecodeTick/startExpedition 应用修饰器;init/hardReset/partialize 兼容
|
||||
- `src/hooks/useGameLoop.ts` — 主循环调用 `tickTide`,可见性恢复补检测
|
||||
- `src/app/page.tsx` — 接入 StarTideOverlay/Indicator/Notifier;Footer 联动;版本号 v0.3
|
||||
|
||||
## 十、下一步规划
|
||||
|
||||
- v0.3 已完成客户端星潮(原计划 socket 全局事件,现以单机版先行,后续可扩展为多人同步)
|
||||
- v0.4:云存档 + 排行榜
|
||||
- v0.5:全 5 纪元叙事扩展
|
||||
- 持续关注 Gitea 工单
|
||||
@@ -1,171 +0,0 @@
|
||||
# v0.3.1 · 星图天文台(Constellation Observatory)元进程天赋系统
|
||||
|
||||
> 第二次回应 Issue #1「就一直连连看?」——为飞升循环加入 Slay the Spire 式 3 选 1 天赋 draft,
|
||||
> 跨周目永久生效,配合 Canvas 动态星图可视化,提供长期成长目标与策略选择。
|
||||
|
||||
## 一、设计动机
|
||||
|
||||
v0.3 星潮事件已为放置循环注入动态变化,但玩家仍缺长期 meta-progression 目标。本系统在飞升后触发:
|
||||
|
||||
- **策略选择**:3 选 1 的 draft 让每次飞升都有差异化构建方向
|
||||
- **永久成长**:天赋跨周目保留,配合蓝图/成就形成三层加成
|
||||
- **视觉反馈**:Canvas 动态星图,每解锁一个天赋就在对应星座点亮一颗星
|
||||
- **目标驱动**:18 个天赋 + 2 个专属成就,给玩家清晰的中长期目标
|
||||
|
||||
## 二、系统架构
|
||||
|
||||
### 2.1 数据层(`src/lib/game/constellation.ts`)
|
||||
|
||||
```typescript
|
||||
// 6 大星座 × 3 颗星 = 18 个天赋
|
||||
type ConstellationCategory =
|
||||
| "mining" | "decoding" | "expedition"
|
||||
| "contact" | "economy" | "cosmic";
|
||||
|
||||
interface ConstellationPerk {
|
||||
id: string; // c_min_1, c_dec_2, ...
|
||||
category: ConstellationCategory;
|
||||
name: string;
|
||||
desc: string;
|
||||
order: 1 | 2 | 3; // 星座内位置
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 18 个天赋一览
|
||||
|
||||
| 类别 | #1 | #2 | #3 |
|
||||
|------|-----|-----|-----|
|
||||
| 翠·永动矿脉 | 永动钻头(产能+20%) | 深井网络(上限+50%) | 谐振熔炉(脉冲+50%) |
|
||||
| 玫·光谱矩阵 | 光谱记忆(洞见+15%) | 步幅延展(步数+1) | 自动校准(自解-3s) |
|
||||
| 琥·远征星图 | 维生护盾(生命+25%) | 信标矩阵(探险力+20%) | 能量共振(能量上限+1) |
|
||||
| 紫·接触回响 | 接触共鸣(接触率+20%) | 飞升加速(门槛-10) | 蓝图回响(每蓝图+2%全属性) |
|
||||
| 灰·虚空市场 | 离线缓存(离线+15%) | 晶体富集(T2/T3+8%) | 星潮引导(间隙-10s) |
|
||||
| 金·宇宙回响 | 飞升礼包(+30晶体/+5洞见) | 二周目经验(解码+10%) | 全息共振(全产能+8%) |
|
||||
|
||||
### 2.3 修饰器聚合
|
||||
|
||||
`constellationBonuses(perks: string[]): ConstellationModifiers` 返回 **19 项修饰器**,
|
||||
在 `recomputeStats()` / `tickTide()` / `tick()` / `clickNode()` / `autoDecodeTick()` / `computeExpeditionPower()` / `computeExpeditionHp()` 等所有相关用点处应用。
|
||||
|
||||
### 2.4 状态扩展
|
||||
|
||||
```typescript
|
||||
// types.ts
|
||||
interface GameState {
|
||||
// ... 既有字段
|
||||
constellation: string[]; // 已解锁天赋 ID
|
||||
pendingPerkChoices: string[] | null; // 飞升后待选 3 个
|
||||
}
|
||||
|
||||
// config.ts INITIAL_STATE
|
||||
constellation: [],
|
||||
pendingPerkChoices: null,
|
||||
```
|
||||
|
||||
### 2.5 飞升流程
|
||||
|
||||
```
|
||||
performPrestige(state)
|
||||
├─ 计算 newBp(蓝图数)
|
||||
├─ rollPerkChoices(constellation) → 3 个不同类别的可选天赋
|
||||
├─ 应用「飞升礼包」补偿(+30 晶体 / +5 洞见,若已解锁)
|
||||
├─ 重置资源/技术/接触进度
|
||||
└─ 保留:fragments / totalDecoded / blueprints / achievements / constellation
|
||||
```
|
||||
|
||||
飞升后 `pendingPerkChoices` 不为空 → `ConstellationDialog` 自动弹出 → 玩家选 1 → `chooseConstellationPerk(perkId)` 写入 constellation 并清空 pending。
|
||||
|
||||
## 三、UI 组件
|
||||
|
||||
### 3.1 ConstellationPanel(星图标签内容)
|
||||
|
||||
**Canvas 动态渲染**(requestAnimationFrame 60fps):
|
||||
- 六边形布局 6 个星座中心点
|
||||
- 每星座内 3 颗星(三角形排布:order 1 顶部,2/3 底部)
|
||||
- **未解锁星**:暗灰小点 + 弱光晕
|
||||
- **已解锁星**:类别色填充 + 强光晕 + 4 道十字光线 + 闪烁动画
|
||||
- **同星座已解锁星之间**:连接线(类别色 60% 透明度)
|
||||
- **星座 → 中心星核**:虚线(已解锁 ≥1 时显示,呼吸效果)
|
||||
- **中心星核**:紫红色径向渐变,半径随时间正弦呼吸
|
||||
- **Hover**:星点 14px 范围内显示 tooltip(天赋名/描述/类别/序号)
|
||||
|
||||
**底部信息区**:
|
||||
- 6 类别图例条(彩色圆点 + 名称 + 已解锁/总数)
|
||||
- 已解锁天赋列表(彩色边框 + 名称 + 描述 + 类别标签)
|
||||
- 空状态:虚线边框卡片 + 引导文案
|
||||
|
||||
### 3.2 ConstellationDialog(飞升后 draft 模态)
|
||||
|
||||
- 自动 open:`pendingPerkChoices` 非空时
|
||||
- 3 张卡片:左侧类别色条 + 圆形首字图标 + 天赋名 + 类别徽章 + 描述
|
||||
- 选中动画:8 个粒子 ping + 卡片缩放 + 类别色发光
|
||||
- 「重新抽取」按钮:rerollPerkChoices(依然保证 3 不同类别)
|
||||
- 底部说明:「选择后永久保留,跨周目生效」
|
||||
|
||||
### 3.3 顶部「觉醒」按钮
|
||||
|
||||
当 `pendingPerkChoices` 存在时显示,fuchsia 边框 + animate-pulse,点击打开 ConstellationDialog。
|
||||
|
||||
## 四、平衡设计
|
||||
|
||||
### 4.1 单天赋强度
|
||||
- 每个天赋加成约 +15%~+25% 单项,避免数值膨胀
|
||||
- 「飞升礼包」给前期起飞资源(30 晶体 + 5 洞见)
|
||||
- 「全息共振」给全局 +8%,作为终极通用天赋
|
||||
|
||||
### 4.2 跨周目叠加
|
||||
- 18 个天赋全解锁 ≈ 总加成 200%~300%(与 6 蓝图、14 成就叠加后约 5x~8x)
|
||||
- 配合飞升次数加成(每次 +15% 探险力、+10 生命),后期探险胜率显著提升
|
||||
|
||||
### 4.3 类别多样性
|
||||
- `rollPerkChoices` 第一轮强制不同类别,避免 3 选全是同类
|
||||
- 玩家可在 6 类别间自由构建:采矿流/解码流/探险流/速飞升流/经济流/全能流
|
||||
|
||||
## 五、QA 验证记录
|
||||
|
||||
| 测试项 | 结果 |
|
||||
|--------|------|
|
||||
| 星图标签页渲染 | ✅ 6 类别图例 + Canvas + 空状态 |
|
||||
| 对话框自动弹出 | ✅ pendingPerkChoices 设置后立即开 |
|
||||
| 3 卡片显示 | ✅ 来自 3 不同类别 |
|
||||
| 点击天赋 | ✅ constellation 添加,crystalsPerSec 提升,pending 清空 |
|
||||
| 重新抽取 | ✅ 3 张新卡片(依然不同类别) |
|
||||
| 多天赋叠加 | ✅ c_min_1+c_con_1 → 产能 1.416 / 接触率 1.2 |
|
||||
| 6 类别成就触发 | ✅ 六分星辉 Toast 弹出 |
|
||||
| VLM 视觉确认 | ✅ 单星点亮(绿色永动矿脉) |
|
||||
| lint | ✅ 零错误 |
|
||||
| 编译 | ✅ < 200ms |
|
||||
|
||||
## 六、与既有系统的协同
|
||||
|
||||
```
|
||||
飞升(Prestige)
|
||||
│
|
||||
┌─────────┼─────────┐
|
||||
▼ ▼ ▼
|
||||
蓝图+ 星图天赋+ 成就+
|
||||
(5%/3%/4%) (各天赋) (各成就)
|
||||
│ │ │
|
||||
└─────────┴─────────┘
|
||||
│
|
||||
▼
|
||||
recomputeStats()
|
||||
│
|
||||
┌─────────┼─────────┐
|
||||
▼ ▼ ▼
|
||||
产能 解码奖励 探险力/生命
|
||||
│
|
||||
▼
|
||||
星潮修饰(即时乘)
|
||||
│
|
||||
▼
|
||||
实际游戏体验
|
||||
```
|
||||
|
||||
## 七、后续扩展方向
|
||||
|
||||
- **天赋觉醒动画**:选择天赋时全屏闪光 + 星图缩放
|
||||
- **天赋重置**:消耗稀有资源重置 constellation,重新选择
|
||||
- **隐藏天赋**:达成特殊条件解锁(如「飞升 10 次自选 1 天赋」)
|
||||
- **天赋树依赖**:order 2 需要 order 1 解锁(增加构建深度)
|
||||
- **socket 同步**:多人共享同一星图,协作解锁
|
||||
@@ -1,175 +0,0 @@
|
||||
# v0.4 回响编年史 · Chronicle of Echoes
|
||||
|
||||
> 跨周目叙事时间轴系统。每次飞升自动铭刻一条纪元条目,记录本周目的解码、探险、星潮、天赋觉醒与里程碑,形成永恒叙事流。朝 v0.5「全 5 纪元叙事」目标推进。
|
||||
|
||||
## 动机
|
||||
|
||||
- **回应 Issue #1**「就一直连连看?」的进一步反馈:星潮+星图解决了「玩法单一」,但仍缺**叙事深度**与**进度回顾**。
|
||||
- 玩家完成多次飞升后无回顾入口,对「自己走过多远」缺乏感知,影响长线留存。
|
||||
- v0.5 路线图为「全 5 纪元叙事」,编年史是其底层骨架——先把数据结构与展示框架立起来。
|
||||
|
||||
## 核心模块
|
||||
|
||||
### 数据模型(`src/lib/game/types.ts`)
|
||||
|
||||
```ts
|
||||
export interface ChronicleEntry {
|
||||
ascensionNumber: number; // 第几次飞升(1-based)
|
||||
epochName: string; // 程序化生成的纪元名(5 纪元循环)
|
||||
timestamp: number;
|
||||
durationSec: number; // 本周目持续时间
|
||||
summary: {
|
||||
techsUnlocked: number;
|
||||
constellationsTotal: number;
|
||||
expeditionsCompleted: number;
|
||||
bossKills: number;
|
||||
starTidesEncountered: number;
|
||||
blueprintsAfter: number;
|
||||
crystalsDecoded: number;
|
||||
crystalsDecodedThisRun: number;
|
||||
};
|
||||
perksThisAscension: string[]; // 本次飞升后选择的天赋
|
||||
tidesThisRun: string[]; // 本周目新遇到的星潮 ID
|
||||
lore: string; // 程序化生成的叙事文本
|
||||
milestones: string[]; // 里程碑标签
|
||||
}
|
||||
|
||||
export interface RunStartSnapshot {
|
||||
timestamp: number;
|
||||
techsUnlocked: number;
|
||||
expeditionsCompleted: number;
|
||||
bossKills: number;
|
||||
starTidesEncountered: string[];
|
||||
crystalsDecoded: number;
|
||||
}
|
||||
```
|
||||
|
||||
GameState 新增字段:
|
||||
- `chronicle: ChronicleEntry[]` — 编年史条目列表(上限 50)
|
||||
- `runStart: RunStartSnapshot` — 当前周目起始快照(用于 delta 计算)
|
||||
- `bossKills: number` — 累计 BOSS 击破数(跨周目)
|
||||
- `starTidesEncountered: string[]` — 累计遇到的星潮 ID 集合(去重)
|
||||
|
||||
### 编年史逻辑(`src/lib/game/chronicle.ts`,新建)
|
||||
|
||||
- `EPOCH_NAMES` — 5 纪元 × 4 后缀 = 20 种纪元名组合(如「第一纪元 · 觉醒之晨」「第三纪元 · 虚空回望」)
|
||||
- `generateEpochName(ascensionNumber, seed)` — 按 ascensionNumber 循环 5 纪元,seed 决定后缀
|
||||
- `buildLore(entry)` — 模板化叙事生成:开场 → 核心活动 → 星潮段落 → 觉醒段落 → 里程碑 → 收尾
|
||||
- `detectMilestones(state, deltas)` — 9 种里程碑检测(首次飞升/首杀 BOSS/单周目解码 10+/技术狂人/星潮亲历者/星图六分等)
|
||||
- `computeRunDeltas(state)` — 基于 runStart 快照计算本周目 delta(持续时间/解码数/技术数/探险数/BOSS/新星潮)
|
||||
- `buildChronicleEntry(state, perks)` — 飞升时构建一条 ChronicleEntry
|
||||
- `createRunStartSnapshot(state)` — 飞升后创建新 runStart
|
||||
- `migrateChronicleFields(state)` — 旧存档兼容:补全 chronicle=[]/runStart/bossKills=0/starTidesEncountered=[]
|
||||
- `withPerks(entry, perks)` — 飞升后选天赋时回填 perksThisAscension,并重新生成 lore 包含天赋名
|
||||
- `getPerkCategoryBreakdown(perks)` / `getCategoryName` / `getCategoryColor` — UI 辅助
|
||||
|
||||
### 引擎接入(`src/lib/game/engine.ts`)
|
||||
|
||||
- `performPrestige` 在重置前:
|
||||
1. `buildChronicleEntry(state, [])` 构建本周目条目(perks 暂为空,待玩家选择后回填)
|
||||
2. 追加到 `chronicle` 数组(上限 50 条)
|
||||
3. `createRunStartSnapshot(state)` 创建新周目的快照
|
||||
- 保留跨周目字段:`chronicle`, `bossKills`, `starTidesEncountered`
|
||||
- 重置 `runStart` 为新快照
|
||||
|
||||
### Store 接入(`src/store/gameStore.ts`)
|
||||
|
||||
- `init()` — 调用 `migrateChronicleFields` 补全旧存档
|
||||
- `tickTide` — 新星潮触发时,将 `tide_${type}` 追加到 `starTidesEncountered`(去重)
|
||||
- `resolveCurrentNode` — BOSS 战胜利结算时,`bossKills++`
|
||||
- `chooseConstellationPerk` — 选择天赋后,调用 `withPerks` 回填最近一条 chronicle entry 的 `perksThisAscension`,并重新生成 lore
|
||||
|
||||
### UI 组件(`src/components/game/ChronicleDialog.tsx`,新建)
|
||||
|
||||
- 顶部 BookOpen 图标按钮(带 badge 显示条目数)
|
||||
- 全屏对话框,深紫黑渐变背景 + 顶部三色装饰条
|
||||
- 顶部总览统计:飞升次数 / BOSS 击破 / 星潮亲历
|
||||
- 垂直时间轴:
|
||||
- 左侧渐变时间轴线
|
||||
- 每条卡片左侧有发光圆形节点(5 纪元循环色:翠/玫/琥/紫/金),最新一条带 ping 动画
|
||||
- 卡片含:#编号 / 纪元名(带 textShadow 光晕)/ 时间戳+持续时长 / 叙事文本(衬线字体)/ 6 项统计芯片 / 星潮徽章列表 / 天赋徽章 / 里程碑徽章
|
||||
- 入场动画:fade-in + slide-up,错峰 80ms
|
||||
- 空状态:根据 ascensions 是否 > 0 显示不同提示
|
||||
- 打开时播放 `chronicleOpen` 翻页音效
|
||||
|
||||
### 音频(`src/lib/game/audio.ts`)
|
||||
|
||||
- `chronicle` — 编年史铭刻音效:G3 基音 + C4/G4/C5/G5 多层泛音,深沉钟声感
|
||||
- `chronicleOpen` — 翻页音效:A4→D5→A5 轻柔上升 + 短促气流
|
||||
- 飞升成功时延迟 600ms 触发 `chronicle` sfx,与飞升音分层
|
||||
|
||||
### 成就(`src/lib/game/achievements.ts`)
|
||||
|
||||
新增 6 项成就:
|
||||
- `ach_chronicle_1` 「首部编年」— 完成首次飞升(+40 洞见 · 产能 +6%)
|
||||
- `ach_chronicle_3` 「三纪元回响」— 完成 3 次飞升(产能 +10% · 洞见 +10%)
|
||||
- `ach_chronicle_5` 「五纪元闭环」— 完成 5 次飞升(产能 +15% · 洞见 +15%)
|
||||
- `ach_boss_1` 「首杀维度」— 击破首个 BOSS(+25 洞见 · 产能 +4%)
|
||||
- `ach_boss_5` 「维度猎手」— 累计击破 5 个 BOSS(产能 +10% · 洞见 +8%)
|
||||
- `ach_tides_all` 「星潮亲历者」— 经历全部 6 种星潮(产能 +8% · 洞见 +8%)
|
||||
|
||||
### 飞升对话框增强(`src/components/game/PrestigeDialog.tsx`)
|
||||
|
||||
- 新增「编年史铭刻预告」卡片,提示本次飞升将记录入编年史
|
||||
- **修复关键 BUG**:原 `disabled={newBp <= 0 || confirming}` 当 `confirming=true` 时按钮永久禁用,玩家无法完成飞升!现已改为 `disabled={confirming && !canReconfirm}` 并加 900ms 冷却窗口
|
||||
- **修复 UX**:当 newBp=0(无可获蓝图)时不再永久禁用飞升按钮——v0.3.1 星图天赋 + v0.4 编年史已为飞升提供足够动机。按钮文字改为「飞升(无新蓝图)」
|
||||
- 飞升成功 toast 增加「编年史已铭刻新纪元」提示
|
||||
|
||||
### 统计面板增强(`src/app/page.tsx`)
|
||||
|
||||
新增 3 行统计:
|
||||
- BOSS 击破次数
|
||||
- 星潮亲历数(X/6)
|
||||
- 编年史条目数(X 纪元)
|
||||
|
||||
## QA 验证
|
||||
|
||||
### 修复的关键 BUG
|
||||
- 飞升按钮 `disabled` 逻辑错误导致玩家永远无法飞升(`confirming` 变 true 后按钮被禁用)
|
||||
- 验证:agent-browser 端到端测试,第一次点飞升 → 900ms 冷却 → 第二次点击成功触发,星图觉醒对话框弹出
|
||||
|
||||
### 编年史端到端测试
|
||||
1. 设置 contact=100 + 资源,触发飞升 → ascensions 1→2, chronicle 0→1 ✓
|
||||
2. 选择天赋「二周目经验」→ constellation 增加 c_cos_2, chronicle 末条 perksThisAscension=["c_cos_2"] ✓
|
||||
3. 打开编年史对话框 → 显示 1 条卡片,含完整 lore、统计芯片、星潮徽章、天赋徽章、里程碑 ✓
|
||||
4. 再次飞升 → ascensions 2→3, chronicle 1→2 ✓
|
||||
5. 第二条卡片正确显示新纪元名「第三纪元 · 虚空回望」+ 正确的星潮名「星核悸动」+ 正确的 duration delta(2分42秒) ✓
|
||||
6. 成就解锁:`ach_chronicle_1`「首部编年」+ `ach_prestige_3`自动检测 ✓
|
||||
7. 统计面板新增「BOSS 击破」「星潮亲历」「编年史条目」3 行 ✓
|
||||
8. VLM 视觉评估:8/10,无瑕疵,时间轴清晰,纪元颜色区分明显
|
||||
|
||||
### 视觉细节
|
||||
- 5 纪元循环色:翠 #34d399 / 玫 #fb7185 / 琥 #fbbf24 / 紫 #e879f9 / 金 #fcd34d
|
||||
- 时间轴节点带 boxShadow 光晕(12px + 24px 双层)
|
||||
- 最新条目带 animate-ping 脉冲
|
||||
- 卡片入场动画:fade-in + slide-in-from-bottom-2,错峰 80ms
|
||||
- 叙事文本使用 font-serif 衬线字体增加文学感
|
||||
- 统计芯片按类别用对应颜色 tint
|
||||
|
||||
### 旧存档兼容
|
||||
- `migrateChronicleFields` 自动补全 chronicle=[]/runStart/bossKills=0/starTidesEncountered=[]
|
||||
- 玩家旧存档不丢失进度,新字段以默认值初始化
|
||||
|
||||
## 文件清单
|
||||
|
||||
新增:
|
||||
- `src/lib/game/chronicle.ts` — 编年史核心逻辑
|
||||
- `src/components/game/ChronicleDialog.tsx` — 编年史对话框 UI
|
||||
- `docs/repo/docs/09-回响编年史系统-v0.4.md` — 本文档
|
||||
|
||||
修改:
|
||||
- `src/lib/game/types.ts` — 新增 ChronicleEntry / RunStartSnapshot 类型 + GameState 字段
|
||||
- `src/lib/game/config.ts` — INITIAL_STATE 新增 chronicle/runStart/bossKills/starTidesEncountered
|
||||
- `src/lib/game/engine.ts` — performPrestige 构建编年史条目 + 重置 runStart
|
||||
- `src/lib/game/achievements.ts` — 新增 6 项成就
|
||||
- `src/lib/game/audio.ts` — 新增 chronicle / chronicleOpen 音效
|
||||
- `src/store/gameStore.ts` — init 兼容 + tickTide 记录 + resolveCurrentNode BOSS 计数 + chooseConstellationPerk 回填
|
||||
- `src/components/game/PrestigeDialog.tsx` — 修复 BUG + 编年史预告卡片 + 音效
|
||||
- `src/app/page.tsx` — 头部编年史按钮 + 对话框渲染 + 统计面板新行 + 版本号 v0.4
|
||||
|
||||
## 已知限制 / 后续
|
||||
|
||||
- 编年史上限 50 条(超过自动丢弃最早的)
|
||||
- v0.4 之前发生的飞升不会被回填(无数据可恢复)—— 空状态有对应提示
|
||||
- 5 纪元叙事内容仍为模板化生成,v0.5 将加入手写剧情节点
|
||||
- 编年史目前纯本地,v0.4 云存档+排行榜会将其纳入同步范围
|
||||
@@ -1 +0,0 @@
|
||||
Here are all the generated files.
|
||||
|
Before Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 235 KiB |
|
Before Width: | Height: | Size: 234 KiB |
@@ -1,50 +0,0 @@
|
||||
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTypescript from "eslint-config-next/typescript";
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const eslintConfig = [...nextCoreWebVitals, ...nextTypescript, {
|
||||
rules: {
|
||||
// TypeScript rules
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/prefer-as-const": "off",
|
||||
"@typescript-eslint/no-unused-disable-directive": "off",
|
||||
|
||||
// React rules
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"react-hooks/purity": "off",
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react/display-name": "off",
|
||||
"react/prop-types": "off",
|
||||
"react-compiler/react-compiler": "off",
|
||||
|
||||
// Next.js rules
|
||||
"@next/next/no-img-element": "off",
|
||||
"@next/next/no-html-link-for-pages": "off",
|
||||
|
||||
// General JavaScript rules
|
||||
"prefer-const": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-console": "off",
|
||||
"no-debugger": "off",
|
||||
"no-empty": "off",
|
||||
"no-irregular-whitespace": "off",
|
||||
"no-case-declarations": "off",
|
||||
"no-fallthrough": "off",
|
||||
"no-mixed-spaces-and-tabs": "off",
|
||||
"no-redeclare": "off",
|
||||
"no-undef": "off",
|
||||
"no-unreachable": "off",
|
||||
"no-useless-escape": "off",
|
||||
},
|
||||
}, {
|
||||
ignores: ["node_modules/**", ".next/**", "out/**", "build/**", "next-env.d.ts", "examples/**", "skills"]
|
||||
}];
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -1,196 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { io } from 'socket.io-client';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
|
||||
type User = {
|
||||
id: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
type Message = {
|
||||
id: string;
|
||||
username: string;
|
||||
content: string;
|
||||
timestamp: Date | string;
|
||||
type: 'user' | 'system';
|
||||
}
|
||||
|
||||
export default function SocketDemo() {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [inputMessage, setInputMessage] = useState('');
|
||||
const [username, setUsername] = useState('');
|
||||
const [isUsernameSet, setIsUsernameSet] = useState(false);
|
||||
const [socket, setSocket] = useState<any>(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Connect to websocket server
|
||||
// Never use PORT in the URL, alyways use XTransformPort
|
||||
// DO NOT change the path, it is used by Caddy to forward the request to the correct port
|
||||
const socketInstance = io('/?XTransformPort=3003', {
|
||||
transports: ['websocket', 'polling'],
|
||||
forceNew: true,
|
||||
reconnection: true,
|
||||
reconnectionAttempts: 5,
|
||||
reconnectionDelay: 1000,
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
setSocket(socketInstance);
|
||||
|
||||
socketInstance.on('connect', () => {
|
||||
setIsConnected(true);
|
||||
});
|
||||
|
||||
socketInstance.on('disconnect', () => {
|
||||
setIsConnected(false);
|
||||
});
|
||||
|
||||
socketInstance.on('message', (msg: Message) => {
|
||||
setMessages(prev => [...prev, msg]);
|
||||
});
|
||||
|
||||
socketInstance.on('user-joined', (data: { user: User; message: Message }) => {
|
||||
setMessages(prev => [...prev, data.message]);
|
||||
setUsers(prev => {
|
||||
if (!prev.find(u => u.id === data.user.id)) {
|
||||
return [...prev, data.user];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
});
|
||||
|
||||
socketInstance.on('user-left', (data: { user: User; message: Message }) => {
|
||||
setMessages(prev => [...prev, data.message]);
|
||||
setUsers(prev => prev.filter(u => u.id !== data.user.id));
|
||||
});
|
||||
|
||||
socketInstance.on('users-list', (data: { users: User[] }) => {
|
||||
setUsers(data.users);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socketInstance.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleJoin = () => {
|
||||
if (socket && username.trim() && isConnected) {
|
||||
socket.emit('join', { username: username.trim() });
|
||||
setIsUsernameSet(true);
|
||||
}
|
||||
};
|
||||
|
||||
const sendMessage = () => {
|
||||
if (socket && inputMessage.trim() && username.trim()) {
|
||||
socket.emit('message', {
|
||||
content: inputMessage.trim(),
|
||||
username: username.trim()
|
||||
});
|
||||
setInputMessage('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
sendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4 max-w-2xl">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
WebSocket Demo
|
||||
<span className={`text-sm px-2 py-1 rounded ${isConnected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{isConnected ? 'Connected' : 'Disconnected'}
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{!isUsernameSet ? (
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleJoin();
|
||||
}
|
||||
}}
|
||||
placeholder="Enter your username..."
|
||||
disabled={!isConnected}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleJoin}
|
||||
disabled={!isConnected || !username.trim()}
|
||||
className="w-full"
|
||||
>
|
||||
Join Chat
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<ScrollArea className="h-80 w-full border rounded-md p-4">
|
||||
<div className="space-y-2">
|
||||
{messages.length === 0 ? (
|
||||
<p className="text-gray-500 text-center">No messages yet</p>
|
||||
) : (
|
||||
messages.map((msg) => (
|
||||
<div key={msg.id} className="border-b pb-2 last:border-b-0">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<p className={`text-sm font-medium ${msg.type === 'system'
|
||||
? 'text-blue-600 italic'
|
||||
: 'text-gray-700'
|
||||
}`}>
|
||||
{msg.username}
|
||||
</p>
|
||||
<p className={`${msg.type === 'system'
|
||||
? 'text-blue-500 italic'
|
||||
: 'text-gray-900'
|
||||
}`}>
|
||||
{msg.content}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
{new Date(msg.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Input
|
||||
value={inputMessage}
|
||||
onChange={(e) => setInputMessage(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Type a message..."
|
||||
disabled={!isConnected}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={sendMessage}
|
||||
disabled={!isConnected || !inputMessage.trim()}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import { createServer } from 'http'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
const httpServer = createServer()
|
||||
const io = new Server(httpServer, {
|
||||
// DO NOT change the path, it is used by Caddy to forward the request to the correct port
|
||||
path: '/',
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: ["GET", "POST"]
|
||||
},
|
||||
pingTimeout: 60000,
|
||||
pingInterval: 25000,
|
||||
})
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
username: string
|
||||
}
|
||||
|
||||
interface Message {
|
||||
id: string
|
||||
username: string
|
||||
content: string
|
||||
timestamp: Date
|
||||
type: 'user' | 'system'
|
||||
}
|
||||
|
||||
const users = new Map<string, User>()
|
||||
|
||||
const generateMessageId = () => Math.random().toString(36).substr(2, 9)
|
||||
|
||||
const createSystemMessage = (content: string): Message => ({
|
||||
id: generateMessageId(),
|
||||
username: 'System',
|
||||
content,
|
||||
timestamp: new Date(),
|
||||
type: 'system'
|
||||
})
|
||||
|
||||
const createUserMessage = (username: string, content: string): Message => ({
|
||||
id: generateMessageId(),
|
||||
username,
|
||||
content,
|
||||
timestamp: new Date(),
|
||||
type: 'user'
|
||||
})
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log(`User connected: ${socket.id}`)
|
||||
|
||||
// Add test event handler
|
||||
socket.on('test', (data) => {
|
||||
console.log('Received test message:', data)
|
||||
socket.emit('test-response', {
|
||||
message: 'Server received test message',
|
||||
data: data,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('join', (data: { username: string }) => {
|
||||
const { username } = data
|
||||
|
||||
// Create user object
|
||||
const user: User = {
|
||||
id: socket.id,
|
||||
username
|
||||
}
|
||||
|
||||
// Add to user list
|
||||
users.set(socket.id, user)
|
||||
|
||||
// Send join message to all users
|
||||
const joinMessage = createSystemMessage(`${username} joined the chat room`)
|
||||
io.emit('user-joined', { user, message: joinMessage })
|
||||
|
||||
// Send current user list to new user
|
||||
const usersList = Array.from(users.values())
|
||||
socket.emit('users-list', { users: usersList })
|
||||
|
||||
console.log(`${username} joined the chat room, current online users: ${users.size}`)
|
||||
})
|
||||
|
||||
socket.on('message', (data: { content: string; username: string }) => {
|
||||
const { content, username } = data
|
||||
const user = users.get(socket.id)
|
||||
|
||||
if (user && user.username === username) {
|
||||
const message = createUserMessage(username, content)
|
||||
io.emit('message', message)
|
||||
console.log(`${username}: ${content}`)
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
const user = users.get(socket.id)
|
||||
|
||||
if (user) {
|
||||
// Remove from user list
|
||||
users.delete(socket.id)
|
||||
|
||||
// Send leave message to all users
|
||||
const leaveMessage = createSystemMessage(`${user.username} left the chat room`)
|
||||
io.emit('user-left', { user: { id: socket.id, username: user.username }, message: leaveMessage })
|
||||
|
||||
console.log(`${user.username} left the chat room, current online users: ${users.size}`)
|
||||
} else {
|
||||
console.log(`User disconnected: ${socket.id}`)
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('error', (error) => {
|
||||
console.error(`Socket error (${socket.id}):`, error)
|
||||
})
|
||||
})
|
||||
|
||||
const PORT = 3003
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log(`WebSocket server running on port ${PORT}`)
|
||||
})
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('Received SIGTERM signal, shutting down server...')
|
||||
httpServer.close(() => {
|
||||
console.log('WebSocket server closed')
|
||||
process.exit(0)
|
||||
})
|
||||
})
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('Received SIGINT signal, shutting down server...')
|
||||
httpServer.close(() => {
|
||||
console.log('WebSocket server closed')
|
||||
process.exit(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
1:"$Sreact.fragment"
|
||||
2:I[39756,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"default"]
|
||||
3:I[37457,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"default"]
|
||||
4:I[77855,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js"],"Toaster"]
|
||||
5:I[47257,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"ClientPageRoot"]
|
||||
6:I[52683,["/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","/Super_Z/echo-nexus/_next/static/chunks/efcc875da619c164.js"],"default"]
|
||||
9:I[97367,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"OutletBoundary"]
|
||||
a:"$Sreact.suspense"
|
||||
c:I[97367,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"ViewportBoundary"]
|
||||
e:I[97367,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"MetadataBoundary"]
|
||||
10:I[68027,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"default"]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/chunks/34d933785a17edf3.css","style"]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/chunks/f4a1ac847b63088d.css","style"]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/media/797e433ab948586e-s.p.29207c2f.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
||||
:HL["/Super_Z/echo-nexus/_next/static/media/caa3a2e1cccd8315-s.p.3b6cae6d.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
||||
0:{"P":null,"b":"oyCsV_kYxc3pie8zrYRGs","c":["",""],"q":"","i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/Super_Z/echo-nexus/_next/static/chunks/34d933785a17edf3.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/Super_Z/echo-nexus/_next/static/chunks/f4a1ac847b63088d.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/Super_Z/echo-nexus/_next/static/chunks/2d46f11719722bdc.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/Super_Z/echo-nexus/_next/static/chunks/62cf09ef1d4f1188.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"zh-CN","suppressHydrationWarning":true,"className":"dark","children":["$","body",null,{"className":"geist_a71539c9-module__T19VSG__variable geist_mono_8d43a2aa-module__8Li5zG__variable antialiased bg-background text-foreground","children":[["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}],["$","$L4",null,{}]]}]}]]}],{"children":[["$","$1","c",{"children":[["$","$L5",null,{"Component":"$6","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@7","$@8"]}}],[["$","script","script-0",{"src":"/Super_Z/echo-nexus/_next/static/chunks/efcc875da619c164.js","async":true,"nonce":"$undefined"}]],["$","$L9",null,{"children":["$","$a",null,{"name":"Next.MetadataOutlet","children":"$@b"}]}]]}],{},null,false,false]},null,false,false],["$","$1","h",{"children":[null,["$","$Lc",null,{"children":"$Ld"}],["$","div",null,{"hidden":true,"children":["$","$Le",null,{"children":["$","$a",null,{"name":"Next.Metadata","children":"$Lf"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$10",[]],"S":true}
|
||||
7:{}
|
||||
8:"$0:f:0:1:1:children:0:props:children:0:props:serverProvidedParams:params"
|
||||
d:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
|
||||
11:I[27201,["/Super_Z/echo-nexus/_next/static/chunks/33d3f2c0cda968c7.js"],"IconMark"]
|
||||
b:null
|
||||
f:[["$","title","0",{"children":"回响星核 · Echo Nexus"}],["$","meta","1",{"name":"description","content":"原创深空考古放置策略游戏 — 解码记忆晶体,拼凑以太文明的回响,跨越维度发起接触。"}],["$","meta","2",{"name":"author","content":"Super_Z"}],["$","meta","3",{"name":"keywords","content":"Echo Nexus,回响星核,idle game,browser game,放置游戏,解谜,深空考古"}],["$","meta","4",{"property":"og:title","content":"回响星核 · Echo Nexus"}],["$","meta","5",{"property":"og:description","content":"原创深空考古放置策略游戏"}],["$","meta","6",{"property":"og:site_name","content":"Echo Nexus"}],["$","meta","7",{"property":"og:type","content":"website"}],["$","meta","8",{"name":"twitter:card","content":"summary_large_image"}],["$","meta","9",{"name":"twitter:title","content":"回响星核 · Echo Nexus"}],["$","meta","10",{"name":"twitter:description","content":"原创深空考古放置策略游戏"}],["$","link","11",{"rel":"icon","href":"https://z-cdn.chatglm.cn/z-ai/static/logo.svg"}],["$","$L11","12",{}]]
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -1,12 +0,0 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
/* config options here */
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
reactStrictMode: false,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||