Seed dispersal by herbivores (endozoochory)

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 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-14 16:58:28 +03:00
co-authored by Claude Opus 4.8
parent 562e365be1
commit 5ac34f4dc7
3 changed files with 181 additions and 0 deletions
+13
View File
@@ -309,6 +309,19 @@ public sealed class WorldScene : Scene
_config.Seed + 0x6E66 _config.Seed + 0x6E66
) )
); );
UpdateSystems.Add(
new SeedDispersalSystem(
Store,
_plants,
Context.Clock,
SecondsPerDay,
_config.Width,
_config.Height,
CellSize,
_cellFertility,
_cellLand
)
);
UpdateSystems.Add( UpdateSystems.Add(
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen) new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
+17
View File
@@ -484,6 +484,23 @@ public struct Egg : IComponent
public float IncubateDays; public float IncubateDays;
} }
/// <summary>
/// Проглоченное семя (эндозоохория): травоядное, съевшее плодоносящее растение, какое-то время несёт
/// его семя в кишечнике, затем «высаживает» его в другом месте (<see cref="Sim.SeedDispersalSystem"/>) —
/// зверь становится разносчиком флоры. Геном — managed-ссылка (клон родительского растения).
/// </summary>
public struct GutSeed : IComponent
{
/// <summary>Индекс вида растения в <see cref="Content.PlantSet"/>.</summary>
public int PlantSpecies;
/// <summary>Геном будущего ростка (клон съеденного растения).</summary>
public Genome Genome;
/// <summary>Остаток времени до высадки в игровых днях; ≤0 — росток падает на землю.</summary>
public float DropInDays;
}
/// <summary>Инстанс мысли настроения на особи (фаза A8): деф события + остаток времени до угасания.</summary> /// <summary>Инстанс мысли настроения на особи (фаза A8): деф события + остаток времени до угасания.</summary>
public struct ThoughtInstance public struct ThoughtInstance
{ {
+151
View File
@@ -791,6 +791,8 @@ public sealed class AnimalActionSystem : BaseSystem
private const float ThirstReliefBelow = 0.15f; // ниже этого жажда — питьё даёт мысль «напилась» private const float ThirstReliefBelow = 0.15f; // ниже этого жажда — питьё даёт мысль «напилась»
private const float FleeSpeedMult = 1.2f; // страх подгоняет жертву чуть быстрее обычного шага private const float FleeSpeedMult = 1.2f; // страх подгоняет жертву чуть быстрее обычного шага
private const float PostReproductiveFrac = 0.9f; // доля жизни, после которой самка уже не зачинает private const float PostReproductiveFrac = 0.9f; // доля жизни, после которой самка уже не зачинает
private const float SeedIngestChance = 0.15f; // шанс проглотить семя при выедании зрелого растения
private const float GutSeedDropDays = 2f; // через сколько дней проглоченное семя высаживается
private readonly EntityStore _store; private readonly EntityStore _store;
private readonly PlantSet _plants; private readonly PlantSet _plants;
@@ -815,6 +817,7 @@ public sealed class AnimalActionSystem : BaseSystem
private readonly List<Entity> _consumed = []; // трупы, доеденные до нуля private readonly List<Entity> _consumed = []; // трупы, доеденные до нуля
private readonly List<Entity> _kills = []; // добыча, забитая в этом проходе (смерть после итерации) private readonly List<Entity> _kills = []; // добыча, забитая в этом проходе (смерть после итерации)
private readonly List<(Entity Self, Entity Partner)> _matings = []; private readonly List<(Entity Self, Entity Partner)> _matings = [];
private readonly List<(Entity Eater, GutSeed Seed)> _ingested = []; // проглоченные семена (эндозоохория)
public AnimalActionSystem( public AnimalActionSystem(
EntityStore store, EntityStore store,
@@ -859,6 +862,7 @@ public sealed class AnimalActionSystem : BaseSystem
_consumed.Clear(); _consumed.Clear();
_kills.Clear(); _kills.Clear();
_matings.Clear(); _matings.Clear();
_ingested.Clear();
foreach ( foreach (
var (brains, needsChunk, organisms, transforms, healths, entities) in _query.Chunks var (brains, needsChunk, organisms, transforms, healths, entities) in _query.Chunks
@@ -905,6 +909,8 @@ public sealed class AnimalActionSystem : BaseSystem
o[i].Traits.ToxinTolerance, o[i].Traits.ToxinTolerance,
days days
); );
// Эндозоохория: шанс проглотить семя зрелого растения (высадит позже).
TryIngestSeed(brain.TargetPlant, entities.EntityAt(i));
} }
break; break;
@@ -984,9 +990,55 @@ public sealed class AnimalActionSystem : BaseSystem
AnimalFactory.Die(_store, _animalSet, prey, _cellSize); // забитая добыча → труп AnimalFactory.Die(_store, _animalSet, prey, _cellSize); // забитая добыча → труп
} }
foreach (var (eater, seed) in _ingested)
{
if (!eater.IsNull && !eater.HasComponent<GutSeed>())
{
eater.AddComponent(seed); // структурное добавление — после прохода
}
}
ApplyMatings(); ApplyMatings();
} }
// Эндозоохория: при выедании ЗРЕЛОГО растения шанс проглотить его семя — зверь понесёт его и
// высадит позже в другом месте (SeedDispersalSystem). Геном клонируется (отдельная особь-росток).
private void TryIngestSeed(int plantId, Entity eater)
{
if (
eater.IsNull
|| eater.HasComponent<GutSeed>()
|| plantId < 0
|| _rng.NextSingle() >= SeedIngestChance
|| !_store.TryGetEntityById(plantId, out var plant)
|| plant.IsNull
|| !plant.HasComponent<PlantGrowth>()
|| !plant.HasComponent<PlantOrganism>()
)
{
return;
}
ref readonly var grow = ref plant.GetComponent<PlantGrowth>();
if (grow.Stage < _plants[grow.Species].Stages.Length - 1)
{
return; // только зрелое растение даёт семя
}
var genome = new Genome(plant.GetComponent<PlantOrganism>().Genome.ToDictionary());
_ingested.Add(
(
eater,
new GutSeed
{
PlantSpecies = grow.Species,
Genome = genome,
DropInDays = GutSeedDropDays,
}
)
);
}
// Охота/падальщество: ведёт к цели и при контакте либо ест труп (утоляет голод, расходует мясо), // Охота/падальщество: ведёт к цели и при контакте либо ест труп (утоляет голод, расходует мясо),
// либо кусает живую добычу — урон части тела, острая кровопотеря, рана-кровотечение (первый // либо кусает живую добычу — урон части тела, острая кровопотеря, рана-кровотечение (первый
// травматический урон). Добитая добыча помечается на смерть (труп оставит общий путь Die). // травматический урон). Добитая добыча помечается на смерть (труп оставит общий путь Die).
@@ -2021,3 +2073,102 @@ public sealed class AnimalHealthSystem : BaseSystem
} }
} }
} }
/// <summary>
/// Разнос семян (эндозоохория): тикает таймер проглоченного семени (<see cref="GutSeed"/>); по
/// истечении высаживает росток того вида растения там, где сейчас зверь (если клетка — суша),
/// геномом-клоном съеденного растения, и снимает компонент. Делает травоядных разносчиками флоры —
/// растения расселяются вдоль кормовых маршрутов.
/// </summary>
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<GutSeed, Transform2D> _query;
private readonly List<Entity> _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<GutSeed, Transform2D>();
}
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<GutSeed>())
{
eater.RemoveComponent<GutSeed>();
}
}
}
}