diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-21 11:41:17 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-21 11:41:17 +0800 |
| commit | 89be1bce62a01c6994c0de4d5c9f05338f3b4d4f (patch) | |
| tree | ebba12c078880ae4017318d0c36fc12708221791 /src/LivingVillage.Desktop | |
| parent | 7f0c40dc89672d218f10ba3f81a0ee235da30969 (diff) | |
| download | living-village-89be1bce62a01c6994c0de4d5c9f05338f3b4d4f.tar.gz | |
feat(desktop): 江南氛围标题画面
新增 TitleScreen 纯布局模块(确定性、可测):五段暮色渐变天幕、黛瓦屋脊/檐口轮廓带(复用图集 8/9 号地块)、水面倒影带(1 号地块)、岸边灯笼光晕、江南水乡四倍大字标题;主菜单改为纵向简洁排布浮于其上,子页面板按内容自适应高度,去除全屏暗色遮罩大黑块。Xvfb 截图像素自查通过。
Diffstat (limited to 'src/LivingVillage.Desktop')
| -rw-r--r-- | src/LivingVillage.Desktop/Game.fs | 88 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj | 1 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/TitleScreen.fs | 85 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/VillageArt.fs | 3 |
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 |
