diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj | 1 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/P78NightAmbienceTests.fs | 62 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/Game.fs | 131 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/SceneDetail.fs | 70 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/SceneDetailRender.fs | 75 |
5 files changed, 332 insertions, 7 deletions
diff --git a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj index 1ea6349..5f2ac14 100644 --- a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj +++ b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj @@ -49,6 +49,7 @@ <Compile Include="P75HudStripTests.fs" /> <Compile Include="P76HotbarPanelTests.fs" /> <Compile Include="P77LayoutTests.fs" /> + <Compile Include="P78NightAmbienceTests.fs" /> <Compile Include="SampleTests.fs" /> </ItemGroup> diff --git a/src/LivingVillage.Desktop.Tests/P78NightAmbienceTests.fs b/src/LivingVillage.Desktop.Tests/P78NightAmbienceTests.fs new file mode 100644 index 0000000..2a06798 --- /dev/null +++ b/src/LivingVillage.Desktop.Tests/P78NightAmbienceTests.fs @@ -0,0 +1,62 @@ +namespace LivingVillage.Desktop.Tests + +open Microsoft.VisualStudio.TestTools.UnitTesting +open LivingVillage.Desktop + +/// P78 夜晚氛围与场景细节回归(纯函数层): +/// * 夜帧水面逐格微光斑:只落在水面/水田、帧号驱动且确定性、昼夜两档(白天恒 0); +/// * 炊烟:生成器世界每户一个烟源、夜帧 alpha/尺寸更大、升腾摇晃有界且确定性; +/// * 桥头灯柱近圈光晕:半径恒为 1 tile(窄光晕),复用既有灯柱瓦片。 +[<TestClass>] +type P78NightAmbienceTests () = + + let map = MapGen.generate (MapGen.defaultParams 42UL) + + [<TestMethod>] + member _.WaterGlintsAreNightOnlyDeterministicAndTwoTier () = + let glints = SceneDetail.waterGlintTiles map + Assert.IsTrue(not (List.isEmpty glints), "生成器世界须有水面光斑候选格") + for (x, y) in glints do + Assert.IsTrue(SceneDetail.isWaterSurface map x y, sprintf "光斑 (%d,%d) 必须落在水面/水田" x y) + // 白天恒定关闭(零回归),夜帧两个亮度档。 + for phase in 0 .. SceneDetail.waterGlintPhaseCount - 1 do + Assert.IsTrue((SceneDetail.waterGlintAlpha 0.0f phase) = 0, "白天光斑必须为 0(零回归)") + let nightBright = SceneDetail.waterGlintAlpha 1.0f 0 + let nightDim = SceneDetail.waterGlintAlpha 1.0f 1 + Assert.IsTrue(nightBright > 0, "夜帧光斑须可见") + Assert.IsTrue(nightBright > nightDim && nightDim > 0, "光斑须有两档呼吸层次") + // 帧号驱动且确定性。 + for tick in [ 0L; 8L; 16L; 32L ] do + let phase = SceneDetail.waterGlintPhase tick 3 5 + Assert.IsTrue(phase >= 0 && phase < SceneDetail.waterGlintPhaseCount, "相位越界") + Assert.IsTrue((SceneDetail.waterGlintPhase tick 3 5) = phase, "相位须确定性") + Assert.IsTrue(abs (SceneDetail.waterGlintOffset tick 3 5) <= 1, "光斑位移须有界 ±1px") + Assert.IsTrue( + (SceneDetail.waterGlintPhase 0L 3 5) <> (SceneDetail.waterGlintPhase 8L 3 5) + || (SceneDetail.waterGlintPhase 8L 3 5) <> (SceneDetail.waterGlintPhase 16L 3 5) + || (SceneDetail.waterGlintPhase 16L 3 5) <> (SceneDetail.waterGlintPhase 24L 3 5), + "光斑相位须随帧号变化") + + [<TestMethod>] + member _.SmokeIsProminentAtNightAndDeterministic () = + let sources = SceneDetail.smokeSourceTiles map + let expected = (map.Buildings @ map.Farmhouses) |> List.length + Assert.IsTrue(not (List.isEmpty sources), "生成器世界须有炊烟源(民居/农舍)") + Assert.IsTrue(List.length sources <= expected, "去重后炊烟源数不得超过户数") + let dayAlpha = SceneDetail.smokeAlphaAt 0.0f + let nightAlpha = SceneDetail.smokeAlphaAt 1.0f + Assert.IsTrue(dayAlpha > 0, "白天炊烟仍须可辨") + Assert.IsTrue(nightAlpha > dayAlpha, "夜帧炊烟须更醒目") + Assert.IsTrue((SceneDetail.smokeCellSize 1.0f) > (SceneDetail.smokeCellSize 0.0f), "夜帧炊烟体量须更大") + for tick in [ 0L; 24L; 48L ] do + let frame = SceneDetail.smokeFrame tick + Assert.IsTrue(frame >= 0 && frame <= 2, "炊烟需三相位") + let sway, rise = SceneDetail.smokeDrift tick 7 + Assert.IsTrue(abs sway <= 1, "炊烟摇曳须有界 ±1px") + Assert.IsTrue(rise <= 0 && rise >= -4, "炊烟只向上且位移有界") + Assert.IsTrue((SceneDetail.smokeDrift tick 7) = (sway, rise), "炊烟位移须确定性") + + [<TestMethod>] + member _.BridgeHaloIsNarrowAndMatchesLanternTiles () = + Assert.IsTrue((SceneDetail.bridgeHaloRadiusTiles = 1), "桥头光晕须为窄光晕(1 tile)") + Assert.IsTrue((SceneDetail.bridgeLanternTiles map) = (VillageArt.bridgeLanternTiles map), "桥头灯柱瓦片须同源") diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs index 8c1ea21..a83f04c 100644 --- a/src/LivingVillage.Desktop/Game.fs +++ b/src/LivingVillage.Desktop/Game.fs @@ -396,6 +396,10 @@ type LivingVillageGame() as this = // 6/12/18/22 时各拍一帧(同机位),四张真实帧用于四时段光照档差异核验。 let p73ShotMode = Environment.GetEnvironmentVariable("LV_P73_SHOT") = "1" let mutable p73Step = 0 + // P78 证据钩子:LV_P78_SHOT=1 时在同一机位拍「白天 / 夜晚」两帧,机位同时纳入桥头灯柱、 + // 河面与一户带炊烟的民居,用于核验夜帧水面光斑/桥头窄光晕/炊烟与白天零回归。 + let p78ShotMode = Environment.GetEnvironmentVariable("LV_P78_SHOT") = "1" + let mutable p78Step = 0 // P52 证据钩子:LV_P52_SHOT=1 时选职业开局 → 走到 NPC 旁用真实 Interact 开对话 → // 真实 BuyFromTarget / SellToTarget 各一次,拍「对话入口 / 买入后 / 卖出后」三帧; // 每笔交易打印钱与背包数量前后值,供 analyze 脚本算 delta。 @@ -1482,7 +1486,114 @@ type LivingVillageGame() as this = let placeCameraOn (tileX: int) (tileY: int) = world <- { world with Avatar = { world.Avatar with Pos = { X = float32 (tileX * Sim.tilePixels); Y = float32 (tileY * Sim.tilePixels) } } } this.CenterCamera() - if p73ShotMode then + if p78ShotMode then + // 机位:倒影最长的桥头灯柱 → 其最近一户民居的中点;同机位先拍正午(白天对照), + // 再拍 22 时(夜帧),两帧差异只来自光照与夜帧加成层。 + match riverscapeMap with + | Some map -> + let bridgePick = + SceneDetail.bridgeLanternTiles map + |> List.sortByDescending (fun (x, y) -> SceneDetail.reflectionLength map x y) + |> List.tryHead + match bridgePick with + | None -> + printfn "p78-shot=error no-bridge-lantern" + this.Exit() + | Some (bx, by) -> + let nearestHouse = + (map.Buildings @ map.Farmhouses) + |> List.sortBy (fun b -> abs (b.DoorX - bx) + abs (b.DoorY - by)) + |> List.tryHead + let camTileX, camTileY = + match nearestHouse with + | Some b -> (bx + b.DoorX) / 2, (by + b.DoorY) / 2 + | None -> bx, by + let waterBounds () = + let vw = this.GraphicsDevice.Viewport.Width + let vh = this.GraphicsDevice.Viewport.Height + let visible = + [ for y in 0 .. map.Height - 1 do + for x in 0 .. map.Width - 1 do + if SceneDetail.isWaterSurface map x y then + let sx = x * Sim.tilePixels - int camera.X + let sy = y * Sim.tilePixels - int camera.Y + if sx + Sim.tilePixels > 0 && sx < vw && sy + Sim.tilePixels > 0 && sy < vh then + yield (sx, sy) ] + match visible with + | [] -> (0, 0, 0, 0) + | _ -> + let xs = visible |> List.map fst + let ys = visible |> List.map snd + let x0, y0 = List.min xs, List.min ys + let x1 = List.max xs + Sim.tilePixels + let y1 = List.max ys + Sim.tilePixels + (x0, y0, x1 - x0, y1 - y0) + let smokeScreen = + match nearestHouse with + | Some b -> (b.DoorX * Sim.tilePixels - int camera.X, (b.DoorY - 3) * Sim.tilePixels - int camera.Y) + | None -> (-1, -1) + let vw = this.GraphicsDevice.Viewport.Width + let vh = this.GraphicsDevice.Viewport.Height + let screenX (x: int) = x * Sim.tilePixels - int camera.X + let screenY (y: int) = y * Sim.tilePixels - int camera.Y + let visibleTile (x: int) (y: int) = + screenX x >= 0 && screenX x + Sim.tilePixels <= vw && screenY y >= 0 && screenY y + Sim.tilePixels <= vh + let glintTiles = SceneDetail.waterGlintTiles map + let glintPick = glintTiles |> List.tryPick (fun (x, y) -> if visibleTile x y then Some(x, y) else None) + let glintCtl = + glintPick + |> Option.bind (fun (gx, gy) -> + [ (gx + 1, gy); (gx + 2, gy); (gx - 1, gy) ] + |> List.tryFind (fun (cx, cy) -> SceneDetail.isWaterSurface map cx cy && not (List.contains (cx, cy) glintTiles))) + let bridgeScreen = SceneDetail.bridgeLanternTiles map |> List.tryFind (fun (x, y) -> visibleTile x y) + let regionFmt (x, y) = sprintf "%d,%d" (screenX x) (screenY y) + let regionLine (tag: string) = + printfn + "p78-regions %s glint_src=%s glint_ctl=%s bridge_src=%s" + tag + (match glintPick with Some p -> regionFmt p | None -> "-1,-1") + (match glintCtl with Some p -> regionFmt p | None -> "-1,-1") + (match bridgeScreen with Some p -> regionFmt p | None -> "-1,-1") + if p78Step = 0 then + setHour 12.0 + placeCameraOn camTileX camTileY + p47PendingName <- "p78-day" + p47Pending <- true + let wx, wy, ww, wh = waterBounds () + printfn + "p78-shot=day hour=12.0 tick=%d lantern=%.2f water=%d,%d,%d,%d glints=%d smoke=%d bridge_halo=%d" + world.Tick + (M6Presentation.effectiveLanternGlow world.Tick) + wx wy ww wh + (List.length (SceneDetail.waterGlintTiles map)) + (List.length (SceneDetail.smokeSourceTiles map)) + (List.length (SceneDetail.bridgeLanternTiles map)) + regionLine "day" + elif p78Step = 1 then + // 保持同一机位,只把时刻改到夜晚。 + setHour 22.0 + p47PendingName <- "p78-night" + p47Pending <- true + let wx, wy, ww, wh = waterBounds () + printfn + "p78-shot=night hour=22.0 tick=%d lantern=%.2f water=%d,%d,%d,%d smoke_src=%d,%d smoke_alpha=%d smoke_cell=%d glint_alpha=%d" + world.Tick + (M6Presentation.effectiveLanternGlow world.Tick) + wx wy ww wh + (fst smokeScreen) (snd smokeScreen) + (SceneDetail.smokeAlphaAt (M6Presentation.effectiveLanternGlow world.Tick)) + (SceneDetail.smokeCellSize (M6Presentation.effectiveLanternGlow world.Tick)) + (SceneDetail.waterGlintAlpha + (M6Presentation.effectiveLanternGlow world.Tick) + (SceneDetail.waterGlintPhase world.Tick camTileX camTileY)) + regionLine "night" + else + printfn "p78-shot=done" + this.Exit() + | None -> + printfn "p78-shot=error no-riverscape-map" + this.Exit() + elif p73ShotMode then // 同一桥头光源机位的四时段巡拍;tick 只经 setHour 改变,保证画面差异只来自光照档。 let hours = [ ("p73-morning", 6.0); ("p73-noon", 12.0); ("p73-dusk", 18.0); ("p73-night", 22.0) ] if p73Step < hours.Length then @@ -1626,6 +1737,10 @@ type LivingVillageGame() as this = printfn "p73-frame=%s tick=%d pos=(%.0f,%.0f) camera=(%.0f,%.0f)" p47PendingName world.Tick world.Avatar.Pos.X world.Avatar.Pos.Y camera.X camera.Y p73Step <- p73Step + 1 + elif p78ShotMode then + printfn "p78-frame=%s tick=%d camera=(%.0f,%.0f)" + p47PendingName world.Tick camera.X camera.Y + p78Step <- p78Step + 1 else printfn "p47-shot=%s tick=%d pos=(%.0f,%.0f) camera=(%.0f,%.0f)" p47PendingName world.Tick world.Avatar.Pos.X world.Avatar.Pos.Y camera.X camera.Y @@ -2431,7 +2546,7 @@ type LivingVillageGame() as this = elif p57ShotMode then if menu.Page = MainMenu then this.PrepareP57Shot() - elif p47ShotMode || p73ShotMode then + elif p47ShotMode || p73ShotMode || p78ShotMode then if menu.Page = MainMenu then this.DispatchMenuInput Confirm elif menu.Page = OccupationSelect then @@ -2925,7 +3040,7 @@ type LivingVillageGame() as this = this.CaptureP46Shot() if p57ShotMode then this.CaptureP57Shot() - if (p47ShotMode || p73ShotMode) && menu.Page = Playing then + if (p47ShotMode || p73ShotMode || p78ShotMode) && menu.Page = Playing then this.CaptureP47Shot() if p52ShotMode && menu.Page = Playing then this.CaptureP52Shot() @@ -3259,7 +3374,7 @@ type LivingVillageGame() as this = VillageArt.drawCc0BoatOverlay spriteBatch artTextures camera map worldTint | _ -> () - // P47 场景细节:河道倒影 + 岸边芦苇加密度 + 漂浮元素 + 近景楼层层错落。 + // P47 场景细节:河道倒影 + 岸边芦苇加密度 + 漂浮元素 + 近景楼层层错落 + P78 炊烟。 // 画在夜光/船之后,保证倒影不被灯笼呼吸光晕盖掉、仍可辨认。 match riverscapeMap with | Some map -> @@ -3272,6 +3387,7 @@ type LivingVillageGame() as this = viewport.Height map world.Tick + lanternGlow | None -> () VillageArt.drawGroundShadow spriteBatch pixel camera world.Avatar.Pos @@ -3304,6 +3420,13 @@ type LivingVillageGame() as this = if period.VeilAlpha > 0 then let v = period.Veil spriteBatch.Draw(pixel, Rectangle(0, 0, viewport.Width, viewport.Height), VillageArt.premultiply (int v.R) (int v.G) (int v.B) period.VeilAlpha) + + // P78:夜帧加成层(水面逐格微光斑 + 桥头灯柱窄光晕)。画在全部 veil 之后, + // 故不被夜色乘暗、暗部层次仍可读;白天 lanternGlow=0,本层为空、白天帧零回归。 + match riverscapeMap with + | Some map when lanternGlow > 0.0f -> + SceneDetailRender.drawNight spriteBatch pixel camera map world.Tick lanternGlow + | _ -> () spriteBatch.End() // Contextual floating prompt over the unique target; same CJK atlas font path as the HUD. diff --git a/src/LivingVillage.Desktop/SceneDetail.fs b/src/LivingVillage.Desktop/SceneDetail.fs index 50313ac..54665ac 100644 --- a/src/LivingVillage.Desktop/SceneDetail.fs +++ b/src/LivingVillage.Desktop/SceneDetail.fs @@ -144,7 +144,75 @@ module SceneDetail = let floatFrame (tick: int64) (seed: int) : int = int (positiveModulo64 2L (tick / 32L + int64 seed)) - // ---------------- 近景楼层层错落(屏幕空间前景层) ---------------- + // ---------------- P78 夜帧水面逐格微光斑 ---------------- + + /// 光斑候选水面格:水面/水田按位置 hash 稀疏取样(约 1/6 水面格),位置确定性。 + /// 帧号只驱动相位/位移,同帧同 tick 逐字节可复现。 + let waterGlintTiles (map: MapGen.Result) : (int * int) list = + if map.Width <= 0 then + [] + else + [ for y in 1 .. map.Height - 2 do + for x in 0 .. map.Width - 1 do + if isWaterSurface map x y && (x * 5 + y * 3) % 6 = 0 then + yield (x, y) ] + + let waterGlintPhaseCount = 4 + + /// 逐格相位(0..3),只随 tick 慢速切换,让相邻光斑不同步闪烁。 + let waterGlintPhase (tick: int64) (x: int) (y: int) : int = + int (positiveModulo64 4L (tick / 8L + int64 (x * 3 + y * 5))) + + /// 光斑 alpha:夜越深越亮;相位在满亮/六成亮间呼吸,保证暗部有两档以上层次。 + let waterGlintAlpha (nightStrength: float32) (phase: int) : int = + if nightStrength <= 0.0f then + 0 + else + let baseAlpha = 46.0f + 78.0f * max 0.0f (min 1.0f nightStrength) + int (baseAlpha * (if phase % 2 = 0 then 1.0f else 0.6f)) + + /// 帧号驱动的光斑水平位移(±1px),镜像水面波纹,同帧确定性。 + let waterGlintOffset (tick: int64) (x: int) (y: int) : int = + int (positiveModulo64 3L (tick / 16L + int64 (x + y))) - 1 + + // ---------------- P78 炊烟(生成器世界) ---------------- + + /// 生成器世界的炊烟源:每户民居与散落农舍屋顶脊上一格(沿用样例世界 0,-3 偏移)。 + let smokeSourceTiles (map: MapGen.Result) : (int * int) list = + (map.Buildings @ map.Farmhouses) + |> List.map (fun building -> building.DoorX, building.DoorY - 3) + |> List.distinct + + /// 炊烟三相位(复用 VillageArt 的确定性帧),供绘制取精灵。 + let smokeFrame (tick: int64) : int = + VillageArt.smokeFrameTick tick + + /// 炊烟上升/摇曳位移:随 tick 缓慢升高并左右摆动,有界、逐帧连续。 + let smokeDrift (tick: int64) (seed: int) : int * int = + let rise = int (positiveModulo64 5L (tick / 24L + int64 seed)) + let sway = int (positiveModulo64 3L (tick / 32L + int64 (seed * 3 + 1))) - 1 + (sway, -rise) + + /// 炊烟 alpha:夜帧更醒目(用户要求夜间存在感更强),白天仍可辨。 + let smokeAlphaAt (nightStrength: float32) : int = + let night = max 0.0f (min 1.0f nightStrength) + int (78.0f + 104.0f * night) + + /// 炊烟绘制边长:夜帧略放大,增强存在感。 + let smokeCellSize (nightStrength: float32) : int = + let night = max 0.0f (min 1.0f nightStrength) + int (20.0f + 8.0f * night) + + // ---------------- P78 桥头灯柱近圈光晕 ---------------- + + /// 桥头灯柱瓦片(与 VillageArt.bridgeLanternTiles 同源,供窄光晕绘制与取证计数)。 + let bridgeLanternTiles (map: MapGen.Result) : (int * int) list = + VillageArt.bridgeLanternTiles map + + /// 窄光晕半径(tile):固定 1,只围灯柱一圈,不铺满整条河。 + let bridgeHaloRadiusTiles = 1 + + /// ---------------- 近景楼层层错落(屏幕空间前景层) ---------------- /// 前景层屋顶分段:等宽 tile 分段、高度按确定性函数错落(0/8/16/24 px 四层), /// 并随 tick 极慢移动(视差)。返回 (x, y, width)。 diff --git a/src/LivingVillage.Desktop/SceneDetailRender.fs b/src/LivingVillage.Desktop/SceneDetailRender.fs index abe0b89..df2fa10 100644 --- a/src/LivingVillage.Desktop/SceneDetailRender.fs +++ b/src/LivingVillage.Desktop/SceneDetailRender.fs @@ -79,7 +79,76 @@ module SceneDetailRender = spriteBatch.Draw(pixel, Rectangle(x, y - Sim.tilePixels, half, 2 * Sim.tilePixels), litColor) spriteBatch.Draw(pixel, Rectangle(x + half, y - Sim.tilePixels, w - half, 2 * Sim.tilePixels), darkColor) - /// 一次性绘制全部 P47 场景细节(倒影 → 岸边芦苇 → 漂浮元素 → 近景前景层)。 + /// P78 炊烟:生成器世界每户屋顶脊上方一缕,帧号驱动升腾/摇曳,夜帧更醒目。 + /// 复用江南图集的 22-24 三相位烟精灵,只读、不改瓦片。 + let private drawSmoke + (spriteBatch: SpriteBatch) + (textures: VillageArt.ArtTextures) + (camera: Vector2) + (map: MapGen.Result) + (tick: int64) + (nightStrength: float32) = + let alpha = SceneDetail.smokeAlphaAt nightStrength + let size = SceneDetail.smokeCellSize nightStrength + let source = VillageArt.atlasTileRectangle (22 + SceneDetail.smokeFrame tick) + let color = + if nightStrength > 0.5f then VillageArt.premultiply 238 230 214 alpha + else VillageArt.premultiply 222 226 228 alpha + for (x, y) in SceneDetail.smokeSourceTiles map do + let seed = x * 29 + y * 11 + let dx, dy = SceneDetail.smokeDrift tick seed + let cx = tileX camera x + (Sim.tilePixels - size) / 2 + dx + let cy = tileY camera y + (Sim.tilePixels - size) / 2 + dy + spriteBatch.Draw(textures.TileAtlas, Rectangle(cx, cy, size, size), source, color) + + /// P78 夜帧水面逐格微光斑:帧号驱动、位置确定性,暗部因此有两档以上层次而非整片死黑。 + let private drawNightWater + (spriteBatch: SpriteBatch) + (pixel: Texture2D) + (camera: Vector2) + (map: MapGen.Result) + (tick: int64) + (nightStrength: float32) = + for (x, y) in SceneDetail.waterGlintTiles map do + let alpha = SceneDetail.waterGlintAlpha nightStrength (SceneDetail.waterGlintPhase tick x y) + if alpha > 0 then + let wobble = SceneDetail.waterGlintOffset tick x y + let cx = tileX camera x + let cy = tileY camera y + let soft = VillageArt.premultiply 150 190 232 (alpha / 3) + let cool = VillageArt.premultiply 198 224 255 alpha + spriteBatch.Draw(pixel, Rectangle(cx + 6 + wobble, cy + 12, 20, 4), soft) + spriteBatch.Draw(pixel, Rectangle(cx + 10 + wobble, cy + 13, 11, 2), cool) + + /// P78 桥头灯柱窄光晕:只围灯柱一圈(半径 1 tile),夜里给灯柱一圈暖光而不铺满河面。 + let private drawBridgeHalos + (spriteBatch: SpriteBatch) + (pixel: Texture2D) + (camera: Vector2) + (map: MapGen.Result) + (nightStrength: float32) = + let peak = int (78.0f * max 0.0f (min 1.0f nightStrength)) + if peak > 0 then + let half = Sim.tilePixels / 2 + let radius = SceneDetail.bridgeHaloRadiusTiles * Sim.tilePixels + for (x, y) in SceneDetail.bridgeLanternTiles map do + let cx = tileX camera x + half + let cy = tileY camera y + half + for (rect, color) in VillageArt.lanternGlowLayers cx cy radius 5 255 214 150 peak do + spriteBatch.Draw(pixel, rect, color) + + /// P78 夜帧加成层(水面光斑 + 桥头窄光晕):在 veil 之后调用,故不被夜色乘暗、仍醒目。 + let drawNight + (spriteBatch: SpriteBatch) + (pixel: Texture2D) + (camera: Vector2) + (map: MapGen.Result) + (tick: int64) + (nightStrength: float32) = + drawNightWater spriteBatch pixel camera map tick nightStrength + drawBridgeHalos spriteBatch pixel camera map nightStrength + + /// 一次性绘制全部 P47 场景细节(倒影 → 岸边芦苇 → 漂浮元素 → 近景前景层 → P78 炊烟)。 let draw (spriteBatch: SpriteBatch) (textures: VillageArt.ArtTextures) @@ -88,8 +157,10 @@ module SceneDetailRender = (viewportWidth: int) (viewportHeight: int) (map: MapGen.Result) - (tick: int64) = + (tick: int64) + (nightStrength: float32) = drawReflections spriteBatch pixel camera map tick drawBankReeds spriteBatch textures camera map drawFloaters spriteBatch textures camera map tick drawForeground spriteBatch textures pixel viewportWidth viewportHeight tick + drawSmoke spriteBatch textures camera map tick nightStrength |
