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)