diff options
Diffstat (limited to 'src/LivingVillage.Desktop.Tests')
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj | 5 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/PrototypeTests.fs | 172 |
2 files changed, 175 insertions, 2 deletions
diff --git a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj index 569190a..3a9a142 100644 --- a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj +++ b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj @@ -23,4 +23,9 @@ <ProjectReference Include="..\LivingVillage.Headless\LivingVillage.Headless.fsproj" /> </ItemGroup> + <ItemGroup> + <None Include="..\LivingVillage.Desktop\Assets\cjk-glyph-atlas.png" Link="Assets\cjk-glyph-atlas.png" CopyToOutputDirectory="PreserveNewest" /> + <None Include="..\LivingVillage.Desktop\Assets\cjk-glyphs.txt" Link="Assets\cjk-glyphs.txt" CopyToOutputDirectory="PreserveNewest" /> + </ItemGroup> + </Project> diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs index a4bd1ea..842ab4a 100644 --- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs +++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs @@ -1,11 +1,14 @@ namespace LivingVillage.Desktop.Tests open System +open System.IO +open Microsoft.Xna.Framework open Microsoft.VisualStudio.TestTools.UnitTesting open LivingVillage.Kernel open LivingVillage.Kernel.Sim open LivingVillage.Desktop open LivingVillage.Desktop.ChineseText +open LivingVillage.Desktop.CjkGlyphAtlas open LivingVillage.Desktop.VillageArt open LivingVillage.Desktop.VillagePresentation @@ -68,7 +71,9 @@ type PrototypeTests () = | Error(HomeNotFound(HomeId 999)) -> () | other -> Assert.Fail(sprintf "Expected HomeNotFound, got %A." other) - let farAway = { X = scene.InitialPlayerPosition.X + 20; Y = scene.InitialPlayerPosition.Y + 20 } + let farAway : TilePosition = + { X = scene.InitialPlayerPosition.X + 20 + Y = scene.InitialPlayerPosition.Y + 20 } match enterHome farAway Outside with | Error NotNearHomeDoor -> () | other -> Assert.Fail(sprintf "Expected NotNearHomeDoor, got %A." other) @@ -224,7 +229,7 @@ type PrototypeTests () = sceneLabel Waterway sceneLabel WhiteWallHome sceneLabel StoneBridgeScene - sceneLabel BambooGrove + sceneLabel ChineseText.BambooGrove sceneLabel VegetableGardenScene dayNightLabel DayTime dayNightLabel NightTime @@ -248,3 +253,166 @@ type PrototypeTests () = Assert.AreEqual<string list>([ "江南水乡"; "□" ], layout.Lines) Assert.AreEqual<char list>([ '村' ], layout.MissingGlyphs) Assert.IsFalse(String.concat "" layout.Lines |> fun text -> text.Contains("?")) + + [<TestMethod>] + member _.DesktopPixelPositionUsesExplicitSceneTileMapping () = + let doorPixel : PixelPosition = + { X = float32 (31 * Sim.tilePixels + Sim.tilePixels / 2) + Y = float32 (23 * Sim.tilePixels + Sim.tilePixels / 2) } + + let expectedSceneTile : TilePosition = { X = 6; Y = 5 } + let expectedWorldTile : TilePosition = { X = 31; Y = 23 } + Assert.AreEqual<TilePosition>(expectedSceneTile, sceneTileFromPixel doorPixel) + Assert.AreEqual<TilePosition>(expectedWorldTile, worldTileForSceneTile expectedSceneTile) + + [<TestMethod>] + member _.HomeInteractionPrioritizesEntryExitBeforeNpcDialogue () = + let doorPixel : PixelPosition = + { X = float32 (31 * Sim.tilePixels + Sim.tilePixels / 2) + Y = float32 (23 * Sim.tilePixels + Sim.tilePixels / 2) } + let farPixel = { X = 0.0f; Y = 0.0f } + + Assert.AreEqual<HomeInputAction>(EnterHomeAction(HomeId 1), homeInputAction doorPixel Outside) + Assert.AreEqual<HomeInputAction>(ExitHomeAction(HomeId 1), homeInputAction doorPixel (Inside(HomeId 1))) + Assert.AreEqual<HomeInputAction>(DialogueAction, homeInputAction farPixel Outside) + Assert.AreEqual<HomeMode>(Outside, resetHomeMode ()) + + [<TestMethod>] + member _.JiangnanDesktopRenderPlanIsDeterministicAndHasInteriorElements () = + let first = VillageArt.sampleRenderPlan () + let second = VillageArt.sampleRenderPlan () + + Assert.AreEqual<VillageArt.JiangnanRenderPlan>(first, second) + let expectedOrigin : TilePosition = { X = 25; Y = 18 } + Assert.AreEqual<TilePosition>(expectedOrigin, first.Origin) + Assert.IsTrue(first.Grounds |> List.exists (fun (_, kind) -> kind = VillageArt.Water)) + Assert.IsTrue(first.Grounds |> List.exists (fun (_, kind) -> kind = VillageArt.StonePath)) + Assert.IsTrue(first.Props |> List.exists (fun prop -> prop.Kind = VillageArt.Bridge)) + Assert.AreEqual<int>(4, first.Interior.Elements.Length) + + [<TestMethod>] + member _.FacingUsesTheActualMovementOrTargetVector () = + Assert.AreEqual<VillageArt.FacingDirection>(VillageArt.EastFacing, VillageArt.directionFromVector { X = 1.0f; Y = 0.1f }) + Assert.AreEqual<VillageArt.FacingDirection>(VillageArt.WestFacing, VillageArt.directionFromVector { X = -1.0f; Y = 0.1f }) + Assert.AreEqual<VillageArt.FacingDirection>(VillageArt.SouthFacing, VillageArt.directionFromVector { X = 0.1f; Y = 1.0f }) + Assert.AreEqual<VillageArt.FacingDirection>(VillageArt.NorthFacing, VillageArt.directionFromVector { X = 0.1f; Y = -1.0f }) + + [<TestMethod>] + member _.RuntimeMovementAndNpcTargetsProduceStableSpriteDirections () = + let idle = VillageArt.facingAfterMovement { X = 4.0f; Y = 4.0f } { X = 4.0f; Y = 4.0f } VillageArt.EastFacing + let moved = VillageArt.facingAfterMovement { X = 4.0f; Y = 4.0f } { X = 2.0f; Y = 4.0f } VillageArt.EastFacing + let targetSpec = VillageArt.npcSpriteSpecAtTarget (NpcId 7) { X = 10.0f; Y = 10.0f } { X = 10.0f; Y = 8.0f } 4L + + Assert.AreEqual<VillageArt.FacingDirection>(VillageArt.EastFacing, idle) + Assert.AreEqual<VillageArt.FacingDirection>(VillageArt.WestFacing, moved) + Assert.AreEqual<VillageArt.FacingDirection>(VillageArt.NorthFacing, targetSpec.Direction) + + [<TestMethod>] + member _.RenderPlanCoordinatesAndCharacterAtlasCellsAreDeterministic () = + let plan = VillageArt.sampleRenderPlan () + let localTile : TilePosition = { X = 6; Y = 4 } + let worldTile = VillageArt.worldTile plan localTile + let expectedWorldTile : TilePosition = { X = 31; Y = 22 } + Assert.AreEqual<TilePosition>(expectedWorldTile, worldTile) + + let first = VillageArt.characterSourceRectangle { Variant = IndigoRobe; Direction = NorthFacing; Frame = FrameOne } + let second = VillageArt.characterSourceRectangle { Variant = JadeSash; Direction = EastFacing; Frame = FrameTwo } + Assert.AreNotEqual<Rectangle>(first, second) + Assert.AreEqual<int>(32, first.Width) + Assert.AreEqual<int>(48, first.Height) + Assert.AreEqual<int>(32, second.Width) + Assert.AreEqual<int>(48, second.Height) + + [<TestMethod>] + member _.FormalChineseUiLabelsUseTheRealGlyphAtlasBoundary () = + Assert.IsTrue(ChineseText.requiredUiLabels |> List.forall PixelText.isRenderable) + Assert.IsTrue(ChineseText.requiredUiLabels |> List.forall (fun label -> not (label.Contains("?")))) + Assert.IsFalse(PixelText.isRenderable "龘") + + [<TestMethod>] + member _.ContextualPromptSelectsOneStableReachableNpcAndClearsWhenLeaving () = + let avatar : Vec2 = + { X = float32 (32 * Sim.tilePixels + Sim.tilePixels / 2) + Y = float32 (24 * Sim.tilePixels + Sim.tilePixels / 2) } + let world = + Sim.initialWorldN 42UL 4 + |> fun value -> { value with Avatar = { value.Avatar with Pos = avatar } } + let npcPosition : Vec2 = { X = world.Avatar.Pos.X + 48.0f; Y = world.Avatar.Pos.Y } + let npc = { world.Npcs.[0] with Pos = npcPosition } + let npcs = Array.copy world.Npcs + npcs.[0] <- npc + for index in 1 .. npcs.Length - 1 do + npcs.[index] <- { npcs.[index] with Pos = ({ X = 2000.0f; Y = 1400.0f } : Vec2) } + let nearby = { world with Npcs = npcs } + + let prompt = InteractionResolver.resolve nearby Outside + Assert.IsTrue(prompt.IsSome) + Assert.AreEqual<InteractionAction>(TalkTo(NpcId 0), prompt.Value.Action) + Assert.AreEqual<string>("E 交谈", prompt.Value.Text) + + let view = M5Interaction.refreshPrompt nearby M5Interaction.initial + Assert.AreEqual<string option>(Some "E 交谈", view.Prompt) + let farAway = { nearby with Avatar = { nearby.Avatar with Pos = ({ X = 0.0f; Y = 0.0f } : Vec2) } } + let cleared = M5Interaction.refreshPrompt farAway view + Assert.AreEqual<string option>(None, cleared.Prompt) + + [<TestMethod>] + member _.HomeEntryWinsDeterministicallyAndUsesTheSameActionAsThePrompt () = + let doorPixel : PixelPosition = + { X = float32 (31 * Sim.tilePixels + Sim.tilePixels / 2) + Y = float32 (23 * Sim.tilePixels + Sim.tilePixels / 2) } + let world = Sim.initialWorldN 42UL 0 |> fun value -> { value with Avatar = { value.Avatar with Pos = ({ X = doorPixel.X; Y = doorPixel.Y } : Vec2) } } + let prompt = InteractionResolver.resolve world Outside + + Assert.IsTrue(prompt.IsSome) + Assert.AreEqual<InteractionAction>(EnterHome(HomeId 1), prompt.Value.Action) + Assert.AreEqual<string>("E 进屋", prompt.Value.Text) + + let _, view = M5Interaction.apply Interact world M5Interaction.initial + Assert.AreEqual<HomeMode>(Inside(HomeId 1), view.HomeMode) + Assert.AreEqual<string option>(None, view.Prompt) + + [<TestMethod>] + member _.WallBlocksNpcPromptAndOpenPanelsDoNotAcceptWorldInput () = + let avatar : Vec2 = { X = float32 (32 * Sim.tilePixels + Sim.tilePixels / 2); Y = float32 (24 * Sim.tilePixels + Sim.tilePixels / 2) } + let npcPosition : Vec2 = { X = avatar.X - 40.0f; Y = avatar.Y - 80.0f } + let world = Sim.initialWorldN 42UL 1 + let npc = { world.Npcs.[0] with Pos = npcPosition } + let blockedWorld = { world with Avatar = { world.Avatar with Pos = avatar }; Npcs = [| npc |] } + + Assert.IsFalse(InteractionResolver.hasClearPath avatar npcPosition) + Assert.IsFalse(M5Interaction.worldInputAllowed { M5Interaction.initial with Panel = DialoguePanel }) + Assert.IsTrue(M5Interaction.worldInputAllowed M5Interaction.initial) + Assert.IsTrue(M5Interaction.resolveAction blockedWorld Outside |> Option.isNone) + + [<TestMethod>] + member _.WorldPanelDoesNotRenderA永久KeyList () = + let lines = M5Interaction.panelLines (Sim.initialWorldN 42UL 0) M5Interaction.initial + Assert.IsFalse(lines |> List.exists (fun line -> line = "Q 观察" || line = "TAB 需求" || line = "C 年鉴" || line = "L 关系")) + + [<TestMethod>] + member _.CjkGlyphAtlasHasAStableCharacterTableAndGeometry () = + Assert.AreEqual<int>(188, characters.Length) + CollectionAssert.AreEqual(characters, characters |> Array.sort) + Assert.AreEqual<int>(characters.Length, characters |> Array.distinct |> Array.length) + Assert.AreEqual<int>(16, columns) + Assert.AreEqual<int>(24, cellPixels) + Assert.AreEqual<int>(12, rowCount) + Assert.AreEqual<int>(384, pixelWidth) + Assert.AreEqual<int>(288, pixelHeight) + Assert.AreEqual<string>("Assets/cjk-glyph-atlas.png", assetRelativePath) + Assert.AreEqual<string>("Assets/cjk-glyphs.txt", manifestRelativePath) + + [<TestMethod>] + member _.CjkGlyphAtlasMatchesThePackagedManifestAndRejectsUnknownCharacters () = + Assert.IsTrue(ChineseText.requiredUiLabels |> List.forall (fun label -> label |> Seq.forall (fun character -> character = ' ' || not (Char.IsLetter character) || contains character))) + Assert.IsTrue(contains '江') + Assert.IsTrue(contains '龘' |> not) + Assert.AreEqual<int>(0, sourceRectangle '龘' |> Option.toList |> List.length) + let manifestPath = runtimeManifestPath () + Assert.IsTrue(File.Exists manifestPath, sprintf "Missing atlas manifest: %s" manifestPath) + let manifestLines = File.ReadAllLines manifestPath + Assert.AreEqual<int>(characters.Length, manifestLines.Length) + Assert.IsTrue(manifestLines |> Array.forall (fun line -> line.Length = 1), "The CJK manifest must contain exactly one character per line.") + CollectionAssert.AreEqual(characters, manifestLines |> Array.map (fun line -> line.[0])) + Assert.IsTrue(File.Exists(runtimeAssetPath ()), sprintf "Missing atlas PNG: %s" (runtimeAssetPath ())) |
