summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop
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.Desktop
parent80c99bc9251e4ca6304761ef9bd88f2724642278 (diff)
downloadliving-village-a59a4bbe1ab45adb29d1367995230d3f09847d9b.tar.gz
p55: 当日任务经济闭环——设计口径物品报酬入账(夜捕鱼×2/送粮食物×1, 其余不发) + 连续完成天数(streak 跨日保留/漏完成清零) + v3 streak 尾段与 save(None) 兼容 + LV_P55_SHOT 证据
Diffstat (limited to 'src/LivingVillage.Desktop')
-rw-r--r--src/LivingVillage.Desktop/Game.fs90
-rw-r--r--src/LivingVillage.Desktop/Interaction.fs4
-rw-r--r--src/LivingVillage.Desktop/PlayerTrade.fs28
-rw-r--r--src/LivingVillage.Desktop/TaskRuntime.fs36
-rw-r--r--src/LivingVillage.Desktop/WorldBootstrap.fs10
5 files changed, 133 insertions, 35 deletions
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index 284c246..8162351 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -373,6 +373,13 @@ type LivingVillageGame() as this =
let mutable p54Step = 0
let mutable p54PendingName = ""
let mutable p54Pending = false
+ // P55 证据钩子:LV_P55_SHOT=1 时以农夫真实开局,跳到第 3 日(当日任务=送粮),
+ // 走「面板(待接受) → 接受 → 真实卖出 Food(送粮完成,设计口径 +食物×1)→ 面板(已完成)」,
+ // 打印 streak/背包/反馈,供 analyze 脚本核验报酬入账与连续天数。
+ let p55ShotMode = Environment.GetEnvironmentVariable("LV_P55_SHOT") = "1"
+ let mutable p55Step = 0
+ let mutable p55PendingName = ""
+ let mutable p55Pending = false
let mutable autoplayFrames = 0
let mutable flowStep = 0
let mutable flowHold = 0
@@ -1307,6 +1314,77 @@ type LivingVillageGame() as this =
p54PendingName taskText (M5Interaction.panelName m5View) m5View.Status bag
p54Step <- p54Step + 1
+ /// P55 证据:跳到第 3 日(送粮)→ 面板(待接受) → 接受 → 真实卖出 Food(完成 +食物×1)。
+ member private this.PrepareP55Shot () =
+ let setDay3Noon () =
+ let tick = 3L * Sim.ticksPerDay + M6Presentation.resolveStartTick (Some 12.0) false
+ world <- { world with Tick = tick; Time = float tick * Sim.dtSeconds }
+ let ask (command: M5Command) =
+ let nextWorld, nextView = M5Interaction.apply command world m5View
+ world <- nextWorld
+ m5View <- nextView
+ if not p55Pending then
+ match p55Step with
+ | 0 ->
+ setDay3Noon ()
+ // 用真实跨日刷新把当日任务推进到第 3 日(seed 42/农夫 = 送粮)。
+ match m5View.Task with
+ | Some state -> m5View <- { m5View with Task = Some(WorldBootstrap.refreshOccupationToday 42UL state world.Tick) }
+ | None -> ()
+ let npc = world.Npcs.[0]
+ world <- { world with Avatar = { world.Avatar with Pos = npc.Pos } }
+ ask M5Command.ShowTaskPanel
+ this.CenterCamera()
+ p55PendingName <- "p55-task-offered"
+ p55Pending <- true
+ | 1 ->
+ ask M5Command.AcceptTask
+ p55PendingName <- "p55-task-active"
+ p55Pending <- true
+ | 2 ->
+ ask M5Command.ClosePanel
+ ask M5Command.Interact
+ ask M5Command.SellToTarget
+ p55PendingName <- "p55-reward"
+ p55Pending <- true
+ | 3 ->
+ ask M5Command.ShowTaskPanel
+ p55PendingName <- "p55-task-done"
+ p55Pending <- true
+ | _ ->
+ printfn "p55-shot=done"
+ this.Exit()
+
+ /// P55 证据:落盘并打印模板/状态/streak/背包/反馈。
+ member private this.CaptureP55Shot () =
+ if p55Pending then
+ p55Pending <- false
+ System.IO.Directory.CreateDirectory recordDirectory |> ignore
+ this.SaveBackBuffer(sprintf "%s/%s.png" recordDirectory p55PendingName)
+ let stateText (state: Occupation.TaskState) =
+ match state with
+ | Occupation.Offered -> "offered"
+ | Occupation.Active -> "active"
+ | Occupation.Done -> "done"
+ | Occupation.Failed -> "failed"
+ let templateText, taskState, streak =
+ match m5View.Task with
+ | None -> "none", "none", 0
+ | Some state ->
+ (match state.Today with
+ | Some task -> Occupation.taskNameOf task.TemplateId, stateText task.State, state.Streak
+ | None -> "none", "none", state.Streak)
+ let bag =
+ match m5View.Task with
+ | None -> "-"
+ | Some state ->
+ state.Backpack
+ |> List.map (fun (item, quantity) -> sprintf "%A=%d" item quantity)
+ |> String.concat ";"
+ printfn "p55-shot=%s template=%s state=%s streak=%d panel=%s status=%s bag=[%s]"
+ p55PendingName templateText taskState streak (M5Interaction.panelName m5View) m5View.Status bag
+ p55Step <- p55Step + 1
+
/// P53 证据:真实录制提示出现/消失序列(远处 → 靠近 → 互动 → 关闭+离开)。
member private this.PrepareP53Recording () =
let setHour (hour: float) =
@@ -1844,6 +1922,16 @@ type LivingVillageGame() as this =
else this.DispatchMenuInput Confirm
elif menu.Page = Playing then
this.PrepareP54Shot()
+ elif p55ShotMode then
+ if menu.Page = MainMenu then
+ this.DispatchMenuInput Confirm
+ elif menu.Page = OccupationSelect then
+ let targetIndex =
+ MenuState.occupationOptions |> List.findIndex (fun option -> option = Some Occupation.Farmer)
+ if menu.Selected <> targetIndex then this.DispatchMenuInput Down
+ else this.DispatchMenuInput Confirm
+ elif menu.Page = Playing then
+ this.PrepareP55Shot()
elif menu.Page = Playing then
this.UpdatePlaying gameTime kb pressed pressedAny
elif menuShotMode then
@@ -2263,6 +2351,8 @@ type LivingVillageGame() as this =
this.CaptureP53Recording()
if p54ShotMode && menu.Page = Playing then
this.CaptureP54Shot()
+ if p55ShotMode && menu.Page = Playing then
+ this.CaptureP55Shot()
/// 启动画面:分层天色 + 屋脊/水面剪影 + 标题淡入上浮 + 跳过提示,全部为帧计数纯函数。
member private this.DrawSplash() =
diff --git a/src/LivingVillage.Desktop/Interaction.fs b/src/LivingVillage.Desktop/Interaction.fs
index 68f90d7..75575dd 100644
--- a/src/LivingVillage.Desktop/Interaction.fs
+++ b/src/LivingVillage.Desktop/Interaction.fs
@@ -442,8 +442,8 @@ module M5Interaction =
let outcome =
if command = BuyFromTarget then PlayerTrade.buyFood view.Task menu.Target world
else PlayerTrade.sellFirst view.Task menu.Target world
- // P54:交易完成当日任务时把短中文反馈一并带出。
- let taskMessage = TaskRuntime.completedMessage view.Task outcome.Occupation
+ // P54/P55:交易完成当日任务时把(含奖励的)短中文反馈一并带出。
+ let taskMessage = TaskRuntime.completedMessageWithReward view.Task outcome.Occupation outcome.Reward
outcome.World,
{ view with
Panel = WorldPanel
diff --git a/src/LivingVillage.Desktop/PlayerTrade.fs b/src/LivingVillage.Desktop/PlayerTrade.fs
index 540445d..407536c 100644
--- a/src/LivingVillage.Desktop/PlayerTrade.fs
+++ b/src/LivingVillage.Desktop/PlayerTrade.fs
@@ -23,15 +23,12 @@ module PlayerTrade =
Succeeded: bool
/// 成交时的职业偏置后单位价(失败为 None)。
UnitPrice: float32 option
+ /// P55:本次交易若完成当日任务,设计口径发放的物品奖励。
+ Reward: (ItemKind * int) option
/// 面向 HUD 的短中文反馈。
Message: string }
- let private itemName (item: ItemKind) : string =
- match item with
- | Food -> "食物"
- | Fish -> "鱼"
- | Spice -> "香料"
- | Scroll -> "卷轴"
+ let private itemName (item: ItemKind) : string = Occupation.itemNameOf item
let private quantityOf (item: ItemKind) (inventory: Map<ItemKind, int>) : int =
inventory |> Map.tryFind item |> Option.defaultValue 0
@@ -112,12 +109,13 @@ module PlayerTrade =
| _ -> sprintf "trade tick=%d" world.Tick }
Sim.appendAnnal entry { world with Events = world.Events @ [ event ] }
- /// P54:交易事件同时推进当日任务(Offered 须先接受;判定走 Kernel 纯函数)。
- let private advanceOccupation (event: InteractionEvent) (occupation: Occupation.State) : Occupation.State =
- Occupation.advanceTask event occupation
+ /// P54/P55:交易事件同时推进当日任务(Offered 须先接受;判定走 Kernel 纯函数),
+ /// 并把任务的物品奖励一并返回。
+ let private advanceOccupation (event: InteractionEvent) (occupation: Occupation.State) : Occupation.State * (ItemKind * int) option =
+ Occupation.advanceTaskWithReward event occupation
let private failure (state: Occupation.State option) (world: World) (message: string) : Outcome =
- { World = world; Occupation = state; Succeeded = false; UnitPrice = None; Message = message }
+ { World = world; Occupation = state; Succeeded = false; UnitPrice = None; Reward = None; Message = message }
/// 玩家向 `counterparty` 买入 `quantity` 件 `item`。
let buy
@@ -163,13 +161,14 @@ module PlayerTrade =
|> (fun w -> { w with Avatar = { w.Avatar with Mind = avatarMind } })
|> replaceNpc updatedSeller
|> recordTrade tradeEvent
- let nextOccupation =
- { occupation with Backpack = changeBackpack item quantity occupation.Backpack }
+ let nextOccupation, reward =
+ ({ occupation with Backpack = changeBackpack item quantity occupation.Backpack })
|> advanceOccupation tradeEvent
{ World = nextWorld
Occupation = Some nextOccupation
Succeeded = true
UnitPrice = Some unitPrice
+ Reward = reward
Message = sprintf "买入%s ×%d,单价 %.2f" (itemName item) quantity unitPrice }
/// 玩家向 `counterparty` 卖出 `quantity` 件 `item`(须在背包内)。
@@ -216,13 +215,14 @@ module PlayerTrade =
|> (fun w -> { w with Avatar = { w.Avatar with Mind = avatarMind } })
|> replaceNpc updatedBuyer
|> recordTrade tradeEvent
- let nextOccupation =
- { occupation with Backpack = changeBackpack item -quantity occupation.Backpack }
+ let nextOccupation, reward =
+ ({ occupation with Backpack = changeBackpack item -quantity occupation.Backpack })
|> advanceOccupation tradeEvent
{ World = nextWorld
Occupation = Some nextOccupation
Succeeded = true
UnitPrice = Some unitPrice
+ Reward = reward
Message = sprintf "卖出%s ×%d,单价 %.2f" (itemName item) quantity unitPrice }
/// HUD 入口:向 `counterparty` 买 1 件食物。
diff --git a/src/LivingVillage.Desktop/TaskRuntime.fs b/src/LivingVillage.Desktop/TaskRuntime.fs
index 75675aa..efc57f3 100644
--- a/src/LivingVillage.Desktop/TaskRuntime.fs
+++ b/src/LivingVillage.Desktop/TaskRuntime.fs
@@ -9,10 +9,21 @@ open LivingVillage.Kernel
/// (其内部是 `satisfiesCompletion`),绝不按日历索引硬编码计数。
module TaskRuntime =
- let private completionMessage (before: Occupation.DailyTask option) (after: Occupation.DailyTask option) : string option =
+ /// 完成短中文反馈:有设计口径物品奖励时带「+物品×数量」。
+ let completionText (task: Occupation.DailyTask) (reward: (Sim.ItemKind * int) option) : string =
+ let headline = sprintf "任务完成:%s" (Occupation.taskNameOf task.TemplateId)
+ match reward with
+ | Some(item, quantity) -> sprintf "%s +%s×%d" headline (Occupation.itemNameOf item) quantity
+ | None -> headline
+
+ let private completionMessage
+ (before: Occupation.DailyTask option)
+ (after: Occupation.DailyTask option)
+ (reward: (Sim.ItemKind * int) option)
+ : string option =
match before, after with
| Some previous, Some current when previous.State = Occupation.Active && current.State = Occupation.Done ->
- Some(sprintf "任务完成:%s" (Occupation.taskNameOf current.TemplateId))
+ Some(completionText current reward)
| _ -> None
/// 接受当日任务(Offered → Active);无任务 / 已在进行 / 终态时无变化、无反馈。
@@ -23,19 +34,17 @@ module TaskRuntime =
Some(sprintf "已接受任务:%s" (Occupation.taskNameOf task.TemplateId))
| _ -> state, None
- /// 用一条既有世界事件推进当日任务;Active → Done 时返回短中文反馈。
+ /// 用一条既有世界事件推进当日任务;Active → Done 时返回(含奖励的)短中文反馈。
let advanceWithEvent (event: Sim.InteractionEvent) (state: Occupation.State) : Occupation.State * string option =
let before = state.Today
- let next = Occupation.advanceTask event state
- next, completionMessage before next.Today
+ let next, reward = Occupation.advanceTaskWithReward event state
+ next, completionMessage before next.Today reward
/// 用一条已折叠的任务信号(到达瓦片 / 观察 / 夜在水边)推进当日任务。
let advanceWithSignal (signal: Occupation.TaskSignal) (state: Occupation.State) : Occupation.State * string option =
- match state.Today with
- | Some task ->
- let after = Occupation.applySignal signal task
- { state with Today = Some after }, completionMessage (Some task) (Some after)
- | None -> state, None
+ let before = state.Today
+ let next, reward = Occupation.applySignalToState signal state
+ next, completionMessage before next.Today reward
/// 折叠多条信号(后面的不覆盖已得到的完成反馈)。
let foldSignals (signals: Occupation.TaskSignal list) (state: Occupation.State) : Occupation.State * string option =
@@ -58,7 +67,12 @@ module TaskRuntime =
if isNight && nearWater then yield Occupation.NightAtWater ]
/// 比较两份职业状态,返回本次推进是否让当日任务完成(供交易等路径复用)。
- let completedMessage (before: Occupation.State option) (after: Occupation.State option) : string option =
+ let completedMessageWithReward
+ (before: Occupation.State option)
+ (after: Occupation.State option)
+ (reward: (Sim.ItemKind * int) option)
+ : string option =
completionMessage
(before |> Option.bind (fun state -> state.Today))
(after |> Option.bind (fun state -> state.Today))
+ reward
diff --git a/src/LivingVillage.Desktop/WorldBootstrap.fs b/src/LivingVillage.Desktop/WorldBootstrap.fs
index 7be24a7..e75855f 100644
--- a/src/LivingVillage.Desktop/WorldBootstrap.fs
+++ b/src/LivingVillage.Desktop/WorldBootstrap.fs
@@ -25,15 +25,9 @@ module WorldBootstrap =
Today = Occupation.dailyTaskOf seed (Occupation.occupationSeedOf kind) (Occupation.dayIndexOf nowTick) kind }
/// 跨日刷新当日任务;同日(含 Done/Failed 终态)原样保留。
+ /// P55:经 Kernel `refreshState`,跨日时按上一日是否已完成保留/清零连续天数。
let refreshOccupationToday (seed: uint64) (state: Occupation.State) (nowTick: int64) : Occupation.State =
- { state with
- Today =
- Occupation.refreshToday
- seed
- (Occupation.occupationSeedOf state.Profile.Kind)
- state.Profile.Kind
- nowTick
- state.Today }
+ Occupation.refreshState seed (Occupation.occupationSeedOf state.Profile.Kind) state.Profile.Kind nowTick state
/// 开局身份年鉴(design §3):带职业建世界后写入一条 `StoryAnnal`「以…为生」,
/// 形成个人传记线。Game 与测试共用同一口径。