blob: dfb6f09e8e3ea4dc54388d5c38593ade3f9fb491 (
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
|
# 职业系统设计(occupation-system)
状态:**设计稿,等用户拍板;本单零代码。** 实现按 §6 拆单。
## 1. 开局职业选择
开局在标题→新游戏之后插入一页职业选择(四选一),每个职业包 = 6 个字段:
| 字段 | 农夫 | 渔夫 | 货郎 | 书生 |
|---|---|---|---|---|
| `needs` 初始 Money | 60 | 45 | 90 | 70 |
| `needs` 初始 Energy | 100 | 95 | 100 | 100 |
| 初始物品(Inventory) | Food×12 | Food×8、Fish×3 | Food×6、Spice×4 | Food×10、Scroll×2 |
| 每日任务模板 | 送粮/帮工/翻土 | 夜捕/卖渔/修网 | 收货/转卖/打听行情 | 观察记事/论理/读年鉴 |
| 关系影响点 | 助人时 friendly 加成更强 | 渔讯话题闲聊加成 | 提议交易响应偏向讨价还价但成功率高 | 讲笑话易得保留(反差,叙事味) |
| 谣言影响点 | 无 | 无 | 传闻单价波动感知 | 听到的谣言年鉴多记来源 |
设计原则:所有差异都落在**现有**数据结构(Needs/Inventory/Relations/Rumor/Annals/Dialogue)上,不新增运行时机制;职业参数是「世界生成时一次性写入」的静态系数,`Sim.step` 数值路径不感知职业。
与 `docs/maintenance-notes`(docs/维护说明.md)的 953775FA digest 口径直接相关:本设计的接线**不改 Sim.step 主数值路径**(见 §2),因此 worldDigest 规范化后的基线应继续返回 953775FA…。
## 2. Kernel 接线点(不动 Sim.step 数值路径)
### 数据结构(Kernel 新增 `Occupation.fs`,World 结构本身零改动)
```fsharp
// LivingVillage.Kernel/Occupation.fs —— 新文件,不改 Sim.fs
type OccupationKind = Farmer | Fisher | Peddler | Scholar
type OccupationProfile =
{ Kind: OccupationKind
InitialMoney: float32
InitialEnergy: float32
InitialInventory: (ItemKind * int) list
TradeBias: float32 // 货郎 >1, 农渔 <0, 书生 0
RumorCredibility: float32 // 药童未启用:全部 1.0 占位
DialogueBias: (DialogueIntent * DialogueResponse list) list // 话题倾角
TaskPool: TaskTemplateId list }
type DailyTask =
{ TemplateId: TaskTemplateId
TargetNpc: NpcId option
TargetTile: TilePosition option
OfferedTick: int64
DueTick: int64
State: TaskState } // Offered | Active | Done | Failed
type OccupationState =
{ Profile: OccupationProfile
Today: DailyTask option
StoryStage: int } // 轻剧情线 0..3
```
### 注入方式(三选一,推荐 A)
- **A(推荐)**:`WorldBootstrap`(Desktop)根据用户选择与 seed 生成 `OccupationState`,随 v3 存档保存。`Sim.step` 数值路径不读它;只有:
1) `WorldBootstrap.initialWorldN` 多传一个可选参数(默认 None → 现状不变,digest 不变);
2) 交互层(Desktop/M5)读 `world.Occupation` 决定可用对话枝与任务显示;
3) 交易/谣言仅乘一个**整数偏移的小系数**在 Desktop 层抬/降报价——同 base 数字路径,只是上层乘数。
- B:Sim.fs 加 `match world.Occupation with` 分支 —— 被否:触碰 `Sim.step` 数值路径 = digest 变化。
- C:改 World struct 加字段 = 全 World 构造点迁移 = digest 变化 + 高风险,不采用。
### 存档 v3(迁移方案单独列出)
- v3 头 `LV_WORLD_SAVE_V3`,在 v2 的 `w h` 之后加三段:`occupationKind|taskState|storyStage`
(`occupation.kind` 缺省即 `None`,代表无职业者)。
- 读入规则:v1/v2 读到 `None`(回到完全旧行为);v3 依 kind 查表重建 `OccupationState`。
- v3 迁移仅新增三段 token,去头/重排等 digest 口径在 `PerformanceProbe.WorldDigestOfText`
中同步扩为「剥版本头 + 边界 + 职业三段」,仍保持单一规范体 —— 若此会引起新 digest,
需在 perf README 里记录一次有意的口径切换(由 leader 拍板后进行,属实现单工作,不在本设计单内)。
## 3. 每日任务
- **生成**:`hash(seed ^ occupationSeed, dayIndex) mod |taskPool|` 强制确定性 —— 与
现有 ProceduralMap 的 splitmix 同源,禁止触摸 `System.Random`。
- **完成判定**:任务模板映射为现有事件(`InteractionEvent`/交易 in/out/到达瓦片),走
`Sim.recordInteraction/Annal` 既有入口,状态机 `Offered|Active|Done|Failed`。
- **与谣言系统**:书生「听说」任务会为 `Rumor` 源头留一跳 trace(值不变,只加文案);
货郎「打听行情」用现有谣言池,不写新状态。
- **与交易系统**:货郎 TaskTemplate「转卖」复用现有 TradeRequest 管线,仅 `TradeBias`
参与报价修正(乘法系数,取 `[0.95..1.05]` 区间,用 4 位定点整数表示避免浮点漂移)。
### 任务表(首期内 4×3=12 模板,每日确定性抽 1 条)
| 模板 | 触发条件 | 完成 | 年鉴文案(演示) |
|---|---|---|---|
| 送粮(农) | 进附近民居 | 交给村民 Food×1 | 「把收成送给了村民」 |
| 帮工(农夫) | 与指定 NPC 对话 | 关系 +1 | 「帮村里人做了一天工」 |
| 翻土(农夫) | 到自家地块 | 年鉴记 1 条 | 「翻好了地」 |
| 夜捕(渔夫) | 天黑时在水边 | 得 Fish×2 | 「夜里捕到些鱼」 |
| 卖渔(渔夫) | 提议交易成功 | Money+ | 「渔获卖了出去」 |
| 询价(渔夫) | 和村民说完话 | 年鉴一条 | 「听了听鱼市行情」 |
| 收货(货郎) | 交易买入 ≥1 | 年鉴 + 库存 | 「收了批货」 |
| 转卖(货郎) | 交易卖出 ≥1 | 年鉴 + Money 上浮 | 「货出手了」 |
| 打听行情(货郎) | 听 1 条谣言 | 年鉴一条 | 「打听了几家行情」 |
| 观察记事(书生) | 执行一次观察 | 年鉴一条 | 「记下了村里的事」 |
| 论理(书生) | 与村民对话分支 | 关系微调 | 「同村人论了一回理」 |
| 读年鉴(书生) | 打开年鉴面板 | 年鉴一条 | 「翻了翻村志」 |
## 4. 与现有系统的交互(重点)
- **关系**:`Sim.recordInteraction` 已有 `kind` 与好感变化表;职业仅在「完成判定」之后
给关系**一次性小额加成**(±1),不修改关系公式本体。
- **剧情**:轻剧情线(每职业 1 条)以年鉴条目 + 对话枝呈现,无过场,3 段式(接任务→
走访→收束),收束结局文案由完成时的关系均值决定,仍走年鉴写入。
- **完成判定走旧通道**:统一经 `Sim.applyAction` / `applyDialogue` 的现有事件流,
职业只是触发者标签,不改状态机结构。
## 5. 验收标准(实现单的红绿清单)
1. **旧档兼容**:v1/v2 存档读入回归 + 新 v3 字段缺省行为与 v2 完全一致(读归读,不要吞档)。
2. **同 seed 双跑**:两次初始 `World` 结构体 + `WorldSave.save` 全文逐字节一致;且
`performance-baseline` final_digest 恒为 953775FA…(默认职业路径 digest 不得漂移)。
3. **职业差异端到端(≥3 条)**:
- 同 seed 农夫 vs 货郎:初始 Money 与 Inventory 断言不同;
- 同 seed 货郎 vs 农夫:首笔交易报价修正(TradeBias)方向相反(数值层面仅 ±小系数);
- 书生 vs 其余:年鉴条目多一句注脚(字符串断言)。
4. **中文 UI 回归**:HUD/对话枝/年鉴/职业面板在 UI 出现「农夫 渔夫 货郎 书生」字样时均有
对应 CJK 图集字(无缺字方框)——沿用 RequiredUiLabels 覆盖回归 + Xvfb 截图帧像素抽查。
5. 上面四条全部走「先写测试(红)→ 实现(绿)」小步 TDD。
## 6. 本设计明确不做的事
- 不做全村 NPC 职业化(仅预留数据空间,`OccupationState` 只挂在玩家侧)。
- 不做数值化剧情树/技能树/等级/装备系统。
- 不做多存档分版本同时兼容的迁移 UI(v3 升级静默一次,用户不感知)。
- 不做成就/收藏/付费与全村扩展内容。
- 不改 `Sim.step` 数值路径、不改现有 953775FA digest 口径(worldDigest 规范化已在
d10689d 落定,本实现单只允许通过「world 身份注入 + 依赖注入」挂载)。
- 不动 M3 冻结产物与长测。
|