summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/Game.fs
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Desktop/Game.fs')
-rw-r--r--src/LivingVillage.Desktop/Game.fs261
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()