summaryrefslogtreecommitdiff
path: root/docs/design/occupation-system-design.md
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 16:27:12 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 16:27:12 +0800
commit87bdfe273555c5ae5f698b3b89fa6c39d84d0c33 (patch)
tree221d6ac4dff653f444e876f6df0cf021bfb2cd11 /docs/design/occupation-system-design.md
parent18977f1147a3778b90caad48a53e760c79349175 (diff)
downloadliving-village-87bdfe273555c5ae5f698b3b89fa6c39d84d0c33.tar.gz
docs: 职业系统设计稿 occupation-system-design
Diffstat (limited to 'docs/design/occupation-system-design.md')
-rw-r--r--docs/design/occupation-system-design.md129
1 files changed, 129 insertions, 0 deletions
diff --git a/docs/design/occupation-system-design.md b/docs/design/occupation-system-design.md
new file mode 100644
index 0000000..dfb6f09
--- /dev/null
+++ b/docs/design/occupation-system-design.md
@@ -0,0 +1,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 冻结产物与长测。