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;
+ }
+}