blob: 163f5474de3bc30287a2e82a1bf7e60584c4f5ea (
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
|
# 职业系统设计(江南水乡 · 社会模拟主线扩展)
状态:**已实现(P11-P15 + P31,P-a..d 全部落 main);本稿为第一版设计草稿,已被第二版实现稿
`docs/design/occupation-system-design.md` 取代,保留作历史记录。** 差异对照见文末「实现对照」。
范围:在现有社会模拟主线(需求 / 关系 / 谣言 / 年鉴 / 交易)之上加入开局职业选择。本文档只定义设计、数据结构草案与验收标准;实现走后续独立小步 TDD 单。
## 1. 设计目标
1. 开局有身份感:职业决定玩家起点的经济位、社会位与日常节奏,而不是只改一个数字。
2. 深化现有模拟主线:每个职业都通过**已有系统**(需求曲线、关系网络、谣言传播、年鉴记录、交易行情)产生不同的运行时表现,不新增平行系统。
3. 确定性可验收:同 seed 同职业选择两次开局逐字节一致;同 seed 不同职业的差异必须可解释、可测试。
4. 中文优先:所有职业名、任务名、对话分支、年鉴条目全部中文,走既有 CJK 图集管线(新增用字同步 cjk-glyphs.txt 并重生成图集)。
## 2. 开局职业候选(4 个,全部江南背景)
| 职业 | 定位 | 初始差异(草案) | 一句话体验 |
|---|---|---|---|
| 渔夫 | 生计型 | 初始金钱中下;饥饿/精力消耗略快(劳作);清晨时段行动加成;开局获得"渔获"可交易物 | 靠水吃水,日子看天 |
| 货郎 | 交易型 | 初始金钱最高、初始库存最少;交易报价有小幅优势(更接近行情上限);认识更多村民(初始关系网更广) | 走街串巷,贱买贵卖 |
| 药童 | 社交型 | 初始金钱低;精力消耗慢;对话成功率小幅加成;谣言可信度加成(别人更愿意信他传的话) | 采药问诊,消息灵通 |
| 书生 | 叙事型 | 初始金钱中;各属性均衡;年鉴条目更详细(多一句话注脚);对话分支中多一个"讲道理"选项倾向 | 读过书的人,看事不同 |
取舍说明:刻意不做"战斗/等级"类职业;全部职业差异都落在现有数据字段(Needs、Inventory、关系初值、对话/谣言参数)上,避免引入新的运行时机制。
## 3. 与现有社会模拟主线的接合点
| 系统 | 接合方式(全部复用现有机制) |
|---|---|
| 需求(Needs) | 职业修改初始值与每日消耗系数(如渔夫饥饿消耗 ×1.15、药童精力消耗 ×0.9),不改 Needs 结构 |
| 关系(Relations) | 职业决定初始关系网:货郎开局与 3 名村民认识(关系值小正数),其余职业只与近邻认识;职业身份影响关系事件的可选回应 |
| 谣言(Rumors) | 药童传播谣言可信度加成;书生听到的谣言在年鉴多记来源线索;传播算法本身不变 |
| 年鉴(Annals) | 开局写入一条"身份条目"(如"村民 0 以打渔为生");职业任务完成/失败照常进年鉴,形成个人传记线 |
| 交易(Trade) | 货郎报价系数微调;渔夫有渔获可卖;行情、库存、成交逻辑不变 |
## 4. 每日任务池(示例,每职业 3 类,每日确定性抽取 1 条)
抽取规则:`hash(seed, day, profession) mod 任务数`,种子化、可重放,不用墙钟。
- **渔夫**:「清晨去河埠打渔」(在指定时段到达河岸瓦片附近)→ 产出渔获 ×N;「把渔获卖给村民 X」→ 触发交易事件;「听村民 Y 说说行情」→ 触发对话事件。
- **货郎**:「收购村民 X 的食物」;「把货物带到石桥边卖掉」;「和村民 Y 攀谈打听新品」。
- **药童**:「送药给村民 X」(赠予式交易,关系小加成);「听村民 Y 的烦心事」;「在天黑前回到家」。
- **书生**:「观察村庄记一笔」(执行一次观察);「与村民 X 论理」(特殊对话分支);「翻阅年鉴回顾昨日」。
任务状态机:`提出 → 进行中 → 完成 / 失败(过期)`,完成写年鉴并给小额需求/关系反馈;失败也写年鉴(允许"不顺的日子",不做惩罚性数值扣减)。
## 5. NPC 对话分支与剧情线影响
- 对话意图仍为现有 6 种(闲聊/求助/提议交易/讲笑话/道歉/挑衅);职业**不新增意图**,而是给部分意图加职业限定回应:
- 村民对"渔夫"的闲聊回应可能聊天气与鱼汛;
- 村民对"货郎"的提议交易更容易进入讨价还价回应;
- 村民对"药童"的求助更可能给出友好回应;
- 村民对"书生"的讲笑话更可能给出保留回应(反差感,叙事味)。
- 轻剧情线(每职业一条,纯年鉴/对话驱动,无过场动画):
- 渔夫线「水位与收成」:连续数日渔获走低 → 可向村民打听原因 → 年鉴结案(三种结局文案,由当时关系均值决定);
- 货郎线「新货的路子」、药童线「时疫传闻」、书生线「庙前的争论」同理,均为"接任务 → 走访 → 收束"三段式。
- 所有分支产出仍是 `DialogueEvent / AnnalEntry / 关系变化`,复用现有记录与确定性管线。
## 6. Kernel 数据结构草案(不实现,仅草案)
```fsharp
// 新增(Sim.fs 或独立 Profession.fs,命名空间 LivingVillage.Kernel)
type ProfessionKind =
| Fisher // 渔夫
| Peddler // 货郎
| Apothecary // 药童
| Scholar // 书生
type DailyTask =
{ TaskId: int
Kind: TaskKind // VisitTile / TradeWith / TalkTo / Observe
TargetNpc: NpcId option
TargetTile: TilePosition option
DueTick: int64
State: TaskState } // Offered / Active / Done / Failed
type ProfessionProfile =
{ Kind: ProfessionKind
NeedsDrift: Needs // 每日消耗系数(乘法,1.0 为无修正)
InitialRelations: (NpcId * float32) list
DialogueBias: Map<DialogueIntent, DialogueResponse> // NPC 回应倾向修正
RumorCredibility: float32 }
// World 扩展(v2 字段,全部 Option/默认值,见 §7)
type World = {
...
Profession: ProfessionState option // v2 新增;旧存档反序列化后 = None
}
and ProfessionState =
{ Profile: ProfessionProfile
TodayTask: DailyTask
StoryStage: int } // 轻剧情线进度 0..3
```
要点:`ProfessionState` 挂在 World 上而不是 Avatar 上,便于以后给 NPC 也配职业(NPC 职业超出本单范围,仅留扩展空间)。
## 7. 存档 v2 兼容策略
1. 存档头部加 `version` 字段(v1 = 现有存档,v2 = 含职业);读取时按 version 分派。
2. v1 存档读入后 `Profession = None`:游戏内表现为"无职业村民",一切现状不变;UI 显示"无名之辈"式中性文案或直接不显示职业区块(实现单拍板,倾向后者)。
3. v1 存档允许"补选职业"一次(进入游戏后弹一次选择页),补选后升级为 v2 保存;也可继续以无职业游玩。
4. 新开局默认必选职业(标题→新游戏→职业选择页,UI 复用现有菜单页机制)。
5. 迁移与读写各配确定性测试:v1 读入→原样另存为 v1 字节不变;v1+补选→v2 往返一致。
## 8. 确定性要求
1. 职业选择是**世界输入**的一部分,等价于 seed 之外的第二个种子分量:`hash(seed, profession)`. 同 (seed, profession) 两次开局,`World` 结构逐字节一致(复用现有 `initialWorld` 确定性测试框架,新增参数化用例)。
2. 每日任务抽取用 `(seed, profession, day)` 纯函数哈希,禁止依赖随机数状态推进次序以外的任何外部输入。
3. 职业对 Needs/谣言/交易的修正全部是**纯函数系数**,不引入可变全局量;长期批验(M3 冻结管线口径)不受影响——冻结产物不含职业系统。
4. 同 seed 不同职业的存档互不兼容是预期行为(职业是世界事实),存档头部记录 profession 以便校验。
## 9. 分阶段实现与验收标准(供拍板后拆单)
| 阶段 | 内容 | 验收标准 |
|---|---|---|
| P-a | Kernel:ProfessionKind/Profile/World v2 字段 + 存档 v1/v2 读写 | build/test 全绿;v1 存档读取回归;同 (seed, profession) 两次初始 World 逐字节一致;v1 往返字节不变 |
| P-b | 开局选择页(标题→新游戏→职业页)+ 初始差异生效 | Xvfb 截图:职业选择页 1 张、四种职业开局 HUD/状态差异各 1 张;UI 文案全部走 CJK 图集无缺字 |
| P-c | 每日任务池 + 年鉴接入 | 抽取函数确定性测试(同输入同任务);完成/失败写年鉴回归;真实运行录屏一段 |
| P-d | 对话分支倾向 + 四条轻剧情线 | 回应分布测试(职业偏差可观测且确定);剧情线三种结局由关系均值确定性决定;视频证据 |
## 10. 明确不做(本设计边界)
- 不做职业专属技能树、等级、战斗、装备系统。
- 不做 NPC 职业化(仅预留数据空间)。
- 不改冻结 M3 管线口径;长测基准世界不含职业系统。
- 不引入外部素材与付费内容;UI 沿用现有图集与 CJK 管线。
## 11. 实现对照(2026-09-22 补记,来源见 docs/维护说明.md)
实现按第二版稿 `docs/design/occupation-system-design.md` 落地(P-a..d,Feats `cf21488` /
`f883070` / `27f0ca0` / `c348da2` / `98b6b21` / `0222501` / `e1d8d45` / `60bbc83` / `50875bb`
merge `6ce29c5`;P31 `6c2f180` / `0539bba` / `9821b54` / `4f51232` 合并经 `b3d1cb3`)。
与本稿草案的主要差异:
- 职业集合:药童 Apothecary 未做,第四职业落为农夫 Farmer(数据结构同步改名
`ProfessionKind -> Occupation.Kind`、`ProfessionState -> Occupation.State`)。
- 存档:草案 §7 的「v2 = 含职业」改为实际 **v3**,且 v3 仅在挂职业时产出——无职业世界保存
与 v2 逐字节一致(比草案更保守)。
- §4 每日任务池按第二版 §3 模板表实现,任务完成判定走既有事件通道,未新增运行时机制。
- §5 对话分支与轻剧情线由 P31 完成:`Occupation.biasResponse`(书生 Joke 完全覆盖为
Reserved)、三段式剧情线状态机 + 三种关系阈值结局 + 年鉴 `StoryAnnal` + 身份条目。
- «视频证据»以 4 张真实渲染截图替代:`docs/evidence/p31-story-*.stage2.png`(未入库,
复现命令见 维护说明.md P31 节)。
|