Plant–herbivore coevolution + RimWorld-style health capacities
Adds the first plant↔animal coupling beyond "herbivore eats plant": a toxin/ thorns ↔ tolerance arms race built on the existing gene + hediff engines, plus the fuller RimWorld capacity set on the health scaffold. Coevolution C1 — plant defense → poisons/injures the eater: - Plant genes GeneToxicity/GeneThorns/GenePalatability (GenomeDef + PlantSet template + PlantPhenotype); animal gene GeneToxinTolerance. - Hediff Poisoned (non-progressing; grows from eating via HealthState.Intensify, immunity heals it, overdose is lethal). - AnimalActionSystem.ApplyPlantDefense on each bite: poison dose = toxicity × (1 − tolerance); thorns → ApplyInjury (reuses predator-cluster). - Cost of defense: PlantGrowthSystem growth ×= DefenseGrowthFactor() (1 − 0.4·toxicity − 0.3·thorns) — without a cost defense would max out and coevolution would stall. - Showcase: cactus thorns, poisonous mushroom; grass starts clean so toxicity can evolve on the staple. Deer/boar get small starting tolerance. Coevolution C2 — discrimination + the other half of the arms race: - Smart foraging (reuses A7 intelligence gating): herbivores with effective intelligence ≥ tier-4 weigh plants by palatability − perceived-toxin − distance; reflex grazers (or sick animals with dropped consciousness) eat the nearest plant and risk poison. - Cost of tolerance: detox raises metabolism (need decay ×= 1 + 0.5·tolerance), closing the arms race so tolerance doesn't fix at 1. - popstats reports mean toxinTolerance and a plants line (mean toxicity/thorns/ palatability) to watch the drift. Health capacities (RimWorld parity, user-requested): - New capacities BloodFiltration/Hearing/Talking/Eating; CapacityCalc rewritten to emit only capacities the body declares (a destroyed organ still shows 0%) with the full dependency chain (blood → pumping/breathing/filtration → consciousness → moving/sight/hearing/talking/eating, digestion). - Quadruped body gains kidneys + liver (filtration), ears (hearing), jaw/tongue (eating/talking). Inspector health tab lists all of a body's capacities. Localization (ru/en) for new genes' defense line, Poisoned, and the new capacity labels. Build + --check-content clean (11 def types, 40 genes, 39 traits). Not GUI-verified (headless can't render the world scene). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
97d83606ce
commit
8807045303
@@ -125,6 +125,9 @@ public readonly struct AnimalContext(float[] values, NeedSet needs, float intell
|
||||
public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, NeedSet needs)
|
||||
: QuerySystem<AnimalNeeds, AnimalOrganism, Health>
|
||||
{
|
||||
// Прибавка к обмену веществ при устойчивости к яду 1.0 (цена детоксикации, коэволюция C2).
|
||||
private const float ToleranceMetabolicCost = 0.5f;
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
var days = clock.DeltaTime / secondsPerDay;
|
||||
@@ -146,7 +149,14 @@ public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, Need
|
||||
continue; // защита от старого/пустого состояния
|
||||
}
|
||||
|
||||
var metabolism = MathF.Max(0.1f, o[i].Traits.Metabolism);
|
||||
// Цена устойчивости к яду (коэволюция C2): детоксикация метаболически затратна — чем выше
|
||||
// toxinTolerance, тем быстрее расходуются нужды (зверь голоднее). Без этой платы устойчивость
|
||||
// ушла бы к максимуму у всех и гонка вооружений встала бы. Множитель к обмену веществ.
|
||||
var metabolism =
|
||||
MathF.Max(0.1f, o[i].Traits.Metabolism)
|
||||
* (
|
||||
1f + ToleranceMetabolicCost * Math.Clamp(o[i].Traits.ToxinTolerance, 0f, 1f)
|
||||
);
|
||||
var lethalEmpty = false;
|
||||
for (var k = 0; k < needs.Count; k++)
|
||||
{
|
||||
@@ -237,7 +247,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
||||
AnimalGrowth,
|
||||
Transform2D
|
||||
> _animals;
|
||||
private readonly ArchetypeQuery<Transform2D, PlantGrowth> _plants;
|
||||
private readonly ArchetypeQuery<Transform2D, PlantGrowth, PlantOrganism> _plants;
|
||||
private readonly ArchetypeQuery<Transform2D, Corpse> _corpses;
|
||||
|
||||
public AnimalDecisionSystem(
|
||||
@@ -264,7 +274,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
||||
AnimalGrowth,
|
||||
Transform2D
|
||||
>();
|
||||
_plants = store.Query<Transform2D, PlantGrowth>();
|
||||
_plants = store.Query<Transform2D, PlantGrowth, PlantOrganism>();
|
||||
_corpses = store.Query<Transform2D, Corpse>();
|
||||
}
|
||||
|
||||
@@ -377,7 +387,15 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
||||
brain.Target = preyPos;
|
||||
break;
|
||||
case AnimalActions.Eat
|
||||
when eatsPlants && TryFindPlant(pos, radius, out var plant, out var pp):
|
||||
when eatsPlants
|
||||
&& TryFindForage(
|
||||
pos,
|
||||
radius,
|
||||
intelligence,
|
||||
o[i].Traits.ToxinTolerance,
|
||||
out var plant,
|
||||
out var pp
|
||||
):
|
||||
brain.Action = AnimalActions.Eat;
|
||||
brain.TargetPlant = plant;
|
||||
brain.Target = pp;
|
||||
@@ -586,12 +604,36 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
||||
otherTraits.BodySize <= hunterBody * 1.25f;
|
||||
|
||||
// Ближайшее растение в радиусе (квадрат расстояния); ничьи — по меньшему id (детерминизм).
|
||||
// Порог интеллекта, с которого травоядное РАЗБИРАЕТ корм (избегает ядовитого/невкусного). Ниже —
|
||||
// рефлекторное выедание ближайшего растения (тир мозга 4, как сон/мысли; больной зверь с упавшим
|
||||
// сознанием падает ниже и снова жрёт что попало — последствие гейтинга A7).
|
||||
private const float ForageSmartMinBrain = 0.4f;
|
||||
|
||||
// Вес, с которым воспринимаемый яд (с поправкой на устойчивость) отталкивает от растения, и штраф за
|
||||
// дальность — чтобы умный зверь предпочитал близкий и безопасный корм, а не бежал через всю карту.
|
||||
private const float ToxinAvoidWeight = 1.2f;
|
||||
private const float ForageDistanceWeight = 0.5f;
|
||||
|
||||
// Выбор корма: умный (избегает яда/невкусного) если интеллект ≥ порога, иначе рефлекс — ближайшее.
|
||||
private bool TryFindForage(
|
||||
Vector2 from,
|
||||
float radius,
|
||||
float intelligence,
|
||||
float toxinTolerance,
|
||||
out int plantId,
|
||||
out Vector2 position
|
||||
) =>
|
||||
intelligence >= ForageSmartMinBrain
|
||||
? TryFindBestPlant(from, radius, toxinTolerance, out plantId, out position)
|
||||
: TryFindPlant(from, radius, out plantId, out position);
|
||||
|
||||
// Рефлекс: ближайшее растение в радиусе (ничья — меньший id, детерминизм).
|
||||
private bool TryFindPlant(Vector2 from, float radius, out int plantId, out Vector2 position)
|
||||
{
|
||||
var bestSq = radius * radius;
|
||||
plantId = -1;
|
||||
position = default;
|
||||
foreach (var (transforms, _, entities) in _plants.Chunks)
|
||||
foreach (var (transforms, _, _, entities) in _plants.Chunks)
|
||||
{
|
||||
var t = transforms.Span;
|
||||
for (var i = 0; i < t.Length; i++)
|
||||
@@ -610,6 +652,54 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
||||
return plantId >= 0;
|
||||
}
|
||||
|
||||
// Умный выбор: среди растений в радиусе максимизируем привлекательность = вкусность −
|
||||
// воспринимаемый_яд − штраф_за_дальность. Воспринимаемый яд = toxicity × (1 − устойчивость): чем
|
||||
// выше устойчивость зверя, тем меньше его отпугивает токсичный корм. Если всё в округе невкусно/
|
||||
// ядовито, голод всё равно заставит выбрать наименее плохой (берётся максимум, даже отрицательный).
|
||||
private bool TryFindBestPlant(
|
||||
Vector2 from,
|
||||
float radius,
|
||||
float toxinTolerance,
|
||||
out int plantId,
|
||||
out Vector2 position
|
||||
)
|
||||
{
|
||||
var radiusSq = radius * radius;
|
||||
var tol = Math.Clamp(toxinTolerance, 0f, 1f);
|
||||
var bestScore = float.NegativeInfinity;
|
||||
plantId = -1;
|
||||
position = default;
|
||||
foreach (var (transforms, _, organisms, entities) in _plants.Chunks)
|
||||
{
|
||||
var t = transforms.Span;
|
||||
var o = organisms.Span;
|
||||
for (var i = 0; i < t.Length; i++)
|
||||
{
|
||||
var sq = Vector2.DistanceSquared(from, t[i].Position);
|
||||
if (sq > radiusSq)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ref readonly var tr = ref o[i].Traits;
|
||||
var perceivedToxin = tr.Toxicity * (1f - tol);
|
||||
var score =
|
||||
tr.Palatability
|
||||
- ToxinAvoidWeight * perceivedToxin
|
||||
- ForageDistanceWeight * (radius > 0f ? MathF.Sqrt(sq) / radius : 0f);
|
||||
var id = entities.EntityAt(i).Id;
|
||||
if (score > bestScore || (score == bestScore && id < plantId))
|
||||
{
|
||||
bestScore = score;
|
||||
plantId = id;
|
||||
position = t[i].Position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return plantId >= 0;
|
||||
}
|
||||
|
||||
// Ближайшая кромка воды (без лимита по зрению — звери «помнят» водопои; упрощение фазы A2).
|
||||
private bool TryFindShore(Vector2 from, out Vector2 position)
|
||||
{
|
||||
@@ -707,6 +797,7 @@ public sealed class AnimalActionSystem : BaseSystem
|
||||
private readonly int _cellSize;
|
||||
private readonly RectF _bounds;
|
||||
private readonly HediffDef? _bleeding;
|
||||
private readonly HediffDef? _poisoned;
|
||||
private readonly Random _rng;
|
||||
private readonly ArchetypeQuery<
|
||||
AnimalBrain,
|
||||
@@ -731,6 +822,7 @@ public sealed class AnimalActionSystem : BaseSystem
|
||||
int cellSize,
|
||||
RectF bounds,
|
||||
HediffDef? bleeding,
|
||||
HediffDef? poisoned,
|
||||
int seed
|
||||
)
|
||||
{
|
||||
@@ -744,6 +836,7 @@ public sealed class AnimalActionSystem : BaseSystem
|
||||
_cellSize = cellSize;
|
||||
_bounds = bounds;
|
||||
_bleeding = bleeding;
|
||||
_poisoned = poisoned;
|
||||
_rng = new Random(seed);
|
||||
_query = store.Query<AnimalBrain, AnimalNeeds, AnimalOrganism, Transform2D, Health>();
|
||||
}
|
||||
@@ -792,6 +885,13 @@ public sealed class AnimalActionSystem : BaseSystem
|
||||
{
|
||||
Refill(values, AnimalActions.Eat, days);
|
||||
Graze(brain.TargetPlant, days, def.ForageBiteDays);
|
||||
// Коэволюция: защита растения бьёт по поедателю (яд/шипы).
|
||||
ApplyPlantDefense(
|
||||
brain.TargetPlant,
|
||||
hh[i].State,
|
||||
o[i].Traits.ToxinTolerance,
|
||||
days
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -1010,6 +1110,65 @@ public sealed class AnimalActionSystem : BaseSystem
|
||||
}
|
||||
}
|
||||
|
||||
// Доза яда за день укуса при токсичности 1 и нулевой устойчивости (масштабируется тяжести Poisoned).
|
||||
private const float PoisonDosePerDay = 1.6f;
|
||||
|
||||
// Порог шипов, выше которого укус о растение травмирует, и масштабы урона/кровопотери от шипов.
|
||||
private const float ThornThreshold = 0.2f;
|
||||
private const float ThornPartDamage = 4f;
|
||||
private const float ThornBloodLoss = 0.02f;
|
||||
|
||||
// Коэволюция: при поедании растение «отвечает» — токсичность отравляет (тем сильнее, чем ниже
|
||||
// устойчивость зверя к яду), шипы наносят лёгкую травму. Доза яда копится в хедифе Poisoned; если
|
||||
// зверь ест яд быстрее, чем выводит, тяжесть дойдёт до летальной. Детерминированно (общий _rng).
|
||||
private void ApplyPlantDefense(
|
||||
int plantId,
|
||||
HealthState? health,
|
||||
float toxinTolerance,
|
||||
float days
|
||||
)
|
||||
{
|
||||
if (
|
||||
health is null
|
||||
|| plantId < 0
|
||||
|| !_store.TryGetEntityById(plantId, out var plant)
|
||||
|| plant.IsNull
|
||||
|| !plant.HasComponent<PlantOrganism>()
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ref readonly var traits = ref plant.GetComponent<PlantOrganism>().Traits;
|
||||
|
||||
var poisoned = false;
|
||||
if (_poisoned is not null && traits.Toxicity > 0f)
|
||||
{
|
||||
var dose = traits.Toxicity * (1f - Math.Clamp(toxinTolerance, 0f, 1f));
|
||||
if (dose > 0f)
|
||||
{
|
||||
health.Intensify(_poisoned, dose * PoisonDosePerDay * days);
|
||||
poisoned = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (traits.Thorns > ThornThreshold)
|
||||
{
|
||||
// ApplyInjury сам пересчитывает способности (учтёт и свежий яд).
|
||||
health.ApplyInjury(
|
||||
traits.Thorns * ThornPartDamage * days,
|
||||
traits.Thorns * ThornBloodLoss * days,
|
||||
_bleeding,
|
||||
traits.Thorns * 0.15f * days,
|
||||
_rng
|
||||
);
|
||||
}
|
||||
else if (poisoned)
|
||||
{
|
||||
health.RecomputeCapacities(); // отравление меняет capacity-моды — применить сразу
|
||||
}
|
||||
}
|
||||
|
||||
// Применяет спаривания после прохода: самка пары беременеет геномом самца (структурное добавление —
|
||||
// после итерации), влечение обоих обнуляется. Двойные события на одну самку отсеиваются.
|
||||
private void ApplyMatings()
|
||||
|
||||
Reference in New Issue
Block a user