Implement diet genes for animals, introducing GeneHerbivory, GeneCarnivory, and GeneOmnivory to define dietary behaviors. Add new animal types: Wolf (carnivore) and WildBoar (omnivore), with associated attributes and hunting mechanics. Enhance animal behavior systems to include hunting and fleeing dynamics based on genetic traits. Update documentation to reflect these changes and ensure localization for new terms. Clean build with no content errors.

This commit is contained in:
Leonid Pershin
2026-06-14 13:14:36 +03:00
parent aae76e9083
commit 97d83606ce
12 changed files with 717 additions and 46 deletions
+353 -41
View File
@@ -22,6 +22,12 @@ public static class AnimalActions
public const string Drink = "Drink";
public const string Sleep = "Sleep";
public const string Mate = "Mate";
/// <summary>Охота: преследовать живую добычу и кусать (урон) либо есть труп — утоляет голод хищника.</summary>
public const string Hunt = "Hunt";
/// <summary>Бегство: уходить от ближайшего хищника (реакция жертвы, важнее прочих нужд).</summary>
public const string Flee = "Flee";
}
/// <summary>
@@ -69,6 +75,12 @@ public struct AnimalBrain : IComponent
/// <summary>Id партнёра для <see cref="AnimalActions.Mate"/>; -1 — нет.</summary>
public int TargetMate;
/// <summary>Id цели для <see cref="AnimalActions.Hunt"/> — живой добычи или трупа; -1 — нет.</summary>
public int TargetPrey;
/// <summary>Цель — труп (падаль), а не живая добыча: при достижении едим, а не атакуем.</summary>
public bool TargetIsCorpse;
/// <summary>Секунды до следующего пересмотра решения.</summary>
public float DecideIn;
}
@@ -226,6 +238,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
Transform2D
> _animals;
private readonly ArchetypeQuery<Transform2D, PlantGrowth> _plants;
private readonly ArchetypeQuery<Transform2D, Corpse> _corpses;
public AnimalDecisionSystem(
EntityStore store,
@@ -252,6 +265,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
Transform2D
>();
_plants = store.Query<Transform2D, PlantGrowth>();
_corpses = store.Query<Transform2D, Corpse>();
}
// Строит utility-reasoner из данных: на каждую нужду — действие с соображением. Deplete: чем ниже
@@ -316,15 +330,54 @@ public sealed class AnimalDecisionSystem : BaseSystem
);
// Настроение (A8): стресс/истощение (низкое настроение) подавляет тягу к спариванию.
var mood = self.GetComponent<Mood>();
// Бегство (предатор-кластер): жертва, заметившая рядом хищника, спасается — это важнее
// любых нужд (инстинкт выживания). Сам хищник от добычи не бежит (см. IsThreatTo).
if (TryFindThreat(pos, radius, o[i].Species, o[i].Traits, out var threatPos))
{
brain.Action = AnimalActions.Flee;
brain.TargetPlant = -1;
brain.TargetPrey = -1;
var away = pos - threatPos;
var len = away.Length();
brain.Target =
len > 0.001f
? pos + away / len * (radius + _cellSize)
: pos + new Vector2(_cellSize, 0f);
continue;
}
var (eatsPlants, eatsMeat) = AnimalFactory.Diet(
o[i].Traits,
_animalSet[o[i].Species].Def
);
var name =
_brain.Select(new AnimalContext(n[i].Values, _needs, intelligence))?.Name
?? AnimalActions.Wander;
switch (name)
{
// Хищник/всеядное голодает → ищет труп (падаль) или живую добычу и охотится.
case AnimalActions.Eat
when EatsPlants(o[i].Species)
&& TryFindPlant(pos, radius, out var plant, out var pp):
when eatsMeat
&& TryFindKill(
pos,
radius,
o[i].Species,
o[i].Traits,
self.Id,
out var preyId,
out var preyPos,
out var preyIsCorpse
):
brain.Action = AnimalActions.Hunt;
brain.TargetPlant = -1;
brain.TargetPrey = preyId;
brain.TargetIsCorpse = preyIsCorpse;
brain.Target = preyPos;
break;
case AnimalActions.Eat
when eatsPlants && TryFindPlant(pos, radius, out var plant, out var pp):
brain.Action = AnimalActions.Eat;
brain.TargetPlant = plant;
brain.Target = pp;
@@ -371,21 +424,167 @@ public sealed class AnimalDecisionSystem : BaseSystem
private float VisionRadius(int species, float vision) =>
MathF.Max(1f, _animalSet[species].Def.VisionCells) * MathF.Max(0.2f, vision) * _cellSize;
// Ест ли вид растения (диета данными) — гейт поиска корма; хищники (meat) появятся на горизонте.
private bool EatsPlants(int species)
// Ближайший хищник-угроза в радиусе восприятия (для бегства жертвы). Угроза = другой вид, который
// ест мясо и достаточно крупный (см. IsThreatTo). O(n) на особь — как поиск корма.
private bool TryFindThreat(
Vector2 from,
float radius,
int selfSpecies,
in AnimalPhenotype selfTraits,
out Vector2 threatPos
)
{
var diet = _animalSet[species].Def.Diet;
foreach (var food in diet)
var bestSq = radius * radius;
threatPos = default;
var found = false;
var (_, selfEatsMeat) = AnimalFactory.Diet(selfTraits, _animalSet[selfSpecies].Def);
var selfBody = selfTraits.BodySize;
foreach (var (_, _, organisms, _, transforms, _) in _animals.Chunks)
{
if (string.Equals(food, "plant", StringComparison.Ordinal))
var oo = organisms.Span;
var tt = transforms.Span;
for (var i = 0; i < tt.Length; i++)
{
return true;
if (
oo[i].Species == selfSpecies
|| !IsThreatTo(selfEatsMeat, selfBody, oo[i].Species, oo[i].Traits)
)
{
continue;
}
var sq = Vector2.DistanceSquared(from, tt[i].Position);
if (sq < bestSq)
{
bestSq = sq;
threatPos = tt[i].Position;
found = true;
}
}
}
return found;
}
// Является ли вид-кандидат угрозой особи: он ест мясо и не мельче её. Чистая жертва (не мясоед)
// бежит от любого хищника не мельче себя; сам хищник — лишь от заметно более крупного мясоеда.
private bool IsThreatTo(
bool selfEatsMeat,
float selfBody,
int otherSpecies,
in AnimalPhenotype otherTraits
)
{
var (_, otherEatsMeat) = AnimalFactory.Diet(otherTraits, _animalSet[otherSpecies].Def);
if (!otherEatsMeat)
{
return false;
}
var otherBody = otherTraits.BodySize;
return selfEatsMeat ? otherBody > selfBody * 1.1f : otherBody >= selfBody * 0.9f;
}
// Ближайшая «мясная» цель: труп (падаль, приоритет — даровая еда без риска) или живая добыча.
// Возвращает id, позицию и флаг трупа. Добыча — другой вид не крупнее охотника (см. IsPreyFor).
private bool TryFindKill(
Vector2 from,
float radius,
int selfSpecies,
in AnimalPhenotype selfTraits,
int selfId,
out int targetId,
out Vector2 position,
out bool isCorpse
)
{
targetId = -1;
position = default;
isCorpse = false;
// 1) Падаль с остатком мяса.
var bestCorpseSq = radius * radius;
var corpseId = -1;
var corpsePos = default(Vector2);
foreach (var (transforms, corpses, entities) in _corpses.Chunks)
{
var tt = transforms.Span;
var cc = corpses.Span;
for (var i = 0; i < tt.Length; i++)
{
if (cc[i].Meat <= 0f)
{
continue;
}
var sq = Vector2.DistanceSquared(from, tt[i].Position);
var id = entities.EntityAt(i).Id;
if (sq < bestCorpseSq || (sq == bestCorpseSq && id < corpseId))
{
bestCorpseSq = sq;
corpseId = id;
corpsePos = tt[i].Position;
}
}
}
// 2) Живая добыча.
var bestPreySq = radius * radius;
var preyId = -1;
var preyPos = default(Vector2);
var selfBody = selfTraits.BodySize;
foreach (var (_, _, organisms, _, transforms, entities) in _animals.Chunks)
{
var oo = organisms.Span;
var tt = transforms.Span;
for (var i = 0; i < tt.Length; i++)
{
var id = entities.EntityAt(i).Id;
if (oo[i].Species == selfSpecies || id == selfId)
{
continue;
}
if (!IsPreyFor(selfBody, oo[i].Traits))
{
continue;
}
var sq = Vector2.DistanceSquared(from, tt[i].Position);
if (sq < bestPreySq || (sq == bestPreySq && id < preyId))
{
bestPreySq = sq;
preyId = id;
preyPos = tt[i].Position;
}
}
}
// Предпочитаем падаль, если она не намного дальше живой добычи (даровая еда, без риска).
if (corpseId >= 0 && (preyId < 0 || bestCorpseSq <= bestPreySq * 2f))
{
targetId = corpseId;
position = corpsePos;
isCorpse = true;
return true;
}
if (preyId >= 0)
{
targetId = preyId;
position = preyPos;
isCorpse = false;
return true;
}
return false;
}
// Подходит ли особь в добычу охотнику данного размера: важно лишь, что она не крупнее охотника
// (на превосходящих не нападаем). Вид/мясоедство добычи не важны — хищник ест и травоядных, и прочих.
private static bool IsPreyFor(float hunterBody, in AnimalPhenotype otherTraits) =>
otherTraits.BodySize <= hunterBody * 1.25f;
// Ближайшее растение в радиусе (квадрат расстояния); ничьи — по меньшему id (детерминизм).
private bool TryFindPlant(Vector2 from, float radius, out int plantId, out Vector2 position)
{
@@ -496,6 +695,7 @@ public sealed class AnimalActionSystem : BaseSystem
{
private const float GrazeKillAge = 1f; // трава с возрастом ниже этого после выедания исчезает
private const float ThirstReliefBelow = 0.15f; // ниже этого жажда — питьё даёт мысль «напилась»
private const float FleeSpeedMult = 1.2f; // страх подгоняет жертву чуть быстрее обычного шага
private readonly EntityStore _store;
private readonly PlantSet _plants;
@@ -504,7 +704,10 @@ public sealed class AnimalActionSystem : BaseSystem
private readonly ThoughtSet _thoughts;
private readonly GameClock _clock;
private readonly float _secondsPerDay;
private readonly int _cellSize;
private readonly RectF _bounds;
private readonly HediffDef? _bleeding;
private readonly Random _rng;
private readonly ArchetypeQuery<
AnimalBrain,
AnimalNeeds,
@@ -513,6 +716,8 @@ public sealed class AnimalActionSystem : BaseSystem
Health
> _query;
private readonly List<Entity> _eaten = [];
private readonly List<Entity> _consumed = []; // трупы, доеденные до нуля
private readonly List<Entity> _kills = []; // добыча, забитая в этом проходе (смерть после итерации)
private readonly List<(Entity Self, Entity Partner)> _matings = [];
public AnimalActionSystem(
@@ -523,7 +728,10 @@ public sealed class AnimalActionSystem : BaseSystem
ThoughtSet thoughts,
GameClock clock,
float secondsPerDay,
RectF bounds
int cellSize,
RectF bounds,
HediffDef? bleeding,
int seed
)
{
_store = store;
@@ -533,7 +741,10 @@ public sealed class AnimalActionSystem : BaseSystem
_thoughts = thoughts;
_clock = clock;
_secondsPerDay = secondsPerDay;
_cellSize = cellSize;
_bounds = bounds;
_bleeding = bleeding;
_rng = new Random(seed);
_query = store.Query<AnimalBrain, AnimalNeeds, AnimalOrganism, Transform2D, Health>();
}
@@ -547,6 +758,8 @@ public sealed class AnimalActionSystem : BaseSystem
var days = seconds / _secondsPerDay;
_eaten.Clear();
_consumed.Clear();
_kills.Clear();
_matings.Clear();
foreach (
@@ -622,6 +835,14 @@ public sealed class AnimalActionSystem : BaseSystem
break;
case AnimalActions.Hunt:
Hunt(ref brain, ref pos, values, def, speed * seconds, days);
break;
case AnimalActions.Flee:
MoveTo(ref pos, brain.Target, speed * FleeSpeedMult * seconds);
break;
default: // Wander
MoveTo(ref pos, brain.Target, speed * seconds);
break;
@@ -637,9 +858,109 @@ public sealed class AnimalActionSystem : BaseSystem
plant.DeleteEntity();
}
foreach (var corpse in _consumed)
{
if (!corpse.IsNull)
{
corpse.DeleteEntity(); // труп доеден до нуля
}
}
foreach (var prey in _kills)
{
AnimalFactory.Die(_store, _animalSet, prey, _cellSize); // забитая добыча → труп
}
ApplyMatings();
}
// Охота/падальщество: ведёт к цели и при контакте либо ест труп (утоляет голод, расходует мясо),
// либо кусает живую добычу — урон части тела, острая кровопотеря, рана-кровотечение (первый
// травматический урон). Добитая добыча помечается на смерть (труп оставит общий путь Die).
private void Hunt(
ref AnimalBrain brain,
ref Vector2 pos,
float[] values,
AnimalDef def,
float step,
float days
)
{
if (
brain.TargetPrey < 0
|| !_store.TryGetEntityById(brain.TargetPrey, out var target)
|| target.IsNull
|| !target.HasComponent<Transform2D>()
)
{
return; // цель исчезла — действие пересмотрят на следующем решении
}
var targetPos = target.GetComponent<Transform2D>().Position;
var range = def.AttackRangeCells * _cellSize;
var inRange = Vector2.DistanceSquared(pos, targetPos) <= range * range;
if (brain.TargetIsCorpse)
{
if (!target.HasComponent<Corpse>())
{
return;
}
if (!inRange)
{
MoveTo(ref pos, targetPos, step);
return;
}
ref var corpse = ref target.GetComponent<Corpse>();
Refill(values, AnimalActions.Eat, days);
corpse.Meat -= def.ForageBiteDays * days; // расход мяса той же «силой укуса», что и выедание
if (corpse.Meat <= 0f && !_consumed.Contains(target))
{
_consumed.Add(target);
}
return;
}
// Живая добыча.
if (!target.HasComponent<Health>() || !target.HasComponent<AnimalOrganism>())
{
return;
}
if (!inRange)
{
MoveTo(ref pos, targetPos, step);
return;
}
var preyHealth = target.GetComponent<Health>().State;
if (preyHealth is null)
{
return;
}
preyHealth.ApplyInjury(
def.AttackDamage * days,
def.AttackBloodLoss * days,
_bleeding,
def.AttackBleed * days,
_rng
);
if (
preyHealth.BloodLevel <= 0f
|| preyHealth.Capacity(AnimalCapacities.Consciousness) <= 0.01f
)
{
if (!_kills.Contains(target))
{
_kills.Add(target); // добита — смерть после прохода (структурное изменение)
}
}
}
// Восполняет нужду, которую утоляет действие (по NeedSet), на её FeedPerDay.
private void Refill(float[] values, string action, float days)
{
@@ -985,22 +1306,7 @@ public sealed class AnimalMortalitySystem : BaseSystem
foreach (var dead in _deaths)
{
if (dead.IsNull || !dead.HasComponent<AnimalOrganism>())
{
continue;
}
ref readonly var org = ref dead.GetComponent<AnimalOrganism>();
var position = dead.GetComponent<Transform2D>().Position;
AnimalFactory.CreateCorpse(
_store,
_animals,
org.Species,
position,
org.Traits.BodySize,
_cellSize
);
dead.DeleteEntity();
AnimalFactory.Die(_store, _animals, dead, _cellSize);
}
}
}
@@ -1228,6 +1534,7 @@ public sealed class AnimalPregnancySystem : BaseSystem
public sealed class AnimalHealthSystem : BaseSystem
{
private const float TickDays = 1f / 24f; // ~игровой час
private const float BloodRecoveryPerDay = 0.25f; // восстановление крови в покое (нет кровотечений)
private readonly EntityStore _store;
private readonly AnimalSet _animals;
@@ -1295,6 +1602,26 @@ public sealed class AnimalHealthSystem : BaseSystem
}
var lethal = state.AdvanceHediffs(days);
// Кровопотеря от ран (кровотечения), иначе — постепенное восстановление крови в покое.
var bleed = 0f;
foreach (var hd in state.Hediffs)
{
bleed += hd.Def.BloodLossPerDay * hd.Severity;
}
if (bleed > 0f)
{
state.BloodLevel = Math.Clamp(state.BloodLevel - bleed * days, 0f, 1f);
}
else if (state.BloodLevel < 1f)
{
state.BloodLevel = Math.Clamp(
state.BloodLevel + BloodRecoveryPerDay * days,
0f,
1f
);
}
state.RecomputeCapacities();
if (
lethal
@@ -1309,22 +1636,7 @@ public sealed class AnimalHealthSystem : BaseSystem
foreach (var dead in _deaths)
{
if (dead.IsNull || !dead.HasComponent<AnimalOrganism>())
{
continue;
}
ref readonly var org = ref dead.GetComponent<AnimalOrganism>();
var position = dead.GetComponent<Transform2D>().Position;
AnimalFactory.CreateCorpse(
_store,
_animals,
org.Species,
position,
org.Traits.BodySize,
_cellSize
);
dead.DeleteEntity();
AnimalFactory.Die(_store, _animals, dead, _cellSize);
}
}
}