Животные A3: жизненный цикл, стадии и пол

Компонент AnimalGrowth (стадия + возраст). Ген пола GeneSex (локус XX/XY; генерация основателя задаёт пол явно через AnimalSet.GenerateGenome, исключая невозможный YY) и GeneMaturityAge. Стадии Baby->Juvenile->Adult->Senior с порогами из генов. AnimalGrowthSystem меняет спрайт (детёныш/самец/самка) и размер по стадии; AnimalMortalitySystem — смерть от старости и истощения (деспавн), замыкает контур ёмкости среды. Стадо спавнится разновозрастным.

Сборка чистая, --check-content (30 генов, 29 признаков).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-14 04:20:44 +03:00
co-authored by Claude Opus 4.8
parent e35e620655
commit 355ced4937
9 changed files with 255 additions and 17 deletions
+3
View File
@@ -9,6 +9,8 @@
"label": "pawn.deer", "label": "pawn.deer",
"kind": "animal", "kind": "animal",
"texture": "things/pawn/animal/deer/DeerFemale_east", "texture": "things/pawn/animal/deer/DeerFemale_east",
"maleTexture": "things/pawn/animal/deer/DeerMale_east",
"babyTexture": "things/pawn/animal/deer/DeerBaby_east",
"genome": { "genome": {
"bodySize": 1.5, "bodySize": 1.5,
"metabolism": 1.0, "metabolism": 1.0,
@@ -18,6 +20,7 @@
"brainSize": 0.45, "brainSize": 0.45,
"insulation": 0.55, "insulation": 0.55,
"furColor": 0.45, "furColor": 0.45,
"maturityAge": 90,
"lifespan": 360, "lifespan": 360,
"spread": 0.08 "spread": 0.08
} }
+9
View File
@@ -99,6 +99,15 @@
{ "defName": "GeneFurColor", "parent": "BaseNumericGene", "label": "gene.furColor", { "defName": "GeneFurColor", "parent": "BaseNumericGene", "label": "gene.furColor",
"default": 0.5, "min": 0.0, "max": 1.0, "spread": 0.06, "tags": ["animal", "morphology", "color"], "default": 0.5, "min": 0.0, "max": 1.0, "spread": 0.06, "tags": ["animal", "morphology", "color"],
"effects": { "furHue": "value" } }, "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 (рецессивный) виден только в гомозиготе. // Дискретный ген морфы: вариант 0 доминирует, 1 (рецессивный) виден только в гомозиготе.
{ "defName": "GeneMorph", "kind": "Discrete", "label": "gene.morph", { "defName": "GeneMorph", "kind": "Discrete", "label": "gene.morph",
+9 -1
View File
@@ -1,8 +1,16 @@
# Животные (зоология) # Животные (зоология)
> Статус: **A1A2 реализованы**, остальное по плану. Это живой план — правится по мере > Статус: **A1A3 реализованы**, остальное по плану. Это живой план — правится по мере
> реализации фаз A1–A8. Стиль и правила — как в [концепте](concept.md) и [модах](mods.md). > реализации фаз 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` (олень); > **A1 (готово):** тип дефа `Animal` (`AnimalDef : PawnDef`) + `animals.json` (олень);
> 8 животных генов в `genes.json`; `AnimalPhenotype`/`AnimalOrganism`; `AnimalSet` > 8 животных генов в `genes.json`; `AnimalPhenotype`/`AnimalOrganism`; `AnimalSet`
> (шаблон генома вида) + `AnimalFactory`; детерминированный спавн стада оленей по суше в > (шаблон генома вида) + `AnimalFactory`; детерминированный спавн стада оленей по суше в
+30 -4
View File
@@ -12,14 +12,20 @@ namespace LittleSim.Content;
/// </summary> /// </summary>
public sealed class AnimalSet public sealed class AnimalSet
{ {
/// <summary>Вид животного: его деф, спрайт и набор генов для генерации особи.</summary> /// <summary>Вид животного: его деф, спрайты по полу/стадии и набор генов для генерации особи.</summary>
public sealed class Species public sealed class Species
{ {
/// <summary>Исходный деф животного.</summary> /// <summary>Исходный деф животного.</summary>
public required AnimalDef Def { get; init; } public required AnimalDef Def { get; init; }
/// <summary>Спрайт вида (фаза A1 — единый; стадии/пол позже).</summary> /// <summary>Спрайт взрослой самки (он же общий/fallback).</summary>
public required Texture2DRegion Region { get; init; } public required Texture2DRegion Female { get; init; }
/// <summary>Спрайт взрослого самца (fallback — самка).</summary>
public required Texture2DRegion Male { get; init; }
/// <summary>Спрайт детёныша/подростка (fallback — самка).</summary>
public required Texture2DRegion Baby { get; init; }
/// <summary>Набор генов вида: базовые значения для генерации особи.</summary> /// <summary>Набор генов вида: базовые значения для генерации особи.</summary>
public required GenomeTemplate Template { get; init; } public required GenomeTemplate Template { get; init; }
@@ -44,10 +50,17 @@ public sealed class AnimalSet
for (var i = 0; i < defs.Length; i++) for (var i = 0; i < defs.Length; i++)
{ {
var def = defs[i]; var def = defs[i];
var female = atlases.GetRegion(device, def.Texture);
_species[i] = new Species _species[i] = new Species
{ {
Def = def, 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), Template = BuildTemplate(def.Genome, genes),
}; };
_index[def] = i; _index[def] = i;
@@ -63,6 +76,18 @@ public sealed class AnimalSet
/// <summary>Индекс вида по дефу.</summary> /// <summary>Индекс вида по дефу.</summary>
public int IndexOf(AnimalDef def) => _index[def]; public int IndexOf(AnimalDef def) => _index[def];
/// <summary>
/// Генерирует геном новой особи-основателя вида: аллели из шаблона + явно назначенный пол
/// (XX самка / XY самец, 50/50) — чтобы не возник невозможный YY (см. genes.json GeneSex).
/// Скрещивание (A4) наследует пол само (мать XX отдаёт только X).
/// </summary>
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) в набор генов: каждое число — центр // Переводит базовый геном вида (числа из animals.json) в набор генов: каждое число — центр
// аллелей соответствующего общего GeneDef. Зеркало PlantSet.BuildTemplate. // аллелей соответствующего общего GeneDef. Зеркало PlantSet.BuildTemplate.
private static GenomeTemplate BuildTemplate( private static GenomeTemplate BuildTemplate(
@@ -90,6 +115,7 @@ public sealed class AnimalSet
new GenomeTemplate.Entry(Gene("GeneBrainSize"), g.BrainSize, 0.03f), new GenomeTemplate.Entry(Gene("GeneBrainSize"), g.BrainSize, 0.03f),
Numeric("GeneInsulation", g.Insulation), Numeric("GeneInsulation", g.Insulation),
new GenomeTemplate.Entry(Gene("GeneFurColor"), g.FurColor, 0.06f), new GenomeTemplate.Entry(Gene("GeneFurColor"), g.FurColor, 0.06f),
Numeric("GeneMaturityAge", g.MaturityAge),
Numeric("GeneLifespan", g.Lifespan), Numeric("GeneLifespan", g.Lifespan),
] ]
); );
+9
View File
@@ -241,6 +241,9 @@ public sealed class AnimalGenomeDef
/// <summary>Оттенок меха (0..1) — сдвиг тинта спрайта (как leafHue у растений).</summary> /// <summary>Оттенок меха (0..1) — сдвиг тинта спрайта (как leafHue у растений).</summary>
public float FurColor { get; init; } = 0.5f; public float FurColor { get; init; } = 0.5f;
/// <summary>Возраст полового созревания (игровых дней) — порог стадии Adult.</summary>
public float MaturityAge { get; init; } = 90f;
/// <summary>Продолжительность жизни (игровых дней) до смерти от старости.</summary> /// <summary>Продолжительность жизни (игровых дней) до смерти от старости.</summary>
public float Lifespan { get; init; } = 160f; public float Lifespan { get; init; } = 160f;
@@ -255,6 +258,12 @@ public sealed class AnimalGenomeDef
/// </summary> /// </summary>
public sealed class AnimalDef : PawnDef public sealed class AnimalDef : PawnDef
{ {
/// <summary>Текстура взрослого самца (фаза A3); пусто — берётся <see cref="PawnDef.Texture"/> (самка/общая).</summary>
public string MaleTexture { get; init; } = "";
/// <summary>Текстура детёныша/подростка (фаза A3); пусто — берётся <see cref="PawnDef.Texture"/>.</summary>
public string BabyTexture { get; init; } = "";
/// <summary>Базовый геном вида: центры аллелей животных генов.</summary> /// <summary>Базовый геном вида: центры аллелей животных генов.</summary>
public AnimalGenomeDef Genome { get; init; } = new(); public AnimalGenomeDef Genome { get; init; } = new();
} }
+10 -6
View File
@@ -220,6 +220,8 @@ public sealed class WorldScene : Scene
new AnimalActionSystem(Store, _plants, Context.Clock, SecondsPerDay, _bounds) new AnimalActionSystem(Store, _plants, Context.Clock, SecondsPerDay, _bounds)
); );
UpdateSystems.Add(new AnimalAppearanceSystem()); UpdateSystems.Add(new AnimalAppearanceSystem());
UpdateSystems.Add(new AnimalGrowthSystem(Context.Clock, SecondsPerDay, _animals, CellSize));
UpdateSystems.Add(new AnimalMortalitySystem(Store));
UpdateSystems.Add( UpdateSystems.Add(
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen) new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
@@ -482,7 +484,7 @@ public sealed class WorldScene : Scene
} }
var species = _animals.IndexOf(deerDef); 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 random = new Random(_config.Seed + 0x5EED);
var landCells = 0; var landCells = 0;
@@ -520,7 +522,8 @@ public sealed class WorldScene : Scene
_animals, _animals,
species, species,
new Vector2(px, py), new Vector2(px, py),
template.Generate(random), ageDays: random.NextSingle() * maxAge,
_animals.GenerateGenome(species, random),
generation: 0, generation: 0,
CellSize CellSize
); );
@@ -794,13 +797,14 @@ public sealed class WorldScene : Scene
var index = _animals.IndexOf(def); var index = _animals.IndexOf(def);
var seed = args.Length > 1 ? int.Parse(args[1]) : Random.Shared.Next(); 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 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( console.WriteLine(
$" body {traits.BodySize:0.##} cells, lifespan {traits.Lifespan:0} d, " $" body {traits.BodySize:0.##} cells, maturity {traits.MaturityAge:0} d, "
+ $"brain {traits.BrainSize:0.##}, metabolism {traits.Metabolism:0.##}" + $"lifespan {traits.Lifespan:0} d, brain {traits.BrainSize:0.##}, metabolism {traits.Metabolism:0.##}"
); );
console.WriteLine( console.WriteLine(
$" move {traits.MoveSpeed:0.##}, vision {traits.Vision:0.##}, blood {traits.BloodVolume:0.##}, " $" move {traits.MoveSpeed:0.##}, vision {traits.Vision:0.##}, blood {traits.BloodVolume:0.##}, "
+60 -6
View File
@@ -18,12 +18,22 @@ public static class AnimalFactory
private static readonly Color FurWarm = new(255, 226, 188); private static readonly Color FurWarm = new(255, 226, 188);
private static readonly Color FurCool = new(206, 222, 235); private static readonly Color FurCool = new(206, 222, 235);
/// <summary>Создаёт животное вида <paramref name="species"/> с заданным геномом и поколением.</summary> /// <summary>Стадии роста животного (фаза A3) по индексам, как в <see cref="AnimalGrowth.Stage"/>.</summary>
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];
/// <summary>Создаёт животное вида <paramref name="species"/> заданного возраста, генома и поколения.</summary>
public static Entity Create( public static Entity Create(
EntityStore store, EntityStore store,
AnimalSet animals, AnimalSet animals,
int species, int species,
Vector2 position, Vector2 position,
float ageDays,
Genome genome, Genome genome,
int generation, int generation,
int cellSize int cellSize
@@ -31,24 +41,26 @@ public static class AnimalFactory
{ {
var sp = animals[species]; var sp = animals[species];
var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, animals.GeneRegistry)); 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.CenterOrigin();
sprite.Color = FurTint(traits); sprite.Color = FurTint(traits);
// Размер спрайта задаёт ген размера тела; деф-fallback — на случай отсутствия гена.
var sizeCells = traits.BodySize > 0f ? traits.BodySize : sp.Def.SizeCells;
return store.CreateEntity( 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, sprite,
new AnimalOrganism new AnimalOrganism
{ {
Genome = genome, Genome = genome,
Traits = traits, Traits = traits,
Species = species, Species = species,
IsMale = isMale,
Generation = generation, Generation = generation,
}, },
new AnimalGrowth { Stage = stage, AgeDays = ageDays },
// Нужды стартуют полными; «мозг» решает действие сразу (DecideIn=0). // Нужды стартуют полными; «мозг» решает действие сразу (DecideIn=0).
new AnimalNeeds new AnimalNeeds
{ {
@@ -60,6 +72,48 @@ public static class AnimalFactory
); );
} }
/// <summary>Пол из локуса GeneSex: наличие аллеля Y (значение ≈1) → самец; иначе самка (XX).</summary>
public static bool IsMaleGenome(Genome genome) =>
genome.Has("GeneSex") && (genome["GeneSex"].A > 0.5f || genome["GeneSex"].B > 0.5f);
/// <summary>Стадия по возрасту: Baby &lt; 0.2·созревание, Juvenile &lt; созревание, Adult &lt; 0.8·жизнь, иначе Senior.</summary>
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;
}
/// <summary>Спрайт по стадии и полу: детёныш/подросток — baby-текстура; взрослые — по полу.</summary>
public static Texture2DRegion RegionFor(AnimalSet.Species sp, int stage, bool isMale) =>
stage <= StageJuvenile ? sp.Baby
: isMale ? sp.Male
: sp.Female;
/// <summary>Масштаб Transform2D для спрайта стадии (размер тела × множитель стадии / ширина региона).</summary>
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;
}
/// <summary> /// <summary>
/// Цвет животного из генов: лёгкий тёплый/холодный сдвиг меха (ген furHue) поверх текстуры — /// Цвет животного из генов: лёгкий тёплый/холодный сдвиг меха (ген furHue) поверх текстуры —
/// субтильный, чтобы не перекрашивать уже цветной спрайт зверя (как leafHue у растений). /// субтильный, чтобы не перекрашивать уже цветной спрайт зверя (как leafHue у растений).
+22
View File
@@ -35,6 +35,9 @@ public struct AnimalPhenotype
/// <summary>Оттенок меха (0..1) — сдвиг тинта спрайта.</summary> /// <summary>Оттенок меха (0..1) — сдвиг тинта спрайта.</summary>
public float FurHue; public float FurHue;
/// <summary>Возраст созревания (игровых дней) — порог стадии Adult.</summary>
public float MaturityAge;
/// <summary>Продолжительность жизни (игровых дней).</summary> /// <summary>Продолжительность жизни (игровых дней).</summary>
public float Lifespan; public float Lifespan;
@@ -52,6 +55,7 @@ public struct AnimalPhenotype
BrainSize = T("brainSize"), BrainSize = T("brainSize"),
Insulation = T("insulation"), Insulation = T("insulation"),
FurHue = T("furHue"), FurHue = T("furHue"),
MaturityAge = T("maturityAge"),
Lifespan = T("lifespan"), Lifespan = T("lifespan"),
}; };
} }
@@ -74,6 +78,24 @@ public struct AnimalOrganism : IComponent
/// <summary>Индекс вида в <see cref="Content.AnimalSet"/>.</summary> /// <summary>Индекс вида в <see cref="Content.AnimalSet"/>.</summary>
public int Species; public int Species;
/// <summary>Пол: самец (XY) или самка (XX) — выведен из локуса GeneSex при создании.</summary>
public bool IsMale;
/// <summary>Номер поколения: max(родителей) + 1; у основателей 0.</summary> /// <summary>Номер поколения: max(родителей) + 1; у основателей 0.</summary>
public int Generation; public int Generation;
} }
/// <summary>
/// Состояние роста животного (фаза A3): текущая стадия и накопленный возраст в игровых днях. Плоские
/// данные — переход между стадиями (Baby→Juvenile→Adult→Senior), смену спрайта и размера выполняет
/// <see cref="AnimalGrowthSystem"/>, читая пороги из генов (возраст созревания, продолжительность жизни).
/// Зеркало <see cref="PlantGrowth"/>.
/// </summary>
public struct AnimalGrowth : IComponent
{
/// <summary>Текущая стадия: 0 Baby, 1 Juvenile, 2 Adult, 3 Senior.</summary>
public int Stage;
/// <summary>Накопленный возраст в игровых днях.</summary>
public float AgeDays;
}
+103
View File
@@ -40,6 +40,9 @@ public struct AnimalNeeds : IComponent
/// <summary>Отдых/бодрость [0,1].</summary> /// <summary>Отдых/бодрость [0,1].</summary>
public float Rest; public float Rest;
/// <summary>Накоплено игровых дней истощения (голод/жажда на нуле); порог — смерть (A3).</summary>
public float Starve;
/// <summary>Наименьшая (самая острая) нужда — для выбора и внешнего вида.</summary> /// <summary>Наименьшая (самая острая) нужда — для выбора и внешнего вида.</summary>
public readonly float Worst() => MathF.Min(Hunger, MathF.Min(Thirst, Rest)); 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.Hunger = Math.Clamp(need.Hunger - HungerPerDay * metabolism * days, 0f, 1f);
need.Thirst = Math.Clamp(need.Thirst - ThirstPerDay * 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); 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<AnimalNeeds, AnimalOrga
} }
} }
} }
/// <summary>
/// Рост и стадии (фаза A3): копит возраст в игровых днях, переключает стадию (Baby→Juvenile→Adult→
/// Senior) по порогам из генов (возраст созревания, продолжительность жизни) и при смене стадии меняет
/// спрайт (детёныш/самка/самец) и размер. Зеркало <see cref="PlantGrowthSystem"/>.
/// </summary>
public sealed class AnimalGrowthSystem(
GameClock clock,
float secondsPerDay,
AnimalSet animals,
int cellSize
) : QuerySystem<AnimalGrowth, AnimalOrganism, Transform2D, Sprite>
{
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));
}
}
}
}
/// <summary>
/// Смертность (фаза A3): простая гибель с деспавном — от старости (возраст превысил продолжительность
/// жизни) или от истощения (счётчик голода/жажды превысил порог). Замыкает контур ёмкости среды.
/// На A5 «апгрейдится» в смерть через здоровье и оставит труп вместо деспавна.
/// </summary>
public sealed class AnimalMortalitySystem : BaseSystem
{
private const float LethalStarveDays = 3f;
private readonly ArchetypeQuery<AnimalGrowth, AnimalOrganism, AnimalNeeds> _query;
private readonly List<Entity> _deaths = [];
public AnimalMortalitySystem(EntityStore store)
{
_query = store.Query<AnimalGrowth, AnimalOrganism, AnimalNeeds>();
}
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();
}
}
}