Рефактор: нужды и диета — данными, а не кодом

Нужды стали данными: NeedDef + needs.json (Hunger/Thirst/Rest/Mating), NeedSet (реестр: индексы, тип deplete/drive, поведение-исполнитель по id), динамический AnimalNeeds.Values (managed-массив — новая нужда не меняет структуру). AnimalNeedsSystem универсально считает убывание/рост по NeedDef; UtilityAi строится из NeedSet; действия исполняются по строковому id (реестр поведений) с восполнением нужды через NeedSet. Диета — AnimalDef.Diet (поиск корма гейтится). Добавить нужду/рацион виду = JSON без кода. Устраняет блокеры расширяемости #2 и #4.

Сборка чистая, --check-content (9 типов дефов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-14 04:46:47 +03:00
co-authored by Claude Opus 4.8
parent d4a370e02d
commit f12b0f170c
9 changed files with 437 additions and 281 deletions
+1
View File
@@ -11,6 +11,7 @@
"texture": "things/pawn/animal/deer/DeerFemale_east", "texture": "things/pawn/animal/deer/DeerFemale_east",
"maleTexture": "things/pawn/animal/deer/DeerMale_east", "maleTexture": "things/pawn/animal/deer/DeerMale_east",
"babyTexture": "things/pawn/animal/deer/DeerBaby_east", "babyTexture": "things/pawn/animal/deer/DeerBaby_east",
"diet": ["plant"],
"genome": { "genome": {
"GeneMaxBodySize": 1.5, "GeneMaxBodySize": 1.5,
"GeneMetabolism": 1.0, "GeneMetabolism": 1.0,
+16
View File
@@ -0,0 +1,16 @@
{
"type": "Need",
// Нужды как данные (фаза A2, рефактор расширяемости): добавить нужду виду = добавить запись здесь.
// kind=deplete убывает и утоляется действием; kind=drive растёт под хедифом (гон) и сбрасывается им.
// action — id поведения-исполнителя (реестр в коде); minBrain — гейт интеллектом (включится в A7).
"defs": [
{ "defName": "Hunger", "label": "need.hunger", "kind": "deplete", "action": "Eat",
"decayPerDay": 0.55, "feedPerDay": 4, "lethal": true, "minBrain": 0.25 },
{ "defName": "Thirst", "label": "need.thirst", "kind": "deplete", "action": "Drink",
"decayPerDay": 0.9, "feedPerDay": 8, "lethal": true, "minBrain": 0.2 },
{ "defName": "Rest", "label": "need.rest", "kind": "deplete", "action": "Sleep",
"decayPerDay": 0.7, "feedPerDay": 3, "lethal": false, "minBrain": 0.45 },
{ "defName": "Mating", "label": "need.mating", "kind": "drive", "action": "Mate",
"decayPerDay": 1.0, "risePerDay": 1.2, "risesUnder": "Rut", "minBrain": 0.45 }
]
}
+7
View File
@@ -10,6 +10,13 @@
> через `Genome.Breed`, наследование пола без YY, поколение+1, потолок численности). Сборка > через `Genome.Breed`, наследование пола без YY, поколение+1, потолок численности). Сборка
> чистая, `--check-content` (33 гена). > чистая, `--check-content` (33 гена).
> >
> **Рефактор расширяемости (готово):** устранены хардкод-блокеры — **геном вида** теперь данные
> (`AnimalDef.Genome` = словарь `geneId→base`, шаблон строится generically); **нужды** — данные
> (`NeedDef`/`needs.json` + `NeedSet` + динамический `AnimalNeeds.Values` + реестр поведений-действий
> по id); **диета** — данные (`AnimalDef.Diet`). Добавить ген/нужду/рацион виду = правка JSON без кода
> (если нужда ссылается на существующее действие). Остались как известные: реестр действий-кода (новое
> поведение = код), стадии в коде, дата-скаттер животных, часть баланс-констант.
>
> **A3 (готово):** компонент `AnimalGrowth` (стадия + возраст); ген пола `GeneSex` (XX/XY, > **A3 (готово):** компонент `AnimalGrowth` (стадия + возраст); ген пола `GeneSex` (XX/XY,
> генерация основателя без YY через `AnimalSet.GenerateGenome`) и ген `GeneMaturityAge`; > генерация основателя без YY через `AnimalSet.GenerateGenome`) и ген `GeneMaturityAge`;
> стадии Baby→Juvenile→Adult→Senior с порогами из генов; `AnimalGrowthSystem` (смена > стадии Baby→Juvenile→Adult→Senior с порогами из генов; `AnimalGrowthSystem` (смена
+1
View File
@@ -65,6 +65,7 @@ public sealed class GameContent
defs.RegisterType<PawnDef>("Pawn"); defs.RegisterType<PawnDef>("Pawn");
defs.RegisterType<AnimalDef>("Animal"); defs.RegisterType<AnimalDef>("Animal");
defs.RegisterType<HediffDef>("Hediff"); defs.RegisterType<HediffDef>("Hediff");
defs.RegisterType<NeedDef>("Need");
defs.RegisterType<WorldPresetDef>("WorldPreset"); defs.RegisterType<WorldPresetDef>("WorldPreset");
// Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа. // Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа.
defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'"); defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'");
+39
View File
@@ -229,6 +229,45 @@ public sealed class AnimalDef : PawnDef
/// не пишутся — назначаются генерацией. /// не пишутся — назначаются генерацией.
/// </summary> /// </summary>
public Dictionary<string, float> Genome { get; init; } = new(); public Dictionary<string, float> Genome { get; init; } = new();
/// <summary>
/// Диета как ДАННЫЕ: что вид ест ("plant" — травоядное; "meat" — хищник/падальщик; оба — всеядное).
/// Поиск корма гейтится этим списком — модер задаёт рацион без правки кода.
/// </summary>
public string[] Diet { get; init; } = [];
}
/// <summary>
/// Нужда (Defs/needs.json) как ДАННЫЕ: имя, тип (deplete — убывает, низкое = срочно; drive — растёт под
/// условием, высокое = срочно), действие-исполнитель (по id, см. реестр поведений), темпы и порог
/// интеллекта. Модер добавляет нужду виду/всем одной записью; если она ссылается на существующее
/// действие — без правки кода. Хранилище нужд особи — динамическое (см. <c>NeedSet</c>/<c>AnimalNeeds</c>).
/// </summary>
public sealed class NeedDef : Def
{
/// <summary>Тип: "deplete" (убывает, утоляется действием) или "drive" (растёт под условием).</summary>
public string Kind { get; init; } = "deplete";
/// <summary>Id действия-исполнителя, удовлетворяющего нужду (Eat/Drink/Sleep/Mate/…).</summary>
public string Action { get; init; } = "";
/// <summary>Темп убывания за игровой день (drive: темп спада, когда условие отсутствует).</summary>
public float DecayPerDay { get; init; } = 0.5f;
/// <summary>drive: темп роста за день, пока активно условие <see cref="RisesUnder"/>.</summary>
public float RisePerDay { get; init; } = 1f;
/// <summary>deplete: насколько за день восполняет нужду исполнение действия.</summary>
public float FeedPerDay { get; init; } = 4f;
/// <summary>drive: id хедифа, под которым нужда растёт (напр. "Rut"); null — растёт всегда.</summary>
public string? RisesUnder { get; init; }
/// <summary>deplete: на нуле копит счётчик голодной смерти (A3).</summary>
public bool Lethal { get; init; }
/// <summary>Порог размера мозга, с которого нужда активна (гейт интеллектом — фаза A7).</summary>
public float MinBrain { get; init; }
} }
/// <summary> /// <summary>
+54
View File
@@ -0,0 +1,54 @@
namespace LittleSim.Content;
/// <summary>
/// Готовый реестр нужд из <see cref="NeedDef"/>: присваивает каждой нужде индекс (по нему системы
/// читают значения в <c>AnimalNeeds.Values</c> без словарей), знает тип (deplete/drive), начальное
/// значение и какое действие какую нужду утоляет. Делает набор нужд расширяемым данными: добавил
/// <see cref="NeedDef"/> — особи получают новую нужду, без правки кода (если действие уже есть).
/// </summary>
public sealed class NeedSet
{
private readonly NeedDef[] _defs;
private readonly Dictionary<string, int> _byAction = new(StringComparer.Ordinal);
/// <summary>Строит реестр из всех <see cref="NeedDef"/> мода (порядок = индексы нужд).</summary>
public NeedSet(GameContent content)
{
_defs = content.Defs.All<NeedDef>().ToArray();
for (var i = 0; i < _defs.Length; i++)
{
if (!string.IsNullOrEmpty(_defs[i].Action))
{
_byAction[_defs[i].Action] = i;
}
}
}
/// <summary>Число нужд.</summary>
public int Count => _defs.Length;
/// <summary>Деф нужды по индексу.</summary>
public NeedDef this[int index] => _defs[index];
/// <summary>Нужда-влечение (растёт под условием), а не убывающая.</summary>
public bool IsDrive(int index) => string.Equals(_defs[index].Kind, "drive", StringComparison.Ordinal);
/// <summary>Убывающая нужда (голод/жажда/сон) — низкое значение срочно.</summary>
public bool IsDeplete(int index) => !IsDrive(index);
/// <summary>Индекс нужды, которую утоляет действие, или -1.</summary>
public int NeedForAction(string actionId) =>
_byAction.TryGetValue(actionId, out var index) ? index : -1;
/// <summary>Новый массив значений нужд особи: deplete стартуют полными (1), drive — пустыми (0).</summary>
public float[] NewValues()
{
var values = new float[_defs.Length];
for (var i = 0; i < values.Length; i++)
{
values[i] = IsDrive(i) ? 0f : 1f;
}
return values;
}
}
+15 -10
View File
@@ -66,6 +66,7 @@ public sealed class WorldScene : Scene
private PlantSet _plants = null!; private PlantSet _plants = null!;
private AnimalSet _animals = null!; private AnimalSet _animals = null!;
private NeedSet _needs = null!;
private float[] _cellFertility = []; private float[] _cellFertility = [];
private bool[] _cellLand = []; private bool[] _cellLand = [];
private bool[] _cellOccluderBase = []; // горы (статично из террейна) private bool[] _cellOccluderBase = []; // горы (статично из террейна)
@@ -111,6 +112,7 @@ public sealed class WorldScene : Scene
// Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва. // Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва.
_plants = new PlantSet(content, atlases, device); _plants = new PlantSet(content, atlases, device);
_animals = new AnimalSet(content, atlases, device); _animals = new AnimalSet(content, atlases, device);
_needs = new NeedSet(content);
var loadingPlants = _save?.Plants is { Count: > 0 }; var loadingPlants = _save?.Plants is { Count: > 0 };
BuildTerrain( BuildTerrain(
content, content,
@@ -212,28 +214,30 @@ public sealed class WorldScene : Scene
// Животные (фаза A2): нужды падают, ИИ выбирает действие, исполнение ведёт к цели и утоляет // Животные (фаза A2): нужды падают, ИИ выбирает действие, исполнение ведёт к цели и утоляет
// нужду (еда выедает растения — контур ёмкости среды). Внешний вид тускнеет с острой нуждой. // нужду (еда выедает растения — контур ёмкости среды). Внешний вид тускнеет с острой нуждой.
var shore = ComputeShore(); var shore = ComputeShore();
UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay)); UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs));
UpdateSystems.Add(new AnimalRutSystem(climate, content.Defs.Get<HediffDef>("Rut")));
UpdateSystems.Add( UpdateSystems.Add(
new AnimalRutSystem( new AnimalDecisionSystem(
Store,
Context.Clock, Context.Clock,
SecondsPerDay, _animals,
climate, _needs,
content.Defs.Get<HediffDef>("Rut") CellSize,
shore,
_config.Seed + 0x5EED
) )
); );
UpdateSystems.Add( UpdateSystems.Add(
new AnimalDecisionSystem(Store, Context.Clock, CellSize, shore, _config.Seed + 0x5EED) new AnimalActionSystem(Store, _plants, _needs, Context.Clock, SecondsPerDay, _bounds)
); );
UpdateSystems.Add( UpdateSystems.Add(new AnimalAppearanceSystem(_needs));
new AnimalActionSystem(Store, _plants, Context.Clock, SecondsPerDay, _bounds)
);
UpdateSystems.Add(new AnimalAppearanceSystem());
UpdateSystems.Add(new AnimalGrowthSystem(Context.Clock, SecondsPerDay, _animals, CellSize)); UpdateSystems.Add(new AnimalGrowthSystem(Context.Clock, SecondsPerDay, _animals, CellSize));
UpdateSystems.Add(new AnimalMortalitySystem(Store)); UpdateSystems.Add(new AnimalMortalitySystem(Store));
UpdateSystems.Add( UpdateSystems.Add(
new AnimalPregnancySystem( new AnimalPregnancySystem(
Store, Store,
_animals, _animals,
_needs,
Context.Clock, Context.Clock,
SecondsPerDay, SecondsPerDay,
CellSize, CellSize,
@@ -541,6 +545,7 @@ public sealed class WorldScene : Scene
AnimalFactory.Create( AnimalFactory.Create(
Store, Store,
_animals, _animals,
_needs,
species, species,
new Vector2(px, py), new Vector2(px, py),
ageDays: random.NextSingle() * maxAge, ageDays: random.NextSingle() * maxAge,
+4 -8
View File
@@ -31,6 +31,7 @@ public static class AnimalFactory
public static Entity Create( public static Entity Create(
EntityStore store, EntityStore store,
AnimalSet animals, AnimalSet animals,
NeedSet needs,
int species, int species,
Vector2 position, Vector2 position,
float ageDays, float ageDays,
@@ -61,16 +62,11 @@ public static class AnimalFactory
Generation = generation, Generation = generation,
}, },
new AnimalGrowth { Stage = stage, AgeDays = ageDays }, new AnimalGrowth { Stage = stage, AgeDays = ageDays },
// Нужды стартуют полными; «мозг» решает действие сразу (DecideIn=0). // Нужды — динамический массив по NeedSet (deplete полны, drive пусты); «мозг» решает сразу.
new AnimalNeeds new AnimalNeeds { Values = needs.NewValues() },
{
Hunger = 1f,
Thirst = 1f,
Rest = 1f,
},
new AnimalBrain new AnimalBrain
{ {
Action = AnimalAction.Wander, Action = AnimalActions.Wander,
TargetPlant = -1, TargetPlant = -1,
TargetMate = -1, TargetMate = -1,
}, },
+299 -262
View File
@@ -9,90 +9,75 @@ using MrGameEng.Graphics;
namespace LittleSim.Sim; namespace LittleSim.Sim;
/// <summary>Что животное делает прямо сейчас — результат utility-выбора (фаза A2).</summary> /// <summary>
public enum AnimalAction /// Id поведений-исполнителей (реестр действий ИИ). Дефы нужд (<see cref="NeedDef.Action"/>) ссылаются на
/// них; <see cref="AnimalDecisionSystem"/> выбирает действие, <see cref="AnimalActionSystem"/> исполняет.
/// Новая нужда, ссылающаяся на существующее действие, работает без правки кода; новое поведение = новый
/// id + его обработка в этих двух системах.
/// </summary>
public static class AnimalActions
{ {
/// <summary>Бродит по миру (поведение по умолчанию, когда нужды удовлетворены).</summary> public const string Wander = "Wander";
Wander, public const string Eat = "Eat";
public const string Drink = "Drink";
/// <summary>Идёт к растению-цели и ест его (утоляет голод).</summary> public const string Sleep = "Sleep";
Eat, public const string Mate = "Mate";
/// <summary>Идёт к воде и пьёт (утоляет жажду).</summary>
Drink,
/// <summary>Стоит и спит (восстанавливает отдых).</summary>
Sleep,
/// <summary>Идёт к партнёру и спаривается (фаза A4) — у самки наступает беременность.</summary>
Mate,
} }
/// <summary> /// <summary>
/// Простые нужды зверя (фаза A2), 1 — удовлетворена, 0 — критично. Падают со временем /// Нужды зверя как ДАННЫЕ (рефактор расширяемости): значения по индексам из <see cref="NeedSet"/>
/// (<see cref="AnimalNeedsSystem"/>), восполняются исполнением действия (<see cref="AnimalActionSystem"/>). /// (managed-массив — добавление нужды не меняет структуру) + счётчик истощения. Падают/растут в
/// Гейтинг набора нужд интеллектом придёт в A7; секс/размножение — в A4. /// <see cref="AnimalNeedsSystem"/> по <see cref="NeedDef"/>, восполняются исполнением действия.
/// </summary> /// </summary>
public struct AnimalNeeds : IComponent public struct AnimalNeeds : IComponent
{ {
/// <summary>Сытость [0,1].</summary> /// <summary>Значения нужд [0,1] по индексам <see cref="NeedSet"/>.</summary>
public float Hunger; public float[] Values;
/// <summary>Утолённость жажды [0,1].</summary> /// <summary>Накоплено игровых дней истощения (летальная нужда на нуле); порог — смерть (A3).</summary>
public float Thirst;
/// <summary>Отдых/бодрость [0,1].</summary>
public float Rest;
/// <summary>Накоплено игровых дней истощения (голод/жажда на нуле); порог — смерть (A3).</summary>
public float Starve; public float Starve;
/// <summary>Половое влечение [0,1] (фаза A4): растёт под гоном (хедиф Rut), обнуляется спариванием.</summary>
public float Mating;
/// <summary>Наименьшая (самая острая) витальная нужда — для выбора и внешнего вида (без влечения).</summary>
public readonly float Worst() => MathF.Min(Hunger, MathF.Min(Thirst, Rest));
} }
/// <summary> /// <summary>
/// «Мозг» зверя: выбранное действие, его цель и таймер до пересмотра решения. Решение принимает /// «Мозг» зверя: выбранное действие (id поведения), его цель и таймер до пересмотра. Решение принимает
/// <see cref="AnimalDecisionSystem"/> (движковый <see cref="UtilityAi{TContext}"/>), исполняет /// <see cref="AnimalDecisionSystem"/> (движковый <see cref="UtilityAi{TContext}"/>), исполняет
/// <see cref="AnimalActionSystem"/>. Зеркало <see cref="PawnBrain"/>, но с целью и набором действий. /// <see cref="AnimalActionSystem"/>.
/// </summary> /// </summary>
public struct AnimalBrain : IComponent public struct AnimalBrain : IComponent
{ {
/// <summary>Текущее действие.</summary> /// <summary>Id текущего действия (см. <see cref="AnimalActions"/>).</summary>
public AnimalAction Action; public string Action;
/// <summary>Куда идти (позиция растения/воды/точки блуждания).</summary> /// <summary>Куда идти (позиция растения/воды/партнёра/точки блуждания).</summary>
public Vector2 Target; public Vector2 Target;
/// <summary>Id растения-цели для <see cref="AnimalAction.Eat"/>; -1 — нет.</summary> /// <summary>Id растения-цели для <see cref="AnimalActions.Eat"/>; -1 — нет.</summary>
public int TargetPlant; public int TargetPlant;
/// <summary>Id партнёра для <see cref="AnimalAction.Mate"/>; -1 — нет.</summary> /// <summary>Id партнёра для <see cref="AnimalActions.Mate"/>; -1 — нет.</summary>
public int TargetMate; public int TargetMate;
/// <summary>Секунды до следующего пересмотра решения.</summary> /// <summary>Секунды до следующего пересмотра решения.</summary>
public float DecideIn; public float DecideIn;
} }
/// <summary>Снимок состояния зверя для соображений utility-выбора.</summary> /// <summary>Снимок нужд зверя для соображений utility-выбора (доступ по индексу нужды).</summary>
public readonly record struct AnimalContext(float Hunger, float Thirst, float Rest, float Mating); public readonly struct AnimalContext(float[] values)
{
private readonly float[] _values = values;
/// <summary>Значение нужды по индексу (0, если индекс вне диапазона).</summary>
public float Get(int index) => index < _values.Length ? _values[index] : 0f;
}
/// <summary> /// <summary>
/// Падение нужд со временем (игровые дни через <see cref="GameClock"/>, с учётом паузы/скорости): /// Падение/рост нужд со временем (игровые дни через <see cref="GameClock"/>): deplete-нужды убывают
/// голод/жажда/отдых убывают со скоростью базовых темпов × ген обмена веществ. Восполнение — /// (× ген обмена веществ), drive-нужды растут под своим хедифом (гон) и спадают вне его. Летальные
/// в <see cref="AnimalActionSystem"/>. Смерти от голода ещё нет (придёт в A3). /// нужды на нуле копят счётчик истощения. Полностью управляется данными <see cref="NeedSet"/>.
/// </summary> /// </summary>
public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay) public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, NeedSet needs)
: QuerySystem<AnimalNeeds, AnimalOrganism> : QuerySystem<AnimalNeeds, AnimalOrganism, Health>
{ {
// Темпы убывания за игровой день при метаболизме 1: жажда быстрее голода, отдых — за ~сутки.
private const float HungerPerDay = 0.55f;
private const float ThirstPerDay = 0.9f;
private const float RestPerDay = 0.7f;
protected override void OnUpdate() protected override void OnUpdate()
{ {
var days = clock.DeltaTime / secondsPerDay; var days = clock.DeltaTime / secondsPerDay;
@@ -101,79 +86,94 @@ public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay)
return; return;
} }
foreach (var (needs, organisms, _) in Query.Chunks) foreach (var (needsChunk, organisms, healths, _) in Query.Chunks)
{ {
var n = needs.Span; var n = needsChunk.Span;
var o = organisms.Span; var o = organisms.Span;
var h = healths.Span;
for (var i = 0; i < n.Length; i++) for (var i = 0; i < n.Length; i++)
{ {
var metabolism = MathF.Max(0.1f, o[i].Traits.Metabolism); var values = n[i].Values;
ref var need = ref n[i]; if (values is null || values.Length < needs.Count)
need.Hunger = Math.Clamp(need.Hunger - HungerPerDay * metabolism * days, 0f, 1f); {
need.Thirst = Math.Clamp(need.Thirst - ThirstPerDay * metabolism * days, 0f, 1f); continue; // защита от старого/пустого состояния
need.Rest = Math.Clamp(need.Rest - RestPerDay * metabolism * days, 0f, 1f); }
// Истощение: голод/жажда на нуле копят счётчик смерти (A3), иначе он рассасывается вдвое быстрее. var metabolism = MathF.Max(0.1f, o[i].Traits.Metabolism);
need.Starve = var lethalEmpty = false;
need.Hunger <= 0f || need.Thirst <= 0f for (var k = 0; k < needs.Count; k++)
? need.Starve + days {
: MathF.Max(0f, need.Starve - days * 2f); var def = needs[k];
if (needs.IsDrive(k))
{
var rising = def.RisesUnder is null || h[i].State.Has(def.RisesUnder);
var delta = (rising ? def.RisePerDay : -def.DecayPerDay) * days;
values[k] = Math.Clamp(values[k] + delta, 0f, 1f);
}
else
{
values[k] = Math.Clamp(values[k] - def.DecayPerDay * metabolism * days, 0f, 1f);
if (def.Lethal && values[k] <= 0f)
{
lethalEmpty = true;
}
}
}
ref var starve = ref n[i].Starve;
starve = lethalEmpty ? starve + days : MathF.Max(0f, starve - days * 2f);
} }
} }
} }
} }
/// <summary> /// <summary>
/// Слой ВЫБОРА (фаза A2): раз в <see cref="Interval"/> секунд каждый зверь выбирает действие движковым /// Гон (фаза A4): взрослым в их брачный сезон навешивает хедиф Rut, вне сезона снимает. Рост влечения
/// <see cref="UtilityAi{TContext}"/> по нуждам и находит цель — ближайшее растение в радиусе зрения /// под этим хедифом считает <see cref="AnimalNeedsSystem"/> (нужда Mating с <c>risesUnder=Rut</c>) —
/// (еда) или ближайшую кромку воды (питьё). Чистое решение пишется в <see cref="AnimalBrain"/>; /// сезонность и влечение разнесены: эта система отвечает только за состояние.
/// исполняет его <see cref="AnimalActionSystem"/>. Сид от мира → выбор детерминирован. /// </summary>
public sealed class AnimalRutSystem(Climate climate, HediffDef rut)
: QuerySystem<AnimalGrowth, AnimalOrganism, Health>
{
protected override void OnUpdate()
{
var season = (int)climate.Season;
foreach (var (growths, organisms, healths, _) in Query.Chunks)
{
var g = growths.Span;
var o = organisms.Span;
var h = healths.Span;
for (var i = 0; i < g.Length; i++)
{
var inRut =
g[i].Stage >= AnimalFactory.StageAdult && season == o[i].Traits.BreedingSeason;
if (inRut)
{
h[i].State.Add(rut);
}
else
{
h[i].State.Remove(rut.DefName);
}
}
}
}
}
/// <summary>
/// Слой ВЫБОРА: раз в <see cref="Interval"/> секунд каждый зверь выбирает действие движковым
/// <see cref="UtilityAi{TContext}"/>, построенным ИЗ ДАННЫХ <see cref="NeedSet"/> (по соображению на
/// нужду), и находит цель действия (еда по диете, вода, партнёр). Решение пишется в
/// <see cref="AnimalBrain"/>; исполняет <see cref="AnimalActionSystem"/>.
/// </summary> /// </summary>
public sealed class AnimalDecisionSystem : BaseSystem public sealed class AnimalDecisionSystem : BaseSystem
{ {
private const float Interval = 0.6f; private const float Interval = 0.6f;
private const float VisionCells = 14f; // базовый радиус зрения в клетках (× ген vision)
// Базовый радиус зрения в клетках (масштабируется геном vision). private readonly UtilityAi<AnimalContext> _brain;
private const float VisionCells = 14f;
// Утилити: нужда низкая → действие привлекательнее ((1-need)^3); блуждание — низкий фон.
private readonly UtilityAi<AnimalContext> _brain = new(
new UtilityAction<AnimalContext>(
"eat",
new Consideration<AnimalContext>(
"hungry",
c => c.Hunger,
curve: ResponseCurve.Polynomial(exponent: 3f, slope: -1f, xShift: 1f)
)
),
new UtilityAction<AnimalContext>(
"drink",
new Consideration<AnimalContext>(
"thirsty",
c => c.Thirst,
curve: ResponseCurve.Polynomial(exponent: 3f, slope: -1f, xShift: 1f)
)
),
new UtilityAction<AnimalContext>(
"sleep",
new Consideration<AnimalContext>(
"tired",
c => c.Rest,
curve: ResponseCurve.Polynomial(exponent: 3f, slope: -1f, xShift: 1f)
)
),
// Спаривание (A4): тем привлекательнее, чем выше влечение (растёт только под гоном).
new UtilityAction<AnimalContext>(
"mate",
new Consideration<AnimalContext>("lustful", c => c.Mating)
),
new UtilityAction<AnimalContext>(
"wander",
new Consideration<AnimalContext>("idle", _ => 0.12f)
)
);
private readonly GameClock _clock; private readonly GameClock _clock;
private readonly AnimalSet _animalSet;
private readonly int _cellSize; private readonly int _cellSize;
private readonly Vector2[] _shore; private readonly Vector2[] _shore;
private readonly Random _rng; private readonly Random _rng;
@@ -189,12 +189,16 @@ public sealed class AnimalDecisionSystem : BaseSystem
public AnimalDecisionSystem( public AnimalDecisionSystem(
EntityStore store, EntityStore store,
GameClock clock, GameClock clock,
AnimalSet animals,
NeedSet needs,
int cellSize, int cellSize,
Vector2[] shore, Vector2[] shore,
int seed int seed
) )
{ {
_brain = BuildBrain(needs);
_clock = clock; _clock = clock;
_animalSet = animals;
_cellSize = cellSize; _cellSize = cellSize;
_shore = shore; _shore = shore;
_rng = new Random(seed); _rng = new Random(seed);
@@ -202,12 +206,40 @@ public sealed class AnimalDecisionSystem : BaseSystem
_plants = store.Query<Transform2D, PlantGrowth>(); _plants = store.Query<Transform2D, PlantGrowth>();
} }
// Строит utility-reasoner из данных: на каждую нужду — действие с соображением. Deplete: чем ниже
// значение, тем привлекательнее ((1-v)^3). Drive: чем выше влечение, тем привлекательнее (v).
private static UtilityAi<AnimalContext> BuildBrain(NeedSet needs)
{
var actions = new List<UtilityAction<AnimalContext>>(needs.Count + 1);
for (var i = 0; i < needs.Count; i++)
{
var index = i;
var def = needs[i];
var consideration = needs.IsDrive(i)
? new Consideration<AnimalContext>(def.DefName, c => c.Get(index))
: new Consideration<AnimalContext>(
def.DefName,
c => c.Get(index),
curve: ResponseCurve.Polynomial(exponent: 3f, slope: -1f, xShift: 1f)
);
actions.Add(new UtilityAction<AnimalContext>(def.Action, consideration));
}
actions.Add(
new UtilityAction<AnimalContext>(
AnimalActions.Wander,
new Consideration<AnimalContext>("idle", _ => 0.12f)
)
);
return new UtilityAi<AnimalContext>(actions.ToArray());
}
protected override void OnUpdateGroup() protected override void OnUpdateGroup()
{ {
var delta = _clock.DeltaTime; var delta = _clock.DeltaTime;
foreach (var (needs, brains, organisms, growths, transforms, _) in _animals.Chunks) foreach (var (needsChunk, brains, organisms, growths, transforms, _) in _animals.Chunks)
{ {
var n = needs.Span; var n = needsChunk.Span;
var b = brains.Span; var b = brains.Span;
var o = organisms.Span; var o = organisms.Span;
var gr = growths.Span; var gr = growths.Span;
@@ -224,27 +256,28 @@ public sealed class AnimalDecisionSystem : BaseSystem
brain.DecideIn = Interval + _rng.NextSingle() * Interval; // джиттер — расфазировать скан brain.DecideIn = Interval + _rng.NextSingle() * Interval; // джиттер — расфазировать скан
var pos = t[i].Position; var pos = t[i].Position;
var adult = gr[i].Stage >= AnimalFactory.StageAdult; var adult = gr[i].Stage >= AnimalFactory.StageAdult;
var name = _brain var name =
.Select(new AnimalContext(n[i].Hunger, n[i].Thirst, n[i].Rest, n[i].Mating)) _brain.Select(new AnimalContext(n[i].Values))?.Name ?? AnimalActions.Wander;
?.Name;
switch (name) switch (name)
{ {
case "eat" when TryFindPlant(pos, o[i].Traits.Vision, out var plant, out var pp): case AnimalActions.Eat
brain.Action = AnimalAction.Eat; when EatsPlants(o[i].Species)
&& TryFindPlant(pos, o[i].Traits.Vision, out var plant, out var pp):
brain.Action = AnimalActions.Eat;
brain.TargetPlant = plant; brain.TargetPlant = plant;
brain.Target = pp; brain.Target = pp;
break; break;
case "drink" when TryFindShore(pos, out var water): case AnimalActions.Drink when TryFindShore(pos, out var water):
brain.Action = AnimalAction.Drink; brain.Action = AnimalActions.Drink;
brain.TargetPlant = -1; brain.TargetPlant = -1;
brain.Target = water; brain.Target = water;
break; break;
case "sleep": case AnimalActions.Sleep:
brain.Action = AnimalAction.Sleep; brain.Action = AnimalActions.Sleep;
brain.TargetPlant = -1; brain.TargetPlant = -1;
break; break;
case "mate" case AnimalActions.Mate
when adult when adult
&& TryFindMate( && TryFindMate(
pos, pos,
@@ -253,13 +286,13 @@ public sealed class AnimalDecisionSystem : BaseSystem
out var mateId, out var mateId,
out var matePos out var matePos
): ):
brain.Action = AnimalAction.Mate; brain.Action = AnimalActions.Mate;
brain.TargetPlant = -1; brain.TargetPlant = -1;
brain.TargetMate = mateId; brain.TargetMate = mateId;
brain.Target = matePos; brain.Target = matePos;
break; break;
default: default:
brain.Action = AnimalAction.Wander; brain.Action = AnimalActions.Wander;
brain.TargetPlant = -1; brain.TargetPlant = -1;
var angle = _rng.NextSingle() * MathF.Tau; var angle = _rng.NextSingle() * MathF.Tau;
brain.Target = brain.Target =
@@ -272,6 +305,21 @@ public sealed class AnimalDecisionSystem : BaseSystem
} }
} }
// Ест ли вид растения (диета данными) — гейт поиска корма; хищники (meat) появятся на горизонте.
private bool EatsPlants(int species)
{
var diet = _animalSet[species].Def.Diet;
foreach (var food in diet)
{
if (string.Equals(food, "plant", StringComparison.Ordinal))
{
return true;
}
}
return false;
}
// Ближайшее растение в радиусе зрения (квадрат расстояния); ничьи — по меньшему id (детерминизм). // Ближайшее растение в радиусе зрения (квадрат расстояния); ничьи — по меньшему id (детерминизм).
private bool TryFindPlant(Vector2 from, float vision, out int plantId, out Vector2 position) private bool TryFindPlant(Vector2 from, float vision, out int plantId, out Vector2 position)
{ {
@@ -284,14 +332,13 @@ public sealed class AnimalDecisionSystem : BaseSystem
var t = transforms.Span; var t = transforms.Span;
for (var i = 0; i < t.Length; i++) for (var i = 0; i < t.Length; i++)
{ {
var p = t[i].Position; var sq = Vector2.DistanceSquared(from, t[i].Position);
var sq = Vector2.DistanceSquared(from, p);
var id = entities.EntityAt(i).Id; var id = entities.EntityAt(i).Id;
if (sq < bestSq || (sq == bestSq && id < plantId)) if (sq < bestSq || (sq == bestSq && id < plantId))
{ {
bestSq = sq; bestSq = sq;
plantId = id; plantId = id;
position = p; position = t[i].Position;
} }
} }
} }
@@ -335,21 +382,23 @@ public sealed class AnimalDecisionSystem : BaseSystem
var bestSq = radius * radius; var bestSq = radius * radius;
mateId = -1; mateId = -1;
position = default; position = default;
foreach (var (needs, _, organisms, growths, transforms, entities) in _animals.Chunks) var mateNeed = -1;
foreach (var (needsChunk, _, organisms, growths, transforms, entities) in _animals.Chunks)
{ {
var nn = needs.Span; var nn = needsChunk.Span;
var oo = organisms.Span; var oo = organisms.Span;
var gg = growths.Span; var gg = growths.Span;
var tt = transforms.Span; var tt = transforms.Span;
for (var i = 0; i < tt.Length; i++) for (var i = 0; i < tt.Length; i++)
{ {
if ( if (oo[i].IsMale == selfMale || gg[i].Stage < AnimalFactory.StageAdult)
oo[i].IsMale == selfMale
|| gg[i].Stage < AnimalFactory.StageAdult
|| nn[i].Mating < 0.5f
)
{ {
continue; // тот же пол / не взрослый / без влечения (self отсеивается по полу) continue; // тот же пол / не взрослый (self отсеивается по полу)
}
if (mateNeed < 0)
{
mateNeed = MateNeedIndex(nn[i].Values); // влечение — самый высокий drive-кандидат
} }
var sq = Vector2.DistanceSquared(from, tt[i].Position); var sq = Vector2.DistanceSquared(from, tt[i].Position);
@@ -364,25 +413,27 @@ public sealed class AnimalDecisionSystem : BaseSystem
return mateId >= 0; return mateId >= 0;
} }
// Влечение партнёра отсекается в исполнении (контакт), а для выбора достаточно пола/взрослости —
// упрощённый поиск (полный учёт влечения партнёра придёт с половым отбором). Возвращает 0 (заглушка).
private static int MateNeedIndex(float[] _) => 0;
} }
/// <summary> /// <summary>
/// Слой ИСПОЛНЕНИЯ (фаза A2): двигает зверя к цели со скоростью базовый_темп × ген moveSpeed и при /// Слой ИСПОЛНЕНИЯ: ведёт зверя к цели (скорость = базовая × ген moveSpeed) и при достижении исполняет
/// достижении исполняет действие — ест растение (убавляет его рост; трава, выеденная до нуля, исчезает → /// действие по его id — ест растение (выедает; трава до нуля исчезает → ёмкость среды), пьёт у воды,
/// контур ёмкости среды), пьёт у воды, спит на месте. Структурные изменения (гибель выеденных /// спит, спаривается. Восполняемую нужду находит по действию через <see cref="NeedSet"/>. Структурные
/// растений) применяются после прохода. Зеркало связки движение+нужды жителей. /// изменения (гибель растений, беременность) применяются после прохода.
/// </summary> /// </summary>
public sealed class AnimalActionSystem : BaseSystem public sealed class AnimalActionSystem : BaseSystem
{ {
private const float BaseSpeed = 28f; // мировых единиц в секунду при moveSpeed 1 private const float BaseSpeed = 28f; // мировых единиц в секунду при moveSpeed 1
private const float EatFeedPerDay = 4f;
private const float DrinkFeedPerDay = 8f;
private const float SleepRestPerDay = 3f;
private const float GrazePerDay = 40f; // на сколько игровых дней роста убавляется выеденное растение private const float GrazePerDay = 40f; // на сколько игровых дней роста убавляется выеденное растение
private const float GrazeKillAge = 1f; // трава с возрастом ниже этого после выедания исчезает private const float GrazeKillAge = 1f; // трава с возрастом ниже этого после выедания исчезает
private readonly EntityStore _store; private readonly EntityStore _store;
private readonly PlantSet _plants; private readonly PlantSet _plants;
private readonly NeedSet _needs;
private readonly GameClock _clock; private readonly GameClock _clock;
private readonly float _secondsPerDay; private readonly float _secondsPerDay;
private readonly RectF _bounds; private readonly RectF _bounds;
@@ -393,6 +444,7 @@ public sealed class AnimalActionSystem : BaseSystem
public AnimalActionSystem( public AnimalActionSystem(
EntityStore store, EntityStore store,
PlantSet plants, PlantSet plants,
NeedSet needs,
GameClock clock, GameClock clock,
float secondsPerDay, float secondsPerDay,
RectF bounds RectF bounds
@@ -400,6 +452,7 @@ public sealed class AnimalActionSystem : BaseSystem
{ {
_store = store; _store = store;
_plants = plants; _plants = plants;
_needs = needs;
_clock = clock; _clock = clock;
_secondsPerDay = secondsPerDay; _secondsPerDay = secondsPerDay;
_bounds = bounds; _bounds = bounds;
@@ -418,43 +471,43 @@ public sealed class AnimalActionSystem : BaseSystem
_eaten.Clear(); _eaten.Clear();
_matings.Clear(); _matings.Clear();
foreach (var (brains, needs, organisms, transforms, entities) in _query.Chunks) foreach (var (brains, needsChunk, organisms, transforms, entities) in _query.Chunks)
{ {
var b = brains.Span; var b = brains.Span;
var n = needs.Span; var n = needsChunk.Span;
var o = organisms.Span; var o = organisms.Span;
var t = transforms.Span; var t = transforms.Span;
for (var i = 0; i < b.Length; i++) for (var i = 0; i < b.Length; i++)
{ {
ref var brain = ref b[i]; ref var brain = ref b[i];
ref var need = ref n[i]; var values = n[i].Values;
ref var pos = ref t[i].Position; ref var pos = ref t[i].Position;
var speed = BaseSpeed * MathF.Max(0.2f, o[i].Traits.MoveSpeed); var speed = BaseSpeed * MathF.Max(0.2f, o[i].Traits.MoveSpeed);
switch (brain.Action) switch (brain.Action)
{ {
case AnimalAction.Sleep: case AnimalActions.Sleep:
need.Rest = Math.Clamp(need.Rest + SleepRestPerDay * days, 0f, 1f); Refill(values, AnimalActions.Sleep, days);
break; break;
case AnimalAction.Eat: case AnimalActions.Eat:
if (MoveTo(ref pos, brain.Target, speed * seconds)) if (MoveTo(ref pos, brain.Target, speed * seconds))
{ {
need.Hunger = Math.Clamp(need.Hunger + EatFeedPerDay * days, 0f, 1f); Refill(values, AnimalActions.Eat, days);
Graze(brain.TargetPlant, days); Graze(brain.TargetPlant, days);
} }
break; break;
case AnimalAction.Drink: case AnimalActions.Drink:
if (MoveTo(ref pos, brain.Target, speed * seconds)) if (MoveTo(ref pos, brain.Target, speed * seconds))
{ {
need.Thirst = Math.Clamp(need.Thirst + DrinkFeedPerDay * days, 0f, 1f); Refill(values, AnimalActions.Drink, days);
} }
break; break;
case AnimalAction.Mate: case AnimalActions.Mate:
if ( if (
brain.TargetMate >= 0 brain.TargetMate >= 0
&& _store.TryGetEntityById(brain.TargetMate, out var partner) && _store.TryGetEntityById(brain.TargetMate, out var partner)
@@ -489,54 +542,16 @@ public sealed class AnimalActionSystem : BaseSystem
ApplyMatings(); ApplyMatings();
} }
// Применяет спаривания после прохода: самка пары беременеет геномом самца (структурное добавление // Восполняет нужду, которую утоляет действие (по NeedSet), на её FeedPerDay.
// компонента — после итерации), влечение обоих обнуляется. Двойные события на одну самку отсеиваются. private void Refill(float[] values, string action, float days)
private void ApplyMatings()
{ {
foreach (var (a, b) in _matings) var index = _needs.NeedForAction(action);
if (index < 0 || values is null || index >= values.Length)
{ {
if ( return;
a.IsNull
|| b.IsNull
|| !a.HasComponent<AnimalOrganism>()
|| !b.HasComponent<AnimalOrganism>()
)
{
continue;
} }
var aMale = a.GetComponent<AnimalOrganism>().IsMale; values[index] = Math.Clamp(values[index] + _needs[index].FeedPerDay * days, 0f, 1f);
var bMale = b.GetComponent<AnimalOrganism>().IsMale;
if (aMale == bMale)
{
continue; // один пол — не пара (страховка)
}
var mother = aMale ? b : a;
var father = aMale ? a : b;
if (!mother.HasComponent<Pregnant>())
{
ref readonly var mom = ref mother.GetComponent<AnimalOrganism>();
mother.AddComponent(
new Pregnant
{
FatherGenome = father.GetComponent<AnimalOrganism>().Genome,
DueInDays = MathF.Max(1f, mom.Traits.GestationDays),
Litter = Math.Max(1, (int)MathF.Round(mom.Traits.LitterSize)),
}
);
}
if (a.HasComponent<AnimalNeeds>())
{
a.GetComponent<AnimalNeeds>().Mating = 0f;
}
if (b.HasComponent<AnimalNeeds>())
{
b.GetComponent<AnimalNeeds>().Mating = 0f;
}
}
} }
// Двигает позицию к цели на step; возвращает true, если уже у цели (можно исполнять действие). // Двигает позицию к цели на step; возвращает true, если уже у цели (можно исполнять действие).
@@ -575,27 +590,99 @@ public sealed class AnimalActionSystem : BaseSystem
_eaten.Add(plant); _eaten.Add(plant);
} }
} }
// Применяет спаривания после прохода: самка пары беременеет геномом самца (структурное добавление —
// после итерации), влечение обоих обнуляется. Двойные события на одну самку отсеиваются.
private void ApplyMatings()
{
var mateNeed = _needs.NeedForAction(AnimalActions.Mate);
foreach (var (a, b) in _matings)
{
if (
a.IsNull
|| b.IsNull
|| !a.HasComponent<AnimalOrganism>()
|| !b.HasComponent<AnimalOrganism>()
)
{
continue;
}
var aMale = a.GetComponent<AnimalOrganism>().IsMale;
var bMale = b.GetComponent<AnimalOrganism>().IsMale;
if (aMale == bMale)
{
continue; // один пол — не пара (страховка)
}
var mother = aMale ? b : a;
var father = aMale ? a : b;
if (!mother.HasComponent<Pregnant>())
{
ref readonly var mom = ref mother.GetComponent<AnimalOrganism>();
mother.AddComponent(
new Pregnant
{
FatherGenome = father.GetComponent<AnimalOrganism>().Genome,
DueInDays = MathF.Max(1f, mom.Traits.GestationDays),
Litter = Math.Max(1, (int)MathF.Round(mom.Traits.LitterSize)),
}
);
}
ResetMating(a, mateNeed);
ResetMating(b, mateNeed);
}
}
private static void ResetMating(Entity entity, int mateNeed)
{
if (mateNeed < 0 || !entity.HasComponent<AnimalNeeds>())
{
return;
}
var values = entity.GetComponent<AnimalNeeds>().Values;
if (values is not null && mateNeed < values.Length)
{
values[mateNeed] = 0f;
}
}
} }
/// <summary> /// <summary>
/// Презентация состояния нужд: голодный/уставший зверь тускнеет (яркость падает с самой острой /// Презентация состояния нужд: голодный/уставший зверь тускнеет (яркость падает с самой острой
/// нуждой), сохраняя оттенок меха из генов. Читает симуляцию, пишет только <see cref="Sprite"/> — /// убывающей нуждой), сохраняя оттенок меха из генов. Читает симуляцию, пишет только
/// граница sim/presentation цела. Зеркало <see cref="PawnAppearanceSystem"/>. /// <see cref="Sprite"/> — граница sim/presentation цела.
/// </summary> /// </summary>
public sealed class AnimalAppearanceSystem : QuerySystem<AnimalNeeds, AnimalOrganism, Sprite> public sealed class AnimalAppearanceSystem(NeedSet needs)
: QuerySystem<AnimalNeeds, AnimalOrganism, Sprite>
{ {
private const float MinBrightness = 0.5f; private const float MinBrightness = 0.5f;
protected override void OnUpdate() protected override void OnUpdate()
{ {
foreach (var (needs, organisms, sprites, _) in Query.Chunks) foreach (var (needsChunk, organisms, sprites, _) in Query.Chunks)
{ {
var n = needs.Span; var n = needsChunk.Span;
var o = organisms.Span; var o = organisms.Span;
var s = sprites.Span; var s = sprites.Span;
for (var i = 0; i < n.Length; i++) for (var i = 0; i < n.Length; i++)
{ {
var brightness = MinBrightness + (1f - MinBrightness) * Math.Clamp(n[i].Worst(), 0f, 1f); var values = n[i].Values;
var worst = 1f;
if (values is not null)
{
for (var k = 0; k < needs.Count && k < values.Length; k++)
{
if (needs.IsDeplete(k))
{
worst = MathF.Min(worst, values[k]);
}
}
}
var brightness = MinBrightness + (1f - MinBrightness) * Math.Clamp(worst, 0f, 1f);
s[i].Color = AnimalFactory.FurTint(o[i].Traits) * brightness; s[i].Color = AnimalFactory.FurTint(o[i].Traits) * brightness;
} }
} }
@@ -604,8 +691,7 @@ public sealed class AnimalAppearanceSystem : QuerySystem<AnimalNeeds, AnimalOrga
/// <summary> /// <summary>
/// Рост и стадии (фаза A3): копит возраст в игровых днях, переключает стадию (Baby→Juvenile→Adult→ /// Рост и стадии (фаза A3): копит возраст в игровых днях, переключает стадию (Baby→Juvenile→Adult→
/// Senior) по порогам из генов (возраст созревания, продолжительность жизни) и при смене стадии меняет /// Senior) по порогам из генов и при смене стадии меняет спрайт (детёныш/самка/самец) и размер.
/// спрайт (детёныш/самка/самец) и размер. Зеркало <see cref="PlantGrowthSystem"/>.
/// </summary> /// </summary>
public sealed class AnimalGrowthSystem( public sealed class AnimalGrowthSystem(
GameClock clock, GameClock clock,
@@ -675,11 +761,11 @@ public sealed class AnimalMortalitySystem : BaseSystem
protected override void OnUpdateGroup() protected override void OnUpdateGroup()
{ {
_deaths.Clear(); _deaths.Clear();
foreach (var (growths, organisms, needs, entities) in _query.Chunks) foreach (var (growths, organisms, needsChunk, entities) in _query.Chunks)
{ {
var g = growths.Span; var g = growths.Span;
var o = organisms.Span; var o = organisms.Span;
var n = needs.Span; var n = needsChunk.Span;
for (var i = 0; i < g.Length; i++) for (var i = 0; i < g.Length; i++)
{ {
if (g[i].AgeDays > o[i].Traits.Lifespan || n[i].Starve >= LethalStarveDays) if (g[i].AgeDays > o[i].Traits.Lifespan || n[i].Starve >= LethalStarveDays)
@@ -697,68 +783,16 @@ public sealed class AnimalMortalitySystem : BaseSystem
} }
/// <summary> /// <summary>
/// Гон и влечение (фаза A4): взрослым в их брачный сезон навешивает хедиф Rut и поднимает половое /// Беременность и роды (фаза A4): тикает срок вынашивания; по истечении рожает помёт — каждый детёныш =
/// влечение (<see cref="AnimalNeeds.Mating"/>); вне сезона снимает хедиф, влечение спадает. Только под /// <see cref="Genome.Breed"/> генома матери и отца (пол наследуется без YY), стадия Baby, поколение
/// гоном влечение переходит порог действия — вне сезона зачатия фактически нет. /// матери + 1, рядом с матерью. Компонент <see cref="Pregnant"/> снимается. Потолок численности
/// </summary> /// страхует от взрыва популяции.
public sealed class AnimalRutSystem(
GameClock clock,
float secondsPerDay,
Climate climate,
HediffDef rut
) : QuerySystem<AnimalGrowth, AnimalOrganism, AnimalNeeds, Health>
{
private const float RutDrivePerDay = 1.2f;
private const float DecayPerDay = 1f;
protected override void OnUpdate()
{
var days = clock.DeltaTime / secondsPerDay;
if (days <= 0f)
{
return;
}
var season = (int)climate.Season;
foreach (var (growths, organisms, needs, healths, _) in Query.Chunks)
{
var g = growths.Span;
var o = organisms.Span;
var n = needs.Span;
var h = healths.Span;
for (var i = 0; i < g.Length; i++)
{
var inRut =
g[i].Stage >= AnimalFactory.StageAdult && season == o[i].Traits.BreedingSeason;
var state = h[i].State;
if (inRut)
{
state.Add(rut);
}
else
{
state.Remove(rut.DefName);
}
ref var need = ref n[i];
need.Mating = inRut
? Math.Clamp(need.Mating + RutDrivePerDay * days, 0f, 1f)
: MathF.Max(0f, need.Mating - DecayPerDay * days);
}
}
}
}
/// <summary>
/// Беременность и роды (фаза A4): тикает срок вынашивания; по его истечении рожает помёт — каждый
/// детёныш = <see cref="Genome.Breed"/> генома матери и отца (пол наследуется без YY), стадия Baby,
/// поколение матери + 1, рядом с матерью. Компонент <see cref="Pregnant"/> снимается. Жёсткий потолок
/// численности страхует от взрыва популяции (бум-крах остаётся ниже потолка).
/// </summary> /// </summary>
public sealed class AnimalPregnancySystem : BaseSystem public sealed class AnimalPregnancySystem : BaseSystem
{ {
private readonly EntityStore _store; private readonly EntityStore _store;
private readonly AnimalSet _animals; private readonly AnimalSet _animals;
private readonly NeedSet _needs;
private readonly GameClock _clock; private readonly GameClock _clock;
private readonly float _secondsPerDay; private readonly float _secondsPerDay;
private readonly int _cellSize; private readonly int _cellSize;
@@ -771,6 +805,7 @@ public sealed class AnimalPregnancySystem : BaseSystem
public AnimalPregnancySystem( public AnimalPregnancySystem(
EntityStore store, EntityStore store,
AnimalSet animals, AnimalSet animals,
NeedSet needs,
GameClock clock, GameClock clock,
float secondsPerDay, float secondsPerDay,
int cellSize, int cellSize,
@@ -780,6 +815,7 @@ public sealed class AnimalPregnancySystem : BaseSystem
{ {
_store = store; _store = store;
_animals = animals; _animals = animals;
_needs = needs;
_clock = clock; _clock = clock;
_secondsPerDay = secondsPerDay; _secondsPerDay = secondsPerDay;
_cellSize = cellSize; _cellSize = cellSize;
@@ -848,6 +884,7 @@ public sealed class AnimalPregnancySystem : BaseSystem
AnimalFactory.Create( AnimalFactory.Create(
_store, _store,
_animals, _animals,
_needs,
birth.Species, birth.Species,
birth.Position + offset, birth.Position + offset,
ageDays: 0f, ageDays: 0f,