Compare commits

..
42 Commits
Author SHA1 Message Date
Super_Z 5bed2599f5 v0.16: 解码阈值叙事事件 + loreLayer 已读标记 + idleHistory 详情 + 移动端适配
- 5 个解码阈值叙事里程碑(ms_5/12/25/40/60)+ 全屏碎片释放叠层
- loreLayer 已读标记(readLoreLayers + 点击文本标记 + 已读/未读视觉区分)
- idleProjectHistory 详情展开(倒序最近 5 条 + 相对时间)
- 新建 format.ts formatRelativeTime 工具函数
- 移动端 375px 适配审计(4 组件字号 9→10px + flex-wrap + 响应式 padding)
- 严格 4 色全息,lint 零错误,agent-browser 端到端验证通过
2026-06-24 23:45:34 +00:00
Super_Z 174d1227c0 68f373b9-921a-4acf-844c-ab7e89ffb727 2026-06-24 22:50:18 +00:00
Z.ai Code 774ddaf9d1 docs: v0.15 部署记录 + main 分叉说明 2026-06-24 22:47:11 +00:00
Super_Z 22588425cf 48c8e466-e804-444f-9f6d-4a2d34b3379a 2026-06-24 12:34:05 +00:00
Super_Z 96748d726f v0.15: 叙事深化 + 放置平衡 + UX 增强
- loreLayers 多层碎片文本(8 碎片 × 2 层 = 16 层深层叙事,5 条暗线交织)
- 永久加成 softcap(≤30 全额 / 30-100 ×0.5 / >100 ×0.25,防滚雪球)
- 批量领取全部工程 + 工程完成 toast 通知
- StatsPanel 独立组件 + idle 统计行(永久加成/完成工程/产出晶体/历史)
- Codex 分层渲染(L0 emerald 表层 / L1 amber 隐秘注脚 / L2 fuchsia 深层回响)
- 修复 v0.14 双重计算 bug(idleTotalPerSec)
- 严格 4 色全息,lint 零错误,agent-browser 端到端验证通过
2026-06-24 12:32:56 +00:00
Super_Z 20dc1f252f docs: v0.14 README + 放置系统设计文档 + worklog 更新 2026-06-24 10:23:08 +00:00
Super_Z 17820b1f28 v0.14: 放置系统 (Idle Operations) — 采矿无人机舰队 + 6放置工程 + 永久产能加成 + idle徽章 2026-06-24 10:19:40 +00:00
Super_Z 1701a908c9 docs: worklog 更新 v0.8.1 P1 完成记录 2026-06-23 22:35:30 +00:00
Super_Z 71ca5b48a9 v0.8.1: 探险平衡 + 信标系统扩展(周挑战 + 信标链)
P1-a 探险 BOSS 胜率平衡 + 能量恢复(expedition.ts + config.ts + gameStore.ts + ExpeditionPanel.tsx):
- combatWinRate: scale 8→4, floor 0.25→0.35(基础力10对BOSS胜率 25%→35-45%)
- BOSS 难度 5-7→3-5(配合新公式基础胜率达 45-55%)
- computeEnergyRegenInterval(state): 动态恢复间隔
  - exp_2 解锁 -30%, exp_3 解锁 -20%, 探索力属性 -最高30%
  - 下限 12s(原固定 45s)
- computeEnergyRegen 接受 intervalSec 参数
- gameStore tick 传入动态间隔
- config.ts: exp_2/exp_3 描述加'能量恢复 +30%/+20%'
- ExpeditionPanel: 显示实际恢复速度 + '已加速'标记

P1-b 信标系统扩展(beacon.ts + BeaconPanel.tsx + gameStore.ts)[subagent 9-b]:
- 周挑战: getWeekKey ISO 8601 + FNV-1a 种子确定性生成
  - 难度强制 anomaly 60%/singular 40%, goal 日挑战×3-5倍
  - 完整进度/领奖/排行榜推送(isWeekly标记)
- 信标链: 4里程碑(3/7/14/30天) + grace续命机制(每链1次)
  - recordChainCompletion 核心断链/续命逻辑
  - 奖励 50→680洞见递增
