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
|
namespace LivingVillage.Kernel
/// Occupation system first slice (design §1/§2, docs/design/occupation-system-design.md).
/// Data lives beside the player only — NPCs are never occupied.
/// Nothing here runs inside Sim.step; WorldBootstrap stamps the profile onto the
/// world's avatar at creation time, which keeps the numeric state path untouched.
module Occupation =
type Kind =
| Farmer
| Fisher
| Peddler
| Scholar
/// Chinese labels, shared for a later UI slice (农夫 / 渔夫 / 货郎 / 书生).
let nameOf (kind: Kind) : string =
match kind with
| Farmer -> "农夫"
| Fisher -> "渔夫"
| Peddler -> "货郎"
| Scholar -> "书生"
let saveToken (kind: Kind) : string =
match kind with
| Farmer -> "farmer"
| Fisher -> "fisher"
| Peddler -> "peddler"
| Scholar -> "scholar"
let all : Kind list = [ Farmer; Fisher; Peddler; Scholar ]
type Profile =
{ Kind: Kind
InitialMoney: int
InitialEnergy: float32
InitialInventory: (Sim.ItemKind * int) list }
/// Initial values straight from the approved design §1 table.
let profileOf (kind: Kind) : Profile =
match kind with
| Farmer -> { Kind = Farmer; InitialMoney = 60; InitialEnergy = 100.0f; InitialInventory = [ Sim.Food, 12 ] }
| Fisher -> { Kind = Fisher; InitialMoney = 45; InitialEnergy = 95.0f; InitialInventory = [ Sim.Food, 8; Sim.Fish, 3 ] }
| Peddler -> { Kind = Peddler; InitialMoney = 90; InitialEnergy = 100.0f; InitialInventory = [ Sim.Food, 6; Sim.Spice, 4 ] }
| Scholar -> { Kind = Scholar; InitialMoney = 70; InitialEnergy = 100.0f; InitialInventory = [ Sim.Food, 10; Sim.Scroll, 2 ] }
// ---- 每日任务(design §3):Kernel 侧纯函数片,不进 Sim.step 数值路径 ----
type TaskState =
| Offered
| Active
| Done
| Failed
/// 首期任务池 = 农夫3(送粮/帮工/翻土)+ 渔夫3(夜捕/卖渔/询价),design §3 表。
type TaskTemplateId =
| DeliverGrain
| HelpWork
| TillSoil
| NightCatch
| SellFish
| MarketInquiry
let taskNameOf (template: TaskTemplateId) : string =
match template with
| DeliverGrain -> "送粮"
| HelpWork -> "帮工"
| TillSoil -> "翻土"
| NightCatch -> "夜捕"
| SellFish -> "卖渔"
| MarketInquiry -> "询价"
/// 货郎/书生模板留后续单,当前为空池 → 不生成每日任务。
let taskPoolOf (kind: Kind) : TaskTemplateId list =
match kind with
| Farmer -> [ DeliverGrain; HelpWork; TillSoil ]
| Fisher -> [ NightCatch; SellFish; MarketInquiry ]
| Peddler
| Scholar -> []
type DailyTask =
{ TemplateId: TaskTemplateId
TargetNpc: Sim.NpcId option
TargetTile: (int * int) option
OfferedTick: int64
DueTick: int64
State: TaskState }
/// 完成判定信号(design §3:走既有事件通道)。
/// Dialogued 来自 DialogueEvent(chooseDialogue);Traded 来自 TradeEvent,
/// 字段序 = 卖方/物品/数量(玩家递出即 seller = playerId);ArrivedAt 为到达瓦片;
/// NightAtWater 由调用方把 isNight + 水边判定折叠后喂入。Sim.step 零感知。
type TaskSignal =
| Dialogued of Sim.NpcId
| Traded of Sim.NpcId * Sim.ItemKind * int
| ArrivedAt of int * int
| NightAtWater
/// TargetNpc/TargetTile 为 None 时视为通配(指定目标由后续 Desktop 接线单落地)。
let satisfiesCompletion (task: DailyTask) (signal: TaskSignal) : bool =
match task.TemplateId, signal with
| DeliverGrain, Traded (seller, item, quantity) -> seller = Sim.playerId && item = Sim.Food && quantity >= 1
| HelpWork, Dialogued npc ->
match task.TargetNpc with
| Some target -> target = npc
| None -> true
| TillSoil, ArrivedAt (tileX, tileY) ->
match task.TargetTile with
| Some target -> target = (tileX, tileY)
| None -> true
| NightCatch, NightAtWater -> true
| SellFish, Traded (seller, item, quantity) -> seller = Sim.playerId && item = Sim.Fish && quantity >= 1
| MarketInquiry, Dialogued _ -> true
| _ -> false
/// Offered → Active;其余状态原样返回(终态不可复活)。
let acceptTask (task: DailyTask) : DailyTask =
if task.State = Offered then { task with State = Active } else task
/// 仅 Active 下命中完成信号才置 Done;Offered 须先 accept,Done/Failed 终态原样返回。
let applySignal (signal: TaskSignal) (task: DailyTask) : DailyTask =
if task.State = Active && satisfiesCompletion task signal then
{ task with State = Done }
else
task
/// 逾期判负:DueTick 之后仍未完成 → Failed;Done/Failed 原样返回。
let expireAt (nowTick: int64) (task: DailyTask) : DailyTask =
if nowTick > task.DueTick && task.State <> Done && task.State <> Failed then
{ task with State = Failed }
else
task
let dayIndexOf (tick: int64) : int64 = tick / Sim.ticksPerDay
/// hash(seed ^ occupationSeed, dayIndex) —— 与 Rng.fs splitmix 同源(禁 System.Random)。
/// 输出只依赖三元组,与调用顺序无关:seed 异或 occupationSeed 后连续推进
/// dayIndex+1 次 splitmix,末次输出即当日哈希。
let taskHash (seed: uint64) (occupationSeed: uint64) (dayIndex: int64) : uint64 =
let mutable state = Rng.ofSeed (seed ^^^ occupationSeed)
let mutable out = 0UL
if dayIndex >= 0L then
for _ in 1L .. dayIndex + 1L do
let value, next = Rng.nextUInt64 state
out <- value
state <- next
out
/// 当日任务生成:池内确定性抽 1 条;空池(货郎/书生首期)→ None。
/// TargetNpc/TargetTile 首期留 None(通配),OfferedTick/DueTick 覆盖整日。
let dailyTaskOf (seed: uint64) (occupationSeed: uint64) (dayIndex: int64) (kind: Kind) : DailyTask option =
match taskPoolOf kind with
| [] -> None
| pool ->
let index = int (taskHash seed occupationSeed dayIndex % uint64 pool.Length)
let offered = dayIndex * Sim.ticksPerDay
Some
{ TemplateId = pool.[index]
TargetNpc = None
TargetTile = None
OfferedTick = offered
DueTick = offered + Sim.ticksPerDay - 1L
State = Offered }
/// 挂在玩家侧的职业状态,随存档 v3 保存(design §2)。
/// Today = 当日任务(§3);TaskToken 保留为 v3 前缀占位扩展位(恒 0,暂未启用)。
type State =
{ Profile: Profile
TaskToken: int
Today: DailyTask option
StoryStage: int }
let stateOf (kind: Kind) : State =
{ Profile = profileOf kind; TaskToken = 0; Today = None; StoryStage = 0 }
// ---- 报价偏置(design §2/§3):Kernel 纯函数旁路,不改 Sim.fs 数值口径 ----
// 4 位定点整数基点(1/10000)表达偏置,避免浮点漂移;结果钳在 [0.95, 1.05]。
// 货郎:买卖双向有利(买压价/卖抬价 ±3.00%);书生:解读行情小幅加成 ±1.00%;
// 农夫/渔夫/无职业:恒 1.0,与旧行为逐位一致。
let private peddlerTradeBasisPoints = 300
let private scholarTradeBasisPoints = 100
let quoteBiasBasisPoints (kind: Kind option) (playerIsBuyer: bool) : int =
match kind with
| Some Peddler -> if playerIsBuyer then -peddlerTradeBasisPoints else peddlerTradeBasisPoints
| Some Scholar -> if playerIsBuyer then -scholarTradeBasisPoints else scholarTradeBasisPoints
| _ -> 0
let quoteBiasOf (kind: Kind option) (playerIsBuyer: bool) : float32 =
float32 (10000 + quoteBiasBasisPoints kind playerIsBuyer) / 10000.0f
/// Sim.quotePrice 的输出经职业偏置映射;None(无报价)原样穿透。
let biasedQuote (kind: Kind option) (playerIsBuyer: bool) (baseQuote: float32 option) : float32 option =
baseQuote |> Option.map (fun price -> price * quoteBiasOf kind playerIsBuyer)
|