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" | Chat -> "chat" | 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 stepsPerFrame = match Environment.GetEnvironmentVariable("LV_STEPS_PER_FRAME") with | null | "" -> 1 | s -> match Int32.TryParse s with | true, n when n > 0 -> n | _ -> 1 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.initialWorldN 42UL Sim.npcCount let mutable camera = Vector2.Zero let mutable fpsFrames = 0 let mutable fpsSeconds = 0.0 let mutable prevKb = Unchecked.defaultof let mutable relationView = false let mutable relationSnapshot: float32[,] option = None 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 this.Initialize() = base.Initialize() prevKb <- Keyboard.GetState() 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 lPressed = kb.IsKeyDown(Keys.L) && not (prevKb.IsKeyDown(Keys.L)) prevKb <- kb if lPressed then relationView <- not relationView if relationView then relationSnapshot <- Some(Sim.relationMatrix world) let state = if relationView then "on" else "off" printfn $"relation view={state} tick={world.Tick}" if not relationView then 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 } for _ in 1 .. stepsPerFrame do 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 = if world.Npcs.Length > 0 then NpcView.actionName world.Npcs.[0].Mind.Action else "none" let viewTag = if relationView then " | [RELATIONS]" else "" this.Window.Title <- $"Living Village M3b{titleSuffix} | fps {fps:F1} | tick {world.Tick} | npc {npcAction}{viewTag}" printfn $"fps={fps:F1} tick={world.Tick} pos=({world.Avatar.Pos.X:F0},{world.Avatar.Pos.Y:F0})" member private this.DrawLine (a: Vector2) (b: Vector2) (thickness: float32) (color: Color) = let d = b - a let len = d.Length() if len > 1.0f then let angle = MathF.Atan2(d.Y, d.X) spriteBatch.Draw(pixel, a, System.Nullable(), color, angle, Vector2.Zero, Vector2(len, thickness), SpriteEffects.None, 0.0f) member private this.DrawRelationView() = this.GraphicsDevice.Clear(Color(16, 20, 26)) let viewport = this.GraphicsDevice.Viewport let vw = float32 viewport.Width let vh = float32 viewport.Height let mapW = float32 (Sim.mapWidthTiles * Sim.tilePixels) let mapH = float32 (Sim.mapHeightTiles * Sim.tilePixels) let margin = 48.0f let scale = min ((vw - 2.0f * margin) / mapW) ((vh - 2.0f * margin) / mapH) let ox = (vw - mapW * scale) / 2.0f let oy = (vh - mapH * scale) / 2.0f let toScreen (p: Vec2) : Vector2 = Vector2(ox + p.X * scale, oy + p.Y * scale) spriteBatch.Begin() match relationSnapshot with | Some m -> let n = Array2D.length1 m for i in 0 .. n - 1 do for j in i + 1 .. n - 1 do let r = m.[i, j] if abs r > Sim.relationThreshold then let a = toScreen world.Npcs.[i].Pos let b = toScreen world.Npcs.[j].Pos let color = if r > 0.0f then Color.LimeGreen else Color.IndianRed let thickness = Sim.clamp (1.5f + 3.0f * abs r) 1.5f 12.0f this.DrawLine a b thickness color for npc in world.Npcs do let p = toScreen npc.Pos spriteBatch.Draw(pixel, Rectangle(int p.X - 5, int p.Y - 5, 10, 10), Color.Cyan) | None -> () spriteBatch.End() override this.Draw(gameTime: GameTime) = if relationView then this.DrawRelationView() else this.DrawWorldView() member private this.DrawWorldView() = 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.Red) let npcShades = [| Color.Navy; Color.MediumBlue; Color.Blue; Color.RoyalBlue; Color.DodgerBlue |] for npc in world.Npcs do let npcDst = Rectangle(int npc.Pos.X - camX, int npc.Pos.Y - camY, Sim.tilePixels, Sim.tilePixels) let (NpcId idx) = npc.Id spriteBatch.Draw(pixel, npcDst, npcShades.[idx % npcShades.Length]) spriteBatch.End()