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(); + } } }