diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-21 16:27:12 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-21 16:27:12 +0800 |
| commit | 87bdfe273555c5ae5f698b3b89fa6c39d84d0c33 (patch) | |
| tree | 221d6ac4dff653f444e876f6df0cf021bfb2cd11 | |
| parent | 18977f1147a3778b90caad48a53e760c79349175 (diff) | |
| download | living-village-87bdfe273555c5ae5f698b3b89fa6c39d84d0c33.tar.gz | |
docs: 职业系统设计稿 occupation-system-design
| -rw-r--r-- | docs/design/occupation-system-design.md | 129 |
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 冻结产物与长测。 |
