summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/Interaction.fs
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Desktop/Interaction.fs')
-rw-r--r--src/LivingVillage.Desktop/Interaction.fs171
1 files changed, 130 insertions, 41 deletions
diff --git a/src/LivingVillage.Desktop/Interaction.fs b/src/LivingVillage.Desktop/Interaction.fs
index 6f175d6..da78b2e 100644
--- a/src/LivingVillage.Desktop/Interaction.fs
+++ b/src/LivingVillage.Desktop/Interaction.fs
@@ -52,19 +52,50 @@ module M5Interaction =
let private intentName (intent: DialogueIntent) : string =
match intent with
- | SmallTalk -> "SMALLTALK"
- | AskHelp -> "ASK HELP"
- | OfferTrade -> "OFFER TRADE"
- | Joke -> "JOKE"
- | Apologize -> "APOLOGIZE"
- | Provoke -> "PROVOKE"
+ | SmallTalk -> "闲聊"
+ | AskHelp -> "求助"
+ | OfferTrade -> "提议交易"
+ | Joke -> "讲笑话"
+ | Apologize -> "道歉"
+ | Provoke -> "挑衅"
let private needName (kind: NeedKind) : string =
match kind with
- | HungerNeed -> "HUNGER"
- | EnergyNeed -> "ENERGY"
- | SocialNeed -> "SOCIAL"
- | MoneyNeed -> "MONEY"
+ | HungerNeed -> "饥饿"
+ | EnergyNeed -> "精力"
+ | SocialNeed -> "社交"
+ | MoneyNeed -> "金钱"
+
+ let private itemName (item: ItemKind) : string =
+ match item with
+ | Food -> "食物"
+
+ let private responseName (response: DialogueResponse) : string =
+ match response with
+ | Friendly -> "友好"
+ | Helpful -> "乐于助人"
+ | Bargaining -> "讨价还价"
+ | Amused -> "开心"
+ | Forgiving -> "原谅"
+ | Hostile -> "敌意"
+ | Reserved -> "保留"
+ | Refused -> "拒绝"
+ | Offended -> "恼怒"
+
+ let private dialogueFailureName (failure: DialogueFailure) : string =
+ match failure with
+ | DialogueTargetNotFound -> "找不到目标"
+ | DialogueTargetOutOfRange -> "目标不在范围内"
+ | DialogueTargetUnavailable -> "目标暂时无法互动"
+
+ let private tradeFailureName (failure: TradeFailure) : string =
+ match failure with
+ | BuyerNotFound -> "找不到买家"
+ | SellerNotFound -> "找不到卖家"
+ | SameParticipant -> "买家和卖家不能是同一人"
+ | InvalidQuantity -> "数量必须大于零"
+ | OutOfStock -> "库存不足"
+ | InsufficientFunds -> "资金不足"
let private npcNumber (NpcId id) = id
@@ -84,7 +115,7 @@ module M5Interaction =
Chronicle = Sim.annalText next
Status = sprintf "dialogue intent=%A response=%A" outcome.Intent outcome.Response }
| DialogueRejected(failure, next) ->
- next, { view with Status = sprintf "dialogue rejected: %A" failure }
+ next, { view with Status = sprintf "dialogue rejected: %s" (dialogueFailureName failure) }
let apply (command: M5Command) (world: World) (view: M5View) : World * M5View =
match command with
@@ -127,6 +158,19 @@ module M5Interaction =
Status = sprintf "chronicle entries=%d" world.Annals.Length }
| ClosePanel -> world, { view with Panel = WorldPanel; Menu = None; Status = "panel closed" }
+ let tradeResultText (request: TradeRequest) (quote: float32 option) (result: TradeResult) : string =
+ match result with
+ | TradeSucceeded _ ->
+ match quote with
+ | Some unitPrice ->
+ sprintf "交易成功:%s ×%d,报价 %.2f,总价 %.2f"
+ (itemName request.Item)
+ request.Quantity
+ unitPrice
+ (unitPrice * float32 request.Quantity)
+ | None -> sprintf "交易成功:%s ×%d" (itemName request.Item) request.Quantity
+ | TradeRejected(failure, _) -> sprintf "交易失败:%s" (tradeFailureName failure)
+
let panelName (view: M5View) : string =
match view.Panel with
| WorldPanel -> "world"
@@ -137,9 +181,9 @@ module M5Interaction =
let titleText (view: M5View) : string =
match view.Panel with
- | WorldPanel -> "E INTERACT | Q OBSERVE | TAB NEEDS | C CHRONICLE | L RELATIONS"
- | NeedsPanel -> "NEEDS | TAB CLOSE | Q OBSERVE | C CHRONICLE"
- | ObservationPanel -> "OBSERVATION | TAB NEEDS | C CHRONICLE | ESC CLOSE"
+ | WorldPanel -> "E 互动 | Q 观察 | TAB 需求 | C 年鉴 | L 关系"
+ | NeedsPanel -> "需求 | TAB 关闭 | Q 观察 | C 年鉴"
+ | ObservationPanel -> "观察 | TAB 需求 | C 年鉴 | ESC 关闭"
| DialoguePanel ->
match view.Menu with
| Some menu ->
@@ -147,38 +191,83 @@ module M5Interaction =
menu.Options
|> List.mapi (fun index intent -> sprintf "%d %s" (index + 1) (intentName intent))
|> String.concat " | "
- sprintf "DIALOGUE TARGET=%A | %s | ESC CLOSE" menu.Target options
- | None -> "DIALOGUE RECORDED | E INTERACT | ESC CLOSE"
- | ChroniclePanel -> "CHRONICLE | TAB NEEDS | Q OBSERVE | ESC CLOSE"
+ sprintf "互动:村民 %d | %s | ESC 关闭" (npcNumber menu.Target) options
+ | None -> "互动已记录 | E 互动 | ESC 关闭"
+ | ChroniclePanel -> "年鉴 | TAB 需求 | Q 观察 | ESC 关闭"
+
+ let private statusLabel (status: string) : string =
+ match status with
+ | "ready" -> "准备就绪"
+ | "needs panel open" -> "需求面板已打开"
+ | "needs panel closed" -> "需求面板已关闭"
+ | "panel closed" -> "面板已关闭"
+ | value when value.StartsWith("dialogue target=") -> "请选择对话方式"
+ | value when value.StartsWith("dialogue intent=") -> "对话完成"
+ | value when value.StartsWith("dialogue rejected:") ->
+ let reason = value.Substring("dialogue rejected:".Length).Trim()
+ if String.IsNullOrWhiteSpace reason then "对话失败" else sprintf "对话失败:%s" reason
+ | value when value.StartsWith("observation") -> "观察完成"
+ | value when value.StartsWith("chronicle") -> "年鉴已打开"
+ | value when value.StartsWith("no dialogue") -> "当前没有可用对话"
+ | value when value.StartsWith("invalid dialogue") -> "对话选项无效"
+ | value when value.StartsWith("no nearby") -> "附近没有可互动村民"
+ | _ -> "状态已更新"
+
+ let private annalLine (entry: AnnalEntry) : string =
+ match entry.Kind with
+ | DialogueAnnal outcome ->
+ sprintf "对话:村民 %d 对村民 %d 使用%s,回应%s"
+ (npcNumber outcome.Actor)
+ (npcNumber outcome.Target)
+ (intentName outcome.Intent)
+ (responseName outcome.Response)
+ | RumorAnnal rumor ->
+ sprintf "传闻:村民 %d 传给村民 %d,传播 %d 层"
+ (npcNumber rumor.Narrator)
+ (npcNumber rumor.Receiver)
+ rumor.Depth
+ | TradeAnnal event ->
+ match event.Kind with
+ | TradeEvent(buyer, seller, item, quantity, unitPrice) ->
+ sprintf "交易:村民 %d 从村民 %d 获得%s ×%d,单价 %.2f"
+ (npcNumber buyer)
+ (npcNumber seller)
+ (itemName item)
+ quantity
+ unitPrice
+ | _ -> "交易记录"
- let private chronicleLines (text: string) : string list =
- text.Split([| '\n' |], System.StringSplitOptions.RemoveEmptyEntries) |> Array.toList
+ let private chronicleLines (world: World) : string list =
+ world.Annals
+ |> List.rev
+ |> List.truncate 10
+ |> List.map annalLine
let panelLines (world: World) (view: M5View) : string list =
- let status = if String.IsNullOrWhiteSpace view.Status then "READY" else view.Status.ToUpperInvariant()
+ let status = if String.IsNullOrWhiteSpace view.Status then "准备就绪" else statusLabel view.Status
match view.Panel with
| WorldPanel ->
- [ "M5 VILLAGE"
- "E INTERACT"
- "Q OBSERVE"
- "TAB NEEDS"
- "C CHRONICLE"
- "L RELATIONS"
+ [ "村庄"
+ "E 互动"
+ "Q 观察"
+ "TAB 需求"
+ "C 年鉴"
+ "L 关系"
status ]
| NeedsPanel ->
match view.Needs with
- | None -> [ "NEEDS"; "NO DATA"; status ]
+ | None -> [ "需求"; "暂无数据"; status ]
| Some panel ->
let topNeed =
panel.Ranked
|> List.tryHead
- |> Option.map (fun score -> sprintf "TOP %s" (needName score.Kind))
- |> Option.defaultValue "TOP NONE"
- [ "NEEDS"
- sprintf "HUNGER %.0f" panel.Needs.Hunger
- sprintf "ENERGY %.0f" panel.Needs.Energy
- sprintf "SOCIAL %.0f" panel.Needs.Social
- sprintf "MONEY %.0f" panel.Needs.Money
+ |> Option.map (fun score -> sprintf "最紧迫:%s" (needName score.Kind))
+ |> Option.defaultValue "暂无紧迫需求"
+ [ "需求"
+ sprintf "饥饿 %.0f" panel.Needs.Hunger
+ sprintf "精力 %.0f" panel.Needs.Energy
+ sprintf "社交 %.0f" panel.Needs.Social
+ sprintf "金钱 %.0f" panel.Needs.Money
topNeed
status ]
| ObservationPanel ->
@@ -191,21 +280,21 @@ module M5Interaction =
|> List.tryHead
|> Option.map (fun score -> score.Urgency)
|> Option.defaultValue 0.0f
- [ sprintf "NPC %d U %.0f" (npcNumber observation.Id) urgency
- sprintf "H %.0f E %.0f S %.0f M %.0f"
+ [ sprintf "村民 %d 紧迫度 %.0f" (npcNumber observation.Id) urgency
+ sprintf "饥饿 %.0f 精力 %.0f 社交 %.0f 金钱 %.0f"
observation.Needs.Hunger
observation.Needs.Energy
observation.Needs.Social
observation.Needs.Money ])
- "OBSERVATION" :: (if rows.IsEmpty then [ "NO NPC IN RANGE" ] else rows) @ [ status ]
+ "观察" :: (if rows.IsEmpty then [ "范围内暂无村民" ] else rows) @ [ status ]
| DialoguePanel ->
match view.Menu with
- | None -> [ "DIALOGUE RECORDED"; status ]
+ | None -> [ "互动已记录"; status ]
| Some menu ->
let options =
menu.Options
|> List.mapi (fun index intent -> sprintf "%d %s" (index + 1) (intentName intent))
- [ sprintf "DIALOGUE NPC %d" (npcNumber menu.Target) ] @ options @ [ "ESC CLOSE"; status ]
+ [ sprintf "互动:村民 %d" (npcNumber menu.Target) ] @ options @ [ "ESC 关闭"; status ]
| ChroniclePanel ->
- let entries = chronicleLines view.Chronicle |> List.truncate 10
- "CHRONICLE" :: (if entries.IsEmpty then [ "NO ENTRIES" ] else entries) @ [ status ]
+ let entries = chronicleLines world
+ "年鉴" :: (if entries.IsEmpty then [ "暂无记录" ] else entries) @ [ status ]