diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs
index 5f5c7bd..92954ad 100644
--- a/src/LittleSim/Scenes/WorldScene.cs
+++ b/src/LittleSim/Scenes/WorldScene.cs
@@ -309,6 +309,19 @@ public sealed class WorldScene : Scene
_config.Seed + 0x6E66
)
);
+ UpdateSystems.Add(
+ new SeedDispersalSystem(
+ Store,
+ _plants,
+ Context.Clock,
+ SecondsPerDay,
+ _config.Width,
+ _config.Height,
+ CellSize,
+ _cellFertility,
+ _cellLand
+ )
+ );
UpdateSystems.Add(
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs
index 73acdcc..8f93ae5 100644
--- a/src/LittleSim/Sim/AnimalOrganism.cs
+++ b/src/LittleSim/Sim/AnimalOrganism.cs
@@ -484,6 +484,23 @@ public struct Egg : IComponent
public float IncubateDays;
}
+///
+/// Проглоченное семя (эндозоохория): травоядное, съевшее плодоносящее растение, какое-то время несёт
+/// его семя в кишечнике, затем «высаживает» его в другом месте () —
+/// зверь становится разносчиком флоры. Геном — managed-ссылка (клон родительского растения).
+///
+public struct GutSeed : IComponent
+{
+ /// Индекс вида растения в .
+ public int PlantSpecies;
+
+ /// Геном будущего ростка (клон съеденного растения).
+ public Genome Genome;
+
+ /// Остаток времени до высадки в игровых днях; ≤0 — росток падает на землю.
+ public float DropInDays;
+}
+
/// Инстанс мысли настроения на особи (фаза A8): деф события + остаток времени до угасания.
public struct ThoughtInstance
{
diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs
index 2289e98..c16c642 100644
--- a/src/LittleSim/Sim/AnimalSystems.cs
+++ b/src/LittleSim/Sim/AnimalSystems.cs
@@ -791,6 +791,8 @@ public sealed class AnimalActionSystem : BaseSystem
private const float ThirstReliefBelow = 0.15f; // ниже этого жажда — питьё даёт мысль «напилась»
private const float FleeSpeedMult = 1.2f; // страх подгоняет жертву чуть быстрее обычного шага
private const float PostReproductiveFrac = 0.9f; // доля жизни, после которой самка уже не зачинает
+ private const float SeedIngestChance = 0.15f; // шанс проглотить семя при выедании зрелого растения
+ private const float GutSeedDropDays = 2f; // через сколько дней проглоченное семя высаживается
private readonly EntityStore _store;
private readonly PlantSet _plants;
@@ -815,6 +817,7 @@ public sealed class AnimalActionSystem : BaseSystem
private readonly List _consumed = []; // трупы, доеденные до нуля
private readonly List _kills = []; // добыча, забитая в этом проходе (смерть после итерации)
private readonly List<(Entity Self, Entity Partner)> _matings = [];
+ private readonly List<(Entity Eater, GutSeed Seed)> _ingested = []; // проглоченные семена (эндозоохория)
public AnimalActionSystem(
EntityStore store,
@@ -859,6 +862,7 @@ public sealed class AnimalActionSystem : BaseSystem
_consumed.Clear();
_kills.Clear();
_matings.Clear();
+ _ingested.Clear();
foreach (
var (brains, needsChunk, organisms, transforms, healths, entities) in _query.Chunks
@@ -905,6 +909,8 @@ public sealed class AnimalActionSystem : BaseSystem
o[i].Traits.ToxinTolerance,
days
);
+ // Эндозоохория: шанс проглотить семя зрелого растения (высадит позже).
+ TryIngestSeed(brain.TargetPlant, entities.EntityAt(i));
}
break;
@@ -984,9 +990,55 @@ public sealed class AnimalActionSystem : BaseSystem
AnimalFactory.Die(_store, _animalSet, prey, _cellSize); // забитая добыча → труп
}
+ foreach (var (eater, seed) in _ingested)
+ {
+ if (!eater.IsNull && !eater.HasComponent())
+ {
+ eater.AddComponent(seed); // структурное добавление — после прохода
+ }
+ }
+
ApplyMatings();
}
+ // Эндозоохория: при выедании ЗРЕЛОГО растения шанс проглотить его семя — зверь понесёт его и
+ // высадит позже в другом месте (SeedDispersalSystem). Геном клонируется (отдельная особь-росток).
+ private void TryIngestSeed(int plantId, Entity eater)
+ {
+ if (
+ eater.IsNull
+ || eater.HasComponent()
+ || plantId < 0
+ || _rng.NextSingle() >= SeedIngestChance
+ || !_store.TryGetEntityById(plantId, out var plant)
+ || plant.IsNull
+ || !plant.HasComponent()
+ || !plant.HasComponent()
+ )
+ {
+ return;
+ }
+
+ ref readonly var grow = ref plant.GetComponent();
+ if (grow.Stage < _plants[grow.Species].Stages.Length - 1)
+ {
+ return; // только зрелое растение даёт семя
+ }
+
+ var genome = new Genome(plant.GetComponent().Genome.ToDictionary());
+ _ingested.Add(
+ (
+ eater,
+ new GutSeed
+ {
+ PlantSpecies = grow.Species,
+ Genome = genome,
+ DropInDays = GutSeedDropDays,
+ }
+ )
+ );
+ }
+
// Охота/падальщество: ведёт к цели и при контакте либо ест труп (утоляет голод, расходует мясо),
// либо кусает живую добычу — урон части тела, острая кровопотеря, рана-кровотечение (первый
// травматический урон). Добитая добыча помечается на смерть (труп оставит общий путь Die).
@@ -2021,3 +2073,102 @@ public sealed class AnimalHealthSystem : BaseSystem
}
}
}
+
+///
+/// Разнос семян (эндозоохория): тикает таймер проглоченного семени (); по
+/// истечении высаживает росток того вида растения там, где сейчас зверь (если клетка — суша),
+/// геномом-клоном съеденного растения, и снимает компонент. Делает травоядных разносчиками флоры —
+/// растения расселяются вдоль кормовых маршрутов.
+///
+public sealed class SeedDispersalSystem : BaseSystem
+{
+ private readonly EntityStore _store;
+ private readonly PlantSet _plants;
+ private readonly GameClock _clock;
+ private readonly float _secondsPerDay;
+ private readonly int _width;
+ private readonly int _height;
+ private readonly int _cellSize;
+ private readonly float[] _cellFertility;
+ private readonly bool[] _cellLand;
+ private readonly ArchetypeQuery _query;
+ private readonly List _dropped = [];
+
+ public SeedDispersalSystem(
+ EntityStore store,
+ PlantSet plants,
+ GameClock clock,
+ float secondsPerDay,
+ int width,
+ int height,
+ int cellSize,
+ float[] cellFertility,
+ bool[] cellLand
+ )
+ {
+ _store = store;
+ _plants = plants;
+ _clock = clock;
+ _secondsPerDay = secondsPerDay;
+ _width = width;
+ _height = height;
+ _cellSize = cellSize;
+ _cellFertility = cellFertility;
+ _cellLand = cellLand;
+ _query = store.Query();
+ }
+
+ protected override void OnUpdateGroup()
+ {
+ var days = _clock.DeltaTime / _secondsPerDay;
+ if (days <= 0f)
+ {
+ return;
+ }
+
+ _dropped.Clear();
+ foreach (var (seeds, transforms, entities) in _query.Chunks)
+ {
+ var s = seeds.Span;
+ var t = transforms.Span;
+ for (var i = 0; i < s.Length; i++)
+ {
+ ref var seed = ref s[i];
+ seed.DropInDays -= days;
+ if (seed.DropInDays > 0f)
+ {
+ continue;
+ }
+
+ _dropped.Add(entities.EntityAt(i));
+ 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;
+ if (!_cellLand[cell])
+ {
+ continue; // упало в воду — семя пропадает
+ }
+
+ PlantFactory.Create(
+ _store,
+ _plants,
+ seed.PlantSpecies,
+ pos,
+ ageDays: 0f,
+ seed.Genome,
+ _cellFertility[cell],
+ _cellSize
+ );
+ }
+ }
+
+ foreach (var eater in _dropped)
+ {
+ if (!eater.IsNull && eater.HasComponent())
+ {
+ eater.RemoveComponent();
+ }
+ }
+ }
+}