diff --git a/Mods/Core/Defs/animals.json b/Mods/Core/Defs/animals.json index e7882a8..b7edc6d 100644 --- a/Mods/Core/Defs/animals.json +++ b/Mods/Core/Defs/animals.json @@ -22,6 +22,9 @@ "furColor": 0.45, "maturityAge": 90, "lifespan": 360, + "breedingSeason": 2, + "gestationDays": 30, + "litterSize": 1, "spread": 0.08 } } diff --git a/Mods/Core/Defs/genes.json b/Mods/Core/Defs/genes.json index e361b6b..c0a8f2f 100644 --- a/Mods/Core/Defs/genes.json +++ b/Mods/Core/Defs/genes.json @@ -109,6 +109,17 @@ { "defName": "GeneSex", "kind": "Discrete", "label": "gene.sex", "variants": 2, "variantWeights": [0.5, 0.5], "mutationChance": 0, "tags": ["animal", "sex"] }, + // Размножение (фаза A4): сезон гона фиксирован по виду (0 весна … 3 зима), срок вынашивания и помёт. + { "defName": "GeneBreedingSeason", "parent": "BaseNumericGene", "label": "gene.breedingSeason", + "default": 2, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["animal", "reproduction"], + "effects": { "breedingSeason": "value" } }, + { "defName": "GeneGestationDays", "parent": "BaseNumericGene", "label": "gene.gestationDays", + "default": 30, "min": 1, "max": 200, "tags": ["animal", "reproduction"], + "effects": { "gestationDays": "value" } }, + { "defName": "GeneLitterSize", "parent": "BaseNumericGene", "label": "gene.litterSize", + "default": 1, "min": 1, "max": 12, "tags": ["animal", "reproduction"], + "effects": { "litterSize": "value" } }, + // Дискретный ген морфы: вариант 0 доминирует, 1 (рецессивный) виден только в гомозиготе. { "defName": "GeneMorph", "kind": "Discrete", "label": "gene.morph", "variants": 2, "variantWeights": [0.82, 0.18], "mutationChance": 0.05, "tags": ["morphology"], diff --git a/Mods/Core/Defs/hediffs.json b/Mods/Core/Defs/hediffs.json new file mode 100644 index 0000000..7f786db --- /dev/null +++ b/Mods/Core/Defs/hediffs.json @@ -0,0 +1,8 @@ +{ + "type": "Hediff", + // Хедифы — состояния организма (фаза A4: лёгкий каркас). Гон (Rut) навешивается сезонно на взрослых + // и поднимает половое влечение. Раны/болезни/возрастные эффекты со стадиями придут в A5/A6. + "defs": [ + { "defName": "Rut", "label": "hediff.rut" } + ] +} diff --git a/docs/животные.md b/docs/животные.md index 891b25f..15902a3 100644 --- a/docs/животные.md +++ b/docs/животные.md @@ -1,8 +1,15 @@ # Животные (зоология) -> Статус: **A1–A3 реализованы**, остальное по плану. Это живой план — правится по мере +> Статус: **A1–A4 реализованы**, остальное по плану. Это живой план — правится по мере > реализации фаз A1–A8. Стиль и правила — как в [концепте](concept.md) и [модах](mods.md). > +> **A4 (готово):** нужда `Mating`; лёгкий hediff-каркас (`HediffDef`, `hediffs.json` с `Rut`, +> компонент `Health`/`HealthState`); гены `GeneBreedingSeason`/`GeneGestationDays`/`GeneLitterSize`; +> `AnimalRutSystem` (сезонный гон-хедиф поднимает влечение — вне сезона зачатия нет); действие +> `Mate` (поиск партнёра, сближение); `Pregnant` + `AnimalPregnancySystem` (вынашивание → роды +> через `Genome.Breed`, наследование пола без YY, поколение+1, потолок численности). Сборка +> чистая, `--check-content` (33 гена). +> > **A3 (готово):** компонент `AnimalGrowth` (стадия + возраст); ген пола `GeneSex` (XX/XY, > генерация основателя без YY через `AnimalSet.GenerateGenome`) и ген `GeneMaturityAge`; > стадии Baby→Juvenile→Adult→Senior с порогами из генов; `AnimalGrowthSystem` (смена diff --git a/src/LittleSim/Content/AnimalSet.cs b/src/LittleSim/Content/AnimalSet.cs index e07cd74..f028eeb 100644 --- a/src/LittleSim/Content/AnimalSet.cs +++ b/src/LittleSim/Content/AnimalSet.cs @@ -117,6 +117,9 @@ public sealed class AnimalSet new GenomeTemplate.Entry(Gene("GeneFurColor"), g.FurColor, 0.06f), Numeric("GeneMaturityAge", g.MaturityAge), Numeric("GeneLifespan", g.Lifespan), + new GenomeTemplate.Entry(Gene("GeneBreedingSeason"), g.BreedingSeason, 0f), + Numeric("GeneGestationDays", g.GestationDays), + Numeric("GeneLitterSize", g.LitterSize), ] ); } diff --git a/src/LittleSim/Content/GameContent.cs b/src/LittleSim/Content/GameContent.cs index 7643072..25e57fa 100644 --- a/src/LittleSim/Content/GameContent.cs +++ b/src/LittleSim/Content/GameContent.cs @@ -64,6 +64,7 @@ public sealed class GameContent defs.RegisterType("Plant"); defs.RegisterType("Pawn"); defs.RegisterType("Animal"); + defs.RegisterType("Hediff"); defs.RegisterType("WorldPreset"); // Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа. defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'"); diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs index 1118b07..a7340c7 100644 --- a/src/LittleSim/Content/GameDefs.cs +++ b/src/LittleSim/Content/GameDefs.cs @@ -247,6 +247,15 @@ public sealed class AnimalGenomeDef /// Продолжительность жизни (игровых дней) до смерти от старости. public float Lifespan { get; init; } = 160f; + /// Сезон гона (0 весна, 1 лето, 2 осень, 3 зима) — фиксирован по виду. + public float BreedingSeason { get; init; } = 2f; + + /// Срок вынашивания (игровых дней). + public float GestationDays { get; init; } = 30f; + + /// Размер помёта (число детёнышей за роды). + public float LitterSize { get; init; } = 1f; + /// Доля разброса аллелей вокруг базы при генерации особи. public float Spread { get; init; } = 0.08f; } @@ -267,3 +276,10 @@ public sealed class AnimalDef : PawnDef /// Базовый геном вида: центры аллелей животных генов. public AnimalGenomeDef Genome { get; init; } = new(); } + +/// +/// Состояние/хедиф организма (Defs/hediffs.json): гон, позже — раны, болезни, возрастные эффекты. +/// Фаза A4 — лёгкий каркас: пока идентичность + локализуемый ; стадии, +/// модификаторы способностей и иммунитет придут в A5/A6. +/// +public sealed class HediffDef : Def { } diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index ac61b13..0268584 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -213,6 +213,14 @@ public sealed class WorldScene : Scene // нужду (еда выедает растения — контур ёмкости среды). Внешний вид тускнеет с острой нуждой. var shore = ComputeShore(); UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay)); + UpdateSystems.Add( + new AnimalRutSystem( + Context.Clock, + SecondsPerDay, + climate, + content.Defs.Get("Rut") + ) + ); UpdateSystems.Add( new AnimalDecisionSystem(Store, Context.Clock, CellSize, shore, _config.Seed + 0x5EED) ); @@ -222,6 +230,17 @@ public sealed class WorldScene : Scene UpdateSystems.Add(new AnimalAppearanceSystem()); UpdateSystems.Add(new AnimalGrowthSystem(Context.Clock, SecondsPerDay, _animals, CellSize)); UpdateSystems.Add(new AnimalMortalitySystem(Store)); + UpdateSystems.Add( + new AnimalPregnancySystem( + Store, + _animals, + Context.Clock, + SecondsPerDay, + CellSize, + maxAnimals: 500, + _config.Seed + 0x4BED + ) + ); UpdateSystems.Add( new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen) diff --git a/src/LittleSim/Sim/AnimalFactory.cs b/src/LittleSim/Sim/AnimalFactory.cs index f419016..83017ea 100644 --- a/src/LittleSim/Sim/AnimalFactory.cs +++ b/src/LittleSim/Sim/AnimalFactory.cs @@ -68,7 +68,13 @@ public static class AnimalFactory Thirst = 1f, Rest = 1f, }, - new AnimalBrain { Action = AnimalAction.Wander, TargetPlant = -1 } + new AnimalBrain + { + Action = AnimalAction.Wander, + TargetPlant = -1, + TargetMate = -1, + }, + new Health { State = new HealthState() } ); } diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs index 2977571..56b4a03 100644 --- a/src/LittleSim/Sim/AnimalOrganism.cs +++ b/src/LittleSim/Sim/AnimalOrganism.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Friflo.Engine.ECS; +using LittleSim.Content; using MrGameEng.Genetics; namespace LittleSim.Sim; @@ -41,6 +42,15 @@ public struct AnimalPhenotype /// Продолжительность жизни (игровых дней). public float Lifespan; + /// Сезон гона (0 весна … 3 зима). + public int BreedingSeason; + + /// Срок вынашивания (игровых дней). + public float GestationDays; + + /// Размер помёта. + public float LitterSize; + /// Собирает фенотип из карты признаков, посчитанной . public static AnimalPhenotype FromTraits(IReadOnlyDictionary traits) { @@ -57,6 +67,9 @@ public struct AnimalPhenotype FurHue = T("furHue"), MaturityAge = T("maturityAge"), Lifespan = T("lifespan"), + BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)), + GestationDays = T("gestationDays"), + LitterSize = T("litterSize"), }; } } @@ -99,3 +112,78 @@ public struct AnimalGrowth : IComponent /// Накопленный возраст в игровых днях. public float AgeDays; } + +/// +/// Состояние здоровья особи (фаза A4 — лёгкий каркас): список активных хедифов. Managed-объект, как +/// (Friflo допускает ссылку в компоненте). В A5/A6 прирастёт частями тела, +/// кровью, capacities и иммунитетом — контейнер один, растёт по фазам. +/// +public sealed class HealthState +{ + private readonly List _hediffs = []; + + /// Активные хедифы (гон, позже — раны/болезни). + public IReadOnlyList Hediffs => _hediffs; + + /// Несёт ли особь хедиф с данным именем дефа. + public bool Has(string defName) + { + foreach (var h in _hediffs) + { + if (h.DefName == defName) + { + return true; + } + } + + return false; + } + + /// Навешивает хедиф (если ещё не навешан). + public void Add(HediffDef def) + { + if (!Has(def.DefName)) + { + _hediffs.Add(def); + } + } + + /// Снимает хедиф по имени дефа; возвращает, был ли он. + public bool Remove(string defName) + { + for (var i = 0; i < _hediffs.Count; i++) + { + if (_hediffs[i].DefName == defName) + { + _hediffs.RemoveAt(i); + return true; + } + } + + return false; + } +} + +/// Здоровье особи: managed-состояние со списком хедифов (см. ). +public struct Health : IComponent +{ + /// Состояние здоровья (хедифы; позже части тела/кровь). + public HealthState State; +} + +/// +/// Беременность самки (фаза A4): managed-ссылка на геном отца («слепок» на момент зачатия), остаток +/// срока вынашивания и размер помёта. По истечении срока +/// рожает детёнышей через и снимает компонент. +/// +public struct Pregnant : IComponent +{ + /// Геном отца на момент зачатия. + public Genome FatherGenome; + + /// Остаток срока вынашивания (игровых дней). + public float DueInDays; + + /// Сколько детёнышей родится. + public int Litter; +} diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs index 6f0ef68..53a5866 100644 --- a/src/LittleSim/Sim/AnimalSystems.cs +++ b/src/LittleSim/Sim/AnimalSystems.cs @@ -4,6 +4,7 @@ using LittleSim.Content; using Microsoft.Xna.Framework; using MrGameEng.AI; using MrGameEng.Core; +using MrGameEng.Genetics; using MrGameEng.Graphics; namespace LittleSim.Sim; @@ -22,6 +23,9 @@ public enum AnimalAction /// Стоит и спит (восстанавливает отдых). Sleep, + + /// Идёт к партнёру и спаривается (фаза A4) — у самки наступает беременность. + Mate, } /// @@ -43,7 +47,10 @@ public struct AnimalNeeds : IComponent /// Накоплено игровых дней истощения (голод/жажда на нуле); порог — смерть (A3). public float Starve; - /// Наименьшая (самая острая) нужда — для выбора и внешнего вида. + /// Половое влечение [0,1] (фаза A4): растёт под гоном (хедиф Rut), обнуляется спариванием. + public float Mating; + + /// Наименьшая (самая острая) витальная нужда — для выбора и внешнего вида (без влечения). public readonly float Worst() => MathF.Min(Hunger, MathF.Min(Thirst, Rest)); } @@ -63,12 +70,15 @@ public struct AnimalBrain : IComponent /// Id растения-цели для ; -1 — нет. public int TargetPlant; + /// Id партнёра для ; -1 — нет. + public int TargetMate; + /// Секунды до следующего пересмотра решения. public float DecideIn; } /// Снимок состояния зверя для соображений utility-выбора. -public readonly record struct AnimalContext(float Hunger, float Thirst, float Rest); +public readonly record struct AnimalContext(float Hunger, float Thirst, float Rest, float Mating); /// /// Падение нужд со временем (игровые дни через , с учётом паузы/скорости): @@ -152,6 +162,11 @@ public sealed class AnimalDecisionSystem : BaseSystem curve: ResponseCurve.Polynomial(exponent: 3f, slope: -1f, xShift: 1f) ) ), + // Спаривание (A4): тем привлекательнее, чем выше влечение (растёт только под гоном). + new UtilityAction( + "mate", + new Consideration("lustful", c => c.Mating) + ), new UtilityAction( "wander", new Consideration("idle", _ => 0.12f) @@ -162,7 +177,13 @@ public sealed class AnimalDecisionSystem : BaseSystem private readonly int _cellSize; private readonly Vector2[] _shore; private readonly Random _rng; - private readonly ArchetypeQuery _animals; + private readonly ArchetypeQuery< + AnimalNeeds, + AnimalBrain, + AnimalOrganism, + AnimalGrowth, + Transform2D + > _animals; private readonly ArchetypeQuery _plants; public AnimalDecisionSystem( @@ -177,18 +198,19 @@ public sealed class AnimalDecisionSystem : BaseSystem _cellSize = cellSize; _shore = shore; _rng = new Random(seed); - _animals = store.Query(); + _animals = store.Query(); _plants = store.Query(); } protected override void OnUpdateGroup() { var delta = _clock.DeltaTime; - foreach (var (needs, brains, organisms, transforms, _) in _animals.Chunks) + foreach (var (needs, brains, organisms, growths, transforms, _) in _animals.Chunks) { var n = needs.Span; var b = brains.Span; var o = organisms.Span; + var gr = growths.Span; var t = transforms.Span; for (var i = 0; i < b.Length; i++) { @@ -201,8 +223,9 @@ public sealed class AnimalDecisionSystem : BaseSystem brain.DecideIn = Interval + _rng.NextSingle() * Interval; // джиттер — расфазировать скан var pos = t[i].Position; + var adult = gr[i].Stage >= AnimalFactory.StageAdult; var name = _brain - .Select(new AnimalContext(n[i].Hunger, n[i].Thirst, n[i].Rest)) + .Select(new AnimalContext(n[i].Hunger, n[i].Thirst, n[i].Rest, n[i].Mating)) ?.Name; switch (name) @@ -221,6 +244,20 @@ public sealed class AnimalDecisionSystem : BaseSystem brain.Action = AnimalAction.Sleep; brain.TargetPlant = -1; break; + case "mate" + when adult + && TryFindMate( + pos, + o[i].IsMale, + o[i].Traits.Vision, + out var mateId, + out var matePos + ): + brain.Action = AnimalAction.Mate; + brain.TargetPlant = -1; + brain.TargetMate = mateId; + brain.Target = matePos; + break; default: brain.Action = AnimalAction.Wander; brain.TargetPlant = -1; @@ -284,6 +321,49 @@ public sealed class AnimalDecisionSystem : BaseSystem return true; } + + // Ближайший подходящий партнёр в радиусе зрения: взрослый, противоположного пола, с влечением > 0.5. + private bool TryFindMate( + Vector2 from, + bool selfMale, + float vision, + out int mateId, + out Vector2 position + ) + { + var radius = VisionCells * MathF.Max(0.2f, vision) * _cellSize; + var bestSq = radius * radius; + mateId = -1; + position = default; + foreach (var (needs, _, organisms, growths, transforms, entities) in _animals.Chunks) + { + var nn = needs.Span; + var oo = organisms.Span; + var gg = growths.Span; + var tt = transforms.Span; + for (var i = 0; i < tt.Length; i++) + { + if ( + oo[i].IsMale == selfMale + || gg[i].Stage < AnimalFactory.StageAdult + || nn[i].Mating < 0.5f + ) + { + continue; // тот же пол / не взрослый / без влечения (self отсеивается по полу) + } + + var sq = Vector2.DistanceSquared(from, tt[i].Position); + if (sq > 0.01f && sq < bestSq) + { + bestSq = sq; + mateId = entities.EntityAt(i).Id; + position = tt[i].Position; + } + } + } + + return mateId >= 0; + } } /// @@ -308,6 +388,7 @@ public sealed class AnimalActionSystem : BaseSystem private readonly RectF _bounds; private readonly ArchetypeQuery _query; private readonly List _eaten = []; + private readonly List<(Entity Self, Entity Partner)> _matings = []; public AnimalActionSystem( EntityStore store, @@ -335,8 +416,9 @@ public sealed class AnimalActionSystem : BaseSystem var days = seconds / _secondsPerDay; _eaten.Clear(); + _matings.Clear(); - foreach (var (brains, needs, organisms, transforms, _) in _query.Chunks) + foreach (var (brains, needs, organisms, transforms, entities) in _query.Chunks) { var b = brains.Span; var n = needs.Span; @@ -372,6 +454,23 @@ public sealed class AnimalActionSystem : BaseSystem break; + case AnimalAction.Mate: + if ( + brain.TargetMate >= 0 + && _store.TryGetEntityById(brain.TargetMate, out var partner) + && !partner.IsNull + && partner.HasComponent() + ) + { + var partnerPos = partner.GetComponent().Position; + if (MoveTo(ref pos, partnerPos, speed * seconds)) + { + _matings.Add((entities.EntityAt(i), partner)); + } + } + + break; + default: // Wander MoveTo(ref pos, brain.Target, speed * seconds); break; @@ -386,6 +485,58 @@ public sealed class AnimalActionSystem : BaseSystem { plant.DeleteEntity(); } + + ApplyMatings(); + } + + // Применяет спаривания после прохода: самка пары беременеет геномом самца (структурное добавление + // компонента — после итерации), влечение обоих обнуляется. Двойные события на одну самку отсеиваются. + private void ApplyMatings() + { + foreach (var (a, b) in _matings) + { + if ( + a.IsNull + || b.IsNull + || !a.HasComponent() + || !b.HasComponent() + ) + { + continue; + } + + var aMale = a.GetComponent().IsMale; + var bMale = b.GetComponent().IsMale; + if (aMale == bMale) + { + continue; // один пол — не пара (страховка) + } + + var mother = aMale ? b : a; + var father = aMale ? a : b; + if (!mother.HasComponent()) + { + ref readonly var mom = ref mother.GetComponent(); + mother.AddComponent( + new Pregnant + { + FatherGenome = father.GetComponent().Genome, + DueInDays = MathF.Max(1f, mom.Traits.GestationDays), + Litter = Math.Max(1, (int)MathF.Round(mom.Traits.LitterSize)), + } + ); + } + + if (a.HasComponent()) + { + a.GetComponent().Mating = 0f; + } + + if (b.HasComponent()) + { + b.GetComponent().Mating = 0f; + } + } } // Двигает позицию к цели на step; возвращает true, если уже у цели (можно исполнять действие). @@ -544,3 +695,179 @@ public sealed class AnimalMortalitySystem : BaseSystem } } } + +/// +/// Гон и влечение (фаза A4): взрослым в их брачный сезон навешивает хедиф Rut и поднимает половое +/// влечение (); вне сезона снимает хедиф, влечение спадает. Только под +/// гоном влечение переходит порог действия — вне сезона зачатия фактически нет. +/// +public sealed class AnimalRutSystem( + GameClock clock, + float secondsPerDay, + Climate climate, + HediffDef rut +) : QuerySystem +{ + private const float RutDrivePerDay = 1.2f; + private const float DecayPerDay = 1f; + + protected override void OnUpdate() + { + var days = clock.DeltaTime / secondsPerDay; + if (days <= 0f) + { + return; + } + + var season = (int)climate.Season; + foreach (var (growths, organisms, needs, healths, _) in Query.Chunks) + { + var g = growths.Span; + var o = organisms.Span; + var n = needs.Span; + var h = healths.Span; + for (var i = 0; i < g.Length; i++) + { + var inRut = + g[i].Stage >= AnimalFactory.StageAdult && season == o[i].Traits.BreedingSeason; + var state = h[i].State; + if (inRut) + { + state.Add(rut); + } + else + { + state.Remove(rut.DefName); + } + + ref var need = ref n[i]; + need.Mating = inRut + ? Math.Clamp(need.Mating + RutDrivePerDay * days, 0f, 1f) + : MathF.Max(0f, need.Mating - DecayPerDay * days); + } + } + } +} + +/// +/// Беременность и роды (фаза A4): тикает срок вынашивания; по его истечении рожает помёт — каждый +/// детёныш = генома матери и отца (пол наследуется без YY), стадия Baby, +/// поколение матери + 1, рядом с матерью. Компонент снимается. Жёсткий потолок +/// численности страхует от взрыва популяции (бум-крах остаётся ниже потолка). +/// +public sealed class AnimalPregnancySystem : BaseSystem +{ + private readonly EntityStore _store; + private readonly AnimalSet _animals; + private readonly GameClock _clock; + private readonly float _secondsPerDay; + private readonly int _cellSize; + private readonly int _maxAnimals; + private readonly Random _rng; + private readonly ArchetypeQuery _query; + private readonly ArchetypeQuery _census; + private readonly List _births = []; + + public AnimalPregnancySystem( + EntityStore store, + AnimalSet animals, + GameClock clock, + float secondsPerDay, + int cellSize, + int maxAnimals, + int seed + ) + { + _store = store; + _animals = animals; + _clock = clock; + _secondsPerDay = secondsPerDay; + _cellSize = cellSize; + _maxAnimals = maxAnimals; + _rng = new Random(seed); + _query = store.Query(); + _census = store.Query(); + } + + protected override void OnUpdateGroup() + { + var days = _clock.DeltaTime / _secondsPerDay; + if (days <= 0f) + { + return; + } + + _births.Clear(); + foreach (var (pregnancies, organisms, transforms, entities) in _query.Chunks) + { + var p = pregnancies.Span; + var o = organisms.Span; + var t = transforms.Span; + for (var i = 0; i < p.Length; i++) + { + ref var preg = ref p[i]; + preg.DueInDays -= days; + if (preg.DueInDays > 0f) + { + continue; + } + + _births.Add( + new Birth + { + Mother = entities.EntityAt(i), + Species = o[i].Species, + MotherGenome = o[i].Genome, + FatherGenome = preg.FatherGenome, + Litter = preg.Litter, + Generation = o[i].Generation + 1, + Position = t[i].Position, + } + ); + } + } + + var count = _census.Count; + foreach (var birth in _births) + { + if (birth.Mother.HasComponent()) + { + birth.Mother.RemoveComponent(); + } + + for (var k = 0; k < birth.Litter && count < _maxAnimals; k++) + { + var child = Genome.Breed( + birth.MotherGenome, + birth.FatherGenome, + _animals.GeneRegistry, + _rng + ); + var offset = + new Vector2(_rng.NextSingle() - 0.5f, _rng.NextSingle() - 0.5f) * _cellSize; + AnimalFactory.Create( + _store, + _animals, + birth.Species, + birth.Position + offset, + ageDays: 0f, + child, + birth.Generation, + _cellSize + ); + count++; + } + } + } + + private struct Birth + { + public Entity Mother; + public int Species; + public Genome MotherGenome; + public Genome FatherGenome; + public int Litter; + public int Generation; + public Vector2 Position; + } +}