blob: bbc068774b47ec17f1e337792ad78c1b0eadcfe5 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
# 交互与交易(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:<reason>` → 文案「对话失败:<reason>」;
无可用对话 → 「当前没有可用对话」;无效选项 → 「对话选项无效」;附近无目标 → 「附近没有可互动目标」。
- 交易失败:`PlayerTrade` 返回 `Succeeded = false` 且 `Message` 为中文原因——
「未选择职业」「找不到对方」「数量必须大于零」「库存不足」「无法报价」「对方资金不足」/
「背包里没有可卖物品」;对话交易侧失败名为「找不到目标 / 目标不在范围内 / 目标暂时无法互动 /
找不到买家 / 找不到卖家 / 买家和卖家不能是同一人 / 数量必须大于零 / 库存不足 / 资金不足」。
- 未知状态 token:`statusLabel` 兜底「状态已更新」。
## 算法与性能取舍
- `apply` 是纯状态转换:命令 → 校验(`commandAllowed`)→ 执行 → 生成 `Status` token;
面板文本在帧内按需拼接,无额外缓存。
- `PlayerTrade` 报价复用 `Sim.quotePrice`(先经 `Occupation.biasedQuote`;无职业恒等 1.0),
通过「报价世界副本」把玩家镜像成 `Npc`,副本不写回真实世界——避免给 `Avatar` 引入 Inventory 字段。
- 面板文案统一走既有 CJK 图集,不新增字体/外部素材。
|