summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Kernel/Occupation.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 22:55:01 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 22:55:01 +0800
commit0222501162d18447aa162e9c53e000bc42ccbbd9 (patch)
treed3f1c8f6bf7c0e88158664b188061c0b7fd9f74c /src/LivingVillage.Kernel/Occupation.fs
parent34c7ebc9ed8b6295ec109c177f75ba0803cc04f2 (diff)
downloadliving-village-0222501162d18447aa162e9c53e000bc42ccbbd9.tar.gz
feat(occupation): P15 补齐货郎/书生各 2 条每日任务模板 + 既有事件通道信号桥(Kernel 纯函数 TDD)
- TaskTemplateId 扩出 BuyGoods(收货)/Resell(转卖)/ObserveNotes(观察记事)/ReasonDebate(论理), taskPoolOf 货郎=[收货,转卖]、书生=[观察记事,论理],四职业池均非空。 - TaskSignal 增 Purchased(玩家买入)/Observed(观察),satisfiesCompletion 给出四模板完成判定; 新增 occupationSeedOf/refreshToday(跨日才重生成,同日含 Done/Failed 终态保留)。 - signalOfInteraction/advanceTask:仅 playerId 发起的 DialogueEvent/TradeEvent 产生信号, NPC 互动不派任务;Offered 须先 acceptTask,终态不可复活。WorldSave 模板 token 双向同步。 - 测试 83 全绿(新增 6:池非空、货郎书生逐日池内确定、模板中文名、完成信号正负、 事件桥含玩家买/卖与 NPC 忽略、既有 DialogueEvent→accept→Done 全链路、refreshToday 逐日)。 - 不动 Sim.step 数值路径;旧档 v3 模板 token 仅新增合法值,未知模板仍拒绝。
Diffstat (limited to 'src/LivingVillage.Kernel/Occupation.fs')
-rw-r--r--src/LivingVillage.Kernel/Occupation.fs61
1 files changed, 55 insertions, 6 deletions
diff --git a/src/LivingVillage.Kernel/Occupation.fs b/src/LivingVillage.Kernel/Occupation.fs
index 5c75c81..2168c4e 100644
--- a/src/LivingVillage.Kernel/Occupation.fs
+++ b/src/LivingVillage.Kernel/Occupation.fs
@@ -51,7 +51,8 @@ module Occupation =
| Done
| Failed
- /// 首期任务池 = 农夫3(送粮/帮工/翻土)+ 渔夫3(夜捕/卖渔/询价),design §3 表。
+ /// 首期任务池 = 农夫3(送粮/帮工/翻土)+ 渔夫3(夜捕/卖渔/询价)
+ /// + P15 补齐货郎2(收货/转卖)+ 书生2(观察记事/论理),design §3 表。
type TaskTemplateId =
| DeliverGrain
| HelpWork
@@ -59,6 +60,10 @@ module Occupation =
| NightCatch
| SellFish
| MarketInquiry
+ | BuyGoods
+ | Resell
+ | ObserveNotes
+ | ReasonDebate
let taskNameOf (template: TaskTemplateId) : string =
match template with
@@ -68,14 +73,26 @@ module Occupation =
| NightCatch -> "夜捕"
| SellFish -> "卖渔"
| MarketInquiry -> "询价"
+ | BuyGoods -> "收货"
+ | Resell -> "转卖"
+ | ObserveNotes -> "观察记事"
+ | ReasonDebate -> "论理"
- /// 货郎/书生模板留后续单,当前为空池 → 不生成每日任务。
let taskPoolOf (kind: Kind) : TaskTemplateId list =
match kind with
| Farmer -> [ DeliverGrain; HelpWork; TillSoil ]
| Fisher -> [ NightCatch; SellFish; MarketInquiry ]
- | Peddler
- | Scholar -> []
+ | Peddler -> [ BuyGoods; Resell ]
+ | Scholar -> [ ObserveNotes; ReasonDebate ]
+
+ /// 每职业任务流种子:与世界 seed 异或后驱动 taskHash,使四职业的每日序列互不相同。
+ /// 纯常量(splitmix 同源),禁 System.Random。
+ let occupationSeedOf (kind: Kind) : uint64 =
+ match kind with
+ | Farmer -> 0x1111111111111111UL
+ | Fisher -> 0x2222222222222222UL
+ | Peddler -> 0x3333333333333333UL
+ | Scholar -> 0x4444444444444444UL
type DailyTask =
{ TemplateId: TaskTemplateId
@@ -87,12 +104,15 @@ module Occupation =
/// 完成判定信号(design §3:走既有事件通道)。
/// Dialogued 来自 DialogueEvent(chooseDialogue);Traded 来自 TradeEvent,
- /// 字段序 = 卖方/物品/数量(玩家递出即 seller = playerId);ArrivedAt 为到达瓦片;
+ /// 字段序 = 卖方/物品/数量(玩家递出即 seller = playerId);Purchased = 玩家买入
+ /// (TradeEvent 中 playerId 为 buyer);ArrivedAt 为到达瓦片;Observed 为观察面板;
/// NightAtWater 由调用方把 isNight + 水边判定折叠后喂入。Sim.step 零感知。
type TaskSignal =
| Dialogued of Sim.NpcId
| Traded of Sim.NpcId * Sim.ItemKind * int
+ | Purchased of Sim.ItemKind * int
| ArrivedAt of int * int
+ | Observed
| NightAtWater
/// TargetNpc/TargetTile 为 None 时视为通配(指定目标由后续 Desktop 接线单落地)。
@@ -110,6 +130,10 @@ module Occupation =
| NightCatch, NightAtWater -> true
| SellFish, Traded (seller, item, quantity) -> seller = Sim.playerId && item = Sim.Fish && quantity >= 1
| MarketInquiry, Dialogued _ -> true
+ | BuyGoods, Purchased (_, quantity) -> quantity >= 1
+ | Resell, Traded (seller, _, quantity) -> seller = Sim.playerId && quantity >= 1
+ | ObserveNotes, Observed -> true
+ | ReasonDebate, Dialogued _ -> true
| _ -> false
/// Offered → Active;其余状态原样返回(终态不可复活)。
@@ -145,7 +169,7 @@ module Occupation =
state <- next
out
- /// 当日任务生成:池内确定性抽 1 条;空池(货郎/书生首期)→ None。
+ /// 当日任务生成:池内确定性抽 1 条;空池 → None。
/// TargetNpc/TargetTile 首期留 None(通配),OfferedTick/DueTick 覆盖整日。
let dailyTaskOf (seed: uint64) (occupationSeed: uint64) (dayIndex: int64) (kind: Kind) : DailyTask option =
match taskPoolOf kind with
@@ -161,6 +185,13 @@ module Occupation =
DueTick = offered + Sim.ticksPerDay - 1L
State = Offered }
+ /// 当日任务刷新:已持任务的 OfferedTick 仍落在 nowTick 所属日时原样保留(含
+ /// Done/Failed 终态,保证当日面板与存档稳定);跨日才按当日重新生成 Offered。
+ let refreshToday (seed: uint64) (occupationSeed: uint64) (kind: Kind) (nowTick: int64) (today: DailyTask option) : DailyTask option =
+ match today with
+ | Some task when dayIndexOf task.OfferedTick = dayIndexOf nowTick -> Some task
+ | _ -> dailyTaskOf seed occupationSeed (dayIndexOf nowTick) kind
+
/// 挂在玩家侧的职业状态,随存档 v3 保存(design §2)。
/// Today = 当日任务(§3);TaskToken 保留为 v3 前缀占位扩展位(恒 0,暂未启用)。
type State =
@@ -172,6 +203,24 @@ module Occupation =
let stateOf (kind: Kind) : State =
{ Profile = profileOf kind; TaskToken = 0; Today = None; StoryStage = 0 }
+ // ---- 既有事件通道 → 任务信号桥(design §3:完成判定统一走 InteractionEvent)----
+
+ /// 仅玩家发起的对话/交易才产生任务信号;NPC 之间互动的副作用不派任务。
+ /// TradeEvent 字段序 = buyer/seller/item/quantity/price,玩家为卖方 → Traded,为买方 → Purchased。
+ let signalOfInteraction (event: Sim.InteractionEvent) : TaskSignal option =
+ match event.Kind with
+ | Sim.DialogueEvent outcome when outcome.Actor = Sim.playerId -> Some(Dialogued outcome.Target)
+ | Sim.TradeEvent (_, seller, item, quantity, _) when seller = Sim.playerId -> Some(Traded(seller, item, quantity))
+ | Sim.TradeEvent (buyer, _, item, quantity, _) when buyer = Sim.playerId -> Some(Purchased(item, quantity))
+ | _ -> None
+
+ /// 用一条既有事件推进当日任务(Offered 须先 acceptTask;终态不可复活)。
+ /// 无职业 / 无当日任务 / 非玩家事件时原样返回。
+ let advanceTask (event: Sim.InteractionEvent) (state: State) : State =
+ match state.Today, signalOfInteraction event with
+ | Some task, Some signal -> { state with Today = Some(applySignal signal task) }
+ | _ -> state
+
// ---- 报价偏置(design §2/§3):Kernel 纯函数旁路,不改 Sim.fs 数值口径 ----
// 4 位定点整数基点(1/10000)表达偏置,避免浮点漂移;结果钳在 [0.95, 1.05]。
// 货郎:买卖双向有利(买压价/卖抬价 ±3.00%);书生:解读行情小幅加成 ±1.00%;