summaryrefslogtreecommitdiff
path: root/docs/profession-system-design.md
blob: efb3517021799d219b17a40efacc2885bd17066b (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
# 职业系统设计(江南水乡 · 社会模拟主线扩展)

状态:**设计稿,待用户拍板;未写任何实现代码。**
范围:在现有社会模拟主线(需求 / 关系 / 谣言 / 年鉴 / 交易)之上加入开局职业选择。本文档只定义设计、数据结构草案与验收标准;实现走后续独立小步 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 管线。