diff --git a/Mods/Core/Defs/terrain.json b/Mods/Core/Defs/terrain.json
index f2caabf..a53f681 100644
--- a/Mods/Core/Defs/terrain.json
+++ b/Mods/Core/Defs/terrain.json
@@ -17,6 +17,6 @@
{ "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA"] }
] },
{ "defName": "Mountain", "label": "terrain.mountain", "maxHeight": 1.01, "color": [136, 132, 128],
- "surface": "terrain/surfaces/roughhewnrock", "fertility": 0.2 }
+ "surface": "terrain/surfaces/roughhewnrock", "fertility": 0.2, "blocksLight": true }
]
}
diff --git a/engine b/engine
index d0104df..79d406a 160000
--- a/engine
+++ b/engine
@@ -1 +1 @@
-Subproject commit d0104df3044961f74bda68d3e6cc97dd0d34b326
+Subproject commit 79d406a9f4687fdb47a059a6b1e0593eb33e3061
diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs
index 41c8de0..443f94f 100644
--- a/src/LittleSim/Content/GameDefs.cs
+++ b/src/LittleSim/Content/GameDefs.cs
@@ -27,6 +27,9 @@ public sealed class TerrainDef : Def
/// Суша: здесь появляются жители, животные и растения.
public bool IsLand { get; init; }
+ /// Загораживает свет (горы) — базовый окклюдер для лайтмапа/теней.
+ public bool BlocksLight { get; init; }
+
/// Ключ текстуры поверхности для тайловой сцены; null — тонированный тайл.
public string? Surface { get; init; }
diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs
index 9727d26..31dcfaf 100644
--- a/src/LittleSim/Scenes/WorldScene.cs
+++ b/src/LittleSim/Scenes/WorldScene.cs
@@ -53,6 +53,8 @@ public sealed class WorldScene : Scene
private PlantSet _plants = null!;
private float[] _cellFertility = [];
private bool[] _cellLand = [];
+ private bool[] _cellOccluderBase = []; // горы (статично из террейна)
+ private bool[] _cellOccluder = []; // горы + зрелые деревья (пересобирается лайтмапом)
/// Новый мир из конфига.
public WorldScene(WorldConfig config)
@@ -84,7 +86,7 @@ public sealed class WorldScene : Scene
var calendar = Context.UseCalendar(SecondsPerDay);
var climate = Context.UseClimate(ClimateSettings.Default);
- var dayNight = this.UseDayNight(renderer); // мир темнеет ночью — амбиент идёт в рендер
+ var dayNight = new DayNight(calendar, DayNightSettings.Default);
// Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва.
_plants = new PlantSet(content, atlases, device);
@@ -101,6 +103,18 @@ public sealed class WorldScene : Scene
RestorePlants(content);
}
+ // Освещение: лайтмап (день/ночь × окклюзия от гор/крон + точечные) множится поверх мира,
+ // а Lighting.SampleAt даёт локальный свет системе роста (подлесок под кронами растёт хуже).
+ var lighting = this.UseLighting(
+ renderer,
+ dayNight,
+ _config.Width,
+ _config.Height,
+ CellSize,
+ Vector2.Zero,
+ BuildOccluders
+ );
+
var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds));
// HUD, полоса скорости и меню-пауза в одной корневой панели.
@@ -120,8 +134,31 @@ public sealed class WorldScene : Scene
this.UseInspector(renderer);
var console = this.UseDevConsole();
RegisterCommands(console, content, atlases);
+ console.Register(
+ "light",
+ "light [radius] — place a point light at the cursor (night shadows demo)",
+ (c, args) =>
+ {
+ var radius =
+ args.Length > 0
+ ? float.Parse(args[0], System.Globalization.CultureInfo.InvariantCulture)
+ : 120f;
+ var mouse = Mouse.GetState();
+ var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y));
+ Store.CreateEntity(
+ Transform2D.At(world),
+ new PointLight
+ {
+ Radius = radius,
+ Color = Color.White,
+ Intensity = 0.9f,
+ }
+ );
+ c.WriteLine($"light at {world.X:0},{world.Y:0} r{radius:0}");
+ }
+ );
- UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, dayNight, CellSize));
+ UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, lighting, CellSize));
UpdateSystems.Add(
new PlantLifecycleSystem(
Store,
@@ -193,8 +230,11 @@ public sealed class WorldScene : Scene
_config.SmoothPasses
);
var grid = new TileGrid(_config.Width, _config.Height);
- _cellFertility = new float[_config.Width * _config.Height];
- _cellLand = new bool[_config.Width * _config.Height];
+ var cells = _config.Width * _config.Height;
+ _cellFertility = new float[cells];
+ _cellLand = new bool[cells];
+ _cellOccluderBase = new bool[cells];
+ _cellOccluder = new bool[cells];
for (var x = 0; x < _config.Width; x++)
{
for (var y = 0; y < _config.Height; y++)
@@ -204,6 +244,7 @@ public sealed class WorldScene : Scene
var cell = y * _config.Width + x;
_cellFertility[cell] = terrain.Fertility;
_cellLand[cell] = terrain.IsLand;
+ _cellOccluderBase[cell] = terrain.BlocksLight;
if (scatterRandom is not null)
{
ScatterSpawner.Spawn(
@@ -355,6 +396,40 @@ public sealed class WorldScene : Scene
}
}
+ // Текущая сетка окклюдеров для лайтмапа: статичные горы + клетки со зрелыми деревьями.
+ private bool[] BuildOccluders()
+ {
+ Array.Copy(_cellOccluderBase, _cellOccluder, _cellOccluder.Length);
+ Store
+ .Query()
+ .ForEachEntity(
+ (ref PlantGrowth grow, ref Transform2D transform, Entity _) =>
+ {
+ var species = _plants[grow.Species];
+ if (
+ species.Def.TrunkRadiusCells <= 0f
+ || grow.Stage < species.Stages.Length - 1
+ )
+ {
+ return; // затеняют только зрелые деревья (со стволом)
+ }
+
+ var cx = Math.Clamp(
+ (int)(transform.Position.X / CellSize),
+ 0,
+ _config.Width - 1
+ );
+ var cy = Math.Clamp(
+ (int)(transform.Position.Y / CellSize),
+ 0,
+ _config.Height - 1
+ );
+ _cellOccluder[cy * _config.Width + cx] = true;
+ }
+ );
+ return _cellOccluder;
+ }
+
private void Switch(Scene scene)
{
if (!Context.Scenes.IsTransitioning)
diff --git a/src/LittleSim/Sim/PlantGrowthSystem.cs b/src/LittleSim/Sim/PlantGrowthSystem.cs
index 54e8fb9..c2f4260 100644
--- a/src/LittleSim/Sim/PlantGrowthSystem.cs
+++ b/src/LittleSim/Sim/PlantGrowthSystem.cs
@@ -19,7 +19,7 @@ public sealed class PlantGrowthSystem(
PlantSet plants,
Calendar calendar,
Climate climate,
- DayNight dayNight,
+ Lighting lighting,
int cellSize
) : QuerySystem
{
@@ -31,8 +31,7 @@ public sealed class PlantGrowthSystem(
return; // пауза — рост стоит
}
- // В фазе B свет и температура глобальные — считаем раз за кадр (фаза D даст локальный свет).
- var light = dayNight.Intensity;
+ // Температура глобальная; свет — локальный (лайтмап: подлесок под кронами темнее).
var temperature = climate.Temperature;
foreach (var (growths, genomes, sprites, transforms, _) in Query.Chunks)
@@ -52,6 +51,7 @@ public sealed class PlantGrowthSystem(
}
ref var gene = ref dna[i];
+ var light = lighting.SampleAt(t[i].Position);
var rate =
gene.Vigor
* Suitability.Gaussian(light, gene.OptimalLight, gene.LightTolerance)