diff --git a/Mods/Core/Defs/plants.json b/Mods/Core/Defs/plants.json index 1a83da0..7014534 100644 --- a/Mods/Core/Defs/plants.json +++ b/Mods/Core/Defs/plants.json @@ -3,7 +3,9 @@ "defs": [ { "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28, "genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 16, - "optimalFertility": 1.4, "fertilityTolerance": 0.9, "vigor": 1.0, "spread": 0.08 }, + "optimalFertility": 1.4, "fertilityTolerance": 0.9, "vigor": 1.0, + "lifespan": 160, "dispersalRange": 2, "reproduceInterval": 14, "selfPollination": 0.2, + "mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08 }, "stages": [ { "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" }, { "sizeCells": 1.2, "growDays": 9, "label": "plant.stage.sapling" }, @@ -16,7 +18,9 @@ { "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.2, "genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 20, "temperatureTolerance": 12, - "optimalFertility": 1.0, "fertilityTolerance": 1.0, "vigor": 1.3, "spread": 0.1 }, + "optimalFertility": 1.0, "fertilityTolerance": 1.0, "vigor": 1.3, + "lifespan": 22, "dispersalRange": 3, "reproduceInterval": 3, "selfPollination": 0.8, + "mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1 }, "stages": [ { "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 2, "label": "plant.stage.sprout" }, { "sizeCells": 1.2, "label": "plant.stage.mature" } @@ -24,7 +28,9 @@ { "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4, "genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 13, - "optimalFertility": 1.1, "fertilityTolerance": 1.0, "vigor": 1.0, "spread": 0.1 }, + "optimalFertility": 1.1, "fertilityTolerance": 1.0, "vigor": 1.0, + "lifespan": 60, "dispersalRange": 2, "reproduceInterval": 7, "selfPollination": 0.5, + "mutationRate": 0.05, "variantChance": 0.18, "spread": 0.1 }, "stages": [ { "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 3, "label": "plant.stage.sprout" }, { "sizeCells": 1.4, "label": "plant.stage.mature" } diff --git a/src/LittleSim/App/WorldSave.cs b/src/LittleSim/App/WorldSave.cs index 2b4fac9..6fe6158 100644 --- a/src/LittleSim/App/WorldSave.cs +++ b/src/LittleSim/App/WorldSave.cs @@ -1,7 +1,33 @@ using System.Text.Json; +using LittleSim.Sim; namespace LittleSim.App; +/// Сериализуемое состояние одного растения: вид (деф), позиция, возраст, стадия, почва и геном. +public sealed class PlantSave +{ + /// Имя дефа вида — по нему при загрузке берётся индекс/стадии/текстуры. + public string Species { get; set; } = ""; + + /// Позиция X в мировых координатах. + public float X { get; set; } + + /// Позиция Y в мировых координатах. + public float Y { get; set; } + + /// Накопленный возраст в игровых днях. + public float AgeDays { get; set; } + + /// Текущая стадия роста. + public int Stage { get; set; } + + /// Плодородность клетки. + public float CellFertility { get; set; } + + /// Геном особи (аллели сериализуются как поля структуры). + public PlantGenome Genome { get; set; } +} + /// Сериализуемое состояние одного жителя (sim-компоненты; спрайт пересобирается из дефа по ). public sealed class PawnSave { @@ -70,6 +96,9 @@ public sealed class WorldSave /// Снимок жителей. public List Pawns { get; set; } = []; + /// Снимок всех растений (геномы, позиции, возраст, стадия). + public List Plants { get; set; } = []; + /// Конфиг мира для пересоздания сцены. public WorldConfig ToConfig() => new() @@ -90,6 +119,7 @@ public sealed class SaveStore { WriteIndented = true, PropertyNameCaseInsensitive = true, + IncludeFields = true, // аллели генома (PlantGenome) — публичные поля структуры }; private readonly string _directory; diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs index a33e1d7..41c8de0 100644 --- a/src/LittleSim/Content/GameDefs.cs +++ b/src/LittleSim/Content/GameDefs.cs @@ -91,6 +91,24 @@ public sealed class GenomeDef /// Базовая бодрость роста (множитель скорости в идеальных условиях). public float Vigor { get; init; } = 1f; + /// Продолжительность жизни (игровых дней) до смерти от старости. + public float Lifespan { get; init; } = 80f; + + /// Дальность расселения семян (в клетках). + public float DispersalRange { get; init; } = 2f; + + /// Средний интервал между попытками дать семя (игровых дней). + public float ReproduceInterval { get; init; } = 8f; + + /// Способность к самоопылению (0..1): шанс дать семя без партнёра. + public float SelfPollination { get; init; } = 0.4f; + + /// Темп мутаций (0..1): вероятность сдвига аллеля у потомка. + public float MutationRate { get; init; } = 0.05f; + + /// Доля рецессивного аллеля морфы в стартовой популяции (0..1). + public float VariantChance { get; init; } = 0.15f; + /// Доля разброса аллелей вокруг базы при генерации особи. public float Spread { get; init; } = 0.08f; } diff --git a/src/LittleSim/Scenes/ContentHelpers.cs b/src/LittleSim/Scenes/ContentHelpers.cs index 3f43486..1003ffa 100644 --- a/src/LittleSim/Scenes/ContentHelpers.cs +++ b/src/LittleSim/Scenes/ContentHelpers.cs @@ -36,29 +36,19 @@ internal static class ScatterSpawner var def = content.Defs.Get(entry.Options[random.Next(entry.Options.Count)]); var index = plants.IndexOf(def); - var species = plants[index]; var px = (cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize; var py = (cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize; - var age = random.NextSingle() * species.MaturityDays; - var stage = species.StageAt(age); - var resolved = species.Stages[stage]; + var age = random.NextSingle() * plants[index].MaturityDays; - var sprite = new Sprite(resolved.Region, GameLayers.Beings); - sprite.CenterOrigin(); - scene.Store.CreateEntity( - new Transform2D( - new Vector2(px, py), - scale: new Vector2(cellSize * resolved.SizeCells / resolved.Region.Width) - ), - sprite, - new PlantGrowth - { - Species = index, - Stage = stage, - AgeDays = age, - CellFertility = terrain.Fertility, - }, - PlantGenome.FromDef(def.Genome, random) + PlantFactory.Create( + scene.Store, + plants, + index, + new Vector2(px, py), + age, + PlantGenome.FromDef(def.Genome, random), + terrain.Fertility, + cellSize ); } } diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 1afd90f..9727d26 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -25,8 +25,9 @@ namespace LittleSim.Scenes; /// Мир LittleSim: тайловый рельеф целиком описан дефами Core-мода и строится из /// (размер/сид/масштаб деталей) процедурной генерацией движка /// (). Каждый тип клетки рисуется текстурой-поверхностью из атласа -/// (вода — тонированным тайлом). Поверх мира — HUD, полоса скорости (пауза/x1/x3/x6 + горячие -/// клавиши), меню-пауза (Esc) и дев-консоль. Жителей/растений пока нет — только террейн. +/// (вода — тонированным тайлом). Поверх растёт растительность с геномом (рост по свету/температуре/ +/// почве, размножение по Менделю и смерть), климат и день/ночь. UI: HUD, полоса скорости +/// (пауза/x1/x3/x6 + горячие клавиши), меню-пауза (Esc), дев-консоль и ECS-инспектор (F1). /// public sealed class WorldScene : Scene { @@ -38,6 +39,9 @@ public sealed class WorldScene : Scene /// public const float SecondsPerDay = 480f; + /// Максимум растений на клетку — потолок плотности для размножения. + public const int DensityCap = 4; + private readonly WorldConfig _config; private readonly WorldSave? _save; private readonly RectF _bounds; @@ -46,6 +50,10 @@ public sealed class WorldScene : Scene private PauseMenu _pause = null!; private readonly List _speedRefreshers = []; + private PlantSet _plants = null!; + private float[] _cellFertility = []; + private bool[] _cellLand = []; + /// Новый мир из конфига. public WorldScene(WorldConfig config) : this(config, null) { } @@ -78,10 +86,20 @@ public sealed class WorldScene : Scene var climate = Context.UseClimate(ClimateSettings.Default); var dayNight = this.UseDayNight(renderer); // мир темнеет ночью — амбиент идёт в рендер - // Рельеф и расстановка растений детерминированы сидом мира (независимые потоки seed). - var plants = new PlantSet(content, atlases, device); - var random = new Random(_config.Seed); - BuildTerrain(content, atlases, device, assets, plants, random); + // Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва. + _plants = new PlantSet(content, atlases, device); + var loadingPlants = _save?.Plants is { Count: > 0 }; + BuildTerrain( + content, + atlases, + device, + assets, + loadingPlants ? null : new Random(_config.Seed) + ); + if (loadingPlants) + { + RestorePlants(content); + } var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds)); @@ -103,7 +121,22 @@ public sealed class WorldScene : Scene var console = this.UseDevConsole(); RegisterCommands(console, content, atlases); - UpdateSystems.Add(new PlantGrowthSystem(plants, calendar, climate, dayNight, CellSize)); + UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, dayNight, CellSize)); + UpdateSystems.Add( + new PlantLifecycleSystem( + Store, + _plants, + calendar, + Context.Clock, + _config.Width, + _config.Height, + CellSize, + _cellFertility, + _cellLand, + DensityCap, + _config.Seed + ) + ); UpdateSystems.Add( new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen) ); @@ -127,17 +160,17 @@ public sealed class WorldScene : Scene } /// - /// Строит одну сущность-: тайлсет из дефов рельефа (поверхность из - /// атласа либо тонированный тайл) и сетка тайлов по карте высот процедурной генерации, а - /// поверх — растительность по скаттеру дефов (с компонентом роста, разной зрелости). + /// Строит сущность- (тайлсет из дефов рельефа + сетка по карте высот) и + /// заполняет по-клеточные массивы плодородности/суши для жизненного цикла. Если + /// задан (новый мир) — рассыпает растительность по дефам; + /// при загрузке передаётся null (растения восстанавливаются из сейва отдельно). /// private void BuildTerrain( GameContent content, ModAtlases atlases, Microsoft.Xna.Framework.Graphics.GraphicsDevice device, AssetManager assets, - PlantSet plants, - Random random + Random? scatterRandom ) { var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White)); @@ -160,21 +193,29 @@ 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]; for (var x = 0; x < _config.Width; x++) { for (var y = 0; y < _config.Height; y++) { var terrain = content.Terrains.Classify(heights[x, y]); grid[x, y] = tileByDef[terrain]; - ScatterSpawner.Spawn( - this, - content, - plants, - terrain, - new Point(x, y), - CellSize, - random - ); + var cell = y * _config.Width + x; + _cellFertility[cell] = terrain.Fertility; + _cellLand[cell] = terrain.IsLand; + if (scatterRandom is not null) + { + ScatterSpawner.Spawn( + this, + content, + _plants, + terrain, + new Point(x, y), + CellSize, + scatterRandom + ); + } } } @@ -254,7 +295,6 @@ public sealed class WorldScene : Scene private string SaveWorld() { - // Жителей пока нет — сохраняем только конфиг мира; рельеф воспроизводится из сида. var save = new WorldSave { Name = _config.Name, @@ -267,11 +307,54 @@ public sealed class WorldScene : Scene ElapsedSeconds = Context.Clock.TotalTime, }; + // Снимок всей популяции растений: вид (деф), позиция, возраст, стадия, почва и геном. + Store + .Query() + .ForEachEntity( + (ref PlantGrowth grow, ref PlantGenome gene, ref Transform2D transform, Entity _) => + { + save.Plants.Add( + new PlantSave + { + Species = _plants[grow.Species].Def.DefName, + X = transform.Position.X, + Y = transform.Position.Y, + AgeDays = grow.AgeDays, + Stage = grow.Stage, + CellFertility = grow.CellFertility, + Genome = gene, + } + ); + } + ); + new SaveStore().Write(save); - Log.Info($"World '{_config.Name}' saved"); + Log.Info($"World '{_config.Name}' saved ({save.Plants.Count} plants)"); return _config.Name; } + private void RestorePlants(GameContent content) + { + foreach (var plant in _save!.Plants) + { + if (!content.Defs.TryGet(plant.Species, out var def)) + { + continue; // вид пропал (мод убрали) — пропускаем растение + } + + PlantFactory.Create( + Store, + _plants, + _plants.IndexOf(def), + new Vector2(plant.X, plant.Y), + plant.AgeDays, + plant.Genome, + plant.CellFertility, + CellSize + ); + } + } + private void Switch(Scene scene) { if (!Context.Scenes.IsTransitioning) diff --git a/src/LittleSim/Sim/PlantFactory.cs b/src/LittleSim/Sim/PlantFactory.cs new file mode 100644 index 0000000..ebeb22a --- /dev/null +++ b/src/LittleSim/Sim/PlantFactory.cs @@ -0,0 +1,55 @@ +using Friflo.Engine.ECS; +using LittleSim.Content; +using LittleSim.Scenes; +using Microsoft.Xna.Framework; +using MrGameEng.Graphics; + +namespace LittleSim.Sim; + +/// +/// Единая точка создания сущности-растения: спрайт стадии по возрасту, тинт по морфе генома и +/// компоненты роста/генома. Переиспользуется начальным скаттером, размножением и загрузкой сейва — +/// чтобы все растения собирались одинаково. +/// +public static class PlantFactory +{ + // Тинт рецессивной морфы (домножается на текстуру) — делает вариант визуально отличимым. + private static readonly Color VariantTint = new(214, 150, 176); + + /// Создаёт растение вида с заданным возрастом и геномом. + public static Entity Create( + EntityStore store, + PlantSet plants, + int species, + Vector2 position, + float ageDays, + in PlantGenome genome, + float cellFertility, + int cellSize + ) + { + var sp = plants[species]; + var stage = sp.StageAt(ageDays); + var resolved = sp.Stages[stage]; + + var sprite = new Sprite(resolved.Region, GameLayers.Beings); + sprite.CenterOrigin(); + sprite.Color = genome.IsVariant ? VariantTint : Color.White; + + return store.CreateEntity( + new Transform2D( + position, + scale: new Vector2(cellSize * resolved.SizeCells / resolved.Region.Width) + ), + sprite, + new PlantGrowth + { + Species = species, + Stage = stage, + AgeDays = ageDays, + CellFertility = cellFertility, + }, + genome + ); + } +} diff --git a/src/LittleSim/Sim/PlantGenome.cs b/src/LittleSim/Sim/PlantGenome.cs index a7971db..b697088 100644 --- a/src/LittleSim/Sim/PlantGenome.cs +++ b/src/LittleSim/Sim/PlantGenome.cs @@ -4,39 +4,42 @@ using LittleSim.Content; namespace LittleSim.Sim; /// -/// Диплоидный геном растения: по паре аллелей на ген. Для числовых генов экспрессия гибридная — -/// фенотип есть среднее двух аллелей (дискретные доминант/рецессивные гены добавятся с размножением). -/// Аллели — наследуемые данные (правятся в инспекторе); фенотип-свойства читает система роста. +/// Диплоидный геном растения: по паре аллелей на ген. Числовые гены экспрессируются гибридно — +/// фенотип есть среднее двух аллелей; дискретный ген морфы — по Менделю (рецессив виден только в +/// гомозиготе). Аллели наследуются (правятся в инспекторе); фенотип-свойства читают системы роста и +/// жизненного цикла. моделирует мейоз и мутацию. /// public struct PlantGenome : IComponent { - /// Аллели оптимума освещённости (0..1). + // --- Числовые гены (аллель A/B), фенотип = среднее ------------------------------------------ public float OptimalLightA; public float OptimalLightB; - - /// Аллели толерантности по свету. public float LightToleranceA; public float LightToleranceB; - - /// Аллели оптимума температуры (°C). public float OptimalTemperatureA; public float OptimalTemperatureB; - - /// Аллели толерантности по температуре (°C). public float TemperatureToleranceA; public float TemperatureToleranceB; - - /// Аллели оптимума плодородности. public float OptimalFertilityA; public float OptimalFertilityB; - - /// Аллели толерантности по плодородности. public float FertilityToleranceA; public float FertilityToleranceB; - - /// Аллели бодрости роста. public float VigorA; public float VigorB; + public float LifespanA; + public float LifespanB; + public float DispersalRangeA; + public float DispersalRangeB; + public float ReproduceIntervalA; + public float ReproduceIntervalB; + public float SelfPollinationA; + public float SelfPollinationB; + public float MutationRateA; + public float MutationRateB; + + // --- Дискретный ген морфы (0 — доминантный, 1 — рецессивный) -------------------------------- + public int MorphA; + public int MorphB; /// Фенотип: оптимальная освещённость. public readonly float OptimalLight => (OptimalLightA + OptimalLightB) * 0.5f; @@ -60,15 +63,36 @@ public struct PlantGenome : IComponent /// Фенотип: бодрость роста. public readonly float Vigor => (VigorA + VigorB) * 0.5f; + /// Фенотип: продолжительность жизни (дней). + public readonly float Lifespan => (LifespanA + LifespanB) * 0.5f; + + /// Фенотип: дальность расселения (клеток). + public readonly float DispersalRange => (DispersalRangeA + DispersalRangeB) * 0.5f; + + /// Фенотип: интервал размножения (дней). + public readonly float ReproduceInterval => (ReproduceIntervalA + ReproduceIntervalB) * 0.5f; + + /// Фенотип: способность к самоопылению (0..1). + public readonly float SelfPollination => (SelfPollinationA + SelfPollinationB) * 0.5f; + + /// Фенотип: темп мутаций (0..1). + public readonly float MutationRate => (MutationRateA + MutationRateB) * 0.5f; + + /// Фенотип морфы: рецессивный вариант проявляется только в гомозиготе (1,1). + public readonly bool IsVariant => MorphA == 1 && MorphB == 1; + /// - /// Генерирует особь из базового генома вида: каждый аллель — база ± относительный разброс - /// (). Сидируется переданным — детерминизм. + /// Генерирует особь из базового генома вида: числовые аллели — база ± относительный разброс + /// (); аллель морфы = рецессивный (1) с вероятностью + /// . Сидируется — детерминизм. /// public static PlantGenome FromDef(GenomeDef def, Random random) { float Allele(float baseValue) => baseValue + (random.NextSingle() * 2f - 1f) * def.Spread * baseValue; + int MorphAllele() => random.NextSingle() < def.VariantChance ? 1 : 0; + return new PlantGenome { OptimalLightA = Allele(def.OptimalLight), @@ -85,6 +109,63 @@ public struct PlantGenome : IComponent FertilityToleranceB = Allele(def.FertilityTolerance), VigorA = Allele(def.Vigor), VigorB = Allele(def.Vigor), + LifespanA = Allele(def.Lifespan), + LifespanB = Allele(def.Lifespan), + DispersalRangeA = Allele(def.DispersalRange), + DispersalRangeB = Allele(def.DispersalRange), + ReproduceIntervalA = Allele(def.ReproduceInterval), + ReproduceIntervalB = Allele(def.ReproduceInterval), + SelfPollinationA = Allele(def.SelfPollination), + SelfPollinationB = Allele(def.SelfPollination), + MutationRateA = Allele(def.MutationRate), + MutationRateB = Allele(def.MutationRate), + MorphA = MorphAllele(), + MorphB = MorphAllele(), + }; + } + + /// + /// Потомок двух родителей (мейоз): аллель A — случайный из родителя , аллель + /// B — из ; затем мутация (с вероятностью среднего темпа мутаций родителей — + /// сдвиг числового аллеля на малую долю / переключение морфы). Самоопыление = Breed(self, self). + /// + public static PlantGenome Breed(in PlantGenome a, in PlantGenome b, Random rng) + { + var rate = (a.MutationRate + b.MutationRate) * 0.5f; + + float Mut(float v) => + rng.NextSingle() < rate ? v + (rng.NextSingle() * 2f - 1f) * 0.12f * v : v; + float FromA(float x, float y) => Mut(rng.NextSingle() < 0.5f ? x : y); + int MutMorph(int m) => rng.NextSingle() < rate ? 1 - m : m; + + return new PlantGenome + { + OptimalLightA = FromA(a.OptimalLightA, a.OptimalLightB), + OptimalLightB = FromA(b.OptimalLightA, b.OptimalLightB), + LightToleranceA = FromA(a.LightToleranceA, a.LightToleranceB), + LightToleranceB = FromA(b.LightToleranceA, b.LightToleranceB), + OptimalTemperatureA = FromA(a.OptimalTemperatureA, a.OptimalTemperatureB), + OptimalTemperatureB = FromA(b.OptimalTemperatureA, b.OptimalTemperatureB), + TemperatureToleranceA = FromA(a.TemperatureToleranceA, a.TemperatureToleranceB), + TemperatureToleranceB = FromA(b.TemperatureToleranceA, b.TemperatureToleranceB), + OptimalFertilityA = FromA(a.OptimalFertilityA, a.OptimalFertilityB), + OptimalFertilityB = FromA(b.OptimalFertilityA, b.OptimalFertilityB), + FertilityToleranceA = FromA(a.FertilityToleranceA, a.FertilityToleranceB), + FertilityToleranceB = FromA(b.FertilityToleranceA, b.FertilityToleranceB), + VigorA = FromA(a.VigorA, a.VigorB), + VigorB = FromA(b.VigorA, b.VigorB), + LifespanA = FromA(a.LifespanA, a.LifespanB), + LifespanB = FromA(b.LifespanA, b.LifespanB), + DispersalRangeA = FromA(a.DispersalRangeA, a.DispersalRangeB), + DispersalRangeB = FromA(b.DispersalRangeA, b.DispersalRangeB), + ReproduceIntervalA = FromA(a.ReproduceIntervalA, a.ReproduceIntervalB), + ReproduceIntervalB = FromA(b.ReproduceIntervalA, b.ReproduceIntervalB), + SelfPollinationA = FromA(a.SelfPollinationA, a.SelfPollinationB), + SelfPollinationB = FromA(b.SelfPollinationA, b.SelfPollinationB), + MutationRateA = FromA(a.MutationRateA, a.MutationRateB), + MutationRateB = FromA(b.MutationRateA, b.MutationRateB), + MorphA = MutMorph(rng.NextSingle() < 0.5f ? a.MorphA : a.MorphB), + MorphB = MutMorph(rng.NextSingle() < 0.5f ? b.MorphA : b.MorphB), }; } } diff --git a/src/LittleSim/Sim/PlantGrowthSystem.cs b/src/LittleSim/Sim/PlantGrowthSystem.cs index aafdc72..54e8fb9 100644 --- a/src/LittleSim/Sim/PlantGrowthSystem.cs +++ b/src/LittleSim/Sim/PlantGrowthSystem.cs @@ -47,7 +47,8 @@ public sealed class PlantGrowthSystem( var stages = plants[grow.Species].Stages; if (grow.Stage >= stages.Length - 1) { - continue; // уже зрелое — рост окончен + grow.AgeDays += deltaDays; // зрелое: старение в реальном времени → смерть по старости + continue; } ref var gene = ref dna[i]; diff --git a/src/LittleSim/Sim/PlantLifecycleSystem.cs b/src/LittleSim/Sim/PlantLifecycleSystem.cs new file mode 100644 index 0000000..7d49d00 --- /dev/null +++ b/src/LittleSim/Sim/PlantLifecycleSystem.cs @@ -0,0 +1,292 @@ +using Friflo.Engine.ECS; +using Friflo.Engine.ECS.Systems; +using LittleSim.Content; +using Microsoft.Xna.Framework; +using MrGameEng.Core; +using MrGameEng.Graphics; + +namespace LittleSim.Sim; + +/// +/// Жизненный цикл растений: размножение (наследование по Менделю) и смерть от старости. Запускается +/// не каждый кадр, а раз в игрового дня. За тик: строит по-клеточную сетку +/// (счётчик плотности + «представитель» вида/генома), собирает умерших по возрасту, затем зрелые с +/// вероятностью elapsed/ReproduceInterval сеют потомка — приоритет перекрёстного опыления +/// (зрелый сосед того же вида в радиусе расселения), иначе самоопыление по гену. Семя падает в +/// случайную клетку радиуса, если это суша и не превышен лимит плотности. Структурные изменения +/// (создание/удаление сущностей) применяются после проходов по запросу — это безопасно. +/// +public sealed class PlantLifecycleSystem : BaseSystem +{ + private const float IntervalDays = 0.25f; + + private readonly EntityStore _store; + private readonly PlantSet _plants; + private readonly Calendar _calendar; + private readonly GameClock _clock; + private readonly int _width; + private readonly int _height; + private readonly int _cellSize; + private readonly float[] _cellFertility; + private readonly bool[] _cellLand; + private readonly int _densityCap; + private readonly Random _rng; + private readonly ArchetypeQuery _query; + + private readonly int[] _count; + private readonly CellRep[] _rep; + private readonly List _deaths = []; + private readonly List _candidates = []; + private readonly List _births = []; + private float _accumulator; + + public PlantLifecycleSystem( + EntityStore store, + PlantSet plants, + Calendar calendar, + GameClock clock, + int width, + int height, + int cellSize, + float[] cellFertility, + bool[] cellLand, + int densityCap, + int seed + ) + { + _store = store; + _plants = plants; + _calendar = calendar; + _clock = clock; + _width = width; + _height = height; + _cellSize = cellSize; + _cellFertility = cellFertility; + _cellLand = cellLand; + _densityCap = densityCap; + _rng = new Random(seed); + _query = store.Query(); + _count = new int[width * height]; + _rep = new CellRep[width * height]; + } + + protected override void OnUpdateGroup() + { + _accumulator += _clock.DeltaTime / _calendar.SecondsPerDay; // прошло игровых дней (с учётом паузы/скорости) + if (_accumulator < IntervalDays) + { + return; + } + + var elapsed = _accumulator; + _accumulator = 0f; + + BuildGridAndCollect(); + Reproduce(elapsed); + Apply(); + } + + // Проход 1: сетка плотности + представители, сбор умерших и зрелых-кандидатов. + private void BuildGridAndCollect() + { + Array.Clear(_count); + for (var c = 0; c < _rep.Length; c++) + { + _rep[c].Species = -1; + } + + _deaths.Clear(); + _candidates.Clear(); + _births.Clear(); + + foreach (var (growths, genomes, transforms, entities) in _query.Chunks) + { + var g = growths.Span; + var dna = genomes.Span; + var t = transforms.Span; + for (var i = 0; i < g.Length; i++) + { + ref var grow = ref g[i]; + ref var gene = ref dna[i]; + if (grow.AgeDays > gene.Lifespan) + { + _deaths.Add(entities.EntityAt(i)); // умер от старости + continue; + } + + var pos = t[i].Position; + var cx = Math.Clamp((int)(pos.X / _cellSize), 0, _width - 1); + var cy = Math.Clamp((int)(pos.Y / _cellSize), 0, _height - 1); + var cell = cy * _width + cx; + _count[cell]++; + + var mature = grow.Stage >= _plants[grow.Species].Stages.Length - 1; + _rep[cell] = new CellRep + { + Species = grow.Species, + Mature = mature, + Genome = gene, + }; + if (mature) + { + _candidates.Add( + new Candidate + { + Cell = cell, + Species = grow.Species, + Genome = gene, + } + ); + } + } + } + } + + // Проход 2: зрелые сеют потомка (перекрёстно или само-) в подходящую клетку радиуса. + private void Reproduce(float elapsed) + { + foreach (var cand in _candidates) + { + var interval = MathF.Max(0.5f, cand.Genome.ReproduceInterval); + if (_rng.NextSingle() >= elapsed / interval) + { + continue; // в этот тик не сеет + } + + var range = Math.Clamp((int)MathF.Round(cand.Genome.DispersalRange), 1, 6); + var cx = cand.Cell % _width; + var cy = cand.Cell / _width; + + if (!TryFindPartner(cx, cy, range, cand.Species, out var partner)) + { + if (_rng.NextSingle() >= cand.Genome.SelfPollination) + { + continue; // нет партнёра и самоопыление не удалось + } + + partner = cand.Genome; // самоопыление + } + + if (!TryPickTarget(cx, cy, range, out var tcell)) + { + continue; // некуда сеять (нет суши/свободного места рядом) + } + + var child = PlantGenome.Breed(cand.Genome, partner, _rng); + var tx = tcell % _width; + var ty = tcell / _width; + var position = new Vector2( + (tx + 0.3f + _rng.NextSingle() * 0.4f) * _cellSize, + (ty + 0.3f + _rng.NextSingle() * 0.4f) * _cellSize + ); + _births.Add( + new Birth + { + Species = cand.Species, + Position = position, + Genome = child, + Fertility = _cellFertility[tcell], + } + ); + _count[tcell]++; // резервируем место, чтобы не переполнить клетку за тик + } + } + + private bool TryFindPartner(int cx, int cy, int range, int species, out PlantGenome partner) + { + for (var dy = -range; dy <= range; dy++) + { + for (var dx = -range; dx <= range; dx++) + { + if (dx == 0 && dy == 0) + { + continue; // партнёр — другая клетка (приоритет перекрёстного) + } + + var nx = cx + dx; + var ny = cy + dy; + if (nx < 0 || nx >= _width || ny < 0 || ny >= _height) + { + continue; + } + + ref var rep = ref _rep[ny * _width + nx]; + if (rep.Species == species && rep.Mature) + { + partner = rep.Genome; + return true; + } + } + } + + partner = default; + return false; + } + + private bool TryPickTarget(int cx, int cy, int range, out int cell) + { + for (var attempt = 0; attempt < 6; attempt++) + { + var tx = cx + _rng.Next(-range, range + 1); + var ty = cy + _rng.Next(-range, range + 1); + if (tx < 0 || tx >= _width || ty < 0 || ty >= _height) + { + continue; + } + + var candidate = ty * _width + tx; + if (_cellLand[candidate] && _count[candidate] < _densityCap) + { + cell = candidate; + return true; + } + } + + cell = -1; + return false; + } + + private void Apply() + { + foreach (var entity in _deaths) + { + entity.DeleteEntity(); + } + + foreach (var birth in _births) + { + PlantFactory.Create( + _store, + _plants, + birth.Species, + birth.Position, + ageDays: 0f, + birth.Genome, + birth.Fertility, + _cellSize + ); + } + } + + private struct CellRep + { + public int Species; + public bool Mature; + public PlantGenome Genome; + } + + private struct Candidate + { + public int Cell; + public int Species; + public PlantGenome Genome; + } + + private struct Birth + { + public int Species; + public Vector2 Position; + public PlantGenome Genome; + public float Fertility; + } +}