summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-29 08:56:42 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-29 08:56:42 +0800
commita578791b0dd8ca95fba3af6f3c2f39c816bd7e52 (patch)
treeaf71c5a7d69087a59f5f5116134c602f65960ae0 /src
parent8cfdb2aa8b19595f5fa7870c4ad3e6c4c20c421e (diff)
downloadliving-village-a578791b0dd8ca95fba3af6f3c2f39c816bd7e52.tar.gz
p67: 每日任务面板展示目标(TargetNpc/TargetTile 可见化)
- TaskRuntime.targetText/taskLine:帮工→「村民 N」、翻土→「自家地块」, 无目标时与旧任务行文案逐字节一致(不加箭头/目标名) - Interaction TaskPanel 任务行改用 taskLine(其余面板/文案不动) - 字形表新增 地 块 →(343 字,图集重生成);ChineseText.requiredUiLabels 增补 - P67TaskTargetUiTests(5 例,红先)+ PrototypeTests 计数同步 340→343 - LV_P67_SHOT 证据钩子(有目标/无目标各一帧)+ scripts/analyze-p67.py - Desktop 333/333、Kernel 119/119;digest 953775FA…7F6C ×3 PASS;未动 Kernel/Sim
Diffstat (limited to 'src')
-rw-r--r--src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj1
-rw-r--r--src/LivingVillage.Desktop.Tests/P67TaskTargetUiTests.fs74
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs2
-rw-r--r--src/LivingVillage.Desktop/Assets/cjk-glyph-atlas.pngbin89701 -> 90591 bytes
-rw-r--r--src/LivingVillage.Desktop/Assets/cjk-glyphs.txt3
-rw-r--r--src/LivingVillage.Desktop/ChineseText.fs2
-rw-r--r--src/LivingVillage.Desktop/CjkGlyphAtlas.fs2
-rw-r--r--src/LivingVillage.Desktop/Game.fs94
-rw-r--r--src/LivingVillage.Desktop/Interaction.fs2
-rw-r--r--src/LivingVillage.Desktop/TaskRuntime.fs14
10 files changed, 191 insertions, 3 deletions
diff --git a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj
index 0e121d9..2ea84b5 100644
--- a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj
+++ b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj
@@ -39,6 +39,7 @@
<Compile Include="P64EvidenceTests.fs" />
<Compile Include="P65RumorInquiryTests.fs" />
<Compile Include="P66TaskTargetTests.fs" />
+ <Compile Include="P67TaskTargetUiTests.fs" />
<Compile Include="SampleTests.fs" />
</ItemGroup>
diff --git a/src/LivingVillage.Desktop.Tests/P67TaskTargetUiTests.fs b/src/LivingVillage.Desktop.Tests/P67TaskTargetUiTests.fs
new file mode 100644
index 0000000..f6e7168
--- /dev/null
+++ b/src/LivingVillage.Desktop.Tests/P67TaskTargetUiTests.fs
@@ -0,0 +1,74 @@
+namespace LivingVillage.Desktop.Tests
+
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open LivingVillage.Kernel
+open LivingVillage.Kernel.Sim
+open LivingVillage.Desktop
+
+/// P67(设计 §2.4-3 收口后的可见化):把 P66 定点的 TargetNpc/TargetTile
+/// 展示到玩家可见的每日任务面板;无目标时文案逐字节不变;新字形(地/块/→)有覆盖断言。
+[<TestClass>]
+type P67TaskTargetUiTests () =
+
+ let world =
+ WorldBootstrap.initialWorldWithOccupation false 42UL 30 (Some(Occupation.stateOf Occupation.Farmer))
+
+ let task template npc tile state : Occupation.DailyTask =
+ { TemplateId = template
+ TargetNpc = npc
+ TargetTile = tile
+ OfferedTick = 0L
+ DueTick = 0L
+ State = state }
+
+ let farmerState = Occupation.stateOf Occupation.Farmer
+
+ let taskPanelView (state: Occupation.State option) : M5View =
+ { M5Interaction.initial with Panel = TaskPanel; Task = state; Status = "ready" }
+
+ [<TestMethod>]
+ member _.HelpWorkTargetShowsVillagerName () =
+ let target = task Occupation.HelpWork (Some(Sim.NpcId 9)) None Occupation.Active
+ Assert.AreEqual<string>("帮工:进行中 → 村民 9", TaskRuntime.taskLine target "进行中")
+
+ [<TestMethod>]
+ member _.TillSoilTargetShowsHomePlot () =
+ let target = task Occupation.TillSoil None (Some(33, 25)) Occupation.Offered
+ Assert.AreEqual<string>("翻土:待接受 → 自家地块", TaskRuntime.taskLine target "待接受")
+
+ [<TestMethod>]
+ member _.NoTargetKeepsLegacyCopy () =
+ let helpWildcard = task Occupation.HelpWork None None Occupation.Active
+ Assert.AreEqual<string>("帮工:进行中", TaskRuntime.taskLine helpWildcard "进行中")
+ let nightWildcard = task Occupation.NightCatch None None Occupation.Offered
+ Assert.AreEqual<string>("夜捕:待接受", TaskRuntime.taskLine nightWildcard "待接受")
+ let tillWildcard = task Occupation.TillSoil None None Occupation.Done
+ Assert.AreEqual<string>("翻土:已完成", TaskRuntime.taskLine tillWildcard "已完成")
+
+ [<TestMethod>]
+ member _.TaskPanelShowsTargetLineAndKeepsWildcardCopy () =
+ let targeted =
+ { farmerState with
+ Today = Some(task Occupation.HelpWork (Some(Sim.NpcId 7)) None Occupation.Active) }
+ let targetedLines = M5Interaction.panelLines world (taskPanelView (Some targeted))
+ CollectionAssert.Contains(
+ ResizeArray(targetedLines),
+ "帮工:进行中 → 村民 7",
+ sprintf "目标面板行缺失:%A" targetedLines)
+ let wildcard =
+ { farmerState with Today = Some(task Occupation.HelpWork None None Occupation.Active) }
+ let wildcardLines = M5Interaction.panelLines world (taskPanelView (Some wildcard))
+ CollectionAssert.Contains(ResizeArray(wildcardLines), "帮工:进行中")
+ Assert.IsFalse(
+ wildcardLines |> List.exists (fun line -> line.Contains '→'),
+ sprintf "无目标面板不应出现目标箭头:%A" wildcardLines)
+
+ [<TestMethod>]
+ member _.TargetLinesAreFullyRenderableIncludingArrow () =
+ let lines =
+ [ TaskRuntime.taskLine (task Occupation.HelpWork (Some(Sim.NpcId 9)) None Occupation.Active) "进行中"
+ TaskRuntime.taskLine (task Occupation.TillSoil None (Some(34, 25)) Occupation.Offered) "待接受"
+ TaskRuntime.taskLine (task Occupation.HelpWork None None Occupation.Active) "进行中" ]
+ for line in lines do
+ Assert.IsTrue(PixelText.isRenderable line, sprintf "任务面板文案有缺字/不可渲染:%s" line)
+ Assert.IsTrue(CjkGlyphAtlas.contains '→', "箭头字形 → 未入图集")
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
index a49cecd..61fda7d 100644
--- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
+++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
@@ -1096,7 +1096,7 @@ type PrototypeTests () =
[<TestMethod>]
member _.CjkGlyphAtlasHasAStableCharacterTableAndGeometry () =
- Assert.AreEqual<int>(340, characters.Length)
+ Assert.AreEqual<int>(343, characters.Length)
CollectionAssert.AreEqual(characters, characters |> Array.sort)
Assert.AreEqual<int>(characters.Length, characters |> Array.distinct |> Array.length)
Assert.AreEqual<int>(16, columns)
diff --git a/src/LivingVillage.Desktop/Assets/cjk-glyph-atlas.png b/src/LivingVillage.Desktop/Assets/cjk-glyph-atlas.png
index 69662c0..6d422e7 100644
--- a/src/LivingVillage.Desktop/Assets/cjk-glyph-atlas.png
+++ b/src/LivingVillage.Desktop/Assets/cjk-glyph-atlas.png
Binary files differ
diff --git a/src/LivingVillage.Desktop/Assets/cjk-glyphs.txt b/src/LivingVillage.Desktop/Assets/cjk-glyphs.txt
index 5c37717..7532222 100644
--- a/src/LivingVillage.Desktop/Assets/cjk-glyphs.txt
+++ b/src/LivingVillage.Desktop/Assets/cjk-glyphs.txt
@@ -1,4 +1,5 @@
×
+→
一
上
不
@@ -83,7 +84,9 @@
圃
土
在
+地
坏
+块
墙
备
夏
diff --git a/src/LivingVillage.Desktop/ChineseText.fs b/src/LivingVillage.Desktop/ChineseText.fs
index 9789ae7..349bbec 100644
--- a/src/LivingVillage.Desktop/ChineseText.fs
+++ b/src/LivingVillage.Desktop/ChineseText.fs
@@ -218,6 +218,8 @@ module ChineseText =
"转卖"
"观察记事"
"论理"
+ "自家地块"
+ "→"
"剧情线"
"雨水与年成"
"水位与收成"
diff --git a/src/LivingVillage.Desktop/CjkGlyphAtlas.fs b/src/LivingVillage.Desktop/CjkGlyphAtlas.fs
index ab03039..a295f10 100644
--- a/src/LivingVillage.Desktop/CjkGlyphAtlas.fs
+++ b/src/LivingVillage.Desktop/CjkGlyphAtlas.fs
@@ -11,7 +11,7 @@ module CjkGlyphAtlas =
// Keep this table sorted and stable so the generated PNG has a reproducible layout.
let private characterTable =
- "×一上不与业东两中为主乐乡书买了争事于互些交人今从以件价任众会传位体作使保信倍停先入关内册再农冬准几出分切到前剧力功务动助匀包北单卖南卷原去友取受口只可各同和品回围圃土在坏墙备夏多夜大天夫失头好始子存完定家察对将小尚就层居屋工差己已巴巷币布帮常平年庄库应庙度开式当录待得心必态怒总恼情愁意戏成或户打找报拒择指挑捕据接提播操收改效敌数文料新方无日旧时明昏易春是晓晚晨景暂更最有未村束板林果柴标格档桥歉段民水求江没法涨淡清渔游热照物状玩理瓦生用田留白的盐目看真石确社离秋移稳空竹笑第米粮精系紧纭纷线结给绝继绪续置翻耕聊职背能自范茶获菜落衅行西观认讨议记讲论设访试话询说请读谅谈败货贩资走足路跳车转轴过近返还进迫退送选速道邻郎都里量金鉴钱门闭问闯闲间闻附雨零需面页项须食饥饿香鱼黄黛,:"
+ "×→一上不与业东两中为主乐乡书买了争事于互些交人今从以件价任众会传位体作使保信倍停先入关内册再农冬准几出分切到前剧力功务动助匀包北单卖南卷原去友取受口只可各同和品回围圃土在地坏块墙备夏多夜大天夫失头好始子存完定家察对将小尚就层居屋工差己已巴巷币布帮常平年庄库应庙度开式当录待得心必态怒总恼情愁意戏成或户打找报拒择指挑捕据接提播操收改效敌数文料新方无日旧时明昏易春是晓晚晨景暂更最有未村束板林果柴标格档桥歉段民水求江没法涨淡清渔游热照物状玩理瓦生用田留白的盐目看真石确社离秋移稳空竹笑第米粮精系紧纭纷线结给绝继绪续置翻耕聊职背能自范茶获菜落衅行西观认讨议记讲论设访试话询说请读谅谈败货贩资走足路跳车转轴过近返还进迫退送选速道邻郎都里量金鉴钱门闭问闯闲间闻附雨零需面页项须食饥饿香鱼黄黛,:"
let characters = characterTable.ToCharArray()
let rowCount = (characters.Length + columns - 1) / columns
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index a0e6fc0..ba925f3 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -343,6 +343,12 @@ type LivingVillageGame() as this =
let mutable p65Step = 0
let mutable p65PendingName = ""
let mutable p65Pending = false
+ // P67 证据钩子(职业每日任务 UI 目标展示):农夫真实开局 → 找到当日「帮工」任务 →
+ // 拍带目标帧(帮工:… → 村民 N)与同模板无目标帧(帮工:…,文案与旧版逐字节一致)各一帧。
+ let p67ShotMode = Environment.GetEnvironmentVariable("LV_P67_SHOT") = "1"
+ let mutable p67Step = 0
+ let mutable p67PendingName = ""
+ let mutable p67Pending = false
// P45 证据钩子:LV_P45_SHOT=1 时拍角色 idle / 行走朝下 / 行走朝右 三帧(真实游戏截图)。
let p45ShotMode = Environment.GetEnvironmentVariable("LV_P45_SHOT") = "1"
let mutable p45Step = 0
@@ -1238,6 +1244,84 @@ type LivingVillageGame() as this =
m5View.Status
p65Step <- p65Step + 1
+ /// P67 证据:任务面板的目标展示(带目标 / 无目标各一帧),走真实每日任务纯函数生成的 DailyTask。
+ member private this.PrepareP67Shot () =
+ let noonOfDay (day: int64) : int64 =
+ day * Sim.ticksPerDay + M6Presentation.resolveStartTick (Some 12.0) false
+ let targets = WorldBootstrap.reachableTargets world
+ let dayTask (day: int64) (targeted: bool) =
+ if targeted then
+ Occupation.dailyTaskOfWith targets 42UL (Occupation.occupationSeedOf Occupation.Farmer) day Occupation.Farmer
+ else
+ Occupation.dailyTaskOf 42UL (Occupation.occupationSeedOf Occupation.Farmer) day Occupation.Farmer
+ // 找一天当日任务为「帮工」(有目标语义),使两帧同模板可直接对照。
+ let mutable helpDay = 0L
+ let mutable found = false
+ for day in 0L .. 120L do
+ if not found then
+ match dayTask day true with
+ | Some task when task.TemplateId = Occupation.HelpWork -> helpDay <- day; found <- true
+ | _ -> ()
+ let setToday (task: Occupation.DailyTask) =
+ match m5View.Task with
+ | Some state -> m5View <- { m5View with Task = Some { state with Today = Some task } }
+ | None -> ()
+ let ask (command: M5Command) =
+ let nextWorld, nextView = M5Interaction.apply command world m5View
+ world <- nextWorld
+ m5View <- nextView
+ if not p67Pending then
+ match p67Step with
+ | 0 ->
+ let tick = noonOfDay helpDay
+ world <- { world with Tick = tick; Time = float tick * Sim.dtSeconds }
+ match dayTask helpDay true with
+ | Some task -> setToday task
+ | None -> ()
+ ask M5Command.ShowTaskPanel
+ this.CenterCamera()
+ p67PendingName <- "p67-target"
+ p67Pending <- true
+ | 1 ->
+ match dayTask helpDay false with
+ | Some task -> setToday task
+ | None -> ()
+ p67PendingName <- "p67-wildcard"
+ p67Pending <- true
+ | _ ->
+ printfn "p67-shot=done"
+ this.Exit()
+
+ /// P67 证据:落盘并打印帧名/面板/模板/目标/整行文案/缺字槽位,供 analyze 核验。
+ member private this.CaptureP67Shot () =
+ if p67Pending then
+ p67Pending <- false
+ System.IO.Directory.CreateDirectory recordDirectory |> ignore
+ this.SaveBackBuffer(sprintf "%s/%s.png" recordDirectory p67PendingName)
+ let lines = M5Interaction.panelLines world m5View
+ let missingSlots =
+ lines
+ |> List.sumBy (fun line ->
+ line
+ |> Seq.filter (fun c -> c <> ' ' && not (PixelText.isRenderable (string c)))
+ |> Seq.length)
+ let templateText, targetText, lineText =
+ match m5View.Task |> Option.bind (fun state -> state.Today) with
+ | None -> "none", "<none>", "<none>"
+ | Some task ->
+ Occupation.taskNameOf task.TemplateId,
+ (TaskRuntime.targetText task |> Option.defaultValue "<none>"),
+ (TaskRuntime.taskLine task "进行中")
+ printfn
+ "p67-shot=%s panel=%s template=%s target=%s line=%s missing_glyph_slots=%d"
+ p67PendingName
+ (M5Interaction.panelName m5View)
+ templateText
+ targetText
+ lineText
+ missingSlots
+ p67Step <- p67Step + 1
+
member private this.PrepareP45Shot () =
let baseTick = M6Presentation.resolveStartTick (Some 12.0) false
let setTick (tick: int64) =
@@ -2255,6 +2339,14 @@ type LivingVillageGame() as this =
else this.DispatchMenuInput Confirm
elif menu.Page = Playing then
this.PrepareP65Shot()
+ elif p67ShotMode then
+ if menu.Page = MainMenu then
+ this.DispatchMenuInput Confirm
+ elif menu.Page = OccupationSelect then
+ if menu.Selected <> 0 then this.DispatchMenuInput Down
+ else this.DispatchMenuInput Confirm
+ elif menu.Page = Playing then
+ this.PrepareP67Shot()
elif p45ShotMode then
if menu.Page = MainMenu then
this.DispatchMenuInput Confirm
@@ -2733,6 +2825,8 @@ type LivingVillageGame() as this =
this.CaptureP64Shot()
if p65ShotMode && menu.Page = Playing then
this.CaptureP65Shot()
+ if p67ShotMode && menu.Page = Playing then
+ this.CaptureP67Shot()
if p45ShotMode && menu.Page = Playing then
this.CaptureP45Shot()
if p46ShotMode then
diff --git a/src/LivingVillage.Desktop/Interaction.fs b/src/LivingVillage.Desktop/Interaction.fs
index ba792d1..6c62661 100644
--- a/src/LivingVillage.Desktop/Interaction.fs
+++ b/src/LivingVillage.Desktop/Interaction.fs
@@ -719,7 +719,7 @@ module M5Interaction =
match occupationState.Today with
| None -> [ "今日暂无任务" ]
| Some task ->
- [ sprintf "%s:%s" (Occupation.taskNameOf task.TemplateId) (taskStateText task.State) ]
+ [ TaskRuntime.taskLine task (taskStateText task.State) ]
@ (match task.State with
| Occupation.Offered -> [ "回车:接受任务" ]
| _ -> [])
diff --git a/src/LivingVillage.Desktop/TaskRuntime.fs b/src/LivingVillage.Desktop/TaskRuntime.fs
index efc57f3..ce9bfbc 100644
--- a/src/LivingVillage.Desktop/TaskRuntime.fs
+++ b/src/LivingVillage.Desktop/TaskRuntime.fs
@@ -16,6 +16,20 @@ module TaskRuntime =
| Some(item, quantity) -> sprintf "%s +%s×%d" headline (Occupation.itemNameOf item) quantity
| None -> headline
+ // ---- P67 任务目标 UI(design §2.4-3 收口后的可见化)----
+ /// 带目标时的中文目标名:帮工 → 指定村民;翻土 → 自家地块。其余模板/无目标 → None。
+ let targetText (task: Occupation.DailyTask) : string option =
+ match task.TemplateId, task.TargetNpc, task.TargetTile with
+ | Occupation.HelpWork, Some(Sim.NpcId id), _ -> Some(sprintf "村民 %d" id)
+ | Occupation.TillSoil, _, Some _ -> Some "自家地块"
+ | _ -> None
+
+ /// 任务标题行:有目标时「模板:状态 → 目标」;无目标时与既有文案逐字节一致。
+ let taskLine (task: Occupation.DailyTask) (stateText: string) : string =
+ match targetText task with
+ | Some target -> sprintf "%s:%s → %s" (Occupation.taskNameOf task.TemplateId) stateText target
+ | None -> sprintf "%s:%s" (Occupation.taskNameOf task.TemplateId) stateText
+
let private completionMessage
(before: Occupation.DailyTask option)
(after: Occupation.DailyTask option)