- BeaconPanel: 周挑战fuchsia主题 + 信标链amber→rose渐变里程碑节点
- gameStore: trackBeacon升级 + claimWeeklyBeacon/claimChainReward actions
- VLM 8/10, lint零错误, 5场景bun测试全PASS
2026-06-23 22:34:52 +00:00
Super_Z a3aa381656 docs: worklog 更新 v0.8 完成记录 + 工单修正 + cron 228357 2026-06-23 21:31:26 +00:00
Super_Z 494bc5ff64 v0.8: 巡航玩法大增强 - BOSS 战 + 道具掉落 + 事件选择节点
- cruise.ts (+1487行): BossEntity/BossPhase/BulletEntity/PowerupEntity/UpgradeCard 类型
  - BOSS 三阶段攻击(直射/散射/追踪弹+召唤), HP=100+lvl×20
  - 飞船射击系统(空格/射击按钮, 能量消耗5/发, 150ms冷却)
  - 5种道具掉落(shield/energy/rapid/overdrive/magnet)
  - 10种升级牌(速度/护盾/能量/火力/吸引/无敌等), 每关3选1
  - shootShip/applyUpgradeCard/rollUpgradeChoices 函数
  - generateLevel: level%5===0 生成BOSS关
- CruiseMode.tsx (+858行): BOSS多层环+HP条+触手绘制, 子弹尾焰, 道具六边形+图标
  - buff状态指示器, 选择升级阶段3张玻璃卡牌翻转
  - 射击按钮(移动端右下), BOSS HP条(顶部), buff列表(左侧)
  - 修复 lint: isTouch/stats 改用 useState initializer
- page.tsx: 版本号 v0.7 → v0.8
- worklog.md: 工单修正(标注v0.7已完成), cron job_id 228357重建
2026-06-23 21:30:43 +00:00
Super_Z 43dfc2d41b docs: README 添加在线游玩链接 + v0.7 状态更新 2026-06-23 21:10:47 +00:00
Super_Z 3d916a1574 0804500b-ffe8-4b5f-8419-5e7aafd9fc73 2026-06-23 21:01:32 +00:00
Super_Z 4545fe380b v0.7: CrystalOrb Canvas 粒子系统升级 + 角色属性系统(四维)
CrystalOrb 升级(P0 视觉震撼):
- 从 CSS 动画重写为 Canvas 2D 粒子系统
- 环绕能量粒子(3层32个,emerald/fuchsia/rose,带拖尾)
- 环境星尘(40个,闪烁漂移)
- 点击爆发粒子 + 冲击波环(连击解锁更多层)
- 晶核辉光呼吸 + 3层六边形纹理 + 鼠标视差
- VLM 视觉评分 8/10

角色属性系统(P1 RPG 深度):
- 新增 attributes.ts 逻辑层 + AttributesPanel UI
- 四维属性:探索力/智慧/勇气/灵感(emerald/fuchsia/amber/rose)
- 两区加成公式(0-50线性,50-100递减)
- 飞升获得属性点(ascensions×2+1),手动分配
- 完成活动获得属性经验,自动升级
- 接入 pulse/clickNode/resolveCurrentNode/grantCruiseReward/tickTide
- 旧存档兼容(migrateAttributes)
- 新增 2 项成就 + 第 8 个标签页「角色」
- VLM 视觉评分 7-8/10

lint 零错误;HTTP 200
2026-06-23 21:00:42 +00:00
Super_Z 8474f4caa7 0c799010-d26c-48d2-8c38-f465011f59d8 2026-06-23 20:32:33 +00:00
Super_Z c0535e6ebe d3c86ebe-91f4-4f29-a530-6cb46cab2abc 2026-06-23 20:19:46 +00:00
Super_Z 7111e154b2 906d0c00-e2bc-4edd-ae26-665c03a1ba37 2026-06-23 20:10:42 +00:00
Super_Z 72e1f3fc3c da4fe2ad-c3b9-41fc-82e7-336a69a7947c 2026-06-23 19:51:59 +00:00
Super_Z fafa2ee20a v0.6: 深空巡航 Canvas 2D 实时玩法 + UI 偏移重叠修复
## 玩法拓展(回应用户: 玩法太单调/全是点点点)
- 新增「深空巡航」Canvas 2D 全屏实时玩法
  - 飞船操控(WASD/方向键/移动端虚拟摇杆)
  - 实时躲避陨石/虚空风暴,收集晶体/洞见/信标
  - 到达星门通关,护盾归零失败
  - 3层视差星空 + 粒子系统(尾焰/收集/碰撞/烟花) + 辉光 + 屏幕震动
  - 奖励同步主游戏(grantCruiseReward)
  - 独立 localStorage 记录最高分/通关数

