summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/VillagePresentation.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-20 21:16:49 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-20 21:16:49 +0800
commitc5ff7685e5b3ac5dc46fce74ef1b302de09af1e6 (patch)
treec1fccb5d44bd6b055f5f1521ea259446e77057a5 /src/LivingVillage.Desktop/VillagePresentation.fs
parent5cb4970e2410a43e528b4b3e44dda15fac0a219a (diff)
downloadliving-village-c5ff7685e5b3ac5dc46fce74ef1b302de09af1e6.tar.gz
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.
Diffstat (limited to 'src/LivingVillage.Desktop/VillagePresentation.fs')
-rw-r--r--src/LivingVillage.Desktop/VillagePresentation.fs171
1 files changed, 171 insertions, 0 deletions
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。
+ **)
+
+ [<Struct>]
+ type TilePosition =
+ { X: int
+ Y: int }
+
+ [<Struct>]
+ type SimulationTick =
+ | SimulationTick of int64
+
+ [<Struct>]
+ 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<HomeMode, HomeTransitionError> =
+ 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<HomeMode, HomeTransitionError> =
+ 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 = "进入民居" }