From 5ac34f4dc767c471f6d92edaa73092fb1339fa64 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sun, 14 Jun 2026 16:58:28 +0300 Subject: [PATCH] Seed dispersal by herbivores (endozoochory) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eating a mature plant has a chance to lodge a seed in the herbivore's gut (GutSeed component, cloned plant genome). SeedDispersalSystem ticks the timer and plants a seedling of that species wherever the animal has wandered to (if the cell is land), then drops the component. Herbivores become agents of plant spread — flora follows grazing routes — closing a plant<->animal loop on top of the existing Fruiting/PlantFactory machinery. GutSeed is transient (not serialized), like AI targets. Build clean. Co-Authored-By: Claude Opus 4.8 --- src/LittleSim/Scenes/WorldScene.cs | 13 +++ src/LittleSim/Sim/AnimalOrganism.cs | 17 ++++ src/LittleSim/Sim/AnimalSystems.cs | 151 ++++++++++++++++++++++++++++ 3 files changed, 181 insertions(+) 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(); + } + } + } +}