Files
Genarrative/docs/technical/M4_MODULE_COMBAT_SPACETIMEDB_BASELINE_2026-04-21.md
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

9.0 KiB
Raw Permalink Blame History

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_stateresolve_combat_action 两个 reducer。

2. 当前冻结的实现边界

2.1 首版必须支持的战斗 function

首版与 ../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_recordnpc_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_stateBattleModeBattleStatusCombatOutcomeResolveCombatActionInput 已冻结到代码。
  3. spacetime-module 已新增 battle_state 表。
  4. spacetime-module 已新增 create_battle_stateresolve_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 真正打通。