From eaedd532c7d93dec9cca5d361685ed32c85aeebb Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 21 Sep 2026 12:37:05 +0800 Subject: fix(desktop): 目标选择纯距离优先与 id 平局 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 情境提示目标选择改为距离最近优先(不再按门/人类型特权),平局按实体 id 升序确定唯一目标;新增门/人两侧距离优先对照、精确等距 tie、NPC 数组快照不变(resolve 与 refreshPrompt 均不突变 Npcs)回归测试;透墙与超距判定沿用既有 实现,已有边界/竞争/切换用例保持全绿。 --- src/LivingVillage.Desktop.Tests/PrototypeTests.fs | 35 +++++++++++++++++++++++ 1 file changed, 35 insertions(+) (limited to 'src/LivingVillage.Desktop.Tests') diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs index 77d2530..af34802 100644 --- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs +++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs @@ -594,6 +594,41 @@ type PrototypeTests () = Assert.IsTrue(M5Interaction.worldInputAllowed M5Interaction.initial) Assert.IsTrue(M5Interaction.resolveAction blockedWorld Outside |> Option.isNone) + [] + member _.TargetSelectionUsesPureDistancePriorityWithIdTiebreak () = + let doorPixel : PixelPosition = + { X = float32 (31 * Sim.tilePixels + Sim.tilePixels / 2) + Y = float32 (23 * Sim.tilePixels + Sim.tilePixels / 2) } + let avatar : Vec2 = { X = doorPixel.X - 32.0f; Y = doorPixel.Y } + let world = Sim.initialWorldN 42UL 2 + + // Door nearer than the villager: door wins by pure distance (no kind privilege). + let doorNearer = + { world with + Avatar = { world.Avatar with Pos = avatar } + Npcs = [| { world.Npcs.[0] with Pos = { X = avatar.X + 40.0f; Y = avatar.Y } } |] } + Assert.AreEqual(Some(EnterHome(HomeId 1)), InteractionResolver.resolve doorNearer Outside |> Option.map (fun candidate -> candidate.Action)) + + // NPC nearer than the door: villager wins. + let npcNearerWorld = { doorNearer with Npcs = [| { world.Npcs.[0] with Pos = { X = avatar.X + 4.0f; Y = avatar.Y } } |] } + Assert.AreEqual(Some(TalkTo(NpcId 0)), InteractionResolver.resolve npcNearerWorld Outside |> Option.map (fun candidate -> candidate.Action)) + + // Exact distance tie between two villagers: lower entity id wins deterministically. + let tieAvatar : Vec2 = { X = 1600.0f; Y = 800.0f } + let tieWorld = + { world with + Avatar = { world.Avatar with Pos = tieAvatar } + Npcs = [| { world.Npcs.[0] with Pos = { X = tieAvatar.X + 64.0f; Y = tieAvatar.Y } } + { world.Npcs.[1] with Pos = { X = tieAvatar.X - 64.0f; Y = tieAvatar.Y } } |] } + Assert.AreEqual(Some(TalkTo(NpcId 0)), InteractionResolver.resolve tieWorld Outside |> Option.map (fun candidate -> candidate.Action)) + + // Array snapshot immutability: neither resolve nor refreshPrompt may mutate Npcs. + let snapshot = Array.copy tieWorld.Npcs + let resolvedIgnore = InteractionResolver.resolve tieWorld Outside + CollectionAssert.AreEqual(snapshot, tieWorld.Npcs) + let view = M5Interaction.refreshPrompt tieWorld M5Interaction.initial + CollectionAssert.AreEqual(snapshot, tieWorld.Npcs) + [] member _.WorldPanelDoesNotRenderA永久KeyList () = let world = Sim.initialWorldN 42UL 0 -- cgit v1.2.3