# 交互与交易(M5Interaction / PlayerTrade) 源码:`src/LivingVillage.Desktop/Interaction.fs`、`src/LivingVillage.Desktop/PlayerTrade.fs`。 ## 职责 把键盘输入翻译为 M5 面板/对话/交易命令,并维护玩家可见面板与状态文案; `PlayerTrade` 把职业背包侧车变成世界里可买卖的物品(不改 `Sim.step` 数值路径)。 ## 对外 API 概览 - `type M5Panel = WorldPanel | NeedsPanel | ObservationPanel | DialoguePanel | ChroniclePanel | TaskPanel`。 - `type M5Command = Interact | Intent1..Intent6 | BuyFromTarget | SellToTarget | AcceptTask | Observe | ToggleNeeds | ShowChronicle | ShowTaskPanel | ClosePanel`。 - `type M5View = { Panel; Menu; Needs; Observations; Chronicle; Status; StatusTick; HomeMode; Prompt; PromptTargetPos; Task: Occupation.State option; Seed: uint64 }`。 - `M5Interaction.initial : M5View`;`M5Interaction.apply command world view : World * M5View`(public,纯函数); `panelLines world view`、`statusText view`、`panelName view`、`titleText view`、 `transientStatusLine nowTick view`、`cornerStatusLines world`、`refreshPrompt world view`。 - `PlayerTrade.Outcome = { World; Occupation; Succeeded; UnitPrice: float32 option; Reward: (ItemKind*int) option; Message: string }`。 - `PlayerTrade.quoteBase / quote`、`PlayerTrade.buy state counterparty item quantity world`、 `PlayerTrade.sell ...`、`PlayerTrade.buyFood`、`PlayerTrade.sellFirst`。 ## 单位与量纲 - `Status: string` 为**内部 token**(如 `"dialogue target=..."`、`"task|..."`、`"inquiry|..."`), 经 `statusLabel` 映射为中文;`StatusTick: int64 option` 为显示计时(`statusDisplayDurationTicks = 150L`)。 - 交易金额/单价为 `float32`,与 `Sim.Needs.Money` 同尺度;数量 `int`(必须 > 0)。 - 面板快捷键:`1..6` → 六种对话意图;`B` 买入食物×1、`V` 卖出背包首件×1;`Tab` 需求、`Q` 观察、 `C` 年鉴、`T`/回车 任务面板、`Esc` 关闭;`E` 互动/进屋/出屋。文案见 `ChineseText.requiredUiLabels`。 ## 不变量 1. **apply 纯函数**:返回新 `(World, M5View)`,不改动入参;世界交互仅在 `WorldPanel` 生效 (`worldInputAllowed`)。 2. **面板行来源唯一**:所有面板正文由 `panelLines` 生成;文案经 `CjkGlyphAtlas` 全可渲染。 3. **无伪造状态**:面板只展示 `M5View`/`World` 中真实存在的值(需求、观察、年鉴、任务)。 4. **交易不改数值路径**:`PlayerTrade` 只追加 `TradeEvent`/`TradeAnnal` 并写双方记忆与背包, 不动 `Sim.step`/地图/seed;玩家以临时镜像 `NpcId -1` 仅用于报价。 5. **背包口径**:`Occupation.State.Backpack` 为声明顺序稳定的 `(ItemKind*int) list`,归零移除、新物品追加尾部。 ## 失败分支 - 命令不可用(如非对话面板发 `Intent1`):`applyAllowed` 返回 `(world, view)` 原样,不改变状态。 - 对话失败 token:`dialogue rejected:` → 文案「对话失败:」; 无可用对话 → 「当前没有可用对话」;无效选项 → 「对话选项无效」;附近无目标 → 「附近没有可互动目标」。 - 交易失败:`PlayerTrade` 返回 `Succeeded = false` 且 `Message` 为中文原因—— 「未选择职业」「找不到对方」「数量必须大于零」「库存不足」「无法报价」「对方资金不足」/ 「背包里没有可卖物品」;对话交易侧失败名为「找不到目标 / 目标不在范围内 / 目标暂时无法互动 / 找不到买家 / 找不到卖家 / 买家和卖家不能是同一人 / 数量必须大于零 / 库存不足 / 资金不足」。 - 未知状态 token:`statusLabel` 兜底「状态已更新」。 ## 算法与性能取舍 - `apply` 是纯状态转换:命令 → 校验(`commandAllowed`)→ 执行 → 生成 `Status` token; 面板文本在帧内按需拼接,无额外缓存。 - `PlayerTrade` 报价复用 `Sim.quotePrice`(先经 `Occupation.biasedQuote`;无职业恒等 1.0), 通过「报价世界副本」把玩家镜像成 `Npc`,副本不写回真实世界——避免给 `Avatar` 引入 Inventory 字段。 - 面板文案统一走既有 CJK 图集,不新增字体/外部素材。