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
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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): дерево частей тела вида и способности здоровой особи.
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user