diff --git a/Mods/Core/Defs/Animals/Animals.json b/Mods/Core/Defs/Animals/Animals.json
index 8ace26b..ed10555 100644
--- a/Mods/Core/Defs/Animals/Animals.json
+++ b/Mods/Core/Defs/Animals/Animals.json
@@ -101,6 +101,36 @@
"GeneOmnivory": 0.85,
"GeneToxinTolerance": 0.25
}
+ },
+
+ {
+ "defName": "Chicken",
+ "label": "pawn.chicken",
+ "kind": "animal",
+ "texture": "things/pawn/animal/chicken/Chicken_east",
+ "body": "Quadruped",
+ "diet": ["plant"],
+ "spawnPer1000Cells": 1.5,
+ "baseSpeed": 22,
+ "visionCells": 10,
+ "genome": {
+ "GeneMaxBodySize": 0.5,
+ "GeneMetabolism": 1.3,
+ "GeneMoveSpeed": 0.9,
+ "GeneBloodVolume": 0.7,
+ "GeneVision": 1.1,
+ "GeneBrainSize": 0.3,
+ "GeneInsulation": 0.45,
+ "GeneFurColor": 0.6,
+ "GeneMaturityAge": 50,
+ "GeneLifespan": 180,
+ "GeneBreedingSeason": 1,
+ "GeneGestationDays": 12,
+ "GeneLitterSize": 6,
+ "GeneHerbivory": 0.8,
+ "GeneToxinTolerance": 0.15,
+ "GeneEggLaying": 1.0
+ }
}
]
}
diff --git a/Mods/Core/Defs/Genes/Animal.json b/Mods/Core/Defs/Genes/Animal.json
index a5c05fa..cc54db5 100644
--- a/Mods/Core/Defs/Genes/Animal.json
+++ b/Mods/Core/Defs/Genes/Animal.json
@@ -59,6 +59,13 @@
{ "defName": "GeneSex", "kind": "Discrete", "label": "gene.sex",
"variants": 2, "variantWeights": [0.5, 0.5], "mutationChance": 0, "tags": ["animal", "sex"] },
+ // Тип рождения (цикл зачатия): живородящие (eggLaying < 0.5) вынашивают и рожают живых детёнышей;
+ // яйцекладущие (≥ 0.5) после вынашивания ОТКЛАДЫВАЮТ яйца-сущности, которые инкубируются и
+ // вылупляются (см. AnimalPregnancySystem/EggSystem). Признак фиксирован по виду (spread/mutation 0).
+ { "defName": "GeneEggLaying", "parent": "BaseNumericGene", "label": "gene.eggLaying",
+ "default": 0.0, "min": 0.0, "max": 1.0, "spread": 0, "mutationChance": 0, "tags": ["animal", "reproduction"],
+ "effects": { "eggLaying": "value" } },
+
// Размножение (фаза A4): сезон гона фиксирован по виду (0 весна … 3 зима), срок вынашивания и помёт.
{ "defName": "GeneBreedingSeason", "parent": "BaseNumericGene", "label": "gene.breedingSeason",
"default": 2, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["animal", "reproduction"],
diff --git a/Mods/Core/Languages/en/ui.json b/Mods/Core/Languages/en/ui.json
index 57f6032..a63e94b 100644
--- a/Mods/Core/Languages/en/ui.json
+++ b/Mods/Core/Languages/en/ui.json
@@ -163,6 +163,7 @@
"pawn.hare": "hare",
"pawn.boar": "boar",
"pawn.wolf": "wolf",
+ "pawn.chicken": "chicken",
"pawn.muffalo": "muffalo",
"pawn.squirrel": "squirrel",
"thought.gaveBirth": "gave birth",
diff --git a/Mods/Core/Languages/ru/ui.json b/Mods/Core/Languages/ru/ui.json
index 00b9a3e..8cb7bef 100644
--- a/Mods/Core/Languages/ru/ui.json
+++ b/Mods/Core/Languages/ru/ui.json
@@ -163,6 +163,7 @@
"pawn.hare": "заяц",
"pawn.boar": "кабан",
"pawn.wolf": "волк",
+ "pawn.chicken": "курица",
"pawn.muffalo": "муффало",
"pawn.squirrel": "белка",
"thought.gaveBirth": "родила потомство",
diff --git a/src/LittleSim/App/WorldSave.cs b/src/LittleSim/App/WorldSave.cs
index ce1b9e8..752672c 100644
--- a/src/LittleSim/App/WorldSave.cs
+++ b/src/LittleSim/App/WorldSave.cs
@@ -173,6 +173,28 @@ public sealed class CorpseSave
public float Meat { get; set; }
}
+/// Сериализуемое яйцо (тип рождения «яйцекладка»): вид, позиция, геном детёныша, поколение, инкубация.
+public sealed class EggSave
+{
+ /// Имя дефа вида — по нему берётся индекс/спрайт/тело при вылуплении.
+ public string Species { get; set; } = "";
+
+ /// Позиция X в мировых координатах.
+ public float X { get; set; }
+
+ /// Позиция Y в мировых координатах.
+ public float Y { get; set; }
+
+ /// Поколение будущего детёныша.
+ public int Generation { get; set; }
+
+ /// Остаток инкубации в игровых днях.
+ public float IncubateDays { get; set; }
+
+ /// Геном будущего детёныша (уже скрещён на момент кладки).
+ public Dictionary Genome { get; set; } = new();
+}
+
///
/// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание),
/// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он
@@ -216,6 +238,9 @@ public sealed class WorldSave
/// Снимок трупов (вид, позиция, таймер/стадия разложения, мясо).
public List Corpses { get; set; } = [];
+ /// Снимок яиц (вид, позиция, геном детёныша, поколение, инкубация).
+ public List Eggs { get; set; } = [];
+
/// Конфиг мира для пересоздания сцены.
public WorldConfig ToConfig() =>
new()
diff --git a/src/LittleSim/Content/AnimalSet.cs b/src/LittleSim/Content/AnimalSet.cs
index 4c6792b..ca3fbb8 100644
--- a/src/LittleSim/Content/AnimalSet.cs
+++ b/src/LittleSim/Content/AnimalSet.cs
@@ -30,6 +30,9 @@ public sealed class AnimalSet
/// Спрайт трупа (fallback — самка).
public required Texture2DRegion Corpse { get; init; }
+ /// Спрайт яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт-точка.
+ public required Texture2DRegion Egg { get; init; }
+
/// Стадии роста (из дефа или встроенный дефолт), по возрастанию возраста входа.
public required AnimalStageDef[] Stages { get; init; }
@@ -43,10 +46,40 @@ public sealed class AnimalSet
// Встроенный набор стадий по умолчанию (если вид не задал свой): как было до выноса в данные.
private static readonly AnimalStageDef[] DefaultStages =
[
- new() { Name = "Baby", EnterAt = 0f, RelativeTo = "maturity", Scale = 0.45f, Texture = "baby" },
- new() { Name = "Juvenile", EnterAt = 0.22f, RelativeTo = "maturity", Scale = 0.7f, Texture = "baby" },
- new() { Name = "Adult", EnterAt = 1f, RelativeTo = "maturity", Scale = 1f, Texture = "adult", Adult = true },
- new() { Name = "Senior", EnterAt = 0.8f, RelativeTo = "lifespan", Scale = 0.92f, Texture = "adult", Adult = true },
+ new()
+ {
+ Name = "Baby",
+ EnterAt = 0f,
+ RelativeTo = "maturity",
+ Scale = 0.45f,
+ Texture = "baby",
+ },
+ new()
+ {
+ Name = "Juvenile",
+ EnterAt = 0.22f,
+ RelativeTo = "maturity",
+ Scale = 0.7f,
+ Texture = "baby",
+ },
+ new()
+ {
+ Name = "Adult",
+ EnterAt = 1f,
+ RelativeTo = "maturity",
+ Scale = 1f,
+ Texture = "adult",
+ Adult = true,
+ },
+ new()
+ {
+ Name = "Senior",
+ EnterAt = 0.8f,
+ RelativeTo = "lifespan",
+ Scale = 0.92f,
+ Texture = "adult",
+ Adult = true,
+ },
];
private readonly Species[] _species;
@@ -82,6 +115,9 @@ public sealed class AnimalSet
Corpse = string.IsNullOrEmpty(def.CorpseTexture)
? female
: atlases.GetRegion(device, def.CorpseTexture),
+ Egg = string.IsNullOrEmpty(def.EggTexture)
+ ? female
+ : atlases.GetRegion(device, def.EggTexture),
Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages,
Body = content.Defs.TryGet(def.Body, out var body) ? body : null,
Template = BuildTemplate(def.Genome, genes),
@@ -111,6 +147,35 @@ public sealed class AnimalSet
return genome;
}
+ ///
+ /// ОСНОВА создания гибрида (полноценная команда/UI — позже): геном помеси, привязанный к базовому виду
+ /// (его тело/спрайт/набор генов), но с общими генами, скрещёнными с
+ /// (по одной аллели-гамете от каждого родителя, как при размножении).
+ /// Пол берётся от базового вида; гены, которых нет у базового, игнорируются (его тело их не читает).
+ /// Сущность создаётся обычной с этим геномом и базовым видом —
+ /// то есть «создать любой вид» = Create(вид, GenerateGenome), «любой гибрид» = Create(вид, HybridGenome).
+ ///
+ public Genome HybridGenome(int baseSpecies, int otherSpecies, Random random)
+ {
+ var hybrid = GenerateGenome(baseSpecies, random);
+ var other = _species[otherSpecies].Template.Generate(random);
+ foreach (var (geneId, allele) in other.ToDictionary())
+ {
+ if (string.Equals(geneId, "GeneSex", StringComparison.Ordinal) || !hybrid.Has(geneId))
+ {
+ continue; // пол — от базового вида; чужие гены базовое тело не читает
+ }
+
+ var mine = hybrid[geneId];
+ hybrid[geneId] = new Allele(
+ random.NextSingle() < 0.5f ? mine.A : mine.B,
+ random.NextSingle() < 0.5f ? allele.A : allele.B
+ );
+ }
+
+ return hybrid;
+ }
+
// Строит шаблон генома вида из ДАННЫХ (geneId→base из Defs/Animals/) generically: разброс берётся из
// самого гена. Состав открыт — модер добавляет ген строкой JSON, без правки кода. Дискретные гены
// (база/разброс не нужны — варианты из GeneDef) тоже поддержаны.
diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs
index 43c6637..c3fa062 100644
--- a/src/LittleSim/Content/GameDefs.cs
+++ b/src/LittleSim/Content/GameDefs.cs
@@ -261,6 +261,9 @@ public sealed class AnimalDef : PawnDef
/// Текстура трупа (фаза A5); пусто — берётся .
public string CorpseTexture { get; init; } = "";
+ /// Текстура яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт семени-точки.
+ public string EggTexture { get; init; } = "things/plant/seed_default";
+
/// Имя — анатомия вида (части тела/органы); пусто — без частей тела.
public string Body { get; init; } = "";
diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs
index 01bd96d..8ecf0a0 100644
--- a/src/LittleSim/Scenes/WorldScene.cs
+++ b/src/LittleSim/Scenes/WorldScene.cs
@@ -294,6 +294,17 @@ public sealed class WorldScene : Scene
_config.Seed + 0x4BED
)
);
+ UpdateSystems.Add(
+ new EggSystem(
+ Store,
+ _animals,
+ _needs,
+ Context.Clock,
+ SecondsPerDay,
+ CellSize,
+ _config.Seed + 0x6E66
+ )
+ );
UpdateSystems.Add(
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
@@ -519,7 +530,7 @@ public sealed class WorldScene : Scene
new SaveStore().Write(save);
Log.Info(
$"World '{_config.Name}' saved ({save.Plants.Count} plants, "
- + $"{save.Animals.Count} animals, {save.Corpses.Count} corpses)"
+ + $"{save.Animals.Count} animals, {save.Corpses.Count} corpses, {save.Eggs.Count} eggs)"
);
return _config.Name;
}
@@ -614,6 +625,23 @@ public sealed class WorldScene : Scene
}
)
);
+
+ Store
+ .Query()
+ .ForEachEntity(
+ (ref Egg egg, ref Transform2D transform, Entity _) =>
+ save.Eggs.Add(
+ new EggSave
+ {
+ Species = _animals[egg.Species].Def.DefName,
+ X = transform.Position.X,
+ Y = transform.Position.Y,
+ Generation = egg.Generation,
+ IncubateDays = egg.IncubateDays,
+ Genome = egg.Genome?.ToDictionary() ?? new(),
+ }
+ )
+ );
}
private void RestorePlants(GameContent content)
@@ -695,8 +723,31 @@ public sealed class WorldScene : Scene
data.Meat = saved.Meat;
}
+ foreach (var saved in _save.Eggs)
+ {
+ if (!content.Defs.TryGet(saved.Species, out var def))
+ {
+ continue; // вид пропал (мод убрали) — пропускаем яйцо
+ }
+
+ var index = _animals.IndexOf(def);
+ var genome = saved.Genome is { Count: > 0 }
+ ? new Genome(saved.Genome)
+ : _animals.GenerateGenome(index, fallback);
+ AnimalFactory.CreateEgg(
+ Store,
+ _animals,
+ index,
+ new Vector2(saved.X, saved.Y),
+ genome,
+ saved.Generation,
+ saved.IncubateDays,
+ CellSize
+ );
+ }
+
Log.Info(
- $"Restored {_save.Animals.Count} animals, {_save.Corpses.Count} corpses from save"
+ $"Restored {_save.Animals.Count} animals, {_save.Corpses.Count} corpses, {_save.Eggs.Count} eggs from save"
);
}
@@ -1136,6 +1187,10 @@ public sealed class WorldScene : Scene
$" diet: herb {traits.Herbivory:0.##} / carn {traits.Carnivory:0.##} / omni {traits.Omnivory:0.##}, "
+ $"toxinTolerance {traits.ToxinTolerance:0.##}"
);
+ console.WriteLine(
+ $" reproduction: {(traits.Oviparous ? "oviparous (lays eggs)" : "viviparous (live birth)")}, "
+ + $"gestation {traits.GestationDays:0} d, litter {traits.LitterSize:0.#}"
+ );
}
// Диета из генов (предатор-кластер): по каждому виду печатает herbivory/carnivory/omnivory,
diff --git a/src/LittleSim/Sim/AnimalFactory.cs b/src/LittleSim/Sim/AnimalFactory.cs
index 6eb5596..268ece5 100644
--- a/src/LittleSim/Sim/AnimalFactory.cs
+++ b/src/LittleSim/Sim/AnimalFactory.cs
@@ -110,6 +110,46 @@ public static class AnimalFactory
);
}
+ // Тинт яйца (бледно-кремовый) и доля размера клетки, до которой масштабируется спрайт яйца.
+ private static readonly Color EggTint = new(238, 230, 208);
+ private const float EggSizeFraction = 0.45f;
+
+ ///
+ /// Создаёт сущность-яйцо (тип рождения «яйцекладка»): спрайт-яйцо и компонент с уже
+ /// скрещённым геномом будущего детёныша. Инкубируется и вылупляется в .
+ ///
+ public static Entity CreateEgg(
+ EntityStore store,
+ AnimalSet animals,
+ int species,
+ Vector2 position,
+ Genome genome,
+ int generation,
+ float incubateDays,
+ int cellSize
+ )
+ {
+ var sp = animals[species];
+ var region = sp.Egg;
+ var sprite = new Sprite(region, GameLayers.Beings);
+ sprite.CenterOrigin();
+ sprite.Color = EggTint;
+ return store.CreateEntity(
+ new Transform2D(
+ position,
+ scale: new Vector2(cellSize * EggSizeFraction / region.Width)
+ ),
+ sprite,
+ new Egg
+ {
+ Species = species,
+ Genome = genome,
+ Generation = generation,
+ IncubateDays = incubateDays,
+ }
+ );
+ }
+
/// Порог выраженности признака диеты, с которого вид реально ест данный корм.
public const float DietThreshold = 0.5f;
diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs
index 6523e76..73acdcc 100644
--- a/src/LittleSim/Sim/AnimalOrganism.cs
+++ b/src/LittleSim/Sim/AnimalOrganism.cs
@@ -63,6 +63,12 @@ public struct AnimalPhenotype
/// Размер помёта.
public float LitterSize;
+ /// Тип рождения [0..1]: ≥0.5 — яйцекладущий (откладывает яйца), иначе живородящий.
+ public float EggLaying;
+
+ /// Яйцекладущий ли вид (тип рождения) — определяет цикл зачатия.
+ public readonly bool Oviparous => EggLaying >= 0.5f;
+
/// Собирает фенотип из карты признаков, посчитанной .
public static AnimalPhenotype FromTraits(IReadOnlyDictionary traits)
{
@@ -86,6 +92,7 @@ public struct AnimalPhenotype
BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)),
GestationDays = T("gestationDays"),
LitterSize = T("litterSize"),
+ EggLaying = T("eggLaying"),
};
}
}
@@ -457,6 +464,26 @@ public struct Corpse : IComponent
public float Meat;
}
+///
+/// Яйцо (тип рождения «яйцекладка»): отдельная сущность, отложенная яйцекладущей самкой вместо живых
+/// родов. Несёт уже скрещённый геном будущего детёныша, вид и поколение; инкубируется во времени и
+/// вылупляется в детёныша (). Геном — managed-ссылка (как у трупа/беременности).
+///
+public struct Egg : IComponent
+{
+ /// Индекс вида в (для спрайта при вылуплении и сейва).
+ public int Species;
+
+ /// Геном будущего детёныша (уже скрещён из родителей на момент кладки).
+ public Genome Genome;
+
+ /// Поколение будущего детёныша.
+ public int Generation;
+
+ /// Остаток инкубации в игровых днях; ≤0 — вылупление.
+ public float IncubateDays;
+}
+
/// Инстанс мысли настроения на особи (фаза A8): деф события + остаток времени до угасания.
public struct ThoughtInstance
{
diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs
index c7d78bf..90d2974 100644
--- a/src/LittleSim/Sim/AnimalSystems.cs
+++ b/src/LittleSim/Sim/AnimalSystems.cs
@@ -1656,6 +1656,8 @@ public sealed class AnimalPregnancySystem : BaseSystem
Litter = preg.Litter,
Generation = o[i].Generation + 1,
Position = t[i].Position,
+ Oviparous = o[i].Traits.Oviparous,
+ IncubateDays = MathF.Max(1f, o[i].Traits.GestationDays),
}
);
}
@@ -1685,17 +1687,35 @@ public sealed class AnimalPregnancySystem : BaseSystem
);
var offset =
new Vector2(_rng.NextSingle() - 0.5f, _rng.NextSingle() - 0.5f) * _cellSize;
- AnimalFactory.Create(
- _store,
- _animals,
- _needs,
- birth.Species,
- birth.Position + offset,
- ageDays: 0f,
- child,
- birth.Generation,
- _cellSize
- );
+ if (birth.Oviparous)
+ {
+ // Яйцекладка: вместо живого детёныша откладываем яйцо с его геномом — вылупится позже.
+ AnimalFactory.CreateEgg(
+ _store,
+ _animals,
+ birth.Species,
+ birth.Position + offset,
+ child,
+ birth.Generation,
+ birth.IncubateDays,
+ _cellSize
+ );
+ }
+ else
+ {
+ AnimalFactory.Create(
+ _store,
+ _animals,
+ _needs,
+ birth.Species,
+ birth.Position + offset,
+ ageDays: 0f,
+ child,
+ birth.Generation,
+ _cellSize
+ );
+ }
+
count++;
}
}
@@ -1710,6 +1730,90 @@ public sealed class AnimalPregnancySystem : BaseSystem
public int Litter;
public int Generation;
public Vector2 Position;
+ public bool Oviparous;
+ public float IncubateDays;
+ }
+}
+
+///
+/// Инкубация яиц (тип рождения «яйцекладка»): тикает остаток инкубации каждого яйца; по истечении
+/// вылупляет детёныша ( из хранимого генома, стадия Baby) рядом с
+/// яйцом и удаляет яйцо. Зеркало родов для яйцекладущих видов.
+///
+public sealed class EggSystem : BaseSystem
+{
+ private readonly EntityStore _store;
+ private readonly AnimalSet _animals;
+ private readonly NeedSet _needs;
+ private readonly GameClock _clock;
+ private readonly float _secondsPerDay;
+ private readonly int _cellSize;
+ private readonly Random _rng;
+ private readonly ArchetypeQuery _query;
+ private readonly List _hatched = [];
+
+ public EggSystem(
+ EntityStore store,
+ AnimalSet animals,
+ NeedSet needs,
+ GameClock clock,
+ float secondsPerDay,
+ int cellSize,
+ int seed
+ )
+ {
+ _store = store;
+ _animals = animals;
+ _needs = needs;
+ _clock = clock;
+ _secondsPerDay = secondsPerDay;
+ _cellSize = cellSize;
+ _rng = new Random(seed);
+ _query = store.Query();
+ }
+
+ protected override void OnUpdateGroup()
+ {
+ var days = _clock.DeltaTime / _secondsPerDay;
+ if (days <= 0f)
+ {
+ return;
+ }
+
+ _hatched.Clear();
+ foreach (var (eggs, transforms, entities) in _query.Chunks)
+ {
+ var e = eggs.Span;
+ for (var i = 0; i < e.Length; i++)
+ {
+ ref var egg = ref e[i];
+ egg.IncubateDays -= days;
+ if (egg.IncubateDays > 0f)
+ {
+ continue;
+ }
+
+ var offset =
+ new Vector2(_rng.NextSingle() - 0.5f, _rng.NextSingle() - 0.5f) * _cellSize;
+ AnimalFactory.Create(
+ _store,
+ _animals,
+ _needs,
+ egg.Species,
+ transforms.Span[i].Position + offset,
+ ageDays: 0f,
+ egg.Genome,
+ egg.Generation,
+ _cellSize
+ );
+ _hatched.Add(entities.EntityAt(i));
+ }
+ }
+
+ foreach (var egg in _hatched)
+ {
+ egg.DeleteEntity();
+ }
}
}