diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-21 19:34:32 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-21 19:34:32 +0800 |
| commit | 98b6b21c56c7b061578e7d4b56d52450dc7bd575 (patch) | |
| tree | 8c3ce5912294207d37863c37b15f2bce89c0787f /src | |
| parent | c348da258f072bc88ea422cbc534c0831817b3c8 (diff) | |
| download | living-village-98b6b21c56c7b061578e7d4b56d52450dc7bd575.tar.gz | |
feat(desktop): P12 中文每日任务面板——T 键任务页、Offered/Active/Done 三态+职业名,存读档字段级携带职业
Diffstat (limited to 'src')
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/DesktopTests.fs | 52 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/Game.fs | 8 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/Interaction.fs | 37 |
3 files changed, 91 insertions, 6 deletions
diff --git a/src/LivingVillage.Desktop.Tests/DesktopTests.fs b/src/LivingVillage.Desktop.Tests/DesktopTests.fs index e9713e5..52443e9 100644 --- a/src/LivingVillage.Desktop.Tests/DesktopTests.fs +++ b/src/LivingVillage.Desktop.Tests/DesktopTests.fs @@ -156,3 +156,55 @@ type DesktopTests () = Assert.AreEqual<int64>(0L, defaultWorld.Tick) Assert.AreEqual<float>(0.0, defaultWorld.Time) Assert.AreEqual<M6DayNight>(Night, defaultProfile.Mode) + +// ---- P12 中文每日任务面板 ---- + +module private TaskPanelHarness = + + let occupationWithTask (taskState: Occupation.TaskState) : Occupation.State = + let occupation = Occupation.stateOf Occupation.Farmer + let task = + (Occupation.dailyTaskOf 42UL 7UL 0L Occupation.Farmer).Value + |> Occupation.acceptTask + |> fun active -> if taskState = Occupation.Done then { active with State = Occupation.Done } else { active with State = taskState } + { occupation with Today = Some task } + +[<TestClass>] +type TaskPanelTests () = + + [<TestMethod>] + member _.TaskPanelWithoutOccupationShowsNoOccupationLine () = + let world = Sim.initialWorldN 42UL 4 + let _, view = M5Interaction.apply ShowTaskPanel world M5Interaction.initial + + Assert.AreEqual<M5Panel>(TaskPanel, view.Panel) + Assert.AreEqual<string>("task", M5Interaction.panelName view) + let lines = M5Interaction.panelLines world view + Assert.IsTrue(lines |> List.contains "任务", "panel heading expected") + Assert.IsTrue(lines |> List.contains "无职业") + + [<TestMethod>] + member _.TaskPanelShowsChineseOccupationNameAndTheThreeStates () = + let world = Sim.initialWorldN 42UL 4 + for taskState, expected in [ Occupation.Offered, "待接受"; Occupation.Active, "进行中"; Occupation.Done, "已完成" ] do + let withTask = { M5Interaction.initial with Task = Some (TaskPanelHarness.occupationWithTask taskState) } + let _, view = M5Interaction.apply ShowTaskPanel world withTask + let lines = M5Interaction.panelLines world view + Assert.IsTrue(lines |> List.contains "农夫", $"occupation name missing for {taskState}") + Assert.IsTrue(lines |> List.exists (fun line -> line.Contains(expected)), $"state {expected} missing in {lines}") + + [<TestMethod>] + member _.TaskPanelTitleUsesChineseHeading () = + let world = Sim.initialWorldN 42UL 4 + let _, view = M5Interaction.apply ShowTaskPanel world M5Interaction.initial + Assert.IsTrue(M5Interaction.titleText view |> fun text -> text.Contains("任务")) + + [<TestMethod>] + member _.ShowTaskPanelIsRejectedFromDialoguePanelLikeOtherOverlays () = + let world = Sim.initialWorldN 42UL 4 |> Harness.atNpc 0 + let openedWorld, dialogueView = M5Interaction.apply Interact world M5Interaction.initial + let _, afterTask = M5Interaction.apply ShowTaskPanel openedWorld dialogueView + + Assert.AreEqual<M5View>(dialogueView, afterTask) + let _, closed = M5Interaction.apply ClosePanel world { dialogueView with Panel = TaskPanel } + Assert.AreEqual<M5Panel>(WorldPanel, closed.Panel) diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs index 9fab5f7..cbe2b9d 100644 --- a/src/LivingVillage.Desktop/Game.fs +++ b/src/LivingVillage.Desktop/Game.fs @@ -202,7 +202,7 @@ type LivingVillageGame() as this = member private this.SaveWorld (prefix: string) : bool = try - WorldSave.saveToFile savePath world + WorldSave.saveToFileWith m5View.Task savePath world m5View <- { m5View with Status = "save succeeded"; StatusTick = Some world.Tick } printfn $"{prefix}=ok path={savePath} tick={world.Tick}" true @@ -213,10 +213,11 @@ type LivingVillageGame() as this = false member private this.LoadWorld (prefix: string) : Result<int64, string> = - match WorldSave.loadFromFile savePath with - | Ok nextWorld -> + match WorldSave.loadFromFileWith savePath with + | Ok (nextWorld, occupation) -> world <- nextWorld this.ResetWorldView() + m5View <- { m5View with Task = occupation } printfn $"{prefix}=ok path={savePath} tick={world.Tick}" Ok world.Tick | Error failure -> @@ -384,6 +385,7 @@ type LivingVillageGame() as this = elif pressed Keys.Q then Some Observe elif pressed Keys.Tab then Some ToggleNeeds elif pressed Keys.C then Some ShowChronicle + elif pressed Keys.T then Some ShowTaskPanel else None match m5Command with | Some command -> diff --git a/src/LivingVillage.Desktop/Interaction.fs b/src/LivingVillage.Desktop/Interaction.fs index 3f04da8..95aa84c 100644 --- a/src/LivingVillage.Desktop/Interaction.fs +++ b/src/LivingVillage.Desktop/Interaction.fs @@ -22,6 +22,7 @@ type M5Panel = | ObservationPanel | DialoguePanel | ChroniclePanel + | TaskPanel type M5Command = | Interact @@ -34,6 +35,7 @@ type M5Command = | Observe | ToggleNeeds | ShowChronicle + | ShowTaskPanel | ClosePanel type M5View = @@ -46,7 +48,8 @@ type M5View = StatusTick: int64 option HomeMode: HomeMode Prompt: string option - PromptTargetPos: Vec2 option } + PromptTargetPos: Vec2 option + Task: Occupation.State option } module InteractionResolver = @@ -208,7 +211,8 @@ module M5Interaction = StatusTick = None HomeMode = Outside Prompt = None - PromptTargetPos = None } + PromptTargetPos = None + Task = None } let worldInputAllowed (view: M5View) : bool = view.Panel = WorldPanel @@ -326,7 +330,8 @@ module M5Interaction = | Intent6 -> view.Panel = DialoguePanel | Observe | ToggleNeeds - | ShowChronicle -> view.Panel <> DialoguePanel + | ShowChronicle + | ShowTaskPanel -> view.Panel <> DialoguePanel let private applyAllowed (command: M5Command) (world: World) (view: M5View) : World * M5View = match command with @@ -404,6 +409,13 @@ module M5Interaction = Prompt = None PromptTargetPos = None Status = sprintf "chronicle entries=%d" world.Annals.Length } + | ShowTaskPanel -> + world, + { view with + Panel = TaskPanel + Prompt = None + PromptTargetPos = None + Status = "task panel open" } | ClosePanel -> world, refreshPrompt world { view with Panel = WorldPanel; Menu = None; Status = "panel closed" } let apply (command: M5Command) (world: World) (view: M5View) : World * M5View = @@ -440,6 +452,7 @@ module M5Interaction = | ObservationPanel -> "observation" | DialoguePanel -> "dialogue" | ChroniclePanel -> "chronicle" + | TaskPanel -> "task" let titleText (view: M5View) : string = match view.Panel with @@ -456,6 +469,7 @@ module M5Interaction = sprintf "互动:村民 %d | %s | ESC 关闭" (npcNumber menu.Target) options | None -> "互动已记录 | E 互动 | ESC 关闭" | ChroniclePanel -> "年鉴 | TAB 需求 | Q 观察 | ESC 关闭" + | TaskPanel -> "任务 | TAB 需求 | Q 观察 | ESC 关闭" let private statusLabel (status: string) : string = match status with @@ -474,6 +488,7 @@ module M5Interaction = if String.IsNullOrWhiteSpace reason then "对话失败" else sprintf "对话失败:%s" reason | value when value.StartsWith("observation") -> "观察完成" | value when value.StartsWith("chronicle") -> "年鉴已打开" + | "task panel open" -> "任务面板已打开" | value when value.StartsWith("no dialogue") -> "当前没有可用对话" | value when value.StartsWith("invalid dialogue") -> "对话选项无效" | value when value.StartsWith("no nearby") -> "附近没有可互动目标" @@ -576,3 +591,19 @@ module M5Interaction = | ChroniclePanel -> let entries = chronicleLines world "年鉴" :: (if entries.IsEmpty then [ "暂无记录" ] else entries) @ [ status ] + | TaskPanel -> + let taskStateText (state: Occupation.TaskState) : string = + match state with + | Occupation.Offered -> "待接受" + | Occupation.Active -> "进行中" + | Occupation.Done -> "已完成" + | Occupation.Failed -> "已失败" + match view.Task with + | None -> [ "任务"; "无职业"; status ] + | Some occupationState -> + let taskLines = + match occupationState.Today with + | None -> [ "今日暂无任务" ] + | Some task -> + [ sprintf "%s:%s" (Occupation.taskNameOf task.TemplateId) (taskStateText task.State) ] + Occupation.nameOf occupationState.Profile.Kind :: taskLines @ [ status ] |
