From c5ff7685e5b3ac5dc46fce74ef1b302de09af1e6 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Sun, 20 Sep 2026 21:16:49 +0800 Subject: feat(desktop): add Chinese village presentation prototype [Change Nature] - This commit adds a deterministic presentation-model feature, not MonoGame rendering. [New Capability] - Provide an immutable Jiangnan village scene with homes, waterways, paths, bridge, bamboo, and garden elements. - Provide typed home transitions, four-way input mapping, tick animation, NPC styles, and Chinese stage-one labels. [Implementation] - Add the pure VillagePresentation module and six focused MSTest cases. - Keep the model free of mutable global state and update only the required F# compile order. [Impact] - This commit affects the Desktop presentation and test projects only. - Kernel behavior, Game.fs wiring, MonoGame drawing, and existing acceptance artifacts remain unchanged. --- .../LivingVillage.Desktop.fsproj | 1 + src/LivingVillage.Desktop/VillagePresentation.fs | 171 +++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 src/LivingVillage.Desktop/VillagePresentation.fs (limited to 'src/LivingVillage.Desktop') diff --git a/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj index 36f1216..fbb7c4a 100644 --- a/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj +++ b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj @@ -6,6 +6,7 @@ + diff --git a/src/LivingVillage.Desktop/VillagePresentation.fs b/src/LivingVillage.Desktop/VillagePresentation.fs new file mode 100644 index 0000000..95dc178 --- /dev/null +++ b/src/LivingVillage.Desktop/VillagePresentation.fs @@ -0,0 +1,171 @@ +namespace LivingVillage.Desktop + +module VillagePresentation = + + (** + 中文江南水乡阶段一样板的纯展示快照。 + + TilePosition 使用整数瓦片坐标;一个坐标单位代表一块场景瓦片,原点和 + 屏幕像素换算留给后续 MonoGame 组合层。SimulationTick 使用 int64,表示 + Kernel 同一时间轴上的一个模拟 tick。PrototypeScene 以及其中的 record + 和 list 都是不可变快照,调用方只能读取,不能把它们当作可变模拟状态; + 本模块不保存可变全局状态,也不依赖 MonoGame。 + **) + + [] + type TilePosition = + { X: int + Y: int } + + [] + type SimulationTick = + | SimulationTick of int64 + + [] + type HomeId = + | HomeId of int + + type VillageProp = + | WhiteWallDarkTileHome + | HomeDoor + | RiverChannel + | RiverBank + | StoneBridge + | StonePaving + | Bamboo + | VegetableGarden + + type VillageElement = + { Prop: VillageProp + Position: TilePosition } + + type Home = + { Id: HomeId + Door: TilePosition } + + type PrototypeScene = + { Elements: VillageElement list + Homes: Home list + InitialPlayerPosition: TilePosition } + + type Direction = + | North + | South + | West + | East + + type DirectionInput = + | UpInput + | DownInput + | LeftInput + | RightInput + | NoInput + + type CharacterAnimationFrame = + | FrameA + | FrameB + + type CharacterStyle = + | StrawHatFarmer + | BlueApronVillager + | GreenScarfVillager + + type HomeMode = + | Outside + | Inside of HomeId + + type HomeTransitionError = + | NotNearHomeDoor + | AlreadyInsideHome of HomeId + | NotInsideHome + | HomeNotFound of HomeId + + type StageOneCopy = + { Interact: string + Needs: string + Trade: string + EnterHome: string } + + let private tile x y : TilePosition = + { X = x; Y = y } + + let private element prop position : VillageElement = + { Prop = prop + Position = position } + + let createScene () : PrototypeScene = + let home = + { Id = HomeId 1 + Door = tile 6 5 } + + { Elements = + [ element WhiteWallDarkTileHome (tile 6 4) + element HomeDoor home.Door + element StonePaving (tile 4 5) + element StonePaving (tile 5 5) + element StonePaving (tile 6 5) + element StonePaving (tile 7 5) + element RiverBank (tile 10 2) + element RiverBank (tile 10 3) + element RiverChannel (tile 10 4) + element RiverChannel (tile 10 5) + element StoneBridge (tile 10 4) + element Bamboo (tile 3 2) + element Bamboo (tile 3 3) + element VegetableGarden (tile 8 7) + element VegetableGarden (tile 9 7) ] + Homes = [ home ] + InitialPlayerPosition = tile 5 5 } + + let scene : PrototypeScene = + createScene () + + let private manhattanDistance first second : int = + abs (first.X - second.X) + abs (first.Y - second.Y) + + let isNearHomeDoor (position: TilePosition) (home: Home) : bool = + manhattanDistance position home.Door <= 1 + + let enterHome (position: TilePosition) (mode: HomeMode) : Result = + match mode with + | Inside homeId -> Error(AlreadyInsideHome homeId) + | Outside -> + match scene.Homes |> List.tryFind (isNearHomeDoor position) with + | Some home -> Ok(Inside home.Id) + | None -> Error NotNearHomeDoor + + let exitHome (mode: HomeMode) : Result = + match mode with + | Outside -> Error NotInsideHome + | Inside homeId -> + if scene.Homes |> List.exists (fun home -> home.Id = homeId) then + Ok Outside + else + Error(HomeNotFound homeId) + + let directionForInput (input: DirectionInput) : Direction option = + match input with + | UpInput -> Some North + | DownInput -> Some South + | LeftInput -> Some West + | RightInput -> Some East + | NoInput -> None + + let private positiveModulo divisor value = + let remainder = value % divisor + if remainder < 0L then remainder + divisor else remainder + + let animationFrame (SimulationTick tick) : CharacterAnimationFrame = + if positiveModulo 8L tick < 4L then FrameA else FrameB + + let characterStyle (LivingVillage.Kernel.Sim.NpcId npcId) : CharacterStyle = + match ((npcId % 3) + 3) % 3 with + | 0 -> StrawHatFarmer + | 1 -> BlueApronVillager + | _ -> GreenScarfVillager + + let stageOneCopy : StageOneCopy = + { Interact = "互动" + Needs = "需求" + Trade = "交易" + EnterHome = "进入民居" } -- cgit v1.2.3