From ebe7f80bcf54189cc51a15bf17b99293b6ff57a3 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Tue, 22 Sep 2026 09:26:53 +0800 Subject: feat(desktop): P26 夜晚冷月光 veil(纯渲染层)+ 灯笼光晕径向无方斑回归 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/LivingVillage.Desktop.Tests/DesktopTests.fs | 24 +++++++++++++++++++++++ src/LivingVillage.Desktop.Tests/PrototypeTests.fs | 22 +++++++++++++++++++++ src/LivingVillage.Desktop/Game.fs | 8 ++++++++ src/LivingVillage.Desktop/M6Presentation.fs | 12 ++++++++++++ 4 files changed, 66 insertions(+) (limited to 'src') diff --git a/src/LivingVillage.Desktop.Tests/DesktopTests.fs b/src/LivingVillage.Desktop.Tests/DesktopTests.fs index b78f4e0..7e4a849 100644 --- a/src/LivingVillage.Desktop.Tests/DesktopTests.fs +++ b/src/LivingVillage.Desktop.Tests/DesktopTests.fs @@ -159,6 +159,30 @@ type DesktopTests () = Assert.IsTrue(noon.Background.B > noon.Background.G) Assert.IsTrue(noon.Background.B > noon.Background.R) + [] + member _.MoonlightVeilIsCoolAndOnlyAtNight () = + let noonTick = 12L * Sim.ticksPerDay / 24L + let midnightTick = 0L + let deepNightTick = 23L * Sim.ticksPerDay / 24L + Assert.IsTrue(M6Presentation.moonlightVeilAlpha noonTick = 0, "daytime must have no moonlight") + + let moon = M6Presentation.moonlightVeilAlpha midnightTick + Assert.IsTrue(moon > 0, "deep night must get a cool moonlight wash") + Assert.AreEqual(moon, M6Presentation.moonlightVeilAlpha deepNightTick) + Assert.IsTrue(moon <= 72, "moonlight veil stays a low alpha so the scene is not washed out") + + let r, g, b = M6Presentation.moonlightRgb + Assert.IsTrue(b > g && g > r, "moonlight must be cool: blue-dominant channels") + + // Dusk is owned by the warm golden veil, so moonlight is suppressed there. + let duskTick = M6Presentation.startTickForHour 20.75 + Assert.IsTrue(M6Presentation.moonlightVeilAlpha duskTick < moon, "dusk is warmed, not moonlit") + + // Premultiplied premultiply of this colour never saturates to white. + let veiled = VillageArt.premultiply r g b moon + Assert.IsTrue(int veiled.B <= int veiled.A) + Assert.IsTrue(int (max veiled.R (max veiled.G veiled.B)) < 250) + [] member _.DaylightAutoplayBootstrapStartsAtDawnAndPreservesDefaultStart () = let dawnTick = 6L * Sim.ticksPerDay / 24L diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs index dd4e7b3..bc3425a 100644 --- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs +++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs @@ -640,6 +640,28 @@ type PrototypeTests () = Assert.IsFalse(covers (centerX + radius) (centerY + radius), "a square corner must stay dark: the glow is circular") Assert.IsFalse(covers (centerX - radius - 5) centerY) + [] + member _.LanternGlowIsRadialWithoutSquareCorners () = + // P26 复核:光晕为同心圆叠加,逐像素累积 alpha;边缘/四角不得出现方斑。 + let centerX, centerY, radius, rings, peakAlpha = 80, 80, 40, 6, 140 + let layers = VillageArt.lanternGlowLayers centerX centerY radius rings 255 214 150 peakAlpha + let size = radius * 2 + 3 + let origin = centerX - radius - 1 + let alpha = Array2D.zeroCreate size size + for (rect, color) in layers do + for y in rect.Y .. rect.Y + rect.Height - 1 do + for x in rect.X .. rect.X + rect.Width - 1 do + alpha.[y - origin, x - origin] <- alpha.[y - origin, x - origin] + int color.A + let center = alpha.[radius + 1, radius + 1] + Assert.IsTrue(center > 0, "the core must be lit") + let corner = [ (0, 0); (size - 1, 0); (0, size - 1); (size - 1, size - 1) ] + for (cx, cy) in corner do + Assert.IsTrue(alpha.[cy, cx] = 0, "glow corner must stay empty: no square patch") + // Radial falloff along the centre row: alpha never rises as distance from the core grows. + let row = [ for dx in 0 .. radius -> alpha.[radius + 1, radius + 1 + dx] ] + Assert.IsTrue(row |> List.pairwise |> List.forall (fun (a, b) -> a >= b), "glow must fade radially, not form a flat block") + Assert.IsTrue(row.[0] = center && row.[radius] < center, "core is brightest, rim is dimmer") + [] member _.CharacterFeetSitOnTheTileGroundLine () = Assert.AreEqual(16, VillageArt.characterFeetOffset) diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs index c6da443..537ae3b 100644 --- a/src/LivingVillage.Desktop/Game.fs +++ b/src/LivingVillage.Desktop/Game.fs @@ -1186,6 +1186,14 @@ type LivingVillageGame() as this = let veilAlpha = M6Presentation.goldenVeilAlpha world.Tick if veilAlpha > 0 then spriteBatch.Draw(pixel, Rectangle(0, 0, viewport.Width, viewport.Height), VillageArt.premultiply 255 150 70 veilAlpha) + + // Cool moonlight wash on top of the night scene: a premultiplied blue veil whose + // alpha is a pure tick function (zero by day, suppressed through dusk/dawn warmth). + // Like the golden veil it adds light and can never saturate to white. + let moonAlpha = M6Presentation.moonlightVeilAlpha world.Tick + if moonAlpha > 0 then + let mr, mg, mb = M6Presentation.moonlightRgb + spriteBatch.Draw(pixel, Rectangle(0, 0, viewport.Width, viewport.Height), VillageArt.premultiply mr mg mb moonAlpha) spriteBatch.End() // Contextual floating prompt over the unique target; same CJK atlas font path as the HUD. diff --git a/src/LivingVillage.Desktop/M6Presentation.fs b/src/LivingVillage.Desktop/M6Presentation.fs index 90d2e65..825847a 100644 --- a/src/LivingVillage.Desktop/M6Presentation.fs +++ b/src/LivingVillage.Desktop/M6Presentation.fs @@ -65,6 +65,18 @@ module M6Presentation = let goldenVeilAlpha (tick: int64) : int = int (150.0f * transitionWarmth tick) + /// Cool moonlight wash applied as a premultiplied veil over the finished night scene. + /// Multiplicative night tint can only darken, so this additive cool layer is what makes deep + /// night read moon-blue. It is zero whenever the sun lights the scene (blend 0) and is + /// suppressed through the warm dawn/dusk windows (warmth), so it never competes with the + /// golden veil. Purely desktop render state; the Kernel never sees it. + let moonlightRgb : int * int * int = (120, 170, 235) + + let moonlightVeilAlpha (tick: int64) : int = + let blend = lightingBlend tick + let warmth = transitionWarmth tick + int (72.0f * blend * (1.0f - warmth)) + let private lerp (a: int) (b: int) (blend: float32) : int = a + int (float32 (b - a) * blend) -- cgit v1.2.3