## UI 修复(回应用户: 解析球左上偏移/成就小屏重叠)
- DecodeArray 节点: 移除 Tailwind translate 与 inline transform 冲突
- AchievementsPanel: 移除固定 max-h, 改 flex-1 min-h-0 自适应
- page.tsx 布局: 小屏减小 min-h, 避免挤压重叠

## 集成
- gameStore 新增 grantCruiseReward action
- header 新增 amber 主题「巡航」按钮 + 全屏 CruiseMode 渲染

## 文件
- src/lib/game/cruise.ts (新增, ~1008行)
- src/components/game/CruiseMode.tsx (新增, ~1350行)
- src/app/page.tsx (集成巡航入口)
- src/store/gameStore.ts (grantCruiseReward action)
- src/components/game/DecodeArray.tsx (节点偏移修复)
- src/components/game/AchievementsPanel.tsx (小屏重叠修复)

QA: lint 零错误; agent-browser+VLM 验证主界面无偏移+巡航玩法正常运行
2026-06-23 19:33:51 +00:00
Super_Z fcf94b6c3f docs: update worklog for v0.5.2 (offline report + persist fix) 2026-06-23 17:04:58 +00:00
Super_Z 296c609479 v0.5.2: offline report + tab UI + persist race fix
- New: Offline earnings report dialog (welcome back popup with gain/duration/rate/progress)
  - src/lib/game/offlineReport.ts: module-level store + custom event
  - src/components/game/OfflineReportDialog.tsx: Radix Dialog UI
  - gameStore.init: setPendingOfflineReport before set()
- Fix: useGameLoop waits for persist.hasHydrated() before init
  - Root cause: Zustand persist rehydrate is async (Promise.resolve wrapper)
  - Before fix: init read INITIAL_STATE (lastTick=undefined), skipped offline branch
  - After fix: init waits for rehydrate, correctly reads saved lastTick
- UI: Tab bar redesign with per-tab accent colors + flex-col layout
- Version: v0.5 -> v0.5.2
2026-06-23 17:04:28 +00:00
Super_Z bd28219b65 docs: update worklog for v0.5.1 (Gitea Pages deploy + tutorial + UI) 2026-06-23 16:38:44 +00:00
Super_Z 8605f9bb5f chore: remove QA screenshots from repo + gitignore
- Remove qa-static-*.png and qa-tut-01.png (temporary QA artifacts)
- Add qa-*.png to .gitignore
2026-06-23 16:37:49 +00:00
Super_Z 00a46a0bf9 Deploy: v0.5.1 static export
- New: Tutorial system (7-step onboarding with spotlight + action detection)
- UI: ResourceBar spacing optimization (gap, dividers, tabular-nums)
- UI: SettingsDialog adds tutorial replay + online play link
- Anim: tutorial pulse + pop-in keyframes
2026-06-23 16:34:00 +00:00
Super_Z 084387174b 69e481d9-d5e1-44d1-9561-71b5173e04bb 2026-06-23 15:25:33 +00:00
Super_Z ab511fe9e0 chore: update docs submodule ref (v0.5 beacon docs) 2026-06-23 15:24:51 +00:00
Super_Z 60445abd42 v0.5: 深空信标系统 + 编年史历史 BUG 修复
新功能:深空信标(Deep Space Beacon)
- 每日挑战:UTC 日期种子确定性生成,5 种类型 × 3 档难度
- 本地排行榜 Top 20,奖牌图标 + 难度色点 + 今日高亮
- 进度追踪独立 localStorage,不污染 GameState
- 倒计时 + 领取奖励发放到游戏状态

BUG 修复:编年史历史条目显示原始 tide 键名
- 新增 regenerateLoreFromEntry(),显示时重新生成 lore
- 修复 v0.4 之前条目 lore 中 tide_ruins 等原始键名

UI:第 7 标签页「信标」+ 统计面板「信标最高分」行
版本号 v0.4 → v0.5

