# M4 成长与 quest/combat 联动设计(2026-04-21) 更新时间:`2026-04-21` ## 0. 文档目标 本文件只冻结一件事: **把 `player_progression / chapter_progression` 从“可单独调用的成长基座”推进到“任务交付与战斗胜利可自动写入的最小联动闭环”。** 本轮只落 `turn_in_quest` 和 `resolve_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. `SparComplete`、`Escaped`、`Ongoing` 都不发经验。 --- ## 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` 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`