diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/PrototypeTests.fs | 72 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/ChineseText.fs | 84 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj | 2 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/VillageArt.fs | 95 |
4 files changed, 253 insertions, 0 deletions
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs index 3abb064..a4bd1ea 100644 --- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs +++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs @@ -5,6 +5,8 @@ open Microsoft.VisualStudio.TestTools.UnitTesting open LivingVillage.Kernel open LivingVillage.Kernel.Sim open LivingVillage.Desktop +open LivingVillage.Desktop.ChineseText +open LivingVillage.Desktop.VillageArt open LivingVillage.Desktop.VillagePresentation [<TestClass>] @@ -176,3 +178,73 @@ type PrototypeTests () = Assert.AreEqual<string>("1倍速", M6Presentation.clockLabel { Paused = false; Speed = OneX }) Assert.AreEqual<string>("2倍速", M6Presentation.clockLabel { Paused = false; Speed = TwoX }) Assert.AreEqual<string>("5倍速", M6Presentation.clockLabel { Paused = false; Speed = FiveX }) + + [<TestMethod>] + member _.JiangnanArtPlanIsDeterministicAndContainsRequiredElements () = + let first = samplePlan () + let second = samplePlan () + let requiredProps = + [ VillageArt.WhiteWallDarkTileHouse + VillageArt.StoneBridge + VillageArt.Bamboo + VillageArt.VegetableGarden ] + + Assert.AreEqual<JiangnanArtPlan>(first, second) + Assert.IsTrue( + requiredProps + |> List.forall (fun prop -> first.Props |> List.exists (fun sprite -> sprite.Kind = prop)), + "The Jiangnan plan is missing one or more required props.") + Assert.IsTrue(first.Tiles |> List.exists (fun tile -> tile.Kind = (VillageArt.RiverWater))) + Assert.IsTrue(first.Tiles |> List.exists (fun tile -> tile.Kind = (VillageArt.StonePaving))) + + [<TestMethod>] + member _.NpcVariantsAreStableAndAnimationCoversFourDirectionsAndTwoFrames () = + let sameNpc = npcVisualVariant (NpcId 7) + let repeatedNpc = npcVisualVariant (NpcId 7) + let otherNpc = npcVisualVariant (NpcId 8) + let directions = + [ NorthFacing + SouthFacing + WestFacing + EastFacing ] + let sprites = directions |> List.map (fun direction -> npcSpriteSpec (NpcId 7) direction 4L) + + Assert.AreEqual<NpcVisualVariant>(sameNpc, repeatedNpc) + Assert.AreNotEqual<NpcVisualVariant>(sameNpc, otherNpc) + Assert.AreEqual<int>(4, sprites |> List.map (fun sprite -> sprite.Direction) |> List.distinct |> List.length) + Assert.IsTrue(sprites |> List.forall (fun sprite -> sprite.Frame = FrameTwo)) + Assert.AreEqual<AnimationFrame>(FrameOne, (VillageArt.animationFrame 0L)) + Assert.AreEqual<AnimationFrame>(FrameTwo, (VillageArt.animationFrame 4L)) + Assert.AreEqual<AnimationFrame>(FrameOne, (VillageArt.animationFrame 8L)) + + [<TestMethod>] + member _.ChineseCopyProvidesFormalSceneTimeMenuAndStatusLabels () = + let labels = + [ sceneLabel VillageScene + sceneLabel Waterway + sceneLabel WhiteWallHome + sceneLabel StoneBridgeScene + sceneLabel BambooGrove + sceneLabel VegetableGardenScene + dayNightLabel DayTime + dayNightLabel NightTime + menuHeading MainMenuHeading + menuHeading SettingsHeading + menuHeading ControlsHeading + statusHeading StatusHeading + statusHeading ReadyStatus + statusHeading PausedStatus ] + + Assert.IsTrue(labels |> List.forall (fun label -> not (String.IsNullOrWhiteSpace label))) + Assert.IsTrue(labels |> List.forall (fun label -> not (label.Contains("?")))) + Assert.AreEqual<string>("江南水乡", copy.SceneTitle) + Assert.AreEqual<string>("准备就绪", copy.Ready) + + [<TestMethod>] + member _.ChineseTextWrapsAndUsesAnExplicitMissingGlyphPolicy () = + let supported = Set.ofList [ '江'; '南'; '水'; '乡' ] + let layout = layoutText 4 supported "江南水乡村" + + Assert.AreEqual<string list>([ "江南水乡"; "□" ], layout.Lines) + Assert.AreEqual<char list>([ '村' ], layout.MissingGlyphs) + Assert.IsFalse(String.concat "" layout.Lines |> fun text -> text.Contains("?")) diff --git a/src/LivingVillage.Desktop/ChineseText.fs b/src/LivingVillage.Desktop/ChineseText.fs new file mode 100644 index 0000000..b0c2fc6 --- /dev/null +++ b/src/LivingVillage.Desktop/ChineseText.fs @@ -0,0 +1,84 @@ +namespace LivingVillage.Desktop + +open System + +module ChineseText = + + type SceneKind = + | VillageScene + | Waterway + | WhiteWallHome + | StoneBridgeScene + | BambooGrove + | VegetableGardenScene + + type DayNightKind = + | DayTime + | NightTime + + type MenuHeadingKind = + | MainMenuHeading + | SettingsHeading + | ControlsHeading + + type StatusHeadingKind = + | StatusHeading + | ReadyStatus + | PausedStatus + + type ChineseCopy = + { SceneTitle: string + Ready: string } + + type TextLayout = + { Lines: string list + MissingGlyphs: char list } + + let sceneLabel scene = + match scene with + | VillageScene -> "江南水乡" + | Waterway -> "水道" + | WhiteWallHome -> "白墙黛瓦民居" + | StoneBridgeScene -> "石桥" + | BambooGrove -> "竹林" + | VegetableGardenScene -> "菜圃" + + let dayNightLabel time = + match time with + | DayTime -> "白天" + | NightTime -> "夜晚" + + let menuHeading heading = + match heading with + | MainMenuHeading -> "主菜单" + | SettingsHeading -> "设置" + | ControlsHeading -> "操作说明" + + let statusHeading status = + match status with + | StatusHeading -> "状态" + | ReadyStatus -> "准备就绪" + | PausedStatus -> "已暂停" + + let copy : ChineseCopy = + { SceneTitle = "江南水乡" + Ready = "准备就绪" } + + let layoutText (maxWidth: int) (supported: Set<char>) (text: string) : TextLayout = + let width = max 1 maxWidth + let normalized, missing = + text + |> Seq.fold + (fun (characters, missingGlyphs) character -> + if Set.contains character supported then + character :: characters, missingGlyphs + else + '□' :: characters, character :: missingGlyphs) + ([], []) + + { Lines = + normalized + |> List.rev + |> List.chunkBySize width + |> List.map (fun line -> new String(Array.ofList line)) + MissingGlyphs = List.rev missing } diff --git a/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj index fbb7c4a..171ba71 100644 --- a/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj +++ b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj @@ -7,6 +7,8 @@ <ItemGroup> <Compile Include="VillagePresentation.fs" /> + <Compile Include="VillageArt.fs" /> + <Compile Include="ChineseText.fs" /> <Compile Include="MenuState.fs" /> <Compile Include="Interaction.fs" /> <Compile Include="M6Presentation.fs" /> diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs new file mode 100644 index 0000000..0be4786 --- /dev/null +++ b/src/LivingVillage.Desktop/VillageArt.fs @@ -0,0 +1,95 @@ +namespace LivingVillage.Desktop + +open LivingVillage.Kernel.Sim +open LivingVillage.Desktop.VillagePresentation + +module VillageArt = + + type PropKind = + | WhiteWallDarkTileHouse + | StoneBridge + | Bamboo + | VegetableGarden + + type TileKind = + | RiverWater + | StonePaving + + type ArtProp = + { Kind: PropKind + Position: TilePosition } + + type ArtTile = + { Kind: TileKind + Position: TilePosition } + + type JiangnanArtPlan = + { Props: ArtProp list + Tiles: ArtTile list } + + type NpcVisualVariant = + | IndigoRobe + | OchreVest + | JadeSash + + type FacingDirection = + | NorthFacing + | SouthFacing + | WestFacing + | EastFacing + + type AnimationFrame = + | FrameOne + | FrameTwo + + type NpcSpriteSpec = + { Variant: NpcVisualVariant + Direction: FacingDirection + Frame: AnimationFrame } + + let private position x y : TilePosition = + { X = x + Y = y } + + let private prop kind x y : ArtProp = + { Kind = kind + Position = position x y } + + let private tile kind x y : ArtTile = + { Kind = kind + Position = position x y } + + let samplePlan () : JiangnanArtPlan = + { Props = + [ prop WhiteWallDarkTileHouse 6 4 + prop StoneBridge 10 4 + prop Bamboo 3 2 + prop VegetableGarden 8 7 ] + Tiles = + [ tile RiverWater 10 2 + tile RiverWater 10 3 + tile StonePaving 4 5 + tile StonePaving 5 5 + tile StonePaving 6 5 ] } + + let private positiveModulo divisor value = + let remainder = value % divisor + if remainder < 0 then remainder + divisor else remainder + + let npcVisualVariant (NpcId npcId) : NpcVisualVariant = + match positiveModulo 3 npcId with + | 0 -> IndigoRobe + | 1 -> OchreVest + | _ -> JadeSash + + let private positiveModulo64 divisor value = + let remainder = value % divisor + if remainder < 0L then remainder + divisor else remainder + + let animationFrame (tick: int64) : AnimationFrame = + if positiveModulo64 8L tick < 4L then FrameOne else FrameTwo + + let npcSpriteSpec (npcId: NpcId) (direction: FacingDirection) (tick: int64) : NpcSpriteSpec = + { Variant = npcVisualVariant npcId + Direction = direction + Frame = animationFrame tick } |
