diff options
Diffstat (limited to 'src/LivingVillage.Desktop/Game.fs')
| -rw-r--r-- | src/LivingVillage.Desktop/Game.fs | 261 |
1 files changed, 102 insertions, 159 deletions
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs index 5465ba2..dd04e26 100644 --- a/src/LivingVillage.Desktop/Game.fs +++ b/src/LivingVillage.Desktop/Game.fs @@ -6,71 +6,8 @@ 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<TileKind> 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<Color> (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" +open LivingVillage.Desktop.ChineseText +open LivingVillage.Desktop.VillagePresentation module PixelText = let private glyphs = @@ -110,29 +47,49 @@ module PixelText = '7', [| "11111"; "00001"; "00010"; "00100"; "01000"; "01000"; "01000" |] '8', [| "01110"; "10001"; "10001"; "01110"; "10001"; "10001"; "01110" |] '9', [| "01110"; "10001"; "10001"; "01111"; "00001"; "00001"; "01110" |] - '-', [| "00000"; "00000"; "00000"; "11111"; "00000"; "00000"; "00000" |] - '.', [| "00000"; "00000"; "00000"; "00000"; "00000"; "00110"; "00110" |] - '=', [| "00000"; "11111"; "00000"; "00000"; "11111"; "00000"; "00000" |] - ':', [| "00000"; "00110"; "00110"; "00000"; "00110"; "00110"; "00000" |] - ';', [| "00000"; "00110"; "00110"; "00000"; "00110"; "00100"; "01000" |] - '/', [| "00001"; "00010"; "00010"; "00100"; "01000"; "01000"; "10000" |] - '>', [| "10000"; "01000"; "00100"; "00010"; "00100"; "01000"; "10000" |] - '?', [| "01110"; "10001"; "00001"; "00010"; "00100"; "00000"; "00100" |] ] - |> Map.ofList + ('-', [| "00000"; "00000"; "00000"; "11111"; "00000"; "00000"; "00000" |]) + ('.', [| "00000"; "00000"; "00000"; "00000"; "00000"; "00110"; "00110" |]) + ('=', [| "00000"; "11111"; "00000"; "00000"; "11111"; "00000"; "00000" |]) + (':', [| "00000"; "00110"; "00110"; "00000"; "00110"; "00110"; "00000" |]) + (';', [| "00000"; "00110"; "00110"; "00000"; "00110"; "00100"; "01000" |]) + ('/', [| "00001"; "00010"; "00010"; "00100"; "01000"; "01000"; "10000" |]) + ('>', [| "10000"; "01000"; "00100"; "00010"; "00100"; "01000"; "10000" |]) + ('?', [| "01110"; "10001"; "00001"; "00010"; "00100"; "00000"; "00100" |]) ] + |> Map.ofList + + let isRenderable (text: string) : bool = + not (isNull text) + && text + |> Seq.forall (fun character -> + character = ' ' + || Map.containsKey (Char.ToUpperInvariant character) glyphs + || CjkGlyphAtlas.contains character) - let draw (spriteBatch: SpriteBatch) (pixel: Texture2D) (x: int) (y: int) (scale: int) (color: Color) (text: string) = - let fallback = glyphs.['?'] + let draw (spriteBatch: SpriteBatch) (pixel: Texture2D) (cjkAtlas: Texture2D) (x: int) (y: int) (scale: int) (color: Color) (text: string) = let mutable cursor = x for character in text.ToUpperInvariant() do if character = ' ' then cursor <- cursor + (4 * scale) + elif CjkGlyphAtlas.sourceRectangle character |> Option.isSome then + let source = CjkGlyphAtlas.sourceRectangle character |> Option.get + let glyphPixels = 8 * scale + spriteBatch.Draw( + cjkAtlas, + Rectangle(cursor, y, glyphPixels, glyphPixels), + Nullable<Rectangle>(source), + color) + cursor <- cursor + glyphPixels else - let rows = glyphs |> Map.tryFind character |> Option.defaultValue fallback - for row in 0 .. rows.Length - 1 do - for column in 0 .. rows.[row].Length - 1 do - if rows.[row].[column] = '1' then - spriteBatch.Draw(pixel, Rectangle(cursor + column * scale, y + row * scale, scale, scale), color) - cursor <- cursor + (6 * scale) + match glyphs |> Map.tryFind character with + | Some rows -> + for row in 0 .. rows.Length - 1 do + for column in 0 .. rows.[row].Length - 1 do + if rows.[row].[column] = '1' then + spriteBatch.Draw(pixel, Rectangle(cursor + column * scale, y + row * scale, scale, scale), color) + cursor <- cursor + (6 * scale) + | None -> + // Unsupported characters are intentionally blank, never a fake glyph. + cursor <- cursor + (6 * scale) type LivingVillageGame() as this = inherit Game() @@ -140,7 +97,6 @@ type LivingVillageGame() as this = let autoplay = Environment.GetEnvironmentVariable("LV_AUTOPLAY") = "1" let autoplayFlow = Environment.GetEnvironmentVariable("LV_AUTOPLAY_FLOW") = "1" let automated = autoplay || autoplayFlow - let titleSuffix = if automated then " [AUTOPLAY]" else "" let modeName = if autoplayFlow then "autoplay-flow" elif autoplay then "autoplay" @@ -160,11 +116,13 @@ type LivingVillageGame() as this = | path -> path let graphics = new GraphicsDeviceManager(this) let mutable spriteBatch = Unchecked.defaultof<SpriteBatch> - let mutable atlas = Unchecked.defaultof<Texture2D> + let mutable artTextures = Unchecked.defaultof<VillageArt.ArtTextures> + let mutable cjkAtlas = Unchecked.defaultof<Texture2D> let mutable pixel = Unchecked.defaultof<Texture2D> - let mutable map = MapGen.generate Sim.mapWidthTiles Sim.mapHeightTiles + let renderPlan = VillageArt.sampleRenderPlan () let mutable world = Sim.initialWorldN 42UL Sim.npcCount let mutable camera = Vector2.Zero + let mutable avatarFacing = VillageArt.SouthFacing let mutable fpsFrames = 0 let mutable fpsSeconds = 0.0 let mutable prevKb = Unchecked.defaultof<KeyboardState> @@ -183,7 +141,7 @@ type LivingVillageGame() as this = graphics.SynchronizeWithVerticalRetrace <- true this.IsFixedTimeStep <- true this.TargetElapsedTime <- TimeSpan.FromTicks(TimeSpan.TicksPerSecond / 60L) - this.Window.Title <- $"Living Village M6{titleSuffix}" + this.Window.Title <- ChineseText.copy.SceneTitle printfn $"mode={modeName} seed=42" printfn "controls=WASD move E interact 1-6 choose Q observe Tab needs C chronicle L relations P pause F1/F2/F3 speed F6 save F7 load Esc close/exit" member private this.CenterCamera() = @@ -200,7 +158,8 @@ type LivingVillageGame() as this = member private this.ResetWorldView() = relationView <- false relationSnapshot <- None - m5View <- M5Interaction.initial + avatarFacing <- VillageArt.SouthFacing + m5View <- M5Interaction.refreshPrompt world M5Interaction.initial this.CenterCamera() member private this.StartNewGame() = @@ -212,10 +171,12 @@ type LivingVillageGame() as this = member private this.SaveWorld (prefix: string) : bool = try WorldSave.saveToFile savePath world + m5View <- { m5View with Status = "save succeeded" } printfn $"{prefix}=ok path={savePath} tick={world.Tick}" true with | ex -> + m5View <- { m5View with Status = "save failed" } printfn $"{prefix}=error path={savePath} message={ex.Message}" false @@ -326,19 +287,19 @@ type LivingVillageGame() as this = this.OpenMainMenu() leftGame <- true if not loadFailed && not leftGame then - if lPressed then + if lPressed && M5Interaction.worldInputAllowed m5View 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}" let m5Command = - if pressed Keys.E then Some Interact - elif pressedAny [ Keys.D1; Keys.NumPad1 ] then Some Intent1 - elif pressedAny [ Keys.D2; Keys.NumPad2 ] then Some Intent2 - elif pressedAny [ Keys.D3; Keys.NumPad3 ] then Some Intent3 - elif pressedAny [ Keys.D4; Keys.NumPad4 ] then Some Intent4 - elif pressedAny [ Keys.D5; Keys.NumPad5 ] then Some Intent5 - elif pressedAny [ Keys.D6; Keys.NumPad6 ] then Some Intent6 + if M5Interaction.worldInputAllowed m5View && pressed Keys.E then Some Interact + elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D1; Keys.NumPad1 ] then Some Intent1 + elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D2; Keys.NumPad2 ] then Some Intent2 + elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D3; Keys.NumPad3 ] then Some Intent3 + elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D4; Keys.NumPad4 ] then Some Intent4 + elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D5; Keys.NumPad5 ] then Some Intent5 + elif m5View.Panel = DialoguePanel && pressedAny [ Keys.D6; Keys.NumPad6 ] then Some Intent6 elif pressed Keys.Q then Some Observe elif pressed Keys.Tab then Some ToggleNeeds elif pressed Keys.C then Some ShowChronicle @@ -370,8 +331,11 @@ type LivingVillageGame() as this = let stepCount = SimulationControl.stepsPerFrameWithLegacy legacyStepsPerFrame simulationControl if stepCount > 0 then for _ in 1 .. stepCount do + let previousPosition = world.Avatar.Pos world <- Sim.step ts world + avatarFacing <- VillageArt.facingAfterMovement previousPosition world.Avatar.Pos avatarFacing this.CenterCamera() + m5View <- M5Interaction.refreshPrompt world m5View member private this.RunAutoplayFlow() = let moveDown count = @@ -423,10 +387,18 @@ type LivingVillageGame() as this = override _.LoadContent() = spriteBatch <- new SpriteBatch(this.GraphicsDevice) - atlas <- Atlas.build this.GraphicsDevice + artTextures <- VillageArt.buildTextures this.GraphicsDevice + cjkAtlas <- CjkGlyphAtlas.load this.GraphicsDevice pixel <- new Texture2D(this.GraphicsDevice, 1, 1) pixel.SetData([| Color.White |]) + override _.UnloadContent() = + if not (isNull artTextures.TileAtlas) then artTextures.TileAtlas.Dispose() + if not (isNull artTextures.CharacterAtlas) then artTextures.CharacterAtlas.Dispose() + if not (isNull cjkAtlas) then cjkAtlas.Dispose() + if not (isNull pixel) then pixel.Dispose() + if not (isNull spriteBatch) then spriteBatch.Dispose() + override this.Update(gameTime: GameTime) = let kb = Keyboard.GetState() let pressed (key: Keys) = kb.IsKeyDown(key) && not (prevKb.IsKeyDown(key)) @@ -443,25 +415,6 @@ type LivingVillageGame() as this = 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 profile = M6Presentation.profileAtTick world.Tick - let dayNight = - match profile.Mode with - | Day -> "DAY" - | Night -> "NIGHT" - let viewTag = if relationView then " | [RELATIONS]" else "" - let pageName = - match menu.Page with - | MainMenu -> "MAIN MENU" - | Settings -> "SETTINGS" - | Controls -> "CONTROLS" - | Playing -> "PLAYING" - | LoadError -> "LOAD ERROR" - this.Window.Title <- - $"Living Village M6{titleSuffix} | {pageName} | fps {fps:F1} | tick {world.Tick} | {dayNight} | {M6Presentation.clockLabel simulationControl} | npc {npcAction}{viewTag} | panel {M5Interaction.panelName m5View} | {M5Interaction.titleText m5View}" printfn $"fps={fps:F1} tick={world.Tick} pos=({world.Avatar.Pos.X:F0},{world.Avatar.Pos.Y:F0})" prevKb <- kb @@ -529,7 +482,7 @@ type LivingVillageGame() as this = let textColor = Color(210, 226, 220) let mutedColor = Color(140, 170, 170) let drawText (x: int) (y: int) (color: Color) (text: string) = - PixelText.draw spriteBatch pixel x y textScale color text + PixelText.draw spriteBatch pixel cjkAtlas x y textScale color text let drawSelection (index: int) (label: string) (y: int) = let selected = index = menu.Selected let prefix = if selected then ">" else " " @@ -538,62 +491,59 @@ type LivingVillageGame() as this = spriteBatch.Draw(pixel, Rectangle(0, 0, width, height), Color(4, 9, 14, 190)) spriteBatch.Draw(pixel, Rectangle(panelX, panelY, panelWidth, panelHeight), Color(11, 19, 27, 245)) spriteBatch.Draw(pixel, Rectangle(panelX, panelY, panelWidth, 6), Color(92, 180, 190)) - drawText innerX (panelY + 28) (Color(235, 246, 232)) "LIVING VILLAGE" + drawText innerX (panelY + 28) (Color(235, 246, 232)) ChineseText.copy.SceneTitle let contentY = panelY + 100 match menu.Page with | MainMenu -> - drawText innerX (panelY + 70) mutedColor "MAIN MENU" + drawText innerX (panelY + 70) mutedColor (ChineseText.menuHeading MainMenuHeading) MenuState.items menu |> List.iteri (fun index item -> drawSelection index (MenuState.itemLabel item) (contentY + index * lineHeight)) - drawText innerX (panelY + panelHeight - 54) mutedColor "UP DOWN SELECT ENTER CONFIRM" + drawText innerX (panelY + panelHeight - 54) mutedColor "UP DOWN 选择 ENTER 确认" | Settings -> - drawText innerX (panelY + 70) mutedColor "SETTINGS" + drawText innerX (panelY + 70) mutedColor (ChineseText.menuHeading SettingsHeading) MenuState.settings |> List.iteri (fun index setting -> drawSelection index (MenuState.settingsLabel setting) (contentY + index * lineHeight)) - drawText innerX (panelY + panelHeight - 84) mutedColor (sprintf "ACTIVE %s" (M6Presentation.clockLabel menu.Settings)) - drawText innerX (panelY + panelHeight - 54) mutedColor "ENTER APPLY ESC BACK" + drawText innerX (panelY + panelHeight - 84) mutedColor (sprintf "当前 %s" (M6Presentation.clockLabel menu.Settings)) + drawText innerX (panelY + panelHeight - 54) mutedColor "ENTER 应用 ESC 返回" | Controls -> - drawText innerX (panelY + 70) mutedColor "CONTROLS" + drawText innerX (panelY + 70) mutedColor (ChineseText.menuHeading ControlsHeading) MenuState.controlRows |> List.iteri (fun index row -> drawText innerX (contentY + index * 25) textColor (sprintf "%s %s" row.Keys row.Action)) - drawText innerX (panelY + panelHeight - 28) mutedColor "ESC BACK" + drawText innerX (panelY + panelHeight - 28) mutedColor "ESC 返回" | LoadError -> - drawText innerX (panelY + 70) (Color(240, 130, 120)) "LOAD ERROR" - let message = menu.Error |> Option.defaultValue "NO SAVE AVAILABLE" |> fun value -> value.ToUpperInvariant() + drawText innerX (panelY + 70) (Color(240, 130, 120)) "读取失败" + let message = menu.Error |> Option.defaultValue "暂无存档" let visible = if message.Length > 34 then message.Substring(0, 34) else message drawText innerX contentY textColor visible - drawText innerX (contentY + lineHeight * 2) mutedColor "ENTER OR ESC BACK" + drawText innerX (contentY + lineHeight * 2) mutedColor "ENTER ESC 返回" | Playing -> () spriteBatch.End() member private this.DrawWorldView() = let profile = M6Presentation.profileAtTick world.Tick - this.GraphicsDevice.Clear(profile.Background) 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) + this.GraphicsDevice.Clear(profile.Background) 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, profile.WorldTint) - 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]) + match m5View.HomeMode with + | Inside _ -> + VillageArt.drawInterior spriteBatch pixel viewport.Width viewport.Height renderPlan.Interior + | Outside -> + VillageArt.drawWorld + spriteBatch + artTextures + camera + viewport.Width + viewport.Height + renderPlan + profile.WorldTint + + let avatarSpec = VillageArt.npcSpriteSpec (NpcId 0) avatarFacing world.Tick + VillageArt.drawCharacter spriteBatch artTextures camera world.Avatar.Pos avatarSpec profile.WorldTint + + for npc in world.Npcs do + let spec = VillageArt.npcSpriteSpecAtTarget npc.Id npc.Pos npc.Mind.Target world.Tick + VillageArt.drawCharacter spriteBatch artTextures camera npc.Pos spec profile.WorldTint spriteBatch.End() this.DrawM5Overlay() @@ -611,14 +561,7 @@ type LivingVillageGame() as this = let maxChars = max 1 (innerWidth / (6 * textScale)) let maxLines = max 1 ((panelHeight - 32) / lineHeight) let profile = M6Presentation.profileAtTick world.Tick - let dayNight = - match profile.Mode with - | Day -> "DAY" - | Night -> "NIGHT" - let m6Lines = - [ sprintf "TIME %s %s" dayNight (M6Presentation.clockLabel simulationControl) - "P PAUSE F1 F2 F3 SPEED" - "F6 SAVE F7 LOAD" ] + let m6Lines = [ sprintf "时间 %s %s" (M6Presentation.dayNightLabel profile.Mode) (M6Presentation.clockLabel simulationControl) ] let lines = (m6Lines @ M5Interaction.panelLines world m5View) |> List.truncate maxLines spriteBatch.Begin() spriteBatch.Draw(pixel, panel, Color(15, 19, 25, 220)) @@ -627,5 +570,5 @@ type LivingVillageGame() as this = |> List.iteri (fun index line -> let visible = if line.Length > maxChars then line.Substring(0, maxChars) else line let color = if index = 0 then Color(220, 235, 220) else Color(190, 205, 210) - PixelText.draw spriteBatch pixel innerX (panelY + 12 + index * lineHeight) textScale color visible) + PixelText.draw spriteBatch pixel cjkAtlas innerX (panelY + 12 + index * lineHeight) textScale color visible) spriteBatch.End() |
