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