清理:删除无用文档

This commit is contained in:
桃树夭 committed 2026-08-14 16:26:16 +08:00
1 parent 4cd9b85c3a
commit c4f9e5375a
12 files changed
+316 -5898

No files matched your search

+13 -289
View File
@@ -8,8 +8,7 @@
<strong>一款 AIGC 驱动、高自由度文字体验、修仙世界观的开源游戏项目。</strong>
</p>
> 本仓库当前实现为 `Hono + React SPA`。
> 这里的说明以现有代码为准,已不再适用于旧版 Next.js 架构。
> 本仓库当前实现为 `Hono + React SPA`。这里的说明以现有代码为准,已不再适用于旧版 Next.js 架构。
---
@@ -43,18 +42,18 @@
<img src="https://page-r2.daoyou.org/index/Xnip2026-02-02_19-02-21.png" alt="云游坊市" width="260" />
</p>
## 当前技术栈
## 技术概览
- 服务端:`Hono 4` + `Bun`
- 前端:`React 19` + `React Router 7` + `Vite 8`
- 样式:`Tailwind CSS 4`
- 数据库:`PostgreSQL` + `Drizzle ORM`
- 认证:`Better Auth`
- 缓存 / 分布式协调:`Redis`
- 消息与实时广播:`NATS JetStream` + `NATS Core`
- 认证:`Better Auth`
- AI 能力:`AI SDK` + `DeepSeek`
## 当前目录结构
## 仓库布局
```text
.
@@ -64,292 +63,14 @@
├── src/shared/ # 共享引擎、配置、类型、契约
├── drizzle/ # 业务表 Drizzle migrations
├── drizzle-auth/ # Better Auth Drizzle migrations
├── drizzle.auth.config.ts # Better Auth 独立迁移配置
├── scripts/ # 部署脚本与生产/NATS Compose
├── docker/Dockerfile.app # Bun 主服务镜像
└── vite.config.ts
```
## 运行方式
## 本地开发与部署
这个仓库不是 SSR 应用。
- `src/react-app` 使用 `BrowserRouter` 管理前端路由
- `src/server/app.ts` 提供 `/api/*` 和 `/internal/*` 接口
- `src/index.ts` 在生产环境注册 Bun 内置 cron;Cron 只向 NATS WorkQueue 发布后台 command
- 前端 SPA 独立部署到 Cloudflare Pages;后端 Docker 不再服务 `index.html` 或静态资源
当前路由约定:
- `/api/*`:游戏与后台 API
- `/api/auth/*`:Better Auth
- `/internal/cron/*`:内部定时任务接口
- `/api/health-check`:健康检查
- 其余如 `/login`、`/game`、`/admin`:Cloudflare Pages 上的前端 SPA 路由
## 环境要求
- `Bun 1.3+`
- `PostgreSQL`
- `Redis`:在线对局、邀请、截止时间、恢复索引和 API 部分能力的权威存储
- `NATS`:进程启动硬依赖;JetStream 承载领域事件、异步投影、后台 command、战斗演算指针、终态清理和回放归档,Core 只承载可丢失的跨实例实时提示
说明:
- 仓库脚本默认围绕 `bun` / `bunx` 编写,不建议继续沿用旧的 `npm + Next.js` 使用方式
- 开发模式默认端口是 `5173`
- 构建后服务默认端口是 `3000`
## 安装
```bash
bun install
cp .env.example .env.local
```
## 环境变量
### 启动时必需
这些变量缺失时,服务会在启动阶段或鉴权初始化阶段直接报错:
| 变量 | 说明 |
| --- | --- |
| `DATABASE_URL` | PostgreSQL 连接串 |
| `BETTER_AUTH_SECRET` | Better Auth 密钥 |
| `BETTER_AUTH_URL` | Better Auth 后端对外基准地址;生产填 API 域名,如 `https://api.example.com` |
| `NATS_SERVERS` | NATS 服务地址,多个地址使用逗号分隔 |
| `NATS_USER` / `NATS_PASSWORD` | NATS 应用用户凭据 |
实时战斗由 Bun/Hono 主服务直接承载;生产环境还必须配置:
| 变量 | 说明 |
| --- | --- |
| `REDIS_URL` | 在线对局唯一权威状态、邀请、凭据、截止时间与恢复索引 |
| `NATS_SERVERS` / `NATS_USER` / `NATS_PASSWORD` | 战斗演算、终态清理和回放归档使用的 JetStream,以及跨实例状态提示使用的 NATS Core |
客户端通过已认证的 session API 获取 60 秒有效的一次性 WebSocket ticket,不能自行声明玩家身份。实时战斗的数据边界固定为:`battle-v5` 只做确定性规则解析;Bun 主服务负责协议、调度和广播;进行中的权威状态、选招、锁定、邀请、演出战报和回放素材只在 Redis。主服务通过 JetStream 小型指针任务分配统一演算和终态清理;回放归档 consumer 再从 Redis 组装 `BattleReplayV1`,异步、幂等写入 `wanjiedaoyou_battle_replay_archives`。NATS 消息不承载完整 battle save 或战报;玩家 command 链路不查询或写入 PostgreSQL,也不使用 Redis Stream。
### 建议同时配置
| 变量 | 说明 |
| --- | --- |
| `REDIS_URL` | Redis 连接串;缺失时相关功能会在运行时失败 |
| `API_IP_RATE_LIMIT_WINDOW_SECONDS` | `/api/*` 全局 IP 令牌桶补充周期秒数;默认 `60` |
| `API_IP_RATE_LIMIT_MAX_REQUESTS` | `/api/*` 同 IP 令牌桶容量和每周期补充 token 数;默认 `300` |
| `PUBLIC_WEB_ORIGINS` | 允许访问 API 的前端 origin,逗号分隔,如 `https://app.example.com,http://localhost:5173` |
| `BETTER_AUTH_COOKIE_DOMAIN` | 可选;同站子域部署时可填 `.example.com` 启用跨子域 cookie |
| `ADMIN_EMAILS` | 管理员邮箱白名单,逗号分隔 |
| `ADMIN_USER_IDS` | Better Auth 管理员用户 ID 白名单,逗号分隔;账号管理工具必须配置 |
### 生产 cron 必需
| 变量 | 说明 |
| --- | --- |
| `CRON_SECRET` | 保护 `/internal/cron/*` 接口的 Bearer 密钥;生产环境必须配置,调度器调用时也要携带它 |
### 登录 / 注册相关
当前鉴权中,以下接口会强制要求 ALTCHA PoW payload:
- `/api/auth/sign-in/email`
- `/api/auth/sign-up/email`
- `/api/auth/request-password-reset`
- `/api/auth/email-otp/send-verification-otp`
前端通过 `/api/captcha/challenge` 获取带场景和过期时间的 challenge,完成 PoW
后把 payload 发送给认证接口。服务端会验证签名、场景和有效期,并通过 Redis
原子记录 challenge 的单次消费状态,防止重放。
| 变量 | 说明 |
| --- | --- |
| `VITE_API_BASE_URL` | 前端构建时注入的后端 API 基地址,如 `https://api.example.com` |
| `ALTCHA_HMAC_SECRET` | 服务端签发和验证 ALTCHA challenge 的独立 HMAC 密钥;生产环境必须配置 |
ALTCHA 不需要前端 site key。认证 CAPTCHA 启用时 Redis 也是强依赖;Redis 不可用时,
受保护的认证请求会失败关闭,避免 challenge 被重复使用。
### 邮件能力
邮箱验证码、密码注册验证邮件、重置密码邮件、后台邮件广播都会使用SMTP。密码注册必须完成邮箱验证后才能登录;验证链接完成后会自动登录:
| 变量 | 说明 |
| ----------------------------------------- | ------------- |
| `SMTP_HOST` / `SMTP_PORT` / `SMTP_SECURE` | SMTP 连接配置 |
| `SMTP_USER` / `SMTP_PASS` | SMTP 认证信息 |
| `MAIL_FROM` | 发件人 |
### AI 能力
AI 相关功能只调用 DeepSeek 官方 API。服务端默认配置:
- `DEEPSEEK_API_KEY`:服务端 DeepSeek API Key
- `DEEPSEEK_MODEL`:服务端默认模型,未配置时使用 `deepseek-chat`
玩家也可以在游戏设置中保存自己的 DeepSeek API Key 与模型。BYOK 配置只保存在当前浏览器;请求携带的配置不完整或格式无效时,服务端会返回 400,不会静默消耗服务器额度。
## 数据库初始化
首次启动通常要做两件事:
1. 应用业务表迁移
2. Better Auth 表迁移
```bash
bunx drizzle-kit migrate
bun run auth:migrate
```
说明:
- 运行这些命令前,请先确保 `DATABASE_URL`、`BETTER_AUTH_SECRET`、`BETTER_AUTH_URL` 已在当前进程环境中可见
- `drizzle/` 目录下已经存在业务表迁移文件
- `drizzle/` 只管理 `wanjiedaoyou_*` 业务表
- `drizzle-auth/` 只管理固定 `better_auth` schema,并使用独立迁移历史表
- `bun run auth:migrate` 使用 `drizzle.auth.config.ts` 执行认证迁移
- `bun run auth:generate` 用于认证 Drizzle schema 变更后生成迁移,不是每次启动都要执行
- 升级部署时先执行 `bun run auth:migrate` 建立认证基线,再部署使用共享 Bun SQL 连接池的新版本
## 本地开发
1. 准备好 `.env.local`
2. 确保数据库、Redis 和 NATS JetStream 可连接
3. 执行迁移
4. 启动开发服务器
```bash
docker compose -f scripts/docker-compose.nats.yml up -d
bun run dev
```
本地 NATS 容器监听 `4222`,监控端口为 `8222`,开发凭据与 `.env.example` 一致。持久数据保存在 Docker volume `nats-data` 中。
生产硬切顺序、Stream/consumer、DLQ 和故障检查参见 [`docs/nats-domain-events.md`](docs/nats-domain-events.md)。
访问:
- 前端页面:`http://localhost:5173`
- 健康检查:`http://localhost:5173/api/health-check`
`bun run dev` 会同时启动 Vite 前端和 Bun/Hono 主服务;Vite 将 `/api`、`/internal` 与 WebSocket 升级代理到 Bun 服务。
本地 NATS 使用 JetStream 文件卷保存消息和 durable consumer 的投递位点。启动时发现历史消息是预期行为;回放归档和事务消息会在 PostgreSQL 可用后继续消费。若 PostgreSQL 暂时不可用,事务消息恢复器会以 5 秒至 60 秒退避重试,避免连接超时期间持续打满连接池;不应通过删除 NATS 数据卷来规避数据库故障。
## 构建与运行
| 命令 | 作用 |
| --- | --- |
| `bun run dev` | 启动 Vite 与 Bun/Hono 主服务 |
| `bun run build` | 依次构建前端与服务端 |
| `bun run build:client` | 构建 Cloudflare Pages 使用的前端 SPA |
| `bun run build:server` | 构建 Docker 使用的 Bun/Hono 后端 |
| `bun run preview` | 先构建,再运行 `dist/index.js` |
| `bun run start` | 直接运行已构建产物 |
| `bun run lint` | ESLint 检查 |
| `bun run test` | Vitest |
| `bun run battle:smoke` | 实时战斗 2v2/4v4、超时默认出招、协议、负载与 Worker 故障注入验收 |
| `REDIS_URL=redis://127.0.0.1:6379/15 bun run battle:e2e:redis` | 使用本机隔离 Redis DB 验收重启、多实例、幂等和归档 staging |
| `bun run auth:generate` | 生成 `better_auth` Drizzle 迁移 |
| `bun run auth:migrate` | 执行 `better_auth` 独立迁移流 |
构建产物:
- `build:client` 产出前端 SPA
- `build:server` 产出 Bun 运行的 Hono 服务入口 `dist/index.js`
## Docker
React SPA 继续独立部署到 Cloudflare Pages,不进入后端镜像。`app`(`3000`)使用 Bun,同时承载 Hono API 与实时战斗 WebSocket;`battle-v5` 仍是无框架依赖的纯战斗引擎。PostgreSQL 回放归档由应用侧 NATS consumer 完成。
本地构建镜像:
```bash
docker build -t daoyou-app:local -f docker/Dockerfile.app .
```
运行镜像:
```bash
docker run --rm -p 3000:3000 \
--env-file /path/to/.env.production \
daoyou-app:local
```
注意:
- `VITE_API_BASE_URL` 是前端 Pages 构建期变量,不进入后端 Docker 镜像
- 服务运行时环境变量通过 shell、容器环境或 `--env-file` 注入
## 仓库内现成部署脚本
### Hono API 蓝绿发布
```bash
APP_IMAGE=swkzymlyy/daoyou-app:<version> \
ENV_FILE=/root/daoyou/.env.production \
./scripts/blue-green-app.sh
```
这个脚本会:
- 在 `daoyou-app-blue` / `daoyou-app-green` 间部署闲置颜色
- 同时验证 Docker health 与宿主机 `/api/health-check`
- 通过 `nginx -t` 后原子切换 OpenResty upstream
- 短暂 drain 后停止旧颜色容器
生产 Compose 只定义 `app-blue` 和 `app-green`;实时战斗与 API 随同一 Bun 主服务蓝绿发布; `blue-green-app.sh` 通过 Compose 启动闲置 app profile 并切换 OpenResty。React SPA 仍由 Cloudflare Pages 独立部署。
## 生产 cron 配置方式
当前仓库默认采用两层设计:
- 生产环境中 `src/index.ts` 注册 Bun 内置 cron,Cron 只发布 JetStream command,durable Worker consumer 执行 job runner
- `/internal/cron/*` 仍然保留,便于手动触发、联调,或后续切回外部调度器
- `GET /internal/cron/auction-expire`
- `GET /internal/cron/bet-battle-expire`
- `GET /internal/cron/rank-rewards`
- `GET /internal/cron/market-refresh`
- `GET /internal/cron/tower-enemy-sets`
- `GET /internal/cron/player-state-events-cleanup`
- `GET /internal/cron/expired-data-cleanup`
当前内置调度频率:
- `auction-expire`:每 2 分钟
- `bet-battle-expire`:每 2 分钟
- `rank-rewards`:每天 `00:00 Asia/Shanghai`
- `market-refresh`:每 5 分钟
- `tower-enemy-sets`:每小时
- `player-state-events-cleanup`:每天 `02:30 Asia/Shanghai`
- `expired-data-cleanup`:每天 `02:45 Asia/Shanghai`
说明:
- Bun 内置 cron 仍运行在 Web 进程内,但实际任务已与调度回调解耦;发布成功的 command 由 JetStream 持久化并可跨应用重启继续执行
- Bun 的 cron 表达式按 `UTC` 解释,所以 `rank-rewards` 在代码里配置为 `0 16 * * *`,对应北京时间次日 `00:00`
- 内置调度不直接调用 job runner,也不走 HTTP;它发布 `daoyou.command.cron.>` command
- `/internal/cron/*` 接口继续要求 `Authorization: Bearer ${CRON_SECRET}`,适合人工补跑或外部调度
- 这些任务内部带 Redis 分布式锁与幂等保护,重复触发会返回 `skipped`
如果你想改回外部 HTTP 调度,可使用:
```cron
*/2 * * * * curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" https://your-domain/internal/cron/auction-expire
*/2 * * * * curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" https://your-domain/internal/cron/bet-battle-expire
0 0 * * * curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" https://your-domain/internal/cron/rank-rewards
*/5 * * * * curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" https://your-domain/internal/cron/market-refresh
0 * * * * curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" https://your-domain/internal/cron/tower-enemy-sets
30 2 * * * curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" https://your-domain/internal/cron/player-state-events-cleanup
45 2 * * * curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" https://your-domain/internal/cron/expired-data-cleanup
```
## CI / 镜像发布
当前仓库的 [`.github/workflows/deploy.yml`](.github/workflows/deploy.yml) 会在 `master` 分支推送时:
- 构建 Docker 镜像
- 推送到 Docker Hub
环境要求、环境变量、数据库初始化、本地开发、构建、Docker、生产 cron 与部署脚本说明已整理到 [docs/development.md](docs/development.md)。
## 贡献指南
@@ -361,10 +82,7 @@ ENV_FILE=/root/daoyou/.env.production \
4. 推送到分支 (`git push origin feature/NewFeature`)。
5. 提交 Pull Request。
- **架构原则**:
- 引擎层(`engine/`)完全独立于 UI 和框架
- 业务逻辑放在 Service 层
- 数据访问使用 Repository 模式
开发与部署的完整约定请参见 [本地开发与部署](#本地开发与部署)。
## 💬 交流群
@@ -376,6 +94,12 @@ ENV_FILE=/root/daoyou/.env.production \
感谢每一位帮助《万界道友》持续维护与成长的道友。新的赞助统一通过 [爱发电](https://afdian.com/u/baef2b20501311f09da252540025c377) 进行;具体规则请见 [赞助说明](SPONSORING.md),公开赞助人名单与历史鸣谢请见 [SPONSORS.md](SPONSORS.md)。
<p align="center">
<a href="https://afdian.com/u/baef2b20501311f09da252540025c377">
<img src="sponsorkit/sponsors.svg" alt="爱发电赞助人名单" width="720" />
</a>
</p>
赞助名单不会展示支付信息或具体金额,赞助不会影响游戏数值、账号权益或项目决策权。
## 🤝 致谢
@@ -1,888 +0,0 @@
# battle-v5 与宗门机制解耦优化重构方案
> - 状态:历史实施方案,已实施并通过验证(2026-07-22)
> - 分析基线:2026-07-22 当前工作区
> - 当前架构提示:本文记录的是旧 `BattleEngineV5` 1v1 主流程下的宗门解耦决策。该引擎已被删除;当前生产基线为 `Team/Roster + BattleRuntime + Checkpoint + BattleRoundResolver`。本文只用于追溯宗门通用原语的取舍,不再是战斗主流程的实现依据。
> - 适用范围:`src/shared/engine/battle-v5`、`src/shared/engine/sect`、宗门战斗日志展示与相关测试
> - 重点宗门:幽都、天衍圣地;红尘剑宗与无相宗作为回归样本
> - 实施原则:不重做战斗架构,不修改持久化模型,不改变稳定宗门/流派/节点/能力 ID
> - 实施报告:[battle-v5-sect-decoupling-refactor-report.md](./battle-v5-sect-decoupling-refactor-report.md)
## 1. 背景与结论
当前四个生产宗门按 `src/shared/engine/sect/content/productionRuntime.ts` 的注册顺序为:
1. 红尘剑宗:`lingxiao`
2. 无相宗:`wuxiang`
3. 天衍圣地:`tianyan`
4. 幽都:`youdu`
现状没有在 battle-v5 生产核心中按上述宗门 ID、能力 ID 或“蚀魂、魂火、法印”等内容语义执行 `if/switch`,也没有为幽都新增灵魂生命值、专属伤害系统或多目标战斗分支。总体架构仍然可控。
风险主要来自另一种更隐蔽的耦合:用中性字段名包装只有一个宗门需要的执行阶段、运行时状态或展示参数,并让它们贯穿 `AbilityConfig`、Factory、DamageSystem、日志、心法成长和 UI。此类接口虽然没有出现宗门 ID,仍会扩大 battle-v5 的长期维护面。
本方案作出以下核心决策:
- 删除幽都驱动的冗余执行阶段 `postDamageEffects`,复用现有 `completionEffects`。
- 删除伤害核心中的 `DamageDisplayMetadata/display/tone`,不让战斗事件携带颜色和宗门美术语义。
- 删除仅由幽都终结技使用的 `buffLayerScalar`,在宗门编译期生成四层和五层两个固定 effect plan。
- 删除仅由天衍使用的 `element_history` 与 `BattleRuntimeState.elementHistories`,用宗门内容层隐藏 marker 和通用 runtime counter 表达不同元素历史。
- 删除天衍驱动的 `buff_periodic_settlement` 与 `manualSettlementEffects`,用分层 DOT 和固定反应伤害近似“立即结算剩余持续伤害”。
- 保留具有清晰通用语义的分层状态、逐层驱散、受治疗削弱、伤害行为开关、命中策略、effect plan、能力模式和监听预算。
- 扩大架构守卫覆盖四个生产宗门,并增加依赖方向、移除项和中性测试门禁。
## 2. 目标与非目标
### 2.1 目标
1. battle-v5 只理解伤害、治疗、Buff、层数、资源、命中、行动、事件、条件和监听预算等通用战斗事实。
2. 宗门世界观语义、流派规则、节点开关和复杂效果组合只存在于 `sect/content/<sect-id>`。
3. 玩家关键决策语义保持不变;无法无代价精确复刻时,优先采用可验证的近似效果。
4. 每个重构阶段可独立提交、验证和回滚,不要求一次完成全部工作。
5. 战斗与展示继续读取同一个 `SectCompiledBuild`,不在 UI、Service 或 Adapter 重新解释倍率。
6. 后续新增宗门能够依据本方案的准入门禁判断是否应扩展 battle-v5。
### 2.2 非目标
- 当时不修改 `BattleEngineV5` 严格 1v1 的单位、目标、回合、胜负和 AI 模型;该非目标仅适用于本文的历史实施批次,不能用于约束当前 Team/Roster 引擎。
- 不新增 `DamageType.SOUL` 或其他宗门专属枚举。
- 不改变 `Cultivator.attributes` 的五维持久化边界。
- 不持久化 `battleProjection`、宗门战斗资源、Buff、运行时 counter 或 marker。
- 不修改宗门、心法、能力、流派、节点和战术稳定 ID。
- 不重做 `EffectRegistry`、`EventBus`、`AbilityFactory` 或整个日志架构。
- 不在本轮统一重写四个宗门的数值平衡。
- 不借机整理无关代码或处理工作区中的其他用户改动。
## 3. 目标分层与依赖方向
```mermaid
flowchart LR
A["宗门 definition / nodes / Build Facade"] --> B["宗门内容编译器"]
B --> C["AbilityConfig / BuffConfig / EffectConfig"]
C --> D["battle-v5 通用执行核心"]
D --> E["结构化战斗事件与日志"]
E --> F["LogPresenter / React 展示"]
G["GameplayTags"] --> B
G --> D
F -. "禁止反向影响计算" .-> D
D -. "禁止依赖具体宗门内容" .-> B
```
### 3.1 battle-v5 核心允许承担的职责
- 通用伤害请求与结算。
- 通用治疗、护盾、资源和属性修改。
- Buff 生命周期、层数、驱散和控制抗性。
- 主动能力命中、费用、冷却和效果顺序。
- 通用监听范围、条件、优先级和触发预算。
- 不含内容句子的结构化日志事实。
### 3.2 宗门内容层允许承担的职责
- 把“魂伤”编译为真实伤害、不可暴击、不可吸血和幽都机制标签。
- 把“蚀魂”编译为一个分层 Debuff 及其监听器。
- 把“法印反应”编译为 effect plan、隐藏 marker、counter 和通用效果链。
- 根据心法等级、流派、节点和战术生成固定的运行时配置。
- 在编译期展开少量枚举分支,例如四层/五层终结技。
### 3.3 展示层允许承担的职责
- 能力名称、机制名称、说明文本、颜色、图标和日志排版。
- 从结构化 `cause`、能力、Buff 和资源事件生成玩家文本。
- 对 debug marker、内部 counter 和稳定 ID 做隐藏处理。
展示层不得向战斗核心增加 `tone`、CSS 色名、Tailwind 类名或宗门主题字段。
## 4. 通用原语保留与退出清单
### 4.1 明确保留
| 原语 | 当前消费者 | 保留理由 | 必须满足的边界 |
| --- | --- | --- | --- |
| `BuffLayerChangedEvent` | 幽都及通用分层状态 | 完整表达 0→N、叠层、减层、驱散和阈值 | 事件字段只包含层数、原因、来源和通用对象 |
| `ApplyBuffParams.layers` | 幽都 | 一次添加多层是 Buff 的自然能力 | 缺省为 1,受 `maxLayers` 限制 |
| `AttributeModifierConfig.valueByLayer` | 幽都 | 非线性层级属性是通用状态能力 | 不写宗门名称;clone、增减层正确重挂 |
| `BuffConfig.dispelMode` | 幽都 | 逐层驱散是通用 Buff 策略 | 缺省 `whole`,旧内容行为不变 |
| `HEAL_RECEIVED_REDUCTION` | 幽都等状态 | 受治疗削弱属于基础属性 | 统一在 `Unit.heal()` 生效,不影响 MP、护盾、免死 |
| `DamageParams.canCrit/canLifesteal` | 幽都 | 伤害包级行为开关可复用 | 缺省 `true`,不能改变其他真实伤害 |
| `AbilityConfig.hitPolicy` | 红尘剑宗、幽都 | 能力自身必然命中具有通用语义 | 不按目标宗门或状态动态改写全局命中 |
| `effectLayers/effectPlans` | 无相、天衍、幽都 | 已有多个独立消费者 | 在施法准备阶段快照;计划只引用固定效果层 |
| `AbilityMode` | 无相 | “接下来 N 次能力使用某模式”足够通用 | battle-v5 不理解佛相、魔相、无相 |
| listener `budget/group` | 四宗门中的多个监听器 | 通用防重和共享预算 | group 只作为运行时 key,不包含执行特判 |
| `refund_paid_cost` | 天衍、幽都 | 已有两个独立消费者 | 只读取本次施法快照中的实际支付值 |
| `mechanic_log` | 天衍 | 当前仅发布结构化日志,不参与计算 | 暂保留但冻结能力,不允许加入内容句子模板或计算副作用 |
### 4.2 必须删除
| 原语 | 当前问题 | 替代方案 |
| --- | --- | --- |
| `AbilityConfig.postDamageEffects` | 与 `completionEffects` 语义重叠,迫使 Factory、能力分析、心法成长和展示理解新阶段 | 合并进有序的 `completionEffects` |
| `DamageDisplayMetadata`、伤害 `display`、`PresentedLogPart.tone` | 将“魂伤”“术伤”和 `violet` 贯穿伤害核心到 React | 使用标准伤害日志;必要时复用已有 `LogCauseRef.displayName`,不传颜色 |
| `DamageParams.buffLayerScalar` | 只有幽都终结技使用,使 DamageEffect 读取任意 Buff 层数 | 宗门编译期生成四层/五层两个固定伤害计划 |
| `ElementHistoryParams`、`element_history`、`elementHistories` | 将天衍的元素历史固化进通用运行时 | 天衍隐藏 marker + 通用 runtime counter |
| `BuffConfig.manualSettlementEffects`、`buff_periodic_settlement` | 外部效果读取 Buff 内部周期配置,增加反向耦合并扩散到心法成长 | 分层 DOT + 固定反应伤害 + 清除 Buff |
### 4.3 暂不扩展
- 不新增任意的 `afterHitEffects`、`afterDamageEffects`、`afterControlEffects` 等更多能力生命周期数组。
- 不新增通用“读取另一个 EffectConfig 并重新执行”的反射型效果。
- 不新增通用元素集合、宗门状态机或专属日志样式注册到 battle-v5。
- 不为单个节点增加新的 DamageSystem 分支、事件字段或伤害类型。
## 5. 行为保真等级
实施时按以下等级标记每一项验收:
| 等级 | 定义 | 适用项 |
| --- | --- | --- |
| E:精确等价 | 输入、结算顺序、事件数量和数值完全一致 | 幽都效果顺序、终结技四/五层伤害、魂火消费、失魂与归窍 |
| S:语义等价 | 玩家决策、反制窗口和强弱关系一致,允许内部表示改变 | 天衍不同元素历史改为 marker/counter |
| A:可接受近似 | 保留用途和战术选择,允许小幅数值或展示变化 | 天衍提前结算 DOT、魂伤日志颜色 |
以下内容不得降级:
- 幽都魂火必须在伤害请求后消费,不能消费本次新获得且未参与增伤的魂火。
- 幽都终结技必须按伤害前的四层或五层计算,随后才清层或保留两层。
- 幽都失魂成功、抵抗、免疫和首次解控必须收束到既定层数并进入归窍。
- 天衍元素反应仍必须由有效的元素组合触发,普通装备、功法和其他宗门不能误触发。
- 所有伤害、控制、治疗、状态和资源仍须通过现有 Factory 与事件管道。
## 6. 阶段 0:冻结基线与建立对照
### 6.1 目的
在删除接口前固化现有行为,避免重构过程中把已有机制 Bug 误当成新设计。
### 6.2 操作
1. 记录当前工作区状态,不覆盖或回滚任何用户未提交改动。
2. 运行当前 battle-v5 与宗门测试,保存命令和结果。
3. 为后续将要改写的机制补足“行为契约测试”,测试应面向结果而不是字段存在性。
4. 对日志允许变化的部分单独记录快照,避免把颜色删除误报为战斗语义变化。
### 6.3 基线测试清单
幽都:
- 《一叹》伤害后增加 1 层蚀魂。
- 《离魂引》按施法前层数选择伤害,伤害后消费魂火并增加 2 层蚀魂。
- 《忘川潮》按“直接伤害→蚀魂→忘川”执行。
- 《夺魄》《镇魂》发布术伤和魂伤两个独立请求,随后加层并施加状态。
- 混合伤害首段致死时,不发布第二次伤害请求,不遗留后续状态。
- “先定其形”只被铺层神通消费,每场一次,终结技和照影不能误消费。
- 《魂兮不归》四层、五层及相关节点组合的伤害值、请求数和完成效果。
天衍:
- 相同元素重复不能计为三种不同元素。
- 三种不同元素触发后历史重置。
- 燎原立即追加一跳但不减少原灼烧剩余跳数。
- 蒸发按剩余灼烧跳数追加伤害并移除灼烧。
- 反应伤害不会再次触发法印、DOT 或同类宗门反应链。
通用引擎:
- 已死亡目标不接受后续 `damage` effect,且该测试不得出现任何宗门 ID。
- `completionEffects` 在所有计划主效果之后、计划层完成效果之前执行。
- listener budget 的共享 group 在 action、source_action、round、battle 和 buff_lifetime 下正确重置。
### 6.4 门禁
```bash
bunx vitest run src/shared/engine/battle-v5/tests
bunx vitest run src/shared/engine/sect/content/youdu
bunx vitest run src/shared/engine/sect/content/tianyan
bunx vitest run src/shared/engine/sect
```
只有基线通过后才进入阶段 1。若基线本身失败,应先记录为独立 Bug,不在重构提交中混修。
## 7. 阶段 1:删除幽都驱动的 `postDamageEffects`
### 7.1 目标
移除新的全局生命周期阶段,同时保持幽都所有能力的伤害、魂火、加层和控制顺序精确等价。
### 7.2 目标执行顺序
`LayeredDataDrivenActiveSkill` 继续只执行:
```text
plan.effects
→ plan.completionEffects
→ consumeModeKey
```
`resolveAbilityEffectPlan()` 当前将完成效果组合为:
```text
base completionEffects
→ selected layer completionEffects
```
幽都可以利用这一既有顺序,让基础加层先发生,再结算《镇魂》的高低层控制完成效果;计划选择仍在施法准备时完成,不会被本次加层改变。
### 7.3 能力迁移表
| 能力 | 当前主效果 | 迁移后的 `completionEffects` 顺序 | 保真等级 |
| --- | --- | --- | --- |
| 一叹 | 条件术伤 | 加 1 层蚀魂→先定其形额外层→清 pending marker | E |
| 离魂引 | 四/五层计划魂伤 | 消费魂火→加 2 层蚀魂→先定其形→清 marker | E |
| 忘川潮 | 直接魂伤 | 加 1 层蚀魂→先定其形→目标存活时施加忘川 | E |
| 夺魄 | 术伤→魂伤→消费魂火 | 加 2 层蚀魂→先定其形→目标存活时施加降攻 | E |
| 镇魂 | 术伤→魂伤→消费魂火 | 加 2 层蚀魂→先定其形→计划层控制/减速 | E |
### 7.4 文件范围
删除字段及透传:
- `src/shared/engine/battle-v5/core/configs.ts`
- `src/shared/engine/battle-v5/abilities/LayeredDataDrivenActiveSkill.ts`
- `src/shared/engine/battle-v5/factories/AbilityFactory.ts`
- `src/shared/engine/battle-v5/factories/AbilityCapabilityAnalyzer.ts`
- `src/shared/engine/sect/core/authoring/SectAbilityFactory.ts`
- `src/shared/engine/sect/core/authoring/StandardSectMethodGrowthPolicy.ts`
- `src/shared/engine/sect/core/presentation/abilityFacts.ts`
迁移内容与测试:
- `src/shared/engine/sect/content/youdu/base/YouduBaseCompiler.ts`
- `src/shared/engine/battle-v5/tests/core/AbilityEffectPlans.test.ts`
- `src/shared/engine/sect/content/youdu/tests/*.test.ts`
- `src/shared/engine/sect/core/tests/methodGrowth.test.ts`
### 7.5 实施约束
- 不新增同义字段替代 `postDamageEffects`。
- 不把顺序逻辑写进 DamageSystem 或宗门 ID 分支。
- `firstShadowPending` 仍只由命中事件创建,并在技能完成阶段清理。
- 目标死亡时,加层、忘川、降攻和控制必须被存活条件阻止;marker 清理仍需执行。
- 能力详情继续展示“命中后”,但这是 `abilityFacts` 对 completion 中具体效果的描述,不代表新增运行时阶段。
### 7.6 验收
```bash
rg -n "postDamageEffects" src
bunx vitest run src/shared/engine/battle-v5/tests/core/AbilityEffectPlans.test.ts
bunx vitest run src/shared/engine/sect/core/tests/methodGrowth.test.ts
bunx vitest run src/shared/engine/sect/content/youdu
```
完成条件:
- `rg` 无结果。
- 幽都伤害请求数、魂火消费、蚀魂层数、状态顺序与基线一致。
- battle-v5 不新增其他生命周期数组。
## 8. 阶段 2:移除伤害核心中的展示元数据
### 8.1 目标
让 DamageEffect、DamageSystem 和伤害事件只传递影响计算或结构化归因的字段,不传递“术伤”“魂伤”和 `violet`。
### 8.2 决策
本轮默认采用标准伤害日志:
- 术伤继续由 `DamageType.MAGICAL` 计算。
- 魂伤继续由 `DamageType.TRUE + soulDamageTag + canCrit=false + canLifesteal=false` 计算。
- 同一技能的两个伤害包仍由现有 action span 聚合。
- 删除紫色染色,视为 A 级允许变化。
如果产品验收明确要求展示“魂伤”文字,只允许复用已有 `LogCauseRef`:
```ts
cause: {
kind: 'mechanic',
id: 'sect.youdu.soul-damage',
displayName: '魂伤',
}
```
`cause` 是已有结构化归因,不得再携带颜色、CSS 类、句子模板或宗门主题对象。若现有 Presenter 不能在不增加核心字段的前提下展示该名称,本轮接受标准日志,不继续扩展。
### 8.3 文件范围
- `src/shared/engine/battle-v5/core/configs.ts`
- `src/shared/engine/battle-v5/core/events.ts`
- `src/shared/engine/battle-v5/effects/DamageEffect.ts`
- `src/shared/engine/battle-v5/systems/DamageSystem.ts`
- `src/shared/engine/battle-v5/systems/log/LogCollector.ts`
- `src/shared/engine/battle-v5/systems/log/LogPresenter.ts`
- `src/shared/engine/battle-v5/systems/log/types.ts`
- `src/react-app/components/feature/battle/v5/CombatActionLog.tsx`
- `src/shared/engine/sect/content/youdu/base/YouduBaseCompiler.ts`
- 对应日志与幽都测试
### 8.4 验收
必须删除:
- `DamageDisplayMetadata`
- `DamageParams.display`
- 三类伤害事件中的 `display`
- `DamageEntryData.display`
- `PresentedLogPart.tone`
- `CombatActionLog` 中的 `part.tone === 'violet'`
验证命令:
```bash
rg -n "DamageDisplayMetadata|tone.*violet|display: \{ label: '(魂伤|术伤)'" src
bunx vitest run src/shared/engine/battle-v5/tests/systems/log
bunx vitest run src/shared/engine/sect/content/youdu
bun run build
```
完成条件:
- 搜索无结果。
- 术伤、魂伤数值和请求数量不变。
- React 不再根据 battle-v5 核心传来的颜色枚举选择 CSS。
- 允许日志颜色和分量前缀与基线不同,但不允许伤害总值和分组错误。
## 9. 阶段 3:用固定 effect plan 替代 `buffLayerScalar`
### 9.1 目标
精确保留《魂兮不归》的单次伤害请求和四/五层公式,同时让 DamageEffect 不读取任意 Buff 层数。
### 9.2 编译方案
在 `compileAbilities()` 中根据最终 `YouduBuildSettings` 计算:
```ts
const coefficientAtFour =
settings.finishBaseCoefficient + settings.finishPerLayerCoefficient * 4;
const coefficientAtFive =
settings.finishBaseCoefficient + settings.finishPerLayerCoefficient * 5;
```
生成两个效果层:
```text
finish-four:一次 coefficientAtFour 的魂伤
finish-five:一次 coefficientAtFive 的魂伤
```
生成两个计划,五层计划优先:
```text
priority 20:施法前至少5层 → finish-five
priority 10:施法前至少4层且少于5层 → finish-four
```
保持既有 `castConditions >= 4`。伤害完成后继续执行原有 `finishCompletion`:
1. 清除蚀魂或设置为 2 层。
2. 目标存活时施加不归。
3. 可选施加忘川。
4. 消费魂火。
5. 处理一名一判、名落幽都等节点。
### 9.3 文件范围
- `src/shared/engine/sect/content/youdu/base/YouduBaseCompiler.ts`
- `src/shared/engine/battle-v5/core/configs.ts`
- `src/shared/engine/battle-v5/effects/DamageEffect.ts`
- `src/shared/engine/sect/core/presentation/abilityFacts.ts`
- `src/shared/engine/battle-v5/tests/effects/LayeredStatusAndSoulPrimitives.test.ts`
- `src/shared/engine/sect/content/youdu/tests/*.test.ts`
### 9.4 测试矩阵
至少覆盖:
| 蚀魂层数 | 基础配置 | 司命判词 | 七寸断魂 | 期望 |
| --- | --- | --- | --- | --- |
| 3 | 任意 | 任意 | 任意 | 不可施放 |
| 4 | 默认 | 关 | 关 | `base + perLayer × 4`,一个请求 |
| 5 | 默认 | 关 | 关 | `base + perLayer × 5`,一个请求 |
| 4 | 开 | 开/关 | 开/关 | 使用节点后的编译系数,一个请求 |
| 5 | 开 | 开/关 | 开/关 | 使用节点后的编译系数,一个请求 |
| 4/5 | 不归亦不散 | 任意 | 任意 | 伤害后保留 2 层 |
还需验证《照影》易伤读取终结前层数,本次伤害之后才清层。
### 9.5 验收
```bash
rg -n "buffLayerScalar" src
bunx vitest run src/shared/engine/battle-v5/tests/core/AbilityEffectPlans.test.ts
bunx vitest run src/shared/engine/sect/content/youdu
```
完成条件:
- 搜索无结果。
- 四层和五层最终伤害与基线一致。
- 每次终结仍只有一个 `DamageRequestEvent`。
- DamageEffect 不新增等价的状态层数动态读取字段。
## 10. 阶段 4:将天衍元素历史下沉到内容层
### 10.1 目标
删除 battle-v5 中的元素集合状态,同时精确保留“不同元素累计到三种后触发”的玩家语义。
### 10.2 内容层表示
天衍为每个需要记录的元素创建隐藏 marker Buff:
```text
sect.tianyan.element-history.fire
sect.tianyan.element-history.water
sect.tianyan.element-history.wood
sect.tianyan.element-history.earth
sect.tianyan.element-history.metal
```
marker 约束:
- `logVisibility: debug`
- `statusVisibility: hidden`
- `countsAsStatus: false`
- `dispelPolicy: protected`
- `duration: -1`
- 使用 `GameplayTags.STATUS.SECT.state(...)` 构造具体标签和共同清理标签
- 只存在于战斗运行时,不进入宗门持久状态
每套历史使用一个通用 runtime counter,例如:
```text
sect.tianyan.strategy.element-count
sect.tianyan.hetu.three-talents-count
```
### 10.3 记录流程
对一次合法天衍元素反应:
1. 若施术者尚无当前元素 marker,则执行 `runtime_counter_modify(add 1, max 3)`。
2. 在该 counter effect 的后续效果中施加当前元素 marker。
3. counter 达到 3 时触发对应节点/机制效果。
4. 清除共同标签匹配的全部元素 marker。
5. reset 对应 runtime counter。
相同元素重复时,第一步的 `has_not_tag` 条件失败,不增加 counter。
天衍策略不再调用 `readElementHistory()`,只读取通用 `readRuntimeCounter()` 或当前 marker 标签。策略仍位于 `sect/content/tianyan`,因此可以理解天衍元素 ID。
### 10.4 删除范围
- `ElementHistoryParams`
- `EffectConfig` 中的 `element_history`
- `src/shared/engine/battle-v5/effects/ElementHistoryEffect.ts`
- 对应 effect export/registry 引用
- `BattleRuntimeState.elementHistories`
- `rememberElement/readElementHistory/clearElementHistory`
- BattleStateRecorder 中对元素历史的快照字段或复制逻辑
- 天衍编译器和策略中的 `element_history`/`readElementHistory` 使用
### 10.5 文件范围
- `src/shared/engine/battle-v5/core/configs.ts`
- `src/shared/engine/battle-v5/core/runtimeState.ts`
- `src/shared/engine/battle-v5/effects/ElementHistoryEffect.ts`
- `src/shared/engine/battle-v5/effects/index.ts`
- `src/shared/engine/battle-v5/systems/state/*`
- `src/shared/engine/sect/content/tianyan/base/TianyanBaseCompiler.ts`
- `src/shared/engine/sect/content/tianyan/strategy.ts`
- `src/shared/engine/sect/content/tianyan/tests/*`
- 对应 battle-v5 原语测试
### 10.6 测试矩阵
- 火→火→火:counter 保持 1,不触发。
- 火→水→火:counter 为 2,不触发。
- 火→水→木:第三种触发一次,随后 counter=0、marker 全清。
- 触发后再次火→水→木:可再次触发。
- 两套不同 key 的历史互不影响。
- marker 不显示在玩家状态栏、不计入普通 Buff 数量、不受驱散。
- 战斗状态克隆/录制后不出现旧 `elementHistories` 字段;marker 与 counter 按现有通用规则恢复或复制。
- 其他宗门的元素技能不能写入天衍 marker。
### 10.7 验收
```bash
rg -n "ElementHistory|element_history|elementHistories|readElementHistory|rememberElement|clearElementHistory" src
bunx vitest run src/shared/engine/battle-v5/tests/core/RuntimePrimitives.test.ts
bunx vitest run src/shared/engine/battle-v5/tests/systems/BattleStateRecorder.test.ts
bunx vitest run src/shared/engine/sect/content/tianyan
```
完成条件:搜索无结果;不同元素语义与基线一致;battle-v5 runtime state 不再出现元素领域名词。
## 11. 阶段 5:删除天衍周期 Buff 的反射式手动结算
### 11.1 目标
移除外部效果读取并重新执行 Buff 内部 `manualSettlementEffects` 的能力,保留燎原和蒸发“提前兑现灼烧价值”的战术用途。
### 11.2 允许的语义近似
原设计要求蒸发读取原灼烧来源、剩余完整跳数和原周期效果。精确保留这些内部细节会继续要求通用引擎反射式读取 Buff 配置,不符合本方案边界。
本阶段采用 A 级近似:
- 灼烧剩余层数仍决定蒸发额外伤害强弱。
- 燎原仍额外造成一跳等价伤害且不消耗原灼烧。
- 蒸发仍立即兑现剩余灼烧并清除灼烧。
- 允许立即伤害使用天衍编译器中的标准灼烧系数,而不是运行时反查原 Buff 的完整 EffectConfig。
- 允许日志归因为燎原/蒸发的直接追击,而不是“手动触发原 Buff”。
### 11.3 灼烧表示
仅将天衍灼烧改为层数代表剩余跳数:
- 初始 `layers = 2`
- `maxLayers = 2`
- 每次目标行动前:造成一跳 DOT,然后减少 1 层
- 0 层时移除
- 重新施加时恢复到 2 层并刷新持续时间
- Buff 仍保留正常的周期监听器,普通自然结算不走新引擎分支
熔岩等不参与燎原/蒸发提前结算的 DOT 不必改为同一表示。
### 11.4 反应编译
燎原:
```text
本次主伤害与基础效果
→ 施加/刷新2层灼烧
→ 追加一次标准灼烧系数的 DELAYED/FOLLOW_UP 伤害
→ 不减少灼烧层数
```
蒸发:
```text
若目标灼烧至少2层:追加2倍标准灼烧系数伤害
若目标灼烧恰好1层:追加1倍标准灼烧系数伤害
随后清除灼烧
```
两档使用现有 effect plan 或带层数条件的两个互斥 EffectConfig,不新增“按剩余持续时间读取 Buff 配置”的引擎能力。
### 11.5 删除范围
- `BuffPeriodicSettlementParams`
- `EffectConfig` 中的 `buff_periodic_settlement`
- `src/shared/engine/battle-v5/effects/BuffPeriodicSettlementEffect.ts`
- `BuffConfig.manualSettlementEffects`
- `DataDrivenBuff` 对该字段的暴露用途(若删除后无其他消费者)
- `StandardSectMethodGrowthPolicy` 对 `manualSettlementEffects` 的递归投影
- 相关 affix text 分支与 EffectRegistry 注册
### 11.6 测试矩阵
- 新灼烧为 2 层,第一跳后 1 层,第二跳后移除。
- 重复施加恢复 2 层,不超过 2 层。
- 燎原额外造成一跳,灼烧仍保持 2 层。
- 一层灼烧蒸发追加一档伤害并清除。
- 两层灼烧蒸发追加两档伤害并清除。
- 蒸发不会结算熔岩或其他 DOT。
- 目标死亡后不追加后续伤害、不遗留新法印或 DOT。
- 反应追加伤害不再次触发天衍法印与宗门反应。
- 相关心法成长与节点倍率仍能投影到新的固定反应伤害。
### 11.7 验收
```bash
rg -n "BuffPeriodicSettlement|buff_periodic_settlement|manualSettlementEffects" src
bunx vitest run src/shared/engine/battle-v5/tests/effects/TianyanEngineExtensions.test.ts
bunx vitest run src/shared/engine/sect/content/tianyan
bunx vitest run src/shared/engine/sect/core/tests/methodGrowth.test.ts
```
完成条件:搜索无结果;燎原和蒸发的用途、相对强弱和清除规则保留;不新增等价的反射式 Buff 配置读取接口。
## 12. 阶段 6:加强架构与准入守卫
### 12.1 扩大现有架构守卫
修改 `src/shared/engine/sect/testing/architecture/architectureGuard.test.ts`:
1. 对 battle-v5 非测试核心扫描全部生产宗门 ID 和展示名称,而不是只扫描幽都。
2. `battle-v5/adapters` 作为明确集成边界可以依赖 `sect` 投影;其余 battle-v5 目录禁止导入 `sect/content`。
3. `sect/core` 禁止依赖 `sect/content` 和具体宗门 ID。
4. 通用 React 战斗组件禁止出现宗门 ID;宗门主题展示必须通过数据或已有结构化日志进入。
5. 禁止重新出现本方案删除的字段和 effect type。
建议扫描集合至少包括:
```text
lingxiao / 红尘剑宗 / 凌霄
wuxiang / 无相
tianyan / 天衍
youdu / 幽都
sect.lingxiao / sect.wuxiang / sect.tianyan / sect.youdu
```
测试夹具和宗门内容目录本身不参与禁止扫描。
### 12.2 依赖方向守卫
可先用架构测试实现,不强制本轮改 ESLint 配置:
```text
battle-v5/core|abilities|buffs|effects|factories|systems|units
不得 import sect/content、react-app
sect/core
不得 import sect/content
sect/content
可以 import battle-v5、sect/core、GameplayTags
react-app
可以 import battle-v5 日志与只读展示类型
不得向 battle-v5 注入颜色或宗门计算规则
```
### 12.3 新增核心原语准入模板
以后任何宗门要求扩展 battle-v5,都必须在设计稿中回答:
1. 现有 Effect/Listener/Condition 为什么不能组合?
2. 为什么不能在宗门编译期展开成有限个固定 plan?
3. 为什么不能用隐藏 Buff、GameplayTag 或通用 counter 表达?
4. 可接受的近似效果是什么,为什么仍不能满足?
5. 是否已有第二个独立生产消费者?
6. 默认值是否严格保持旧内容行为?
7. 是否有不引用任何宗门 ID 的原子测试?
8. 是否穿透 DamageSystem、Unit、行动流、胜负、持久化或展示层?
准入判断:
- 已有两个独立消费者:可考虑通用原语。
- 修复明确的全局战斗不变量:可考虑通用原语。
- 只有一个宗门且存在近似方案:拒绝扩展,采用内容层组合或近似。
- 涉及单位模型、目标、胜负、持久化或伤害主管道:单独立项,不得随宗门实现夹带。
- “未来可能复用”不能单独作为准入理由。
### 12.4 验收
```bash
bunx vitest run src/shared/engine/sect/testing/architecture/architectureGuard.test.ts
```
应额外用故障注入方式验证守卫有效:临时在本地向 battle-v5 核心加入一个生产宗门 ID 或 `sect/content` import,确认测试失败后撤销该临时改动。故障注入不得提交。
## 13. 阶段 7:文档与作者指南同步
代码验收后更新:
- `docs/sect-authoring-guide.md`
- `docs/youdu-battle-v5-implementation-plan.md`
- `docs/youdu-sect-design.md`
- `docs/tianyan-sacred-land-design.md`
- 必要时更新 `src/shared/engine/sect/README.md`
文档必须反映最终实现,不再把以下接口列为有效能力:
- `postDamageEffects`
- `DamageDisplayMetadata/display/tone`
- `buffLayerScalar`
- `element_history`
- `manualSettlementEffects`
- `buff_periodic_settlement`
宗门接入指南应新增“核心原语准入模板”和“优先编译固定 plan”的规则。
## 14. 分阶段验证矩阵
| 阶段 | 最小测试 | 宗门回归 | 通用回归 | 构建门禁 |
| --- | --- | --- | --- | --- |
| 0 基线 | 现有聚焦测试 | 幽都、天衍 | battle-v5、sect | 无代码改动时可不 build |
| 1 删除 post phase | AbilityEffectPlans、methodGrowth | 幽都全量 | battle-v5 core | `bun run build` |
| 2 删除 display/tone | log tests | 幽都日志/机制 | battle-v5 log | `bun run build` |
| 3 固定终结计划 | AbilityEffectPlans | 幽都全量 | battle-v5 effects | `bun run build` |
| 4 元素历史下沉 | RuntimePrimitives、Recorder | 天衍全量 | battle-v5 state | `bun run build` |
| 5 DOT 结算简化 | TianyanEngineExtensions、methodGrowth | 天衍全量 | battle-v5 effects | `bun run build` |
| 6 架构守卫 | architectureGuard | 四宗门编译 | battle-v5、sect | `bun run lint` |
| 最终 | 所有上述测试 | 四宗门 | creation-v2、全项目 | lint、test、build |
最终命令:
```bash
bunx vitest run src/shared/engine/battle-v5/tests
bunx vitest run src/shared/engine/sect
bunx vitest run src/shared/engine/creation-v2
bun run lint
bun run test
bun run build
```
说明:
- `bun run build` 不会运行测试,不能替代 `bun run test`。
- Vitest 当前使用 node 环境;日志和纯展示逻辑优先使用纯函数测试,不为此次重构新增不必要的 DOM 测试。
- 每阶段先运行最近的聚焦测试,再运行更广测试;不能只依赖最终全量测试。
## 15. 最终验收标准
### 15.1 代码边界
- battle-v5 核心不存在具体生产宗门 ID、名称、技能 ID 和状态 ID。
- 除明确的 Adapter 集成边界外,battle-v5 不依赖 `sect/content`。
- `sect/core` 不依赖具体宗门内容。
- 所有宗门能力继续通过 `SectAbilityFactory` 和 `AbilityFactory`。
- 运行时标签继续来自 `GameplayTags`,不手写运行时标签字符串。
- 没有新持久化字段、数据库迁移或 `battleProjection` 落库。
### 15.2 删除项
以下搜索必须无结果:
```bash
rg -n --glob '!src/shared/engine/sect/testing/architecture/architectureGuard.test.ts' "postDamageEffects|DamageDisplayMetadata|buffLayerScalar" src
rg -n --glob '!src/shared/engine/sect/testing/architecture/architectureGuard.test.ts' "ElementHistory|element_history|elementHistories" src
rg -n --glob '!src/shared/engine/sect/testing/architecture/architectureGuard.test.ts' "BuffPeriodicSettlement|buff_periodic_settlement|manualSettlementEffects" src
rg -n "tone.*violet|display: \{ label: '(魂伤|术伤)'" src
```
架构守卫测试本身保留上述历史符号作为禁止项正则和自检样本,因此搜索生产源时显式排除该测试文件;除此以外实际结果必须为 0。
### 15.3 幽都行为
- 五层蚀魂、非线性削弱、逐层驱散和受治疗削弱保留。
- 失魂与归窍四条收束路径保留。
- 魂火每来源行动一次、三点增伤和伤害后消费顺序保留。
- 混合技能仍是两个伤害包,共享一次命中。
- 《魂兮不归》四层/五层伤害精确等价且只有一个伤害请求。
- 先定其形、名落幽都、一名一判等节点不因执行阶段调整误触发。
- 允许魂伤紫色和分量前缀被删除。
### 15.4 天衍行为
- 不同元素累计和触发重置语义保留。
- 法印、化生、冲克和三数资源不泄漏给其他宗门。
- 燎原仍提供额外一跳价值且不消耗原灼烧。
- 蒸发仍按剩余灼烧强弱兑现并清除灼烧。
- 允许提前结算的内部伤害来源和日志表述变化,但反应用途与相对强弱必须保留。
- 天衍策略不再依赖 battle-v5 的元素专用运行时 API。
### 15.5 其他宗门回归
- 红尘剑宗的蓄势、调息、资源、追击和节点组合测试通过。
- 无相宗的佛相、魔相、无相 effect plan、AbilityMode 和节点矩阵测试通过。
- 四宗门编译、详情投影和战斗投影继续读取同一 `SectCompiledBuild`。
## 16. 提交拆分与回滚策略
建议拆成以下独立提交,不把多个阶段压成一个大提交:
1. `test(battle): freeze sect combat refactor contracts`
2. `refactor(battle): remove redundant post-damage phase`
3. `refactor(battle): keep damage logs presentation-neutral`
4. `refactor(sect): compile youdu finisher by layer plans`
5. `refactor(sect): move tianyan element history into content state`
6. `refactor(sect): simplify tianyan periodic reactions`
7. `test(sect): strengthen battle-content architecture guards`
8. `docs(sect): align authoring guides with engine boundaries`
每个提交要求:
- 只包含本阶段列出的文件和必要测试。
- 聚焦测试通过。
- `git diff --check` 通过。
- 不顺带清理无关导入、格式或旧代码。
回滚策略:
- 各阶段不修改稳定 ID 和持久化数据,因此可以按提交逆序回滚,不需要数据库回滚。
- 如果阶段 4 或阶段 5 的天衍近似效果未通过产品验收,只回滚对应天衍阶段,不回滚已经完成的幽都和架构守卫阶段。
- 不为开发环境旧战斗快照增加兼容代码;battle runtime 和 `battleProjection` 不持久化。
## 17. 风险与处理
| 风险 | 表现 | 处理 |
| --- | --- | --- |
| completion 顺序变化 | 幽都先加层后控制或魂火消费顺序错误 | 用事件序列测试记录 DamageRequest、ResourceChange、BuffLayerChanged、BuffApplied 顺序 |
| effect plan 读取实时层数 | 终结伤害被本次完成效果影响 | 保持 prepareCast 快照,测试施法前四/五层 |
| marker 被玩家看到 | 天衍元素历史污染状态栏 | hidden/debug/countsAsStatus=false,并加状态展示测试 |
| counter 与 marker 不一致 | 重复元素误计数或触发后残留 | 让 counter 修改、marker 施加、触发和清理处于同一有序效果链 |
| 灼烧层数与持续时间错位 | 多跳、少跳或重施不刷新 | 对 2→1→0、重施、自然过期逐行动测试 |
| 日志快照大面积变化 | 把展示差异误认为机制回归 | 单独批准颜色/前缀变化,伤害值和 action span 仍严格断言 |
| 删除类型影响 creation-v2 | 共享 AbilityConfig 类型编译失败 | 最终运行 creation-v2 测试和双端 build |
| 架构守卫误伤 Adapter | 合法宗门投影无法导入 | 明确排除 `battle-v5/adapters`,其余核心保持禁止 |
## 18. 完成定义
本方案仅在以下条件全部满足时视为完成:
1. 阶段 1 至阶段 7 全部完成,或未完成阶段有明确延期记录和原因。
2. 第 15 节所有代码、删除项和行为验收通过。
3. 最终测试、lint 和 build 全部通过。
4. `docs/sect-authoring-guide.md`、幽都和天衍设计稿与最终代码一致。
5. 没有为了恢复精确文案而重新引入同义的核心字段或 Effect。
6. 新增宗门原语准入规则已经由架构测试或作者指南固化。
7. 实施报告列出实际命令、结果、允许的行为差异和所有跳过项。
完成后的目标状态不是“battle-v5 没有任何复杂能力”,而是:每个核心能力都具有独立、稳定、可复用的战斗语义;宗门特色由内容编译组合产生;展示差异不会反向塑造伤害和运行时模型。
## 19. 实施与验证记录
完整结果见 [battle-v5-sect-decoupling-refactor-report.md](./battle-v5-sect-decoupling-refactor-report.md)。后续修改应继续使用本节模板记录验证;如果实现偏离本文,必须记录偏离原因、替代设计和重新评估后的耦合风险。
### 19.1 阶段进度
- [x] 阶段 0:冻结基线并补齐行为契约测试
- [x] 阶段 1:删除 `postDamageEffects`
- [x] 阶段 2:删除伤害展示元数据与 `violet` 透传
- [x] 阶段 3:用固定 effect plan 替代 `buffLayerScalar`
- [x] 阶段 4:将天衍元素历史下沉到内容层
- [x] 阶段 5:删除周期 Buff 反射式手动结算
- [x] 阶段 6:加强架构与准入守卫
- [x] 阶段 7:同步作者指南与宗门设计稿
- [x] 最终全量验证
### 19.2 每阶段实施记录
```md
## 阶段 N:<名称>
- 实施提交:<commit>
- 实际修改文件:
- `<path>`
- 删除的核心接口:
- `<symbol>`
- 新增的核心接口:无 / `<symbol + 准入理由>`
- 行为保真等级:E / S / A
- 允许差异:无 / `<差异>`
- 非预期差异:无 / `<差异与处理>`
### 验证
| 命令 | 结果 | 测试数量/错误摘要 |
| ----------- | -------------- | ----------------- |
| `<command>` | 通过/失败/跳过 | `<detail>` |
### 搜索门禁
| 搜索项 | 期望 | 实际 |
| ------------------ | ---- | --------- |
| `<removed symbol>` | 0 | `<count>` |
### 偏离方案
- 无;或说明偏离原因、替代实现、影响范围和批准结论。
```
### 19.3 最终差异审计
最终验收报告必须回答:
1. 本方案列出的五类删除项是否全部从 `src` 消失?
2. 是否新增了同义但改名的生命周期阶段、动态 Buff 读取或展示字段?
3. battle-v5 核心是否出现任何具体宗门 ID、状态 ID、节点 ID 或中文宗门术语?
4. 是否有只被一个宗门使用的新 `EffectConfig` 类型、runtime state 字段或事件字段?
5. 幽都和天衍的允许差异是否仍限定在本文批准的范围?
6. 红尘剑宗、无相宗、creation-v2 和双端 build 是否完成回归?
7. 是否有命令被跳过;如果跳过,原因和剩余风险是什么?
任一问题无法给出明确答案时,不应将重构标记为完成。
@@ -1,143 +0,0 @@
# battle-v5 与宗门机制解耦实施报告
> - 对应方案:[battle-v5-sect-decoupling-refactor-plan.md](./battle-v5-sect-decoupling-refactor-plan.md)
> - 实施与验收日期:2026-07-22
> - 结论:阶段 0~7 全部完成,最终测试、lint、build、格式和搜索门禁全部通过
> - 数据边界:无数据库迁移、无持久化模型变化、无稳定宗门/道途/节点/能力 ID 变化
> - 当前架构提示:这是旧 `BattleEngineV5` 1v1 基线上的历史实施报告。旧引擎现已删除,当前生产主流程为 `Team/Roster + BattleRuntime + Checkpoint + BattleRoundResolver`;报告中的测试数量和 1v1 架构陈述不代表当前状态。
## 1. 最终结论
本轮删除了五组由单宗门需求向 battle-v5 核心扩散的抽象,并用现有通用原语在宗门内容编译层重组:
1. `postDamageEffects` 被既有有序 `completionEffects` 替代。
2. 伤害 `display/tone` 透传被删除,计算层和 React 展示层不再理解“魂伤紫色”等宗门美术语义。
3. `buffLayerScalar` 被幽都四层/五层两个互斥固定 effect plan 替代。
4. `element_history` 专用 Effect 和 runtime state 被天衍隐藏 marker Buff 与通用 runtime counter 替代。
5. `buff_periodic_settlement/manualSettlementEffects` 被分层灼烧与固定反应追伤替代。
该批次当时没有改写 `BattleEngineV5` 的 1v1、行动、事件、伤害、Buff 或胜负架构。后续 Team/Roster 重构已经替换并删除这条旧主流程;本报告仍可用于追溯“battle-v5 非 Adapter 生产核心不依赖具体宗门内容”的解耦决策。
## 2. 分阶段实施结果
### 2.1 阶段 0:冻结基线
- battle-v5 基线:32 个测试文件、280 项测试通过。
- 幽都基线:5 个测试文件、90 项测试通过。
- 天衍基线:9 个测试文件、131 项测试通过。
- 四宗门基线:35 个测试文件、478 项测试通过。
基线用于确认删除抽象前后的行为边界;本阶段没有数据或架构改动。
### 2.2 阶段 1:合并冗余执行阶段
- 从 `AbilityConfig`、分层主动技能、Factory、能力分析、宗门编写/成长/展示和幽都编译器删除 `postDamageEffects`。
- 幽都效果顺序统一为:计划 effects → 基础 completion → 层 completion → 资源消费。
- 未增加同义生命周期字段。
- 聚焦验证:7 个测试文件、128 项测试通过;删除项搜索为 0。
### 2.3 阶段 2:伤害展示中性化
- 删除 `DamageDisplayMetadata`、`DamageParams.display`、伤害事件与日志数据的 `display`、`PresentedLogPart.tone`。
- 删除 React 对 `violet` 的特殊样式分支。
- 幽都混合伤害仍发布法术/真实两个标准伤害请求,但玩家日志使用中性分段展示。
- 聚焦验证:10 个测试文件、147 项测试通过;双端 build 通过。
### 2.4 阶段 3:固定计划替代动态 Buff 读层
- 从伤害配置、DamageEffect、能力事实和测试删除 `buffLayerScalar`。
- 幽都编译器生成 `finish-four` 与 `finish-five` 两个固定层和互斥计划;五层优先级 20,四层优先级 10 且要求 `< 5`。
- 每个分支只发布一个真实伤害请求,之后再执行清层/保留两层与“不归”状态。
- 聚焦验证:7 个测试文件、110 项测试通过;删除项搜索为 0。
### 2.5 阶段 4:天衍元素历史下沉
- 删除 `ElementHistoryParams`、`element_history`、`ElementHistoryEffect`、`BattleRuntimeState.elementHistories` 及其读写 API。
- 每个不同反应元素由天衍内容层创建隐藏、受保护、`countsAsStatus=false` 的 marker Buff。
- 通用 runtime counter 负责计数;重复元素不增加,第三种不同元素触发后清理全部 marker 并重置 counter。
- `creation-v2` 的稳定词缀“万象归一”改为通用的每三次激活触发,不重新引入动态元素作者模型。
- 聚焦验证:天衍/核心 12 个测试文件、170 项测试;creation-v2 18 个测试文件、133 项测试;build 通过。
### 2.6 阶段 5:移除周期 Buff 反射式结算
- 删除 `BuffPeriodicSettlementParams`、`buff_periodic_settlement`、`BuffPeriodicSettlementEffect`、`BuffConfig.manualSettlementEffects` 及成长/词缀文本投影支持。
- 灼烧改为 2 层、上限 2 层;每次目标 `ActionPre` 结算一次 `0.16 × MAGIC_ATK` DOT 后减少一层,按 2→1→0 结束;重施恢复并封顶到 2 层且刷新持续时间。
- 燎原发布固定 `0.16 × MAGIC_ATK` 的 `FOLLOW_UP`,不消耗灼烧。
- 蒸发保留主伤害 80% 追伤;另按 1/2 层灼烧发布固定 `0.16/0.32 × MAGIC_ATK` 追伤,随后只清除灼烧。熔岩独立存在。
- 聚焦验证:11 个测试文件、167 项测试通过;build 通过。
### 2.7 阶段 6:架构守卫
`architectureGuard.test.ts` 现覆盖:
- battle-v5 非 Adapter 生产核心不得包含四宗门 ID、名称或中文内容语义;
- battle-v5 核心不得导入宗门内容层或 React;
- `sect/core` 不得依赖生产宗门内容或稳定宗门 ID;
- 通用战斗 React UI 不得包含生产宗门 ID/名称;
- 五组已删除扩展不得回到生产源码;
- 守卫正则通过代表性非法输入自检。
正常验证为 1 个测试文件、11 项测试通过。另执行了一次未提交的故障注入:临时向 battle-v5 核心加入 `sect.youdu`,测试按预期失败,随后删除探针文件并再次验证通过。
### 2.8 阶段 7:文档与准入规则
已同步:
- `docs/sect-authoring-guide.md`
- `docs/youdu-battle-v5-implementation-plan.md`
- `docs/youdu-sect-design.md`
- `docs/tianyan-sacred-land-design.md`
- `src/shared/engine/sect/README.md`
作者指南新增八问准入模板,并明确优先使用固定 plan、marker、counter 和内容组合。只有第二个独立生产消费者或明确全局不变量才能支持新增 battle-v5 原语;单宗门生命周期、动态 Buff 反射和展示字段不得进入核心。
## 3. 允许的行为差异
| 范围 | 最终差异 | 接受理由 |
| --- | --- | --- |
| 幽都日志 | 删除魂伤紫色、术伤/魂伤分量前缀,改用标准中性日志 | 展示颜色不应穿透伤害核心、事件和 React 通用组件 |
| 幽都终结 | 四/五层总系数作为一个固定 coefficient 统一取整;一项既有用例出现 1 点气血差 | 固定计划的数学语义稳定,避免 DamageEffect 读取任意 Buff 层数 |
| 天衍元素记录 | `creation-v2` 的“万象归一”由三种不同元素近似为每三次激活 | 通用造物内容无法在不恢复元素专用核心 API 的情况下动态生成每元素 marker 配置 |
| 天衍燎原/蒸发 | 不再读取并重放灼烧 Buff;改为固定 `0.16` 与按层 `0.16/0.32` 追伤 | 保留额外一跳、按剩余灼烧强弱兑现和清除灼烧的战术用途,同时消除反射式 Buff 耦合 |
没有发现超出以上批准范围的非预期差异。
## 4. 最终验证记录
| 命令 | 结果 | 明细 |
| --- | --- | --- |
| `bunx vitest run src/shared/engine/sect/testing/architecture/architectureGuard.test.ts` | 通过 | 1 文件,11 测试 |
| `bunx vitest run src/shared/engine/battle-v5/tests` | 通过 | 32 文件,277 测试 |
| `bunx vitest run src/shared/engine/sect` | 通过 | 35 文件,484 测试 |
| `bunx vitest run src/shared/engine/creation-v2` | 通过 | 18 文件,133 测试 |
| `bun run lint` | 通过 | ESLint 无错误 |
| `bun run test` | 通过 | 245 文件,1795 测试 |
| `bun run build` | 通过 | client 与 server TypeScript/Vite 构建均通过 |
| `bunx prettier --check <七个同步文档>` | 通过 | 全部符合 Prettier |
| `git diff --check` | 通过 | 无空白错误 |
### 4.1 删除项搜索门禁
架构守卫测试必须保留历史符号作为禁止项正则和自检样本,因此下列生产源码搜索排除该测试文件:
```bash
rg -n --glob '!src/shared/engine/sect/testing/architecture/architectureGuard.test.ts' "postDamageEffects|DamageDisplayMetadata|buffLayerScalar" src
rg -n --glob '!src/shared/engine/sect/testing/architecture/architectureGuard.test.ts' "ElementHistory|element_history|elementHistories" src
rg -n --glob '!src/shared/engine/sect/testing/architecture/architectureGuard.test.ts' "BuffPeriodicSettlement|buff_periodic_settlement|manualSettlementEffects" src
rg -n "tone.*violet|display: \{ label: '(魂伤|术伤)'" src
```
四项结果均为 0。代码审计也未发现同用途改名替代:没有新的宗门专用生命周期阶段、动态 Buff 配置执行器、元素专用 runtime state、伤害展示字段或单宗门 Effect 类型。
## 5. 完成审计
1. 五组删除项已从生产源码消失:是。
2. 是否新增同义替代抽象:否。
3. battle-v5 非 Adapter 生产核心是否含具体宗门 ID、状态 ID、节点 ID 或中文宗门术语:否,由架构测试约束。
4. 是否新增仅单宗门使用的 EffectConfig、runtime state 或事件字段:否。
5. 幽都和天衍差异是否仅限批准范围:是,见第 3 节。
6. 红尘剑宗、无相宗、creation-v2 和双端 build 是否完成回归:是。
7. 是否有跳过命令:否。
8. 是否存在数据迁移、持久化兼容或稳定 ID 风险:否。
本轮重构达到方案完成定义。后续宗门若需要类似复杂机制,应先依据作者指南的八问模板证明其通用性;无法证明时,继续优先采用内容层固定计划、隐藏 marker、通用 counter 或可接受的近似语义。
File diff suppressed because it is too large. Load diff
-721
View File
@@ -1,721 +0,0 @@
# 「天地灵气」统一体力系统 — 重构方案
## 一、背景与动机
### 1.1 现状问题
《万界道友》当前各玩法采用**独立的限制机制**:副本每天 2 次、天骄榜每天 10 次、闭关修炼每天消耗 200 年寿元上限等。这些限制各自为政,带来了几个核心问题:
- **玩家体验僵硬**:每种玩法的次数相互独立,玩家无法按自己的偏好分配游戏时间。想多刷副本的玩家无法把天骄榜的次数"转移"过来。
- **成本管控分散**:每个需要 LLM 参与的玩法都有自己的限制器(`dungeonLimiter`、`characterGenerationLimiter` 等),缺乏统一的成本视角。
- **扩展性差**:新增一个玩法就要写一个新的 Limiter,数值调整需要逐个系统修改。
- **付费设计困难**:独立的次数限制难以设计统一的付费补充方案。
### 1.2 设计目标
引入一个**统一的「天地灵气」资源系统**,实现:
1. **统一成本度量**:将所有涉及 LLM 调用或重要游戏进展的行为,统一折算为「灵气」消耗。
2. **玩家自主分配**:取消各玩法的独立次数限制,玩家持有一个灵气池,自由选择参与什么玩法。
3. **每日懒刷新**:每天按需恢复灵气(玩家操作或登录时触发),不做全表批量刷新,保证日活节奏的同时避免百万级角色的性能问题。
4. **付费扩展预留**:可通过道具(如「聚灵丹」)补充灵气,为后续商业化留口子。
5. **反作弊保底**:保留必要的冷却时间和并发锁,防止短时间内恶意刷取。
---
## 二、核心概念设计
### 2.1 名称与世界观融合
推荐名称:**「天地灵气」**(简称「灵气」)
世界观包装:修仙世界中,天地间的灵气是万物修行的根基。每一次探索秘境、炼制丹药、推演天机,都需要汲取天地灵气。灵气会随着时间自然汇聚,也可通过灵石阵法或丹药加速恢复。
选择理由:
- 「灵气」是修仙世界观中最通用的概念,天然适合做统一资源。
- 比「体力值」更有沉浸感,比「疲劳值」更正面积极。
- 与现有的灵石、修为形成三层资源体系:**灵石(货币)→ 灵气(行动力)→ 修为(进度)**。
### 2.2 资源定义
| 属性 | 值 | 说明 |
|------|------|------|
| 资源名称 | 天地灵气(Qi) | 统一体力资源 |
| 存储位置 | `cultivators.qi` (integer) | 新增数据库列 |
| 上限(Max Qi) | **200**(全角色统一) | 不随境界变化,所有玩家公平一致 |
| 每日恢复量 | **200**(全角色统一) | 每日恢复至上限,等效 4 次副本 |
| 恢复策略 | **懒刷新**(无批量 cron) | 玩家执行操作或登录时按需刷新,不对全表做定时 UPDATE |
| 溢出规则 | 基础上限 200 | 自然恢复不超过上限,道具补充可临时超出上限(不超过 300,即 150%) |
### 2.3 统一上限设计说明
所有角色不论境界,灵气上限和每日恢复量均为 **200 点**。
设计理由:
- **公平性**:新玩家与老玩家拥有相同的每日行动力,降低了新玩家的追赶压力。
- **简单直观**:不需要向玩家解释"为什么我的灵气上限比他少",一个数字走天下。
- **成本可控**:统一的灵气上限意味着每个角色的 LLM 成本上限一致,便于运营预算。
- **境界差异化体现在别处**:高境界角色在战斗数值、可用玩法、装备品质上有优势,灵气层面保持平等。
> 注:如果未来希望给高境界玩家更多灵气,只需在配置文件中引入境界映射表即可,当前设计不做过度预留。
### 2.4 灵气突破上限(道具补充溢出)
通过付费道具(「聚灵丹」等)补充灵气时,允许临时超出基础上限,但设有**溢出上限**:
- 溢出上限 = 200 × 150% = **300**
- 自然恢复只在低于 200 时生效
- 灵气会随时间消耗逐渐回到基础上限以内
这个设计参考了主流手游(如原神的浓缩树脂、明日方舟的理智)的做法,既保护了付费意愿,又防止无限堆积。
---
## 三、各玩法灵气消耗定价
### 3.1 定价原则
1. **LLM 成本正比原则**:LLM 调用越重的玩法,灵气消耗越高。
2. **进度价值原则**:对角色成长影响越大的行为,消耗越高。
3. **低门槛保底**:基础体验行为(如查看信息、世界聊天)不消耗灵气。
4. **PvP 公平性**:PvP 战斗本身的确定性部分(天骄榜挑战、赌战)消耗较低,但附带 LLM 的战前/战后叙事则增加消耗。
### 3.2 灵气消耗表
| 玩法 | 行为 | 灵气消耗 | LLM 调用 | 说明 |
|------|------|---------|---------|------|
| **副本探索** | 开始一次副本 | **50** | 是(多轮叙事生成) | 原限制 2 次/天 → 现等效 4 次,LLM 最重的玩法 |
| **造物炼器** | 创建一件物品 | **8** | 是(LLM 生成) | 含法宝、功法、神通,含叙事润色 |
| **炼丹(自由)** | 自由炼丹一次 | **15** | 是(LLM 属性解析) | 高 LLM 消耗,需完整解析玩家输入 |
| **炼丹(丹方)** | 按丹方炼丹 | **8** | 轻度(叙事润色) | LLM 消耗较轻,与造物齐平 |
| **丹方解析** | 解析未知丹方 | **5** | 是(LLM 分析) | 保留 30 秒冷却防刷 |
| **幻境之塔** | 开始新一轮 | **30** | 是(敌人生成) | 赛季制,LLM 调用较重 |
| **闭关修炼** | 每消耗 10 年寿元 | **5** | 是(突破故事) | 原限制 200 年/天,现等效可闭关 400 年 |
| **突破** | 尝试破境 | **15** | 是(破境叙事) | 重要节点,单独定价 |
| **历练收益** | 领取离线收益 | **8** | 是(收益故事 + 材料生成) | 每日 1-2 次 |
| **天骄榜挑战** | 发起一次挑战 | **8** | 否 | 确定性战斗,轻度消耗 |
| **赌战** | 创建/参与赌战 | **8** | 否 | 确定性战斗 |
| **命格重塑** | 重塑命格 | **30** | 是(LLM 生成) | 重要行为,高消耗 |
| **天机推演** | 每日运势 | **5** | 是(LLM 生成) | 每日 1 次,轻量 |
| **坊市鉴定** | 鉴定物品 | **5** | 是(LLM 评估) | 低消耗 |
| **灵眼之泉疗养** | 恢复 HP/MP | **0** | 否 | 纯数值操作,不消耗灵气 |
| **服用丹药** | 使用消耗品 | **0** | 否 | 纯数值操作 |
| **拍卖行** | 买/卖物品 | **0** | 否 | 玩家间经济 |
| **世界聊天** | 发送消息 | **0** | 否 | 保留 60 秒冷却即可 |
| **问法寻卷** | 抽卡 | **0** | 否(间接) | 已有符箓消耗做门槛 |
### 3.3 不消耗灵气但保留限制的行为
以下行为不涉及 LLM 或已用其他资源做门槛,无需纳入灵气系统:
- **角色创建**:保持每日 6 次 / 邮箱 / IP 的限制(防止批量注册滥用)
- **兑换码**:保持现有机制
- **日常任务**:任务本身不消耗灵气(它们是被动追踪的行为)
---
## 四、替代现有限制器的迁移策略
### 4.1 被替代的独立限制器
| 现有限制器 | 原限制 | 迁移方案 |
|-----------|-------|---------|
| `dungeonLimiter` | 2 次/天 | **废弃**。由灵气系统统一管控,副本 50 灵气/次,等效 4 次 |
| 每日寿元上限 | 200 年/天 | **已删除**。闭关改为消耗灵气(5 灵气/10 年),寿元仍按角色实际寿元扣除 |
| `rankings daily challenges` | 10 次/天 | **废弃**。天骄榜挑战改为每次消耗 8 灵气 |
### 4.2 保留的限制器
| 限制器 | 原因 |
|--------|------|
| `characterGenerationLimiter` | 防批量注册,与游戏内体力无关 |
| `worldChatLimiter`(60 秒冷却) | 防刷屏,保留 |
| `alchemyFormulaCooldown`(30 秒冷却) | 防脚本刷解析,保留作为反刷机制 |
| 并发锁(`retreatLock`、challenge lock 等) | 防并发冲突,保留 |
| 拍卖行上架数量上限(5 个) | 经济平衡,保留 |
| 赌战上限(1 个) | 经济平衡,保留 |
### 4.3 迁移方式:直接切换
本系统采用**一次性直接切换**,不设过渡期:
1. 执行数据库迁移,为所有角色新增灵气列,初始值设为 200(满灵气)。
2. 在同一个版本中,所有玩法入口统一接入灵气消耗检查。
3. 同步废弃旧限制器(`dungeonLimiter`、每日寿元上限、`rankings daily challenges`)。
4. 旧限制器的 Redis 键会在次日自然过期,无需手动清理。
选择直接切换的原因:
- 灵气系统在各玩法的等效次数上不低于旧限制(副本从 2 次提升到 4 次),玩家体验只会变好。
- 灰度并存阶段会让代码复杂度翻倍(既要检查灵气又要检查旧限制),增加出错风险。
- 数值配置化设计,如有问题可快速调整灵气消耗值来应对。
---
## 五、数据库与技术方案
### 5.1 数据库变更
**新增列(迁移文件 0048+):**
```sql
ALTER TABLE wanjiedaoyou_cultivators
ADD COLUMN qi INTEGER NOT NULL DEFAULT 200,
ADD COLUMN qi_max INTEGER NOT NULL DEFAULT 200,
ADD COLUMN qi_last_refreshed_at TIMESTAMPTZ DEFAULT NOW();
```
| 列名 | 类型 | 说明 |
|------|------|------|
| `qi` | INTEGER | 当前灵气值,默认 200(满灵气) |
| `qi_max` | INTEGER | 灵气上限,当前统一 200,预留给未来可能的调整 |
| `qi_last_refreshed_at` | TIMESTAMPTZ | 上次灵气刷新时间,用于懒刷新判断 |
**新增灵气日志表(可选但推荐):**
```sql
CREATE TABLE wanjiedaoyou_qi_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cultivator_id UUID NOT NULL REFERENCES wanjiedaoyou_cultivators(id),
action VARCHAR(50) NOT NULL, -- 'dungeon_start', 'retreat', 'craft', etc.
qi_cost INTEGER NOT NULL, -- 消耗量(正数)
qi_gain INTEGER NOT NULL DEFAULT 0,-- 获得量(道具补充等)
qi_before INTEGER NOT NULL,
qi_after INTEGER NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_qi_logs_cultivator_date
ON wanjiedaoyou_qi_logs(cultivator_id, created_at DESC);
```
这张表用于审计、数据分析和反作弊。可以定期清理超过 30 天的日志。
### 5.2 核心服务层设计
**新增 `QiService`(`src/server/lib/services/QiService.ts`):**
```typescript
/**
* 天地灵气管理服务
*
* 核心职责:
* 1. 检查灵气是否充足
* 2. 扣除灵气(原子操作)
* 3. 补充灵气(道具/付费)
* 4. 懒刷新(操作/登录时按需触发)
*/
// 灵气消耗配置表
const QI_COSTS: Record<string, number> = {
dungeon_start: 50,
retreat_per_10_years: 5,
breakthrough: 15,
yield_claim: 8,
craft_creation: 8,
alchemy_improvised: 15,
alchemy_formula: 8,
alchemy_formula_analysis: 5,
tower_start: 30,
ranking_challenge: 8,
bet_battle: 8,
divine_fortune: 5,
fate_reshape: 30,
market_appraisal: 5,
};
// 统一灵气上限与每日恢复量
const QI_MAX = 200;
const QI_DAILY_REFRESH = 200;
```
**核心方法:**
```typescript
class QiService {
/**
* 检查灵气是否充足(不扣除)
* 内部会先触发懒刷新(如跨天)
*/
async checkQi(cultivatorId: string, action: string): Promise<{
sufficient: boolean;
current: number;
required: number;
}>;
/**
* 消耗灵气(原子操作,含并发保护)
* 执行前自动触发懒刷新(如跨天)
* 返回扣除后的灵气值
*/
async consumeQi(cultivatorId: string, action: string): Promise<{
success: boolean;
qiBefore: number;
qiAfter: number;
consumed: number;
}>;
/**
* 补充灵气(道具/付费)
* 允许溢出至 300(QI_MAX * 1.5)
*/
async restoreQi(cultivatorId: string, amount: number, source: string): Promise<{
success: boolean;
qiBefore: number;
qiAfter: number;
restored: number;
overflowCap: number;
}>;
/**
* 懒刷新:检查是否需要跨天恢复灵气
* 在执行灵气操作或角色登录时调用
* 如果 qi_last_refreshed_at 不是今天(Asia/Shanghai),则恢复至上限
*/
async refreshIfNeeded(cultivatorId: string): Promise<void>;
}
```
### 5.3 每日刷新策略:纯懒刷新
**不使用定时 cron 任务**,完全依赖懒刷新。
原因:当角色数量达到数十万甚至百万级别时,每日全表 UPDATE 不仅成本高昂(大量无用写操作),而且会产生明显的执行延迟。绝大多数角色可能长期不登录,批量刷新纯属浪费。
**懒刷新机制:**
1. **触发时机**:每次玩家执行灵气消耗操作前,或角色登录加载信息时。
2. **判断逻辑**:比较 `qi_last_refreshed_at` 与当前时间(Asia/Shanghai 时区)是否为同一天。如果跨天,则执行一次刷新。
3. **刷新行为**:如果当前灵气 < 上限(200),则恢复至上限;如果灵气已 >= 上限(如道具补充溢出),则保持不变,只更新时间戳。
4. **原子性**:刷新和灵气扣除在同一次数据库操作中完成,避免并发问题。
**懒刷新伪代码:**
```typescript
async function refreshAndConsume(cultivatorId: string, action: string) {
const cost = QI_COSTS[action];
if (!cost) throw new Error(`Unknown action: ${action}`);
const today = getTodayString('Asia/Shanghai'); // 'YYYY-MM-DD'
// 一次 SQL 同时完成懒刷新 + 扣除(利用 CASE WHEN 保证原子性)
const result = await db.execute(sql`
UPDATE wanjiedaoyou_cultivators
SET
qi = CASE
WHEN DATE(qi_last_refreshed_at AT TIME ZONE 'Asia/Shanghai') < ${today}::date
THEN LEAST(${QI_MAX}, GREATEST(qi, ${QI_MAX})) - ${cost}
ELSE qi - ${cost}
END,
qi_last_refreshed_at = CASE
WHEN DATE(qi_last_refreshed_at AT TIME ZONE 'Asia/Shanghai') < ${today}::date
THEN NOW()
ELSE qi_last_refreshed_at
END
WHERE id = ${cultivatorId}
AND (
CASE
WHEN DATE(qi_last_refreshed_at AT TIME ZONE 'Asia/Shanghai') < ${today}::date
THEN LEAST(${QI_MAX}, GREATEST(qi, ${QI_MAX}))
ELSE qi
END
) >= ${cost}
RETURNING qi, qi_last_refreshed_at
`);
if (result.length === 0) {
// 灵气不足,查询当前值返回给调用方
return { success: false, ... };
}
return { success: true, qiAfter: result[0].qi, consumed: cost };
}
```
**登录时预刷新(可选优化):**
在角色信息加载接口中调用 `refreshIfNeeded()`,让玩家登录时就能看到正确的灵气值,而不是等到操作时才发现跨天了。这只是一个展示优化,核心逻辑仍由操作时的原子刷新保证。
**为什么不需要 cron 兜底:**
- 灵气数据只在玩家操作时才被读取和消费,不活跃的角色的灵气值不需要是最新的。
- 排行榜等读取场景只关心排名数据(战斗结果),不依赖灵气值做排序。
- 如果需要查看不活跃玩家的灵气值(如管理后台),可以在查询层做日期判断后展示"明日恢复"标签。
### 5.4 与现有 Redis 限制器的关系
灵气系统**主要使用数据库**存储,不依赖 Redis 做核心计数。原因:
- 灵气值需要与角色数据一起参与事务(扣灵气 + 执行玩法 = 一个事务)。
- 数据库操作可以保证原子性和一致性。
- Redis 限制器的模式(`{feature}:daily:{id}:{date}`)不再需要,可以随旧限制器一起清理。
但仍保留 Redis 用于:
- **并发锁**:防止同一玩家同时发起多个消耗灵气的操作。
- **短冷却**:炼丹公式解析 30 秒、世界聊天 60 秒等。
---
## 六、付费设计预留
### 6.1 灵气补充道具
| 道具名称 | 效果 | 获取方式 | 设计意图 |
|---------|------|---------|---------|
| **聚灵丹(小)** | 恢复 50 灵气 | 商城购买 / 活动奖励 | 等效 1 次副本 |
| **聚灵丹(中)** | 恢复 100 灵气 | 商城购买 / 周卡奖励 | 等效半日行动力 |
| **聚灵丹(大)** | 恢复 200 灵气 | 商城购买 / 月卡奖励 | 等效一整管灵气 |
| **天地灵泉** | 立即回满至上限 | 稀有活动 / 高价位商城 | 应急使用 |
### 6.2 付费边界原则
- **绝不售卖灵气本身**:灵气只能通过游戏内自然恢复或道具补充,道具可以通过游戏内途径获得。
- **付费加速而非付费获胜**:付费只增加每日可做的事情量,不影响单次玩法的收益。
- **防沉迷底线**:每日道具补充灵气有次数上限(如聚灵丹每天最多使用 3 次),防止无限制氪金刷取。
- **VIP/月卡体系**:可设计「道友令牌」月卡,效果为每日灵气上限 +40(即 240)、每日自动恢复量 +40。
### 6.3 未来扩展接口
道具补充灵气的接口设计为通用的 `restoreQi(cultivatorId, amount, source)` 方法,`source` 参数可以是:
- `'item'`:使用背包中的聚灵丹
- `'shop'`:商城直接购买
- `'monthly_card'`:月卡每日自动补充
- `'gm'`:管理员手动补充
- `'compensation'`:系统补偿(如维护补偿邮件附带灵气)
---
## 七、前端交互设计
### 7.1 灵气显示
在游戏主界面(角色信息面板)顶部显示灵气状态:
```
天地灵气 ████████████████░░░░ 144/200
```
- 进度条颜色:>60% 绿色,30-60% 黄色,<30% 红色
- 点击可展开灵气详情面板
### 7.2 灵气详情面板
展示内容:
- 当前灵气 / 上限
- 今日恢复量 / 已消耗量
- 各玩法消耗明细(今日)
- 道具补充入口(聚灵丹按钮)
- 下次自然恢复时间(次日 00:00)
### 7.3 玩法入口拦截
当灵气不足时,各玩法入口按钮变为灰色,显示:
```
灵气不足(需要 50,当前 12)
[使用聚灵丹] [等待恢复]
```
### 7.4 灵气消耗预览
在玩家确认执行操作前,显示灵气消耗提示:
```
┌─ 副本探索 ──────────────────┐
│ 消耗灵气:50 │
│ 当前灵气:144 → 94 │
│ │
│ [确认出发] │
└────────────────────────────┘
```
### 7.5 前端 Contract 变更
在 `src/shared/contracts/` 中新增:
```typescript
// 灵气相关 contract
export interface QiState {
current: number;
max: number;
dailyRefresh: number;
lastRefreshedAt: string; // ISO datetime
todayConsumed: number; // 今日已消耗(可选,用于详情展示)
todayRestored: number; // 今日已补充(道具)
}
export interface QiConsumeResult {
success: boolean;
qiBefore: number;
qiAfter: number;
consumed: number;
action: string;
message?: string; // 灵气不足时的提示
}
export interface QiRestoreResult {
success: boolean;
qiBefore: number;
qiAfter: number;
restored: number;
overflowCap: number;
}
```
---
## 八、数值平衡与经济影响
### 8.1 等效日常活动量对比
以**200 灵气/天**为例,与当前系统对比:
| 玩法 | 当前限制 | 新系统下的等效量 | 变化 |
|------|---------|----------------|------|
| 副本 | 2 次/天 | 200 ÷ 50 = **4 次**(纯副本的话) | 翻倍 |
| 天骄榜 | 10 次/天 | 200 ÷ 8 = **25 次**(纯挑战的话) | 大幅增加 |
| 闭关 | 200 年/天 | 200 ÷ 5 × 10 = **400 年**(纯闭关的话) | 翻倍 |
| 造物 | 无上限 | 200 ÷ 8 = **25 次**(纯造物的话) | 明确上限 |
**但这正是设计的核心意图**:玩家不可能把所有灵气都花在一个玩法上。灵气系统的本质是让玩家**在多种玩法之间分配有限的行动力**,而不是在每种玩法上都无限制。
一个典型玩家的日活分配可能如下:
| 行为 | 次数 | 灵气消耗 | 小计 |
|------|------|---------|------|
| 领取离线收益 | 1 次 | 8 | 8 |
| 天机推演 | 1 次 | 5 | 5 |
| 闭关修炼 30 年 | 1 次 | 15 | 15 |
| 副本探索 | 2 次 | 50 | 100 |
| 炼丹 | 1 次 | 8 | 8 |
| 天骄榜挑战 | 3 次 | 8 | 24 |
| 幻境之塔 | 1 次 | 30 | 30 |
| 造物 | 1 次 | 8 | 8 |
| **合计** | | | **198 / 200** |
几乎用满了灵气,但玩家可以根据自己的需求灵活调整——今天想多刷一次副本?放弃天骄榜和幻境之塔就行。
### 8.2 LLM 成本预估
按上述典型日活计算 LLM 调用次数:
| 行为 | LLM 调用 | 每日次数 | 总调用 |
|------|---------|---------|-------|
| 离线收益 | 1 次 | 1 | 1 |
| 天机推演 | 1 次 | 1 | 1 |
| 闭关突破故事 | 1 次 | ~0.3(约 3 天 1 次突破) | 0.3 |
| 副本叙事(3-5 轮/次) | 3-5 次 | 2 | 8 |
| 炼丹 | 1 次 | 1 | 1 |
| 幻境之塔敌人生成 | 1 次 | 1 | 1 |
| 造物 | 2-3 次 | 1 | 2.5 |
| 天骄榜(无 LLM) | 0 次 | 3 | 0 |
| **合计** | | | **~14.8 次/天/玩家** |
对比旧系统:
| 行为 | 旧系统每日 LLM 调用上限 |
|------|---------------------|
| 副本叙事 | 2 × 5 = 10 |
| 闭关突破 | ~1(受寿元限制) |
| 离线收益 | 1 |
| 天机推演 | 1 |
| 炼丹 | 无上限(实际 ~2-3) |
| 造物 | 无上限(实际 ~1-2) |
| 幻境之塔 | 无上限 |
| **合计** | **~17-20 次/天/玩家** |
灵气系统下 LLM 成本**反而略有下降**(从 ~18 次降至 ~15 次),因为副本虽然从 2 次增到 4 次(上限),但典型玩家通常只跑 2 次,而造物/天骄榜等低消耗玩法的增加不会带来额外 LLM 成本。灵气系统提供了更精确的成本控制——灵气上限 200 就是一个硬性的每日 LLM 预算天花板。
### 8.3 灵石经济影响
灵气系统与灵石经济正交。灵气控制行动次数,灵石控制物品价值。两者不会产生直接冲突。
需要注意的是:当玩家可以更多地刷副本(从 2 次增到最多 4 次),副本产出的材料/灵石总量会增加。建议同步审视副本掉落表,确保经济不会通胀。如有必要,可以在高灵气消耗下降低单次副本的掉落量。
---
## 九、数值配置化设计
所有灵气相关数值应集中在配置文件中,便于后续调整:
**新增文件 `src/shared/config/qiSystem.ts`:**
```typescript
/**
* 天地灵气系统配置
* 所有数值集中管理,方便策划调整
*/
/** 统一灵气上限(所有角色一致) */
export const QI_MAX = 200;
/** 每日灵气恢复量(所有角色一致) */
export const QI_DAILY_REFRESH = 200;
/** 各玩法灵气消耗 */
export const QI_ACTION_COSTS = {
dungeon_start: 50,
retreat_per_10_years: 5,
breakthrough: 15,
yield_claim: 8,
craft_creation: 8,
alchemy_improvised: 15,
alchemy_formula: 8,
alchemy_formula_analysis: 5,
tower_start: 30,
ranking_challenge: 8,
bet_battle: 8,
divine_fortune: 5,
fate_reshape: 30,
market_appraisal: 5,
} as const;
/** 道具补充灵气的溢出系数(相对于基础上限) */
export const QI_OVERFLOW_RATIO = 1.5;
/** 溢出上限(QI_MAX * QI_OVERFLOW_RATIO) */
export const QI_OVERFLOW_MAX = 300;
/** 每日道具补充次数上限 */
export const QI_DAILY_RESTORE_LIMIT = 3;
/** 灵气刷新时区 */
export const QI_REFRESH_TIMEZONE = 'Asia/Shanghai';
```
---
## 十、API 设计
### 10.1 新增接口
| 方法 | 路径 | 说明 |
|------|------|------|
| `GET` | `/api/cultivator/qi` | 获取当前灵气状态 |
| `POST` | `/api/cultivator/qi/restore` | 使用道具恢复灵气 |
| `GET` | `/api/cultivator/qi/logs` | 查看今日灵气消耗明细 |
### 10.2 现有接口变更
所有需要消耗灵气的接口,在执行主逻辑之前,统一调用 `QiService.consumeQi()` 进行灵气扣除。扣除失败则返回 `402 Qi Insufficient` 错误码(自定义),附带所需灵气量和当前灵气量。
**错误响应示例:**
```json
{
"error": "QI_INSUFFICIENT",
"message": "灵气不足,无法探索副本",
"required": 50,
"current": 12,
"action": "dungeon_start"
}
```
---
## 十一、实施计划
### Phase 0:准备(1-2 天)
- 创建 `qiSystem.ts` 配置文件
- 新增 `QiService` 服务(含懒刷新逻辑)
- 编写数据库迁移文件
- 新增灵气相关 Contract 类型
### Phase 1:核心玩法接入(3-5 天)
- 数据库迁移(新增列 + 日志表)
- `QiService` 实现(消耗、恢复、懒刷新)
- 改造副本系统接入灵气消耗(50/次)
- 改造闭关/突破系统接入灵气消耗
- 改造天骄榜接入灵气消耗
- **同步废弃**旧限制器(`dungeonLimiter`、每日寿元上限、rankings daily)
### Phase 2:全玩法接入(3-5 天)
- 改造造物/炼丹系统(8/15)
- 改造幻境之塔(30)
- 改造历练收益(8)
- 改造命格重塑(30)
- 改造天机推演(5)
- 改造坊市鉴定(5)
### Phase 3:前端适配(3-5 天)
- 灵气状态栏 UI
- 灵气详情面板
- 各玩法入口拦截与消耗预览
- 道具补充入口
### Phase 4:付费道具(后续版本)
- 实现聚灵丹道具(走现有消耗品系统)
- 商城上架
- 月卡/VIP 灵气加成
---
## 十二、风险与对策
### 12.1 风险清单
| 风险 | 影响 | 概率 | 对策 |
|------|------|------|------|
| 灵气消耗定价不合理 | 某些玩法被冷落或过度使用 | 高 | 配置化设计便于快速调整,上线后根据数据微调 |
| LLM 成本意外上升 | 运营费用增加 | 低 | 灵气系统天然限制了每日 LLM 调用总量(~15 次/天),比旧系统更可控 |
| 老玩家不适应 | 社区负面反馈 | 中 | 提前公告,副本从 2 次提升到 4 次,整体体验只升不降 |
| 副本产出通胀 | 灵石/材料经济失衡 | 中 | 同步审视副本掉落表,必要时按灵气消耗比例降低单次掉落 |
| 灵气刷新时区问题 | 海外玩家体验差 | 低 | 当前以 Asia/Shanghai 为准,后续可考虑按玩家时区个性化 |
### 12.2 回滚方案
灵气系统通过数据库列和独立服务实现,与旧限制器解耦。如需回滚:
1. 在配置中关闭灵气检查(`QiService.consumeQi` 变为 no-op)
2. 重新启用旧限制器
3. 灵气数据保留不删除,便于后续重新上线
---
## 十三、改动文件汇总
| 文件 | 操作 | 说明 |
|------|------|------|
| `drizzle/0048_qi_system.sql` | 新增 | 数据库迁移:灵气列 + 日志表 |
| `src/shared/config/qiSystem.ts` | 新增 | 灵气系统数值配置 |
| `src/shared/types/qi.ts` | 新增 | 灵气相关类型定义 |
| `src/shared/contracts/qi.ts` | 新增 | 灵气 API Contract |
| `src/server/lib/services/QiService.ts` | 新增 | 灵气管理核心服务 |
| `src/server/routes/api/cultivator.ts` | 修改 | 新增灵气查询/恢复接口 |
| `src/server/lib/dungeon/dungeonLimiter.ts` | 废弃 | 由灵气系统替代 |
| `src/server/lib/redis/retreatLock.ts` | 保留 | 仅用于闭关/突破并发锁,不再承载每日寿元限制 |
| `src/server/lib/redis/rankings.ts` | 修改 | 移除每日挑战计数逻辑 |
| `src/server/lib/services/DungeonService.ts` | 修改 | 接入灵气消耗 |
| `src/server/lib/services/CultivationService.ts` | 修改 | 闭关/突破接入灵气消耗 |
| `src/server/lib/services/RankingService.ts` | 修改 | 天骄榜接入灵气消耗 |
| `src/server/lib/services/AlchemyService.ts` | 修改 | 炼丹接入灵气消耗 |
| `src/server/lib/services/CraftService.ts` | 修改 | 造物接入灵气消耗 |
| `src/server/lib/services/TowerService.ts` | 修改 | 幻境之塔接入灵气消耗 |
| `src/server/lib/services/MarketService.ts` | 修改 | 鉴定接入灵气消耗 |
| `src/server/lib/services/FateReshapeService.ts` | 修改 | 命格重塑接入灵气消耗 |
| `src/server/lib/services/DivineFortuneService.ts` | 修改 | 天机推演接入灵气消耗 |
| `src/server/lib/services/YieldService.ts` | 修改 | 历练收益接入灵气消耗 |
| `src/react-app/` | 修改 | 前端灵气 UI 适配 |
| `docs/design-qi-system.md` | 新增 | 本设计文档 |
---
## 十四、总结
「天地灵气」统一体力系统的核心价值在于:
**对玩家**:从「每种玩法各有限制」变为「一个灵气池自由分配」,游戏体验更自由、更有策略深度。选择今天多刷副本还是多炼丹,成为了有意义的决策。
**对开发者**:从「N 个独立限制器」变为「1 个灵气配置表」,新增玩法只需在 `QI_ACTION_COSTS` 中加一行配置。数值调整只需改一个文件。
**对运营**:灵气系统提供了精确的成本控制能力——每个玩家的每日 LLM 调用上限由灵气上限和消耗配置决定,可以精确计算和调整。同时为付费道具(聚灵丹)和会员体系(月卡灵气加成)预留了清晰的接口。
整套方案在现有架构上改动可控,核心依赖已有的数据库事务和并发锁机制,懒刷新设计避免了百万级角色的批量 UPDATE 问题,不需要引入新的中间件或定时任务。
+303
View File
@@ -0,0 +1,303 @@
# 本地开发与部署
> 本页整理《万界道友》的本地开发、环境变量、数据库、构建、Docker、部署脚本与生产 cron 配置。项目简介、玩法、截图与赞助信息见 [README](../README.md)。
## 目录结构
```text
.
├── src/index.ts # Bun 后端入口,导出 Hono API 与 WebSocket 配置
├── src/server/ # Hono API、认证、服务层、数据库访问
├── src/react-app/ # React SPA
├── src/shared/ # 共享引擎、配置、类型、契约
├── drizzle/ # 业务表 Drizzle migrations
├── drizzle-auth/ # Better Auth Drizzle migrations
├── drizzle.auth.config.ts # Better Auth 独立迁移配置
├── scripts/ # 部署脚本与生产/NATS Compose
├── docker/Dockerfile.app # Bun 主服务镜像
└── vite.config.ts
```
## 运行方式
这个仓库不是 SSR 应用。
- `src/react-app` 使用 `BrowserRouter` 管理前端路由
- `src/server/app.ts` 提供 `/api/*` 和 `/internal/*` 接口
- `src/index.ts` 在生产环境注册 Bun 内置 cron;Cron 只向 NATS WorkQueue 发布后台 command
- 前端 SPA 独立部署到 Cloudflare Pages;后端 Docker 不再服务 `index.html` 或静态资源
当前路由约定:
- `/api/*`:游戏与后台 API
- `/api/auth/*`:Better Auth
- `/internal/cron/*`:内部定时任务接口
- `/api/health-check`:健康检查
- 其余如 `/login`、`/game`、`/admin`:Cloudflare Pages 上的前端 SPA 路由
## 环境要求
- `Bun 1.3+`
- `PostgreSQL`
- `Redis`:在线对局、邀请、截止时间、恢复索引和 API 部分能力的权威存储
- `NATS`:进程启动硬依赖;JetStream 承载领域事件、异步投影、后台 command、战斗演算指针、终态清理和回放归档,Core 只承载可丢失的跨实例实时提示
说明:
- 仓库脚本默认围绕 `bun` / `bunx` 编写,不建议继续沿用旧的 `npm + Next.js` 使用方式
- 开发模式默认端口是 `5173`
- 构建后服务默认端口是 `3000`
## 安装
```bash
bun install
cp .env.example .env.local
```
## 环境变量
### 启动时必需
这些变量缺失时,服务会在启动阶段或鉴权初始化阶段直接报错:
| 变量 | 说明 |
| --- | --- |
| `DATABASE_URL` | PostgreSQL 连接串 |
| `BETTER_AUTH_SECRET` | Better Auth 密钥 |
| `BETTER_AUTH_URL` | Better Auth 后端对外基准地址;生产填 API 域名,如 `https://api.example.com` |
| `NATS_SERVERS` | NATS 服务地址,多个地址使用逗号分隔 |
| `NATS_USER` / `NATS_PASSWORD` | NATS 应用用户凭据 |
实时战斗由 Bun/Hono 主服务直接承载;生产环境还必须配置:
| 变量 | 说明 |
| --- | --- |
| `REDIS_URL` | 在线对局唯一权威状态、邀请、凭据、截止时间与恢复索引 |
| `NATS_SERVERS` / `NATS_USER` / `NATS_PASSWORD` | 战斗演算、终态清理和回放归档使用的 JetStream,以及跨实例状态提示使用的 NATS Core |
客户端通过已认证的 session API 获取 60 秒有效的一次性 WebSocket ticket,不能自行声明玩家身份。实时战斗的数据边界固定为:`battle-v5` 只做确定性规则解析;Bun 主服务负责协议、调度和广播;进行中的权威状态、选招、锁定、邀请、演出战报和回放素材只在 Redis。主服务通过 JetStream 小型指针任务分配统一演算和终态清理;回放归档 consumer 再从 Redis 组装 `BattleReplayV1`,异步、幂等写入 `wanjiedaoyou_battle_replay_archives`。NATS 消息不承载完整 battle save 或战报;玩家 command 链路不查询或写入 PostgreSQL,也不使用 Redis Stream。
### 建议同时配置
| 变量 | 说明 |
| --- | --- |
| `REDIS_URL` | Redis 连接串;缺失时相关功能会在运行时失败 |
| `API_IP_RATE_LIMIT_WINDOW_SECONDS` | `/api/*` 全局 IP 令牌桶补充周期秒数;默认 `60` |
| `API_IP_RATE_LIMIT_MAX_REQUESTS` | `/api/*` 同 IP 令牌桶容量和每周期补充 token 数;默认 `300` |
| `PUBLIC_WEB_ORIGINS` | 允许访问 API 的前端 origin,逗号分隔,如 `https://app.example.com,http://localhost:5173` |
| `BETTER_AUTH_COOKIE_DOMAIN` | 可选;同站子域部署时可填 `.example.com` 启用跨子域 cookie |
| `ADMIN_EMAILS` | 管理员邮箱白名单,逗号分隔 |
| `ADMIN_USER_IDS` | Better Auth 管理员用户 ID 白名单,逗号分隔;账号管理工具必须配置 |
### 生产 cron 必需
| 变量 | 说明 |
| --- | --- |
| `CRON_SECRET` | 保护 `/internal/cron/*` 接口的 Bearer 密钥;生产环境必须配置,调度器调用时也要携带它 |
### 登录 / 注册相关
当前鉴权中,以下接口会强制要求 ALTCHA PoW payload:
- `/api/auth/sign-in/email`
- `/api/auth/sign-up/email`
- `/api/auth/request-password-reset`
- `/api/auth/email-otp/send-verification-otp`
前端通过 `/api/captcha/challenge` 获取带场景和过期时间的 challenge,完成 PoW 后把 payload 发送给认证接口。服务端会验证签名、场景和有效期,并通过 Redis 原子记录 challenge 的单次消费状态,防止重放。
| 变量 | 说明 |
| --- | --- |
| `VITE_API_BASE_URL` | 前端构建时注入的后端 API 基地址,如 `https://api.example.com` |
| `ALTCHA_HMAC_SECRET` | 服务端签发和验证 ALTCHA challenge 的独立 HMAC 密钥;生产环境必须配置 |
ALTCHA 不需要前端 site key。认证 CAPTCHA 启用时 Redis 也是强依赖;Redis 不可用时,受保护的认证请求会失败关闭,避免 challenge 被重复使用。
### 邮件能力
邮箱验证码、密码注册验证邮件、重置密码邮件、后台邮件广播都会使用SMTP。密码注册必须完成邮箱验证后才能登录;验证链接完成后会自动登录:
| 变量 | 说明 |
| ----------------------------------------- | ------------- |
| `SMTP_HOST` / `SMTP_PORT` / `SMTP_SECURE` | SMTP 连接配置 |
| `SMTP_USER` / `SMTP_PASS` | SMTP 认证信息 |
| `MAIL_FROM` | 发件人 |
### AI 能力
AI 相关功能只调用 DeepSeek 官方 API。服务端默认配置:
- `DEEPSEEK_API_KEY`:服务端 DeepSeek API Key
- `DEEPSEEK_MODEL`:服务端默认模型,未配置时使用 `deepseek-chat`
玩家也可以在游戏设置中保存自己的 DeepSeek API Key 与模型。BYOK 配置只保存在当前浏览器;请求携带的配置不完整或格式无效时,服务端会返回 400,不会静默消耗服务器额度。
## 数据库初始化
首次启动通常要做两件事:
1. 应用业务表迁移
2. Better Auth 表迁移
```bash
bunx drizzle-kit migrate
bun run auth:migrate
```
说明:
- 运行这些命令前,请先确保 `DATABASE_URL`、`BETTER_AUTH_SECRET`、`BETTER_AUTH_URL` 已在当前进程环境中可见
- `drizzle/` 目录下已经存在业务表迁移文件
- `drizzle/` 只管理 `wanjiedaoyou_*` 业务表
- `drizzle-auth/` 只管理固定 `better_auth` schema,并使用独立迁移历史表
- `bun run auth:migrate` 使用 `drizzle.auth.config.ts` 执行认证迁移
- `bun run auth:generate` 用于认证 Drizzle schema 变更后生成迁移,不是每次启动都要执行
- 升级部署时先执行 `bun run auth:migrate` 建立认证基线,再部署使用共享 Bun SQL 连接池的新版本
## 本地开发
1. 准备好 `.env.local`
2. 确保数据库、Redis 和 NATS JetStream 可连接
3. 执行迁移
4. 启动开发服务器
```bash
docker compose -f scripts/docker-compose.nats.yml up -d
bun run dev
```
本地 NATS 容器监听 `4222`,监控端口为 `8222`,开发凭据与 `.env.example` 一致。持久数据保存在 Docker volume `nats-data` 中。
生产硬切顺序、Stream/consumer、DLQ 和故障检查参见 [nats-domain-events.md](nats-domain-events.md)。
访问:
- 前端页面:`http://localhost:5173`
- 健康检查:`http://localhost:5173/api/health-check`
`bun run dev` 会同时启动 Vite 前端和 Bun/Hono 主服务;Vite 将 `/api`、`/internal` 与 WebSocket 升级代理到 Bun 服务。
本地 NATS 使用 JetStream 文件卷保存消息和 durable consumer 的投递位点。启动时发现历史消息是预期行为;回放归档和事务消息会在 PostgreSQL 可用后继续消费。若 PostgreSQL 暂时不可用,事务消息恢复器会以 5 秒至 60 秒退避重试,避免连接超时期间持续打满连接池;不应通过删除 NATS 数据卷来规避数据库故障。
## 构建与运行
| 命令 | 作用 |
| --- | --- |
| `bun run dev` | 启动 Vite 与 Bun/Hono 主服务 |
| `bun run build` | 依次构建前端与服务端 |
| `bun run build:client` | 构建 Cloudflare Pages 使用的前端 SPA |
| `bun run build:server` | 构建 Docker 使用的 Bun/Hono 后端 |
| `bun run preview` | 先构建,再运行 `dist/index.js` |
| `bun run start` | 直接运行已构建产物 |
| `bun run lint` | ESLint 检查 |
| `bun run test` | Vitest |
| `bun run battle:smoke` | 实时战斗 2v2/4v4、超时默认出招、协议、负载与 Worker 故障注入验收 |
| `REDIS_URL=redis://127.0.0.1:6379/15 bun run battle:e2e:redis` | 使用本机隔离 Redis DB 验收重启、多实例、幂等和归档 staging |
| `bun run auth:generate` | 生成 `better_auth` Drizzle 迁移 |
| `bun run auth:migrate` | 执行 `better_auth` 独立迁移流 |
构建产物:
- `build:client` 产出前端 SPA
- `build:server` 产出 Bun 运行的 Hono 服务入口 `dist/index.js`
## Docker
React SPA 继续独立部署到 Cloudflare Pages,不进入后端镜像。`app`(`3000`)使用 Bun,同时承载 Hono API 与实时战斗 WebSocket;`battle-v5` 仍是无框架依赖的纯战斗引擎。PostgreSQL 回放归档由应用侧 NATS consumer 完成。
本地构建镜像:
```bash
docker build -t daoyou-app:local -f docker/Dockerfile.app .
```
运行镜像:
```bash
docker run --rm -p 3000:3000 \
--env-file /path/to/.env.production \
daoyou-app:local
```
注意:
- `VITE_API_BASE_URL` 是前端 Pages 构建期变量,不进入后端 Docker 镜像
- 服务运行时环境变量通过 shell、容器环境或 `--env-file` 注入
## 仓库内现成部署脚本
### Hono API 蓝绿发布
```bash
APP_IMAGE=swkzymlyy/daoyou-app:<version> \
ENV_FILE=/root/daoyou/.env.production \
./scripts/blue-green-app.sh
```
这个脚本会:
- 在 `daoyou-app-blue` / `daoyou-app-green` 间部署闲置颜色
- 同时验证 Docker health 与宿主机 `/api/health-check`
- 通过 `nginx -t` 后原子切换 OpenResty upstream
- 短暂 drain 后停止旧颜色容器
生产 Compose 只定义 `app-blue` 和 `app-green`;实时战斗与 API 随同一 Bun 主服务蓝绿发布; `blue-green-app.sh` 通过 Compose 启动闲置 app profile 并切换 OpenResty。React SPA 仍由 Cloudflare Pages 独立部署。
## 生产 cron 配置方式
当前仓库默认采用两层设计:
- 生产环境中 `src/index.ts` 注册 Bun 内置 cron,Cron 只发布 JetStream command,durable Worker consumer 执行 job runner
- `/internal/cron/*` 仍然保留,便于手动触发、联调,或后续切回外部调度器
- `GET /internal/cron/auction-expire`
- `GET /internal/cron/bet-battle-expire`
- `GET /internal/cron/rank-rewards`
- `GET /internal/cron/market-refresh`
- `GET /internal/cron/tower-enemy-sets`
- `GET /internal/cron/player-state-events-cleanup`
- `GET /internal/cron/expired-data-cleanup`
当前内置调度频率:
- `auction-expire`:每 2 分钟
- `bet-battle-expire`:每 2 分钟
- `rank-rewards`:每天 `00:00 Asia/Shanghai`
- `market-refresh`:每 5 分钟
- `tower-enemy-sets`:每小时
- `player-state-events-cleanup`:每天 `02:30 Asia/Shanghai`
- `expired-data-cleanup`:每天 `02:45 Asia/Shanghai`
说明:
- Bun 内置 cron 仍运行在 Web 进程内,但实际任务已与调度回调解耦;发布成功的 command 由 JetStream 持久化并可跨应用重启继续执行
- Bun 的 cron 表达式按 `UTC` 解释,所以 `rank-rewards` 在代码里配置为 `0 16 * * *`,对应北京时间次日 `00:00`
- 内置调度不直接调用 job runner,也不走 HTTP;它发布 `daoyou.command.cron.>` command
- `/internal/cron/*` 接口继续要求 `Authorization: Bearer ${CRON_SECRET}`,适合人工补跑或外部调度
- 这些任务内部带 Redis 分布式锁与幂等保护,重复触发会返回 `skipped`
如果你想改回外部 HTTP 调度,可使用:
```cron
*/2 * * * * curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" https://your-domain/internal/cron/auction-expire
*/2 * * * * curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" https://your-domain/internal/cron/bet-battle-expire
0 0 * * * curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" https://your-domain/internal/cron/rank-rewards
*/5 * * * * curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" https://your-domain/internal/cron/market-refresh
0 * * * * curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" https://your-domain/internal/cron/tower-enemy-sets
30 2 * * * curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" https://your-domain/internal/cron/player-state-events-cleanup
45 2 * * * curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" https://your-domain/internal/cron/expired-data-cleanup
```
## CI / 镜像发布
当前仓库的 [`.github/workflows/deploy.yml`](../.github/workflows/deploy.yml) 会在 `master` 分支推送时:
- 构建 Docker 镜像
- 推送到 Docker Hub
## 架构原则
- 引擎层(`src/shared/engine`)完全独立于 UI 和框架
- 业务逻辑放在 Service 层
- 数据访问使用 Repository 模式
-58
View File
@@ -1,58 +0,0 @@
# 红尘剑宗(`lingxiao`)V4.2 平衡实测
## 测试口径
- 使用 battle-v5 实际投影、实际技能蓝耗、心法常驻属性与 AI 选招。
- 筑基初期、金丹圆满、化神圆满的心法等级分别为 25、60、100。
- 每个组合固定种子运行 100 局,并交替先手;无装备和灵根优势。
- 另以 1 级、180 级各 5 局执行边界冒烟。
## V4.2 机制基线
- 神通固定蓝耗,不再随境界变化。
- 神通伤害、护盾、治疗、状态强度和显式声明的持续时间由所属心法统一投影。
- 《一剑破妄》的驱散数量随《观微剑意》成长档由 1 个提高至 7 个,最终配置只保留普通整数。
- 剑痕与裂甲均为 3 层、3–6 回合状态;剑痕逐层增伤,裂甲逐层降低物防。
- 快剑每连续两次自身行动未造成直接伤害才衰减 1 点剑意;重剑以裂甲、护盾破裂和后发重击组织循环。
- 回燕代表构筑使用留痕,装配《剑荡山河》《藏锋听雷》《踏雪无痕》《此剑平生》;极势在裂甲不足 2 层时先补裂甲。
- 普通敌对神通的效果链统一要求命中;基础与重剑《藏锋听雷》改为自身蓄势,施展时登记必中、不可打断的后发《听雷》。
- 宗门节点卡片通过运行时 resolver 展示当前心法等级的实际数值,静态定义只保留机制摘要。
## 编译边界收尾
- 心法成长声明只存在于 sect authoring 阶段,最终投影会剥离全部宗门元数据,battle-v5 的 `BuffConfig` 只保留已结算数值。
- 编译顺序固定为“基础神通 → 流派变体 → 经脉节点 → 一次成长投影 → 展示分析”,不再用 `methodGrowthApplied` 保存编译阶段状态。
- 成长策略对 Effect 联合类型执行穷尽分类;新增 Effect 时必须显式决定其倍率、状态或固定数值语义。
- 分层属性修改器使用稳定标识,仅在层数变化时重挂载依赖层数的 modifier,避免随机标识改变固定种子结果。
## 三境界代表对局
格式为“快剑胜率 / 平均回合 / 平局率”。
| 代表对局 | 筑基初期(25级) | 金丹圆满(60级) | 化神圆满(100级) |
| --- | --: | --: | --: |
| 追风快剑 vs 后发重剑 | 52% / 8.25 / 0% | 79% / 5.58 / 0% | 20% / 4.36 / 0% |
| 连势快剑 vs 极势重剑 | 17% / 7.56 / 0% | 29% / 5.09 / 0% | 88% / 4.72 / 0% |
| 回燕快剑 vs 守山重剑 | 0% / 14.69 / 0% | 1% / 11.89 / 0% | 0% / 7.25 / 0% |
3×3 交叉矩阵实测范围:
| 境界 | 快剑胜率范围 | 平均回合范围 | 平局率 |
| -------- | -----------: | -----------: | -----: |
| 筑基初期 | 0%–97% | 7.07–14.69 | 0% |
| 金丹圆满 | 1%–96% | 4.64–11.89 | 0% |
| 化神圆满 | 0%–88% | 4.06–7.25 | 0% |
## 门禁结论
成长、驱散数量、蓝耗、命中语义、状态覆盖、被动嵌套成长、动态节点文案、authoring 元数据剥离、36 节点运行时矩阵和 729 套组合编译均已进入自动化测试。严格 3×3 门禁也已改为默认执行。
当前固定种子平衡门禁仍未通过:只有筑基“追风 vs 后发”满足代表胜率与回合要求;其余代表对局及多数交叉对局越界,筑基和化神“回燕 vs 守山”仍为 0% 胜率。按本轮“保留九个流派基础神通倍率、只围绕指定身份节点以 5% 粒度校准”的边界,现有早期流派装配已经形成 0%–97% 的克制跨度,不能标记为平衡验收完成。继续收敛需要放开至少一项边界:代表装配/属性分配、流派基础神通数值,或接受更宽的交叉胜率区间。
严格门禁命令:
```bash
bunx vitest run src/shared/engine/sect/content/lingxiao/tests/balanceSimulation.test.ts
```
该命令当前会按上述实测差距失败,避免未达标数据被误标为平衡验收完成。
@@ -1,31 +0,0 @@
# Player State Realtime
Date: 2026-07-06
## Current Transport
- Main character state now initializes through `GET /api/player/state`.
- Live player-state updates use the `player-state` channel on `GET /api/realtime` WebSocket.
- `GET /api/player/state/events?after=` is still active as JSON backfill for WebSocket reconnect/version gaps. It is not an SSE stream.
- Retreat/yield `text/event-stream` code is action story streaming, not global player-state synchronization.
## Current Code
- `src/server/routes/player.router.ts`: `GET /api/player/state` is the authoritative snapshot endpoint.
- `src/server/routes/player.router.ts`: `GET /api/player/state/events?after=` is required for WebSocket gap recovery.
- `src/server/lib/services/playerStateBroadcaster.ts` publishes committed state events to live WebSocket subscribers.
- `src/react-app/lib/player-state/store.ts` applies WebSocket events and uses `/state/events` only as reconnect backfill.
- `src/server/routes/api/realtime.router.ts` owns the WebSocket connection, heartbeat, channel subscription, and per-user/cultivator/IP connection caps.
- `src/shared/contracts/realtime.ts` defines the active realtime channels: `player-state` and `world-chat`.
## Removed Legacy Paths
- `GET /api/player/active` has been removed. Use `GET /api/player/state`.
- Legacy `/api/player/active` contract types have been removed from `src/shared/contracts/player.ts`.
- The obsolete SSE redesign document has been removed. Do not use old SSE examples as current architecture.
## Migration Notes
- Keep `/api/player/state/events?after=` unless WebSocket resume tokens or replay-on-connect replace the current HTTP backfill path.
- Do not reintroduce `EventSource` or SSE for global player-state synchronization.
- New player-state mutations should go through `commitPlayerStateMutation()` so API responses and WebSocket subscribers receive the same versioned events.
@@ -1,110 +0,0 @@
# PostgreSQL 死锁与锁优化方案
## 目标
解决生产环境偶发的 PostgreSQL 死锁、连接池耗尽和全站请求卡住问题。修复范围只覆盖锁、事务边界和必要的可观测性,不借机扩展业务模型。
## 已确认的主要风险
### 事务内重新申请数据库连接
服务进入事务后,如果下层 Repository 或 Service 忽略传入的 `DbTransaction`,再次通过全局 executor 发起查询,就会额外占用连接。并发请求较多时,外层事务占住连接等待内层查询,而内层查询又在等待连接池,最终形成连接池自锁。
处理原则:
- 写链路显式传递 `DbTransaction` / `DbExecutor`。
- 事务内禁止重新获取全局 executor。
- 查询、校验和写入尽量复用同一个事务 executor。
### 在数据库事务中等待外部系统
LLM、Redis、世界广播等外部调用耗时不可控。如果在事务内执行,会延长行锁和连接占用时间。
处理原则:
- LLM 推演、Redis 读取等放在事务开始前。
- 数据库事务只执行必要的条件校验和写入。
- Redis 缓存、广播等副作用在提交后执行。
- 提交后的副作用失败不回滚已经完成的数据库事务。
### 多资源写入顺序不稳定
拍卖、赌战、好友等涉及多个玩家或多条记录时,如果并发事务按不同顺序获取锁,容易形成 PostgreSQL 死锁。
处理原则:
- 多个 ID 在进入数据库写入前排序。
- Repository 按稳定顺序读取和更新。
- 资源扣减使用带余额或版本条件的原子更新。
- 对 PostgreSQL `40P01`、`40001`、`55P03` 做有限次数、短退避重试。
## Redis 与 PostgreSQL 的职责边界
PostgreSQL 保存不可丢失、需要审计或必须参与原子交易的最终状态,例如角色、库存、货币、拍卖成交和战斗结算。
Redis 保存可重建、带 TTL 或只用于并发协调的状态,例如:
- 排行榜及每日挑战次数
- 造物待确认结果
- 命格重塑临时会话
- 坊市本轮个人购买标记
- 分布式锁
- 周期缓存与冷却状态
本次明确不新增以下 PostgreSQL 持久化:
- 排行榜状态表和挑战次数表
- 排行奖励快照、明细和结算表
- 临时玩家操作会话表
- 坊市购买账本
Redis 临时数据过期或丢失时,接受对应临时流程重置;不为此引入双写、回填或补偿框架。
## 保留的修复
- PostgreSQL 连接池连接、空闲、语句、锁和空闲事务超时。
- 连接池等待数与慢事务、失败事务日志。
- 统一 Redis 锁实现、稳定多 key 排序、租约续期和租约失效检查。
- 玩家状态写入的事务 executor 传播。
- Qi、拍卖、赌战、炼丹、造物、地下城等写链路的条件更新和稳定锁顺序。
- LLM、Redis 与广播移出数据库事务。
- 活跃角色唯一约束,防止同一用户出现多个活跃角色。
## 不采用的扩展
- 不为了幂等或临时状态新增业务账本。
- 不把 Redis 数据复制到 PostgreSQL 作为“权威备份”。
- 不给只读接口统一套玩家级分布式锁。
- 不增加读取时自动恢复、跨存储双写修复和多阶段补偿流程。
- 不为无法稳定复现的外部并发时序堆叠架构测试。
## 生产验证
发布后重点观察:
- PostgreSQL `deadlock detected`、`lock timeout`、`statement timeout`
- pool waiting 是否持续大于 0
- 慢事务持续时间、重试次数和 PostgreSQL 错误码
- Redis 锁竞争、续期失败和租约丢失
- 请求 409、429、503 的变化
若再次发生卡死,应优先收集:
```sql
select
pid,
usename,
application_name,
state,
wait_event_type,
wait_event,
xact_start,
query_start,
pg_blocking_pids(pid) as blocking_pids,
query
from pg_stat_activity
where datname = current_database()
order by xact_start nulls last, query_start;
```
同时保存 PostgreSQL deadlock 日志中的完整 wait graph,用具体 SQL 和调用链定位剩余的锁顺序问题。
File diff suppressed because it is too large. Load diff
-605
View File
@@ -1,605 +0,0 @@
# 幽都 × battle-v5 机制实现分析
> 配套设计:`docs/youdu-sect-design.md`
>
> 本文是 2026-07-22 旧 `BattleEngineV5` 1v1 基线上的历史实现分析,用于追溯幽都通用原语与内容层取舍,不再定义当前战斗主流程。旧引擎及其 1v1 限制已经删除;当前生产基线为 `Team/Roster + BattleRuntime + Checkpoint + BattleRoundResolver`。涉及多目标、行动、胜负、回放与运行时接口时必须以当前代码和 `docs/sect-authoring-guide.md` 为准。
>
> 分析基线:2026-07-22 的 battle-v5 能力审计。`controlHitBonus`、`statusVisibility` 与 `stackPriority` 等既有通用能力按可复用接口计入;实施时仍须以当前代码和测试复核,不得复制接口或覆盖其他宗门的实现。
>
> `docs/youdu-sect-design.md` 已按本文结论收敛。下文“原设计”仅指收敛前提案;实施时以主设计稿中的玩家规则和本文的引擎依据共同验收。
## 1. 结论
幽都可以建立在 battle-v5 现有的 AbilityConfig、Buff、Listener、CombatResource 和 Effect 链上,不需要推翻战斗架构,但不能把设计稿逐字直接翻译成现有配置。
推荐边界如下:
- 保留:蚀魂五层、非线性层级削弱、失魂、归窍、受治疗削弱、魂火、首次解控、术伤与魂伤混合技能、四层终结。
- 做通用小扩展:分层状态变更事件、一次添加多层、按层取值、逐层驱散、受治疗削弱、不可暴击/不可吸血伤害、主动技能必然命中;终结技的四层/五层差异复用既有 effect plan,不扩展伤害模型。
- 改为近似效果:魂伤使用现有真实伤害管道,只绕过物法防御,不绕过统一百分比减伤;混合技能产生两次伤害请求;忘川 DOT 使用实时法术攻击而非施加时快照。
- 当时的 V1 删除项:三目标忘川、向其他敌人扩散蚀魂、百鬼同哭影响全体。当时生产主流程严格 1v1,因此没有为单个宗门提前扩建队伍战斗;这条历史降级理由不再代表当前 Team/Roster 引擎能力。
- 暂不实现:设计稿已经标记为暂缓的动画与声音表现。
这套方案新增的是通用原语,不让 battle-v5 认识“幽都”“蚀魂”“忘川”或“魂火”。
### 1.1 引擎变更准入
后续实施先按变更等级判断,不以“能做出来”作为接入理由:
| 等级 | 定义 | 幽都 V1 处理 |
| --- | --- | --- |
| 内容配置 | 复用 Ability、Buff、Listener、Effect、CombatResource 和标签组合,不改变通用语义 | 优先采用,幽都专属 ID 只允许出现在宗门内容目录 |
| 通用原语扩展 | 为现有模型补充可复用参数、事件或属性;默认值保持旧内容行为不变 | 允许,但必须使用中性命名、无宗门特判,并先有不引用幽都 ID 的原子测试 |
| 架构级改造 | 改变单位模型、行动流、目标选择、胜负、持久化或伤害主管道 | V1 禁止;用近似效果替代,等待对应系统统一升级 |
每项机制的实施顺序固定为:检查现有原语 → 能组合则只写内容配置 → 确有通用缺口才扩展 → 无法保持兼容则采用本文降级方案。禁止在 `battle-v5` 中判断 `sectId === 'youdu'`、幽都能力 ID 或幽都状态 ID。
## 2. 实施前 battle-v5 能力基线
本节记录的是判断“直接复用、通用扩展或降级”的审计基线。若表中的缺口已经在当前分支由通用实现补齐,应把它视为复用与回归验证项,不得再次新增平行字段或专属接口。
| 领域 | 当前事实 | 对幽都的影响 |
| --- | --- | --- |
| 战斗人数(历史基线) | 当时的 `BattleEngineV5` 只持有 `player` 与 `opponent`,目标选择系统未接入主行动流 | 此限制已随旧引擎删除;当前多单位能力必须按 Team/Roster 代码重新验收 |
| 技能命中 | 一次 `SkillCastEvent` 决定整条主动效果链是否执行 | 术伤、魂伤、加层和控制天然共用一次命中 |
| 多段伤害 | 每个 `damage` effect 分别发布 `DamageRequestEvent` | 混合伤害可以实现,但术伤与魂伤是两个伤害包,不是一个混合包 |
| 真实伤害 | `DamageType.TRUE` 不读取物防/法防,仍进入统一增减伤、境界、暴击、随机浮动和护盾管道 | 不能直接满足“无视一切减伤、不可暴击” |
| Buff 层数 | `STACK_LAYER`、`maxLayers`、层数修改与刷新持续时间已存在 | 能承载蚀魂,但一次 Apply 默认只加 1 层,且没有层变更事件 |
| 分层属性 | `scaleByLayer` 只支持线性倍率 | 无法直接表达 3%/5%/8%/12% 的非线性曲线 |
| 驱散 | 普通驱散会移除整个 Buff | 无法直接表达“一次净化一层蚀魂” |
| 持续时间 | Buff 在宿主每次行动后减 1;当次行动新施加的 Buff 由运行时标记阻止立即 tick | 与“按目标自身行动计 3 回合”一致,1 回合保护状态无需配置成 2 |
| DOT | Buff 可监听 `ActionPreEvent` 造成伤害 | 忘川两次结算可直接实现,但默认读取实时施术属性 |
| 治疗 | `HEAL_AMPLIFY` 只读取治疗者;所有气血恢复最终都会调用 `Unit.heal()` | 没有受治疗削弱,但适合在 `Unit.heal()` 统一补齐 |
| 控制 | `BuffType.CONTROL` 会读取控制命中/抗性;`NO_ACTION`、`NO_SKILL` 已接入行动流 | 失魂与镇魂钉可复用现有控制体系 |
| 控制加值 | 当前工作区已有 `ApplyBuffParams.controlHitBonus` | “幽都铁律”可直接使用,无需临时修改施术者属性 |
| 监听预算 | 支持每行动、来源行动、回合、战斗和 Buff 生命周期预算 | 魂火每行动一次、首次解控和节点每场一次都可实现 |
| 战斗资源 | 支持定义、增减、全部消费、溢出和事件 | 魂火无需新资源系统 |
| 条件与计划 | 支持 Buff 层数、资源、血量等条件;effect plan 在准备施法时快照 | 镇魂按施法前层数决定 1/2 回合可直接实现 |
| 技能命中策略 | 引擎事件支持 `guaranteed`,但普通 AbilityConfig 不能声明 | 《照影》必然命中需要一个很小的配置扩展 |
## 3. 已纳入主设计稿的 V1 契约
### 3.1 魂伤改用“标准真实伤害”
主设计稿已经收敛为:
> 魂伤属于真实伤害,忽略物理防御与法术防御;仍受统一百分比减伤、境界威压、伤害免疫、护盾与免死影响。魂伤不可暴击,也不能被吸血。
原因:
- 现有真实伤害已经稳定经过统一伤害、护盾、日志和免死管道。
- 新增“绕过所有减伤”会要求每个 DamageRequest 继续区分哪些规则是普通减伤、哪些是规则级保护,很容易出现部分监听先减伤、部分监听后减伤的顺序漏洞。
- 幽都已经同时拥有防御绕过、属性削弱、禁疗和控制,不需要再取消通用减伤这一层反制。
不要新增 `DamageType.SOUL`。魂伤用 `DamageType.TRUE`,并以幽都能力机制标签加 `damageType === TRUE` 共同识别。这样 battle-v5 不需要理解宗门专属伤害枚举。
### 3.2 混合技能允许两个伤害包
《夺魄》《镇魂》的术伤与魂伤建议按两个连续 `damage` effect 实现:
1. 术伤发布一次 `DamageRequestEvent(MAGICAL)`。
2. 魂伤发布一次 `DamageRequestEvent(TRUE)`。
3. 两者共享同一次命中;任一技能闪避时整条效果链不执行。
4. 两个伤害参数不携带宗门标签或色调;日志聚合器按同一 ability span 使用标准中性伤害行,内容详情仍可把真实伤害通道称为“魂伤”。
V1 不实现“一个 DamageRequest 内同时具有法术与真实两种结算方式”。那会要求 `DamageComponent` 自己携带 damageType,并重写防御、减伤、暴击、免疫和日志归类,属于不必要的伤害管道重构。
主设计稿已经明确混合技能的监听边界:
- 每个伤害包可以分别触发现有通用受击监听。
- 幽都自己的魂火、节点和每行动效果必须用 listener budget 保证只触发一次。
- 多个监听器共享次数时使用通用 budget `group`;监听器 ID 仍保持唯一,作为调试与追踪标识。
### 3.3 忘川改为单体、实时取值
V1《忘川潮》:
- 目标由“最多 3 名敌人”改为敌方单体。
- 持续 2 回合,每次目标行动前结算。
- 每次结算读取施术者当时的法术攻击,不做施加时数值快照。
真实三目标战斗需要同时改造:
- 当时 `BattleEngineV5` 的 player/opponent 模型(已删除)
- 行动排序和默认目标
- AbilityContainer 的候选目标
- TargetSelectionSystem 接入
- 胜负判定
- 战斗状态快照和日志聚合
- 宗门自动战术的多目标上下文
这不是内容层扩展,本轮明确不做。
### 3.4 单目标替代条款
| 原设计 | V1 替代 |
| --- | --- |
| 《忘川潮》最多三目标 | 单体 |
| `tide-spread` 分摊多个目标 | 改为 `tide-cycle`,优先维持忘川与 3 层蚀魂 |
| `long-night` 同时维持两个目标 | 改为延后终结,维持单目标 4 层压力 |
| 「渡口回声」向随机另一敌人加层 | 每回合第一次对至少 4 层目标结算忘川时,追加 `0.12 × 法攻`魂伤 |
| 「百鬼同哭」给其余敌人加层 | 每场第一次触发失魂时,对该目标追加 `0.30 × 法攻`魂伤 |
| 「末渡无舟」失去 15%当前法力 | 改为失去 10%最大法力,复用现有 `targetMaxMpRatio` |
| 镇魄司命对照影目标绝对命中 | 改为自身对照影目标的魂伤每层额外提高 1%,最多 5% |
| 「名落幽都」击杀返还 | 改为终结后目标气血低于 20%时获得 3 魂火并返还 2 回合冷却,每场一次 |
这些替代保留流派意图,同时避免队伍战斗、动态命中改写、当前法力比例和精确击杀归因四项额外扩展。
## 4. 需要补齐或复用的通用原语
以下字段名和事件语义是幽都的兼容契约。若当前 battle-v5 已有同名能力,直接复用;若尚无,才按本节做最小通用扩展。所有新增字段必须给出保持旧行为的默认值。
### 4.1 分层状态原语
这是幽都最重要、也最值得做成通用能力的一组扩展。
#### A. 一次施加多层
为 `ApplyBuffParams` 增加:
```ts
layers?: number
```
- 新状态以该层数入场。
- 已有 `STACK_LAYER` 状态增加该层数,而不是固定增加 1。
- 仍受 `maxLayers` 限制。
- 省去在一个技能里重复写两次 `apply_buff`,也避免重复应用日志与阈值监听。
#### B. 层数变化事件
新增通用事件:
```ts
interface BuffLayerChangedEvent extends CombatEvent {
type: 'BuffLayerChangedEvent';
target: Unit;
buff: Buff;
source?: Unit;
ability?: Ability;
previousLayer: number;
currentLayer: number;
delta: number;
reason: 'apply' | 'stack' | 'effect' | 'dispel';
}
```
要求:
- 只在层数实际变化时发布;5→5 的刷新不发布。
- 初次施加按 0→初始层数发布。
- `BuffContainer` 成为层变更的统一出口;`BuffLayerModifyEffect` 和逐层驱散都要透传来源与 ability。
- Buff 监听器必须在初次 0→N 事件发布前完成挂载。
用途:
- 4→5 时触发失魂。
- 实际加层时获取魂火,满层刷新不获取。
- 归窍将 5 层钳制回 4 层。
- 洗魂有价识别驱散掉层。
- 战斗日志精确显示旧层数与新层数。
这是事件模型的自然补全,不是幽都专属钩子。
#### C. 非线性按层属性
为 `AttributeModifierConfig` 增加一种互斥配置:
```ts
valueByLayer?: readonly number[]
```
约定数组下标 0 对应 1 层,超过数组长度沿用末项。不能与 `scaleByLayer` 同时使用。
蚀魂可直接配置:
```ts
ATK/MAGIC_ATK/DEF/MAGIC_DEF/SPEED:
[-0.03, -0.05, -0.08, -0.12, -0.12]
```
`DataDrivenBuff.onLayerChanged()` 已经会重新挂载线性层数 modifier,只需把取值策略从“value × layer”扩展为“按层取值”。
注意:降低 `SPEED` 会按现有派生公式间接影响物攻、物防、暴击和闪避,因此玩家实际面板中的攻击、防御变化会略大于表格名义值。这是当前五维属性架构的自然结果,不应为幽都新增独立“行动速度”属性。
#### D. 逐层驱散
为 `BuffConfig` 增加:
```ts
dispelMode?: 'whole' | 'one_layer'
```
缺省 `whole` 保持兼容。`one_layer` 且当前层数大于 1 时:
- 普通驱散只减 1 层。
- 发布 `BuffLayerChangedEvent(reason: 'dispel')` 与成功的 `DispelEvent`。
- 降到 0 时才真正发布 `BuffRemovedEvent`。
- 明确写“净化全部层数”的能力继续通过 `buff_layer_modify clear` 完整移除。
不要把蚀魂拆成五个独立 Buff。那会把加层、持续时间、状态栏、阈值和终结技读取都变成五状态组合问题。
### 4.2 受治疗削弱
新增外部注入型二级属性:
```ts
AttributeType.HEAL_RECEIVED_REDUCTION;
```
- 底座为 0。
- 所有状态以 `ModifierType.FIXED` 写入削弱值,modifier 相加后在 `[0, 1]` 截断;不能使用会乘在零底座上的 `ADD`。
- 只影响气血恢复,不影响法力与护盾。
在 `Unit.heal(amount)` 统一应用:
```text
requested = 治疗效果根据施术者 HEAL_AMPLIFY 计算后的值
reduction = clamp(target.HEAL_RECEIVED_REDUCTION, 0, 1)
received = round(requested × (1 - reduction))
applied = min(缺失气血, received)
```
这样可以自然覆盖当前所有气血恢复来源:
- `HealEffect`
- `LifestealEffect`
- `DamageMemoryEffect` 的治疗释放
- `ResourceDrainEffect` 的气血恢复
- 后续新增但正确调用 `Unit.heal()` 的恢复
免死继续调用 `setHp(..., 'death_prevent')`,不受禁疗影响。现有 `HealEvent.healAmount/appliedAmount` 已能分别表达请求值与实得值,无需新增一条 HealingSystem 管道。
各状态配置:
| 状态 | 受治疗削弱 |
| ------ | -------------------------------------- |
| 蚀魂 | `[0, 0.15, 0.30, 0.50, 1.00]` 按层取值 |
| 忘川 | `+0.20` |
| 不归 | `+0.80` |
| 镇魂钉 | `+1.00` |
统一在 100% 封顶,符合设计稿的叠加规则。
### 4.3 伤害行为开关
为 `DamageParams` 和对应伤害事件增加并透传:
```ts
canCrit?: boolean // 缺省 true
canLifesteal?: boolean // 缺省 true
```
- 魂伤配置 `false/false`。
- `DamageSystem` 在 `canCrit === false` 时跳过暴击。
- `DamageTakenEvent` 继续携带 `canLifesteal`,`LifestealEffect` 在 false 时退出。
- 不修改现有其他真实伤害的默认行为。
不要全局规定所有 `DamageType.TRUE` 都不能暴击或吸血;那会无意改变现有造物、命格和其他宗门内容。
### 4.4 用固定 effect plan 表达四层/五层终结
不为 `DamageParams` 增加“读取任意 Buff 层数”的动态伤害分量。《魂兮不归》只允许在 4 层或 5 层施放,因此由幽都编译器预先计算两个固定系数:
- `finish-four`:`base + perLayer × 4`;
- `finish-five`:`base + perLayer × 5`,优先级高于四层计划。
两个计划分别使用 `buff_layer_at_least` / `buff_layer_below` 选择,且都只包含一个标准 `damage` effect。《魂兮不归》按以下顺序执行:
1. 施法准备阶段选择四层或五层计划。
2. 计划发布一个真实伤害请求。
3. completion effect 清除蚀魂;“不归亦不散”则改为设置到 2 层。
4. 施加不归。
这样既不会产生四到五个独立伤害包,也不要求 DamageEffect 反向读取宗门 Buff。基础系数或逐层系数被节点修改时,编译器重新生成两个固定计划。
伤害应在清层前结算,使《照影》仍能按终结前的蚀魂层数放大本次伤害。
### 4.5 主动技能命中策略
为 `AbilityConfig`、`ActiveSkillConfig` 和 `SectAbilityFactory` 增加:
```ts
hitPolicy?: 'normal' | 'guaranteed'
```
由 `AbilityContainer._prepareCast()` 写入 `SkillPreCastEvent`。现有 DamageSystem 已支持 `guaranteed`,后续无需改动命中公式。
V1 仅《照影》本身使用必然命中。照影将目标 `EVASION_RATE` 以 OVERRIDE 置 0,但其他能力仍遵守全局最低 3%闪避;因此玩家文案必须写成“闪避属性按 0 计算,仍保留最低闪避”,不能写成“后续攻击必定命中”。镇魄司命不再额外改写后续技能命中,采用第 3.4 节的近似增伤。
## 5. 各核心机制实现
### 5.1 蚀魂
Buff 配置要点:
- `type: DEBUFF`
- `duration: 3`
- `stackRule: STACK_LAYER`
- `maxLayers: 5`
- `dispelMode: one_layer`
- 层级 modifier 使用 `valueByLayer`
- 治疗削弱使用 `HEAL_RECEIVED_REDUCTION.valueByLayer`
- 运行时标签由 `GameplayTags.BUFF.SECT.namespace('youdu', 'soul-erosion')` 和对应状态构造器产生
技能添加 2 层时使用 `apply_buff.layers = 2`,不要重复两个 ApplyBuffEffect。
### 5.2 失魂与归窍
推荐事件流程:
```text
BuffLayerChangedEvent(蚀魂 4→5)
├─ 若目标有归窍:高优先级把蚀魂设为4,结束
└─ 否则施加失魂控制
├─ 成功:目标下一行动因 NO_ACTION 跳过
│ ControlledSkipEvent 时蚀魂设为3,并施加归窍保护窗口
├─ 控制抵抗:onResistEffects 将蚀魂设为3,并施加归窍
├─ 控制免疫:监听 BuffImmuneEvent,将蚀魂设为3,并施加归窍
└─ 被心死神活解除:监听失魂 BuffRemovedEvent,将蚀魂设为3,并施加归窍
```
失魂 Buff:
- `type: CONTROL`
- `statusTags: [NO_ACTION]`
- `duration: 1`
- `HEAL_RECEIVED_REDUCTION +1.0`
- `dispelPolicy: protected`
失魂不再接受普通净化直接移除。它的主要反制是:在 5 层前逐层净化蚀魂、控制抵抗、控制免疫,以及《心死神活》这类明确的控制解除。这样避免为一个控制状态新增通用 `onRemoveEffects` 生命周期协议。
归窍 Buff:
- 内部保护状态,玩家可见,语义上持续 1 个完整反制窗口。
- 运行时 duration 配置为 1。`BuffContainer` 会调用 `markBuffAppliedAtCurrentAction`,`shouldTickBuffDuration` 会阻止新状态在施加它的同一次行动中立刻扣减,因此无需通过 duration 加一补偿。控制抵抗、免疫和首次解控分支统一使用同一配置。
- 监听蚀魂的 `BuffLayerChangedEvent`,优先级高于失魂触发器。
- 若当前层数超过 4,立即设回 4。
- 不直接免疫蚀魂,因此 3→5 的技能仍能实际推进到 4,而不是整次加层被拒绝。
### 5.3 魂火
定义:
```ts
{ id: 'sect.youdu.soul-fire', initial: 0, max: 3 }
```
获取:
- 蚀魂监听 `BuffLayerChangedEvent`。
- `delta > 0` 时将上下文 caster 映射为该 Buff 来源。
- listener budget 使用 `source_action: 1`;分支监听器通过同一 `group` 共享次数,无论一次加 1 层还是 2 层都只获得 1 点。
- 5→5 刷新没有 layer event,因此不获取。
消费与增伤:
- 指定消费技能带幽都 `soul-fire-consumer` 机制标签。
- 常驻被动监听自身 `DamageRequestEvent`。
- 条件:魂火至少 3、伤害类型 TRUE、能力带 consumer 标签。
- 写入 `percent_damage_modifier increase 0.25`,节点可改为 0.35。
- 加层消费技在直接魂伤请求之后、施加蚀魂之前执行 `consume_all`;终结技在伤害后执行。这样施法前 2 点魂火不会被本技能刚产生的第 3 点无增伤地误消费,而施法前满魂火的技能可在消费后由本次加层重新开始积累。
- 忘川不带 consumer 标签,因此不会误消费。
### 5.4 心死神活
控制抗性 `+0.30 FIXED` 已可直接作为被动 modifier。
首次解控无需新增“解除最近控制”效果,可以由数据驱动监听组成:
1. 被动监听 `BuffAppliedEvent`,scope 为 owner_as_target。
2. 分别匹配三类功能控制标签:`NO_ACTION`、`NO_SKILL`、`NO_BASIC`。
3. 条件要求自身没有隐藏的永久 marker `heart-dead-used`。
4. 用 `buff_layer_modify clear` 清除刚落地的对应控制;该操作可明确越过普通 dispelPolicy。
5. 添加 marker,确保三个分类监听共享“每场一次”。
6. 添加持续 1 回合的分类免疫 Buff;其 `BuffAddEvent` 监听使用现有 `buff_immunity` 拦截相同功能标签。
分类免疫运行时配置 `duration: 1`;复用 `markBuffAppliedAtCurrentAction` 的当次行动免 tick 规则,对玩家也显示 1 回合,不额外延长。
“同类控制”在 V1 按行动限制语义定义:
- 眩晕、失魂等不能行动效果归为 `NO_ACTION`。
- 封印、沉默等不能施法效果归为 `NO_SKILL`。
- 不能普攻效果归为 `NO_BASIC`。
不增加睡眠、定身等只有名称没有行动语义的新控制枚举。若未来确实出现位移或站位系统,再扩充标准控制轴。
### 5.5 忘川
忘川是标准 DataDrivenBuff:
- 单体、`duration: 2`、`REFRESH_DURATION`
- 监听宿主 `ActionPreEvent`
- 造成 `0.14 × source.MAGIC_ATK` 的 TRUE 伤害
- `canCrit: false`、`canLifesteal: false`
- 增加 20%受治疗削弱
- 不加蚀魂,不触发主动加层魂火
招魂渡夜的“长夜回潮”不再刷新蚀魂持续时间,改为:
> 忘川对至少 3 层蚀魂目标造成的持续魂伤提高 20%;每回合第一次造成有效气血伤害时获得 1 点魂火。
这样完全复用伤害条件、百分比修正和回合预算,不新增“直接刷新任意现有 Buff 时长”的效果。
### 5.6 镇魂钉
- 控制 Buff 使用 `NO_SKILL`,AbilityContainer 会自动回退到默认攻击。
- 受治疗削弱为 100%,与控制 Buff 同生共灭。
- 控制被抵抗时不会添加 Buff,所以没有禁疗。
- 1/2 回合版本放入两个 effect layer;effect plan 在施法准备阶段按目标施法前蚀魂层数选择其中一个。
- 节点“幽都铁律”直接使用当前工作区已有的 `controlHitBonus: 0.15`。
若同一次《镇魂》先令目标进入 5 层:
- 失魂提供一次 NO_ACTION。
- 镇魂钉提供 NO_SKILL。
- 目标被跳过的行动结束后,两种状态都各自按宿主行动计时;2 回合镇魂钉最多在下一次实际行动继续迫使目标普攻。
- 不把两个控制合并为一个“三回合硬控”。
### 5.7 魂兮不归
- `castConditions`: 目标蚀魂至少 4 层。
- 编译器生成互斥的 `finish-four`、`finish-five` 两个固定 effect plan,每个计划只有一次 damage。
- 基础系数与每层系数由 Build Facade 提供,节点只改参数;编译期分别计算四层与五层总系数。
- 伤害后基础态 clear;“不归亦不散” set 到 2。
- 最后添加不归的速度与受治疗 modifier。
“名落幽都”采用目标结算后低于 20%气血的替代条件,不监听击杀。当前 `UnitDeadEvent` 没有完整 ability 归因和 listener mapping;为了一个终极节点扩充死亡归因收益太低。
### 5.8 照影
- 能力本身 `hitPolicy: guaranteed`。
- 照影 Buff 对 `EVASION_RATE` 使用 `OVERRIDE 0`。
- 蚀魂 Buff 监听宿主受到的 `DamageRequestEvent`;目标同时有照影时,用现有 `percent_damage_modifier` 和 `scaleByBuffLayer` 增加 `0.02 × 蚀魂层数`伤害。
- 镇魄司命额外为自身魂伤追加 `0.01 × 蚀魂层数`,不改变队友收益。
- 蚀魂清除后,其监听随 Buff 移除,易伤自然立即消失。
### 5.9 夺魄与层级属性
夺魄的独立降攻直接使用 ATK/MAGIC_ATK 的 ADD modifier。它会与蚀魂的同类 ADD modifier在现有属性阶段相加。
不要修改五项持久化基础属性,也不要给蚀魂创建旧式 `persistent_statuses`。
## 6. 36 个节点的实现评估
### 6.1 招魂渡夜
| 节点 | 结论 | 实现方式 |
| --- | --- | --- |
| 一水忘川 | 直接支持 | 编译时修改忘川两个伤害系数 |
| 唤名成痕 | 直接支持 | 《一叹》条件伤害或 DamageRequest listener |
| 魂灯初照 | 直接支持 | BattleInit/首次 layer event + battle marker |
| 潮声不歇 | 直接支持 | 忘川 duration 2→3 |
| 彼岸无医 | 依赖受治疗扩展 | modifier 0.20→0.30 |
| 黑水浸魄 | 直接支持 | 忘川增加 SPEED ADD -0.08 |
| 三魂皆远 | 依赖按层取值 | 替换蚀魂 valueByLayer 曲线 |
| 药石难入 | 依赖按层取值与受治疗扩展 | 替换治疗削弱曲线 |
| 渡口回声 | 原效果不支持 | 改为单目标每回合一次追加魂伤 |
| 江流不返 | 直接支持 | 3 层时 DOT 总计提高 20%,4 层时总计提高 30% |
| 洗魂有价 | 依赖层变更事件 | reason=dispel 时每行动一次伤害 |
| 两岸俱失 | 直接支持 | 不归 SPEED -0.30→-0.40 |
| 百鬼同哭 | 原效果不支持 | 改为首次失魂对当前目标追加魂伤 |
| 魂梦相侵 | 直接支持 | 失魂触发时重施 REFRESH_DURATION 忘川 |
| 末渡无舟 | 使用近似 | `mana_burn` 按 10%最大法力 |
| 不归亦不散 | 依赖单次层数缩放 | 伤害后 set 蚀魂为2 |
| 楚些成悲 | 依赖单次层数缩放 | perLayer 0.20→0.24 |
| 黑潮送行 | 直接支持 | 终结 completion 添加忘川 |
### 6.2 镇魄司命
| 节点 | 结论 | 实现方式 |
| --- | --- | --- |
| 铁入其影 | 直接支持 | 修改两个伤害 effect 系数 |
| 见影知名 | 直接支持 | 照影 duration 3→4 |
| 守神如城 | 直接支持 | CONTROL_RESISTANCE FIXED +0.10 |
| 一魄先夺 | 直接支持 | 夺魄 duration 2→3 |
| 钉下无声 | 直接支持 | Build Facade 修改 MP 55→45 |
| 狱火照名 | 直接支持 | 魂火 listener 0.25→0.35 |
| 三魂离座 | 直接支持 | 离魂引高层条件系数 0.78→0.884 |
| 先定其形 | 依赖层变更原语 | 每场一次,仅铺层神通在伤害后 apply 额外1层 |
| 心寂反照 | 直接支持 | 首次解控资源 +1→+2 |
| 四门皆闭 | 直接支持 | 高层镇魂额外 SPEED -0.20 Buff |
| 魂刑有度 | 直接支持 | `onResistEffects` 添加攻速减益 |
| 幽都铁律 | 当前工作区已支持 | `controlHitBonus: 0.15` |
| 五魄俱散 | 直接支持 | 失魂回落 effect 添加降攻 Buff |
| 神归有垣 | 直接支持 | 首次解控添加 10%最大气血护盾 |
| 一名一判 | 当前工作区已支持近似 | 终结后 `refund_paid_cost` 返还 20 法力并消费 marker |
| 司命判词 | 依赖单次层数缩放 | base 0.70→0.85 |
| 七寸断魂 | 依赖单次层数缩放 | perLayer 0.20→0.25 |
| 名落幽都 | 原击杀归因不便 | 改为终结后目标低于20%气血时触发 |
## 7. 不应采用的实现方式
- 不在 DamageSystem 中判断能力或宗门 ID 来绕过减伤。
- 不新增第二条灵魂生命值或在 Unit 上保存 soulHp。
- 不把蚀魂拆成五个独立状态。
- 不用五套 if/switch 在宗门编译器集中判断节点;参数进入 Build Facade,行为节点使用独立插件。
- 不为忘川接入一个只认识幽都的多目标列表。
- 不把 `battleProjection`、蚀魂层数、魂火或首次解控 marker 写入持久化模型。
- 不用负 `HEAL_AMPLIFY` 冒充受治疗削弱;它读取的是治疗者,在未来队伍战斗中语义会错误。
- 不用 `resolved_final` 实现魂伤;它会连统一增伤、境界、随机与标准计算管道一起绕过。
- 不让《照影》通过给目标写入超大负闪避来突破全局最低闪避;能力自身使用 guaranteed,后续能力遵守全局规则。
- 不在幽都实现期间整理、回滚或补完当前天衍工作区的无关改动。
## 8. 开发顺序
### 阶段一:通用原语
1. 分层状态:layers、BuffLayerChangedEvent、valueByLayer、one_layer dispel。
2. `HEAL_RECEIVED_REDUCTION` 与 `Unit.heal()` 统一应用。
3. `canCrit/canLifesteal`。
4. AbilityConfig hitPolicy。
5. 复用既有 effect plan 的条件与优先级,补四层/五层互斥选择测试,不新增伤害参数。
每项先补 battle-v5 原子测试,不引用幽都 ID。
### 阶段二:幽都基础态
1. definition、ids、六心法、魂火。
2. 蚀魂、失魂、归窍和心死神活。
3. 七门神通。
4. 当时的 1v1 自动战术。
先完成无节点基础战斗矩阵,再进入道途。
### 阶段三:双道途
1. Build Facade 收集数值和行为特征。
2. 招魂渡夜 18 节点。
3. 镇魄司命 18 节点。
4. 节点代表组合和固定种子平衡模拟。
### 阶段四:展示与注册
1. 入门演出和现有本地资产。
2. 宗门地图、设施主题和组织主题。
3. 生产 Runtime 注册。
4. 状态栏、日志和能力详情;动画与声音按设计稿暂缓。
## 9. 验证矩阵
### 9.1 通用引擎测试
- 一次 apply 2 层只发布一次 layer change,delta=2。
- 5 层刷新不发布 layer change。
- valueByLayer 在增层、减层和 clone 后正确重挂 modifier。
- one_layer 驱散 4→3,不移除 Buff;1→0 才移除。
- 受治疗削弱覆盖 Heal、Lifesteal、DamageMemory 和 ResourceDrain,MP/护盾/免死不受影响。
- canCrit=false 在高暴击率下仍不暴击。
- canLifesteal=false 不触发吸血。
- 四层/五层固定计划互斥命中,且任一计划只发布一次 DamageRequest。
- guaranteed 能从 AbilityConfig 走完整施法流程。
### 9.2 幽都基础测试
- 蚀魂完整状态矩阵:0→1、1→3、3→5、归窍期3→5钳制为4、5→3。
- 失魂成功、控制抵抗、控制免疫和心死神活解除四条收束路径都回到3层并进入归窍。
- 心死神活只触发一次,按 NO_ACTION/NO_SKILL/NO_BASIC 免疫同类控制。
- 魂火只在实际加层时获得,每来源行动最多1点,满层刷新不获取。
- 混合技能共用命中,但发布魔法与真实两个伤害包。
- 镇魂钉被抵抗时不产生禁疗。
- 终结技按4层/5层读取系数并在伤害后正确 clear/set2。
- 照影易伤实时跟随蚀魂层数。
### 9.3 历史批次暂不测试项
- 三目标忘川与多敌扩散:当时的 1v1 架构没有生产语义;当前 Team/Roster 架构下需要重新立项验证。
- 双幽都共享蚀魂:当时生产战斗没有同队多单位;当前仍缺少明确玩法规则,不能直接视为已支持。
- 动画、音效和受击表现:设计稿已明确暂缓。
### 9.4 完整实现后的命令
```bash
bunx vitest run src/shared/engine/battle-v5/tests
bunx vitest run src/shared/engine/sect/content/youdu
bunx vitest run src/shared/engine/sect
bun run lint
bun run test
bun run build
```
## 10. 最终判断
幽都真正需要 battle-v5 补齐的是“通用分层状态”和“受治疗削弱”两块基础能力;它们都与现有 Buff/Attribute 架构一致,属于自然扩展。
魂伤绕过全部减伤、真实多目标、施加时 DOT 快照、一个事件混合两种伤害通道、按击杀精确返还等设计,不值得在 V1 为单宗门扩大战斗引擎。采用本文近似效果后,幽都仍然完整保留“越防、蚕食、禁疗、失魂、终结”的核心体验,同时不会让 battle-v5 永久背负宗门专属语义。
-221
View File
@@ -1,221 +0,0 @@
推荐分成 3 个独立提交修复:先保证战斗正确性,再修通用展示层,最后补战术与平衡验收。不要在 battle-v5 中增加幽都 ID 特判。
## 1. 先修战斗正确性
### 1.1 修复 4→5 层漏发魂火
根因是监听顺序:
- 归窍钳制:1000
- 失魂触发:500
- 魂火获取:200
失魂抵抗后会先生成归窍,导致魂火监听误以为目标施法前就在归窍期。
建议把两个魂火监听提高到失魂触发之前、归窍钳制之后,例如:
```ts
const YOUDU_LAYER_PRIORITY = {
RETURNING_CLAMP: 1_000,
FIFTH_LAYER_NODE: 600,
SOUL_FIRE_GAIN: 550,
SOUL_LOST: 500,
} as const;
```
这样:
- 普通 4→5:先获得魂火,再处理失魂。
- 已有归窍的 4→5:先被钳制,仍不获得魂火。
- 已有归窍的 3→5:最终实际变为4层,仍获得1点魂火。
- 失魂抵抗/免疫不会反向影响已经成立的实际加层。
修改位置:[YouduBaseCompiler.ts](/Users/churcht/daoyou/Daoyou/src/shared/engine/sect/content/youdu/base/YouduBaseCompiler.ts:386)
至少补四组测试:
- 无归窍 4→5,失魂成功,获得魂火。
- 无归窍 4→5,失魂抵抗,获得魂火。
- 无归窍 4→5,失魂免疫,获得魂火。
- 有归窍 4→5→4,不获得魂火;3→5→4,获得魂火。
### 1.2 按伤害请求区分 MAGIC/TRUE
不要移除混合技能的 MAGIC/TRUE 能力标签;`AbilityFactory` 的标签契约是合理的。应修复伤害结算阶段的匹配方式。
在 [DamageImmunityEffect.ts](/Users/churcht/daoyou/Daoyou/src/shared/engine/battle-v5/effects/DamageImmunityEffect.ts:28) 中:
- MAGIC/TRUE/PHYSICAL 等伤害通道标签,只匹配 `event.damageType`。
- 非伤害通道标签,才匹配 `event.ability.tags`、`event.buff.tags` 或 `damageTags`。
类似:
```ts
function matchesDamageTag(event: DamageEvent, tag: string): boolean {
if (tag === GameplayTags.ABILITY.CHANNEL.MAGIC) {
return event.damageType === DamageType.MAGICAL;
}
if (tag === GameplayTags.ABILITY.CHANNEL.TRUE) {
return event.damageType === DamageType.TRUE;
}
if (tag === GameplayTags.ABILITY.CHANNEL.PHYSICAL) {
return event.damageType === DamageType.PHYSICAL;
}
return (
event.ability?.tags.hasTag(tag) ||
event.buff?.tags.hasTag(tag) ||
event.damageTags?.includes(tag)
);
}
```
同时全局搜索监听 `DamageRequestEvent/DamageEvent/DamageTakenEvent` 时使用 `ability_has_tag(CHANNEL.*)` 的代码。比如“摄魂”应改为:
```ts
{
type: 'damage_type_is',
params: { damageType: DamageType.MAGICAL },
}
```
位置:[artifactAffixes.ts](/Users/churcht/daoyou/Daoyou/src/shared/engine/creation-v2/affixes/definitions/artifactAffixes.ts:801)
回归测试需要证明:
- 法术免疫只阻挡《夺魄》的术伤包。
- 真实伤害免疫只阻挡魂伤包。
- 法术伤害回蓝只触发一次,不响应魂伤。
- 魂伤仍不能暴击、不能吸血。
## 2. 修复通用展示架构
### 2.1 移除通用层中的“佛相”硬编码
[abilityFacts.ts](/Users/churcht/daoyou/Daoyou/src/shared/engine/sect/core/presentation/abilityFacts.ts:590) 不应理解佛相、魔相、无相。
建议给 `AbilityEffectLayerConfig` 增加可选展示名称:
```ts
interface AbilityEffectLayerConfig {
id: string;
displayName?: string;
effects?: EffectConfig[];
completionEffects?: EffectConfig[];
}
```
通用层规则:
- 有 `displayName`:使用展示名。
- 无 `displayName`:根据计划条件生成“施法前至少3层蚀魂”等描述。
- 永远不直接展示 `sever-high`、`pin-low` 等内部 ID。
- 无相宗自己配置“佛相/魔相/无相”,通用层不再猜测。
幽都可配置为:
```ts
{ id: 'sever-low', displayName: '目标施法前少于3层蚀魂' }
{ id: 'sever-high', displayName: '目标施法前至少3层蚀魂' }
```
### 2.2 让展示层认识新的通用原语
[abilityFacts.ts](/Users/churcht/daoyou/Daoyou/src/shared/engine/sect/core/presentation/abilityFacts.ts:418) 当前仍按“重复几个 apply_buff”计算层数,应改为累计:
```ts
const layers = matches.reduce(
(total, effect) => total + (effect.params.layers ?? 1),
0,
);
```
还应展示 effect 条件,特别是:
```text
拥有3点魂火时:本次魂伤提高25%,伤害后消耗全部魂火
```
否则 `consume_all` 会被错误描述成无条件消费。
### 2.3 明确处理手写 detailRows
当前 [SectCompiler.ts](/Users/churcht/daoyou/Daoyou/src/shared/engine/sect/core/compilation/SectCompiler.ts:241) 会完全覆盖内容编译器传入的 `detailRows`。
建议二选一:
- 推荐:有手写 `detailRows` 时作为权威内容,自动事实只作补充。
- 或删除 `SectAbilityFactory` 的 `detailRows` 参数,避免继续产生死代码。
例如:
```ts
const authoredFacts = ability.detailRows ?? [];
const detailRows = Array.from(
new Set([...authoredFacts, ...configFacts, ...modifierFacts].filter(Boolean)),
);
```
随后把幽都详情全部改成基于 `settings` 动态生成,避免节点强化后仍显示基础值。
### 2.4 展示 valueByLayer
在 [buffText.ts](/Users/churcht/daoyou/Daoyou/src/shared/engine/battle-v5/effects/affixText/buffText.ts:95) 中增加 `valueByLayer` 格式化。
推荐把相同曲线的属性合并:
```text
物攻、法攻、物防、法防、身法:
1层-3%,2层-5%,3层-8%,4~5层-12%
受治疗削弱:
1层0%,2层15%,3层30%,4层50%,5层100%
普通驱散每次只移除1层
```
同时使用 `isPercentageAttributeType()` 判断 FIXED 概率属性,避免把 `0.2` 显示成“+0.2”而不是“20%”。
展示回归测试应断言:
- 不出现 `佛相`、`sever-high`、`pin-low`。
- 离魂引和镇魂显示增加2层。
- 蚀魂详情不包含 `+0`。
- 节点强化后显示真实的70%、0.22、40%控制抗性等数值。
- 魂火消费条件和25%/35%增幅可见。
## 3. 补齐战术与平衡验收
### 3.1 自动战术
`healer-drown` 可以检查对手能力:
- `selectionProfile.intents` 是否包含 `heal`。
- 治疗技能 `currentCooldown <= 1` 时提高镇魂优先级。
- 没有治疗能力时退化为普通 `tide-cycle`,避免战术名与行为不符。
“预计终结足够”不建议在幽都策略中复制完整伤害公式。更稳妥的是增加一个通用只读伤害估算接口,供所有宗门 AI 使用;若本轮不希望扩大战斗架构,应明确将设计稿改成“目标低于45%或魂火已满”的启发式规则。
位置:[strategy.ts](/Users/churcht/daoyou/Daoyou/src/shared/engine/sect/content/youdu/strategy.ts:45)
### 3.2 平衡测试
当前测试只是冒烟,应增加可失败的数值边界:
- 基础终结4层/5层精确倍率。
- 各终极节点允许的理论最高倍率。
- 短、中、长战的合理回合区间。
- 六套战术的终结次数、失魂次数、魂火溢出率。
- 忘川流派的持续魂伤占比。
- 默认攻击占比,防止 AI 因法力或条件错误长期只用《一叹》。
- 同种子结果稳定。
- 两条道途不能同时获得对方节点效果。
## 推荐提交顺序
1. `fix(battle): isolate mixed damage channels and soul-fire timing`
2. `fix(sect): make layered ability presentation domain-neutral`
3. `fix(battle-ui): render layered modifiers and dispel modes`
4. `test(youdu): cover tactics and balance contracts`
前两个提交完成前不建议上线;后两个完成后,《幽都》才基本达到设计稿的可验收状态。