后端重写提交

This commit is contained in:
2026-04-22 12:34:49 +08:00
parent cf8da3f50f
commit 997a8daada
438 changed files with 53355 additions and 865 deletions

View File

@@ -0,0 +1,336 @@
# M4 module-combat SpacetimeDB 基线设计2026-04-21
更新时间:`2026-04-22`
## 0. 文档目标
本文件只冻结一件事:
**把 `module-combat` 从“只有 README 占位”推进到“首版 battle_state 与 resolve_combat_action 可真实编码、可编译、可继续扩展”的工程基线。**
本轮不宣称完成完整 `runtime story action` 迁移,也不把 `inventory / npc / story AI 续写` 直接耦进战斗 reducer跨子域写入继续收敛在 `spacetime-module` 聚合层。
---
## 1. 本轮落地范围
本轮只做下面 5 件事:
1. 新增 `server-rs/crates/module-combat/` 真实 crate。
2. 冻结 `battle_state` 的首版领域类型、枚举、输入结构与字段校验 helper。
3. 冻结 `resolve_combat_action` 的首版输入、输出与纯规则推进逻辑。
4.`server-rs/crates/spacetime-module/` 中新增 `battle_state` 表。
5.`spacetime-module` 中新增 `create_battle_state``resolve_combat_action` 两个 reducer。
---
## 2. 当前冻结的实现边界
### 2.1 首版必须支持的战斗 function
首版与 [../prd/AI_NATIVE_BATTLE_SINGLE_ACTION_FUNCTION_PRD_2026-04-18.md](../prd/AI_NATIVE_BATTLE_SINGLE_ACTION_FUNCTION_PRD_2026-04-18.md) 保持一致,只支持以下单行为入口:
1. `battle_attack_basic`
2. `battle_recover_breath`
3. `battle_use_skill`
4. `battle_escape_breakout`
5. 旧兼容攻击类:
- `battle_all_in_crush`
- `battle_guard_break`
- `battle_probe_pressure`
- `battle_feint_step`
- `battle_finisher_window`
本轮刻意不接入:
1. `inventory_use`
2. 技能与物品的正式外部明细读取
3.`quest_record``npc_state` 的联动写入
4. 脱战后 `story_event` 追加与 AI 续写触发
### 2.2 为什么先不做 `inventory_use`
当前 Rust 侧还没有 `inventory_slot` 正式表,也没有稳定的战斗内物品快照输入。
如果现在把 `inventory_use` 硬塞进 `module-combat`,只会出现两种坏结果:
1. reducer 内部引入并不存在的 inventory 真相依赖;
2. 退回成“让 Axum 先算完再写 battle_state”的伪迁移。
因此本轮明确冻结为:
1. `module-combat` 先完成纯战斗状态推进;
2. `inventory_use` 留到 `inventory_slot` 与 runtime snapshot projection 口径稳定后再接。
---
## 3. `battle_state` 首版字段
首版 `battle_state` 冻结为以下字段:
1. `battle_state_id`
2. `story_session_id`
3. `runtime_session_id`
4. `actor_user_id`
5. `target_npc_id`
6. `target_name`
7. `battle_mode`
8. `status`
9. `player_hp`
10. `player_max_hp`
11. `player_mana`
12. `player_max_mana`
13. `target_hp`
14. `target_max_hp`
15. `chapter_id`
16. `experience_reward`
17. `reward_items`
18. `turn_index`
19. `last_action_function_id`
20. `last_action_text`
21. `last_result_text`
22. `last_damage_dealt`
23. `last_damage_taken`
24. `last_outcome`
25. `version`
26. `created_at`
27. `updated_at`
### 3.1 设计意图
首版只解决下面这些真相问题:
1. 当前战斗是否存在、是否仍在进行中;
2. 玩家与当前目标的 HP / MP 最小数值状态;
3. 当前是 `fight` 还是 `spar`
4. 当前战斗归属哪个章节;
5. 本场战斗若胜利应发多少经验;
6. 本场战斗若胜利应发哪些已编译好的 reward item
7. 最近一次动作结算了什么;
8. 当前 battle reducer 是否发生过版本推进。
### 3.2 当前刻意不放入的字段
本轮明确不放:
1. 多目标列表
2. 技能冷却 map
3. build buff 详情
4. 掉落预算、好感预算、剧情上下文大对象
5. 大型 `rawGameState` 镜像字段
原因很直接:这些都属于后续跨子域联动层,不适合在 `battle_state` 首版里重新堆一个大 JSON。
---
## 4. 枚举与动作口径
### 4.1 `BattleMode`
只保留两种:
1. `Fight`
2. `Spar`
### 4.2 `BattleStatus`
只保留三种:
1. `Ongoing`
2. `Resolved`
3. `Aborted`
说明:
1. `Resolved` 表示战斗已正常收束,包括胜利、切磋结束、成功逃脱。
2. `Aborted` 预留给后续 session 中断、外部清理、投影回滚等异常收束场景。
### 4.3 `CombatOutcome`
首版冻结:
1. `Ongoing`
2. `Victory`
3. `SparComplete`
4. `Escaped`
这与当前共享契约里的 `RuntimeBattlePresentation.outcome` 一致,避免首版就制造新的枚举翻译成本。
---
## 5. `resolve_combat_action` 首版规则
### 5.1 输入
首版 reducer 输入只包含:
1. `battle_state_id`
2. `function_id`
3. `action_text`
4. `base_damage`
5. `mana_cost`
6. `heal`
7. `mana_restore`
8. `counter_multiplier`
9. `updated_at_micros`
### 5.2 为什么允许输入 `base_damage`
本轮 `module-combat` 的职责是把战斗推进规则固定到 SpacetimeDB。
但玩家技能、装备 build、物品 buff、成长曲线这些正式真相仍未迁完因此首版允许上游把已算好的 `base_damage / mana_cost / heal / mana_restore` 作为确定输入传进 reducer。
这意味着当前模块边界是:
1. `module-combat` 负责状态推进、反击、逃跑、战斗收束规则;
2. 更高层的 build / skill / item 数值来源仍可在后续模块中逐步收敛;
3.`inventory / progression / runtime build` 真相表稳定后,再继续把这些输入收得更窄。
### 5.3 动作规则
#### A. `battle_escape_breakout`
直接结束战斗:
1. `status = Resolved`
2. `last_outcome = Escaped`
3. `last_damage_dealt = 0`
4. `last_damage_taken = 0`
#### B. `battle_recover_breath`
恢复类动作:
1. 玩家回复 `heal`
2. 玩家回复 `mana_restore`
3. 若战斗仍持续,则按 `counter_multiplier` 吃一次敌方反击
#### C. `battle_attack_basic` / 旧兼容攻击类 / `battle_use_skill`
攻击类动作:
1. 目标扣除 `base_damage`
2. 若目标已收束,则按 `battle_mode` 进入 `Victory / SparComplete`
3. 若目标未收束,则玩家按 `counter_multiplier` 吃一次敌方反击
### 5.4 反击规则
首版固定:
1. `fight` 下敌方基础反击伤害 = `max(4, round(target_max_hp * 0.14 * counter_multiplier))`
2. `spar` 下敌方基础反击伤害固定为 `1`
这是对当前 Node 逻辑的直接收敛,先保证行为方向不漂移,不在本轮发明新的战斗公式。
### 5.5 HP 下限规则
1. `fight` 下正常下限为 `0`
2. `spar` 下双方 HP 最低保留为 `1`
这样能保留当前“切磋点到为止”的旧行为,不把 `spar` 错结算成死亡战斗。
---
## 6. `spacetime-module` 接线口径
### 6.1 battle_state 表
`spacetime-module` 首版只新增一张 private 真相表:
1. `battle_state`
建议索引:
1. `by_story_session_id`
2. `by_runtime_session_id`
3. `by_actor_user_id`
### 6.2 reducer
当前仍只保留两个战斗 reducer
1. `create_battle_state`
2. `resolve_combat_action`
职责:
1. `create_battle_state` 只负责插入 battle 真相,不负责故事会话编排。
2. `resolve_combat_action` 负责推进 battle 真相。
3.`Victory` 收束时,由 `spacetime-module` 聚合层继续把 `experience_reward` 联动写入 `player_progression / chapter_progression`
4.`Victory` 收束且 `reward_items` 非空时,由 `spacetime-module` 聚合层继续把战利品写入 `inventory_slot`
5. `resolve_combat_action` 仍不负责 AI 续写和 quest signal 全量分发。
---
## 7. 与后续子域的边界
### 7.1 与 `story`
当前关系:
1. `story` 负责更高层 action 路由与后续 story_event 追加;
2. `combat` 只返回 battle 真相推进结果。
后续再补:
1. 战斗结束时的 `story_event`
2. 脱战后的 `continue_story` / `resolve_story_action`
### 7.2 与 `inventory`
当前不直接耦合到 `module-combat` reducer。
后续再补:
1. 战斗内 `inventory_use`
2. 消耗品扣减
3. 战斗 buff 写入
当前已存在的聚合层联动:
1. `Victory` 时可把 `battle_state.reward_items` 写入 `inventory_slot`
### 7.3 与 `progression`
当前不直接在 `module-combat` reducer 内发经验与等级变更。
后续再补:
1. hostile scaling 与 reward 编译口径
当前已存在的聚合层联动:
1. `fight_victory` 的经验发放
2. 章节账本写入
### 7.4 与 `npc`
当前不直接改好感。
后续再补:
1. `spar_complete` 的 affinity 变化
2. `fight / spar` 与 encounter 状态同步
---
## 8. 本轮验收口径
满足以下条件,视为本轮 `module-combat` 基线完成:
1. `server-rs/crates/module-combat` 已从 README 占位升级为真实 crate。
2. `battle_state``BattleMode``BattleStatus``CombatOutcome``ResolveCombatActionInput` 已冻结到代码。
3. `spacetime-module` 已新增 `battle_state` 表。
4. `spacetime-module` 已新增 `create_battle_state``resolve_combat_action` reducer。
5. `cargo check -p module-combat -p spacetime-module` 通过。
---
## 9. 下一步建议
在本轮基线稳定后,下一步按以下顺序推进最稳:
1. 设计 `inventory_slot` 与战斗内 `inventory_use` 的最小真相输入。
2. 设计 `resolve_story_action` 如何编排 `story + combat + npc + quest + inventory`
3.`battle_state` 结束事件接入 `story_event`
4. 再把 Axum facade 与 `RuntimeStoryActionResponse.battle` 真正打通。