summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/LivingVillage.Desktop.Tests/DesktopTests.fs52
-rw-r--r--src/LivingVillage.Desktop/Game.fs8
-rw-r--r--src/LivingVillage.Desktop/Interaction.fs37
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 ]