summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Kernel
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-29 00:24:08 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-29 00:24:08 +0800
commita59a4bbe1ab45adb29d1367995230d3f09847d9b (patch)
tree9570b3c7168a6252920138e380e2a9d9cb6f9acf /src/LivingVillage.Kernel
parent80c99bc9251e4ca6304761ef9bd88f2724642278 (diff)
downloadliving-village-a59a4bbe1ab45adb29d1367995230d3f09847d9b.tar.gz
p55: 当日任务经济闭环——设计口径物品报酬入账(夜捕鱼×2/送粮食物×1, 其余不发) + 连续完成天数(streak 跨日保留/漏完成清零) + v3 streak 尾段与 save(None) 兼容 + LV_P55_SHOT 证据
Diffstat (limited to 'src/LivingVillage.Kernel')
-rw-r--r--src/LivingVillage.Kernel/Occupation.fs88
-rw-r--r--src/LivingVillage.Kernel/WorldSave.fs18
2 files changed, 97 insertions, 9 deletions
diff --git a/src/LivingVillage.Kernel/Occupation.fs b/src/LivingVillage.Kernel/Occupation.fs
index 45ef428..66ed4f2 100644
--- a/src/LivingVillage.Kernel/Occupation.fs
+++ b/src/LivingVillage.Kernel/Occupation.fs
@@ -201,19 +201,75 @@ module Occupation =
TaskToken: int
Today: DailyTask option
StoryStage: int
- Backpack: (Sim.ItemKind * int) list }
+ 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 }
+ 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 之间互动的副作用不派任务。
@@ -225,12 +281,34 @@ module Occupation =
| 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 =
- match state.Today, signalOfInteraction event with
- | Some task, Some signal -> { state with Today = Some(applySignal signal task) }
- | _ -> 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]。
diff --git a/src/LivingVillage.Kernel/WorldSave.fs b/src/LivingVillage.Kernel/WorldSave.fs
index 3e11810..29c8d18 100644
--- a/src/LivingVillage.Kernel/WorldSave.fs
+++ b/src/LivingVillage.Kernel/WorldSave.fs
@@ -617,6 +617,11 @@ module WorldSave =
| Some task -> writeDailyTask tokens task
| None -> ()
writeBackpack tokens state.Backpack
+ // P55:连续完成天数仅在 > 0 时写入尾段,旧 v3(无此段)读回默认 0,
+ // 且缺省(无 streak)存档的文本布局与 P51/P52 完全一致。
+ if state.Streak > 0 then
+ add tokens "streak"
+ addInt tokens state.Streak
| None -> ()
String.Join("|", tokens)
@@ -660,7 +665,8 @@ module WorldSave =
TaskToken = task
Today = None
StoryStage = stage
- Backpack = (Occupation.profileOf kind).InitialInventory })
+ Backpack = (Occupation.profileOf kind).InitialInventory
+ Streak = 0 })
| _ -> ()
let tick = readInt64 reader "tick"
let time = readFloat reader "time"
@@ -679,20 +685,24 @@ module WorldSave =
let tail =
let mutable task : Occupation.DailyTask option = None
let mutable backpack : (ItemKind * int) list option = None
+ let mutable streak = 0
while reader.Remaining > 0 do
match reader.Take("occupation.tail") with
| "task-today" -> task <- Some(readDailyTask reader)
| "backpack" -> backpack <- Some(readBackpack reader)
+ | "streak" -> streak <- readInt reader "occupation.streak"
| _ -> invalid "trailing save data"
- task, backpack
+ task, backpack, streak
+ let tailTask, tailBackpack, tailStreak = tail
if reader.Remaining <> 0 then
invalid "trailing save data"
let occupation =
occupationOption
|> Option.map (fun state ->
{ state with
- Today = fst tail
- Backpack = snd tail |> Option.defaultValue state.Backpack })
+ Today = tailTask
+ Backpack = tailBackpack |> Option.defaultValue state.Backpack
+ Streak = tailStreak })
Ok
({ Tick = tick
Time = time