Files
Genarrative/docs/technical/M4_PROGRESSION_QUEST_COMBAT_INTEGRATION_2026-04-21.md
2026-04-22 12:34:49 +08:00

4.5 KiB
Raw Blame History

M4 成长与 quest/combat 联动设计2026-04-21

更新时间:2026-04-21

0. 文档目标

本文件只冻结一件事:

player_progression / chapter_progression 从“可单独调用的成长基座”推进到“任务交付与战斗胜利可自动写入的最小联动闭环”。

本轮只落 turn_in_questresolve_combat_action(Victory) 两条经验链,不扩到完整章节蓝图 Rust 化、掉落分配、好感奖励或前端展示切换。


1. 本轮联动范围

本轮只接下面两条确定链路:

  1. turn_in_quest 成功后,把 quest_record.reward.experience 发放到 player_progression
  2. resolve_combat_action 结算为 Victory 后,把 battle_state.experience_reward 发放到 player_progression

补充规则:

  1. 若存在 chapter_id,同时尝试把经验记到 chapter_progression 账本。
  2. 若对应 chapter_progression 不存在,联动必须静默跳过,不能让任务交付或战斗结算失败。
  3. SparCompleteEscapedOngoing 都不发经验。

2. turn_in_quest 联动口径

2.1 经验来源

任务交付经验固定读取:

  1. quest_record.reward.experience.unwrap_or(0)

2.2 成长写入

当经验值 > 0 时,spacetime-module::turn_in_quest 需要在任务状态切换为 TurnedIn 后调用:

  1. upsert_player_progression_after_grant_tx

写入参数固定为:

  1. user_id = next.actor_user_id
  2. amount = reward_experience
  3. source = PlayerProgressionGrantSource::Quest
  4. updated_at_micros = next.updated_at_micros

2.3 章节账本写入

next.chapter_id 存在,则在成长写入后继续尝试调用章节账本 helper

  1. granted_quest_xp = reward_experience
  2. granted_hostile_xp = 0
  3. hostile_defeat_increment = 0
  4. level_at_exit = Some(updated_player.level)

若章节记录不存在:

  1. 静默跳过
  2. 保留任务交付成功
  3. 不把“章节计划尚未初始化”视为任务错误

3. resolve_combat_action 联动口径

3.1 battle_state 新增字段

为避免在 reducer 里临时反查外部上下文,本轮给 BattleStateInput / BattleStateSnapshot / battle_state 表补两个最小字段:

  1. chapter_id: Option<String>
  2. experience_reward: u32

设计意图:

  1. chapter_id 决定战斗胜利时是否记章节账本。
  2. experience_reward 作为已编译好的确定奖励,避免本轮就把章节蓝图和敌对档位计算重新耦回 battle reducer。

3.2 胜利经验发放

resolve_combat_action 返回:

  1. CombatOutcome::Victory

spacetime-module 需要继续执行:

  1. upsert_player_progression_after_grant_tx

写入参数固定为:

  1. user_id = result.snapshot.actor_user_id
  2. amount = result.snapshot.experience_reward
  3. source = PlayerProgressionGrantSource::HostileNpc
  4. updated_at_micros = result.snapshot.updated_at_micros

补充规则:

  1. 只有 experience_reward > 0 时才真正写成长表。
  2. SparComplete 不发经验,因为切磋不算敌对击杀。

3.3 章节账本写入

result.snapshot.chapter_id 存在,且本次为 Victory,则继续尝试:

  1. granted_quest_xp = 0
  2. granted_hostile_xp = experience_reward
  3. hostile_defeat_increment = 1
  4. level_at_exit = Some(updated_player.level)

同样地,若章节记录不存在:

  1. 静默跳过
  2. 仍保留 battle_state 的正常收束结果

4. reducer 分层约束

本轮保持以下分层不变:

  1. module-combat 仍只承接纯战斗状态推进,不直接依赖 module-progression
  2. module-quest 仍只承接纯任务状态流转,不直接依赖 module-progression
  3. 真正的跨域写入统一放在 crates/spacetime-module reducer / transaction helper 中完成。

这样做的原因是:

  1. 领域 crate 保持纯规则,便于后续单测和重用。
  2. SpacetimeDB 事务内的表写顺序集中在同一层,避免跨 crate 重复持久化策略。

5. 本轮明确不做

本轮明确不扩到以下内容:

  1. 还不把 battle reward 在 reducer 内现场计算为经验值。
  2. 还不把 quest 奖励里的物品、货币、好感奖励统一并入同一事务。
  3. 还不把 quest signal 自动从战斗/剧情全量分发到任务系统。
  4. 还不把 chapter_progression 缺失时自动补建计划记录。

6. 验证要求

本轮变更完成后,至少执行:

  1. npm run check:encoding
  2. cargo test -p module-combat
  3. cargo test -p module-progression
  4. cargo test -p module-quest
  5. cargo check -p spacetime-module