# M4 module-npc SpacetimeDB 基座记录(2026-04-21) 更新时间:`2026-04-21` ## 0. 文档目标 本文件只记录一件事: **把 `module-npc` 从“只有占位 README”推进到“已有可编译 Rust 领域 contract,并接入 `SpacetimeDB` 最小 `npc_state` 真相表与社交动作 reducer/procedure”的真实落地结果。** 本轮只做最小基座,不扩到完整 `npc_trade / npc_gift / npc_recruit` 的全链结算迁移,也不改前端 UI。 --- ## 1. 本轮落地范围 本轮只落实下面 6 件事: 1. 新增 `server-rs/crates/module-npc/` 真实 crate,而不是继续停留在目录占位。 2. 在 `module-npc` 中冻结 `relation_state / stance_profile / npc_state` 的首版领域类型与校验 helper。 3. 在 `module-npc` 中补齐 `build_initial_stance_profile`、`normalize_npc_state_snapshot`、`apply_npc_social_action` 等最小规则原语。 4. 在 `server-rs/crates/spacetime-module/` 中新增 `npc_state` 表。 5. 在 `spacetime-module` 中新增 `upsert_npc_state`、`resolve_npc_social_action` 及对应 procedure,形成最小可编译写入口。 6. 在 `module-npc` 中新增 `resolve_npc_interaction` 的首版领域 contract,并在 `spacetime-module` 中补对应 reducer / procedure。 --- ## 2. 本轮新增的真实工程落点 ### 2.1 新增 crate 1. `server-rs/crates/module-npc/Cargo.toml` 2. `server-rs/crates/module-npc/src/lib.rs` ### 2.2 workspace 与主工程聚合 1. `server-rs/Cargo.toml` - 已把 `crates/module-npc` 纳入 workspace members 2. `server-rs/crates/spacetime-module/Cargo.toml` - 已接入 `module-npc` 依赖 3. `server-rs/crates/spacetime-module/src/lib.rs` - 已接入 `module-npc` 类型 - 已新增 `npc_state` - 已新增 `upsert_npc_state` - 已新增 `upsert_npc_state_and_return` - 已新增 `resolve_npc_social_action` - 已新增 `resolve_npc_social_action_and_return` - 已新增 `resolve_npc_interaction` - 已新增 `resolve_npc_interaction_and_return` --- ## 3. 当前冻结的数据口径 ### 3.1 `relation_state` 当前首版冻结为: 1. `affinity` 2. `stance` `stance` 当前只冻结 5 档: 1. `Hostile` 2. `Guarded` 3. `Neutral` 4. `Cooperative` 5. `Bonded` 当前阈值直接对齐现有前端 / Node 原语: 1. `< 0` -> `Hostile` 2. `< 15` -> `Guarded` 3. `< 30` -> `Neutral` 4. `< 60` -> `Cooperative` 5. `>= 60` -> `Bonded` ### 3.2 `stance_profile` 当前首版冻结为: 1. `trust` 2. `warmth` 3. `ideological_fit` 4. `fear_or_guard` 5. `loyalty` 6. `current_conflict_tag` 7. `recent_approvals` 8. `recent_disapprovals` 字段策略: 1. 数值统一收敛到 `0 ~ 100`。 2. 最近好评 / 反感文本统一只保留最近 `3` 条。 3. `current_conflict_tag` 仍允许为空,不在本轮强绑世界线程 ID。 ### 3.3 `npc_state` 当前首版字段冻结为: 1. `npc_state_id` 2. `runtime_session_id` 3. `npc_id` 4. `npc_name` 5. `affinity` 6. `relation_state` 7. `help_used` 8. `chatted_count` 9. `gifts_given` 10. `recruited` 11. `trade_stock_signature` 12. `revealed_facts` 13. `known_attribute_rumors` 14. `first_meaningful_contact_resolved` 15. `seen_backstory_chapter_ids` 16. `stance_profile` 17. `created_at` 18. `updated_at` 当前策略: 1. `npc_state` 保持 private 真相表口径。 2. `npc_state_id` 允许由 `runtime_session_id + npc_id` 自动派生,避免外部每次重复拼接。 3. `relation_state` 作为显式冗余字段落表,避免每次读取都重复派生。 4. `npc_name` 当前保留为调试与兼容聚合字段,不承担唯一键职责。 --- ## 4. 当前 reducer / procedure 口径 ### 4.1 `upsert_npc_state` 当前负责: 1. 校验 `runtime_session_id / npc_id / npc_name` 2. 归一化 `stance_profile` 3. 归一化 `relation_state` 4. 以 `npc_state_id` 为主键执行幂等写入 ### 4.2 `resolve_npc_social_action` 当前只承接 **纯 NPC 关系状态** 的最小变更,不负责背包、任务、队伍、战斗副作用。 当前动作冻结为: 1. `Chat` 2. `Help` 3. `Gift` 4. `Recruit` 5. `QuestAccept` 当前规则: 1. `Chat` - 默认按 `max(2, 6 - chatted_count)` 推进好感 - 递增 `chatted_count` - 强制标记 `first_meaningful_contact_resolved = true` 2. `Help` - 若已使用过援手则拒绝 - 默认推进 `4` 点好感 - 写入 `help_used = true` 3. `Gift` - 递增 `gifts_given` - 默认按 `4` 点好感推进,允许外部显式传入覆盖值 4. `Recruit` - 若当前好感 `< 60` 则拒绝 - 写入 `recruited = true` - 同时标记首遇已完成 5. `QuestAccept` - 默认推进 `3` 点好感 - 只改 NPC 关系侧立场数据,不直接落 quest 真相 当前 procedure 仅返回最新 `NpcStateSnapshot`,不在本轮提前扩出 story patch / UI 文案 contract。 ### 4.3 `resolve_npc_interaction` 当前首版 `resolve_npc_interaction` 不直接承担所有跨子域副作用,而是先固定 **NPC 单次正式交互** 的最小统一结果口径。 当前输入冻结为: 1. `runtime_session_id` 2. `npc_id` 3. `npc_name` 4. `interaction_function_id` 5. `updated_at_micros` 6. `release_npc_id`(仅为后续招募换队预留,当前不在 Rust 侧正式消费) 当前支持的 function 只冻结为: 1. `npc_preview_talk` 2. `npc_chat` 3. `npc_help` 4. `npc_recruit` 5. `npc_fight` 6. `npc_spar` 7. `npc_leave` 当前输出冻结为: 1. `npc_state` 2. `interaction_status` 3. `action_text` 4. `result_text` 5. `story_text` 6. `battle_mode` 7. `encounter_closed` 8. `affinity_changed` 9. `previous_affinity` 10. `next_affinity` 当前规则: 1. `npc_preview_talk` - 只把交互状态切到 `Previewed` - 不改好感 2. `npc_chat` - 复用 `resolve_npc_social_action(Chat)` 的关系推进 - 返回 `interaction_status = Dialogue` 3. `npc_help` - 复用 `resolve_npc_social_action(Help)` - 返回 `interaction_status = Resolved` 4. `npc_recruit` - 当前只负责把 `npc_state.recruited = true` - 不在本轮承担 companion / roster 真相写入 - 返回 `interaction_status = Recruited` 5. `npc_fight` - 不改 `npc_state.affinity` - 返回 `interaction_status = BattlePending` - `battle_mode = Fight` 6. `npc_spar` - 不改 `npc_state.affinity` - 返回 `interaction_status = BattlePending` - `battle_mode = Spar` 7. `npc_leave` - 不改关系真相 - 返回 `interaction_status = Left` - `encounter_closed = true` 当前刻意不做: 1. 不直接生成 `RuntimeStoryPatch` 2. 不直接写 `companions / roster / inventory_slot` 3. 不直接把玩家 HP / MP、切磋战斗目标、战斗奖励塞进这个 reducer 也就是说,这一层当前只负责把 **Node 侧 `resolveNpcInteraction` 的统一入口语义** 先冻结为可编译 contract,不宣称已经迁完全部副作用。 --- ## 5. 当前刻意未做 本轮明确没有扩到以下范围: 1. 还没有落 `npc_trade` 的库存与价格正式结算 2. 还没有落 `npc_gift` 的背包扣减与物品收益结算 3. 还没有落 `npc_recruit` 的队伍替换与 companion 真相迁移 4. `npc_fight / npc_spar` 的正式 `battle_state` 初始化编排不在 `module-npc` crate 内部完成,而是下沉到 `spacetime-module` 聚合 procedure 5. 还没有把 `custom world` 的 `narrativeProfile / backstoryReveal` 真正投影进 SpacetimeDB 6. 还没有把 Node 侧 `npcInteractionService` 全量切到 `server-rs` 7. 还没有给前端接入 `SpacetimeDB` 的 NPC 订阅读模型 也就是说,本轮只是把 **NPC 关系状态基座** 立起来,不宣称已经完成完整 NPC 子域迁移。 --- ## 6. 下一步建议 后续应继续按下面顺序推进: 1. 把 `npc_recruit` 的 companion / roster 真相迁移拆成 `module-npc + module-runtime + module-story` 的联合 reducer 设计。 2. 在 `spacetime-client` / Axum 侧继续把 `npc_fight / npc_spar` 的 `battle_state` 联合编排接口接出来。 3. 把 `npc_trade / npc_gift` 的正式库存、扣减与收益迁到 `inventory / runtime-item` 联动链。 4. 把 `backstoryReveal / privateChatUnlockAffinity / narrativeProfile` 的可见性规则投成显式读模型。 5. 再接 `api-server` 的 NPC facade 与前端 runtime action。