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:
Leonid Pershin
2026-06-14 14:13:51 +03:00
co-authored by Claude Opus 4.8
parent 97d83606ce
commit 8807045303
17 changed files with 400 additions and 25 deletions
+9
View File
@@ -143,6 +143,15 @@ public sealed class GenomeDef
/// <summary>Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета.</summary>
public float LeafHue { get; init; } = 0.33f;
/// <summary>Токсичность (0..1): отравляет поедателя (коэволюция); тормозит рост (цена защиты).</summary>
public float Toxicity { get; init; }
/// <summary>Шипы (0..1): травмируют поедателя; тормозят рост (цена защиты).</summary>
public float Thorns { get; init; }
/// <summary>Вкусность (0..1): обратная привлекательность для умных травоядных (избегание в C2).</summary>
public float Palatability { get; init; } = 1f;
}
/// <summary>Растение (Defs/Plants/): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
+4
View File
@@ -118,6 +118,10 @@ public sealed class PlantSet
new GenomeTemplate.Entry(Gene("GeneFruitSeason"), g.FruitSeason, 0f), // сезон фиксирован по виду
Numeric("GeneHarvestAmount", g.HarvestAmount),
new GenomeTemplate.Entry(Gene("GeneLeafHue"), g.LeafHue, 0.04f),
// Защита (коэволюция с травоядными): токсичность/шипы/вкусность.
Numeric("GeneToxicity", g.Toxicity),
Numeric("GeneThorns", g.Thorns),
Numeric("GenePalatability", g.Palatability),
new GenomeTemplate.Entry(
Gene("GeneMorph"),
0f,
+41 -1
View File
@@ -260,6 +260,7 @@ public sealed class WorldScene : Scene
)
);
var bleeding = content.Defs.TryGet<HediffDef>("Bleeding", out var bl) ? bl : null;
var poisoned = content.Defs.TryGet<HediffDef>("Poisoned", out var ps) ? ps : null;
UpdateSystems.Add(
new AnimalActionSystem(
Store,
@@ -272,6 +273,7 @@ public sealed class WorldScene : Scene
CellSize,
_bounds,
bleeding,
poisoned,
_config.Seed + 0x1B17
)
);
@@ -1070,6 +1072,10 @@ public sealed class WorldScene : Scene
$" temp: grows {tMin:0.#}..{tMax:0.#}°C, optimal {tLow:0.#}..{tHigh:0.#}°C "
+ $"(cold {traits.ColdHardiness:0.#}, heat {traits.HeatHardiness:0.#})"
);
console.WriteLine(
$" defense: toxicity {traits.Toxicity:0.##}, thorns {traits.Thorns:0.##}, "
+ $"palatability {traits.Palatability:0.##} (growth ×{traits.DefenseGrowthFactor():0.##})"
);
if (def.HarvestProduct is { } harvest)
{
@@ -1126,6 +1132,10 @@ public sealed class WorldScene : Scene
$" move {traits.MoveSpeed:0.##}, vision {traits.Vision:0.##}, blood {traits.BloodVolume:0.##}, "
+ $"insulation {traits.Insulation:0.##}, furHue {traits.FurHue:0.##}"
);
console.WriteLine(
$" diet: herb {traits.Herbivory:0.##} / carn {traits.Carnivory:0.##} / omni {traits.Omnivory:0.##}, "
+ $"toxinTolerance {traits.ToxinTolerance:0.##}"
);
}
// Диета из генов (предатор-кластер): по каждому виду печатает herbivory/carnivory/omnivory,
@@ -1187,6 +1197,7 @@ public sealed class WorldScene : Scene
var brain = new double[_animals.Count];
var life = new double[_animals.Count];
var move = new double[_animals.Count];
var tox = new double[_animals.Count];
var maxGen = new int[_animals.Count];
Store
@@ -1200,6 +1211,7 @@ public sealed class WorldScene : Scene
brain[s] += o.Traits.BrainSize;
life[s] += o.Traits.Lifespan;
move[s] += o.Traits.MoveSpeed;
tox[s] += o.Traits.ToxinTolerance;
if (o.Generation > maxGen[s])
{
maxGen[s] = o.Generation;
@@ -1227,7 +1239,8 @@ public sealed class WorldScene : Scene
var c = count[s];
console.WriteLine(
$"{name}: n={c}, gen 0..{maxGen[s]}, body {body[s] / c:0.##}, "
+ $"brain {brain[s] / c:0.##}, lifespan {life[s] / c:0} d, move {move[s] / c:0.##}"
+ $"brain {brain[s] / c:0.##}, lifespan {life[s] / c:0} d, move {move[s] / c:0.##}, "
+ $"toxinTol {tox[s] / c:0.##}"
);
}
@@ -1235,6 +1248,33 @@ public sealed class WorldScene : Scene
{
console.WriteLine(filter is null ? "no animals alive" : $"no '{filter}' alive");
}
// Другая сторона гонки вооружений: средняя защита живых растений (коэволюция C2). Без фильтра.
if (filter is null)
{
var pn = 0;
double pTox = 0,
pThorn = 0,
pPalat = 0;
Store
.Query<PlantOrganism>()
.ForEachEntity(
(ref PlantOrganism o, Entity _) =>
{
pn++;
pTox += o.Traits.Toxicity;
pThorn += o.Traits.Thorns;
pPalat += o.Traits.Palatability;
}
);
if (pn > 0)
{
console.WriteLine(
$"plants: n={pn}, toxicity {pTox / pn:0.###}, thorns {pThorn / pn:0.###}, "
+ $"palatability {pPalat / pn:0.###}"
);
}
}
}
// Наблюдаемость здоровья (фаза A5): дерево частей тела вида и способности здоровой особи.
+4
View File
@@ -48,6 +48,9 @@ public struct AnimalPhenotype
/// <summary>Всеядность [0..1] — генералист: включает оба источника пищи (растения и мясо).</summary>
public float Omnivory;
/// <summary>Устойчивость к растительным ядам [0..1] — снижает дозу отравления токсичным кормом.</summary>
public float ToxinTolerance;
/// <summary>Продолжительность жизни (игровых дней).</summary>
public float Lifespan;
@@ -78,6 +81,7 @@ public struct AnimalPhenotype
Herbivory = T("herbivory"),
Carnivory = T("carnivory"),
Omnivory = T("omnivory"),
ToxinTolerance = T("toxinTolerance"),
Lifespan = T("lifespan"),
BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)),
GestationDays = T("gestationDays"),
+164 -5
View File
@@ -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()
+47 -8
View File
@@ -9,8 +9,27 @@ public static class AnimalCapacities
public const string Moving = "Moving";
public const string Breathing = "Breathing";
public const string BloodPumping = "BloodPumping";
public const string BloodFiltration = "BloodFiltration";
public const string Sight = "Sight";
public const string Hearing = "Hearing";
public const string Talking = "Talking";
public const string Eating = "Eating";
public const string Digestion = "Digestion";
/// <summary>Порядок отображения способностей в UI (сверху вниз). Прочие модовые — после, как есть.</summary>
public static readonly string[] DisplayOrder =
[
Consciousness,
Moving,
Sight,
Hearing,
Talking,
Eating,
Breathing,
BloodPumping,
BloodFiltration,
Digestion,
];
}
/// <summary>
@@ -24,7 +43,11 @@ public static class CapacityCalc
/// <summary>Считает карту способностей из инстансов частей, уровня крови (0..1) и боли (0..1).</summary>
public static Dictionary<string, float> Compute(PartInstance[] parts, float blood, float pain)
{
// Сырой вклад каждой способности (Σ доля·HP) и НАБОР объявленных телом способностей: вид имеет
// только те, к которым его части вообще причастны (у оленя нет манипуляции). Объявленность —
// статична (по дефам частей), поэтому уничтоженный орган всё равно покажет способность на 0%.
var raw = new Dictionary<string, float>(System.StringComparer.Ordinal);
var declared = new HashSet<string>(System.StringComparer.Ordinal);
foreach (var part in parts)
{
if (part.Def is null)
@@ -35,36 +58,52 @@ public static class CapacityCalc
var fraction = part.Fraction;
foreach (var (capacity, contribution) in part.Def.Capacities)
{
declared.Add(capacity);
raw[capacity] = raw.GetValueOrDefault(capacity) + contribution * fraction;
}
}
float Raw(string id) => raw.GetValueOrDefault(id);
static float Clamp01(float v) => v < 0f ? 0f : v > 1f ? 1f : v;
static float Clamp01(float v) =>
v < 0f ? 0f
: v > 1f ? 1f
: v;
// Базовые (независимые) способности: кровоснабжение/дыхание/фильтрация × уровень крови.
var bloodLevel = Clamp01(blood);
var bloodPumping = Raw(AnimalCapacities.BloodPumping) * bloodLevel;
var breathing = Raw(AnimalCapacities.Breathing) * bloodLevel;
var bloodFiltration = Raw(AnimalCapacities.BloodFiltration) * bloodLevel;
// Сознание зависит от кровоснабжения, дыхания и боли (модель RimWorld).
var consciousness =
Raw(AnimalCapacities.Consciousness)
* Clamp01(bloodPumping)
* Clamp01(breathing)
* (1f - Clamp01(pain));
var cons = Clamp01(consciousness);
var pump = Clamp01(bloodPumping);
var result = new Dictionary<string, float>(System.StringComparer.Ordinal)
// Производные способности зависят от сознания (и кровоснабжения для движения/пищеварения).
var computed = new Dictionary<string, float>(System.StringComparer.Ordinal)
{
[AnimalCapacities.BloodPumping] = bloodPumping,
[AnimalCapacities.Breathing] = breathing,
[AnimalCapacities.BloodFiltration] = bloodFiltration,
[AnimalCapacities.Consciousness] = consciousness,
[AnimalCapacities.Moving] = Raw(AnimalCapacities.Moving) * consciousness * Clamp01(bloodPumping),
[AnimalCapacities.Sight] = Raw(AnimalCapacities.Sight) * consciousness,
[AnimalCapacities.Digestion] = Raw(AnimalCapacities.Digestion) * Clamp01(bloodPumping),
[AnimalCapacities.Moving] = Raw(AnimalCapacities.Moving) * cons * pump,
[AnimalCapacities.Sight] = Raw(AnimalCapacities.Sight) * cons,
[AnimalCapacities.Hearing] = Raw(AnimalCapacities.Hearing) * cons,
[AnimalCapacities.Talking] = Raw(AnimalCapacities.Talking) * cons,
[AnimalCapacities.Eating] = Raw(AnimalCapacities.Eating) * cons,
[AnimalCapacities.Digestion] = Raw(AnimalCapacities.Digestion) * pump,
};
// Модовые способности, не охваченные зависимостями, — сырыми.
foreach (var (capacity, value) in raw)
// В результат — только объявленные телом способности: известные по формуле зависимостей, прочие
// (модовые) — сырым вкладом. Так у вида видны ровно его способности, а не весь каталог.
var result = new Dictionary<string, float>(System.StringComparer.Ordinal);
foreach (var capacity in declared)
{
result.TryAdd(capacity, value);
result[capacity] = computed.TryGetValue(capacity, out var v) ? v : Raw(capacity);
}
return result;
+1
View File
@@ -57,6 +57,7 @@ public sealed class PlantGrowthSystem(
var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
var rate =
traits.Vigor
* traits.DefenseGrowthFactor() // цена защиты: токсичные/колючие растут медленнее
* Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance)
* Suitability.Trapezoid(temperature, tMin, tLow, tHigh, tMax)
* Suitability.Gaussian(
+27
View File
@@ -32,6 +32,16 @@ public struct PlantPhenotype
public float HarvestAmount;
public float LeafHue;
// --- Защита (коэволюция с травоядными) ---
/// <summary>Токсичность [0..1]: отравляет поедателя; цена — замедление роста.</summary>
public float Toxicity;
/// <summary>Шипы [0..1]: травмируют поедателя; цена — замедление роста.</summary>
public float Thorns;
/// <summary>Вкусность [0..1]: обратная привлекательность для умных травоядных (избегание в C2).</summary>
public float Palatability;
/// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary>
public bool IsVariant;
@@ -71,9 +81,26 @@ public struct PlantPhenotype
FruitSeason = (int)MathF.Round(Math.Clamp(T("fruitSeason"), 0f, 3f)),
HarvestAmount = T("harvestAmount"),
LeafHue = T("leafHue"),
Toxicity = T("toxicity"),
Thorns = T("thorns"),
Palatability = T("palatability"),
IsVariant = T("variant") >= 0.5f,
};
}
// Цена защиты: токсичность и шипы отнимают ресурсы у роста. Без этой платы отбор гнал бы защиту к
// максимуму у всех растений и коэволюция бы встала — а так под слабым выпасом выгоднее расти быстрее.
private const float ToxicityGrowthCost = 0.4f;
private const float ThornsGrowthCost = 0.3f;
private const float MinGrowthFactor = 0.2f;
/// <summary>Множитель скорости роста с учётом цены защиты (1 — без защиты, ≥ <c>MinGrowthFactor</c>).</summary>
public readonly float DefenseGrowthFactor() =>
Math.Clamp(
1f - ToxicityGrowthCost * Toxicity - ThornsGrowthCost * Thorns,
MinGrowthFactor,
1f
);
}
/// <summary>
+29 -4
View File
@@ -331,6 +331,13 @@ internal sealed class InspectPanel
traits.LeafHue
)
);
if (traits.Toxicity > 0.01f || traits.Thorns > 0.01f)
{
text.AppendLine(
languages.Format("inspect.gene.defense", traits.Toxicity, traits.Thorns)
);
}
if (traits.IsVariant)
{
text.AppendLine(languages.Get("inspect.gene.variant"));
@@ -435,12 +442,27 @@ internal sealed class InspectPanel
text.AppendLine(
languages.Format("inspect.health.blood", state.BloodLevel * 100f, state.Pain * 100f)
);
if (state.Parts.Length > 0)
if (state.Capacities.Count > 0)
{
text.AppendLine(languages.Get("inspect.health.caps"));
AppendCapacity(text, state, AnimalCapacities.Consciousness, "cap.consciousness");
AppendCapacity(text, state, AnimalCapacities.Moving, "cap.moving");
AppendCapacity(text, state, AnimalCapacities.Sight, "cap.sight");
// Сначала известные способности в порядке отображения, затем прочие (модовые) — как есть.
var shown = new HashSet<string>(StringComparer.Ordinal);
foreach (var capId in AnimalCapacities.DisplayOrder)
{
if (state.Capacities.ContainsKey(capId))
{
AppendCapacity(text, state, capId, CapacityLabel(capId));
shown.Add(capId);
}
}
foreach (var capId in state.Capacities.Keys)
{
if (shown.Add(capId))
{
AppendCapacity(text, state, capId, CapacityLabel(capId));
}
}
}
if (state.Hediffs.Count > 0)
@@ -463,6 +485,9 @@ internal sealed class InspectPanel
}
}
// Ключ локализации метки способности: cap.<id в нижнем регистре> (cap.consciousness, cap.bloodpumping…).
private static string CapacityLabel(string capId) => "cap." + capId.ToLowerInvariant();
private void AppendCapacity(
StringBuilder text,
HealthState state,