blob: 071b3377a27f01a9d29508d0dd9ff1ce131dc5ab (
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
|
# 职业系统(Occupation)
源码:`src/LivingVillage.Kernel/Occupation.fs`(`module Occupation`)。
设计依据:`docs/design/occupation-system-design.md`、`docs/design-professions.md`。
## 职责
玩家侧职业系统纯函数:职业档案、每日任务、任务完成判定与刷新、轻剧情线、背包侧车与存档 token。
**不进入 `Sim.step` 数值路径**,也不给 NPC 加职业;职业状态由 Desktop `WorldBootstrap` 在开局注入。
## 对外 API 概览
- `type Kind = Farmer | Fisher | Peddler | Scholar`;`nameOf kind`(农夫/渔夫/货郎/书生)、
`saveToken kind`(`farmer`/`fisher`/`peddler`/`scholar`)、`all`。
- `profileOf kind : Profile = { Kind; InitialMoney; InitialEnergy; InitialInventory }`。
- 每日任务:`taskPoolOf kind`、`taskNameOf template`、`dailyTaskOf seed occupationSeed dayIndex kind`、
`dailyTaskOfWith targets seed occupationSeed dayIndex kind`(带定点)、
`acceptTask`、`applySignal`、`expireAt`、`satisfiesCompletion`、
`refreshState`、`refreshStateWith`、`refreshToday`、`refreshTodayWith`、`dayIndexOf tick`、
`taskHash seed occupationSeed dayIndex`。
- 定点:`type TaskTargets = { Npcs: Sim.NpcId list; Tiles: (int*int) list }`、
`targetNpcOf`、`targetTileOf`。
- 背包/报酬:`stateOf kind`、`backpackQuantity`、`grantItem`、`rewardOf template`、`itemNameOf item`。
- 剧情线:`module Story`(`lineNameOf kind`、`stageNameOf stage` 等)。
- `type State = { Profile; TaskToken: int; Today: DailyTask option; StoryStage: int;
Backpack: (Sim.ItemKind*int) list; Streak: int }`。
## 单位与量纲
- `dayIndex = tick / ticksPerDay`(模拟日);`OfferedTick = dayIndex * ticksPerDay`,
`DueTick = OfferedTick + ticksPerDay - 1`(当日有效)。
- `TaskState = Offered | Active | Done | Failed`;`TaskToken` 为 v3 前缀占位扩展位(当前恒 0)。
- `Backpack`:`(ItemKind*int) list`,数量 `int`;`Streak`:连续完成天数 `int >= 0`。
- 随机源:`taskHash = splitmix(seed ^^^ occupationSeed, dayIndex+1)`,与 `Rng.fs` 同源,禁用 `System.Random`。
## 不变量
1. **无职业零影响**:`dailyTaskOf` / `refreshToday` / `refreshState`(空候选)与历史行为逐字节一致,
目标恒 `None`;`Occupation` 不改变 `Sim` 状态结构。
2. **状态机单向**:`acceptTask` 仅 `Offered → Active`;`applySignal` 仅 `Active` 命中才 `Done`;
`expireAt` 只把非终态在逾期后置 `Failed`;终态不可复活。
3. **目标只影响读取它的模板**:`HelpWork` 读 `TargetNpc`、`TillSoil` 读 `TargetTile`;
其余模板目标保持 `None`(不新增完成语义)。定点在候选集合内确定性选取。
4. **候选注入**:Kernel 不感知地图/世界,候选由调用方(`WorldBootstrap.reachableTargets`)注入;
空候选 → `None`(通配),不臆造目标。
5. **序列化稳定**:`Backpack` 保持声明顺序;`Streak` 仅在 `> 0` 时写尾段(旧 v3 读回默认 0)。
## 失败分支
- 空任务池 / 空候选集合:`dailyTaskOf*` 返回 `None`、`targetNpcOf/targetTileOf` 返回 `None`(不抛异常)。
- 未 `accept` 的任务不因信号完成;`Done/Failed` 终态对 `applySignal/expireAt/acceptTask` 幂等返回原值。
- `grantItem` 对 `quantity <= 0` 原样返回;`backpackQuantity` 无物品返回 0。
- `rewardOf` 仅两处有设计口径物品(夜捕 `Fish×2`、送粮 `Food×1`),其余返回 `None`(不发明奖励)。
## 算法与性能取舍
- 每日任务选择 = `taskHash seed occupationSeed dayIndex % pool.Length`;定点用同源另一段流
(`targetNpcSalt` / `targetTileSalt` 异或后再 hash),候选先排序以消除输入顺序影响。
- 全部为纯函数、无分配敏感热路径(不在 `Sim.step` 内),成本相对 `Sim.step` 可忽略。
- 存档 token:kind 用 `saveToken` 小写英文;任务模板/状态/物品各有稳定 token(见 `world-save.md`)。
|