namespace LivingVillage.Kernel /// Occupation system first slice (design §1/§2, docs/design/occupation-system-design.md). /// Data lives beside the player only — NPCs are never occupied. /// Nothing here runs inside Sim.step; WorldBootstrap stamps the profile onto the /// world's avatar at creation time, which keeps the numeric state path untouched. module Occupation = type Kind = | Farmer | Fisher | Peddler | Scholar /// Chinese labels, shared for a later UI slice (农夫 / 渔夫 / 货郎 / 书生). let nameOf (kind: Kind) : string = match kind with | Farmer -> "农夫" | Fisher -> "渔夫" | Peddler -> "货郎" | Scholar -> "书生" let saveToken (kind: Kind) : string = match kind with | Farmer -> "farmer" | Fisher -> "fisher" | Peddler -> "peddler" | Scholar -> "scholar" let all : Kind list = [ Farmer; Fisher; Peddler; Scholar ] type Profile = { Kind: Kind InitialMoney: int InitialEnergy: float32 InitialInventory: (Sim.ItemKind * int) list } /// Initial values straight from the approved design §1 table. let profileOf (kind: Kind) : Profile = match kind with | Farmer -> { Kind = Farmer; InitialMoney = 60; InitialEnergy = 100.0f; InitialInventory = [ Sim.Food, 12 ] } | Fisher -> { Kind = Fisher; InitialMoney = 45; InitialEnergy = 95.0f; InitialInventory = [ Sim.Food, 8; Sim.Fish, 3 ] } | Peddler -> { Kind = Peddler; InitialMoney = 90; InitialEnergy = 100.0f; InitialInventory = [ Sim.Food, 6; Sim.Spice, 4 ] } | Scholar -> { Kind = Scholar; InitialMoney = 70; InitialEnergy = 100.0f; InitialInventory = [ Sim.Food, 10; Sim.Scroll, 2 ] } // ---- 每日任务(design §3):Kernel 侧纯函数片,不进 Sim.step 数值路径 ---- type TaskState = | Offered | Active | Done | Failed /// 首期任务池 = 农夫3(送粮/帮工/翻土)+ 渔夫3(夜捕/卖渔/询价) /// + P15 补齐货郎2(收货/转卖)+ 书生2(观察记事/论理),design §3 表。 type TaskTemplateId = | DeliverGrain | HelpWork | TillSoil | NightCatch | SellFish | MarketInquiry | BuyGoods | Resell | ObserveNotes | ReasonDebate let taskNameOf (template: TaskTemplateId) : string = match template with | DeliverGrain -> "送粮" | HelpWork -> "帮工" | TillSoil -> "翻土" | 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 -> [ 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 TargetNpc: Sim.NpcId option TargetTile: (int * int) option OfferedTick: int64 DueTick: int64 State: TaskState } /// 完成判定信号(design §3:走既有事件通道)。 /// Dialogued 来自 DialogueEvent(chooseDialogue);Traded 来自 TradeEvent, /// 字段序 = 卖方/物品/数量(玩家递出即 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 接线单落地)。 let satisfiesCompletion (task: DailyTask) (signal: TaskSignal) : bool = match task.TemplateId, signal with | DeliverGrain, Traded (seller, item, quantity) -> seller = Sim.playerId && item = Sim.Food && quantity >= 1 | HelpWork, Dialogued npc -> match task.TargetNpc with | Some target -> target = npc | None -> true | TillSoil, ArrivedAt (tileX, tileY) -> match task.TargetTile with | Some target -> target = (tileX, tileY) | None -> true | 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;其余状态原样返回(终态不可复活)。 let acceptTask (task: DailyTask) : DailyTask = if task.State = Offered then { task with State = Active } else task /// 仅 Active 下命中完成信号才置 Done;Offered 须先 accept,Done/Failed 终态原样返回。 let applySignal (signal: TaskSignal) (task: DailyTask) : DailyTask = if task.State = Active && satisfiesCompletion task signal then { task with State = Done } else task /// 逾期判负:DueTick 之后仍未完成 → Failed;Done/Failed 原样返回。 let expireAt (nowTick: int64) (task: DailyTask) : DailyTask = if nowTick > task.DueTick && task.State <> Done && task.State <> Failed then { task with State = Failed } else task let dayIndexOf (tick: int64) : int64 = tick / Sim.ticksPerDay /// hash(seed ^ occupationSeed, dayIndex) —— 与 Rng.fs splitmix 同源(禁 System.Random)。 /// 输出只依赖三元组,与调用顺序无关:seed 异或 occupationSeed 后连续推进 /// dayIndex+1 次 splitmix,末次输出即当日哈希。 let taskHash (seed: uint64) (occupationSeed: uint64) (dayIndex: int64) : uint64 = let mutable state = Rng.ofSeed (seed ^^^ occupationSeed) let mutable out = 0UL if dayIndex >= 0L then for _ in 1L .. dayIndex + 1L do let value, next = Rng.nextUInt64 state out <- value state <- next out /// 当日任务生成:池内确定性抽 1 条;空池 → None。 /// TargetNpc/TargetTile 首期留 None(通配),OfferedTick/DueTick 覆盖整日。 let dailyTaskOf (seed: uint64) (occupationSeed: uint64) (dayIndex: int64) (kind: Kind) : DailyTask option = match taskPoolOf kind with | [] -> None | pool -> let index = int (taskHash seed occupationSeed dayIndex % uint64 pool.Length) let offered = dayIndex * Sim.ticksPerDay Some { TemplateId = pool.[index] TargetNpc = None TargetTile = None OfferedTick = offered 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,暂未启用)。 /// Backpack = 玩家背包侧车(design §2.4-1):开局 = profile.InitialInventory 的纯数据副本, /// 不进 `Sim.World`、不动 `Sim.step` 数值路径;键值对保持声明顺序以稳定序列化。 type State = { Profile: Profile TaskToken: int Today: DailyTask option StoryStage: int Backpack: (Sim.ItemKind * int) list /// 连续完成天数(P55):当日任务 Done 时 +1;跨日刷新时若上一日已完成则保留,否则清零。 Streak: int } let stateOf (kind: Kind) : State = { Profile = profileOf kind TaskToken = 0 Today = None StoryStage = 0 Backpack = (profileOf kind).InitialInventory Streak = 0 } /// 背包内某物品数量(纯函数;无则 0)。 let backpackQuantity (item: Sim.ItemKind) (state: State) : int = state.Backpack |> List.sumBy (fun (kind, quantity) -> if kind = item then quantity else 0) /// 物品中文名(HUD 短反馈共用;与 PlayerTrade 口径一致)。 let itemNameOf (item: Sim.ItemKind) : string = match item with | Sim.Food -> "食物" | Sim.Fish -> "鱼" | Sim.Spice -> "香料" | Sim.Scroll -> "卷轴" /// 背包发放物品:同物品合并数量,新物品追加尾部(保持声明顺序,稳定序列化)。 let grantItem (item: Sim.ItemKind) (quantity: int) (backpack: (Sim.ItemKind * int) list) : (Sim.ItemKind * int) list = if quantity <= 0 then backpack else let mutable found = false let updated = backpack |> List.map (fun (kind, current) -> if kind = item then found <- true kind, current + quantity else kind, current) if found then updated else updated @ [ item, quantity ] // ---- P55 经济闭环:设计口径报酬 + 连续完成天数 ---- // 报酬 = `docs/design/occupation-system-design.md` §3 任务表已写明的**完成结果物品** // (仅两处有物品+数量:夜捕「得 Fish×2」、送粮「交给村民 Food×1」);其余模板无写明物品 // → 不发、只保留既有年鉴/关系反馈。禁止发明金额或新物品。 /// 完成任务的设计口径物品奖励(无写明 → None)。 let rewardOf (template: TaskTemplateId) : (Sim.ItemKind * int) option = match template with | NightCatch -> Some(Sim.Fish, 2) | DeliverGrain -> Some(Sim.Food, 1) | _ -> None /// 对职业状态施加任务信号:Active 命中完成 → Done + Streak+1 + 发放设计写明物品。 /// 返回(新状态, 本次发放的物品奖励 option)。 let applySignalToState (signal: TaskSignal) (state: State) : State * (Sim.ItemKind * int) option = match state.Today with | Some task when task.State = Active && satisfiesCompletion task signal -> let reward = rewardOf task.TemplateId let backpack = match reward with | Some(item, quantity) -> grantItem item quantity state.Backpack | None -> state.Backpack { state with Today = Some { task with State = Done } Streak = state.Streak + 1 Backpack = backpack }, reward | _ -> state, None // ---- 既有事件通道 → 任务信号桥(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 advanceTaskWithReward (event: Sim.InteractionEvent) (state: State) : State * (Sim.ItemKind * int) option = match signalOfInteraction event with | Some signal -> applySignalToState signal state | None -> state, None /// 用一条既有事件推进当日任务(Offered 须先 acceptTask;终态不可复活)。 /// 无职业 / 无当日任务 / 非玩家事件时原样返回。 let advanceTask (event: Sim.InteractionEvent) (state: State) : State = fst (advanceTaskWithReward event state) /// 跨日刷新职业状态(P55):重算当日任务,并按「上一日是否已完成」保留/清零连续天数。 /// 同日刷新原样保留 Streak(含 Done 终态),避免每帧刷新把 streak 冲掉。 let refreshState (seed: uint64) (occupationSeed: uint64) (kind: Kind) (nowTick: int64) (state: State) : State = let today = refreshToday seed occupationSeed kind nowTick state.Today let sameDay = match state.Today with | Some task -> dayIndexOf task.OfferedTick = dayIndexOf nowTick | None -> false let todayDay = dayIndexOf nowTick let continuing = match state.Today with | Some task -> // 同日刷新保留;跨日时仅当上一日「紧邻且已完成」才保留(跳过多日 = 漏完成 → 清零)。 sameDay || (task.State = Done && dayIndexOf task.OfferedTick + 1L = todayDay) | None -> false { state with Today = today; Streak = (if continuing then state.Streak else 0) } // ---- 报价偏置(design §2/§3):Kernel 纯函数旁路,不改 Sim.fs 数值口径 ---- // 4 位定点整数基点(1/10000)表达偏置,避免浮点漂移;结果钳在 [0.95, 1.05]。 // 货郎:买卖双向有利(买压价/卖抬价 ±3.00%);书生:解读行情小幅加成 ±1.00%; // 农夫/渔夫/无职业:恒 1.0,与旧行为逐位一致。 let private peddlerTradeBasisPoints = 300 let private scholarTradeBasisPoints = 100 let quoteBiasBasisPoints (kind: Kind option) (playerIsBuyer: bool) : int = match kind with | Some Peddler -> if playerIsBuyer then -peddlerTradeBasisPoints else peddlerTradeBasisPoints | Some Scholar -> if playerIsBuyer then -scholarTradeBasisPoints else scholarTradeBasisPoints | _ -> 0 let quoteBiasOf (kind: Kind option) (playerIsBuyer: bool) : float32 = float32 (10000 + quoteBiasBasisPoints kind playerIsBuyer) / 10000.0f /// Sim.quotePrice 的输出经职业偏置映射;None(无报价)原样穿透。 let biasedQuote (kind: Kind option) (playerIsBuyer: bool) (baseQuote: float32 option) : float32 option = baseQuote |> Option.map (fun price -> price * quoteBiasOf kind playerIsBuyer) // ---- 对话分支倾向(design §5 / P-d):村民对玩家职业的确定性回应偏差 ---- // 全部为纯函数重映射:基底仍走 Sim.responseFor 的人格口径,偏差不引入随机。 // 无职业(None)= 原回应逐字节不变。 // 规则(各自只作用于签名意图,其余意图原样穿透): // 农夫 AskHelp:Refused → Helpful(老实人家,求助不落空); // 渔夫 SmallTalk:Reserved → Friendly(聊的是天气鱼汛,冷场变热络); // 货郎 SmallTalk:Reserved → Bargaining(三句不离买卖,更易进讨价还价); // 书生 Joke:完全覆盖为 Reserved(读过书的人讲笑话,众人反而拘谨)—— // 与 personality 相反时采用完全覆盖语义而非概率偏向,确定性可测(design §5 反差感)。 let private remapResponse (kind: Kind) (intent: Sim.DialogueIntent) (baseResponse: Sim.DialogueResponse) : Sim.DialogueResponse = match kind, intent, baseResponse with | Farmer, Sim.AskHelp, Sim.Refused -> Sim.Helpful | Fisher, Sim.SmallTalk, Sim.Reserved -> Sim.Friendly | Peddler, Sim.SmallTalk, Sim.Reserved -> Sim.Bargaining | Scholar, Sim.Joke, _ -> Sim.Reserved | _ -> baseResponse let biasResponse (kind: Kind option) (intent: Sim.DialogueIntent) (personality: Sim.Personality) : Sim.DialogueResponse = let baseResponse = Sim.responseFor intent personality match kind with | None -> baseResponse | Some kind -> remapResponse kind intent baseResponse // ---- P65 职业切片五:货郎「打听行情」(OfferTrade 意图)读既有 World.Rumors 池 ---- // 纯判定 + 文案:不新增任何状态、不动 Sim.fs 数值路径。行情短答只由既有谣言工作集 // (玩家对话已写入,最新在前)按同一套新鲜度/可信度口径派生;同一世界同一次查询可重现。 let private inquiryPlaces = [| "东头"; "西头"; "南头"; "北头"; "桥头"; "巷口" |] let private inquiryGoods = [| "盐巴"; "米价"; "鱼价"; "布价"; "茶价"; "柴价" |] /// 同一套口径(对齐 Sim.latestRumorFor):在新鲜窗口内、Tick 不晚于当前、 /// 且衰减后强度不低于门槛的谣言,才算「可打听的行情」。无接收者过滤(打听面向全村)。 let freshReliableRumors (world: Sim.World) : Sim.RumorEvent list = world.Rumors |> List.filter (fun rumor -> world.Tick - rumor.Tick <= Sim.rumorFreshnessTicks && rumor.Tick <= world.Tick && Sim.rumorStrengthAt world.Tick rumor >= Sim.rumorMinimumStrength) /// 打听命中的目标谣言:衰减强度最大者;平手取 Tick 更大者,再平手取 RumorId 更大者。 /// 全序比较保证同输入完全相同结果(无随机、无哈希遍历顺序依赖)。 let inquiryRumor (world: Sim.World) : Sim.RumorEvent option = let stronger (candidate: Sim.RumorEvent) (best: Sim.RumorEvent) = let candidateStrength = Sim.rumorStrengthAt world.Tick candidate let bestStrength = Sim.rumorStrengthAt world.Tick best if candidateStrength > bestStrength then true elif candidateStrength < bestStrength then false elif candidate.Tick <> best.Tick then candidate.Tick > best.Tick else candidate.Id > best.Id match freshReliableRumors world with | [] -> None | first :: rest -> Some(rest |> List.fold (fun best candidate -> if stronger candidate best then candidate else best) first) let private movementOf (strength: float32) : string = if strength >= 0.75f then "涨了两文" elif strength >= 0.5f then "涨了一文" elif strength >= 0.25f then "还稳当" else "有些回落" /// 由命中谣言派生一条确定性行情短答:新鲜度前缀 + 地点 + 货品 + 涨落。 /// 地点/货品索引只由 (RumorId, Depth) 决定,涨落由衰减强度分档,全程整数/纯函数。 let private inquiryTextOf (world: Sim.World) (rumor: Sim.RumorEvent) : string = let (Sim.RumorId id) = rumor.Id let index = int ((id + int64 rumor.Depth) % 6L) let place = inquiryPlaces.[index] let good = inquiryGoods.[(index + 3) % 6] let recency = if world.Tick - rumor.Tick < Sim.ticksPerDay then "今日" else "近日" sprintf "%s%s的%s%s" recency place good (movementOf (Sim.rumorStrengthAt world.Tick rumor)) /// P65:货郎「提议交易」(= 打听行情/询价)时产出的行情短答。 /// 仅 `Some Peddler + OfferTrade + 命中可用谣言` 返回 Some;其余组合 None, /// 让非货郎/无职业路径逐字节退回既有 `dialogue intent=...` 文案。 /// 与 `MarketInquiry` 当日任务共用同一「一次玩家对话事件」判定条件(见 satisfiesCompletion)。 let marketInquiryText (kind: Kind option) (intent: Sim.DialogueIntent) (world: Sim.World) : string option = match kind, intent with | Some Peddler, Sim.OfferTrade -> inquiryRumor world |> Option.map (inquiryTextOf world) | _ -> None // ---- 轻剧情线(design §5 / P-d):三段式 接任务 → 走访 → 收束 ---- // 纯年鉴/对话驱动:每次玩家对话至多推进一段,产出只有(StoryStage 推进 + 年鉴摘要文本), // 由调用方写入世界。全部输入 = (seed, 世界 tick, 关系状态, 职业状态),纯函数、确定性。 // 开线日沿用每日任务的 (seed, profession, day) splitmix 哈希模式。 module Story = type Ending = | Warm | Plain | Distant /// 剧情线种子:与每日任务种子同源异值,两两不撞(见测试)。 let storySeedOf (kind: Kind) : uint64 = match kind with | Farmer -> 0x5555555555555555UL | Fisher -> 0x6666666666666666UL | Peddler -> 0x7777777777777777UL | Scholar -> 0x8888888888888888UL let lineNameOf (kind: Kind) : string = match kind with | Farmer -> "雨水与年成" | Fisher -> "水位与收成" | Peddler -> "新货的路子" | Scholar -> "庙前的争论" /// StoryStage 语义:0 未开始 / 1 已接任务 / 2 走访中 / 3 已收束。 let stageNameOf (stage: int) : string = match stage with | 1 -> "已接任务" | 2 -> "走访中" | 3 -> "已收束" | _ -> "未开始" /// 开线日 = hash(seed ^ storySeed, day 0) mod 3 + 2,落在 [2, 4]。 let startDayOf (seed: uint64) (kind: Kind) : int64 = (int64 (taskHash seed (storySeedOf kind) 0L % 3UL)) + 2L /// 玩家与村民的关系均值:与 Sim.relationMatrix 同口径(衰减 valence), /// 但只统计认识玩家的村民(有 Chatted/Dialogue 记忆者),无一认识 = 0(中性)。 let relationMeanToPlayer (world: Sim.World) : float32 = let player = Sim.playerId let mutable total = 0.0f let mutable known = 0 for npc in world.Npcs do let mutable valence = 0.0f let mutable touched = false for e in npc.Mind.Memory do if e.Tick <= world.Tick then let involvesPlayer = match e.Kind with | Sim.Chatted partner -> partner = player | Sim.Dialogue (actor, _, _) -> actor = player | _ -> false if involvesPlayer then touched <- true valence <- valence + e.Valence * Sim.relationDecayWeight world.Tick e.Tick if touched then known <- known + 1 total <- total + valence if known = 0 then 0.0f else total / float32 known /// 结局分档:均值 ≥ 0.3 热络;[0, 0.3) 平常;< 0 淡漠。纯阈值,无随机。 let endingOf (mean: float32) : Ending = if mean >= 0.3f then Warm elif mean >= 0.0f then Plain else Distant let endingTextOf (kind: Kind) (ending: Ending) : string = match kind, ending with | Farmer, Warm -> "雨水与年成有了结果:乡邻都说雨水匀,今年差不了" | Farmer, Plain -> "雨水与年成有了结果:雨水尚可,年成将就,照旧耕作" | Farmer, Distant -> "雨水与年成有了结果:问不出准信,年成只能看天" | Fisher, Warm -> "水位与收成有了结果:乡邻说得真切,水位稳了,收成坏不了" | Fisher, Plain -> "水位与收成有了结果:众说平常,水位尚可,收成将就" | Fisher, Distant -> "水位与收成有了结果:问了几家都淡淡的,水情还得自己看" | Peddler, Warm -> "新货的路子有了结果:乡邻热心指路,新货不愁卖" | Peddler, Plain -> "新货的路子有了结果:说法平常,先小量试卖" | Peddler, Distant -> "新货的路子有了结果:各家都淡淡的,路子还得自己闯" | Scholar, Warm -> "庙前的争论有了结果:各说各理,事有分晓" | Scholar, Plain -> "庙前的争论有了结果:众说纷纭,暂无定论" | Scholar, Distant -> "庙前的争论有了结果:无人理会,先记在册上" /// §3 身份条目:开局写入年鉴(如「以打渔为生」),不带主语、沿年鉴短句风格。 let identitySummaryOf (kind: Kind) : string = match kind with | Farmer -> "以耕田为生" | Fisher -> "以打渔为生" | Peddler -> "以贩货为生" | Scholar -> "以读书为生" /// 用一次玩家对话推进剧情线:同输入两次结果一致。 /// 返回(新状态, 应写入年鉴的摘要文本 option);无门控命中时原样返回。 /// 结局文本在收束瞬间按当时关系均值定格,由调用方连同世界存档(年鉴快照)持久化。 let onDialogue (seed: uint64) (state: State) (world: Sim.World) : State * string option = let kind = state.Profile.Kind let day = dayIndexOf world.Tick let startDay = startDayOf seed kind match state.StoryStage with | 0 when day >= startDay -> { state with StoryStage = 1 }, Some (sprintf "接了件事:%s,得去村里多问问" (lineNameOf kind)) | 1 when day >= startDay + 1L -> { state with StoryStage = 2 }, Some "走访了几户人家,改日再问" | 2 when day >= startDay + 2L -> let ending = endingOf (relationMeanToPlayer world) { state with StoryStage = 3 }, Some (endingTextOf kind ending) | _ -> state, None