Рефактор: нужды и диета — данными, а не кодом
Нужды стали данными: 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:
co-authored by
Claude Opus 4.8
parent
d4a370e02d
commit
f12b0f170c
@@ -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,
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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` (смена
|
||||||
|
|||||||
@@ -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'");
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
+300
-263
@@ -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;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
values[index] = Math.Clamp(values[index] + _needs[index].FeedPerDay * days, 0f, 1f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Двигает позицию к цели на 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,
|
||||||
|
|||||||
Reference in New Issue
Block a user