详见 docs/10-深空信标系统-v0.5.md
2026-06-23 15:24:51 +00:00
Super_Z 04f6a398f4 735ccca3-a163-4b49-8cb2-c04c3a043ee7 2026-06-23 15:24:51 +00:00
Super_Z a02b6191c5 fix: websocket example eslint set-state-in-effect 2026-06-23 15:24:24 +00:00
Super_Z 5bc041ac0b docs: v0.5 深空信标系统文档 2026-06-23 15:24:24 +00:00
Super_Z 697e1f360a docs: 更新 worklog 至 v0.4 完成 2026-06-23 14:48:15 +00:00
Super_Z e6ec22132e docs: v0.4 回响编年史系统设计文档 2026-06-23 14:46:06 +00:00
Super_Z e69c48ba69 feat(v0.4): 回响编年史 — 跨周目叙事时间轴系统
- 新增 ChronicleEntry / RunStartSnapshot 类型与 GameState 字段
- 新建 chronicle.ts: 5 纪元名生成、模板化叙事、里程碑检测、run delta 计算
- engine.ts performPrestige 构建编年史条目并重置 runStart
- gameStore: init 兼容旧存档 + tickTide 记录星潮 + resolveCurrentNode 计 BOSS 击杀
- chooseConstellationPerk 回填最近一条 entry 的 perksThisAscension
- 新建 ChronicleDialog.tsx: 时间轴 UI + 5 纪元循环色 + 入场动画
- 新增 6 项成就: 首部编年/三纪元回响/五纪元闭环/首杀维度/维度猎手/星潮亲历者
- 新增 chronicle / chronicleOpen 程序化音效
- 修复关键 BUG: PrestigeDialog disabled 逻辑错误导致玩家永远无法飞升
- 修复 UX: newBp=0 时不再永久禁用飞升按钮(v0.3.1 星图+v0.4 编年史已提供动机)
- 统计面板新增 BOSS 击破/星潮亲历/编年史条目 3 行
- 版本号升至 v0.4
2026-06-23 14:45:38 +00:00
Super_Z 98fbbf1634 docs: 更新 worklog v0.3.1 收尾记录 2026-06-23 14:15:57 +00:00
Super_Z 9a17849859 chore: 清理未用变量 + 新增星图天赋目标提示 + ignore skills/ 2026-06-23 14:15:29 +00:00
Super_Z cc02fc2e7f Merge branch 'main' of https://git.atdunbg.xyz/Super_Z/echo-nexus
# Conflicts:
#	.gitignore
2026-06-23 14:13:30 +00:00
Super_Z 9f4d839c6b feat(v0.3.1): 星图天文台元进程天赋系统 — 18 天赋 / 3 选 1 draft / Canvas 动态星图
- 新增 src/lib/game/constellation.ts:6 类别 × 3 天赋 = 18 个永久天赋,
  constellationBonuses() 聚合 19 项修饰器,rollPerkChoices() 保证不同类别
- 状态扩展:GameState.constellation / pendingPerkChoices
- engine.ts: recomputeStats 聚合三层加成(技术+蓝图+成就+星图),
  performPrestige 触发天赋选择 + 飞升礼包补偿,新增 rollCrystalTierWithBonus
- expedition.ts: 探险力/生命应用星座修饰器
- gameStore.ts: 新增 chooseConstellationPerk + rerollPerkChoices 动作,
  全链路接入(tickTide/autoDecodeTick/clickNode 等用点)
- 新增 ConstellationPanel.tsx: Canvas 动态星图(六边形 6 星座 × 3 星点,
  闪烁/光晕/十字光线/连接线/中心星核呼吸)+ Hover 提示 + 类别图例
