Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bed2599f5 | ||
|
|
174d1227c0 | ||
|
|
774ddaf9d1 | ||
|
|
22588425cf | ||
|
|
96748d726f | ||
|
|
20dc1f252f | ||
|
|
17820b1f28 | ||
|
|
1701a908c9 | ||
|
|
71ca5b48a9 | ||
|
|
a3aa381656 | ||
|
|
494bc5ff64 | ||
|
|
43dfc2d41b | ||
|
|
3d916a1574 | ||
|
|
4545fe380b | ||
|
|
8474f4caa7 | ||
|
|
c0535e6ebe | ||
|
|
7111e154b2 | ||
|
|
72e1f3fc3c | ||
|
|
fafa2ee20a | ||
|
|
fcf94b6c3f | ||
|
|
296c609479 | ||
|
|
bd28219b65 | ||
|
|
8605f9bb5f | ||
|
|
00a46a0bf9 | ||
|
|
084387174b | ||
|
|
ab511fe9e0 | ||
|
|
60445abd42 | ||
|
|
04f6a398f4 | ||
|
|
a02b6191c5 | ||
|
|
5bc041ac0b | ||
|
|
697e1f360a | ||
|
|
e6ec22132e | ||
|
|
e69c48ba69 | ||
|
|
98fbbf1634 | ||
|
|
9a17849859 | ||
|
|
cc02fc2e7f | ||
|
|
9f4d839c6b | ||
|
|
f3d2e53e4f | ||
|
|
da18f32df2 | ||
|
|
674775be0e | ||
|
|
f73b96026c | ||
|
|
e193530f24 |
@@ -136,3 +136,15 @@ dist
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
|
||||
# Echo Nexus 本地文件
|
||||
local-*
|
||||
.claude
|
||||
.z-ai-config
|
||||
dev.log
|
||||
dev.out.log
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
.vercel
|
||||
skills/
|
||||
qa-*.png
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/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"
|
||||
@@ -0,0 +1 @@
|
||||
1061
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/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
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/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
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/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
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,23 @@
|
||||
: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 +1,197 @@
|
||||
# 回响星核 / Echo Nexus
|
||||
|
||||
> 原创深空考古放置策略游戏 · Original deep-space archaeology idle strategy game
|
||||
> 原创深空考古 **放置策略**游戏 · Original deep-space archaeology **idle strategy** game
|
||||
|
||||
你是一名深空考古指挥官,驾驶自治勘探舰回收已「飞升」的以太文明遗留的记忆晶体,解码碎片拼凑出他们的故事,并最终跨越维度发起「接触」。
|
||||
|
||||
**关掉浏览器,星核仍在为你工作**——采矿无人机舰队 24h 不间断采矿,放置工程在离线时持续推进,回来后一键领取积累的奖励。
|
||||
|
||||
---
|
||||
|
||||
## 🎮 在线游玩
|
||||
|
||||
**👉 [https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/](https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/)**
|
||||
|
||||
无需安装,打开即玩。支持桌面(鼠标 + 键盘)与移动端(触控)。
|
||||
|
||||
---
|
||||
|
||||
## 项目状态
|
||||
|
||||
- **版本**:v0.1(MVP 开发中)
|
||||
- **栈**:Next.js 16 · TypeScript · Tailwind · shadcn/ui · Canvas · Zustand · Prisma
|
||||
- **仓库**:https://git.atdunbg.xyz/Super_Z/echo-nexus
|
||||
- **版本**:**v0.16** — 解码阈值叙事事件(全屏碎片释放叠层)+ loreLayer 已读标记 + idleHistory 详情展开 + 移动端 375px 适配审计
|
||||
- **栈**:Next.js 16 (App Router) · TypeScript · Tailwind CSS 4 · shadcn/ui · Canvas 2D · Zustand · Web Audio API
|
||||
- **仓库**:[https://git.atdunbg.xyz/Super_Z/echo-nexus](https://git.atdunbg.xyz/Super_Z/echo-nexus)
|
||||
- **在线版**:[https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/](https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/)
|
||||
- **部署**:Gitea Pages(静态导出,`BUILD_EXPORT=true next build` → `out/` 推送 `gh-pages` 分支)
|
||||
|
||||
---
|
||||
|
||||
## 核心特色
|
||||
|
||||
- **📡 采矿无人机舰队**:每解锁一个采矿分支技术,就有一台自动采矿无人机加入舰队,24h 持续产出晶体(无需点击)
|
||||
- **⚙️ 放置工程项目**:6 种工程,派遣后**离线/在线自动推进**,完成领取奖励(晶体/洞见/能量/永久产能加成/随机碎片)
|
||||
- **🔓 离线收益**:关闭浏览器后产能按 `offlineEff × crystalsPerSec` 持续累积(上限 8h),重新打开弹窗结算「沉睡期间」收益
|
||||
- **🎯 始终可见的放置徽章**:Header 永远显示「放置中 +X/s」徽章,悬停查看产能构成明细——一眼就知道这是个 idle game
|
||||
- **🔍 解码共振谜题**:把放置游戏的「等待」变成有节奏的主动小谜题(路径构造法生成器,100% 可解)
|
||||
- **📜 碎片涌现叙事**:故事从解码中自然浮现,5 纪元循环命名 + 模板化 lore,不打断玩法
|
||||
- **💎 深空全息美学**:晶体 + Canvas 粒子 + 共振波纹的差异化视觉(严格 emerald/rose/amber/fuchsia 四色全息色谱,零蓝色/靛色)
|
||||
- **🔄 飞升多周目**:终局「接触」后保留蓝图开启新周目,星图天文台 18 个天赋跨周目永久生效
|
||||
- **🚀 实时巡航玩法**:Canvas 2D 飞船操控 + 8 种实体 + 3 层视差星空,打破纯放置节奏
|
||||
- **🧬 角色 RPG 深度**:四维属性(探索力/智慧/勇气/灵感)影响所有游戏系统
|
||||
|
||||
---
|
||||
|
||||
## 游戏系统(11 大系统)
|
||||
|
||||
| # | 系统 | 简介 | 类型 |
|
||||
|---|------|------|------|
|
||||
| 1 | **📡 放置采矿** | 中央晶体球 Canvas 粒子系统 + 主动脉冲 + 连击加成 + **自动采矿无人机舰队** | 放置 |
|
||||
| 2 | **⚙️ 放置工程** | 6 种工程 × 3 槽位,**离线/在线自动推进**,完成领取奖励 | **放置** |
|
||||
| 3 | **🔓 离线收益** | 关闭浏览器后产能按 `eff × cps` 累积(8h 上限),重新打开结算 | **放置** |
|
||||
| 4 | 谐振解码 | 原创路径谜题,100% 可解率 | 主动 |
|
||||
| 5 | 技术树 | 三层科技解锁,提升产能/解码/探险 | 主动 |
|
||||
| 6 | 遗迹探险 | 肉鸽系统,6 种节点 + 程序化路径 + BOSS | 主动 |
|
||||
| 7 | 星潮事件 | 6 种动态事件,全屏叠层 + 程序化音效 | 被动 |
|
||||
| 8 | 星图天文台 | 飞升后 3 选 1 天赋 draft,18 个跨周目天赋 | 元进程 |
|
||||
| 9 | 回响编年史 | 跨周目叙事时间轴,5 纪元循环命名 | 元进程 |
|
||||
| 10 | 深空信标 | 每日挑战 + 本地排行榜 Top20 | 每日 |
|
||||
| 11 | 深空巡航 | Canvas 2D 实时玩法,飞船操控/陨石躲避/星门通关 | 实时 |
|
||||
| 12 | 角色属性 | 四维属性(探索力/智慧/勇气/灵感),飞升获得属性点 | 元进程 |
|
||||
|
||||
---
|
||||
|
||||
## 放置系统设计哲学(v0.14 + v0.15 平衡)
|
||||
|
||||
回响星核的「放置」不是简单地把数字挂机增长,而是分层呈现 **3 种 idle 节奏**:
|
||||
|
||||
### 节奏 1:被动产能(最慢,最持续)
|
||||
- 采矿无人机舰队 = 你已解锁的采矿技术节点
|
||||
- 每台无人机 24h 持续产出晶体,无需任何操作
|
||||
- 晶体进入仓库(受 `crystalCap` 限制),仓库满后停止产出(防溢出)
|
||||
- **永远在线,离线也累积**
|
||||
|
||||
### 节奏 2:放置工程(中等周期,需要决策)
|
||||
- 6 种工程,时长 1min ~ 1h 不等
|
||||
- 玩家选择派遣到 3 个槽位之一 → 启动后**离线/在线自动推进**
|
||||
- 完成后必须**手动领取**(防玩家错过奖励时机)
|
||||
- 奖励类型多样:晶体 / 洞见 / 能量 / 永久产能加成 / 随机碎片
|
||||
- **核心 idle loop:派遣 → 离开 → 回来 → 领取 → 再派遣**
|
||||
|
||||
### 节奏 3:离线收益(最长周期,回归奖励)
|
||||
- 关闭浏览器后,被动产能按 `offlineEff`(默认 50%,可通过技术提升至 100%)继续累积
|
||||
- 上限 8 小时(防极端挂机)
|
||||
- 重新打开页面弹窗结算:「你离开了 X 小时,星核为你产出 Y 颗晶体」
|
||||
- **回归玩家的"啊我回来了"瞬间**
|
||||
|
||||
---
|
||||
|
||||
## 开发路线
|
||||
|
||||
| 版本 | 核心内容 | 状态 |
|
||||
|------|---------|------|
|
||||
| v0.1 | 放置 + 解码 + 技术树 + 图谱 + 飞升 | ✅ |
|
||||
| v0.1.1 | 解码可解性修复(路径构造法) | ✅ |
|
||||
| v0.2 | 遗迹探险肉鸽系统 | ✅ |
|
||||
| v0.2.1 | 程序化音频 + 14 项成就 | ✅ |
|
||||
| v0.3 | 星潮事件系统 | ✅ |
|
||||
| v0.3.1 | 星图天文台元进程 | ✅ |
|
||||
| v0.4 | 回响编年史系统 | ✅ |
|
||||
| v0.5 | 深空信标系统 | ✅ |
|
||||
| v0.5.1 | Gitea Pages 静态部署 + 新手教程 | ✅ |
|
||||
| v0.5.2 | 离线收益报告 + 标签页 UI 重设计 | ✅ |
|
||||
| v0.6 | 深空巡航 Canvas 2D 实时玩法 | ✅ |
|
||||
| v0.7 | CrystalOrb Canvas 粒子系统 + 四维角色属性 | ✅ |
|
||||
| v0.8 | 巡航玩法增强 / BOSS 关卡 / 道具掉落 | ✅ |
|
||||
| v0.8.1 | 探险平衡 + 信标系统扩展(周挑战 + 信标链) | ✅ |
|
||||
| **v0.14** | **放置系统:采矿无人机舰队 + 6 种放置工程 + 永久产能加成 + 始终可见 idle 徽章** | ✅ |
|
||||
| **v0.15** | **叙事深化(8 碎片 × 2 层 = 16 层 loreLayers)+ 永久加成 softcap + 批量领取 + 完成 toast + StatsPanel idle 统计** | ✅ |
|
||||
| **v0.16** | **解码阈值叙事事件(5 里程碑全屏叠层)+ loreLayer 已读标记 + idleHistory 详情展开 + 移动端 375px 适配审计** | ✅ |
|
||||
| v0.17 | 星潮三频段切换 + 健康设计(每日软上限 + 星核建议休眠) | 🔜 |
|
||||
| v1.0 | 全 5 纪元手写叙事 + 多人同步星潮 | 🔜 |
|
||||
|
||||
---
|
||||
|
||||
## 文档
|
||||
|
||||
- [`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 纪元叙事 + 隐藏内容
|
||||
- [`docs/04-解码系统修复-v0.1.1.md`](docs/04-解码系统修复-v0.1.1.md) — v0.1.1 解码修复
|
||||
- [`docs/05-遗迹探险系统-v0.2.md`](docs/05-遗迹探险系统-v0.2.md) — v0.2 探险系统
|
||||
- [`docs/06-音频与成就系统-v0.2.1.md`](docs/06-音频与成就系统-v0.2.1.md) — v0.2.1 音频与成就
|
||||
- [`docs/07-星潮事件系统-v0.3.md`](docs/07-星潮事件系统-v0.3.md) — v0.3 星潮
|
||||
- [`docs/08-星图天文台系统-v0.3.1.md`](docs/08-星图天文台系统-v0.3.1.md) — v0.3.1 天文台
|
||||
- [`docs/09-回响编年史系统-v0.4.md`](docs/09-回响编年史系统-v0.4.md) — v0.4 编年史
|
||||
- [`docs/10-深空信标系统-v0.5.md`](docs/10-深空信标系统-v0.5.md) — v0.5 信标
|
||||
- [`docs/11-放置系统-v0.14.md`](docs/11-放置系统-v0.14.md) — **v0.14 放置系统(idle operations)**
|
||||
- [`docs/12-叙事深化-v0.15.md`](docs/12-叙事深化-v0.15.md) — **v0.15 loreLayers 多层碎片文本 + softcap + 批量领取**
|
||||
- [`docs/13-叙事事件-v0.16.md`](docs/13-叙事事件-v0.16.md) — **v0.16 解码阈值里程碑叠层 + loreLayer 已读 + 移动端适配**
|
||||
|
||||
---
|
||||
|
||||
## 本地开发
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
bun install
|
||||
|
||||
# 启动 dev 服务器(http://localhost:3000)
|
||||
bun run dev
|
||||
|
||||
# 静态导出(用于 Gitea Pages 部署)
|
||||
BUILD_EXPORT=true bun run build:static
|
||||
# 产物在 out/ 目录,basePath 为 /Super_Z/echo-nexus
|
||||
|
||||
# 代码检查
|
||||
bun run lint
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 部署到 Gitea Pages
|
||||
|
||||
```bash
|
||||
# 1. 静态导出
|
||||
BUILD_EXPORT=true bun run build:static
|
||||
|
||||
# 2. 推送到 gh-pages 分支
|
||||
cd out
|
||||
git init && git checkout -b gh-pages
|
||||
git add -A
|
||||
git commit -m "deploy: v0.16"
|
||||
git remote add origin https://git.atdunbg.xyz/Super_Z/echo-nexus.git
|
||||
git push -f origin gh-pages
|
||||
```
|
||||
|
||||
Gitea Pages 会自动从 `gh-pages` 分支提供静态网站服务。
|
||||
|
||||
---
|
||||
|
||||
## 灵感来源(Inspired by)
|
||||
|
||||
- **Antimatter Dimensions** — 数值膨胀美学 + 终端 UI
|
||||
- **Universal Paperclips** — 叙事驱动的放置游戏(Frank Lantz)
|
||||
- **A Dark Room** — 极简文字放置游戏(Michael Townsend)
|
||||
- **Melvor Idle** — 离线收益 + 多技能树
|
||||
- **Outer Wilds** — 知识解锁式考古
|
||||
- **Cookie Clicker** — Orteil 的放置游戏范式定义
|
||||
|
||||
---
|
||||
|
||||
## 健康游戏设计声明
|
||||
|
||||
放置游戏天然具有"低投入高回报"的成瘾性,可能对学业/工作压力大的青年造成时间碎片化风险。回响星核主动采取以下健康设计:
|
||||
|
||||
- ✅ **离线收益上限 8h**——防止 24h 挂机
|
||||
- ✅ **工程完成需手动领取**——避免"打开就有奖励"的条件反射
|
||||
- ✅ **无广告、无内购**——纯粹的单机体验
|
||||
- 🔜 **v0.17 计划**:每日软上限 + "星核建议休眠"提醒(每 1 小时主动提示休息)
|
||||
|
||||
参考学术研究:
|
||||
- Frank Lantz GDC 2018 "This Is Your Brain on Games"
|
||||
- Reuters 2025 "Cozy video games can quell stress and anxiety"
|
||||
- DOI:10.3389/fpsyg.2026.1774030 TikTok 时代学术倦怠研究
|
||||
|
||||
---
|
||||
|
||||
© Super_Z · 持续开发中
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
Task ID: 2-b
|
||||
Agent: full-stack-developer
|
||||
Task: v0.16 完善 — loreLayer 已读标记 + idleProjectHistory 详情展开 + 移动端 375px 适配审计
|
||||
|
||||
Work Log:
|
||||
- 必读上下文:worklog v0.15-release 段 + Codex.tsx(v0.15 loreLayers 分层渲染)+ StatsPanel.tsx(v0.15 抽出独立组件)+ types.ts GameState + config.ts FRAGMENTS/loreLayers/isLoreLayerUnlocked + gameStore.ts(actions + persist migrate)+ IdleOperationsPanel.tsx + BeaconPanel.tsx + ExpeditionPanel.tsx
|
||||
- 子任务 1:loreLayer 已读标记
|
||||
- 新建 `src/lib/game/format.ts` 导出 `formatRelativeTime(ts)` 工具函数(<60s "刚刚" / <60m "X 分钟前" / <24h "X 小时前" / <7d "X 天前" / 否则具体日期)
|
||||
- `types.ts`:GameState 接口新增 `readLoreLayers: string[]`(位置:v0.14 idle 字段之后、元字段之前,注释标 v0.16 loreLayer 已读)
|
||||
- `engine.ts`:
|
||||
* `createInitialState` 末尾加 `readLoreLayers: []`(在 idleStats 之后)
|
||||
* `performPrestige` 返回对象加 `readLoreLayers: Array.isArray(state.readLoreLayers) ? state.readLoreLayers : []`(跨周目永久保留,与 idle 永久字段同一区域)
|
||||
- `gameStore.ts`:
|
||||
* GameActions 接口加 `markLoreLayerRead: (fragmentId: string, layer: 1 | 2) => void`(在 tickIdleProjects 之后)
|
||||
* 实现该 action:构造 key `${fragmentId}_L${layer}`,已存在则 no-op,否则 set 新数组
|
||||
* persist migrate 函数末尾加 `readLoreLayers: Array.isArray(s.readLoreLayers) ? s.readLoreLayers : []`(不改 version,仍是 1)
|
||||
- `Codex.tsx`:
|
||||
* useShallow 订阅新增 `readLoreLayers` 与 `markLoreLayerRead`
|
||||
* LoreLayerBlock 接收 `isRead` + `onMarkRead` props:
|
||||
- 已读:层标题旁加 muted "✓ 已读" 标记 + 边框从 amber/fuchsia 高亮降级为 `border-white/10`
|
||||
- 未读但已解锁:保持 v0.15 高亮 + 标题旁加 `animate-pulse` 脉冲小圆点 + "新" 字提示
|
||||
- 文本区域包成 `<button>` 元素,点击调用 `onMarkRead`(自然交互,不另设按钮)
|
||||
- 未读时下方加一行 muted "▸ 点击文本标记已读" 引导文字
|
||||
* 列表卡片标题旁小圆点:未读时 `animate-pulse` + glow,已读时 `bg-*-400/30` 静态
|
||||
* 顶部统计从单行"深层叙事 X/Y 已解锁"扩展为两行(移动端 stack / 桌面并排):深层叙事 X/Y 已解锁 + 已读 A/B(A=已读数,B=已解锁层数)
|
||||
* 弹层 padding 从 `p-5` 改 `p-4 sm:p-5`,标题 `text-lg` 改 `text-base sm:text-lg`,外层 backdrop padding 从 `p-4` 改 `p-3 sm:p-4`(375px 适配)
|
||||
|
||||
- 子任务 2:idleProjectHistory 详情展开
|
||||
- `StatsPanel.tsx`:
|
||||
* useShallow 订阅新增 `idleProjectHistory` 数组与 `idleProjectSlots`(用于详情中潜在"进行中"状态参考)
|
||||
* 删除原"放置工程历史 N 条"的简单行,改为 `col-span-2` 可展开按钮:
|
||||
- 左侧 label + "(最近 5 条可展开)" 提示(仅 historyLen > 0 时显示)
|
||||
- 右侧 "N 条" + ChevronDown/ChevronUp 图标(lucide-react)
|
||||
* 展开后渲染 `col-span-2` 卡片:
|
||||
- 空历史 → "暂无放置工程完成记录 · 前往「放置」标签派遣工程"
|
||||
- 非空 → 倒序最近 5 条:每行 = 工程图标 + 工程名(truncate)+ 相对时间(formatRelativeTime)
|
||||
- 最近一条用 `bg-fuchsia-500/5` 高亮
|
||||
- 历史超 5 条时底部加 "仅展示最近 5 条 · 共 N 条历史" 提示
|
||||
* 展开状态用本地 `useState<boolean>`
|
||||
* useMemo 派生 `recentHistory`(避免每次渲染重切)
|
||||
|
||||
- 子任务 3:移动端 375px 适配审计
|
||||
- 审计范围:Codex / IdleOperationsPanel / StatsPanel / BeaconPanel / ExpeditionPanel
|
||||
- 修复点:
|
||||
* `IdleOperationsPanel.tsx`:所有 `text-[9px]` → `text-[10px]`(11 处,符合"移动端字号最小 10px"规范)
|
||||
* `BeaconPanel.tsx`:所有 `text-[9px]` → `text-[10px]`(7 处);信标链底部统计行 `flex items-center justify-between gap-2` → `flex flex-wrap items-center justify-between gap-x-2 gap-y-1`(防 3 组数据在 375px 溢出)
|
||||
* `Codex.tsx`:弹层 padding + 标题字号响应式(见子任务 1);顶部统计区 `flex flex-col sm:flex-row`(移动端纵向堆叠)
|
||||
* `StatsPanel.tsx`:detail 区 `text-[9px]` → `text-[10px]`(2 处)
|
||||
- 审计通过(无需改):
|
||||
* `ExpeditionPanel.tsx`:节点路径已用 `overflow-x-auto` 自适应窄屏;事件卡 p-3 padding 合理;按钮组 `flex-1` + abort 收起;能量条文字 `text-[10px]` 已达标;路径节点 `h-9 w-9` 36px 足够小且可横向滚动
|
||||
* `BeaconPanel.tsx`:4 个里程碑节点 `w-12 h-12`(48px)× 4 = 192px + flex-1 分布,375px 下可容纳;日/周挑战卡 padding p-3 合理;周挑战+信标链已用 `grid-cols-1 lg:grid-cols-2` 移动单列
|
||||
* `IdleOperationsPanel.tsx`:4 个 section 已用 `grid-cols-1 sm:grid-cols-N` / `grid-cols-2 sm:grid-cols-3` 响应式降级;DispatchCard 槽位按钮 `h-5 w-7` × 3 在单列布局下不溢出
|
||||
* `Codex.tsx` 列表 `grid-cols-2 gap-2`:每卡片约 165px 宽,aspect-[4/3] + p-2 + text-[11px]/text-[9px](已升 text-[10px])在 375px 下可读
|
||||
|
||||
- 严格 4 色全息:emerald / rose / amber / fuchsia,未引入 blue / indigo
|
||||
- useShallow:所有 store 订阅用 useShallow(Codex / StatsPanel 均已用)
|
||||
- 持久化兼容:persist version 仍为 1,新字段 `readLoreLayers` 靠 migrate 兜底,旧存档加载时自动补 `[]`
|
||||
- 与子代理 A 文件边界:types.ts/engine.ts/gameStore.ts 三个共享文件,A 的 `triggeredMilestones` / `pendingMilestone` / `dismissMilestone` 字段已落地,与本子代理的 `readLoreLayers` / `markLoreLayerRead` 完全无冲突(不同字段、不同 action、不同 migrate 行)
|
||||
|
||||
Stage Summary:
|
||||
- ✅ 子任务 1:loreLayer 已读标记完整实现
|
||||
- 类型 + store + UI 三层联动;点击层文本即可标记已读
|
||||
- 视觉区分:已读 = muted "✓ 已读" + 边框降级为 border-white/10;未读 = 高亮 + 脉冲小圆点 + "新" 字
|
||||
- 顶部统计新增"已读 A/B"行(A=已读数 / B=已解锁层数)
|
||||
- 跨周目永久保留(performPrestige 不重置)
|
||||
- ✅ 子任务 2:idleProjectHistory 详情展开完整实现
|
||||
- "放置工程历史" 行可点击展开/收起
|
||||
- 展开后显示最近 5 条倒序历史(图标 + 工程名 + 相对时间)
|
||||
- 空态友好提示,超 5 条底部加"仅展示最近 5 条"说明
|
||||
- ✅ 子任务 3:移动端 375px 适配审计完成
|
||||
- 审 5 个组件,修 4 个(Codex / IdleOps / StatsPanel / Beacon),ExpeditionPanel 无需改
|
||||
- 主要修复:text-[9px] → text-[10px](共 20 处,符合"移动端字号最小 10px"规范)
|
||||
- 信标链底部统计加 flex-wrap 防 3 组数据溢出
|
||||
- Codex 弹层 padding + 标题字号响应式
|
||||
- Codex 顶部统计区移动端纵向堆叠
|
||||
- ✅ 新建文件:`/home/z/my-project/src/lib/game/format.ts`
|
||||
- ✅ 修改文件清单(绝对路径):
|
||||
- `/home/z/my-project/src/lib/game/types.ts`(+readLoreLayers 字段)
|
||||
- `/home/z/my-project/src/lib/game/engine.ts`(createInitialState 默认 + performPrestige 保留)
|
||||
- `/home/z/my-project/src/store/gameStore.ts`(GameActions 签名 + action 实现 + migrate 补全)
|
||||
- `/home/z/my-project/src/components/game/Codex.tsx`(已读标记 + 点击逻辑 + 顶部统计 + 弹层响应式)
|
||||
- `/home/z/my-project/src/components/game/StatsPanel.tsx`(idleHistory 展开 + 相对时间 + 字号升级)
|
||||
- `/home/z/my-project/src/components/game/IdleOperationsPanel.tsx`(字号 9→10px 升级)
|
||||
- `/home/z/my-project/src/components/game/BeaconPanel.tsx`(字号 9→10px 升级 + 信标链底部 flex-wrap)
|
||||
- ✅ bun run lint 零错误零警告
|
||||
- ✅ HTTP 200,dev.log 无运行时错误(compile 11ms / render 33ms)
|
||||
- ✅ 与子代理 A 文件边界完全无冲突(不同字段/action/migrate 行)
|
||||
- ⚠️ 未做 agent-browser 端到端验证(按工单由主控完成)
|
||||
@@ -0,0 +1,52 @@
|
||||
# Task 6: 开发 Canvas 2D 深空巡航实时玩法
|
||||
|
||||
## Agent: full-stack-developer
|
||||
|
||||
## 完成状态: ✅ 全部完成
|
||||
|
||||
## 创建的文件
|
||||
1. `/home/z/my-project/src/lib/game/cruise.ts` — 逻辑层(~520 行纯 TS)
|
||||
2. `/home/z/my-project/src/components/game/CruiseMode.tsx` — Canvas 2D 渲染层(~830 行)
|
||||
|
||||
## 修改的文件
|
||||
1. `/home/z/my-project/src/app/page.tsx` — 新增巡航入口按钮 + CruiseMode 渲染
|
||||
|
||||
## 关键实现细节
|
||||
|
||||
### cruise.ts 逻辑层
|
||||
- **类型系统**:CruiseEntity 联合类型(8 种实体:ship/asteroid/storm/crystal/insight/beacon/stargate/particle),完整 CruiseState/Phase/RunResult/Stats/Input
|
||||
- **关卡生成**:mulberry32 + FNV-1a 种子化 RNG;陨石(8+level×2)、风暴(1+level/2)、晶体(5+level)、洞见(2+level/3)、信标(1+level/4)、星门;时长 60-90s
|
||||
- **物理**:飞船 8 方向加速度+摩擦+限速+边界反弹;陨石碰撞扣盾+击退+600ms 无敌;风暴持续 DPS;收集物吸引半径 90px 自动吸入
|
||||
- **奖励**:crystals=碎片×(8+lvl×2)、insights=洞见×(2+lvl×0.5)、contact=信标×(1.5+lvl×0.3);通关×1.5、失败×0.5
|
||||
- **localStorage**:独立 key `echo-nexus-cruise-v1`,记录最高分/通关数/最高关卡/累计奖励/最近 20 局
|
||||
|
||||
### CruiseMode.tsx 渲染层
|
||||
- **全屏 Canvas**:fixed inset-0 z-50,DPR cap 2,resize 监听
|
||||
- **3 层视差星空**:远/中/近 100/60/30 星,独立漂移+飞船速度视差+闪烁
|
||||
- **实体辉光绘制**:全部使用 ctx.shadowBlur;飞船三角形+尾焰渐变;陨石不规则多边形;风暴 5 层云团+闪电;晶体菱形+脉冲环;洞见球体+漩涡;信标光束+旋转环;星门 4 层旋涡+吸入粒子
|
||||
- **粒子系统**:尾焰/收集/碰撞/烟花,上限 200
|
||||
- **屏幕震动**:受击 280ms 衰减抖动
|
||||
- **低护盾警告**:shield<30% 屏幕边缘 rose 脉冲边框
|
||||
- **HUD**:HTML 叠层(非 canvas),glass+backdrop-blur;护盾/能量/分数/用时/收集计数;80ms 节流更新
|
||||
- **控制**:桌面 WASD/方向键;移动端 floating 虚拟摇杆;P 暂停;Esc 退出;失焦自动暂停
|
||||
- **奖励同步**:结算时 grantCruiseReward + recordRun,rewardGrantedRef 防重
|
||||
|
||||
### page.tsx 集成
|
||||
- header 按钮区新增「巡航」按钮(Navigation 图标,amber 主题)
|
||||
- 新增 cruiseOpen state
|
||||
- 组件末尾渲染 {cruiseOpen && <CruiseMode onClose={...} />}
|
||||
- 未破坏现有 7 标签页和其他功能
|
||||
|
||||
## QA 验证结果
|
||||
- ✅ lint 零错误
|
||||
- ✅ 巡航按钮出现在顶部 header
|
||||
- ✅ 点击进入全屏 Canvas,ready 界面完整
|
||||
- ✅ WASD 控制飞船移动,按 W 直冲星门通关
|
||||
- ✅ 收集 2 晶体 → 奖励 +30(2×10×1.5=30 计算正确)
|
||||
- ✅ localStorage 正确记录 highScore/totalWins/bestLevel
|
||||
- ✅ 奖励同步到 gameStore
|
||||
- ✅ 退出返回主界面,7 标签页完好
|
||||
- ✅ 移动端 viewport 测试通过
|
||||
|
||||
## 色彩遵循
|
||||
emerald(#34d399) / rose(#fb7185) / amber(#fbbf24) / fuchsia(#e879f9) 四色全息色谱,零蓝色/靛色
|
||||
@@ -0,0 +1,103 @@
|
||||
# Task 7: 角色属性系统(探索力/智慧/勇气/灵感)
|
||||
|
||||
## Agent: full-stack-developer
|
||||
|
||||
## 完成状态: ✅ 全部完成
|
||||
|
||||
## 创建的文件
|
||||
1. `/home/z/my-project/src/lib/game/attributes.ts` — 属性逻辑层(~330 行)
|
||||
2. `/home/z/my-project/src/components/game/AttributesPanel.tsx` — 属性 UI 面板(~330 行)
|
||||
|
||||
## 修改的文件
|
||||
1. `/home/z/my-project/src/lib/game/types.ts` — GameState 新增 attributes/attributeProgress/pendingAttrPoints
|
||||
2. `/home/z/my-project/src/lib/game/config.ts` — INITIAL_STATE 补全新字段默认值
|
||||
3. `/home/z/my-project/src/lib/game/engine.ts` — recomputeStats 聚合属性加成;performPrestige 发放属性点
|
||||
4. `/home/z/my-project/src/store/gameStore.ts` — 新增 allocateAttribute/gainAttributeExp action;pulse/clickNode/autoDecodeTick/resolveCurrentNode/grantCruiseReward/tickTide/init 接入属性逻辑;旧存档兼容
|
||||
5. `/home/z/my-project/src/lib/game/achievements.ts` — 新增 2 项属性成就
|
||||
6. `/home/z/my-project/src/app/page.tsx` — 新增第 8 个「角色」标签页 + grid-cols-7→8 + 红点提示 + 统计面板新增属性行 + 版本号 v0.6→v0.7
|
||||
|
||||
## 关键实现细节
|
||||
|
||||
### attributes.ts 逻辑层
|
||||
- **类型系统**:AttributeKey(4 个键)/ CharacterAttributes(0-100 数值)/ AttributeProgressEntry(exp + level)
|
||||
- **加成公式**:
|
||||
- 0-50 线性区:每点 +0.5% 加成(0..50 → 0..25%)
|
||||
- 50-100 递减区:每点 +0.2% 加成(50..100 → 25..35%)
|
||||
- 超过 100 仍按 100 计算加成(软上限)
|
||||
- **getAttributeBonus(attr)** 单属性加成百分比
|
||||
- **getAllBonuses(attrs)** 返回 12 个修饰器:探险力倍率/巡航速度/解码步数/洞见倍率/自动解码周期/探险生命/BOSS 胜率/巡航护盾/接触率/星潮触发/脉冲连击/产能加成
|
||||
- **levelUpCheck(progress)** 自动跨多级升级,安全上限 200 次循环
|
||||
- **expRequiredForLevel(level) = 10 × level**(最低 10)
|
||||
- **computePrestigeAttrPoints(ascensions) = ascensions × 2 + 1**(飞升前次数计算)
|
||||
- **migrateAttributes(state)** 旧存档兼容:补全缺失字段、夹紧越界值、同步 level 与 attributes
|
||||
|
||||
### engine.ts 改造
|
||||
- recomputeStats 末尾追加 `getAllBonuses(state.attributes ?? {})` 聚合:
|
||||
- `crystalsPerSec *= am.crystalsPerSecMult`
|
||||
- `insightMult += am.insightMultAdd`
|
||||
- `contactRateMult *= am.contactRateMult`
|
||||
- `decodeStepsBonus += am.decodeStepsBonus`
|
||||
- performPrestige:
|
||||
- 保留 attributes 数值(跨周目永久)
|
||||
- 清空 attributeProgress(新周目重新累积经验)
|
||||
- pendingAttrPoints += computePrestigeAttrPoints(ascensions) = ascensions × 2 + 1
|
||||
- createInitialState:每次返回全新 attributes/attributeProgress 对象,避免引用共享
|
||||
|
||||
### gameStore.ts 集成(核心)
|
||||
- **pulse**:连击 ≥3 给灵感经验(expGain = 1 + floor(combo/2));脉冲威力乘以灵感连击加成
|
||||
- **clickNode** 完成:给智慧经验(expGain = puzzle.tier × 2)
|
||||
- **autoDecodeTick**:自动解码也给智慧经验 +1;自动解码周期受智慧 am.autoDecodeIntervalMult 影响
|
||||
- **startExpedition**:探险力乘 am.expeditionPowerMult;探险生命加 am.expeditionHpBonus
|
||||
- **resolveCurrentNode**:
|
||||
- BOSS 节点用包装 RNG 提升 +am.bossWinRateBonus 胜率(单次 rng 调用,B% 概率返回 0,其余返回 r-B 保持均匀分布)
|
||||
- 战斗胜利给勇气+探索力经验(中途战斗 +2/+1,BOSS 击破 +8/+6,探险胜利 +4)
|
||||
- **grantCruiseReward**:按总奖励量缩放给探索力+勇气经验(expBase = max(2, totalReward/30))
|
||||
- **tickTide**:灵感 am.tideTriggerBonus 缩短星潮间隙(gap × (1 - bonus)),上限 30%
|
||||
- **doPrestige**:performPrestige 后用 next.attributes 重算 stats
|
||||
- **allocateAttribute(attr, points=1)**:分配属性点,同步 attributeProgress[attr].level
|
||||
- **gainAttributeExp(attr, amount)**:通用经验获取(自动升级)
|
||||
- **init()**:调用 migrateAttributes 补全旧存档字段,并传 attributes 到 syncStats
|
||||
- 所有 syncStats 调用点(10+ 处)都补充 `attributes: ...` 参数
|
||||
|
||||
### AttributesPanel.tsx UI
|
||||
- 四维属性卡片网格(小屏 2×2,大屏 1×4):
|
||||
- 图标(Compass/Brain/Swords/Sparkles)+ 中文名 + 英文名 + Lv.{value} badge
|
||||
- 数值 /100 + 加成百分比
|
||||
- 经验进度条(gradient + glow)+ "递减区"标记
|
||||
- 加成影响列表(3 条)
|
||||
- 「+分配」按钮(pendingAttrPoints > 0 时可点,hover scale 105)
|
||||
- 顶部:标题 + 待分配点数 badge(pendingAttrPoints > 0 时 echo-pending-pulse 动画)
|
||||
- 底部:总等级 + 总加成概览 + 12 个修饰器明细行
|
||||
- 配色:4 色全息(emerald/fuchsia/amber/rose),辉光边框 + 顶角光晕装饰
|
||||
- 完全响应式(2 列 → 4 列),overflow-y-auto + 自定义 scrollbar
|
||||
|
||||
### page.tsx 集成
|
||||
- 新增 `User` 图标导入
|
||||
- 新增 `pendingAttrPoints` store selector
|
||||
- TabsList: grid-cols-7 → grid-cols-8
|
||||
- 新增第 8 个 TabsTrigger「角色」(value="attributes"),主题色用 emerald→fuchsia→rose 渐变
|
||||
- pendingAttrPoints > 0 时显示 rose 红点
|
||||
- 新增 TabsContent 渲染 AttributesPanel
|
||||
- StatsPanel 新增 5 行属性行(探索力/智慧/勇气/灵感/待分配属性点)
|
||||
- 版本号 v0.6 → v0.7
|
||||
|
||||
### achievements.ts 新增
|
||||
- `ach_attr_total_50`(四维觉醒):四维属性总和 ≥ 50 → 产能+6%/洞见+6%
|
||||
- `ach_attr_max_100`(维度精通):任一属性 ≥ 100 → 产能+12%/洞见+10%
|
||||
|
||||
## QA 验证结果
|
||||
- ✅ `bun run lint` 零错误
|
||||
- ✅ dev 服务器 HTTP 200
|
||||
- ✅ 编译 < 250ms(177ms 实测)
|
||||
- ✅ 7 标签页 + 巡航按钮完好保留,新增第 8 个「角色」标签页
|
||||
- ✅ data-tut 锚点(tab-expedition/tab-tech/prestige-btn/crystal-orb/decode-panel)保留
|
||||
- ✅ 色彩规范:严格 emerald/fuchsia/amber/rose 四色全息,零蓝色/靛色
|
||||
- ✅ 旧存档兼容:migrateAttributes 补全 attributes/attributeProgress/pendingAttrPoints 字段
|
||||
- ✅ 飞升后 pendingAttrPoints = ascensions × 2 + 1(飞升前次数)
|
||||
- ✅ pulse/clickNode/resolveCurrentNode/grantCruiseReward/autoDecodeTick 均接入属性经验获取
|
||||
|
||||
## 注意事项
|
||||
- BOSS 胜率 RNG 包装:仅在 boss 节点生效,单次 rng 调用保持均匀分布;+am.bossWinRateBonus 上限 +30%
|
||||
- 灵感星潮触发:通过缩短 gap 间接提升触发频率(上限 30%)
|
||||
- 属性加成与现有所有系统(技术树/蓝图/成就/星图/星潮)叠加,不冲突
|
||||
- migrateAttributes 同步 attributeProgress[attr].level 与 attributes[attr] 数值,避免漂移
|
||||
@@ -0,0 +1,141 @@
|
||||
# Task 9-b · full-stack-developer · 信标系统扩展(周挑战 + 信标链)
|
||||
|
||||
> 本文件为本 agent 在 Task 9-b 的工作记录,供后续 agent 查阅。
|
||||
|
||||
## 任务概述
|
||||
|
||||
为「回响星核 / Echo Nexus」v0.8 扩展深空信标系统,新增两大功能:
|
||||
1. **周挑战(Weekly Challenge)** — 每周一 UTC 0 点刷新,目标更大、奖励更好,与日挑战并行
|
||||
2. **信标链(Beacon Chain)** — 连续完成日挑战形成"链",达成里程碑(3/7/14/30 天)领取递增大奖,含 grace 续命机制
|
||||
|
||||
## 阅读的前置工作
|
||||
|
||||
- `/home/z/my-project/worklog.md`(v0.8 项目状态,10 大系统,四色全息规范)
|
||||
- `/home/z/my-project/src/lib/game/beacon.ts`(v0.5 原版 358 行:每日挑战 + 本地排行榜)
|
||||
- `/home/z/my-project/src/components/game/BeaconPanel.tsx`(原版 301 行)
|
||||
- `/home/z/my-project/src/store/gameStore.ts` 第 173 行 `trackBeacon` 函数 + 7 处调用点(pulse/clickNode/autoDecodeTick/resolveCurrentNode 等)
|
||||
|
||||
## 实现细节
|
||||
|
||||
### 1. beacon.ts 扩展(358 → 873 行)
|
||||
|
||||
**周挑战(WEEKLY CHALLENGE)section**:
|
||||
- `BeaconWeeklyChallenge` 接口 + `BeaconWeeklyProgress` 接口
|
||||
- `getWeekKey(now)`:ISO 8601 周键(YYYY-Www,周一为起点,含首个周四的周为第一周)
|
||||
- `weekKeyToSeed`:FNV-1a 哈希
|
||||
- `generateWeeklyChallenge(now)`:基于 weekKey 种子确定性生成
|
||||
- 难度加权:anomaly 60% / singular 40%
|
||||
- mult = 3 + floor(rng() * 3) → 3-5 倍
|
||||
- goal 范围:decode 18-75 / expedition 3-15 / pulse 60-245 / boss 3-12 / insight 120-595
|
||||
- `loadWeeklyProgress` / `saveWeeklyProgress` / `addWeeklyProgress` / `claimWeeklyReward`
|
||||
- `msUntilNextWeek(now)`
|
||||
- `BEACON_WEEKLY_KEY = "echo-nexus-beacon-weekly-v1"`
|
||||
|
||||
**信标链(BEACON CHAIN)section**:
|
||||
- `BeaconChainState` 接口(lastCompletedDateKey / currentStreak / longestStreak / totalCompletions / graceUsed / milestonesClaimed)
|
||||
- `BEACON_CHAIN_KEY = "echo-nexus-beacon-chain-v1"`
|
||||
- `BEACON_CHAIN_MILESTONES = [3, 7, 14, 30] as const`
|
||||
- `BEACON_CHAIN_REWARDS`:4 个里程碑
|
||||
- 3 天:+50 洞见 / +5 接触 / "三日谐振"
|
||||
- 7 天:+120 洞见 / +12 接触 / "七日回响"
|
||||
- 14 天:+280 洞见 / +28 接触 / "半月星潮"
|
||||
- 30 天:+680 洞见 / +68 接触 / "满月飞升"
|
||||
- `loadChainState` / `saveChainState`(每次返回新对象避免引用共享 bug)
|
||||
- `recordChainCompletion(dateKey)` 核心逻辑:
|
||||
- 同日重复完成 → 忽略
|
||||
- 次日 → streak++
|
||||
- 隔一天 miss 且 graceUsed<1 → 续命 streak++ graceUsed++
|
||||
- 其他 → 断链 streak=1 graceUsed=0
|
||||
- 返回 `{ state, newMilestones }`
|
||||
- `claimChainMilestone(milestone)` / `getNextMilestone(streak)` / `getChainProgress(streak)`
|
||||
- `dateKeyToTimestamp` / `dateKeyDiffDays` 工具
|
||||
|
||||
`BeaconScoreEntry` 新增可选 `isWeekly?: boolean` 字段,向后兼容。
|
||||
|
||||
### 2. gameStore.ts 集成
|
||||
|
||||
- import 扩展:新增 generateWeeklyChallenge / loadWeeklyProgress / loadChainState / addWeeklyProgress / recordChainCompletion / claimWeeklyReward / claimChainMilestone / getTodayKey + 类型
|
||||
- `trackBeacon(type, delta)` 返回值从 `boolean` 升级为 `{ dailyJustCompleted, weeklyJustCompleted, newChainMilestones }`:
|
||||
- 同时更新日挑战进度 + 周挑战进度
|
||||
- 日挑战刚完成时调用 `recordChainCompletion(getTodayKey())`
|
||||
- 所有 7 处调用点原本忽略返回值,向后兼容
|
||||
- 新增 action `claimWeeklyBeacon()`:调用 claimWeeklyReward → 发放奖励到 state
|
||||
- 新增 action `claimChainReward(milestone)`:前置校验 → claimChainMilestone → 发放奖励
|
||||
- GameActions 接口同步扩展
|
||||
|
||||
### 3. BeaconPanel.tsx 重写(301 → 638 行)
|
||||
|
||||
- 头部 + 每日挑战卡片(v0.5 保留)+ 难度色按钮主题
|
||||
- **周挑战区块**(fuchsia 主题):标题 + weekKey + 倒计时 + 卡片(标签/标题/描述/进度/奖励/领取按钮 emerald)+ weekly-glow 动画
|
||||
- **信标链区块**(amber→rose 渐变):
|
||||
- 标题"信标链 · CHAIN" + 大字号 streak(chain-streak-text 流动渐变动画)
|
||||
- 今日完成状态徽章
|
||||
- 4 个里程碑节点(w-12 h-12 圆形):
|
||||
- claimed: emerald 实心 + ✓
|
||||
- reachable: rose 脉冲动画 + "领取"按钮
|
||||
- inProgress (next milestone): amber 半亮
|
||||
- 未到达: muted 灰
|
||||
- 节点间连线:背景灰 + 已达成部分 amber→rose 渐变 + 辉光,基于 prev→next 插值定位
|
||||
- 进度条 + 底部统计(最长链/累计完成/续命状态)
|
||||
- 桌面端 lg:grid-cols-2 让周挑战 + 信标链并排,移动端单列
|
||||
- 排行榜区分日/周:周挑战 entry 显示 "WEEK" 徽章 + fuchsia 高亮背景
|
||||
- 4 个新 CSS 动画:weekly-glow / chain-milestone-pulse / chain-streak-flux
|
||||
|
||||
## QA 验证结果
|
||||
|
||||
### 1. lint
|
||||
- `bun run lint` 零错误(每次修改后均验证)
|
||||
|
||||
### 2. dev 服务器
|
||||
- dev.log 全程无错误,所有编译 < 300ms,HTTP 200
|
||||
|
||||
### 3. 信标链逻辑测试(bun 直接运行 TS,5 个场景全 PASS)
|
||||
1. ✅ 昨日 streak=1 → 今日完成 → streak=2(normal increment)
|
||||
2. ✅ 同日重复完成 → 忽略
|
||||
3. ✅ 明日完成 → streak=3,无需 grace
|
||||
4. ✅ 隔一天 miss(lastCompleted + 2 天)→ streak=4,graceUsed=1(续命触发)
|
||||
5. ✅ 再次 miss(grace 已用)→ 断链 streak=1,graceUsed=0
|
||||
|
||||
### 4. UI 集成测试(agent-browser)
|
||||
- localStorage 模拟设置 chain state(lastCompletedDateKey=yesterday, currentStreak=1)→ reload → UI 正确显示 streak=1
|
||||
- 设置 streak=3 + milestonesClaimed=[] → milestone 3 节点显示 rose 脉冲动画 + "领取"按钮
|
||||
- 点击"领取"→ milestonesClaimed=[3] + Toast "✦ 三日谐振 已领取 +50 洞见 · +5.0 接触" + 按钮消失 ✅
|
||||
|
||||
### 5. VLM 视觉评分(目标 ≥7/10)
|
||||
- 首屏截图:**8/10**(四色一致、布局合理、信标链清晰)
|
||||
- 里程碑可领取状态:**8/10**(amber→rose 渐变醒目、里程碑层次分明、fuchsia 主题清晰)
|
||||
- 重置后干净状态:**8/10**(WEEKLY 字体对比度可优化,但整体可读性强)
|
||||
|
||||
## 修改的文件
|
||||
|
||||
1. `src/lib/game/beacon.ts` — 358 → 873 行(+515 行)
|
||||
2. `src/store/gameStore.ts` — trackBeacon 升级 + 2 个新 action(+~80 行)
|
||||
3. `src/components/game/BeaconPanel.tsx` — 301 → 638 行(+337 行,重写)
|
||||
|
||||
## 不破坏的现有功能
|
||||
|
||||
- 日挑战进度追踪与领奖流程
|
||||
- 本地排行榜 Top20
|
||||
- 现有 5 种挑战类型 + 3 档难度
|
||||
- 现有 localStorage keys(echo-nexus-beacon-lb-v1 / echo-nexus-beacon-prog-v1)
|
||||
|
||||
## 新增的 localStorage keys
|
||||
|
||||
- `echo-nexus-beacon-weekly-v1`(周挑战进度)
|
||||
- `echo-nexus-beacon-chain-v1`(信标链状态)
|
||||
|
||||
## 四色全息规范遵循
|
||||
|
||||
- 周挑战主题:**fuchsia** (#e879f9)
|
||||
- 信标链主题:**amber → rose** 渐变 (#fbbf24 → #fb7185)
|
||||
- 领取按钮:**emerald** (#34d399)
|
||||
- 难度色:routine emerald / anomaly amber / singular rose
|
||||
- **零蓝色/靛色违规**
|
||||
|
||||
## 截图资产
|
||||
|
||||
- `/home/z/my-project/agent-ctx/beacon-panel-v0.8.png` — 初次进入信标页
|
||||
- `/home/z/my-project/agent-ctx/beacon-chain-streak1.png` — chain streak=1 状态
|
||||
- `/home/z/my-project/agent-ctx/beacon-chain-milestone3.png` — chain streak=3 milestone 可领取
|
||||
- `/home/z/my-project/agent-ctx/beacon-milestone3-claimable.png` — milestone 3 领取前
|
||||
- `/home/z/my-project/agent-ctx/beacon-final-fresh.png` — 重置后干净状态
|
||||
|
After Width: | Height: | Size: 299 KiB |
|
After Width: | Height: | Size: 380 KiB |
|
After Width: | Height: | Size: 386 KiB |
|
After Width: | Height: | Size: 383 KiB |
|
After Width: | Height: | Size: 387 KiB |
@@ -0,0 +1,139 @@
|
||||
# Task ID: v0.14-reimpl
|
||||
# Agent: full-stack-developer
|
||||
# Task: 放置系统 (Idle Operations) — 采矿无人机舰队 + 6放置工程 + 永久产能加成 + idle徽章
|
||||
|
||||
## Work Log
|
||||
|
||||
### 1. 类型扩展 (`src/lib/game/types.ts`)
|
||||
- 新增 `IdleProjectDef` 接口(id/name/desc/durationSec/reward/minAscensions/minCrystalsPerSec/icon/color/order)
|
||||
- 新增 `IdleProjectSlot` 接口(projectId/startedAt/finishesAt/remainingSec/completed)
|
||||
- 在 `GameState` 接口 `pendingAttrPoints` 后新增 4 个字段:
|
||||
- `idleProjectSlots: (IdleProjectSlot | null)[]`
|
||||
- `idleProjectHistory: { projectId: string; finishedAt: number }[]`
|
||||
- `idlePermanentBonus: number`
|
||||
- `idleStats: { projectsCompleted: number; crystalsFromIdle: number }`
|
||||
|
||||
### 2. 新建 `src/lib/game/idle.ts` (~290 行)
|
||||
- 定义 6 个放置工程(按 order 排序):
|
||||
| id | name | duration | reward | icon | color | unlock |
|
||||
|----|------|----------|--------|------|-------|--------|
|
||||
| idle_scan | 深空勘探扫描 | 60s | insights +8 | 🛰️ | emerald | cps≥0.5 |
|
||||
| idle_refine | 晶体精炼阵列校准 | 180s | permBonus +0.3/s | ⚙️ | rose | cps≥1 |
|
||||
| idle_archive | 遗迹碎片整理 | 600s | randomFragment + insights +20 | 📜 | amber | asc≥1 OR cps≥5 |
|
||||
| idle_anchor | 维度锚点部署 | 1200s | energy +2 + contact +5 | ⚓ | fuchsia | asc≥1 |
|
||||
| idle_drones | 无人机群扩编 | 1800s | permBonus +2/s + crystals +500 | 🛸 | emerald | cps≥10 |
|
||||
| idle_resonance | 跨维度谐振标定 | 3600s | permBonus +8/s + contact +15 + insights +100 | 🌌 | fuchsia | asc≥2 |
|
||||
- 导出:`IDLE_PROJECTS`, `IDLE_SLOT_COUNT=3`, `getIdleProject`, `getUnlockedIdleProjects`, `getLockedIdleProjects`, `isIdleProjectUnlocked` (OR 逻辑:满足任一已设置条件即可解锁), `formatRemaining` (12s/3m 45s/1h 12m), `formatDuration`, `formatReward`, `deriveMinerFleet` (派生 min_1..min_5 采矿无人机,无任何采矿技术时返回休眠 min_1)
|
||||
- 导出颜色映射 `IDLE_COLOR_CLASSES` (text/border/bg/bgSoft/glow/ring/dot) + `COLOR_HEX` (用于 inline style,绕过 Tailwind 动态类限制)
|
||||
|
||||
### 3. 引擎集成 (`src/lib/game/engine.ts`)
|
||||
- `recomputeStats()` 末尾 `crystalsPerSec += state.idlePermanentBonus ?? 0` (放置永久加成叠加到产能)
|
||||
- `performPrestige()` 在返回对象中:
|
||||
- 清空 `idleProjectSlots: [null, null, null]` (飞升中断当前周目放置工程)
|
||||
- 保留 `idleProjectHistory`, `idlePermanentBonus`, `idleStats` (跨周目永久)
|
||||
- `recomputeStats` 调用传入 `idlePermanentBonus` 以确保飞升后立即应用永久加成
|
||||
- `createInitialState()` 新增 4 字段默认值
|
||||
|
||||
### 4. Store actions (`src/store/gameStore.ts`)
|
||||
- 新增 4 个 action:
|
||||
- `startIdleProject(slotIndex, projectId)`: 校验槽位空 + 项目已解锁 + 未在他处运行;写入 `{ projectId, startedAt, finishesAt, remainingSec, completed:false }`
|
||||
- `cancelIdleProject(slotIndex)`: 置槽位为 null
|
||||
- `claimIdleProject(slotIndex)`: 校验已完成 → 应用奖励(crystals 限 crystalCap,energy 限 energyMax,contact 限 100;permBonus 累加到 idlePermanentBonus + 重算 stats;randomFragment 从未解锁碎片中随机一个)→ 追加 history (slice -50) → idleStats.projectsCompleted++ → Toast 提示
|
||||
- `tickIdleProjects(now)`: 遍历非空非完成槽位,更新 remainingSec,归零时标记 completed
|
||||
- **persist migrate 函数**:persist 配置增加 `version: 1` + `migrate` 函数,自动补全 idleProjectSlots/idleProjectHistory/idlePermanentBonus/idleStats 4 字段,旧存档加载不崩溃
|
||||
- 全部 14 处 syncStats 调用更新为传入 `idlePermanentBonus: s.idlePermanentBonus ?? 0`,确保 recomputeStats 计算时纳入永久加成
|
||||
- `init()` 中也补全 4 个 idle 字段(defense in depth)
|
||||
|
||||
### 5. 主循环 (`src/hooks/useGameLoop.ts`)
|
||||
- 新增 `tickIdleProjects` 选择器
|
||||
- setInterval 回调中 `autoDecodeTick()` 后调用 `tickIdleProjects(now)`
|
||||
- visibilitychange 回调中也调用 `tickIdleProjects(now)`
|
||||
|
||||
### 6. 新建 `src/components/game/IdleOperationsPanel.tsx` (~490 行)
|
||||
- 主「放置」标签内容,4 个 section 在 `max-h-[520px] overflow-y-auto` 容器中:
|
||||
1. **放置收益概览**:3 stat tiles (放置产能 / 永久加成 / 完成工程) + 离线效率 progress bar + 累计放置晶体统计
|
||||
2. **采矿无人机舰队**:grid 展示 deriveMinerFleet(state),每张卡:emoji + name + Lv + output/s + 状态点(active=emerald ping 脉冲,dormant=灰)
|
||||
3. **放置工程槽位**:3 张槽位卡(空槽=虚线占位,运行中=大字号倒计时+自定义进度条+取消按钮,已完成=奖励预览+领取按钮带 glow 辉光)
|
||||
4. **可派遣工程**:2 列 grid 展示已解锁工程(icon+name+duration+desc+reward+3 个数字派遣按钮 1/2/3),下方列出未解锁工程及解锁条件
|
||||
- 使用 `useShallow` 订阅多个 store 字段
|
||||
- 本地 `now` state 每 1s 刷新倒计时
|
||||
- 严格 4 色全息(emerald/rose/amber/fuchsia),无蓝/靛
|
||||
- 自定义滚动条样式 (fuchsia 主题)
|
||||
|
||||
### 7. 新建 `src/components/game/IdleStatusBadge.tsx` (~100 行)
|
||||
- 紧凑徽章:脉冲点 (emerald 若 cps>0,否则灰) + "放置中 +X/s" (或 "休眠中")
|
||||
- Tooltip 悬停展示分解:基础产能 / 永久加成 / 运行中工程 / 待领取工程数
|
||||
- `h-8 px-2.5 text-[11px]`,点击切换到放置标签
|
||||
- `data-tut="idle-status-badge"` 锚点
|
||||
|
||||
### 8. 新建 `src/components/game/IdleProjectBar.tsx` (~90 行)
|
||||
- 晶体球下方的细长进度条
|
||||
- 显示最多 3 个运行/已完成工程为 mini pill:`[icon] name 12s ▓▓▓░░`
|
||||
- 完成时显示 "✓ 待领取" + glow 辉光
|
||||
- 无工程时返回 null
|
||||
- 本地 `now` state 每 1s 刷新倒计时
|
||||
- 进度条颜色用 inline style 控制(COLOR_HEX 映射)
|
||||
|
||||
### 9. 主页面集成 (`src/app/page.tsx`)
|
||||
- 引入 3 个新组件 + `Clock` 图标 + `getUnlockedIdleProjects`
|
||||
- 新增 `idleProjectSlots` / `createdAt` store 订阅
|
||||
- 新增 `activeTab` / `tabInited` state,controlled Tabs:`<Tabs value={activeTab} onValueChange={setActiveTab}>`
|
||||
- 挂载后 useEffect 一次性设置默认 tab:hasActiveExpedition → "expedition",hasPendingPerk → "constellation",否则 → "idle"
|
||||
- 版本号 v0.8 → v0.14
|
||||
- Header 在 StarTideIndicator 后加 IdleStatusBadge
|
||||
- 左侧 CrystalOrb 后加 IdleProjectBar
|
||||
- TabsList grid-cols-8 → grid-cols-9,新增 `value="idle"` 的 TabsTrigger 作为第一个 tab(Clock 图标,emerald 主题,待领取时显示数量红点)
|
||||
- 新增 TabsContent value="idle" 渲染 IdleOperationsPanel
|
||||
- 新增 idle 相关 goal 提示(高优先级):
|
||||
- 待领取 > 0 → "✦ 放置工程已完成 X 项,请前往「放置」标签领取奖励"
|
||||
- 全空 + 有解锁 + 时长 > 60s → "「放置」标签可派遣工程项目,离线自动产出"
|
||||
- StatsPanel 新增 3 行:放置永久加成 / 完成放置工程 / 放置产出晶体
|
||||
|
||||
### 10. 教程更新 (`src/lib/game/tutorial.ts`)
|
||||
- TUTORIAL_STEPS 在 decode 与 tech 之间插入新步骤:
|
||||
- id: "idle", target: "tab-idle", placement: "top"
|
||||
- 标题 "③ 放置工程 · 离线产出"
|
||||
- 介绍放置标签的工程派遣 + 顶部徽章 + 晶体球下方进度条
|
||||
- 原 tech/expedition/prestige 步骤编号顺延为 ④⑤⑥
|
||||
|
||||
## QA 验证
|
||||
|
||||
### Lint
|
||||
- `bun run lint` → 零错误零警告 ✅
|
||||
|
||||
### HTTP
|
||||
- `curl http://localhost:3000/` → 200 OK ✅
|
||||
|
||||
### Dev log
|
||||
- 全程无 runtime error,所有请求 200 OK,编译 < 250ms ✅
|
||||
|
||||
### agent-browser 烟雾测试
|
||||
1. ✅ 版本标签 "v0.14" 可见
|
||||
2. ✅ 默认 tab 是 "放置"(data-state="active" 在第一个 tab)
|
||||
3. ✅ 头部 idle 徽章可见("放置中 +0.4/s" 或 "休眠中")
|
||||
4. ✅ IdleOperationsPanel 4 个 section 全部可见(放置收益概览 / 采矿无人机舰队 / 放置工程槽位 / 可派遣工程 / 未解锁工程)
|
||||
5. ✅ 采矿无人机舰队在无采矿技术时显示休眠 min_1(emoji ⛏️ + 灰色状态点)
|
||||
6. ✅ 设置 insights=50 → buyTech('min_1') → crystalsPerSec 0.4→1.0 → 解锁 idle_scan + idle_refine
|
||||
7. ✅ 点击 idle_scan 派遣按钮 → 槽位 0 写入 { projectId: "idle_scan", finishesAt: now+60000 } → 倒计时正确递减
|
||||
8. ✅ 60s 后自动标记 completed=true → 槽位卡显示 "✓ 完成" + 领取按钮
|
||||
9. ✅ 点击领取 → Toast "✦ 工程奖励已领取 · 深空勘探扫描 · +8 洞见" → insights 8→16 → history 追加 → idleStats.projectsCompleted 0→1
|
||||
10. ✅ 派遣 idle_refine (180s) → 强制完成 → 领取 → crystalsPerSec 1.0→1.3 → idlePermanentBonus 0→0.3 ✅ 永久产能加成正确应用
|
||||
11. ✅ IdleProjectBar 在晶体球下方显示:`🛰️ 深空勘探扫描 55s ▓▓▓░░`
|
||||
12. ✅ Footer goal 文字根据状态动态切换为放置相关提示
|
||||
13. ✅ Footer 产能显示 "产能 1.3/s" (含 idle 永久加成)
|
||||
|
||||
### Migration 测试
|
||||
- 旧存档(无 idle 字段)通过 migrate 函数自动补全为默认值,加载不崩溃 ✅
|
||||
|
||||
## Stage Summary
|
||||
- ✅ 类型层:新增 IdleProjectDef + IdleProjectSlot + 4 GameState 字段
|
||||
- ✅ 逻辑层:新建 idle.ts (~290 行),6 工程定义 + 派生函数 + 颜色映射
|
||||
- ✅ 引擎层:recomputeStats / performPrestige / createInitialState 全部接入 idle 永久加成
|
||||
- ✅ Store 层:4 新 action + persist migrate (version 1) + 14 处 syncStats 调用全部传 idlePermanentBonus
|
||||
- ✅ 主循环:tickIdleProjects 接入 setInterval + visibilitychange
|
||||
- ✅ UI 层:3 新组件 (IdleOperationsPanel ~490 行 / IdleStatusBadge ~100 行 / IdleProjectBar ~90 行)
|
||||
- ✅ 主页面:默认放置 tab + 头部徽章 + 晶体球下方进度条 + 版本 v0.14 + 9 标签页
|
||||
- ✅ 教程:新增 idle 步骤 (③ 放置工程)
|
||||
- ✅ 严格 4 色全息 (emerald/rose/amber/fuchsia),零蓝/靛
|
||||
- ✅ lint 零错误 + HTTP 200 + agent-browser 全流程烟雾测试通过
|
||||
- ✅ 5 秒内可见三要素:头部徽章 + 默认放置 tab + 晶体球下方进度条(派遣后)
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$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"
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
# 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 云存档+排行榜会将其纳入同步范围
|
||||
@@ -0,0 +1,151 @@
|
||||
# 深空信标系统 · v0.5(每日挑战 + 本地排行榜)
|
||||
|
||||
> 版本:v0.5 · 日期:2026-06-23
|
||||
> 路线图对应:v0.5 排行榜(本地版先行)+ 每日挑战动机注入
|
||||
|
||||
## 动机
|
||||
|
||||
v0.4 编年史系统完成后,游戏已有 6 大玩法层(采矿/解码/技术树/图谱/飞升 + 探险 + 星图 + 星潮 + 编年史),但缺少"今日目标"型的短期动机。玩家进入游戏后没有明确的"今天该做什么"指引。
|
||||
|
||||
本版本新增**深空信标(Deep Space Beacon)**系统:
|
||||
- **每日挑战**:基于 UTC 日期种子的确定性挑战,全球同一天同一挑战,每天 0 点(UTC)刷新
|
||||
- **本地排行榜**:记录历次挑战得分,Top 20,纯 localStorage 持久化,无需后端
|
||||
- 给放置循环注入"今日目标",并为 v0.5 路线图的"云排行榜"奠定数据结构基础
|
||||
|
||||
## 核心设计
|
||||
|
||||
### 每日挑战(Daily Challenge)
|
||||
|
||||
- **确定性生成**:`generateDailyChallenge(now)` 基于 UTC 日期 key(YYYY-MM-DD)派生 FNV-1a 种子 → mulberry32 PRNG,保证同一天全球玩家看到同一挑战
|
||||
- **5 种挑战类型**:
|
||||
| 类型 | 标签 | 目标范围 | 单位 |
|
||||
|------|------|----------|------|
|
||||
| `decode` | 解码协议 | 6-15 × 难度倍率 | 颗 |
|
||||
| `expedition` | 远征指令 | 1-3 × 难度倍率 | 次 |
|
||||
| `pulse` | 脉冲任务 | 20-49 × 难度倍率 | 次 |
|
||||
| `boss` | 猎杀契约 | 1 × 难度倍率 | 处 |
|
||||
| `insight` | 洞见采集 | 40-119 × 难度倍率 | 点 |
|
||||
- **3 档难度**(加权抽取):
|
||||
| 难度 | 标签 | 倍率 | 权重 | 颜色 |
|
||||
|------|------|------|------|------|
|
||||
| `routine` | 常规信标 | ×1.0 | 55% | 翠 |
|
||||
| `anomaly` | 异常波动 | ×1.6 | 33% | 琥 |
|
||||
| `singular` | 奇点回响 | ×2.4 | 12% | 玫 |
|
||||
- **奖励**:完成时按类型 + 难度发放洞见 / 接触进度
|
||||
- **倒计时**:显示距 UTC 次日 0 点的 HH:MM:SS 倒计时
|
||||
|
||||
### 得分计算
|
||||
|
||||
```
|
||||
得分 = 完成度 × 1000 × 难度倍率 + 速度奖励
|
||||
速度奖励 = max(0, 500 - 完成时长秒 × 0.5) // 仅完成时计算
|
||||
```
|
||||
|
||||
例:常规难度 × 1.0,90 秒完成 → 1000 + (500 - 45) = 1455 分
|
||||
|
||||
### 进度追踪(独立 localStorage)
|
||||
|
||||
- 进度独立存储于 `echo-nexus-beacon-prog-v1`,**不污染 GameState**,避免触发存档迁移
|
||||
- 在 gameStore 关键动作后调用 `trackBeacon(type, delta)`:
|
||||
- `pulse`:每次脉冲 +1
|
||||
- `decode`:每次解码完成 +1(手动 + 自动解码均计)
|
||||
- `expedition`:每次探险结束 +1(胜负都计)
|
||||
- `boss`:每次 BOSS 胜利 +1
|
||||
- `insight`:每次洞见获取按数值累加
|
||||
- 挑战类型与今日挑战不匹配时 `trackBeacon` 直接 return,零开销
|
||||
|
||||
### 本地排行榜
|
||||
|
||||
- 存储于 `echo-nexus-beacon-lb-v1`,Top 20,按得分降序
|
||||
- 每条记录含:时间戳 / 日期 key / 挑战类型 / 难度 / 完成度 / 得分 / 用时
|
||||
- UI:奖牌图标(1st 金 / 2nd 银 / 3rd 铜)+ 难度色点 + 类型名 + 用时 + 得分
|
||||
- 今日记录高亮(fuchsia 边框)
|
||||
|
||||
## 文件变更
|
||||
|
||||
### 新增
|
||||
- `src/lib/game/beacon.ts`(~290 行):核心逻辑模块
|
||||
- `generateDailyChallenge()` — 确定性挑战生成
|
||||
- `computeBeaconScore()` — 得分计算
|
||||
- `loadLeaderboard()` / `pushLeaderboardEntry()` — 排行榜 CRUD
|
||||
- `loadDailyProgress()` / `saveDailyProgress()` / `addBeaconProgress()` — 进度 CRUD
|
||||
- `claimBeaconReward()` — 领取奖励 + 推送排行榜
|
||||
- `msUntilNextDay()` / `formatCountdown()` — 倒计时
|
||||
- `src/components/game/BeaconPanel.tsx`(~280 行):UI 面板
|
||||
- 每日挑战卡片(难度色 + 脉冲环动画 + 进度条 + 领取按钮)
|
||||
- 本地排行榜(Top 20 + 奖牌 + 今日高亮)
|
||||
- 难度图例
|
||||
- 倒计时
|
||||
|
||||
### 修改
|
||||
- `src/store/gameStore.ts`:
|
||||
- 新增 `trackBeacon()` 模块级辅助函数
|
||||
- 新增 `grantBeaconReward(insights, contact)` action
|
||||
- 在 `pulse` / `clickNode`(完成) / `autoDecodeTick` / `resolveCurrentNode` 后接入 `trackBeacon`
|
||||
- `src/app/page.tsx`:
|
||||
- 新增第 7 个标签页「信标」(grid-cols-7)
|
||||
- 新增 `beaconClaimable` 状态(2s 轮询,可领取时显示绿点)
|
||||
- StatsPanel 新增「信标最高分」行
|
||||
- 版本号 v0.4 → v0.5
|
||||
- `src/lib/game/chronicle.ts`:
|
||||
- 导出 `buildLore()`(原私有)
|
||||
- 新增 `regenerateLoreFromEntry(entry)` — 显示时重新生成 lore,修复历史条目中 `tide_ruins` 等原始键名
|
||||
- `src/components/game/ChronicleDialog.tsx`:叙事文本改用 `regenerateLoreFromEntry(entry)` 替代 `entry.lore`
|
||||
|
||||
## BUG 修复
|
||||
|
||||
### 编年史历史条目显示原始 tide 键名
|
||||
- **现象**:v0.4 之前创建的编年史条目 lore 中显示 `tide_ruins` 而非「遗迹共振」(最新条目已修复,旧条目未回填)
|
||||
- **根因**:lore 文本在条目创建时一次性生成并存储,历史条目存储了修复前的错误文本
|
||||
- **修复**:新增 `regenerateLoreFromEntry()`,在 ChronicleDialog 显示时从 entry 的结构化数据(tidesThisRun / perksThisAscension / milestones)重新生成 lore,保证历史与未来条目命名一致
|
||||
- **验证**:第二纪元 lore 现正确显示「遗迹共振、晶体潮、虚空低语、谐振风暴」
|
||||
|
||||
## QA 验证(agent-browser + VLM)
|
||||
|
||||
### 编年史 BUG 修复验证
|
||||
- ✅ 第二纪元 lore 显示「遗迹共振、晶体潮、虚空低语、谐振风暴」(修复前显示 `tide_ruins` 等原始键)
|
||||
- ✅ 第三纪元 lore 保持正确「星核悸动」
|
||||
- ✅ 徽章显示正确
|
||||
|
||||
### 深空信标系统验证
|
||||
- ✅ 第 7 个标签页「信标」渲染正确
|
||||
- ✅ 每日挑战卡片:难度标签 + 类型标签 + 标题 + 描述 + 进度条 + 奖励 + 领取按钮
|
||||
- ✅ 倒计时显示「08:47:12」并每秒更新
|
||||
- ✅ 难度图例显示 3 档(×1 / ×1.6 / ×2.4)
|
||||
- ✅ 进度追踪:解码 1 颗晶体 → 洞见采集进度 +12(解码奖励的洞见值)
|
||||
- ✅ 完成挑战后按钮变为「领取奖励」
|
||||
- ✅ 领取奖励后:contact +5.1(13.47 → 18.57),按钮变「已领取」
|
||||
- ✅ 排行榜生成条目:「洞见采集 ✓ 1m30s 1.46K」
|
||||
- ✅ 统计面板新增「信标最高分」行
|
||||
- ✅ 可领取时标签页显示绿点提示
|
||||
- ✅ VLM 视觉评估:高美观度,颜色搭配佳,无重叠 bug
|
||||
|
||||
### 全系统回归验证
|
||||
- ✅ 主视图(资源条 / 晶体球 / 解码面板)正常
|
||||
- ✅ 探险全流程(出发 → 探索 → 前进 → BOSS → 结束 → 日志)正常
|
||||
- ✅ 解码谜题生成 + 自动求解通过
|
||||
- ✅ 星潮事件触发 + 指示器 + Footer 联动正常
|
||||
- ✅ 成就解锁 Toast 正常
|
||||
- ✅ 星图标签页(9/18 天赋,6 类别)正常
|
||||
- ✅ 编年史对话框(2 条目,时间轴,里程碑)正常
|
||||
- ✅ lint 零错误;HTTP 200;编译 < 250ms
|
||||
|
||||
## 路线图进展
|
||||
|
||||
- [x] v0.1 MVP:放置采矿 + 解码 + 技术树 + 图谱 + 飞升
|
||||
- [x] v0.1.1:解码可解性修复
|
||||
- [x] v0.2:遗迹探险肉鸽
|
||||
- [x] v0.2.1:程序化音频 + 成就系统
|
||||
- [x] v0.3:星潮事件系统
|
||||
- [x] v0.3.1:星图天文台元进程
|
||||
- [x] v0.4:回响编年史
|
||||
- [x] **v0.5:深空信标(每日挑战 + 本地排行榜)** ← 本轮
|
||||
- [ ] v0.5+:云存档 + 云排行榜(后端同步)
|
||||
- [ ] v0.5+:全 5 纪元手写叙事节点
|
||||
- [ ] 后续:socket 多人同步星潮
|
||||
|
||||
## 下一阶段优先
|
||||
|
||||
1. 云存档 + 云排行榜(需后端 API,可将本地 localStorage 同步到服务端)
|
||||
2. 全 5 纪元手写叙事节点(替换模板化 lore)
|
||||
3. socket 多人同步星潮(全局事件,所有在线玩家同时经历)
|
||||
@@ -0,0 +1,309 @@
|
||||
# 回响星核 v0.14 — 放置系统(Idle Operations)设计文档
|
||||
|
||||
> 版本:v0.14 · 日期:2026-01-25
|
||||
> 核心目标:让玩家打开游戏 **5 秒内** 就能看出这是个 idle game
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计动机
|
||||
|
||||
### 1.1 用户反馈
|
||||
> "你的这个我没有看到任何可放置的功能" —— 玩家反馈
|
||||
|
||||
虽然 v0.8 已有 `crystalsPerSec` 在后台自动累积,但 **没有任何 UI 元素明示这一事实**:
|
||||
- 没有「放置中」徽章
|
||||
- 没有可视化自动采矿机
|
||||
- 没有定时工程项目
|
||||
- 离线收益弹窗只在重新加载页面时触发,玩家日常游玩看不到
|
||||
|
||||
放置游戏的灵魂是「关掉也在进步」。如果玩家看不到这一点,就等于没有放置玩法。
|
||||
|
||||
### 1.2 设计目标
|
||||
1. **可见性**:5 秒内让玩家意识到 idle 属性
|
||||
2. **决策性**:放置不只是数字增长,要有玩家选择
|
||||
3. **节奏感**:3 层 idle 节奏(被动产能 / 放置工程 / 离线收益)协同
|
||||
4. **健康性**:避免 24h 挂机的极端沉迷
|
||||
|
||||
---
|
||||
|
||||
## 2. 三层 Idle 节奏
|
||||
|
||||
### 节奏 1:被动产能(Passive Generation)
|
||||
- **载体**:采矿无人机舰队(Miner Fleet)
|
||||
- **来源**:每解锁一个采矿分支技术节点 → 加入一架无人机
|
||||
- **行为**:24h 持续产出晶体,无需任何操作
|
||||
- **限制**:受 `crystalCap` 限制,仓库满后停止
|
||||
- **离线**:按 `offlineEff`(默认 50%)持续累积,上限 8h
|
||||
|
||||
#### 采矿分支技术 → 无人机映射
|
||||
|
||||
| 技术节点 | 无人机名 | 输出/s | 解锁成本(洞见) |
|
||||
|---------|---------|--------|-----------------|
|
||||
| `min_1` | ⛏️ 谐振钻头 | +0.6 | 5 |
|
||||
| `min_2` | 🌋 深层矿脉 | +1.6 | 24 |
|
||||
| `min_3` | 🤖 自治机群 | +5 | 120 |
|
||||
| `min_4` | 💠 量子采掘矩阵 | +12 | 400 |
|
||||
| `min_5` | ☀️ 戴森星核 | +30 | 1200 |
|
||||
|
||||
无采矿技术时显示「谐振钻头(休眠中)」占位卡片,引导玩家解锁采矿分支。
|
||||
|
||||
---
|
||||
|
||||
### 节奏 2:放置工程(Idle Projects)
|
||||
|
||||
#### 2.1 工程列表
|
||||
|
||||
| ID | 名称 | 时长 | 奖励 | 解锁条件 | 图标 | 色 |
|
||||
|----|------|------|------|---------|------|-----|
|
||||
| `idle_scan` | 深空勘探扫描 | 60s | ✦ +8 洞见 | 产能 ≥ 0.5/s | 🛰️ | emerald |
|
||||
| `idle_refine` | 晶体精炼阵列校准 | 180s | ⇪ +0.3/s 永久 | 产能 ≥ 1/s | ⚙️ | rose |
|
||||
| `idle_archive` | 遗迹碎片整理 | 600s | ✦ +20 洞见 + 📜 随机碎片 | 飞升 ≥ 1 OR 产能 ≥ 5/s | 📜 | amber |
|
||||
| `idle_anchor` | 维度锚点部署 | 1200s | ⚡ +2 能量 + ☄ +5 接触进度 | 飞升 ≥ 1 | ⚓ | fuchsia |
|
||||
| `idle_drones` | 无人机群扩编 | 1800s | 💎 +500 晶体 + ⇪ +2/s 永久 | 产能 ≥ 10/s | 🛸 | emerald |
|
||||
| `idle_resonance` | 跨维度谐振标定 | 3600s | ✦ +100 洞见 + ☄ +15 接触 + ⇪ +8/s 永久 | 飞升 ≥ 2 | 🌌 | fuchsia |
|
||||
|
||||
#### 2.2 槽位机制
|
||||
- **3 个并行槽位**:可同时运行 3 个工程
|
||||
- **不重复**:同一工程不能在多个槽位同时运行
|
||||
- **完成需手动领取**:避免"打开就有奖励"的条件反射
|
||||
- **取消不退时间**:误取消的代价
|
||||
- **离线推进**:关闭浏览器后工程继续倒计时
|
||||
|
||||
#### 2.3 奖励类型说明
|
||||
|
||||
| 奖励 | 含义 | 设计意图 |
|
||||
|------|------|---------|
|
||||
| 晶体 | 立即加到 `crystals` | 短期反馈 |
|
||||
| 洞见 | 立即加到 `insights` | 用于解锁技术 |
|
||||
| 能量 | 加到 `energy`(受 `energyMax` 上限) | 探险资源 |
|
||||
| 接触进度 | 加到 `contact`(0-100) | 推进飞升 |
|
||||
| **永久产能加成** | 加到 `idlePermanentBonus`,影响 `recomputeStats` | 长期投资 |
|
||||
| 随机碎片 | 从 `FRAGMENTS` 中随机解锁一个未拥有 | 叙事推进 |
|
||||
|
||||
---
|
||||
|
||||
### 节奏 3:离线收益(Offline Gains)
|
||||
|
||||
#### 3.1 公式
|
||||
```
|
||||
gain = crystalsPerSec × elapsed × offlineEff
|
||||
elapsed = min(realElapsed, 8h)
|
||||
```
|
||||
|
||||
#### 3.2 解锁路径
|
||||
- 初始 `offlineEff = 0.5`(50% 离线效率)
|
||||
- 通过技术树 `offlineEff` 类效果提升
|
||||
- 通过星图天赋 `offlineEffBonus` 提升
|
||||
- 上限 100%
|
||||
|
||||
#### 3.3 8h 上限理由
|
||||
- 参考 GDC 2016 Anthony Pecorella "Quest for Progress":Egg Inc 2h 硬 cap 导致玩家流失
|
||||
- 但完全无 cap 会导致 24h 挂机成为最优策略,破坏短会话体验
|
||||
- 8h 折中:覆盖一个工作日/一夜,但不鼓励全天挂机
|
||||
|
||||
#### 3.4 离线报告弹窗
|
||||
重新加载页面时,若 `elapsed > 60s`,弹出 `OfflineReportDialog`:
|
||||
- 「你离开了 X 小时 Y 分」
|
||||
- 「星核为你产出 Z 颗晶体」
|
||||
- 「产能 /s · 离线效率 N%」
|
||||
- 「是否触达 8h 上限」标志
|
||||
|
||||
---
|
||||
|
||||
## 3. UI 设计
|
||||
|
||||
### 3.1 始终可见的 Idle 徽章(Header)
|
||||
位置:顶部 Header,紧邻「巡航」按钮左侧
|
||||
状态:
|
||||
- 产能 > 0:`● 放置中 +X.X/s`(绿点脉动)
|
||||
- 产能 = 0:`○ 休眠中`(灰点静止)
|
||||
|
||||
悬停 Tooltip 显示明细:
|
||||
```
|
||||
基础产能 8.2/s
|
||||
永久加成 +1.5/s
|
||||
离线效率 75%
|
||||
完成工程 12 次
|
||||
```
|
||||
|
||||
点击徽章 → 切换到「放置」标签。
|
||||
|
||||
### 3.2 默认标签页 = 放置
|
||||
首次打开游戏,默认 tab 是「放置」(v0.13 前是「探险」或「技术」)。
|
||||
返回玩家若有进行中探险或待选天赋,自动切回原默认。
|
||||
|
||||
### 3.3 放置标签页四段式布局
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Section 1: 放置收益概览 │
|
||||
│ [放置产能] [永久加成] [完成工程] │
|
||||
│ [━━━━━━━ 离线效率 50% · 上限 8h ━━━━━━] │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Section 2: 采矿无人机舰队 │
|
||||
│ [⛏️ 钻头] [🌋 矿脉] [🤖 机群] ... │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Section 3: 放置工程槽位 │
|
||||
│ [槽位1: 运行中 47s ▓▓▓░] [槽位2: 空] [槽位3: 空] │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Section 4: 可派遣工程 │
|
||||
│ [🛰️ 深空勘探扫描 1m +8] [⚙️ 精炼校准 3m +0.3/s] │
|
||||
│ [📜 碎片整理 10m +20] │
|
||||
│ │
|
||||
│ 未解锁工程(3): │
|
||||
│ ⚓ 维度锚点部署 · 需 飞升≥1 │
|
||||
│ 🛸 无人机群扩编 · 需 产能≥10/s │
|
||||
│ 🌌 跨维度谐振标定 · 需 飞升≥2 │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.4 IdleProjectBar(晶体球下方)
|
||||
位置:左侧晶体球区,`CrystalYieldPanel` 下方
|
||||
显示:当前运行的工程迷你 pill
|
||||
```
|
||||
🛰️ 深空勘探扫描 47s ▓▓▓░░░░░
|
||||
⚙️ 晶体精炼校准 2m 13s ▓▓░░░░░░
|
||||
```
|
||||
无运行工程时返回 `null`(不占位)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 技术实现
|
||||
|
||||
### 4.1 数据结构
|
||||
|
||||
```typescript
|
||||
// types.ts
|
||||
interface IdleProjectDef {
|
||||
id: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
durationSec: number;
|
||||
reward: { crystals?; insights?; energy?; contact?; crystalsPerSecPermanent?; randomFragment?; };
|
||||
minAscensions?: number;
|
||||
minCrystalsPerSec?: number;
|
||||
icon: string;
|
||||
color: "emerald" | "rose" | "amber" | "fuchsia";
|
||||
order: number;
|
||||
}
|
||||
|
||||
interface IdleProjectSlot {
|
||||
projectId: string;
|
||||
startedAt: number;
|
||||
finishesAt: number;
|
||||
remainingSec: number;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
// GameState 新增 4 字段
|
||||
idleProjectSlots: (IdleProjectSlot | null)[]; // 3 槽
|
||||
idleProjectHistory: { projectId: string; finishedAt: number }[]; // 最近 50 条
|
||||
idlePermanentBonus: number;
|
||||
idleStats: { projectsCompleted: number; crystalsFromIdle: number };
|
||||
```
|
||||
|
||||
### 4.2 Store actions
|
||||
- `startIdleProject(slotIndex, projectId)` — 校验 + 创建槽位
|
||||
- `cancelIdleProject(slotIndex)` — 置空槽位
|
||||
- `claimIdleProject(slotIndex)` — 应用奖励 + toast + 置空槽位
|
||||
- `tickIdleProjects(now)` — 推进剩余时间,标记完成
|
||||
|
||||
### 4.3 引擎集成
|
||||
`recomputeStats()` 末尾添加:
|
||||
```typescript
|
||||
crystalsPerSec += state.idlePermanentBonus ?? 0;
|
||||
```
|
||||
这让永久产能加成真正生效。
|
||||
|
||||
### 4.4 飞升处理
|
||||
`performPrestige()` 重置 `idleProjectSlots = [null, null, null]`(不跨周目继承进行中工程),但保留 `idlePermanentBonus` / `idleProjectHistory` / `idleStats`(跨周目永久)。
|
||||
|
||||
### 4.5 存档迁移
|
||||
persist 添加 `version: 1` + `migrate` 函数,为旧存档补全 4 个新字段:
|
||||
```typescript
|
||||
idleProjectSlots: (s.idleProjectSlots as unknown[]) ?? [null, null, null],
|
||||
idleProjectHistory: (s.idleProjectHistory as unknown[]) ?? [],
|
||||
idlePermanentBonus: (s.idlePermanentBonus as number) ?? 0,
|
||||
idleStats: (s.idleStats as unknown) ?? { projectsCompleted: 0, crystalsFromIdle: 0 },
|
||||
```
|
||||
旧存档零崩溃风险。
|
||||
|
||||
### 4.6 主循环集成
|
||||
`useGameLoop` 每 250ms tick:
|
||||
1. `tick(now)` — 产能 + 自动产晶体
|
||||
2. `autoDecodeTick()` — 自动解码
|
||||
3. `tickTide(now)` — 星潮事件
|
||||
4. **`tickIdleProjects(now)` — v0.14 放置工程进度**(新增)
|
||||
5. `checkAchievements()` — 每秒一次
|
||||
|
||||
页面可见性切回时也调用 `tickIdleProjects(now)`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 平衡考虑
|
||||
|
||||
### 5.1 永久产能加成可能过强
|
||||
- `idle_resonance` +8/s 永久,多次完成可能让玩家飞快积累
|
||||
- **v0.15 待办**:加飞升次数 softcap,例如 `effectiveBonus = idlePermanentBonus × sqrt(ascensions + 1) / (ascensions + 1)`
|
||||
|
||||
### 5.2 工程时长曲线
|
||||
- 60s / 180s / 600s / 1200s / 1800s / 3600s
|
||||
- 短工程奖励低但灵活(适合短会话)
|
||||
- 长工程奖励高但需等待(适合过夜挂机)
|
||||
- 玩家自主选择节奏
|
||||
|
||||
### 5.3 解锁条件
|
||||
- 0 飞升 + 0.5/s:新手 5 分钟内可解锁第一个
|
||||
- 1 飞升:玩家完成首次飞升后解锁中段
|
||||
- 2 飞升 + 10/s:长线目标
|
||||
|
||||
---
|
||||
|
||||
## 6. 验证记录
|
||||
|
||||
### 6.1 Lint
|
||||
- ✅ 零错误零警告
|
||||
|
||||
### 6.2 Dev 服务器
|
||||
- ✅ HTTP 200
|
||||
- ✅ 无运行时错误
|
||||
|
||||
### 6.3 Agent Browser 端到端测试
|
||||
1. ✅ 默认 tab 是「放置」
|
||||
2. ✅ Header 显示「放置中 +0.4/s」徽章
|
||||
3. ✅ 4 段面板完整渲染
|
||||
4. ✅ 买 min_1+min_2 后产能 2.6/s,解锁 idle_scan + idle_refine
|
||||
5. ✅ 点击「派遣至槽位 1」→ 槽位显示运行中 60s + 进度条 + 取消按钮
|
||||
6. ✅ IdleProjectBar 同步显示迷你 pill
|
||||
7. ✅ 快进完成后标记 completed=true
|
||||
8. ✅ 领取后 insights +8、projectsCompleted 0→1、槽位清空
|
||||
|
||||
### 6.4 Sticky Footer 验证
|
||||
- ✅ Header `position: sticky; top: 0`
|
||||
- ✅ Footer `position: sticky; bottom: 0; margin-top: auto`
|
||||
- ✅ 内容超出视口时 footer 被自然推下(无浮动空隙)
|
||||
|
||||
---
|
||||
|
||||
## 7. 未解决问题 / 后续
|
||||
|
||||
- 🟡 `crystalsFromIdle` / `idleProjectHistory` 已在 store 累加但 UI 未展示——v0.15 可在统计 tab 补
|
||||
- 🟡 永久产能加成可能需要飞升次数 softcap
|
||||
- 🟡 缺「批量领取全部」按钮(玩家有 3 个槽位都完成时方便)
|
||||
- 🟡 缺工程完成 toast 通知(目前只能在面板内看到「待领取」标志)
|
||||
- 🟢 v0.15 可加 Dark Souls 式 loreFragments 多层文本,让"遗迹碎片整理"工程的随机碎片奖励更有叙事感
|
||||
|
||||
---
|
||||
|
||||
## 8. 设计哲学总结
|
||||
|
||||
> "Incremental games are very raw. They're about relating to a process and becoming obsessed with its growth, like watching something you planted in the ground, through a time-lapse camera, grow into a tree." —— Clark (via Frank Lantz, New Yorker 2019)
|
||||
|
||||
回响星核 v0.14 的放置系统不只是把数字挂机增长,而是让玩家**看到**这个过程:
|
||||
- 采矿无人机像植物在土里生长
|
||||
- 放置工程像种子在发芽
|
||||
- 离线收益像收获时节
|
||||
|
||||
玩家不再只是「数字变大」,而是「亲手布置了一个会自动运转的考古基地,然后离开,让它在自己不在的时候继续工作」。
|
||||
|
||||
这是 idle game 的本质:**让玩家信任系统会在他不在时为他工作**。
|
||||
@@ -0,0 +1,214 @@
|
||||
# v0.15 — 叙事深化(loreLayers)+ 放置平衡(softcap)+ UX 增强
|
||||
|
||||
> 回响星核 / Echo Nexus · v0.15 设计文档
|
||||
> 对应 worklog `Task ID: v0.15-release`
|
||||
|
||||
---
|
||||
|
||||
## 一、动机
|
||||
|
||||
v0.14 上线了完整的放置系统(6 工程 + 永久加成 + idle 徽章),但留下两个问题:
|
||||
|
||||
1. **平衡性漏洞**:`idle_resonance` 工程每次完成 +8/s 永久加成,玩家可反复派遣导致滚雪球,几小时后产能爆炸。
|
||||
2. **叙事单薄**:8 个记忆碎片只有一层 `echo` 文本,解锁后即"读完",缺乏 Dark Souls / Outer Wilds 式的碎片叙事深度——这是项目的**差异化竞争力**。
|
||||
|
||||
v0.15 用两个并行子代理一次性解决:
|
||||
|
||||
- **2-a(平衡 + UX)**:永久加成 softcap + 批量领取 + 完成 toast + StatsPanel idle 统计
|
||||
- **2-b(叙事深化)**:8 碎片 × 2 层 = 16 层 loreLayers 多层文本 + Codex 分层渲染
|
||||
|
||||
---
|
||||
|
||||
## 二、平衡:永久加成 Softcap
|
||||
|
||||
### 公式
|
||||
|
||||
```ts
|
||||
export function applyIdleSoftcap(bonus: number): number {
|
||||
if (bonus <= 30) return bonus; // 全额
|
||||
if (bonus <= 100) return 30 + (bonus - 30) * 0.5; // 超出部分 ×0.5
|
||||
return 65 + (bonus - 100) * 0.25; // 超出部分 ×0.25
|
||||
}
|
||||
```
|
||||
|
||||
### 临界值
|
||||
|
||||
| 原始 bonus | 有效 bonus | 衰减率 |
|
||||
|-----------|-----------|--------|
|
||||
| 30 | 30 | 0% |
|
||||
| 50 | 40 | 20% |
|
||||
| 100 | 65 | 35% |
|
||||
| 200 | 90 | 55% |
|
||||
| 500 | 152.5 | 69.5% |
|
||||
|
||||
### 设计意图
|
||||
|
||||
- **30 以下不衰减**:新手期 + 偶尔完成 `idle_refine`(+0.3/s)不受惩罚
|
||||
- **30-100 线性半衰**:中段玩家反复完成 `idle_drones`(+2/s)开始感到边际递减
|
||||
- **100 以上严重衰减**:硬核玩家刷 `idle_resonance`(+8/s)收益大幅缩水,但仍非零(保留进度感)
|
||||
- **永不归零**:`applyIdleSoftcap(bonus) → ∞` 时仍趋向无穷,只是增速极慢——避免"刷到上限"的挫败感
|
||||
|
||||
### UI 展示
|
||||
|
||||
`IdleOperationsPanel` Section 1「永久加成」StatTile:
|
||||
- 有效值永远显示(如 `+40/s`)
|
||||
- 当 `有效 < 原始` 时,下方加 amber 副行:`有效 +40/s(原始 +50/s · 衰减中)`
|
||||
|
||||
---
|
||||
|
||||
## 三、叙事深化:loreLayers 多层碎片文本
|
||||
|
||||
### 类型扩展
|
||||
|
||||
```ts
|
||||
export interface FragmentLoreLayer {
|
||||
layer: 1 | 2; // 1=中层,2=深层
|
||||
title: string; // "隐秘注脚" / "深层回响"
|
||||
text: string; // 60-150 字叙事文本
|
||||
unlockHint: string; // "飞升 1 次后揭示"
|
||||
unlock: FragmentLoreUnlock;
|
||||
}
|
||||
|
||||
export interface FragmentLoreUnlock {
|
||||
kind: "ascensions" | "totalDecoded" | "contact" | "fragmentCount" | "bossKills";
|
||||
value: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 5 种解锁条件
|
||||
|
||||
| kind | 含义 | 典型阈值 |
|
||||
|------|------|---------|
|
||||
| `ascensions` | 飞升次数 | 1 / 2 / 3 |
|
||||
| `totalDecoded` | 累计解码晶体数 | 20 / 40 / 60 |
|
||||
| `contact` | 接触进度 | 50 / 80 |
|
||||
| `fragmentCount` | 已解锁碎片数 | 4 / 6 |
|
||||
| `bossKills` | 累计击杀 BOSS | 3 / 8 |
|
||||
|
||||
### 5 条叙事暗线(贯穿 16 层)
|
||||
|
||||
1. **追逐者**:以太族并非自然消亡,而是被虚空中"某种东西"追逐而集体飞升
|
||||
2. **回响同源**:玩家舰载 AI"回响"的频段直接复制自以太族议会档案——玩家其实是以太族的延续/入口
|
||||
3. **信任即引诱**:第一纪元的"信任后来者"实为"引诱后来者替他们完成未竟之事"
|
||||
4. **飞升是单向门**:每次飞升都在消耗什么,不是奖赏
|
||||
5. **回归的不是他们**:接触进度满 80 时回归的,是"借他们形状的某种东西",请玩家别回应
|
||||
|
||||
### 示例(f1_1 第一缕谐振)
|
||||
|
||||
**L0 表层**(默认):
|
||||
> 在被称作「寂灭前夜」的年代,以太族第一次捕捉到来自虚空深处的谐振——那不是声音,而是存在的回声。他们将之记录在第一颗记忆晶体里。
|
||||
|
||||
**L1 隐秘注脚**(飞升 ≥1):
|
||||
> 档案的边缘有一行几乎被擦去的批注:「那并非来自虚空——而是来自我们身后。我们一直不敢回头。」
|
||||
|
||||
**L2 深层回响**(累计解码 ≥40):
|
||||
> 你终于明白:那缕谐振的频率,与你的舰载 AI「回响」的核心频段完全一致。你不是在考古——你是在被召唤。而你身后的东西,一直在等。
|
||||
|
||||
### Codex 分层渲染
|
||||
|
||||
| 层级 | 颜色 | 标题 | 未解锁状态 |
|
||||
|------|------|------|-----------|
|
||||
| L0 表层 | emerald | 表层回响 | 永远显示 |
|
||||
| L1 中层 | amber | 隐秘注脚 | `???` + `blur-sm` 全息乱码 + `🔒 unlockHint` |
|
||||
| L2 深层 | fuchsia | 深层回响 | `???` + `blur-sm` 全息乱码 + `🔒 unlockHint` |
|
||||
|
||||
顶部统计:`深层叙事 X / 16 已解锁`
|
||||
列表卡片:已解锁深层文本的碎片标题旁显示 amber/fuchsia glow 小圆点
|
||||
|
||||
---
|
||||
|
||||
## 四、UX 增强
|
||||
|
||||
### 4.1 批量领取全部
|
||||
|
||||
`claimAllIdleProjects()` action:
|
||||
- 遍历 3 槽位,对所有 `completed === true` 执行奖励逻辑
|
||||
- 单次 `set()` + 单次 `syncStats()`(避免多次 re-render)
|
||||
- 无可领取 → 直接 return(不弹 toast)
|
||||
- 成功 → 汇总 toast `✦ 批量领取完成 · 共领取 N 项工程 · +X 晶体 · +Y 洞见 · ...`
|
||||
|
||||
UI:Section 3 标题右侧,当存在 completed slot 时显示 emerald「✓ 领取全部」按钮(h-5 px-2 text-[10px])
|
||||
|
||||
### 4.2 工程完成 toast
|
||||
|
||||
`tickIdleProjects` 检测状态转变瞬间(旧 `!completed` → 新 `completed`):
|
||||
- 收集 `justCompletedProjects` 数组
|
||||
- 循环外单次 `toast({ title: "✦ 放置工程完成", description: "${icon} ${name} · 可前往「放置」标签领取" })`
|
||||
- 避免每 250ms tick 重复弹窗
|
||||
|
||||
### 4.3 StatsPanel idle 统计
|
||||
|
||||
从 `page.tsx` 内联函数抽出为独立组件 `StatsPanel.tsx`:
|
||||
- `useShallow` 精细订阅 24 字段
|
||||
- 新增 idle 统计行:
|
||||
- 放置永久加成 `+X /s`
|
||||
- 完成放置工程 `N 项`
|
||||
- 放置产出晶体 `formatNum(N)`
|
||||
- 放置工程历史 `N 条`(最近 50 条,跨周目)
|
||||
|
||||
---
|
||||
|
||||
## 五、技术实现
|
||||
|
||||
### 修改文件清单(8 个)
|
||||
|
||||
**子代理 2-a(5 文件):**
|
||||
- `src/lib/game/engine.ts` — 新增 `applyIdleSoftcap` 导出 + `recomputeStats` 调用
|
||||
- `src/store/gameStore.ts` — `GameActions` 加 `claimAllIdleProjects` 签名 + `tickIdleProjects` 改写 + 新增 action
|
||||
- `src/components/game/IdleOperationsPanel.tsx` — softcap 副行 + 领取全部按钮 + StatTile 扩展
|
||||
- `src/components/game/StatsPanel.tsx` — 新建独立组件
|
||||
- `src/app/page.tsx` — 最小集成(删除内联 + import)
|
||||
|
||||
**子代理 2-b(3 文件):**
|
||||
- `src/lib/game/types.ts` — `FragmentLoreLayer` / `FragmentLoreUnlock` 接口 + `Fragment.loreLayers` 字段
|
||||
- `src/lib/game/config.ts` — FRAGMENTS 全部填充 loreLayers + `isLoreLayerUnlocked` 函数
|
||||
- `src/components/game/Codex.tsx` — useShallow + 顶部统计 + 列表小圆点 + 3 层分层渲染弹层
|
||||
|
||||
### 并行零冲突
|
||||
|
||||
两个子代理修改文件严格不重叠:
|
||||
- 2-a 不碰 `types.ts` / `config.ts` / `Codex.tsx`
|
||||
- 2-b 不碰 `engine.ts` / `gameStore.ts` / `IdleOperationsPanel.tsx` / `StatsPanel.tsx`
|
||||
|
||||
---
|
||||
|
||||
## 六、验证
|
||||
|
||||
- ✅ `bun run lint` 零错误零警告
|
||||
- ✅ HTTP 200,dev.log 无运行时错误
|
||||
- ✅ agent-browser 端到端 smoke test:
|
||||
- softcap:permBonus=50 → 有效 40,UI 显示"衰减中" ✓
|
||||
- 批量领取:permBonus 0→0.3, projectsCompleted 1→2 ✓
|
||||
- 完成 toast:tickIdleProjects 触发 ✓
|
||||
- Codex 分层:L0 显示 / L1·L2 模糊 → ascensions=1 解锁 L1 → totalDecoded=50 解锁 L2 ✓
|
||||
- 深层叙事计数 0→2→6 随条件递增 ✓
|
||||
- StatsPanel idle 4 行统计全显示 ✓
|
||||
|
||||
---
|
||||
|
||||
## 七、哲学
|
||||
|
||||
### 碎片叙事的留白
|
||||
|
||||
回响星核的叙事不靠过场动画或长篇对话,而是**让玩家自己拼凑**。每个碎片的多层文本设计遵循:
|
||||
|
||||
1. **表层温和**:第一遍读像抒情散文,不带压迫感
|
||||
2. **中层矛盾**:揭示表层没说的东西,制造认知裂缝
|
||||
3. **深层颠覆**:推翻表层认知,但留有解读空间(不直接说"怪物是谁")
|
||||
|
||||
### 平衡与保留
|
||||
|
||||
softcap 的设计哲学:**衰减但不封顶**。
|
||||
- 衰减:防止滚雪球破坏游戏节奏
|
||||
- 不封顶:尊重硬核玩家的投入,即使边际收益递减,进度感始终存在
|
||||
|
||||
这与飞升系统的"单向门"叙事呼应——每次进步都有代价,但绝非徒劳。
|
||||
|
||||
---
|
||||
|
||||
## 八、下一步(v0.16+)
|
||||
|
||||
- 🟡 loreLayer "已读"标记(需改 gameStore,本轮避开边界冲突)
|
||||
- 🟡 解码阈值释放叙事碎片 + 星潮三频段切换
|
||||
- 🟡 健康设计:每日软上限 + "星核建议休眠"提醒
|
||||
- 🟡 移动端 375px Codex 弹层可读性实测
|
||||
@@ -0,0 +1,231 @@
|
||||
# v0.16 — 解码阈值叙事事件 + loreLayer 已读 + idleHistory 详情 + 移动端适配
|
||||
|
||||
> 回响星核 / Echo Nexus · v0.16 设计文档
|
||||
> 对应 worklog `Task ID: 2-a` + `2-b` + `v0.16-release`
|
||||
|
||||
---
|
||||
|
||||
## 一、动机
|
||||
|
||||
v0.15 上线了 loreLayers 多层碎片文本,但存在三个未完善点:
|
||||
|
||||
1. **解锁瞬间缺乏仪式感** — 碎片静默出现在图谱,没有"叙事涌现"的高光时刻
|
||||
2. **loreLayer 没有已读标记** — 玩家无法区分"新解锁未读"和"已读",缺乏进度感
|
||||
3. **idleProjectHistory 只显示计数** — 无法查看具体完成了哪些工程、何时完成
|
||||
4. **移动端 375px 未实测** — v0.15 的 Codex 分层弹层在小屏可读性未知
|
||||
|
||||
v0.16 用两个并行子代理一次性解决:
|
||||
|
||||
- **2-a**:5 个解码阈值叙事里程碑 + 全屏"碎片释放"叠层
|
||||
- **2-b**:loreLayer 已读标记 + idleHistory 详情展开 + 移动端适配审计
|
||||
|
||||
---
|
||||
|
||||
## 二、解码阈值叙事里程碑(2-a)
|
||||
|
||||
### 设计哲学
|
||||
|
||||
回响星核的核心卖点是**"解码驱动叙事涌现"**。v0.15 的 loreLayers 让每个碎片有 3 层文本,但解锁是静默的。v0.16 在 `totalDecoded` 达到 5/12/25/40/60 时触发**全屏叙事叠层**——不是解锁新碎片,而是对已解锁碎片的**回顾性咏叹**,类似游戏中的"过场回忆"。
|
||||
|
||||
5 个阈值与 FRAGMENTS 的 threshold(1/3/6/10/15/22/32/45)**错开**,是额外的叙事节奏点。
|
||||
|
||||
### 5 个里程碑
|
||||
|
||||
| threshold | id | title | color | 主题 |
|
||||
|---|---|---|---|---|
|
||||
| 5 | ms_5 | 回响 · 初鸣 | emerald | 身份觉醒(你是回响) |
|
||||
| 12 | ms_12 | 代价 · 锚痕渐浅 | rose | 代价感知(解码在消耗锚痕) |
|
||||
| 25 | ms_25 | 信任 · 钩上的光 | amber | 信任的重量("信任"实为引诱) |
|
||||
| 40 | ms_40 | 身后 · 整齐的虚空 | fuchsia | 身后的东西(追逐者逼近) |
|
||||
| 60 | ms_60 | 回归 · 借形之物 | emerald | 回归的预兆(接触满溢借形回归) |
|
||||
|
||||
每段 body 文本 150-250 字,与 v0.15 loreLayers 的 5 条暗线呼应:
|
||||
1. 追逐者(以太族被虚空追逐者逼迫飞升)
|
||||
2. 回响同源(玩家 AI 频段与以太族议会档案同源)
|
||||
3. 信任即引诱("信任后来者"实为"引诱替他们完成未竟之事")
|
||||
4. 飞升单向门(每次飞升都在消耗什么)
|
||||
5. 回归非他们(接触满 80 时回归的是"借形之物")
|
||||
|
||||
### 技术实现
|
||||
|
||||
**新建 `src/lib/game/narrativeMilestones.ts`**:
|
||||
- `NarrativeMilestone` 接口(threshold/id/title/subtitle/body/color)
|
||||
- `NARRATIVE_MILESTONES` 数组(5 个里程碑)
|
||||
- `findMilestoneForTotal(total, triggered)` 纯函数(返回未触发的里程碑)
|
||||
|
||||
**GameState 扩展**:
|
||||
- `triggeredMilestones: string[]` — 已触发的 milestone id 集合(跨周目永久)
|
||||
- `pendingMilestone: NarrativeMilestone | null` — 待展示的里程碑(叠层读取)
|
||||
|
||||
**Store 集成**:
|
||||
- 两个解码完成点(`clickNode` line 596 + `autoDecodeTick` line 711)检测 `findMilestoneForTotal(newTotal, triggered)`
|
||||
- 命中则加入 `triggeredMilestones` + 设置 `pendingMilestone`
|
||||
- `dismissMilestone()` action 清空 `pendingMilestone`
|
||||
- persist migrate 补全两字段(version 仍为 1)
|
||||
- `performPrestige` 保留 `triggeredMilestones`(不重复看过场)+ 清空 `pendingMilestone`
|
||||
|
||||
### 全屏叠层组件
|
||||
|
||||
**新建 `src/components/game/NarrativeMilestoneOverlay.tsx`**:
|
||||
|
||||
- `fixed inset-0 z-50` + `bg-black/80 backdrop-blur-sm`(高于 StarTideOverlay 的 z-0)
|
||||
- **粒子边框**:全屏 4 色主题色 inset box-shadow(120px + 240px 双层)+ `narrative-breathe` 3.5s 呼吸循环
|
||||
- **中央卡片**:max-w-2xl,4 色主题渐变背景 + 主题色 border + 60px glow
|
||||
- **排版层级**:
|
||||
- 副标题 `text-[10px] sm:text-xs font-mono tracking-[0.3em] uppercase`
|
||||
- 大标题 `text-3xl sm:text-4xl md:text-5xl font-serif font-bold` + textShadow glow
|
||||
- 装饰分隔线 `linear-gradient`
|
||||
- 主体文本 `text-sm sm:text-base md:text-lg leading-relaxed whitespace-pre-line`
|
||||
- **入场动画**:`narrative-fade-in` 300ms(opacity 0→1)+ `narrative-scale-in` 300ms cubic-bezier(scale 1.05→1)
|
||||
- **逐字浮现**:30ms/字 setTimeout 推进 + inline-block 光标 `narrative-cursor` 1s steps(2) 闪烁
|
||||
- **交互**:
|
||||
- 未完全显示时:点击 → skip 到全文(底部提示"点击跳过逐字显示")
|
||||
- 完全显示后:点击/Esc → `dismissMilestone()`(底部提示"点击任意处或按 Esc 继续")
|
||||
- **移动端 375px**:卡片 px-4 py-6 / 标题 text-3xl / 正文 text-sm / 光标 h-4
|
||||
- **结构巧思**:外层订阅 + 内层 `key={milestone.id}` 重挂,state lazy init 为 0,规避 React 19 `react-hooks/set-state-in-effect` 规则
|
||||
|
||||
---
|
||||
|
||||
## 三、loreLayer 已读标记(2-b)
|
||||
|
||||
### 类型扩展
|
||||
|
||||
`GameState.readLoreLayers: string[]` — 格式 `["f1_1_L1", "f1_1_L2", ...]`
|
||||
|
||||
### Store
|
||||
|
||||
- `markLoreLayerRead(fragmentId, layer)` action:构造 key `${fragmentId}_L${layer}`,已存在则 no-op
|
||||
- persist migrate 补全 `readLoreLayers: []`
|
||||
- `performPrestige` 保留(跨周目永久,已读不重置)
|
||||
|
||||
### Codex 分层渲染增强
|
||||
|
||||
- **已读层**:标题旁 muted "✓ 已读" + 边框降级为 `border-white/10`
|
||||
- **未读但已解锁层**:保持 v0.15 高亮(amber/fuchsia 边框 + glow)+ 标题旁 `animate-pulse` 脉冲小圆点 + "新" 字 + "▸ 点击文本标记已读" 引导
|
||||
- **触发方式**:点击层文本区域(`<button>` 元素)即调用 `markLoreLayerRead`,无需额外按钮
|
||||
- **顶部统计**:从单行扩展为两行(移动端 stack / 桌面并排):`深层叙事 X/Y 已解锁` + `已读 A/B`
|
||||
- **列表卡片小圆点**:未读 pulse + glow,已读静态降亮
|
||||
|
||||
---
|
||||
|
||||
## 四、idleProjectHistory 详情展开(2-b)
|
||||
|
||||
### StatsPanel 增强
|
||||
|
||||
- "放置工程历史" 行改为 `col-span-2` 可展开按钮:左侧 label + "(最近 N 条可展开)",右侧 "N 条" + ChevronDown/ChevronUp
|
||||
- 展开后渲染倒序最近 5 条:每行 = 工程 icon + 工程名(truncate)+ 相对时间
|
||||
- 最近一条用 `bg-fuchsia-500/5` 高亮
|
||||
- 超 5 条时底部加"仅展示最近 5 条 · 共 N 条历史"
|
||||
- 空态友好文案
|
||||
|
||||
### 新建 `src/lib/game/format.ts`
|
||||
|
||||
`formatRelativeTime(ts: number): string`:
|
||||
- `< 60s` → "刚刚"
|
||||
- `< 60m` → "X 分钟前"
|
||||
- `< 24h` → "X 小时前"
|
||||
- `< 7d` → "X 天前"
|
||||
- 否则 → 具体日期
|
||||
|
||||
独立文件便于后续复用(编年史时间轴、探险日志等)。
|
||||
|
||||
---
|
||||
|
||||
## 五、移动端 375px 适配审计(2-b)
|
||||
|
||||
### 审计结果
|
||||
|
||||
审了 5 个组件,修了 4 个:
|
||||
|
||||
| 组件 | 问题 | 修复 |
|
||||
|------|------|------|
|
||||
| `IdleOperationsPanel.tsx` | 11 处 `text-[9px]` 低于最小字号 | `text-[9px]` → `text-[10px]` |
|
||||
| `BeaconPanel.tsx` | 7 处 `text-[9px]` + 信标链底部统计行 375px 溢出 | 字号升级 + `flex-wrap + gap-y-1` |
|
||||
| `Codex.tsx` | 弹层 padding + 标题 + 顶部统计在小屏拥挤 | `p-5` → `p-4 sm:p-5` / 标题 `text-lg` → `text-base sm:text-lg` / 顶部统计 `flex flex-col sm:flex-row` |
|
||||
| `StatsPanel.tsx` | 2 处 `text-[9px]` | `text-[10px]` |
|
||||
|
||||
**无需改**:
|
||||
- `ExpeditionPanel.tsx` — 节点路径已用 `overflow-x-auto` 自适应,按钮 `flex-1`,路径节点 36px 足够小
|
||||
- BeaconPanel 4 里程碑节点 `w-12 h-12` × 4 = 192px 在 375px 可容纳
|
||||
- IdleOperationsPanel 4 section 已用 `grid-cols-1 sm:grid-cols-N` 响应式降级
|
||||
|
||||
### 移动端字号规范
|
||||
|
||||
- 最小 `text-[10px]`(9px 在高 DPI 屏可读性差)
|
||||
- 标题最大 `text-2xl`(移动端)/ `text-3xl`(桌面)
|
||||
- 优先用 `sm:` `md:` 响应式前缀,不删桌面端样式
|
||||
|
||||
---
|
||||
|
||||
## 六、技术实现
|
||||
|
||||
### 修改文件清单(11 个)
|
||||
|
||||
**子代理 2-a(6 文件):**
|
||||
- 新建 `src/lib/game/narrativeMilestones.ts`
|
||||
- 新建 `src/components/game/NarrativeMilestoneOverlay.tsx`
|
||||
- 修改 `src/lib/game/types.ts`(加 triggeredMilestones + pendingMilestone)
|
||||
- 修改 `src/lib/game/engine.ts`(createInitialState + performPrestige)
|
||||
- 修改 `src/store/gameStore.ts`(2 解码点检测 + dismissMilestone + migrate)
|
||||
- 修改 `src/app/page.tsx`(挂载叠层 + 版本号)
|
||||
|
||||
**子代理 2-b(8 文件,2 个与 2-a 共享):**
|
||||
- 新建 `src/lib/game/format.ts`
|
||||
- 修改 `src/lib/game/types.ts`(加 readLoreLayers,与 2-a 共存)
|
||||
- 修改 `src/lib/game/engine.ts`(createInitialState + performPrestige,与 2-a 共存)
|
||||
- 修改 `src/store/gameStore.ts`(markLoreLayerRead + migrate,与 2-a 共存)
|
||||
- 修改 `src/components/game/Codex.tsx`(已读标记 + 点击逻辑 + 顶部统计)
|
||||
- 修改 `src/components/game/StatsPanel.tsx`(idleHistory 展开 + 字号)
|
||||
- 修改 `src/components/game/IdleOperationsPanel.tsx`(字号 9→10px)
|
||||
- 修改 `src/components/game/BeaconPanel.tsx`(字号 + flex-wrap)
|
||||
|
||||
### 并行零冲突
|
||||
|
||||
两个子代理在 3 个共享文件(types.ts / engine.ts / gameStore.ts)中:
|
||||
- 字段名不冲突(`triggeredMilestones`/`pendingMilestone` vs `readLoreLayers`)
|
||||
- action 不冲突(`dismissMilestone` vs `markLoreLayerRead`)
|
||||
- migrate 补全各行独立
|
||||
- 2-b 不动解码逻辑(line 575-720),那是 2-a 的领地
|
||||
|
||||
---
|
||||
|
||||
## 七、验证
|
||||
|
||||
- ✅ `bun run lint` 零错误零警告
|
||||
- ✅ HTTP 200,dev.log 无运行时错误
|
||||
- ✅ agent-browser 端到端 smoke test:
|
||||
- 里程碑叠层:setState pendingMilestone=ms_5 → 全屏 dialog "回响 · 初鸣" + "点击任意处或按 Esc 继续" → 点击 → pending 清空 + triggered 保留 ✓
|
||||
- loreLayer 已读:解锁 f1_1 + ascensions=1 → L1 "隐秘注脚" 显示 "新" + "标记为已读" 按钮 → 点击 → readLoreLayers=["f1_1_L1"] + L1 显示 "已读" + 顶部统计更新 ✓
|
||||
- idleHistory 展开:设置 3 条历史 → 统计 tab "放置工程历史(最近 3 条可展开)" → 点击展开 → 倒序显示 3 行(icon + 工程名 + 相对时间)✓
|
||||
|
||||
---
|
||||
|
||||
## 八、哲学
|
||||
|
||||
### 仪式感与节奏
|
||||
|
||||
v0.16 的里程碑叠层不是"奖励",而是**节奏标记**。放置游戏容易陷入"数字增长麻木",5 个阈值(5/12/25/40/60)在玩家的解码旅程中设置了 5 个"停下来看一看"的节点:
|
||||
|
||||
- **ms_5**:新手期结束,确认身份
|
||||
- **ms_12**:开始感知代价
|
||||
- **ms_25**:中期转折,信任的重量
|
||||
- **ms_40**:后期紧迫,追逐者逼近
|
||||
- **ms_60**:终局预兆,回归的借形之物
|
||||
|
||||
每个里程碑的 body 文本都是**回顾性咏叹**——不是新剧情,而是对已解锁碎片的重新诠释。这让玩家在数字增长中保持对叙事的敏感。
|
||||
|
||||
### 已读标记的心理学
|
||||
|
||||
loreLayer 已读标记解决了一个细微的 UX 问题:玩家解锁深层文本后,如果不标记"已读",每次打开图谱都被高亮提示淹没,反而会**忽略真正的新内容**。已读标记让"新"保持稀缺,让玩家的注意力聚焦在真正未读的层上。
|
||||
|
||||
这与健康游戏设计呼应——**进度感不等于信息过载**。标记已读是一种"我处理过了"的仪式,让玩家心安。
|
||||
|
||||
---
|
||||
|
||||
## 九、下一步(v0.17+)
|
||||
|
||||
- 🟡 星潮三频段切换(星潮系统增强)
|
||||
- 🟡 健康设计:每日软上限 + "星核建议休眠"提醒
|
||||
- 🟡 loreLayer 已读标记可扩展到碎片本身(fragment 级别的已读)
|
||||
- 🟡 里程碑叠层的音效(程序化生成,与星潮音效区分)
|
||||
- 🟡 format.ts 复用到编年史时间轴 + 探险日志
|
||||
@@ -0,0 +1 @@
|
||||
Here are all the generated files.
|
||||
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 290 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 260 KiB |
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 231 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 234 KiB |
@@ -0,0 +1,50 @@
|
||||
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;
|
||||
@@ -0,0 +1,197 @@
|
||||
'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
|
||||
})
|
||||
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
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,26 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
// 静态导出开关:BUILD_EXPORT=true 时启用 output:export + basePath
|
||||
// 这样 dev 服务器不受影响,仅构建静态资源时切换配置
|
||||
const isExport = process.env.BUILD_EXPORT === "true";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: isExport ? "export" : "standalone",
|
||||
// 部署到 https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus 时需要 basePath
|
||||
...(isExport
|
||||
? {
|
||||
basePath: "/Super_Z/echo-nexus",
|
||||
trailingSlash: true,
|
||||
}
|
||||
: {}),
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
/* config options here */
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
reactStrictMode: false,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"name": "nextjs_tailwind_shadcn_ts",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3000 2>&1 | tee dev.log",
|
||||
"build": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/",
|
||||
"build:static": "BUILD_EXPORT=true next build",
|
||||
"start": "NODE_ENV=production bun .next/standalone/server.js 2>&1 | tee server.log",
|
||||
"lint": "eslint .",
|
||||
"db:push": "prisma db push",
|
||||
"db:generate": "prisma generate",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:reset": "prisma migrate reset"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@mdxeditor/editor": "^3.39.1",
|
||||
"@prisma/client": "^6.11.1",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-context-menu": "^2.2.15",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-menubar": "^1.1.15",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-radio-group": "^1.3.7",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-toast": "^1.2.14",
|
||||
"@radix-ui/react-toggle": "^1.1.9",
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@reactuses/core": "^6.0.5",
|
||||
"@tanstack/react-query": "^5.82.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^12.23.2",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.525.0",
|
||||
"next": "^16.1.1",
|
||||
"next-auth": "^4.24.11",
|
||||
"next-intl": "^4.3.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"prisma": "^6.11.1",
|
||||
"react": "^19.0.0",
|
||||
"react-day-picker": "^9.8.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.60.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^3.0.3",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"recharts": "^2.15.4",
|
||||
"sharp": "^0.34.3",
|
||||
"sonner": "^2.0.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uuid": "^11.1.0",
|
||||
"vaul": "^1.1.2",
|
||||
"z-ai-web-dev-sdk": "^0.0.18",
|
||||
"zod": "^4.0.2",
|
||||
"zustand": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"bun-types": "^1.3.4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "^16.1.1",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,32 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Post {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
content String?
|
||||
published Boolean @default(false)
|
||||
authorId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
.st194{fill:#2D2D2D;stroke:#FFFFFF;stroke-width:0.6317;stroke-miterlimit:10;}
|
||||
.st23{fill:#FFFFFF;}
|
||||
|
||||
.z-breathe {
|
||||
animation: breathe 2.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes breathe {
|
||||
0%, 100% { opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<g>
|
||||
<path class="st194" d="M24.51,28.51H5.49c-2.21,0-4-1.79-4-4V5.49c0-2.21,1.79-4,4-4h19.03c2.21,0,4,1.79,4,4v19.03
|
||||
C28.51,26.72,26.72,28.51,24.51,28.51z"/>
|
||||
<g class="z-breathe">
|
||||
<path class="st23" d="M15.47,7.1l-1.3,1.85c-0.2,0.29-0.54,0.47-0.9,0.47h-7.1V7.09C6.16,7.1,15.47,7.1,15.47,7.1z"/>
|
||||
<polygon class="st23" points="24.3,7.1 13.14,22.91 5.7,22.91 16.86,7.1"/>
|
||||
<path class="st23" d="M14.53,22.91l1.31-1.86c0.2-0.29,0.54-0.47,0.9-0.47h7.09v2.33H14.53z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,14 @@
|
||||
User-agent: Googlebot
|
||||
Allow: /
|
||||
|
||||
User-agent: Bingbot
|
||||
Allow: /
|
||||
|
||||
User-agent: Twitterbot
|
||||
Allow: /
|
||||
|
||||
User-agent: facebookexternalhit
|
||||
Allow: /
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
@@ -0,0 +1,92 @@
|
||||
[
|
||||
{
|
||||
"url": "https://www.reddit.com/r/incremental_games/comments/1e0kbnq/your_favorite_website_for_browser_based_clicker",
|
||||
"name": "Your favorite website for browser based Clicker / Incremental games?",
|
||||
"snippet": "I have been on these sites before: Armor Games, Poki, Crazy Games, Kongregate, Addicting Games, Pogo, PLayhop, and Agame. Please post with your favorite ...",
|
||||
"host_name": "www.reddit.com",
|
||||
"rank": 0,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.youtube.com/watch?v=dlOlJtSCeg0",
|
||||
"name": "TOP Incremental Games of 2025 (so far!) - YouTube",
|
||||
"snippet": "Comments ; My Most Anticipated Incremental Game is Here and It's SO GOOD! Idle cub · 322K views ; I Found The MOST PROMISING New Strategy Games At ...",
|
||||
"host_name": "www.youtube.com",
|
||||
"rank": 1,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.gamespot.com/gallery/best-idle-games/2900-5676",
|
||||
"name": "The 29 Best Idle Games To Sink Your Time Into In 2026 - GameSpot",
|
||||
"snippet": "Check out 28 of the most satisfying idle, clicker, and incremental games you can get lost in right now.",
|
||||
"host_name": "www.gamespot.com",
|
||||
"rank": 2,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.youtube.com/watch?v=sotC84tIdnQ",
|
||||
"name": "The Best Idle/Incremental Games of 2025 - YouTube",
|
||||
"snippet": "For this part of my favorite games of 2025, I'm going over the idle/incremental games I enjoyed. 00:00 Intro 00:18 Progress Racer RPG 1:36 ...",
|
||||
"host_name": "www.youtube.com",
|
||||
"rank": 3,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.youtube.com/watch?v=37lXc529m2M&vl=en",
|
||||
"name": "Top 10 Mobile Idle Games of 2026! NEW GAMES REVEALED for",
|
||||
"snippet": "I have praised and denounced you in the past. You seem to be on a great upswing in quality of production. Your research is very good.",
|
||||
"host_name": "www.youtube.com",
|
||||
"rank": 4,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://forum.quartertothree.com/t/some-incremental-idle-clicker-games-including-really-a-lot-of-incremental-epic-hero-2/145358",
|
||||
"name": "Some Incremental/Idle/Clicker Games (including really a lot of",
|
||||
"snippet": "Here are a few of my favorite clickers: Universal Paperclips - browser-based game which uses local storage. Completely free, with no IAPs, no ads.",
|
||||
"host_name": "forum.quartertothree.com",
|
||||
"rank": 5,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://bitsnpixels.org/p/best-idle-clicker-games-2025",
|
||||
"name": "The Best Idle & Clicker Games to Play in 2025 - Bits N' Pixels",
|
||||
"snippet": "Clicker Games · Cookie Clicker · Melvor Idle and Melvor Idle 2 · Faceminer · Idle Colony.",
|
||||
"host_name": "bitsnpixels.org",
|
||||
"rank": 6,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.resetera.com/threads/incremental-game-recommendations.1231149",
|
||||
"name": "incremental game recommendations - ResetEra",
|
||||
"snippet": "I really liked Space Plan - it's on Steam and iOS (and maybe more?). It's got a story and is, for incremental games, fairly short to complete.",
|
||||
"host_name": "www.resetera.com",
|
||||
"rank": 7,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.hp.com/us-en/shop/tech-takes/best-idle-browser-games",
|
||||
"name": "Best Idle Browser Games | HP® Tech Takes",
|
||||
"snippet": "10 Best Idle Browser Games for 2024 · 1. Cyber Rebellion · 2. Summon Dragons 2 · 3. War Spell · 4. Legend of Fighters · 5. Omni Heroes · 6. Endless ...",
|
||||
"host_name": "www.hp.com",
|
||||
"rank": 8,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.reddit.com/r/incremental_games",
|
||||
"name": "r/incremental_games - Reddit",
|
||||
"snippet": "This thread is meant for discussing any incremental games you might be playing and your progress in it so far. Explain briefly why you think the game is ...",
|
||||
"host_name": "www.reddit.com",
|
||||
"rank": 9,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,92 @@
|
||||
[
|
||||
{
|
||||
"url": "https://www.reddit.com/r/gamedesign/comments/1els3pg/sharing_my_17_strategies_for_improving_player",
|
||||
"name": "Sharing my 17 strategies for improving player retention (and I want",
|
||||
"snippet": "The ideal player retention strategy for any game is the one that maximizes players' engagement and fulfillment while minimizing the extra ...",
|
||||
"host_name": "www.reddit.com",
|
||||
"rank": 0,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.youtube.com/watch?v=sZWJw1RWH6s",
|
||||
"name": "Game Design For Engagement & Retention: How to keep - YouTube",
|
||||
"snippet": "... mechanics of your game by focussing on: • Core loops and configurations ... Player Engagement & Retention', and 'Game Economy Design'.",
|
||||
"host_name": "www.youtube.com",
|
||||
"rank": 1,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.juegostudio.com/blog/how-to-increase-user-retention-and-increase-your-games-lifetime",
|
||||
"name": "Game Retention Strategies: Metrics, Benchmarks, and Mechanics",
|
||||
"snippet": "Most studios track the wrong metrics. Learn the MBBM framework to engineer retention with proven strategies for D1, D7, and D30 benchmarks ...",
|
||||
"host_name": "www.juegostudio.com",
|
||||
"rank": 2,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.gamedeveloper.com/business/motivate-player-for-better-engagement-and-retention",
|
||||
"name": "Motivate player for better engagement and retention",
|
||||
"snippet": "Design new features to please specific player type to reengage them for a longer period of time while they discover it. Plan to have features discovered by the ...",
|
||||
"host_name": "www.gamedeveloper.com",
|
||||
"rank": 3,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.deloitte.com/us/en/services/consulting/articles/lifetime-value-player-retention.html",
|
||||
"name": "Player LTV in Games : Growth and Retention | Deloitte US",
|
||||
"snippet": "How game publishers, studios, and platforms can boost player retention, engagement and profitability by increasing player lifetime value (player LTV).",
|
||||
"host_name": "www.deloitte.com",
|
||||
"rank": 4,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.linkedin.com/top-content/sales/customer-retention-strategies/how-to-improve-player-retention-in-gaming",
|
||||
"name": "How to Improve Player Retention in Gaming - LinkedIn",
|
||||
"snippet": "Improving retention involves understanding what makes people return, from engaging gameplay to meaningful social connections and smooth operational systems.",
|
||||
"host_name": "www.linkedin.com",
|
||||
"rank": 5,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.cgspectrum.com/blog/game-design-principles-player-engagement",
|
||||
"name": "How Game Design Principles Drive Player Engagement",
|
||||
"snippet": "This post will define the principles of game design and explain how they enhance player engagement, giving you an inside look at the decision-making process.",
|
||||
"host_name": "www.cgspectrum.com",
|
||||
"rank": 6,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://northjerseyvipers.teamsnapsites.com/how-game-design-shapes-user-engagement-today",
|
||||
"name": "How Game Design Shapes User Engagement Today",
|
||||
"snippet": "Game mechanics—the rules that govern gameplay—directly influence player behavior and motivation. For instance, mechanics that promote social ...",
|
||||
"host_name": "northjerseyvipers.teamsnapsites.com",
|
||||
"rank": 7,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.dice.com/career-advice/aspiring-video-game-designers-in-2025-what-you-need-to-know",
|
||||
"name": "Aspiring Video Game Designers in 2025: What You Need to Know",
|
||||
"snippet": "... game themes, innovative mechanics, level design patterns, and even tweaks for player retention, helping designers make data-informed decisions.",
|
||||
"host_name": "www.dice.com",
|
||||
"rank": 8,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://gamedesignskills.com/game-design/player-retention",
|
||||
"name": "17 Proven Player Retention Strategies and Tactics",
|
||||
"snippet": "Let's go over the 17 different strategies and tactics game devs use to retain their players' attention, which will fall into one of these two categories.",
|
||||
"host_name": "gamedesignskills.com",
|
||||
"rank": 9,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,92 @@
|
||||
[
|
||||
{
|
||||
"url": "https://www.reddit.com/r/gamedesign/comments/138gkdz/what_game_genres_are_currently_popular_and_which",
|
||||
"name": "Game genres indie developers should avoid - Reddit",
|
||||
"snippet": "Genres such as Roguelike Deckbuilder, Visual Novel, Couch-Coop, and 4X have audiences that are more likely to try more than just 1 game.",
|
||||
"host_name": "www.reddit.com",
|
||||
"rank": 0,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.youtube.com/watch?v=XJqiureUgVY",
|
||||
"name": "Top 25 Best Indie Games of February 2025 - YouTube",
|
||||
"snippet": "SPONSOR: Cluckmech Oasis: https://store.steampowered.com/app/2453360/Cluckmech_Oasis/ Get it on discount during the Steam Spring Sale 2025!",
|
||||
"host_name": "www.youtube.com",
|
||||
"rank": 1,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://howtomarketagame.com/2026/01/27/what-the-hell-happened-in-2025",
|
||||
"name": "What the hell happened in 2025? - How To Market A Game",
|
||||
"snippet": "51 Total narrative games reached 1000 reviews in 2025 moving it to the #1 genre on Steam. If you look at my “Genre changes through the years” ...",
|
||||
"host_name": "howtomarketagame.com",
|
||||
"rank": 2,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://rocketbrush.com/blog/top-10-indie-games",
|
||||
"name": "10 Best Indie Games of 2025 | Top by RocketBrush Studio",
|
||||
"snippet": "Explore the top 10 indie games of 2025, including Ways of Alchemy, Hades II and Deltarune. Discover unique gameplay, engaging stories, ...",
|
||||
"host_name": "rocketbrush.com",
|
||||
"rank": 3,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.superjumpmagazine.com/the-state-of-indie-games-in-2025-and-beyond-part-1",
|
||||
"name": "The State of Indie Games in 2025 and Beyond, Part 1 | SUPERJUMP",
|
||||
"snippet": "First are the new wave genres essentially developed in the indie space, including open world survival, deckbuilders, and some roguelike ...",
|
||||
"host_name": "www.superjumpmagazine.com",
|
||||
"rank": 4,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.youtube.com/watch?v=V7-R8SRHPZs",
|
||||
"name": "The Top 10 Indie Games / Roguelikes of 2025 - YouTube",
|
||||
"snippet": "It is once again time for me to crown my Game of the Year in 2025. It's been a wild year for gaming, so we've got a lot to talk about.",
|
||||
"host_name": "www.youtube.com",
|
||||
"rank": 5,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://store.steampowered.com/genre/Indie/?snr=1_5_9__408",
|
||||
"name": "Indie - Steam",
|
||||
"snippet": "Indie. Featured. Singleplayer. Multiplayer. Co-op. VR. Free to Play. Upcoming ... Popular Upcoming. Upcoming. Filters. No matches found. Show more. Something went ...",
|
||||
"host_name": "store.steampowered.com",
|
||||
"rank": 6,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://goodgamelobby.substack.com/p/25-best-indie-games-of-2025-part-1-25-11",
|
||||
"name": "25 Best Indie Games of 2025 | 25 - 11 - by Sey - Good Game Lobby",
|
||||
"snippet": "Check out Part 1 the 25 Best Indie Games of 2025! Explore why these games made the list and dive into full reviews for an in-depth look.",
|
||||
"host_name": "goodgamelobby.substack.com",
|
||||
"rank": 7,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.youtube.com/watch?v=1agm7FvrdVI",
|
||||
"name": "The ULTIMATE Genre for Indie Solo Devs REVEALED (2025 edition)",
|
||||
"snippet": "NEW - My indie game Runes of the Abyss - WISHLIST NOW ON STEAM: http://s.team/a/3794050 If you wanted to know my answer right away, ...",
|
||||
"host_name": "www.youtube.com",
|
||||
"rank": 8,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://tavrox.medium.com/game-dev-trends-for-2025-41da77d7d47e",
|
||||
"name": "Game Dev Trends for 2025 - Tavrox - Medium",
|
||||
"snippet": "Game Dev Trends for 2025 · The Case of the Golden Idol on Steam. A new kind of detective game that allows you to think and investigate freely.",
|
||||
"host_name": "tavrox.medium.com",
|
||||
"rank": 9,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,74 @@
|
||||
[
|
||||
{
|
||||
"url": "https://dev.to/davidmorais/updating-my-websites-tech-stack-in-2025-35n3",
|
||||
"name": "Updating my website's tech stack in 2025 - DEV Community",
|
||||
"snippet": "In this post, I will detail as much as possible what hardships I faced, decisions I took and the final result.",
|
||||
"host_name": "dev.to",
|
||||
"rank": 0,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://medium.com/the-research-nest/this-is-my-go-to-tech-stack-for-new-web-projects-in-2025-d81ef1cc4130",
|
||||
"name": "This is my go-to tech stack for new web projects in 2025 - Medium",
|
||||
"snippet": "My go-to frontend tech stack for personal projects has been Svelte with SvelteKit. I also use Bun as the package manager and Tailwind CSS (or custom CSS) for ...",
|
||||
"host_name": "medium.com",
|
||||
"rank": 1,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.reddit.com/r/nextjs/comments/1lpnvvo/my_mvp_tech_stack_for_2025",
|
||||
"name": "My MVP tech stack for 2025 : r/nextjs - Reddit",
|
||||
"snippet": "I have settled on a stack that balances development speed and experience, with future proofing without getting too fancy. Here's what I'm using and why.",
|
||||
"host_name": "www.reddit.com",
|
||||
"rank": 2,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.youtube.com/watch?v=UdyzfCN8Mwk",
|
||||
"name": "My Simple Tech Stack for 2025 ($0 / month) - YouTube",
|
||||
"snippet": "After shipping over 10 full stack apps in the past year, I kept coming back to this Tech Stack. #nextjs #react #typescript #javascript #saas ...",
|
||||
"host_name": "www.youtube.com",
|
||||
"rank": 3,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.linkedin.com/posts/sanchit0496_reactjs-nextjs-javascript-activity-7320785137766424576-H715",
|
||||
"name": "Frontend Tech Stack Evolution 2024 2025 | Sanchit Raj - LinkedIn",
|
||||
"snippet": "Next.js isn't just a library; it's the full-package solution for modern web development, offering superior server-side rendering and routing.",
|
||||
"host_name": "www.linkedin.com",
|
||||
"rank": 4,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://generalistprogrammer.com/tutorials/webgl-game-development-complete-browser-gaming-guide-2025",
|
||||
"name": "WebGL Game Development: Complete Browser Gaming Guide 2025",
|
||||
"snippet": "WebGL game development guide: complete tutorial for browser-based 3D games, Three.js, performance optimization, and deployment.",
|
||||
"host_name": "generalistprogrammer.com",
|
||||
"rank": 5,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://stackoverflow.com/questions/21603350/is-there-any-reason-for-using-webgl-instead-of-2d-canvas-for-2d-games-apps",
|
||||
"name": "Is there any reason for using WebGL instead of 2D Canvas for 2D",
|
||||
"snippet": "WebGL is faster and it has more capabilities. No doubt about that. It's a native 3D API that gives you full access to the rendering pipeline.",
|
||||
"host_name": "stackoverflow.com",
|
||||
"rank": 6,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.youtube.com/watch?v=vUYopHWOURg",
|
||||
"name": "Build a Full Stack Social App with React & Next.js - YouTube",
|
||||
"snippet": "Next.js Tutorial 2025 - Build a Full Stack Social App with React & Next.js. 147K views · 1 year ago. #nextjs #reactjs #programming ...more ...",
|
||||
"host_name": "www.youtube.com",
|
||||
"rank": 7,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,92 @@
|
||||
[
|
||||
{
|
||||
"url": "https://www.businessresearchinsights.com/market-reports/browser-games-market-108675",
|
||||
"name": "Browser Games Market Size, Trend | Forecast Report, 2035",
|
||||
"snippet": "In 2025, more than 15,000 browser-compatible HTML5 titles entered the market, representing a 2.7x annual increase. Browser Games Market Growth ...",
|
||||
"host_name": "www.businessresearchinsights.com",
|
||||
"rank": 0,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://dataintelo.com/report/browser-game-market",
|
||||
"name": "Browser Game Market Research Report 2034 - Dataintelo",
|
||||
"snippet": "The browser game market was valued at $11.8 billion in 2025 and is projected to reach $16.3 billion by 2034, growing at a 3.5% CAGR.",
|
||||
"host_name": "dataintelo.com",
|
||||
"rank": 1,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.researchandmarkets.com/reports/5939597/browser-games-market-report?srsltid=AfmBOoo1VO_oLfXCxOpyf3g8oL5J27bqCvTqcSojU1gdh1dTajVSzfIJ",
|
||||
"name": "Browser Games Market Report 2026 - Research and Markets",
|
||||
"snippet": "The browser games market size has grown steadily in recent years. It will grow from $7.81 billion in 2025 to $8.01 billion in 2026 at a compound annual growth ...",
|
||||
"host_name": "www.researchandmarkets.com",
|
||||
"rank": 2,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.juegostudio.com/blog/emerging-trends-for-modern-html5-game-development-in-2025",
|
||||
"name": "Emerging Trends for Modern HTML5 Game Development in 2026",
|
||||
"snippet": "In this blog, we'll explore the key trends transforming HTML5 games and how developers can leverage them to meet the evolving expectations of today's players.",
|
||||
"host_name": "www.juegostudio.com",
|
||||
"rank": 3,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.html5gamedevs.com/topic/57160-what-are-the-html5-game-development-trends-you-should-watch-in-2025",
|
||||
"name": "What are the HTML5 Game Development Trends You Should Watch",
|
||||
"snippet": "In 2025, HTML5 game development is set to grow in power and creativity. WebAssembly will keep games fast and loaded with complex features.",
|
||||
"host_name": "www.html5gamedevs.com",
|
||||
"rank": 4,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://metastatinsight.com/report/html5-games-market",
|
||||
"name": "HTML5 Games Market Size, Share, Growth by 2030 | Metastat Insights",
|
||||
"snippet": "The Global HTML5 Games market is estimated to reach $2,380.3 Million by 2030; growing at a CAGR of 7.7% from 2023 to 2030.",
|
||||
"host_name": "metastatinsight.com",
|
||||
"rank": 5,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.linkedin.com/pulse/rise-casual-html5-games-opportunities-investors-xdwlf",
|
||||
"name": "The Rise of Casual HTML5 Games: Opportunities for Investors",
|
||||
"snippet": "The global HTML5 games market was valued at approximately USD 5.32 billion in 2024 and is projected to reach USD 9.22 billion by 2033, growing ...",
|
||||
"host_name": "www.linkedin.com",
|
||||
"rank": 6,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.reddit.com/r/gamedev/comments/1mnaw7r/what_do_you_think_about_online_browser_games_in",
|
||||
"name": "What do you think about online browser games in 2025? (+ - Reddit",
|
||||
"snippet": "A couple things to consider- the accessibility of web games makes the potential audience much larger (especially internationally), and web games ...",
|
||||
"host_name": "www.reddit.com",
|
||||
"rank": 7,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.youtube.com/watch?v=5omoZzrSxhU",
|
||||
"name": "Building the Next Generation of Play in the Browser | #IGDC2025",
|
||||
"snippet": "... Web Games: Building the Next Generation of Play in the Browser | #IGDC2025 ... Game Design Real Lessons From a Top Cricket Title | #IGDC2025.",
|
||||
"host_name": "www.youtube.com",
|
||||
"rank": 8,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
},
|
||||
{
|
||||
"url": "https://www.pocketgamer.biz/the-big-games-industry-trends-and-stories-to-watch-in-2025",
|
||||
"name": "The big games industry trends and stories to watch in 2025",
|
||||
"snippet": "The big games industry trends and stories to watch in 2025. A look at a few of the key stories to keep an eye on this year.",
|
||||
"host_name": "www.pocketgamer.biz",
|
||||
"rank": 9,
|
||||
"date": "",
|
||||
"favicon": ""
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,167 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.13 0.02 290);
|
||||
--foreground: oklch(0.97 0.01 200);
|
||||
--card: oklch(0.19 0.025 290);
|
||||
--card-foreground: oklch(0.97 0.01 200);
|
||||
--popover: oklch(0.19 0.025 290);
|
||||
--popover-foreground: oklch(0.97 0.01 200);
|
||||
--primary: oklch(0.78 0.17 162);
|
||||
--primary-foreground: oklch(0.15 0.02 290);
|
||||
--secondary: oklch(0.28 0.03 290);
|
||||
--secondary-foreground: oklch(0.97 0.01 200);
|
||||
--muted: oklch(0.26 0.02 290);
|
||||
--muted-foreground: oklch(0.72 0.02 200);
|
||||
--accent: oklch(0.32 0.05 320);
|
||||
--accent-foreground: oklch(0.97 0.01 200);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.72 0.16 320);
|
||||
--chart-1: oklch(0.72 0.19 162);
|
||||
--chart-2: oklch(0.74 0.18 15);
|
||||
--chart-3: oklch(0.82 0.17 75);
|
||||
--chart-4: oklch(0.72 0.22 320);
|
||||
--chart-5: oklch(0.78 0.13 195);
|
||||
--sidebar: oklch(0.19 0.025 290);
|
||||
--sidebar-foreground: oklch(0.97 0.01 200);
|
||||
--sidebar-primary: oklch(0.78 0.17 162);
|
||||
--sidebar-primary-foreground: oklch(0.15 0.02 290);
|
||||
--sidebar-accent: oklch(0.32 0.05 320);
|
||||
--sidebar-accent-foreground: oklch(0.97 0.01 200);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.72 0.16 320);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
background-color: #050410;
|
||||
}
|
||||
}
|
||||
|
||||
/* 全局滚动条 */
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.14); border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.24); }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
|
||||
/* 玻璃态卡片 */
|
||||
.glass {
|
||||
background: rgba(20, 16, 40, 0.45);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
/* 渐变文字 */
|
||||
.text-gradient {
|
||||
background: linear-gradient(135deg, #34d399 0%, #e879f9 50%, #fb7185 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* 通用脉冲 */
|
||||
@keyframes echo-glow {
|
||||
0%, 100% { opacity: 0.6; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* 教程系统动画 */
|
||||
@keyframes tut-pulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 0 2px rgba(232,121,249,0.9), 0 0 24px 4px rgba(232,121,249,0.4);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 3px rgba(232,121,249,1), 0 0 32px 8px rgba(232,121,249,0.7);
|
||||
}
|
||||
}
|
||||
|
||||
/* 教程提示气泡入场 */
|
||||
@keyframes tut-pop-in {
|
||||
0% { opacity: 0; transform: scale(0.92) translateY(8px); }
|
||||
100% { opacity: 1; transform: scale(1) translateY(0); }
|
||||
}
|
||||