summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 11:41:17 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 11:41:17 +0800
commit89be1bce62a01c6994c0de4d5c9f05338f3b4d4f (patch)
treeebba12c078880ae4017318d0c36fc12708221791 /src/LivingVillage.Desktop
parent7f0c40dc89672d218f10ba3f81a0ee235da30969 (diff)
downloadliving-village-89be1bce62a01c6994c0de4d5c9f05338f3b4d4f.tar.gz
feat(desktop): 江南氛围标题画面
新增 TitleScreen 纯布局模块(确定性、可测):五段暮色渐变天幕、黛瓦屋脊/檐口轮廓带(复用图集 8/9 号地块)、水面倒影带(1 号地块)、岸边灯笼光晕、江南水乡四倍大字标题;主菜单改为纵向简洁排布浮于其上,子页面板按内容自适应高度,去除全屏暗色遮罩大黑块。Xvfb 截图像素自查通过。
Diffstat (limited to 'src/LivingVillage.Desktop')
-rw-r--r--src/LivingVillage.Desktop/Game.fs88
-rw-r--r--src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj1
-rw-r--r--src/LivingVillage.Desktop/TitleScreen.fs85
-rw-r--r--src/LivingVillage.Desktop/VillageArt.fs3
4 files changed, 155 insertions, 22 deletions
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index 4871e80..219cc96 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -505,17 +505,27 @@ type LivingVillageGame() as this =
this.DrawWorldView()
member private this.DrawMenu() =
- this.DrawWorldView()
let viewport = this.GraphicsDevice.Viewport
let width = viewport.Width
let height = viewport.Height
- let panelWidth = min 720 (max 1 (width - 64))
- let panelHeight = min 640 (max 1 (height - 48))
- let panelX = (width - panelWidth) / 2
- let panelY = (height - panelHeight) / 2
- let innerX = panelX + 32
- let textScale = 3
- let lineHeight = 30
+ let layout = TitleScreen.layout width height
+ let panelWidth = layout.PanelWidth
+ let panelX = layout.PanelX
+ let panelY = layout.PanelY
+ let contentRows =
+ match menu.Page with
+ | MainMenu -> MenuState.items menu |> List.length
+ | Settings -> MenuState.settings.Length
+ | Controls -> MenuState.controlRows.Length
+ | LoadError -> 4
+ | Playing -> 0
+ let rowHeight = if menu.Page = Controls then 25 else layout.MenuLineHeight
+ let panelHeight =
+ min (height - panelY - 16)
+ (max layout.PanelHeight (120 + contentRows * rowHeight + 48))
+ let innerX = layout.MenuX
+ let textScale = 2
+ let lineHeight = layout.MenuLineHeight
let selectedColor = Color(245, 222, 120)
let textColor = Color(210, 226, 220)
let mutedColor = Color(140, 170, 170)
@@ -526,33 +536,67 @@ type LivingVillageGame() as this =
let prefix = if selected then ">" else " "
drawText innerX y (if selected then selectedColor else textColor) (sprintf "%s %s" prefix label)
spriteBatch.Begin()
- 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)) ChineseText.copy.SceneTitle
- let contentY = panelY + 100
+
+ // Layered dusk gradient: sky bands darken toward the horizon, never a flat block.
+ let bandCount = List.length layout.SkyBands
+ let bandHeight = height / bandCount
+ List.iteri (fun index (band: TitleScreen.ColorRgb) ->
+ let y0 = index * bandHeight
+ let y1 = if index = bandCount - 1 then height else y0 + bandHeight
+ spriteBatch.Draw(pixel, Rectangle(0, y0, width, y1 - y0), Color(band.R, band.G, band.B))) layout.SkyBands
+
+ // Jiangnan skyline: eave tiles continuously, ridge tiles rising above them.
+ List.iteri (fun column slot ->
+ let x = column * Sim.tilePixels
+ let source = VillageArt.atlasTileRectangle slot
+ if slot = fst TitleScreen.roofSlots then
+ spriteBatch.Draw(artTextures.TileAtlas, Rectangle(x, layout.RidgeY, Sim.tilePixels, Sim.tilePixels), source, Color.White)
+ spriteBatch.Draw(artTextures.TileAtlas, Rectangle(x, layout.EaveY, Sim.tilePixels, Sim.tilePixels), VillageArt.atlasTileRectangle (snd TitleScreen.roofSlots), Color.White))
+ layout.RoofTileSlots
+
+ // Water band drawn from the atlas water tile, reflecting the skyline above.
+ for row in layout.WaterRows do
+ for x in 0 .. Sim.tilePixels .. width - 1 do
+ spriteBatch.Draw(artTextures.TileAtlas, Rectangle(x, row, Sim.tilePixels, Sim.tilePixels), VillageArt.atlasTileRectangle TitleScreen.waterSlot, Color(150, 170, 178))
+
+ // Lantern glows along the water bank.
+ let lanternColor, lanternHalo = TitleScreen.lanternColors
+ for (lx, ly) in layout.Lanterns do
+ spriteBatch.Draw(pixel, Rectangle(lx - 8, ly - 8, 24, 24), Color(lanternHalo.R, lanternHalo.G, lanternHalo.B, 70))
+ spriteBatch.Draw(pixel, Rectangle(lx - 3, ly - 5, 8, 10), Color(lanternColor.R, lanternColor.G, lanternColor.B))
+ spriteBatch.Draw(pixel, Rectangle(lx - 1, ly - 7, 4, 2), Color(90, 60, 40))
+ spriteBatch.Draw(pixel, Rectangle(lx - 2, ly + 5, 6, 2), Color(90, 60, 40))
+
+ // Title: large CJK atlas glyphs with a soft shadow for readability.
+ PixelText.draw spriteBatch pixel cjkAtlas (layout.TitleX + 4) (layout.TitleY + 4) layout.TitleScale (Color(12, 20, 28)) layout.TitleText
+ PixelText.draw spriteBatch pixel cjkAtlas layout.TitleX layout.TitleY layout.TitleScale (Color(235, 246, 232)) layout.TitleText
+
+ // Menu panel floats over the lower half; translucent so the scene layers stay visible.
+ spriteBatch.Draw(pixel, Rectangle(panelX, panelY, panelWidth, panelHeight), Color(11, 19, 27, 200))
+ spriteBatch.Draw(pixel, Rectangle(panelX, panelY, panelWidth, 4), Color(92, 180, 190))
+ let contentY = layout.MenuTopY
match menu.Page with
| MainMenu ->
- drawText innerX (panelY + 70) mutedColor (ChineseText.menuHeading MainMenuHeading)
+ drawText innerX (panelY + 24) 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 选择 ENTER 确认"
+ drawText innerX (panelY + panelHeight - 40) mutedColor "UP DOWN 选择 ENTER 确认"
| Settings ->
- drawText innerX (panelY + 70) mutedColor (ChineseText.menuHeading SettingsHeading)
+ drawText innerX (panelY + 24) 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 "当前 %s" (M6Presentation.clockLabel menu.Settings))
- drawText innerX (panelY + panelHeight - 54) mutedColor "ENTER 应用 ESC 返回"
+ drawText innerX (panelY + panelHeight - 72) mutedColor (sprintf "当前 %s" (M6Presentation.clockLabel menu.Settings))
+ drawText innerX (panelY + panelHeight - 40) mutedColor "ENTER 应用 ESC 返回"
| Controls ->
- drawText innerX (panelY + 70) mutedColor (ChineseText.menuHeading ControlsHeading)
+ drawText innerX (panelY + 24) 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 返回"
+ drawText innerX (panelY + panelHeight - 40) mutedColor "ESC 返回"
| LoadError ->
- drawText innerX (panelY + 70) (Color(240, 130, 120)) "读取失败"
+ drawText innerX (panelY + 24) (Color(240, 130, 120)) "读取失败"
let message = menu.Error |> Option.defaultValue "暂无存档"
- let visible = if message.Length > 34 then message.Substring(0, 34) else message
+ let visible = if message.Length > 24 then message.Substring(0, 24) else message
drawText innerX contentY textColor visible
drawText innerX (contentY + lineHeight * 2) mutedColor "ENTER ESC 返回"
| Playing -> ()
diff --git a/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj
index fcb1883..33b998d 100644
--- a/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj
+++ b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj
@@ -15,6 +15,7 @@
<Compile Include="Interaction.fs" />
<Compile Include="SampleScript.fs" />
<Compile Include="M6Presentation.fs" />
+ <Compile Include="TitleScreen.fs" />
<Compile Include="Game.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
diff --git a/src/LivingVillage.Desktop/TitleScreen.fs b/src/LivingVillage.Desktop/TitleScreen.fs
new file mode 100644
index 0000000..8b1c5d5
--- /dev/null
+++ b/src/LivingVillage.Desktop/TitleScreen.fs
@@ -0,0 +1,85 @@
+namespace LivingVillage.Desktop
+
+module TitleScreen =
+
+ type ColorRgb =
+ { R: int
+ G: int
+ B: int }
+
+ type TitleLayout =
+ { SkyBands: ColorRgb list
+ RoofTileSlots: int list
+ RidgeY: int
+ EaveY: int
+ WaterRows: int list
+ Lanterns: (int * int) list
+ TitleText: string
+ TitleX: int
+ TitleY: int
+ TitleScale: int
+ MenuX: int
+ MenuTopY: int
+ MenuLineHeight: int
+ PanelX: int
+ PanelY: int
+ PanelWidth: int
+ PanelHeight: int }
+
+ // Palette mirrors the VillageArt atlas: roof (45,49,57)/(30,33,38), water (54,116,145),
+ // lantern paper (212,92,74). Purely decorative reuse of existing art colors.
+ let private skyBands : ColorRgb list =
+ [ { R = 16; G = 26; B = 42 }
+ { R = 20; G = 36; B = 52 }
+ { R = 24; G = 46; B = 62 }
+ { R = 28; G = 56; B = 68 }
+ { R = 32; G = 64; B = 72 } ]
+
+ let private roofRidgeSlot = 8
+ let private roofEaveSlot = 9
+ let private waterTileSlot = 1
+ let private lanternColor = { R = 212; G = 92; B = 74 }
+ let private lanternHalo = { R = 252; G = 216; B = 148 }
+
+ let lanternColors : ColorRgb * ColorRgb = lanternColor, lanternHalo
+ let skyBandCount = skyBands.Length
+ let waterSlot = waterTileSlot
+ let roofSlots = roofRidgeSlot, roofEaveSlot
+
+ let layout (viewportWidth: int) (viewportHeight: int) : TitleLayout =
+ let width = max 320 viewportWidth
+ let height = max 240 viewportHeight
+ let tilePixels = 32
+ let columns = width / tilePixels
+ let roofTileSlots =
+ [ 0 .. columns - 1 ]
+ |> List.map (fun column ->
+ if column % 4 = 0 || column % 7 = 3 then roofRidgeSlot else roofEaveSlot)
+ let waterRows =
+ [ height - 96
+ height - 64 ]
+ let lanternCount = max 3 (columns / 10)
+ let lanterns =
+ [ 1 .. lanternCount ]
+ |> List.map (fun index ->
+ let x = index * width / (lanternCount + 1)
+ let y = (List.head waterRows) - 44 + (if index % 2 = 0 then 12 else 0)
+ x, y)
+ let panelWidth = min 460 (width - 64)
+ { SkyBands = skyBands
+ RoofTileSlots = roofTileSlots
+ RidgeY = 44
+ EaveY = 76
+ WaterRows = waterRows
+ Lanterns = lanterns
+ TitleText = "江南水乡"
+ TitleX = (width - 4 * (4 * 16)) / 2
+ TitleY = 120
+ TitleScale = 4
+ MenuX = (width - panelWidth) / 2 + 40
+ MenuTopY = height / 2 - 40
+ MenuLineHeight = 30
+ PanelX = (width - panelWidth) / 2
+ PanelY = height / 2 - 64
+ PanelWidth = panelWidth
+ PanelHeight = min 360 (height - height / 2 - 40) }
diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs
index 3b948e5..db25757 100644
--- a/src/LivingVillage.Desktop/VillageArt.fs
+++ b/src/LivingVillage.Desktop/VillageArt.fs
@@ -343,6 +343,9 @@ module VillageArt =
CharacterAtlas = loadTexture device characterAtlasRelativePath characterAtlasWidth characterAtlasHeight
InteriorAtlas = loadTexture device interiorAtlasRelativePath interiorAtlasWidth interiorAtlasHeight }
+ let atlasTileRectangle (index: int) : Microsoft.Xna.Framework.Rectangle =
+ XnaRectangle(index * Sim.tilePixels, 0, Sim.tilePixels, Sim.tilePixels)
+
let private tileSourceRectangle sprite : XnaRectangle =
let index =
match sprite with