# RPG 开局首幕 NPC 流程收口方案(2026-04-30) ## 目标 本轮只收口“进入游戏开局场景后遇到第一幕第一批人”的运行时流程: 1. 对方主角色好感度 `>= 0` 时,聊天过程中允许出现 `npc_chat`、任务、送礼、交易、获得帮助等 NPC 功能选项;聊天结束后界面只保留一个 `story_continue_adventure`,点击后直接推进到下一幕。 2. 对方主角色好感度 `< 0` 时,聊天过程中只允许 `npc_chat`;聊天可以由模型中断,也可以由玩家主动中断。中断后只允许 `npc_fight` 与 `battle_escape_breakout`。 3. 删除这条主流程里的干扰分支:正好感聊天结束后不再展开旧 NPC 目录或相邻场景旅行;负好感聊天中不再混入交易、送礼、求助、任务、招募、切磋、离开等 function。 ## 工程落点 1. `src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts` - `buildNpcChatFunctionOptionCatalog(...)` 按当前 NPC 好感过滤功能候选。 - 负好感聊天候选只保留 `npc_chat`。 - 正好感聊天结束后的 `story_continue_adventure` 只揭开下一幕入口;若当前场景没有下一幕,才退回相邻场景入口。 2. `src/hooks/rpg-runtime-story/choiceActions.ts` - 应用 `deferredRuntimeState.storyEngineMemory`,保证点击继续后真正切到下一幕的 `currentSceneActState`。 3. `server-rs/crates/api-server/src/runtime_story/compat/presentation.rs` - 服务端 active NPC option catalog 与前端同规则对齐。 - 负好感 active NPC 只返回 `npc_chat`。 - 非负好感 active NPC 返回聊天、帮助、交易、送礼、任务、招募等功能,不再返回战斗、切磋、离开。 ## 验收 1. 正好感 NPC 主动退出聊天后,只显示 `story_continue_adventure`。 2. 点击 `story_continue_adventure` 后,`storyEngineMemory.currentSceneActState.currentActId` 推进到下一幕。 3. 负好感 NPC 聊天请求中的 `functionOptions` 为空,聊天 UI 不出现非聊天 function。 4. 负好感聊天中断后只出现“战斗”和逃跑选项。 5. 服务端 state catalog 对负好感 active NPC 不返回交易、送礼、帮助、任务、招募、切磋、离开等干扰入口。 ## 2026-04-30 补充:负好感主动中止恢复 ### 问题 敌对聊天的模型主动中止依赖后端建议 JSON 中的 `shouldEndChat` 字段,但部分入口没有把负好感 NPC 标记为 `terminationMode: hostile_model`,导致后端即使收到 `shouldEndChat: true` 也会按非敌对聊天忽略。另一个缺口是 NPC 主动开场第一轮只展示后续候选,没有处理 `chatDirective.forceExit`,因此第一轮开场也无法被模型主动中止。 ### 落地 1. `src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts` - 构造 `NpcChatDirective` 时直接读取当前 NPC 好感度。 - 只要 `affinity < 0`,统一写入 `limitReason: negative_affinity`、`terminationMode: hostile_model`、`isHostileChat: true`。 - NPC 主动开场收到 `chatTurn.chatDirective.forceExit === true` 时,立即收起 `npcChatState`,展示战斗与逃跑选项。 ### 补充验收 1. 任意负好感 NPC 聊天轮都必须向后端传 `terminationMode: hostile_model`,不能只依赖第一幕主 NPC 场景幕状态。 2. 负好感 NPC 主动开场第一轮若返回 `forceExit: true`,聊天输入立即关闭,只显示战斗与逃跑。 ## 2026-04-30 补充:聊天首句统一由模型 NPC 发起 ### 问题 NPC 主动开场链路本身已经存在,并会以空玩家消息调用模型,同时传入 `npcInitiatesConversation: true`。但运行时入口曾把这条链路限制在 `firstMeaningfulContactResolved !== true`,导致角色完成首次有效接触后,再次从 NPC 入口或交互选项进入聊天时,会退回旧的 `enterNpcChat(...)` 本地入口:界面先展示玩家可点的话题,没有模型生成的 NPC 首句。负好感且非限定场景幕时,还存在一条本地敌对宣言分支,会直接给战斗/逃跑,绕过“先聊天再中断”的主流程。 ### 落地 1. `enterNpcInteraction(...)` 不再用 `firstMeaningfulContactResolved` 决定是否走 NPC 主动开场;只要是从 NPC 入口新开聊天,都调用 `startNpcInitiatedOpening(...)`。 2. `handleNpcInteraction(...)` 的 `chat` 分支保留“当前已经在同一段 `npcChatState` 内时,点击 `npc_chat` 作为玩家回复”的行为;若不在已有聊天内,统一调用 `startNpcInitiatedOpening(...)`。 3. 删除负好感入口的本地敌对宣言分支。负好感只通过 `NpcChatDirective` 影响模型语气、功能选项和 `forceExit` 后的战斗/逃跑收束,不再跳过模型首句。 4. `enterNpcChat(...)` 仅保留为缺少角色/世界类型或模型开场失败时的兜底入口,不作为正常聊天开场路径。 ### 补充验收 1. 不论好感度正负,也不论 `firstMeaningfulContactResolved` 是否为 `true`,新开聊天首轮都必须调用 `streamNpcChatTurn(..., '', { npcInitiatesConversation: true })`。 2. 新开聊天最终展示的第一条 `dialogue` 必须是模型返回的 NPC 文本,`npcChatState.openingSource` 必须是 `npc_initiated`。 3. 已经处于同一段 `npcChatState` 中时,点击 `npc_chat` 仍作为玩家本轮回复进入 `handleNpcChatTurn(...)`,不能重新开一段 NPC 首句。 4. 负好感入口不能直接显示本地战斗/逃跑;只有模型或玩家中断聊天后,才显示 `npc_fight` 与 `battle_escape_breakout`。