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:
@@ -285,6 +285,18 @@ public sealed class AnimalDef : PawnDef
|
||||
|
||||
/// <summary>Сила выедания: на сколько игровых дней роста убавляется растение за день кормёжки.</summary>
|
||||
public float ForageBiteDays { get; init; } = 40f;
|
||||
|
||||
/// <summary>Дальность атаки в клетках: на этом расстоянии хищник кусает добычу (контакт).</summary>
|
||||
public float AttackRangeCells { get; init; } = 1.2f;
|
||||
|
||||
/// <summary>Урон части тела добычи за игровой день укусов (0 — вид не атакует, не хищник по бою).</summary>
|
||||
public float AttackDamage { get; init; }
|
||||
|
||||
/// <summary>Прирост тяжести раны-кровотечения у добычи за игровой день укусов.</summary>
|
||||
public float AttackBleed { get; init; }
|
||||
|
||||
/// <summary>Острая кровопотеря добычи за игровой день укусов (доля объёма крови) — основной путь к смерти.</summary>
|
||||
public float AttackBloodLoss { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -397,6 +409,12 @@ public sealed class HediffDef : Def
|
||||
/// <summary>Шанс подхватить фоном за день (0 — не фоновая болезнь).</summary>
|
||||
public float AmbientPerDay { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Кровопотеря за игровой день при тяжести 1 (масштабируется тяжестью): доля объёма крови, которую
|
||||
/// рана теряет в сутки. 0 — хедиф не вызывает кровотечения. Кровь на нуле → смерть (см. система здоровья).
|
||||
/// </summary>
|
||||
public float BloodLossPerDay { get; init; }
|
||||
|
||||
/// <summary>Модификаторы способностей: capacity id → множитель при тяжести 1 (лерп 1→factor по severity).</summary>
|
||||
public Dictionary<string, float> CapMods { get; init; } = new();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using LittleSim.Content;
|
||||
using LittleSim.Net;
|
||||
using LittleSim.Scenes;
|
||||
using LittleSim.Sim;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Host;
|
||||
@@ -20,6 +21,36 @@ if (args.Contains("--check-content"))
|
||||
+ $"{content.Defs.NamesOf("Plant").Count} plants, {traits.Count} traits "
|
||||
+ $"(hardiness={traits.GetValueOrDefault("hardiness"):0.##})"
|
||||
);
|
||||
|
||||
// Диета животных из генов (предатор-кластер): для каждого вида строим центральный геном из его
|
||||
// баз, считаем признаки формулами и выводим рацион (травоядное/хищник/всеядное) — проверка без
|
||||
// GPU, что хищники заданы и роль выводится из генов herbivory/carnivory/omnivory.
|
||||
foreach (var def in content.Defs.All<AnimalDef>())
|
||||
{
|
||||
var entries = new List<GenomeTemplate.Entry>();
|
||||
foreach (var (geneId, baseValue) in def.Genome)
|
||||
{
|
||||
if (registry.TryGetValue(geneId, out var gene))
|
||||
{
|
||||
var spread = gene.Kind == GeneKind.Discrete ? 0f : gene.Spread;
|
||||
entries.Add(new GenomeTemplate.Entry(gene, baseValue, spread));
|
||||
}
|
||||
}
|
||||
|
||||
var genome = new GenomeTemplate(entries).Generate(new Random(7));
|
||||
var t = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, registry));
|
||||
var (eatsPlants, eatsMeat) = AnimalFactory.Diet(t, def);
|
||||
var role =
|
||||
eatsMeat ? (eatsPlants ? "omnivore" : "carnivore")
|
||||
: eatsPlants ? "herbivore"
|
||||
: "none";
|
||||
var hunt = eatsMeat && def.AttackDamage > 0f ? $", attack {def.AttackDamage:0}/d" : "";
|
||||
Console.WriteLine(
|
||||
$" {def.DefName}: {role} (herb {t.Herbivory:0.##}/carn {t.Carnivory:0.##}/omni {t.Omnivory:0.##}),"
|
||||
+ $" spawn {def.SpawnPer1000Cells:0.##}/1000{hunt}"
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
catch (Exception error)
|
||||
|
||||
@@ -259,6 +259,7 @@ public sealed class WorldScene : Scene
|
||||
_config.Seed + 0x5EED
|
||||
)
|
||||
);
|
||||
var bleeding = content.Defs.TryGet<HediffDef>("Bleeding", out var bl) ? bl : null;
|
||||
UpdateSystems.Add(
|
||||
new AnimalActionSystem(
|
||||
Store,
|
||||
@@ -268,7 +269,10 @@ public sealed class WorldScene : Scene
|
||||
thoughts,
|
||||
Context.Clock,
|
||||
SecondsPerDay,
|
||||
_bounds
|
||||
CellSize,
|
||||
_bounds,
|
||||
bleeding,
|
||||
_config.Seed + 0x1B17
|
||||
)
|
||||
);
|
||||
UpdateSystems.Add(new AnimalAppearanceSystem(_needs));
|
||||
@@ -952,6 +956,11 @@ public sealed class WorldScene : Scene
|
||||
"animal <species> [seed] — sample an animal genome and show its gene-driven traits",
|
||||
(c, args) => RunAnimalDemo(c, content, args)
|
||||
);
|
||||
console.Register(
|
||||
"diet",
|
||||
"diet [species] — gene-derived diet (herbivory/carnivory/omnivory), role and hunting stats",
|
||||
(c, args) => RunDiet(c, content, args)
|
||||
);
|
||||
console.Register(
|
||||
"popstats",
|
||||
"popstats [species] — live population trait means and generation span (selection drift)",
|
||||
@@ -1119,6 +1128,50 @@ public sealed class WorldScene : Scene
|
||||
);
|
||||
}
|
||||
|
||||
// Диета из генов (предатор-кластер): по каждому виду печатает herbivory/carnivory/omnivory,
|
||||
// выведенный рацион (ест растения/мясо), роль и боевые параметры — проверка, что хищники заданы.
|
||||
private void RunDiet(DevConsole console, GameContent content, string[] args)
|
||||
{
|
||||
var species = content.Defs.NamesOf("Animal");
|
||||
for (var s = 0; s < _animals.Count; s++)
|
||||
{
|
||||
var sp = _animals[s];
|
||||
if (
|
||||
args.Length > 0
|
||||
&& !string.Equals(sp.Def.DefName, args[0], StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var genome = _animals.GenerateGenome(s, new Random(0x01E7 + s));
|
||||
var traits = AnimalPhenotype.FromTraits(
|
||||
Phenotype.Compute(genome, _animals.GeneRegistry)
|
||||
);
|
||||
var (eatsPlants, eatsMeat) = AnimalFactory.Diet(traits, sp.Def);
|
||||
var role =
|
||||
eatsMeat ? (eatsPlants ? "omnivore" : "carnivore")
|
||||
: eatsPlants ? "herbivore"
|
||||
: "none";
|
||||
console.WriteLine(
|
||||
$"{sp.Def.DefName}: {role} — herb {traits.Herbivory:0.##} / carn {traits.Carnivory:0.##} / "
|
||||
+ $"omni {traits.Omnivory:0.##} -> plants={eatsPlants} meat={eatsMeat}, spawn {sp.Def.SpawnPer1000Cells:0.##}/1000"
|
||||
);
|
||||
if (eatsMeat && sp.Def.AttackDamage > 0f)
|
||||
{
|
||||
console.WriteLine(
|
||||
$" hunt: dmg {sp.Def.AttackDamage:0}/d, bleed {sp.Def.AttackBleed:0.#}/d, "
|
||||
+ $"bloodloss {sp.Def.AttackBloodLoss:0.#}/d, range {sp.Def.AttackRangeCells:0.#} cells"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
console.WriteLine($"species: {string.Join(", ", species)}");
|
||||
}
|
||||
}
|
||||
|
||||
// Наблюдаемость отбора (фаза A2): средние ключевых признаков живой популяции и размах поколений —
|
||||
// видно дрейф генов под отбором. Без аргумента — все виды; с аргументом — один вид.
|
||||
private void RunPopStats(DevConsole console, string[] args)
|
||||
|
||||
@@ -110,6 +110,54 @@ public static class AnimalFactory
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Порог выраженности признака диеты, с которого вид реально ест данный корм.</summary>
|
||||
public const float DietThreshold = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Рацион особи из ГЕНОВ: ест растения, если max(herbivory, omnivory) ≥ порога; ест мясо
|
||||
/// (охота/падаль), если max(carnivory, omnivory) ≥ порога — всеядность (omnivory) включает оба
|
||||
/// источника, специалисты задаются высоким herbivory ЛИБО carnivory. Если у вида нет генов диеты
|
||||
/// (все три ≈ 0) — откат на список <see cref="AnimalDef.Diet"/> (легаси/модерский fallback).
|
||||
/// </summary>
|
||||
public static (bool EatsPlants, bool EatsMeat) Diet(in AnimalPhenotype traits, AnimalDef def)
|
||||
{
|
||||
if (traits.Herbivory <= 0f && traits.Carnivory <= 0f && traits.Omnivory <= 0f)
|
||||
{
|
||||
return (DietContains(def, "plant"), DietContains(def, "meat"));
|
||||
}
|
||||
|
||||
var eatsPlants = MathF.Max(traits.Herbivory, traits.Omnivory) >= DietThreshold;
|
||||
var eatsMeat = MathF.Max(traits.Carnivory, traits.Omnivory) >= DietThreshold;
|
||||
return (eatsPlants, eatsMeat);
|
||||
}
|
||||
|
||||
private static bool DietContains(AnimalDef def, string food)
|
||||
{
|
||||
foreach (var f in def.Diet)
|
||||
{
|
||||
if (string.Equals(f, food, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Гибель особи: оставляет труп (мясо ∝ размер тела) и удаляет сущность. Общий путь смерти.</summary>
|
||||
public static void Die(EntityStore store, AnimalSet animals, Entity entity, int cellSize)
|
||||
{
|
||||
if (entity.IsNull || !entity.HasComponent<AnimalOrganism>())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ref readonly var org = ref entity.GetComponent<AnimalOrganism>();
|
||||
var position = entity.GetComponent<Transform2D>().Position;
|
||||
CreateCorpse(store, animals, org.Species, position, org.Traits.BodySize, cellSize);
|
||||
entity.DeleteEntity();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Эффективный интеллект особи (фаза A7): размер мозга (ген) × сознание (capacity из здоровья).
|
||||
/// Повреждение мозга/болезнь/боль/кровопотеря снижают Consciousness → динамически роняют интеллект,
|
||||
|
||||
@@ -39,6 +39,15 @@ public struct AnimalPhenotype
|
||||
/// <summary>Возраст созревания (игровых дней) — порог стадии Adult.</summary>
|
||||
public float MaturityAge;
|
||||
|
||||
/// <summary>Травоядность [0..1] — способность/тяга питаться растениями.</summary>
|
||||
public float Herbivory;
|
||||
|
||||
/// <summary>Хищничество [0..1] — тяга охотиться и питаться мясом (живая добыча/падаль).</summary>
|
||||
public float Carnivory;
|
||||
|
||||
/// <summary>Всеядность [0..1] — генералист: включает оба источника пищи (растения и мясо).</summary>
|
||||
public float Omnivory;
|
||||
|
||||
/// <summary>Продолжительность жизни (игровых дней).</summary>
|
||||
public float Lifespan;
|
||||
|
||||
@@ -66,6 +75,9 @@ public struct AnimalPhenotype
|
||||
Insulation = T("insulation"),
|
||||
FurHue = T("furHue"),
|
||||
MaturityAge = T("maturityAge"),
|
||||
Herbivory = T("herbivory"),
|
||||
Carnivory = T("carnivory"),
|
||||
Omnivory = T("omnivory"),
|
||||
Lifespan = T("lifespan"),
|
||||
BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)),
|
||||
GestationDays = T("gestationDays"),
|
||||
@@ -316,6 +328,89 @@ public sealed class HealthState
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Травма (укус хищника): повреждает часть тела (выбор по весу попадания <c>coverage</c>), вызывает
|
||||
/// мгновенную кровопотерю и навешивает/усиливает кровотечение, затем пересчитывает способности.
|
||||
/// Первый травматический источник урона — оживляет каркас здоровья A5.
|
||||
/// </summary>
|
||||
public void ApplyInjury(
|
||||
float partDamage,
|
||||
float bloodLoss,
|
||||
HediffDef? bleeding,
|
||||
float bleedSeverity,
|
||||
Random rng
|
||||
)
|
||||
{
|
||||
if (Parts.Length > 0 && partDamage > 0f)
|
||||
{
|
||||
var idx = PickPart(rng);
|
||||
var part = Parts[idx];
|
||||
part.Hp = MathF.Max(0f, part.Hp - partDamage);
|
||||
Parts[idx] = part;
|
||||
}
|
||||
|
||||
BloodLevel = Math.Clamp(BloodLevel - bloodLoss, 0f, 1f);
|
||||
if (bleeding is not null && bleedSeverity > 0f)
|
||||
{
|
||||
Intensify(bleeding, bleedSeverity);
|
||||
}
|
||||
|
||||
RecomputeCapacities();
|
||||
}
|
||||
|
||||
// Выбор задетой части по весу попадания (coverage); детерминируется переданным rng.
|
||||
private int PickPart(Random rng)
|
||||
{
|
||||
var total = 0f;
|
||||
foreach (var p in Parts)
|
||||
{
|
||||
total += p.Def?.Coverage ?? 0f;
|
||||
}
|
||||
|
||||
if (total <= 0f)
|
||||
{
|
||||
return rng.Next(Parts.Length);
|
||||
}
|
||||
|
||||
var roll = (float)rng.NextDouble() * total;
|
||||
for (var i = 0; i < Parts.Length; i++)
|
||||
{
|
||||
if (Parts[i].Def is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
roll -= Parts[i].Def.Coverage;
|
||||
if (roll <= 0f)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return Parts.Length - 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Навешивает хедиф или усиливает существующий: тяжесть растёт (до 1), а заживление (иммунитет)
|
||||
/// сбрасывается — свежая рана начинает затягиваться заново. Для повторных укусов/ранений.
|
||||
/// </summary>
|
||||
public void Intensify(HediffDef def, float addSeverity)
|
||||
{
|
||||
for (var i = 0; i < _hediffs.Count; i++)
|
||||
{
|
||||
if (_hediffs[i].Def.DefName == def.DefName)
|
||||
{
|
||||
var h = _hediffs[i];
|
||||
h.Severity = Math.Clamp(h.Severity + addSeverity, 0f, 1f);
|
||||
h.Immunity = 0f; // свежая рана — заживление с нуля
|
||||
_hediffs[i] = h;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_hediffs.Add(new Hediff { Def = def, Severity = Math.Clamp(addSeverity, 0f, 1f) });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Здоровье особи: managed-состояние со списком хедифов (см. <see cref="HealthState"/>).</summary>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user