summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/PlayerTrade.fs
blob: 407536cb80f591002d9a960869c360e71264f134 (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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
namespace LivingVillage.Desktop

open LivingVillage.Kernel
open LivingVillage.Kernel.Sim

/// P52 职业切片二:把 P51 落盘的职业背包侧车变成玩家在世界里可用的物品
/// (docs/design-professions.md §2.4-2)。
///
/// 口径:
/// - 定价仍走既有 `Sim.quotePrice`,再经 `Occupation.biasedQuote`;无职业恒等 1.0。
/// - 钱写 `world.Avatar.Mind.Needs.Money`,物品写 `Occupation.State.Backpack`(侧车)。
/// - 交易像 `Sim.trade` 一样追加 `TradeEvent` + `TradeAnnal` 并写双方记忆,
///   但**不动** `Sim.step` / 地图 / seed,也不给 Avatar 加 Inventory 字段。
///
/// 玩家不是 `world.Npcs` 成员(`playerId = NpcId -1`),而 `Sim.quotePrice` 只认
/// `Npcs`。这里把玩家侧(Avatar 需求/人格 + 背包)镜像成一个临时 `Npc` 追加进
/// 「报价世界副本」再调用 `Sim.quotePrice`;副本只用于报价,不写回真实世界。
module PlayerTrade =

    type Outcome =
        { World: World
          Occupation: Occupation.State option
          Succeeded: bool
          /// 成交时的职业偏置后单位价(失败为 None)。
          UnitPrice: float32 option
          /// P55:本次交易若完成当日任务,设计口径发放的物品奖励。
          Reward: (ItemKind * int) option
          /// 面向 HUD 的短中文反馈。
          Message: string }

    let private itemName (item: ItemKind) : string = Occupation.itemNameOf item

    let private quantityOf (item: ItemKind) (inventory: Map<ItemKind, int>) : int =
        inventory |> Map.tryFind item |> Option.defaultValue 0

    let private changeItem (item: ItemKind) (delta: int) (inventory: Map<ItemKind, int>) : Map<ItemKind, int> =
        let next = quantityOf item inventory + delta
        if next <= 0 then Map.remove item inventory else Map.add item next inventory

    let private backpackMap (backpack: (ItemKind * int) list) : Map<ItemKind, int> =
        backpack
        |> List.fold (fun acc (item, quantity) -> Map.add item (quantityOf item acc + quantity) acc) Map.empty

    /// 背包增减,保持声明顺序;归零移除,新物品追加到尾部。
    let private changeBackpack (item: ItemKind) (delta: int) (backpack: (ItemKind * int) list) : (ItemKind * int) list =
        let mutable found = false
        let updated =
            backpack
            |> List.choose (fun (kind, quantity) ->
                if kind = item then
                    found <- true
                    let next = quantity + delta
                    if next <= 0 then None else Some(kind, next)
                else
                    Some(kind, quantity))
        if not found && delta > 0 then updated @ [ item, delta ] else updated

    let private quoteWorld (world: World) (state: Occupation.State option) : World =
        let backpack = state |> Option.map (fun s -> s.Backpack) |> Option.defaultValue []
        let playerNpc =
            { Id = playerId
              Pos = world.Avatar.Pos
              Inventory = backpackMap backpack
              Mind = world.Avatar.Mind }
        { world with Npcs = Array.append world.Npcs [| playerNpc |] }

    let private requestFor (playerIsBuyer: bool) (counterparty: NpcId) (item: ItemKind) (quantity: int) : TradeRequest =
        if playerIsBuyer then
            { Buyer = playerId; Seller = counterparty; Item = item; Quantity = quantity }
        else
            { Buyer = counterparty; Seller = playerId; Item = item; Quantity = quantity }

    /// 未偏置的 `Sim.quotePrice` 结果(玩家侧镜像为临时参与者)。
    let quoteBase
        (state: Occupation.State option)
        (playerIsBuyer: bool)
        (counterparty: NpcId)
        (item: ItemKind)
        (quantity: int)
        (world: World)
        : float32 option =
        Sim.quotePrice (requestFor playerIsBuyer counterparty item quantity) (quoteWorld world state)

    /// 最终成交单价 = `Sim.quotePrice` → `Occupation.biasedQuote`。
    let quote
        (state: Occupation.State option)
        (playerIsBuyer: bool)
        (counterparty: NpcId)
        (item: ItemKind)
        (quantity: int)
        (world: World)
        : float32 option =
        let kind = state |> Option.map (fun s -> s.Profile.Kind)
        quoteBase state playerIsBuyer counterparty item quantity world
        |> Occupation.biasedQuote kind playerIsBuyer

    let private replaceNpc (npc: Npc) (world: World) : World =
        { world with Npcs = world.Npcs |> Array.map (fun current -> if current.Id = npc.Id then npc else current) }

    let private recordTrade (event: InteractionEvent) (world: World) : World =
        let entry =
            { Tick = world.Tick
              Kind = TradeAnnal event
              Summary =
                match event.Kind with
                | TradeEvent(buyer, seller, item, quantity, unitPrice) ->
                    sprintf "trade tick=%d buyer=%A seller=%A item=%A quantity=%d price=%.2f"
                        world.Tick buyer seller item quantity unitPrice
                | _ -> sprintf "trade tick=%d" world.Tick }
        Sim.appendAnnal entry { world with Events = world.Events @ [ event ] }

    /// P54/P55:交易事件同时推进当日任务(Offered 须先接受;判定走 Kernel 纯函数),
    /// 并把任务的物品奖励一并返回。
    let private advanceOccupation (event: InteractionEvent) (occupation: Occupation.State) : Occupation.State * (ItemKind * int) option =
        Occupation.advanceTaskWithReward event occupation

    let private failure (state: Occupation.State option) (world: World) (message: string) : Outcome =
        { World = world; Occupation = state; Succeeded = false; UnitPrice = None; Reward = None; Message = message }

    /// 玩家向 `counterparty` 买入 `quantity` 件 `item`。
    let buy
        (state: Occupation.State option)
        (counterparty: NpcId)
        (item: ItemKind)
        (quantity: int)
        (world: World)
        : Outcome =
        match state with
        | None -> failure state world "未选择职业"
        | Some occupation ->
            match world.Npcs |> Array.tryFind (fun npc -> npc.Id = counterparty) with
            | None -> failure state world "找不到对方"
            | Some seller when quantity <= 0 -> failure state world "数量必须大于零"
            | Some seller ->
                if quantityOf item seller.Inventory < quantity then
                    failure state world "库存不足"
                else
                    match quote (Some occupation) true counterparty item quantity world with
                    | None -> failure state world "无法报价"
                    | Some unitPrice ->
                        let total = unitPrice * float32 quantity
                        if world.Avatar.Mind.Needs.Money < total then
                            failure state world "资金不足"
                        else
                            let avatarMind =
                                { world.Avatar.Mind with
                                    Needs = { world.Avatar.Mind.Needs with Money = world.Avatar.Mind.Needs.Money - total }
                                    Memory = recordMemory world.Tick (Bought(counterparty, item, quantity, unitPrice)) world.Avatar.Mind.Memory }
                            let updatedSeller =
                                { seller with
                                    Inventory = changeItem item -quantity seller.Inventory
                                    Mind =
                                        { seller.Mind with
                                            Needs = { seller.Mind.Needs with Money = seller.Mind.Needs.Money + total }
                                            Memory = recordMemory world.Tick (Sold(playerId, item, quantity, unitPrice)) seller.Mind.Memory } }
                            let tradeEvent =
                                { Tick = world.Tick
                                  Kind = TradeEvent(playerId, counterparty, item, quantity, unitPrice) }
                            let nextWorld =
                                world
                                |> (fun w -> { w with Avatar = { w.Avatar with Mind = avatarMind } })
                                |> replaceNpc updatedSeller
                                |> recordTrade tradeEvent
                            let nextOccupation, reward =
                                ({ occupation with Backpack = changeBackpack item quantity occupation.Backpack })
                                |> advanceOccupation tradeEvent
                            { World = nextWorld
                              Occupation = Some nextOccupation
                              Succeeded = true
                              UnitPrice = Some unitPrice
                              Reward = reward
                              Message = sprintf "买入%s ×%d,单价 %.2f" (itemName item) quantity unitPrice }

    /// 玩家向 `counterparty` 卖出 `quantity` 件 `item`(须在背包内)。
    let sell
        (state: Occupation.State option)
        (counterparty: NpcId)
        (item: ItemKind)
        (quantity: int)
        (world: World)
        : Outcome =
        match state with
        | None -> failure state world "未选择职业"
        | Some occupation ->
            match world.Npcs |> Array.tryFind (fun npc -> npc.Id = counterparty) with
            | None -> failure state world "找不到对方"
            | Some buyer when quantity <= 0 -> failure state world "数量必须大于零"
            | Some buyer ->
                if Occupation.backpackQuantity item occupation < quantity then
                    failure state world "背包不足"
                else
                    match quote (Some occupation) false counterparty item quantity world with
                    | None -> failure state world "无法报价"
                    | Some unitPrice ->
                        let total = unitPrice * float32 quantity
                        if buyer.Mind.Needs.Money < total then
                            failure state world "对方资金不足"
                        else
                            let avatarMind =
                                { world.Avatar.Mind with
                                    Needs = { world.Avatar.Mind.Needs with Money = world.Avatar.Mind.Needs.Money + total }
                                    Memory = recordMemory world.Tick (Sold(counterparty, item, quantity, unitPrice)) world.Avatar.Mind.Memory }
                            let updatedBuyer =
                                { buyer with
                                    Inventory = changeItem item quantity buyer.Inventory
                                    Mind =
                                        { buyer.Mind with
                                            Needs = { buyer.Mind.Needs with Money = buyer.Mind.Needs.Money - total }
                                            Memory = recordMemory world.Tick (Bought(playerId, item, quantity, unitPrice)) buyer.Mind.Memory } }
                            let tradeEvent =
                                { Tick = world.Tick
                                  Kind = TradeEvent(counterparty, playerId, item, quantity, unitPrice) }
                            let nextWorld =
                                world
                                |> (fun w -> { w with Avatar = { w.Avatar with Mind = avatarMind } })
                                |> replaceNpc updatedBuyer
                                |> recordTrade tradeEvent
                            let nextOccupation, reward =
                                ({ occupation with Backpack = changeBackpack item -quantity occupation.Backpack })
                                |> advanceOccupation tradeEvent
                            { World = nextWorld
                              Occupation = Some nextOccupation
                              Succeeded = true
                              UnitPrice = Some unitPrice
                              Reward = reward
                              Message = sprintf "卖出%s ×%d,单价 %.2f" (itemName item) quantity unitPrice }

    /// HUD 入口:向 `counterparty` 买 1 件食物。
    let buyFood (state: Occupation.State option) (counterparty: NpcId) (world: World) : Outcome =
        buy state counterparty Food 1 world

    /// HUD 入口:卖出背包里第一件有货的物品 1 件;背包为空则失败。
    let sellFirst (state: Occupation.State option) (counterparty: NpcId) (world: World) : Outcome =
        match state with
        | None -> failure state world "未选择职业"
        | Some occupation ->
            match occupation.Backpack |> List.tryFind (fun (_, quantity) -> quantity > 0) with
            | None -> failure state world "背包里没有可卖物品"
            | Some(item, quantity) -> sell state counterparty item (min 1 quantity) world