diff --git a/Mods/Core/Defs/animals.json b/Mods/Core/Defs/animals.json index f7a8a4d..e7882a8 100644 --- a/Mods/Core/Defs/animals.json +++ b/Mods/Core/Defs/animals.json @@ -9,6 +9,8 @@ "label": "pawn.deer", "kind": "animal", "texture": "things/pawn/animal/deer/DeerFemale_east", + "maleTexture": "things/pawn/animal/deer/DeerMale_east", + "babyTexture": "things/pawn/animal/deer/DeerBaby_east", "genome": { "bodySize": 1.5, "metabolism": 1.0, @@ -18,6 +20,7 @@ "brainSize": 0.45, "insulation": 0.55, "furColor": 0.45, + "maturityAge": 90, "lifespan": 360, "spread": 0.08 } diff --git a/Mods/Core/Defs/genes.json b/Mods/Core/Defs/genes.json index cc17691..e361b6b 100644 --- a/Mods/Core/Defs/genes.json +++ b/Mods/Core/Defs/genes.json @@ -99,6 +99,15 @@ { "defName": "GeneFurColor", "parent": "BaseNumericGene", "label": "gene.furColor", "default": 0.5, "min": 0.0, "max": 1.0, "spread": 0.06, "tags": ["animal", "morphology", "color"], "effects": { "furHue": "value" } }, + { "defName": "GeneMaturityAge", "parent": "BaseNumericGene", "label": "gene.maturityAge", + "default": 90, "min": 1, "max": 400, "tags": ["animal", "lifecycle"], + "effects": { "maturityAge": "value" } }, + + // Пол как локус (фаза A3): аллели {X=0, Y=1}; самка XX, самец XY. Эффекта нет — пол читается из + // самих аллелей (наличие Y), не из выраженного значения. mutationChance 0 (X не мутирует в Y). + // Генерация особи-основателя задаёт пол явно (XX или XY), чтобы не возник невозможный YY. + { "defName": "GeneSex", "kind": "Discrete", "label": "gene.sex", + "variants": 2, "variantWeights": [0.5, 0.5], "mutationChance": 0, "tags": ["animal", "sex"] }, // Дискретный ген морфы: вариант 0 доминирует, 1 (рецессивный) виден только в гомозиготе. { "defName": "GeneMorph", "kind": "Discrete", "label": "gene.morph", diff --git a/docs/животные.md b/docs/животные.md index 244fc7a..891b25f 100644 --- a/docs/животные.md +++ b/docs/животные.md @@ -1,8 +1,16 @@ # Животные (зоология) -> Статус: **A1–A2 реализованы**, остальное по плану. Это живой план — правится по мере +> Статус: **A1–A3 реализованы**, остальное по плану. Это живой план — правится по мере > реализации фаз A1–A8. Стиль и правила — как в [концепте](concept.md) и [модах](mods.md). > +> **A3 (готово):** компонент `AnimalGrowth` (стадия + возраст); ген пола `GeneSex` (XX/XY, +> генерация основателя без YY через `AnimalSet.GenerateGenome`) и ген `GeneMaturityAge`; +> стадии Baby→Juvenile→Adult→Senior с порогами из генов; `AnimalGrowthSystem` (смена +> спрайта детёныш/самец/самка и размера по стадии) + `AnimalMortalitySystem` (смерть от +> старости и от истощения → деспавн, замыкает контур ёмкости среды). Спавн стада — +> разновозрастный; команда `animal` показывает пол/созревание. Сборка чистая, `--check-content` +> (30 генов). +> > **A1 (готово):** тип дефа `Animal` (`AnimalDef : PawnDef`) + `animals.json` (олень); > 8 животных генов в `genes.json`; `AnimalPhenotype`/`AnimalOrganism`; `AnimalSet` > (шаблон генома вида) + `AnimalFactory`; детерминированный спавн стада оленей по суше в diff --git a/src/LittleSim/Content/AnimalSet.cs b/src/LittleSim/Content/AnimalSet.cs index f9f66aa..e07cd74 100644 --- a/src/LittleSim/Content/AnimalSet.cs +++ b/src/LittleSim/Content/AnimalSet.cs @@ -12,14 +12,20 @@ namespace LittleSim.Content; /// public sealed class AnimalSet { - /// Вид животного: его деф, спрайт и набор генов для генерации особи. + /// Вид животного: его деф, спрайты по полу/стадии и набор генов для генерации особи. public sealed class Species { /// Исходный деф животного. public required AnimalDef Def { get; init; } - /// Спрайт вида (фаза A1 — единый; стадии/пол позже). - public required Texture2DRegion Region { get; init; } + /// Спрайт взрослой самки (он же общий/fallback). + public required Texture2DRegion Female { get; init; } + + /// Спрайт взрослого самца (fallback — самка). + public required Texture2DRegion Male { get; init; } + + /// Спрайт детёныша/подростка (fallback — самка). + public required Texture2DRegion Baby { get; init; } /// Набор генов вида: базовые значения для генерации особи. public required GenomeTemplate Template { get; init; } @@ -44,10 +50,17 @@ public sealed class AnimalSet for (var i = 0; i < defs.Length; i++) { var def = defs[i]; + var female = atlases.GetRegion(device, def.Texture); _species[i] = new Species { Def = def, - Region = atlases.GetRegion(device, def.Texture), + Female = female, + Male = string.IsNullOrEmpty(def.MaleTexture) + ? female + : atlases.GetRegion(device, def.MaleTexture), + Baby = string.IsNullOrEmpty(def.BabyTexture) + ? female + : atlases.GetRegion(device, def.BabyTexture), Template = BuildTemplate(def.Genome, genes), }; _index[def] = i; @@ -63,6 +76,18 @@ public sealed class AnimalSet /// Индекс вида по дефу. public int IndexOf(AnimalDef def) => _index[def]; + /// + /// Генерирует геном новой особи-основателя вида: аллели из шаблона + явно назначенный пол + /// (XX самка / XY самец, 50/50) — чтобы не возник невозможный YY (см. genes.json GeneSex). + /// Скрещивание (A4) наследует пол само (мать XX отдаёт только X). + /// + public Genome GenerateGenome(int species, Random random) + { + var genome = _species[species].Template.Generate(random); + genome["GeneSex"] = random.NextSingle() < 0.5f ? new Allele(0f, 0f) : new Allele(0f, 1f); + return genome; + } + // Переводит базовый геном вида (числа из animals.json) в набор генов: каждое число — центр // аллелей соответствующего общего GeneDef. Зеркало PlantSet.BuildTemplate. private static GenomeTemplate BuildTemplate( @@ -90,6 +115,7 @@ public sealed class AnimalSet new GenomeTemplate.Entry(Gene("GeneBrainSize"), g.BrainSize, 0.03f), Numeric("GeneInsulation", g.Insulation), new GenomeTemplate.Entry(Gene("GeneFurColor"), g.FurColor, 0.06f), + Numeric("GeneMaturityAge", g.MaturityAge), Numeric("GeneLifespan", g.Lifespan), ] ); diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs index 9842294..1118b07 100644 --- a/src/LittleSim/Content/GameDefs.cs +++ b/src/LittleSim/Content/GameDefs.cs @@ -241,6 +241,9 @@ public sealed class AnimalGenomeDef /// Оттенок меха (0..1) — сдвиг тинта спрайта (как leafHue у растений). public float FurColor { get; init; } = 0.5f; + /// Возраст полового созревания (игровых дней) — порог стадии Adult. + public float MaturityAge { get; init; } = 90f; + /// Продолжительность жизни (игровых дней) до смерти от старости. public float Lifespan { get; init; } = 160f; @@ -255,6 +258,12 @@ public sealed class AnimalGenomeDef /// public sealed class AnimalDef : PawnDef { + /// Текстура взрослого самца (фаза A3); пусто — берётся (самка/общая). + public string MaleTexture { get; init; } = ""; + + /// Текстура детёныша/подростка (фаза A3); пусто — берётся . + public string BabyTexture { get; init; } = ""; + /// Базовый геном вида: центры аллелей животных генов. public AnimalGenomeDef Genome { get; init; } = new(); } diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 0811b73..ac61b13 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -220,6 +220,8 @@ public sealed class WorldScene : Scene new AnimalActionSystem(Store, _plants, Context.Clock, SecondsPerDay, _bounds) ); UpdateSystems.Add(new AnimalAppearanceSystem()); + UpdateSystems.Add(new AnimalGrowthSystem(Context.Clock, SecondsPerDay, _animals, CellSize)); + UpdateSystems.Add(new AnimalMortalitySystem(Store)); UpdateSystems.Add( new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen) @@ -482,7 +484,7 @@ public sealed class WorldScene : Scene } var species = _animals.IndexOf(deerDef); - var template = _animals[species].Template; + var maxAge = deerDef.Genome.Lifespan * 0.85f; // разновозрастное стадо, но не у самой смерти var random = new Random(_config.Seed + 0x5EED); var landCells = 0; @@ -520,7 +522,8 @@ public sealed class WorldScene : Scene _animals, species, new Vector2(px, py), - template.Generate(random), + ageDays: random.NextSingle() * maxAge, + _animals.GenerateGenome(species, random), generation: 0, CellSize ); @@ -794,13 +797,14 @@ public sealed class WorldScene : Scene var index = _animals.IndexOf(def); var seed = args.Length > 1 ? int.Parse(args[1]) : Random.Shared.Next(); - var genome = _animals[index].Template.Generate(new Random(seed)); + var genome = _animals.GenerateGenome(index, new Random(seed)); var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, _animals.GeneRegistry)); + var sex = AnimalFactory.IsMaleGenome(genome) ? "male" : "female"; - console.WriteLine($"{def.DefName} (seed {seed}):"); + console.WriteLine($"{def.DefName} ({sex}, seed {seed}):"); console.WriteLine( - $" body {traits.BodySize:0.##} cells, lifespan {traits.Lifespan:0} d, " - + $"brain {traits.BrainSize:0.##}, metabolism {traits.Metabolism:0.##}" + $" body {traits.BodySize:0.##} cells, maturity {traits.MaturityAge:0} d, " + + $"lifespan {traits.Lifespan:0} d, brain {traits.BrainSize:0.##}, metabolism {traits.Metabolism:0.##}" ); console.WriteLine( $" move {traits.MoveSpeed:0.##}, vision {traits.Vision:0.##}, blood {traits.BloodVolume:0.##}, " diff --git a/src/LittleSim/Sim/AnimalFactory.cs b/src/LittleSim/Sim/AnimalFactory.cs index 06b1d3e..f419016 100644 --- a/src/LittleSim/Sim/AnimalFactory.cs +++ b/src/LittleSim/Sim/AnimalFactory.cs @@ -18,12 +18,22 @@ public static class AnimalFactory private static readonly Color FurWarm = new(255, 226, 188); private static readonly Color FurCool = new(206, 222, 235); - /// Создаёт животное вида с заданным геномом и поколением. + /// Стадии роста животного (фаза A3) по индексам, как в . + public const int StageBaby = 0; + public const int StageJuvenile = 1; + public const int StageAdult = 2; + public const int StageSenior = 3; + + // Масштаб спрайта по стадии (множитель размера тела): детёныш мельче, старик чуть усох. + private static readonly float[] StageScales = [0.45f, 0.7f, 1f, 0.92f]; + + /// Создаёт животное вида заданного возраста, генома и поколения. public static Entity Create( EntityStore store, AnimalSet animals, int species, Vector2 position, + float ageDays, Genome genome, int generation, int cellSize @@ -31,24 +41,26 @@ public static class AnimalFactory { var sp = animals[species]; var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, animals.GeneRegistry)); + var isMale = IsMaleGenome(genome); + var stage = StageAt(ageDays, traits.MaturityAge, traits.Lifespan); + var region = RegionFor(sp, stage, isMale); - var sprite = new Sprite(sp.Region, GameLayers.Beings); + var sprite = new Sprite(region, GameLayers.Beings); sprite.CenterOrigin(); sprite.Color = FurTint(traits); - // Размер спрайта задаёт ген размера тела; деф-fallback — на случай отсутствия гена. - var sizeCells = traits.BodySize > 0f ? traits.BodySize : sp.Def.SizeCells; - return store.CreateEntity( - new Transform2D(position, scale: new Vector2(cellSize * sizeCells / sp.Region.Width)), + new Transform2D(position, scale: new Vector2(Scale(sp, traits, stage, region, cellSize))), sprite, new AnimalOrganism { Genome = genome, Traits = traits, Species = species, + IsMale = isMale, Generation = generation, }, + new AnimalGrowth { Stage = stage, AgeDays = ageDays }, // Нужды стартуют полными; «мозг» решает действие сразу (DecideIn=0). new AnimalNeeds { @@ -60,6 +72,48 @@ public static class AnimalFactory ); } + /// Пол из локуса GeneSex: наличие аллеля Y (значение ≈1) → самец; иначе самка (XX). + public static bool IsMaleGenome(Genome genome) => + genome.Has("GeneSex") && (genome["GeneSex"].A > 0.5f || genome["GeneSex"].B > 0.5f); + + /// Стадия по возрасту: Baby < 0.2·созревание, Juvenile < созревание, Adult < 0.8·жизнь, иначе Senior. + public static int StageAt(float ageDays, float maturity, float lifespan) + { + var babyEnd = MathF.Max(1f, maturity * 0.2f); + var juvEnd = MathF.Max(babyEnd + 1f, maturity); + var adultEnd = MathF.Max(juvEnd + 1f, lifespan * 0.8f); + if (ageDays < babyEnd) + { + return StageBaby; + } + + if (ageDays < juvEnd) + { + return StageJuvenile; + } + + return ageDays < adultEnd ? StageAdult : StageSenior; + } + + /// Спрайт по стадии и полу: детёныш/подросток — baby-текстура; взрослые — по полу. + public static Texture2DRegion RegionFor(AnimalSet.Species sp, int stage, bool isMale) => + stage <= StageJuvenile ? sp.Baby + : isMale ? sp.Male + : sp.Female; + + /// Масштаб Transform2D для спрайта стадии (размер тела × множитель стадии / ширина региона). + public static float Scale( + AnimalSet.Species sp, + in AnimalPhenotype traits, + int stage, + Texture2DRegion region, + int cellSize + ) + { + var sizeCells = (traits.BodySize > 0f ? traits.BodySize : sp.Def.SizeCells) * StageScales[stage]; + return cellSize * sizeCells / region.Width; + } + /// /// Цвет животного из генов: лёгкий тёплый/холодный сдвиг меха (ген furHue) поверх текстуры — /// субтильный, чтобы не перекрашивать уже цветной спрайт зверя (как leafHue у растений). diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs index 80b3387..2977571 100644 --- a/src/LittleSim/Sim/AnimalOrganism.cs +++ b/src/LittleSim/Sim/AnimalOrganism.cs @@ -35,6 +35,9 @@ public struct AnimalPhenotype /// Оттенок меха (0..1) — сдвиг тинта спрайта. public float FurHue; + /// Возраст созревания (игровых дней) — порог стадии Adult. + public float MaturityAge; + /// Продолжительность жизни (игровых дней). public float Lifespan; @@ -52,6 +55,7 @@ public struct AnimalPhenotype BrainSize = T("brainSize"), Insulation = T("insulation"), FurHue = T("furHue"), + MaturityAge = T("maturityAge"), Lifespan = T("lifespan"), }; } @@ -74,6 +78,24 @@ public struct AnimalOrganism : IComponent /// Индекс вида в . public int Species; + /// Пол: самец (XY) или самка (XX) — выведен из локуса GeneSex при создании. + public bool IsMale; + /// Номер поколения: max(родителей) + 1; у основателей 0. public int Generation; } + +/// +/// Состояние роста животного (фаза A3): текущая стадия и накопленный возраст в игровых днях. Плоские +/// данные — переход между стадиями (Baby→Juvenile→Adult→Senior), смену спрайта и размера выполняет +/// , читая пороги из генов (возраст созревания, продолжительность жизни). +/// Зеркало . +/// +public struct AnimalGrowth : IComponent +{ + /// Текущая стадия: 0 Baby, 1 Juvenile, 2 Adult, 3 Senior. + public int Stage; + + /// Накопленный возраст в игровых днях. + public float AgeDays; +} diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs index 6bc5a16..6f0ef68 100644 --- a/src/LittleSim/Sim/AnimalSystems.cs +++ b/src/LittleSim/Sim/AnimalSystems.cs @@ -40,6 +40,9 @@ public struct AnimalNeeds : IComponent /// Отдых/бодрость [0,1]. public float Rest; + /// Накоплено игровых дней истощения (голод/жажда на нуле); порог — смерть (A3). + public float Starve; + /// Наименьшая (самая острая) нужда — для выбора и внешнего вида. public readonly float Worst() => MathF.Min(Hunger, MathF.Min(Thirst, Rest)); } @@ -99,6 +102,12 @@ public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay) need.Hunger = Math.Clamp(need.Hunger - HungerPerDay * metabolism * days, 0f, 1f); need.Thirst = Math.Clamp(need.Thirst - ThirstPerDay * metabolism * days, 0f, 1f); need.Rest = Math.Clamp(need.Rest - RestPerDay * metabolism * days, 0f, 1f); + + // Истощение: голод/жажда на нуле копят счётчик смерти (A3), иначе он рассасывается вдвое быстрее. + need.Starve = + need.Hunger <= 0f || need.Thirst <= 0f + ? need.Starve + days + : MathF.Max(0f, need.Starve - days * 2f); } } } @@ -441,3 +450,97 @@ public sealed class AnimalAppearanceSystem : QuerySystem +/// Рост и стадии (фаза A3): копит возраст в игровых днях, переключает стадию (Baby→Juvenile→Adult→ +/// Senior) по порогам из генов (возраст созревания, продолжительность жизни) и при смене стадии меняет +/// спрайт (детёныш/самка/самец) и размер. Зеркало . +/// +public sealed class AnimalGrowthSystem( + GameClock clock, + float secondsPerDay, + AnimalSet animals, + int cellSize +) : QuerySystem +{ + protected override void OnUpdate() + { + var days = clock.DeltaTime / secondsPerDay; + if (days <= 0f) + { + return; + } + + foreach (var (growths, organisms, transforms, sprites, _) in Query.Chunks) + { + var g = growths.Span; + var o = organisms.Span; + var t = transforms.Span; + var s = sprites.Span; + for (var i = 0; i < g.Length; i++) + { + ref var grow = ref g[i]; + grow.AgeDays += days; + ref readonly var org = ref o[i]; + var stage = AnimalFactory.StageAt( + grow.AgeDays, + org.Traits.MaturityAge, + org.Traits.Lifespan + ); + if (stage == grow.Stage) + { + continue; + } + + grow.Stage = stage; + var sp = animals[org.Species]; + var region = AnimalFactory.RegionFor(sp, stage, org.IsMale); + ref var sprite = ref s[i]; + sprite.Region = region; + sprite.Origin = new Vector2(region.Width / 2f, region.Height / 2f); + t[i].Scale = new Vector2(AnimalFactory.Scale(sp, org.Traits, stage, region, cellSize)); + } + } + } +} + +/// +/// Смертность (фаза A3): простая гибель с деспавном — от старости (возраст превысил продолжительность +/// жизни) или от истощения (счётчик голода/жажды превысил порог). Замыкает контур ёмкости среды. +/// На A5 «апгрейдится» в смерть через здоровье и оставит труп вместо деспавна. +/// +public sealed class AnimalMortalitySystem : BaseSystem +{ + private const float LethalStarveDays = 3f; + + private readonly ArchetypeQuery _query; + private readonly List _deaths = []; + + public AnimalMortalitySystem(EntityStore store) + { + _query = store.Query(); + } + + protected override void OnUpdateGroup() + { + _deaths.Clear(); + foreach (var (growths, organisms, needs, entities) in _query.Chunks) + { + var g = growths.Span; + var o = organisms.Span; + var n = needs.Span; + for (var i = 0; i < g.Length; i++) + { + if (g[i].AgeDays > o[i].Traits.Lifespan || n[i].Starve >= LethalStarveDays) + { + _deaths.Add(entities.EntityAt(i)); + } + } + } + + foreach (var entity in _deaths) + { + entity.DeleteEntity(); + } + } +}