namespace LivingVillage.Desktop open System open Microsoft.Xna.Framework open Microsoft.Xna.Framework.Graphics open Microsoft.Xna.Framework.Input open LivingVillage.Kernel open LivingVillage.Kernel.Sim type TileKind = | Grass = 0 | Path = 1 | Water = 2 | Flower = 3 module MapGen = let generate (width: int) (height: int) : TileKind[,] = let rnd = Random(20260918) let map = Array2D.zeroCreate width height for y in 0 .. height - 1 do for x in 0 .. width - 1 do let border = x = 0 || y = 0 || x = width - 1 || y = height - 1 map.[x, y] <- if border then TileKind.Water elif rnd.NextDouble() < 0.04 then TileKind.Flower else TileKind.Grass let midX = width / 2 let midY = height / 2 for x in 1 .. width - 2 do map.[x, midY] <- TileKind.Path for y in 1 .. height - 2 do map.[midX, y] <- TileKind.Path map module Atlas = let tilePixels = 32 let build (device: GraphicsDevice) : Texture2D = let rnd = Random(7) let c (r: int) (g: int) (b: int) = Color(r, g, b) let tex = new Texture2D(device, tilePixels * 4, tilePixels) let colors = Array.zeroCreate (tilePixels * tilePixels * 4) for t in 0 .. 3 do let baseColor, variance, isFlower = match t with | 1 -> (c 148 118 84), 12, false | 2 -> (c 52 92 168), 10, false | 3 -> (c 56 118 48), 14, true | _ -> (c 56 118 48), 14, false for y in 0 .. tilePixels - 1 do for x in 0 .. tilePixels - 1 do let jitter = rnd.Next(variance + 1) - variance / 2 colors.[t * tilePixels * tilePixels + y * tilePixels + x] <- c (int baseColor.R + jitter) (int baseColor.G + jitter) (int baseColor.B + jitter) if isFlower then for _ in 1 .. 6 do let fx = rnd.Next(tilePixels - 2) let fy = rnd.Next(tilePixels - 2) let fc = if rnd.NextDouble() < 0.5 then c 240 220 80 else c 235 240 245 for dy in 0 .. 1 do for dx in 0 .. 1 do colors.[t * tilePixels * tilePixels + (fy + dy) * tilePixels + fx + dx] <- fc tex.SetData(colors) tex module NpcView = let actionName (k: NpcActionKind) : string = match k with | Eat -> "eat" | Sleep -> "sleep" | Wander -> "wander" | Work -> "work" type LivingVillageGame() as this = inherit Game() let autoplay = Environment.GetEnvironmentVariable("LV_AUTOPLAY") = "1" let titleSuffix = if autoplay then " [AUTOPLAY]" else "" let modeName = if autoplay then "autoplay" else "keyboard" let graphics = new GraphicsDeviceManager(this) let mutable spriteBatch = Unchecked.defaultof let mutable atlas = Unchecked.defaultof let mutable pixel = Unchecked.defaultof let mutable map = MapGen.generate Sim.mapWidthTiles Sim.mapHeightTiles let mutable world = Sim.initialWorld 42UL let mutable camera = Vector2.Zero let mutable fpsFrames = 0 let mutable fpsSeconds = 0.0 do graphics.PreferredBackBufferWidth <- 1280 graphics.PreferredBackBufferHeight <- 720 graphics.SynchronizeWithVerticalRetrace <- true this.IsFixedTimeStep <- true this.TargetElapsedTime <- TimeSpan.FromTicks(TimeSpan.TicksPerSecond / 60L) this.Window.Title <- if autoplay then "Living Village M1 [AUTOPLAY]" else "Living Village M1" printfn $"mode={modeName} seed=42" member private this.CenterCamera() = let viewport = this.GraphicsDevice.Viewport let vw = float32 viewport.Width let vh = float32 viewport.Height let worldW = float32 (Sim.mapWidthTiles * Sim.tilePixels) let worldH = float32 (Sim.mapHeightTiles * Sim.tilePixels) let half = float32 Sim.tilePixels / 2.0f let x = MathHelper.Clamp(world.Avatar.Pos.X + half - vw / 2.0f, 0.0f, max 0.0f (worldW - vw)) let y = MathHelper.Clamp(world.Avatar.Pos.Y + half - vh / 2.0f, 0.0f, max 0.0f (worldH - vh)) camera <- Vector2(x, y) override _.LoadContent() = spriteBatch <- new SpriteBatch(this.GraphicsDevice) atlas <- Atlas.build this.GraphicsDevice pixel <- new Texture2D(this.GraphicsDevice, 1, 1) pixel.SetData([| Color.White |]) override this.Update(gameTime: GameTime) = let kb = Keyboard.GetState() if kb.IsKeyDown(Keys.Escape) then this.Exit() let input = if autoplay then let t = float32 world.Tick { MoveX = MathF.Sin(t / 60.0f) * 0.8f MoveY = MathF.Cos(t / 90.0f) * 0.6f } else { MoveX = (if kb.IsKeyDown(Keys.D) then 1.0f elif kb.IsKeyDown(Keys.A) then -1.0f else 0.0f) MoveY = (if kb.IsKeyDown(Keys.S) then 1.0f elif kb.IsKeyDown(Keys.W) then -1.0f else 0.0f) } let ts = { Input = input } world <- Sim.step ts world this.CenterCamera() fpsFrames <- fpsFrames + 1 fpsSeconds <- fpsSeconds + gameTime.ElapsedGameTime.TotalSeconds if fpsSeconds >= 1.0 then let fps = float fpsFrames / fpsSeconds fpsFrames <- 0 fpsSeconds <- 0.0 let npcAction = match world.Npcs with | npc :: _ -> NpcView.actionName npc.Mind.Action | [] -> "none" this.Window.Title <- $"Living Village M1{titleSuffix} | fps {fps:F1} | tick {world.Tick} | npc {npcAction}" printfn $"fps={fps:F1} tick={world.Tick} pos=({world.Avatar.Pos.X:F0},{world.Avatar.Pos.Y:F0})" override this.Draw(gameTime: GameTime) = this.GraphicsDevice.Clear(Color.CornflowerBlue) let viewport = this.GraphicsDevice.Viewport let camX = int camera.X let camY = int camera.Y let tx0 = max 0 (camX / Sim.tilePixels) let ty0 = max 0 (camY / Sim.tilePixels) let tx1 = min (Sim.mapWidthTiles - 1) ((camX + viewport.Width) / Sim.tilePixels + 1) let ty1 = min (Sim.mapHeightTiles - 1) ((camY + viewport.Height) / Sim.tilePixels + 1) spriteBatch.Begin() for y in ty0 .. ty1 do for x in tx0 .. tx1 do let src = Rectangle(int map.[x, y] * Sim.tilePixels, 0, Sim.tilePixels, Sim.tilePixels) let dst = Rectangle(x * Sim.tilePixels - camX, y * Sim.tilePixels - camY, Sim.tilePixels, Sim.tilePixels) spriteBatch.Draw(atlas, dst, src, Color.White) let avatarDst = Rectangle(int world.Avatar.Pos.X - camX, int world.Avatar.Pos.Y - camY, Sim.tilePixels, Sim.tilePixels) spriteBatch.Draw(pixel, avatarDst, Color.IndianRed) for npc in world.Npcs do let npcDst = Rectangle(int npc.Pos.X - camX, int npc.Pos.Y - camY, Sim.tilePixels, Sim.tilePixels) spriteBatch.Draw(pixel, npcDst, Color.Blue) spriteBatch.End()