namespace LivingVillage.Desktop /// P47 场景细节升级(纯函数层):河道倒影、岸边植被加密度、漂浮元素、近景楼层层错落。 /// 全部为帧计数 / 地图的纯函数:同参数逐字节可复现,无时钟、无随机源。 module SceneDetail = open LivingVillage.Kernel let private positiveModulo64 (modulus: int64) (value: int64) : int64 = let remainder = value % modulus if remainder < 0L then remainder + modulus else remainder /// 统一光源:来自画面左上,所有阴影一律投向右下(方向统一,不随物体改变)。 let shadowOffsetPx : int * int = (3, 4) let isWater (map: MapGen.Result) (x: int) (y: int) : bool = x >= 0 && x < map.Width && y >= 0 && y < map.Height && map.Tiles.[y * map.Width + x] = int MapGen.GroundTile.Water /// 可倒影水面:河面(Water)与水田(PaddyField,同样走水面动画渲染)。 let isWaterSurface (map: MapGen.Result) (x: int) (y: int) : bool = x >= 0 && x < map.Width && y >= 0 && y < map.Height && (map.Tiles.[y * map.Width + x] = int MapGen.GroundTile.Water || map.Tiles.[y * map.Width + x] = int MapGen.GroundTile.PaddyField) // ---------------- 河道水面倒影 ---------------- let reflectionMaxAlpha = 200 let reflectionRange = 4 /// 倒影射线允许穿过的非水面格数(如灯笼所立的桥面/岸沿),使「桥下红灯笼」的倒影 /// 仍落在其正下方的河面,而不是被桥面格截断。 let reflectionGap = 3 /// 倒影 alpha 随离光源距离递减;最低仍保持 96,保证整条暖色光柱在蓝灰水面上 /// 始终可辨(不会淡到被水面波纹吞掉)。 let reflectionAlphaAt (distance: int) : int = max 96 (reflectionMaxAlpha - (distance - 1) * 26) /// 倒影微抖:复用 waterFrameTickAt 的 3 相位,映射到 -1 / 0 / +1 像素水平位移。 let reflectionWobbleAt (tick: int64) (x: int) (y: int) : int = match VillageArt.waterFrameTickAt tick x y with | 0 -> -1 | 1 -> 0 | _ -> 1 /// 光源类型:桥头灯笼 / 主路石灯笼 / 门口红灯笼。倒影按类型着色。 type LightKind = | BridgeLantern | RoadLantern | RedLantern /// 岸上光源(含类型),按位置去重。 let lightSources (map: MapGen.Result) : (int * int * LightKind) list = let bridge = VillageArt.bridgeLanternTiles map |> List.map (fun (x, y) -> x, y, BridgeLantern) let road = VillageArt.cc0LanternTiles map 12 |> List.map (fun (x, y) -> x, y, RoadLantern) let red = VillageArt.cc0RedLanternTiles map |> List.map (fun (x, y) -> x, y, RedLantern) (bridge @ road @ red) |> List.distinctBy (fun (x, y, _) -> x, y) let lightSourceTiles (map: MapGen.Result) : (int * int) list = lightSources map |> List.map (fun (x, y, _) -> x, y) /// 某个光源向下的倒影:先穿过至多 reflectionGap 格非水面(桥面/岸沿),落到正下方 /// 水面上,再沿连续水面画最多 reflectionRange 格;返回 (x, y, alpha, kind)。 let reflectionStreakFor (map: MapGen.Result) (sx: int) (sy: int) (kind: LightKind) : (int * int * int * LightKind) list = let mutable start = sy + 1 let mutable gap = 0 while gap <= reflectionGap && start < map.Height && not (isWaterSurface map sx start) do start <- start + 1 gap <- gap + 1 if start >= map.Height || gap > reflectionGap || not (isWaterSurface map sx start) then [] else [ for d in 0 .. reflectionRange - 1 do let y = start + d if isWaterSurface map sx y then yield (sx, y, reflectionAlphaAt (d + 1), kind) ] /// 光源正下方倒影的格数(用于挑选倒影长度足够的取证机位)。 let reflectionLength (map: MapGen.Result) (x: int) (sy: int) : int = lightSources map |> List.filter (fun (sx, syy, _) -> sx = x && syy = sy) |> List.tryHead |> Option.map (fun (sx, syy, kind) -> reflectionStreakFor map sx syy kind |> List.length) |> Option.defaultValue 0 /// 每个光源在其正下方连续水面格生成一条垂直倒影 (x, y, alpha, kind),最长 4 格。 let reflectionStreaks (map: MapGen.Result) : (int * int * int * LightKind) list = lightSources map |> List.collect (fun (sx, sy, kind) -> reflectionStreakFor map sx sy kind) // ---------------- 岸边植被加密度 ---------------- /// 在既有 `cc0ReedTiles(spacing 4)`(x%4=0 的贴水陆格)基础上补一层: /// x%4<>0、且四邻任一为水的陆格,按位置 hash 取一半。于是芦苇带更密, /// 又与既有芦苇错列不重叠。 let bankReedTiles (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 let nearWater = isWater map (x - 1) y || isWater map (x + 1) y || isWater map x (y - 1) || isWater map x (y + 1) if not (isWater map x y) && nearWater && x % 4 <> 0 && (x * 7 + y * 13) % 2 = 0 then yield (x, y) ] // ---------------- 漂浮元素(鸭子浮标 / 漂浮落叶) ---------------- type FloatKind = | DuckFloat | FallenLeaf let floatKindOf (x: int) (y: int) : FloatKind = if (x * 3 + y * 5) % 3 = 0 then DuckFloat else FallenLeaf /// 确定性落在开阔水面的漂浮元素(2 格连续水面、非桥面),数量即「元素数量指标」。 let floatingProps (map: MapGen.Result) : (int * int * FloatKind) list = if map.Width <= 4 then [] else let bridgeSet = map.Bridges |> List.map (fun (x, y) -> y * map.Width + x) |> Set.ofList let isBridge x y = Set.contains (y * map.Width + x) bridgeSet [ for y in 2 .. map.Height - 3 do for x in 2 .. map.Width - 3 do if isWater map x y && isWater map (x + 1) y && not (isBridge x y) && not (isBridge (x + 1) y) && (x * 11 + y * 17) % 23 = 0 then yield (x, y, floatKindOf x y) ] /// 按 tick 的慢漂移(像素):水平 ±2、垂直 ±1,纯函数、有界、逐帧连续。 let floatDrift (tick: int64) (seed: int) : int * int = let dx = int (positiveModulo64 5L (tick / 24L + int64 seed)) - 2 let dy = int (positiveModulo64 3L (tick / 40L + int64 (seed * 7 + 1))) - 1 (dx, dy) /// 该漂浮元素的帧(0/1),只随 tick 慢速切换,保证水面鸭/叶有轻微起伏。 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 + 12.0f * night) // ---------------- P78 桥头灯柱近圈光晕 ---------------- /// 桥头灯柱瓦片(与 VillageArt.bridgeLanternTiles 同源,供窄光晕绘制与取证计数)。 let bridgeLanternTiles (map: MapGen.Result) : (int * int) list = VillageArt.bridgeLanternTiles map /// 窄光晕半径(tile):固定 1,只围灯柱一圈,不铺满整条河。 let bridgeHaloRadiusTiles = 1 // ---------------- P79 夜帧炊烟增强(烟柱 + 外发光) ---------------- /// 夜帧烟柱分段数:向上堆叠成可辨烟柱。 let nightSmokeSegments = 4 /// 每段抬升像素。 let nightSmokeSegmentLift = 10 /// 夜帧烟柱 alpha:显著高于白天 underlay,裸眼可从屋顶看到每一缕。 let nightSmokeAlphaAt (nightStrength: float32) : int = int (180.0f + 75.0f * max 0.0f (min 1.0f nightStrength)) /// 夜帧外发光峰值 alpha:提亮烟柱周边形成外发光。 let nightSmokeGlowPeakAt (nightStrength: float32) : int = int (90.0f + 110.0f * max 0.0f (min 1.0f nightStrength)) /// 夜帧外发光半径(像素)。 let nightSmokeGlowRadius = 30 // ---------------- P79 夜景光晕去叠 ---------------- /// 收紧后的单个光源光晕半径(tile):4 → 3。 let nightHaloRadiusTiles = 3 /// 每个光源最多可见层数(内芯 + 外环),层间留暗隙。 let nightHaloVisualLayers = 2 /// 收紧半径后的峰值 alpha(略降,避免叠加成一整团)。 let nightHaloPeakAlphaAt (nightStrength: float32) : int = int (118.0f * max 0.0f (min 1.0f nightStrength)) /// ---------------- 近景楼层层错落(屏幕空间前景层) ---------------- /// 前景层屋顶分段:等宽 tile 分段、高度按确定性函数错落(0/8/16/24 px 四层), /// 并随 tick 极慢移动(视差)。返回 (x, y, width)。 let foregroundRooftops (viewportWidth: int) (viewportHeight: int) (tick: int64) : (int * int * int) list = let tile = Sim.tilePixels let count = max 1 (viewportWidth / tile + 2) let baseY = viewportHeight - 64 [ for i in 0 .. count - 1 do let x = (i - 1) * tile let stagger = ((i * 7 + int (tick / 120L)) % 4) * 8 yield (x, baseY - stagger, tile) ]