1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
|
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 + 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)。
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) ]
|