- 新增 ConstellationDialog.tsx: 飞升后自动弹出,3 卡片 draft + 重新抽取
- PrestigeDialog: 增加「星图觉醒预告」卡片
- page.tsx: 6 列标签栏 + 顶部「觉醒」按钮 + 统计面板加星图天赋
- audio.ts: 新增 constellation SFX(上升琶音 + 高频闪光)
- achievements.ts: 新增「星图初绘」「六分星辉」2 项成就
- QA: agent-browser + VLM 全流程通过;lint 零错误;编译 < 200ms
- 二次回应 Issue #1:从「连连看」扩展为 6 大玩法层 + 元进程 draft
2026-06-23 14:12:37 +00:00
Super_Z f3d2e53e4f 42773b7e-7147-4ad5-828d-da7fcf0290b8 2026-06-23 13:52:13 +00:00
Super_Z da18f32df2 3d8566bc-063f-41e8-8934-68c9ac5b8211 2026-06-23 13:38:23 +00:00
Super_Z 674775be0e 6f5491c0-6c94-4f8e-8ad3-136d04ab862f 2026-06-23 13:04:15 +00:00
Super_Z f73b96026c d9404a31-1970-4e79-ac9a-f4f9db60350b 2026-06-23 12:47:49 +00:00
Z User e193530f24 Initial commit 2026-06-23 11:50:15 +00:00
203 changed files with 30382 additions and 19 deletions
Executable
+1
View File
@@ -0,0 +1 @@
DATABASE_URL=file:/home/z/my-project/db/custom.db
Regular → Executable
+12
View File
@@ -136,3 +136,15 @@ dist
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
# Echo Nexus 本地文件
local-*
.claude
.z-ai-config
dev.log
dev.out.log
*.tsbuildinfo
next-env.d.ts
.vercel
skills/
qa-*.png
+122
View File
@@ -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"
+1
View File
@@ -0,0 +1 @@
1061
+154
View File
@@ -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
+78
View File
@@ -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
+65
View File
@@ -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
+123
View File
@@ -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
+135
View File
@@ -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
Executable
+23
View File
@@ -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}
}
}
}
Regular → Executable
+181 -19
View File
@@ -1,35 +1,197 @@
# 回响星核 / Echo Nexus # 回响星核 / 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.1MVP 开发中) - **版本****v0.16** — 解码阈值叙事事件(全屏碎片释放叠层)+ loreLayer 已读标记 + idleHistory 详情展开 + 移动端 375px 适配审计
- **栈**Next.js 16 · TypeScript · Tailwind · shadcn/ui · Canvas · Zustand · Prisma - **栈**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://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/01-调研报告.md`](docs/01-调研报告.md) — 游戏市场调研
- [`docs/02-游戏设计文档.md`](docs/02-游戏设计文档.md) — GDD 策划设计 - [`docs/02-游戏设计文档.md`](docs/02-游戏设计文档.md) — GDD 策划设计
- [`docs/03-技术架构.md`](docs/03-技术架构.md) — 技术架构 - [`docs/03-技术架构.md`](docs/03-技术架构.md) — 技术架构
- [`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 已读 + 移动端适配**
- v0.1 放置 + 解码 + 技术树 + 图谱 + 飞升(当前)
- v0.2 遗迹探险肉鸽
- v0.3 音频 + 全局星潮 socket 事件
- v0.4 云存档 + 排行榜
- v0.5 全 5 纪元叙事 + 隐藏内容
--- ---
## 本地开发
```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 · 持续开发中 © Super_Z · 持续开发中
+89
View File
@@ -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.tsxv0.15 loreLayers 分层渲染)+ StatsPanel.tsxv0.15 抽出独立组件)+ types.ts GameState + config.ts FRAGMENTS/loreLayers/isLoreLayerUnlocked + gameStore.tsactions + persist migrate+ IdleOperationsPanel.tsx + BeaconPanel.tsx + ExpeditionPanel.tsx
- 子任务 1loreLayer 已读标记
- 新建 `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 适配)
- 子任务 2idleProjectHistory 详情展开
- `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 订阅用 useShallowCodex / 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 不重置)
- ✅ 子任务 2idleProjectHistory 详情展开完整实现
- "放置工程历史" 行可点击展开/收起
- 展开后显示最近 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 200dev.log 无运行时错误(compile 11ms / render 33ms
- ✅ 与子代理 A 文件边界完全无冲突(不同字段/action/migrate 行)
- ⚠️ 未做 agent-browser 端到端验证(按工单由主控完成)
+52
View File
@@ -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-50DPR cap 2resize 监听
- **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 + recordRunrewardGrantedRef 防重
### 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) 四色全息色谱,零蓝色/靛色
+103
View File
@@ -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 actionpulse/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 逻辑层
- **类型系统**AttributeKey4 个键)/ CharacterAttributes0-100 数值)/ AttributeProgressEntryexp + 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
- 顶部:标题 + 待分配点数 badgependingAttrPoints > 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
- ✅ 编译 < 250ms177ms 实测)
- ✅ 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] 数值,避免漂移
+141
View File
@@ -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 CHALLENGEsection**
- `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 CHAINsection**
- `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" + 大字号 streakchain-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 全程无错误,所有编译 < 300msHTTP 200
### 3. 信标链逻辑测试(bun 直接运行 TS,5 个场景全 PASS)
1. ✅ 昨日 streak=1 → 今日完成 → streak=2normal increment
2. ✅ 同日重复完成 → 忽略
3. ✅ 明日完成 → streak=3,无需 grace
4. ✅ 隔一天 misslastCompleted + 2 天)→ streak=4graceUsed=1(续命触发)
5. ✅ 再次 missgrace 已用)→ 断链 streak=1graceUsed=0
### 4. UI 集成测试(agent-browser
- localStorage 模拟设置 chain statelastCompletedDateKey=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 keysecho-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` — 重置后干净状态
Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

+139
View File
@@ -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 限 crystalCapenergy 限 energyMaxcontact 限 100permBonus 累加到 idlePermanentBonus + 重算 statsrandomFragment 从未解锁碎片中随机一个)→ 追加 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` statecontrolled Tabs`<Tabs value={activeTab} onValueChange={setActiveTab}>`
- 挂载后 useEffect 一次性设置默认 tabhasActiveExpedition → "expedition"hasPendingPerk → "constellation",否则 → "idle"
- 版本号 v0.8 → v0.14
- Header 在 StarTideIndicator 后加 IdleStatusBadge
- 左侧 CrystalOrb 后加 IdleProjectBar
- TabsList grid-cols-8 → grid-cols-9,新增 `value="idle"` 的 TabsTrigger 作为第一个 tabClock 图标,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 + 晶体球下方进度条(派遣后)
Executable
+1965
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -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"
}
Executable
BIN
View File
Binary file not shown.
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
View File
View File
View File
View File
View File
+175
View File
@@ -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 delta2分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 云存档+排行榜会将其纳入同步范围
+151
View File
@@ -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 日期 keyYYYY-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.090 秒完成 → 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.113.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 多人同步星潮(全局事件,所有在线玩家同时经历)
+309
View File
@@ -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 的本质:**让玩家信任系统会在他不在时为他工作**。
+214
View File
@@ -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 洞见 · ...`
UISection 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-a5 文件):**
- `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-b3 文件):**
- `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 200dev.log 无运行时错误
- ✅ agent-browser 端到端 smoke test
- softcappermBonus=50 → 有效 40UI 显示"衰减中" ✓
- 批量领取:permBonus 0→0.3, projectsCompleted 1→2 ✓
- 完成 toasttickIdleProjects 触发 ✓
- 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 弹层可读性实测
+231
View File
@@ -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 的 threshold1/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-shadow120px + 240px 双层)+ `narrative-breathe` 3.5s 呼吸循环
- **中央卡片**max-w-2xl4 色主题渐变背景 + 主题色 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` 300msopacity 0→1+ `narrative-scale-in` 300ms cubic-bezierscale 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-a6 文件):**
- 新建 `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 200dev.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 复用到编年史时间轴 + 探险日志
Submodule
+1
Submodule docs/repo added at a02b6191c5
+1
View File
@@ -0,0 +1 @@
Here are all the generated files.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

+50
View File
@@ -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;
+197
View File
@@ -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>
);
}
+138
View File
@@ -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)
})
})
View File
Executable
+26
View File
@@ -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;
Executable
+95
View File
@@ -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"
}
}
+5
View File
@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;
+32
View File
@@ -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
}
+29
View File
@@ -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

+14
View File
@@ -0,0 +1,14 @@
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
User-agent: Twitterbot
Allow: /
User-agent: facebookexternalhit
Allow: /
User-agent: *
Allow: /
+92
View File
@@ -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": ""
}
]
+92
View File
@@ -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": ""
}
]
+92
View File
@@ -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": ""
}
]
+74
View File
@@ -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": ""
}
]
+92
View File
@@ -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": ""
}
]
+167
View File
@@ -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); }
}

Some files were not shown because too many files have changed in this diff Show More