diff --git a/Mods/Core/Defs/Animals/Animals.json b/Mods/Core/Defs/Animals/Animals.json
index 0b10d67..ed10555 100644
--- a/Mods/Core/Defs/Animals/Animals.json
+++ b/Mods/Core/Defs/Animals/Animals.json
@@ -30,7 +30,8 @@
"GeneBreedingSeason": 2,
"GeneGestationDays": 30,
"GeneLitterSize": 1,
- "GeneHerbivory": 1.0
+ "GeneHerbivory": 1.0,
+ "GeneToxinTolerance": 0.1
}
},
@@ -97,7 +98,38 @@
"GeneLitterSize": 5,
"GeneHerbivory": 0.4,
"GeneCarnivory": 0.2,
- "GeneOmnivory": 0.85
+ "GeneOmnivory": 0.85,
+ "GeneToxinTolerance": 0.25
+ }
+ },
+
+ {
+ "defName": "Chicken",
+ "label": "pawn.chicken",
+ "kind": "animal",
+ "texture": "things/pawn/animal/chicken/Chicken_east",
+ "body": "Quadruped",
+ "diet": ["plant"],
+ "spawnPer1000Cells": 1.5,
+ "baseSpeed": 22,
+ "visionCells": 10,
+ "genome": {
+ "GeneMaxBodySize": 0.5,
+ "GeneMetabolism": 1.3,
+ "GeneMoveSpeed": 0.9,
+ "GeneBloodVolume": 0.7,
+ "GeneVision": 1.1,
+ "GeneBrainSize": 0.3,
+ "GeneInsulation": 0.45,
+ "GeneFurColor": 0.6,
+ "GeneMaturityAge": 50,
+ "GeneLifespan": 180,
+ "GeneBreedingSeason": 1,
+ "GeneGestationDays": 12,
+ "GeneLitterSize": 6,
+ "GeneHerbivory": 0.8,
+ "GeneToxinTolerance": 0.15,
+ "GeneEggLaying": 1.0
}
}
]
diff --git a/Mods/Core/Defs/Bodies/Bodies.json b/Mods/Core/Defs/Bodies/Bodies.json
index 88c1e06..c688659 100644
--- a/Mods/Core/Defs/Bodies/Bodies.json
+++ b/Mods/Core/Defs/Bodies/Bodies.json
@@ -7,23 +7,36 @@
{
"defName": "Quadruped",
"parts": [
- { "name": "torso", "coverage": 0.32, "maxHp": 40, "vital": true },
+ { "name": "torso", "coverage": 0.3, "maxHp": 40, "vital": true },
{ "name": "heart", "parent": "torso", "coverage": 0.02, "maxHp": 12, "vital": true,
"capacities": { "BloodPumping": 1.0 } },
{ "name": "lungLeft", "parent": "torso", "coverage": 0.03, "maxHp": 12,
"capacities": { "Breathing": 0.5 } },
{ "name": "lungRight", "parent": "torso", "coverage": 0.03, "maxHp": 12,
"capacities": { "Breathing": 0.5 } },
- { "name": "liver", "parent": "torso", "coverage": 0.03, "maxHp": 14, "vital": true },
+ { "name": "liver", "parent": "torso", "coverage": 0.03, "maxHp": 14, "vital": true,
+ "capacities": { "BloodFiltration": 0.4 } },
+ { "name": "kidneyLeft", "parent": "torso", "coverage": 0.02, "maxHp": 10,
+ "capacities": { "BloodFiltration": 0.3 } },
+ { "name": "kidneyRight", "parent": "torso", "coverage": 0.02, "maxHp": 10,
+ "capacities": { "BloodFiltration": 0.3 } },
{ "name": "stomach", "parent": "torso", "coverage": 0.03, "maxHp": 12,
"capacities": { "Digestion": 1.0 } },
- { "name": "head", "coverage": 0.1, "maxHp": 25 },
+ { "name": "head", "coverage": 0.08, "maxHp": 25 },
{ "name": "brain", "parent": "head", "coverage": 0.02, "maxHp": 12, "vital": true,
"capacities": { "Consciousness": 1.0 } },
{ "name": "eyeLeft", "parent": "head", "coverage": 0.015, "maxHp": 8,
"capacities": { "Sight": 0.5 } },
{ "name": "eyeRight", "parent": "head", "coverage": 0.015, "maxHp": 8,
"capacities": { "Sight": 0.5 } },
+ { "name": "earLeft", "parent": "head", "coverage": 0.01, "maxHp": 8,
+ "capacities": { "Hearing": 0.5 } },
+ { "name": "earRight", "parent": "head", "coverage": 0.01, "maxHp": 8,
+ "capacities": { "Hearing": 0.5 } },
+ { "name": "jaw", "parent": "head", "coverage": 0.02, "maxHp": 10,
+ "capacities": { "Eating": 1.0, "Talking": 0.5 } },
+ { "name": "tongue", "parent": "head", "coverage": 0.01, "maxHp": 8,
+ "capacities": { "Talking": 0.5 } },
{ "name": "legFrontLeft", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } },
{ "name": "legFrontRight", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } },
{ "name": "legBackLeft", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } },
diff --git a/Mods/Core/Defs/Genes/Animal.json b/Mods/Core/Defs/Genes/Animal.json
index 452e3ef..cc54db5 100644
--- a/Mods/Core/Defs/Genes/Animal.json
+++ b/Mods/Core/Defs/Genes/Animal.json
@@ -46,12 +46,26 @@
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"],
"effects": { "omnivory": "value" } },
+ // Устойчивость к растительным ядам (коэволюция): снижает дозу отравления при поедании токсичных
+ // растений (effectivePoison = toxicity × (1 − toxinTolerance), см. AnimalActionSystem). Растёт под
+ // давлением ядовитого корма — другая сторона гонки вооружений с GeneToxicity растений.
+ { "defName": "GeneToxinTolerance", "parent": "BaseNumericGene", "label": "gene.toxinTolerance",
+ "default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"],
+ "effects": { "toxinTolerance": "value" } },
+
// Пол как локус (фаза A3): аллели {X=0, Y=1}; самка XX, самец XY. Эффекта нет — пол читается из
// самих аллелей (наличие Y), не из выраженного значения. mutationChance 0 (X не мутирует в Y).
// Генерация особи-основателя задаёт пол явно (XX или XY), чтобы не возник невозможный YY.
{ "defName": "GeneSex", "kind": "Discrete", "label": "gene.sex",
"variants": 2, "variantWeights": [0.5, 0.5], "mutationChance": 0, "tags": ["animal", "sex"] },
+ // Тип рождения (цикл зачатия): живородящие (eggLaying < 0.5) вынашивают и рожают живых детёнышей;
+ // яйцекладущие (≥ 0.5) после вынашивания ОТКЛАДЫВАЮТ яйца-сущности, которые инкубируются и
+ // вылупляются (см. AnimalPregnancySystem/EggSystem). Признак фиксирован по виду (spread/mutation 0).
+ { "defName": "GeneEggLaying", "parent": "BaseNumericGene", "label": "gene.eggLaying",
+ "default": 0.0, "min": 0.0, "max": 1.0, "spread": 0, "mutationChance": 0, "tags": ["animal", "reproduction"],
+ "effects": { "eggLaying": "value" } },
+
// Размножение (фаза A4): сезон гона фиксирован по виду (0 весна … 3 зима), срок вынашивания и помёт.
{ "defName": "GeneBreedingSeason", "parent": "BaseNumericGene", "label": "gene.breedingSeason",
"default": 2, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["animal", "reproduction"],
diff --git a/Mods/Core/Defs/Genes/Content.json b/Mods/Core/Defs/Genes/Content.json
index 17d9258..92e5d64 100644
--- a/Mods/Core/Defs/Genes/Content.json
+++ b/Mods/Core/Defs/Genes/Content.json
@@ -15,6 +15,21 @@
"default": 0.33, "min": 0, "max": 1, "spread": 0.04, "tags": ["morphology", "color"],
"effects": { "leafHue": "value" } },
+ // --- Защита растения (коэволюция с травоядными) ---
+ // Токсичность отравляет поедателя (hediff Poisoned, тем сильнее, чем ниже его устойчивость к яду);
+ // шипы наносят поедателю лёгкую травму; вкусность (palatability) — обратная привлекательность для
+ // умных травоядных (избегание появится в C2). У защиты ЕСТЬ ЦЕНА: токсичность/шипы тормозят рост
+ // (см. PlantGrowthSystem), иначе все растения дошли бы до максимума и коэволюция бы встала.
+ { "defName": "GeneToxicity", "parent": "BaseNumericGene", "label": "gene.toxicity",
+ "default": 0.0, "min": 0.0, "max": 1.0, "tags": ["defense"],
+ "effects": { "toxicity": "value" } },
+ { "defName": "GeneThorns", "parent": "BaseNumericGene", "label": "gene.thorns",
+ "default": 0.0, "min": 0.0, "max": 1.0, "tags": ["defense"],
+ "effects": { "thorns": "value" } },
+ { "defName": "GenePalatability", "parent": "BaseNumericGene", "label": "gene.palatability",
+ "default": 1.0, "min": 0.0, "max": 1.0, "tags": ["defense"],
+ "effects": { "palatability": "value" } },
+
// Производный ген: признак собирается группировкой по регэкспу — сумма всех генов-толерантностей
// (демонстрация gsom-функций фазы G5). Собственное значение гена не используется.
{ "defName": "GeneHardiness", "parent": "BaseNumericGene", "label": "gene.hardiness",
diff --git a/Mods/Core/Defs/Hediffs/Hediffs.json b/Mods/Core/Defs/Hediffs/Hediffs.json
index 00b001e..f684124 100644
--- a/Mods/Core/Defs/Hediffs/Hediffs.json
+++ b/Mods/Core/Defs/Hediffs/Hediffs.json
@@ -22,6 +22,16 @@
"defName": "Bleeding", "label": "hediff.bleeding",
"initialSeverity": 0.3, "immunityPerDay": 0.5, "bloodLossPerDay": 0.8,
"pain": 0.25, "capMods": { "Moving": 0.92, "Consciousness": 0.95 }
+ },
+
+ // Отравление растительным ядом (коэволюция): сама не прогрессирует (severityPerDay 0) — тяжесть
+ // копится при поедании токсичных растений (HealthState.Intensify в AnimalActionSystem). Иммунитет
+ // её рассасывает (~1.5 дня), но если зверь ест яд быстрее, чем выводит, тяжесть доходит до 1 → смерть.
+ // Это селективное давление: ядовитый корм опасен для травоядных с низкой устойчивостью.
+ {
+ "defName": "Poisoned", "label": "hediff.poisoned",
+ "initialSeverity": 0.0, "immunityPerDay": 0.65, "lethalSeverity": 1.0, "pain": 0.2,
+ "capMods": { "Moving": 0.85, "Consciousness": 0.9, "Digestion": 0.7 }
}
]
}
diff --git a/Mods/Core/Defs/Plants/_Bases.json b/Mods/Core/Defs/Plants/_Bases.json
index dbbda4c..e5d2935 100644
--- a/Mods/Core/Defs/Plants/_Bases.json
+++ b/Mods/Core/Defs/Plants/_Bases.json
@@ -66,7 +66,8 @@
"optimalFertility": 0.4, "fertilityTolerance": 0.5, "vigor": 0.5,
"lifespan": 300, "dispersalRange": 2, "reproduceInterval": 25, "selfPollination": 0.6,
"mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08,
- "fruitYield": 3, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.5 },
+ "fruitYield": 3, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.5,
+ "thorns": 0.6, "palatability": 0.5 },
"stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 5, "label": "plant.stage.sprout" },
{ "label": "plant.stage.mature" }
@@ -81,7 +82,8 @@
"optimalFertility": 1.6, "fertilityTolerance": 1.1, "vigor": 1.2,
"lifespan": 25, "dispersalRange": 3, "reproduceInterval": 4, "selfPollination": 0.9,
"mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1,
- "fruitYield": 0, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.5 },
+ "fruitYield": 0, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.5,
+ "toxicity": 0.5, "palatability": 0.4 },
"stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.4, "growDays": 2, "label": "plant.stage.sprout" },
{ "label": "plant.stage.mature" }
diff --git a/Mods/Core/Languages/en/ui.json b/Mods/Core/Languages/en/ui.json
index 5f1abda..a63e94b 100644
--- a/Mods/Core/Languages/en/ui.json
+++ b/Mods/Core/Languages/en/ui.json
@@ -30,6 +30,7 @@
"inspect.gene.repro": "Dispersal {0:0.0} · interval {1:0.0} d",
"inspect.gene.repro2": "Self-pollin. {0:0.00} · mutation {1:0.00}",
"inspect.gene.hardy": "Cold/heat {0:0}/{1:0} · leaf hue {2:0.00}",
+ "inspect.gene.defense": "Defense: toxin {0:0.00} · thorns {1:0.00}",
"inspect.gene.variant": "Morph: recessive variant",
"inspect.harvest": "Harvest: {0} ×{1:0.#}",
"inspect.fruit": "Fruit: {0} ×{1:0.#} ({2}) · ripe {3:0.#}",
@@ -61,9 +62,17 @@
"cap.consciousness": "consciousness",
"cap.moving": "moving",
"cap.sight": "sight",
+ "cap.hearing": "hearing",
+ "cap.talking": "talking",
+ "cap.eating": "eating",
+ "cap.breathing": "breathing",
+ "cap.bloodpumping": "blood pumping",
+ "cap.bloodfiltration": "blood filtration",
+ "cap.digestion": "digestion",
"hediff.rut": "rut",
"hediff.fever": "fever",
"hediff.bleeding": "bleeding",
+ "hediff.poisoned": "poisoned",
"inspect.hint": "LMB — select · RMB/Esc — clear",
"hud.paused": "PAUSED",
"menu.title": "LittleSim",
@@ -154,6 +163,7 @@
"pawn.hare": "hare",
"pawn.boar": "boar",
"pawn.wolf": "wolf",
+ "pawn.chicken": "chicken",
"pawn.muffalo": "muffalo",
"pawn.squirrel": "squirrel",
"thought.gaveBirth": "gave birth",
diff --git a/Mods/Core/Languages/ru/ui.json b/Mods/Core/Languages/ru/ui.json
index 0c6d660..8cb7bef 100644
--- a/Mods/Core/Languages/ru/ui.json
+++ b/Mods/Core/Languages/ru/ui.json
@@ -30,6 +30,7 @@
"inspect.gene.repro": "Расселение {0:0.0} кл · период {1:0.0} дн",
"inspect.gene.repro2": "Самоопыление {0:0.00} · мутации {1:0.00}",
"inspect.gene.hardy": "Морозо/жаро {0:0}/{1:0} · оттенок {2:0.00}",
+ "inspect.gene.defense": "Защита: яд {0:0.00} · шипы {1:0.00}",
"inspect.gene.variant": "Морфа: рецессивный вариант",
"inspect.harvest": "Сбор: {0} ×{1:0.#}",
"inspect.fruit": "Плоды: {0} ×{1:0.#} ({2}) · зрелых {3:0.#}",
@@ -61,9 +62,17 @@
"cap.consciousness": "сознание",
"cap.moving": "движение",
"cap.sight": "зрение",
+ "cap.hearing": "слух",
+ "cap.talking": "речь",
+ "cap.eating": "питание",
+ "cap.breathing": "дыхание",
+ "cap.bloodpumping": "кровоснабжение",
+ "cap.bloodfiltration": "фильтрация крови",
+ "cap.digestion": "пищеварение",
"hediff.rut": "гон",
"hediff.fever": "лихорадка",
"hediff.bleeding": "кровотечение",
+ "hediff.poisoned": "отравление",
"inspect.hint": "ЛКМ — выбрать · ПКМ/Esc — снять",
"hud.paused": "ПАУЗА",
"menu.title": "LittleSim",
@@ -154,6 +163,7 @@
"pawn.hare": "заяц",
"pawn.boar": "кабан",
"pawn.wolf": "волк",
+ "pawn.chicken": "курица",
"pawn.muffalo": "муффало",
"pawn.squirrel": "белка",
"thought.gaveBirth": "родила потомство",
diff --git a/src/LittleSim/App/WorldSave.cs b/src/LittleSim/App/WorldSave.cs
index ce1b9e8..752672c 100644
--- a/src/LittleSim/App/WorldSave.cs
+++ b/src/LittleSim/App/WorldSave.cs
@@ -173,6 +173,28 @@ public sealed class CorpseSave
public float Meat { get; set; }
}
+/// Сериализуемое яйцо (тип рождения «яйцекладка»): вид, позиция, геном детёныша, поколение, инкубация.
+public sealed class EggSave
+{
+ /// Имя дефа вида — по нему берётся индекс/спрайт/тело при вылуплении.
+ public string Species { get; set; } = "";
+
+ /// Позиция X в мировых координатах.
+ public float X { get; set; }
+
+ /// Позиция Y в мировых координатах.
+ public float Y { get; set; }
+
+ /// Поколение будущего детёныша.
+ public int Generation { get; set; }
+
+ /// Остаток инкубации в игровых днях.
+ public float IncubateDays { get; set; }
+
+ /// Геном будущего детёныша (уже скрещён на момент кладки).
+ public Dictionary Genome { get; set; } = new();
+}
+
///
/// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание),
/// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он
@@ -216,6 +238,9 @@ public sealed class WorldSave
/// Снимок трупов (вид, позиция, таймер/стадия разложения, мясо).
public List Corpses { get; set; } = [];
+ /// Снимок яиц (вид, позиция, геном детёныша, поколение, инкубация).
+ public List Eggs { get; set; } = [];
+
/// Конфиг мира для пересоздания сцены.
public WorldConfig ToConfig() =>
new()
diff --git a/src/LittleSim/Content/AnimalSet.cs b/src/LittleSim/Content/AnimalSet.cs
index 4c6792b..ca3fbb8 100644
--- a/src/LittleSim/Content/AnimalSet.cs
+++ b/src/LittleSim/Content/AnimalSet.cs
@@ -30,6 +30,9 @@ public sealed class AnimalSet
/// Спрайт трупа (fallback — самка).
public required Texture2DRegion Corpse { get; init; }
+ /// Спрайт яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт-точка.
+ public required Texture2DRegion Egg { get; init; }
+
/// Стадии роста (из дефа или встроенный дефолт), по возрастанию возраста входа.
public required AnimalStageDef[] Stages { get; init; }
@@ -43,10 +46,40 @@ public sealed class AnimalSet
// Встроенный набор стадий по умолчанию (если вид не задал свой): как было до выноса в данные.
private static readonly AnimalStageDef[] DefaultStages =
[
- new() { Name = "Baby", EnterAt = 0f, RelativeTo = "maturity", Scale = 0.45f, Texture = "baby" },
- new() { Name = "Juvenile", EnterAt = 0.22f, RelativeTo = "maturity", Scale = 0.7f, Texture = "baby" },
- new() { Name = "Adult", EnterAt = 1f, RelativeTo = "maturity", Scale = 1f, Texture = "adult", Adult = true },
- new() { Name = "Senior", EnterAt = 0.8f, RelativeTo = "lifespan", Scale = 0.92f, Texture = "adult", Adult = true },
+ new()
+ {
+ Name = "Baby",
+ EnterAt = 0f,
+ RelativeTo = "maturity",
+ Scale = 0.45f,
+ Texture = "baby",
+ },
+ new()
+ {
+ Name = "Juvenile",
+ EnterAt = 0.22f,
+ RelativeTo = "maturity",
+ Scale = 0.7f,
+ Texture = "baby",
+ },
+ new()
+ {
+ Name = "Adult",
+ EnterAt = 1f,
+ RelativeTo = "maturity",
+ Scale = 1f,
+ Texture = "adult",
+ Adult = true,
+ },
+ new()
+ {
+ Name = "Senior",
+ EnterAt = 0.8f,
+ RelativeTo = "lifespan",
+ Scale = 0.92f,
+ Texture = "adult",
+ Adult = true,
+ },
];
private readonly Species[] _species;
@@ -82,6 +115,9 @@ public sealed class AnimalSet
Corpse = string.IsNullOrEmpty(def.CorpseTexture)
? female
: atlases.GetRegion(device, def.CorpseTexture),
+ Egg = string.IsNullOrEmpty(def.EggTexture)
+ ? female
+ : atlases.GetRegion(device, def.EggTexture),
Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages,
Body = content.Defs.TryGet(def.Body, out var body) ? body : null,
Template = BuildTemplate(def.Genome, genes),
@@ -111,6 +147,35 @@ public sealed class AnimalSet
return genome;
}
+ ///
+ /// ОСНОВА создания гибрида (полноценная команда/UI — позже): геном помеси, привязанный к базовому виду
+ /// (его тело/спрайт/набор генов), но с общими генами, скрещёнными с
+ /// (по одной аллели-гамете от каждого родителя, как при размножении).
+ /// Пол берётся от базового вида; гены, которых нет у базового, игнорируются (его тело их не читает).
+ /// Сущность создаётся обычной с этим геномом и базовым видом —
+ /// то есть «создать любой вид» = Create(вид, GenerateGenome), «любой гибрид» = Create(вид, HybridGenome).
+ ///
+ public Genome HybridGenome(int baseSpecies, int otherSpecies, Random random)
+ {
+ var hybrid = GenerateGenome(baseSpecies, random);
+ var other = _species[otherSpecies].Template.Generate(random);
+ foreach (var (geneId, allele) in other.ToDictionary())
+ {
+ if (string.Equals(geneId, "GeneSex", StringComparison.Ordinal) || !hybrid.Has(geneId))
+ {
+ continue; // пол — от базового вида; чужие гены базовое тело не читает
+ }
+
+ var mine = hybrid[geneId];
+ hybrid[geneId] = new Allele(
+ random.NextSingle() < 0.5f ? mine.A : mine.B,
+ random.NextSingle() < 0.5f ? allele.A : allele.B
+ );
+ }
+
+ return hybrid;
+ }
+
// Строит шаблон генома вида из ДАННЫХ (geneId→base из Defs/Animals/) generically: разброс берётся из
// самого гена. Состав открыт — модер добавляет ген строкой JSON, без правки кода. Дискретные гены
// (база/разброс не нужны — варианты из GeneDef) тоже поддержаны.
diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs
index 436545e..c3fa062 100644
--- a/src/LittleSim/Content/GameDefs.cs
+++ b/src/LittleSim/Content/GameDefs.cs
@@ -143,6 +143,15 @@ public sealed class GenomeDef
/// Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета.
public float LeafHue { get; init; } = 0.33f;
+
+ /// Токсичность (0..1): отравляет поедателя (коэволюция); тормозит рост (цена защиты).
+ public float Toxicity { get; init; }
+
+ /// Шипы (0..1): травмируют поедателя; тормозят рост (цена защиты).
+ public float Thorns { get; init; }
+
+ /// Вкусность (0..1): обратная привлекательность для умных травоядных (избегание в C2).
+ public float Palatability { get; init; } = 1f;
}
/// Растение (Defs/Plants/): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.
@@ -252,6 +261,9 @@ public sealed class AnimalDef : PawnDef
/// Текстура трупа (фаза A5); пусто — берётся .
public string CorpseTexture { get; init; } = "";
+ /// Текстура яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт семени-точки.
+ public string EggTexture { get; init; } = "things/plant/seed_default";
+
/// Имя — анатомия вида (части тела/органы); пусто — без частей тела.
public string Body { get; init; } = "";
diff --git a/src/LittleSim/Content/PlantSet.cs b/src/LittleSim/Content/PlantSet.cs
index 684b7a1..2f4b894 100644
--- a/src/LittleSim/Content/PlantSet.cs
+++ b/src/LittleSim/Content/PlantSet.cs
@@ -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,
diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs
index 700ae0b..8ecf0a0 100644
--- a/src/LittleSim/Scenes/WorldScene.cs
+++ b/src/LittleSim/Scenes/WorldScene.cs
@@ -260,6 +260,7 @@ public sealed class WorldScene : Scene
)
);
var bleeding = content.Defs.TryGet("Bleeding", out var bl) ? bl : null;
+ var poisoned = content.Defs.TryGet("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
)
);
@@ -292,6 +294,17 @@ public sealed class WorldScene : Scene
_config.Seed + 0x4BED
)
);
+ UpdateSystems.Add(
+ new EggSystem(
+ Store,
+ _animals,
+ _needs,
+ Context.Clock,
+ SecondsPerDay,
+ CellSize,
+ _config.Seed + 0x6E66
+ )
+ );
UpdateSystems.Add(
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
@@ -517,7 +530,7 @@ public sealed class WorldScene : Scene
new SaveStore().Write(save);
Log.Info(
$"World '{_config.Name}' saved ({save.Plants.Count} plants, "
- + $"{save.Animals.Count} animals, {save.Corpses.Count} corpses)"
+ + $"{save.Animals.Count} animals, {save.Corpses.Count} corpses, {save.Eggs.Count} eggs)"
);
return _config.Name;
}
@@ -612,6 +625,23 @@ public sealed class WorldScene : Scene
}
)
);
+
+ Store
+ .Query()
+ .ForEachEntity(
+ (ref Egg egg, ref Transform2D transform, Entity _) =>
+ save.Eggs.Add(
+ new EggSave
+ {
+ Species = _animals[egg.Species].Def.DefName,
+ X = transform.Position.X,
+ Y = transform.Position.Y,
+ Generation = egg.Generation,
+ IncubateDays = egg.IncubateDays,
+ Genome = egg.Genome?.ToDictionary() ?? new(),
+ }
+ )
+ );
}
private void RestorePlants(GameContent content)
@@ -693,8 +723,31 @@ public sealed class WorldScene : Scene
data.Meat = saved.Meat;
}
+ foreach (var saved in _save.Eggs)
+ {
+ if (!content.Defs.TryGet(saved.Species, out var def))
+ {
+ continue; // вид пропал (мод убрали) — пропускаем яйцо
+ }
+
+ var index = _animals.IndexOf(def);
+ var genome = saved.Genome is { Count: > 0 }
+ ? new Genome(saved.Genome)
+ : _animals.GenerateGenome(index, fallback);
+ AnimalFactory.CreateEgg(
+ Store,
+ _animals,
+ index,
+ new Vector2(saved.X, saved.Y),
+ genome,
+ saved.Generation,
+ saved.IncubateDays,
+ CellSize
+ );
+ }
+
Log.Info(
- $"Restored {_save.Animals.Count} animals, {_save.Corpses.Count} corpses from save"
+ $"Restored {_save.Animals.Count} animals, {_save.Corpses.Count} corpses, {_save.Eggs.Count} eggs from save"
);
}
@@ -1070,6 +1123,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 +1183,14 @@ 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.##}"
+ );
+ console.WriteLine(
+ $" reproduction: {(traits.Oviparous ? "oviparous (lays eggs)" : "viviparous (live birth)")}, "
+ + $"gestation {traits.GestationDays:0} d, litter {traits.LitterSize:0.#}"
+ );
}
// Диета из генов (предатор-кластер): по каждому виду печатает herbivory/carnivory/omnivory,
@@ -1187,6 +1252,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 +1266,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 +1294,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 +1303,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()
+ .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): дерево частей тела вида и способности здоровой особи.
diff --git a/src/LittleSim/Sim/AnimalFactory.cs b/src/LittleSim/Sim/AnimalFactory.cs
index 6eb5596..268ece5 100644
--- a/src/LittleSim/Sim/AnimalFactory.cs
+++ b/src/LittleSim/Sim/AnimalFactory.cs
@@ -110,6 +110,46 @@ public static class AnimalFactory
);
}
+ // Тинт яйца (бледно-кремовый) и доля размера клетки, до которой масштабируется спрайт яйца.
+ private static readonly Color EggTint = new(238, 230, 208);
+ private const float EggSizeFraction = 0.45f;
+
+ ///
+ /// Создаёт сущность-яйцо (тип рождения «яйцекладка»): спрайт-яйцо и компонент с уже
+ /// скрещённым геномом будущего детёныша. Инкубируется и вылупляется в .
+ ///
+ public static Entity CreateEgg(
+ EntityStore store,
+ AnimalSet animals,
+ int species,
+ Vector2 position,
+ Genome genome,
+ int generation,
+ float incubateDays,
+ int cellSize
+ )
+ {
+ var sp = animals[species];
+ var region = sp.Egg;
+ var sprite = new Sprite(region, GameLayers.Beings);
+ sprite.CenterOrigin();
+ sprite.Color = EggTint;
+ return store.CreateEntity(
+ new Transform2D(
+ position,
+ scale: new Vector2(cellSize * EggSizeFraction / region.Width)
+ ),
+ sprite,
+ new Egg
+ {
+ Species = species,
+ Genome = genome,
+ Generation = generation,
+ IncubateDays = incubateDays,
+ }
+ );
+ }
+
/// Порог выраженности признака диеты, с которого вид реально ест данный корм.
public const float DietThreshold = 0.5f;
diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs
index 9fd739e..73acdcc 100644
--- a/src/LittleSim/Sim/AnimalOrganism.cs
+++ b/src/LittleSim/Sim/AnimalOrganism.cs
@@ -48,6 +48,9 @@ public struct AnimalPhenotype
/// Всеядность [0..1] — генералист: включает оба источника пищи (растения и мясо).
public float Omnivory;
+ /// Устойчивость к растительным ядам [0..1] — снижает дозу отравления токсичным кормом.
+ public float ToxinTolerance;
+
/// Продолжительность жизни (игровых дней).
public float Lifespan;
@@ -60,6 +63,12 @@ public struct AnimalPhenotype
/// Размер помёта.
public float LitterSize;
+ /// Тип рождения [0..1]: ≥0.5 — яйцекладущий (откладывает яйца), иначе живородящий.
+ public float EggLaying;
+
+ /// Яйцекладущий ли вид (тип рождения) — определяет цикл зачатия.
+ public readonly bool Oviparous => EggLaying >= 0.5f;
+
/// Собирает фенотип из карты признаков, посчитанной .
public static AnimalPhenotype FromTraits(IReadOnlyDictionary traits)
{
@@ -78,10 +87,12 @@ 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"),
LitterSize = T("litterSize"),
+ EggLaying = T("eggLaying"),
};
}
}
@@ -187,9 +198,11 @@ public sealed class HealthState
///
/// Пересчитывает боль (из хедифов) и способности: базовые из частей/крови/боли, затем модификаторы
- /// способностей от хедифов (лерп 1→factor по тяжести). Вызывать после изменения частей/крови/хедифов.
+ /// способностей от хедифов (лерп 1→factor по тяжести), затем общий множитель
+ /// (старение/сенесценс: дряхлеющий организм слабее по всем способностям). Вызывать после изменения
+ /// частей/крови/хедифов; =1 — без возрастного спада.
///
- public void RecomputeCapacities()
+ public void RecomputeCapacities(float capacityScale = 1f)
{
var pain = 0f;
foreach (var h in _hediffs)
@@ -214,14 +227,25 @@ public sealed class HealthState
}
}
+ if (capacityScale < 1f)
+ {
+ var scale = Math.Clamp(capacityScale, 0f, 1f);
+ foreach (var capacity in new List(caps.Keys))
+ {
+ caps[capacity] *= scale;
+ }
+ }
+
Capacities = caps;
}
///
/// Прогрессирует болезни (тяжесть и иммунитет растут со временем); снимает выздоровевшие
/// (иммунитет ≥ 1); возвращает true, если какая-то болезнь достигла летальной тяжести.
+ /// — множитель набора иммунитета (способность «фильтрация крови»:
+ /// почки/печень выводят токсины и помогают бороться с болезнью; повреждённые → медленнее).
///
- public bool AdvanceHediffs(float days)
+ public bool AdvanceHediffs(float days, float immunityScale = 1f)
{
var lethal = false;
for (var i = _hediffs.Count - 1; i >= 0; i--)
@@ -233,7 +257,7 @@ public sealed class HealthState
}
h.Severity += h.Def.SeverityPerDay * days;
- h.Immunity += h.Def.ImmunityPerDay * days;
+ h.Immunity += h.Def.ImmunityPerDay * days * immunityScale;
if (h.Immunity >= 1f)
{
_hediffs.RemoveAt(i); // иммунитет победил — выздоровление
@@ -440,6 +464,26 @@ public struct Corpse : IComponent
public float Meat;
}
+///
+/// Яйцо (тип рождения «яйцекладка»): отдельная сущность, отложенная яйцекладущей самкой вместо живых
+/// родов. Несёт уже скрещённый геном будущего детёныша, вид и поколение; инкубируется во времени и
+/// вылупляется в детёныша (). Геном — managed-ссылка (как у трупа/беременности).
+///
+public struct Egg : IComponent
+{
+ /// Индекс вида в (для спрайта при вылуплении и сейва).
+ public int Species;
+
+ /// Геном будущего детёныша (уже скрещён из родителей на момент кладки).
+ public Genome Genome;
+
+ /// Поколение будущего детёныша.
+ public int Generation;
+
+ /// Остаток инкубации в игровых днях; ≤0 — вылупление.
+ public float IncubateDays;
+}
+
/// Инстанс мысли настроения на особи (фаза A8): деф события + остаток времени до угасания.
public struct ThoughtInstance
{
diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs
index c51183f..90d2974 100644
--- a/src/LittleSim/Sim/AnimalSystems.cs
+++ b/src/LittleSim/Sim/AnimalSystems.cs
@@ -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
{
+ // Прибавка к обмену веществ при устойчивости к яду 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 _plants;
+ private readonly ArchetypeQuery _plants;
private readonly ArchetypeQuery _corpses;
public AnimalDecisionSystem(
@@ -264,7 +274,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
AnimalGrowth,
Transform2D
>();
- _plants = store.Query();
+ _plants = store.Query();
_corpses = store.Query();
}
@@ -320,20 +330,24 @@ public sealed class AnimalDecisionSystem : BaseSystem
brain.DecideIn = Interval + _rng.NextSingle() * Interval; // джиттер — расфазировать скан
var pos = t[i].Position;
var adult = AnimalFactory.IsAdult(_animalSet[o[i].Species], gr[i].Stage);
- var radius = VisionRadius(o[i].Species, o[i].Traits.Vision);
var self = entities.EntityAt(i);
+ var health = self.GetComponent().State;
// Эффективный интеллект (мозг × сознание) гейтит доступные нужды/действия (фаза A7):
// больной/раненый зверь с упавшим сознанием теряет высшие нужды (сон/секс) → рефлексы.
- var intelligence = AnimalFactory.Intelligence(
- o[i].Traits,
- self.GetComponent().State
- );
+ var intelligence = AnimalFactory.Intelligence(o[i].Traits, health);
+ // Радиус восприятия по способностям: зрение ищет корм/партнёра/добычу, угрозу замечаем
+ // зрением ИЛИ слухом (повреждённые глаза/уши сужают восприятие).
+ var baseRadius = VisionRadius(o[i].Species, o[i].Traits.Vision);
+ var sight = health?.Capacity(AnimalCapacities.Sight) ?? 1f;
+ var hearing = health?.Capacity(AnimalCapacities.Hearing) ?? 1f;
+ var seeRadius = baseRadius * MathF.Max(0.2f, sight);
+ var senseRadius = baseRadius * MathF.Max(0.2f, MathF.Max(sight, hearing));
// Настроение (A8): стресс/истощение (низкое настроение) подавляет тягу к спариванию.
var mood = self.GetComponent();
// Бегство (предатор-кластер): жертва, заметившая рядом хищника, спасается — это важнее
// любых нужд (инстинкт выживания). Сам хищник от добычи не бежит (см. IsThreatTo).
- if (TryFindThreat(pos, radius, o[i].Species, o[i].Traits, out var threatPos))
+ if (TryFindThreat(pos, senseRadius, o[i].Species, o[i].Traits, out var threatPos))
{
brain.Action = AnimalActions.Flee;
brain.TargetPlant = -1;
@@ -342,7 +356,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
var len = away.Length();
brain.Target =
len > 0.001f
- ? pos + away / len * (radius + _cellSize)
+ ? pos + away / len * (senseRadius + _cellSize)
: pos + new Vector2(_cellSize, 0f);
continue;
}
@@ -362,7 +376,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
when eatsMeat
&& TryFindKill(
pos,
- radius,
+ seeRadius,
o[i].Species,
o[i].Traits,
self.Id,
@@ -377,7 +391,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,
+ seeRadius,
+ intelligence,
+ o[i].Traits.ToxinTolerance,
+ out var plant,
+ out var pp
+ ):
brain.Action = AnimalActions.Eat;
brain.TargetPlant = plant;
brain.Target = pp;
@@ -397,7 +419,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
&& TryFindMate(
pos,
o[i].IsMale,
- radius,
+ seeRadius,
out var mateId,
out var matePos
):
@@ -586,12 +608,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 +656,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)
{
@@ -696,6 +790,7 @@ public sealed class AnimalActionSystem : BaseSystem
private const float GrazeKillAge = 1f; // трава с возрастом ниже этого после выедания исчезает
private const float ThirstReliefBelow = 0.15f; // ниже этого жажда — питьё даёт мысль «напилась»
private const float FleeSpeedMult = 1.2f; // страх подгоняет жертву чуть быстрее обычного шага
+ private const float PostReproductiveFrac = 0.9f; // доля жизни, после которой самка уже не зачинает
private readonly EntityStore _store;
private readonly PlantSet _plants;
@@ -707,6 +802,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 +827,7 @@ public sealed class AnimalActionSystem : BaseSystem
int cellSize,
RectF bounds,
HediffDef? bleeding,
+ HediffDef? poisoned,
int seed
)
{
@@ -744,6 +841,7 @@ public sealed class AnimalActionSystem : BaseSystem
_cellSize = cellSize;
_bounds = bounds;
_bleeding = bleeding;
+ _poisoned = poisoned;
_rng = new Random(seed);
_query = store.Query();
}
@@ -780,6 +878,10 @@ public sealed class AnimalActionSystem : BaseSystem
// Способность Moving (падает от ран/болезней) — множитель к скорости: больные медленнее.
var moving = hh[i].State.Capacity(AnimalCapacities.Moving);
var speed = def.BaseSpeed * MathF.Max(0.2f, o[i].Traits.MoveSpeed) * moving;
+ // Питание (челюсть) масштабирует укус, пищеварение (желудок) — усвоение: их произведение
+ // даёт сытость с корма; раненая челюсть/больной желудок → зверь голоднее (опция 1).
+ var eating = hh[i].State.Capacity(AnimalCapacities.Eating);
+ var foodEff = eating * hh[i].State.Capacity(AnimalCapacities.Digestion);
switch (brain.Action)
{
@@ -790,8 +892,19 @@ public sealed class AnimalActionSystem : BaseSystem
case AnimalActions.Eat:
if (MoveTo(ref pos, brain.Target, speed * seconds))
{
- Refill(values, AnimalActions.Eat, days);
- Graze(brain.TargetPlant, days, def.ForageBiteDays);
+ Refill(values, AnimalActions.Eat, days, foodEff);
+ Graze(
+ brain.TargetPlant,
+ days,
+ def.ForageBiteDays * MathF.Max(0.2f, eating)
+ );
+ // Коэволюция: защита растения бьёт по поедателю (яд/шипы).
+ ApplyPlantDefense(
+ brain.TargetPlant,
+ hh[i].State,
+ o[i].Traits.ToxinTolerance,
+ days
+ );
}
break;
@@ -836,7 +949,7 @@ public sealed class AnimalActionSystem : BaseSystem
break;
case AnimalActions.Hunt:
- Hunt(ref brain, ref pos, values, def, speed * seconds, days);
+ Hunt(ref brain, ref pos, values, def, speed * seconds, days, foodEff);
break;
case AnimalActions.Flee:
@@ -883,7 +996,8 @@ public sealed class AnimalActionSystem : BaseSystem
float[] values,
AnimalDef def,
float step,
- float days
+ float days,
+ float foodEff
)
{
if (
@@ -914,7 +1028,7 @@ public sealed class AnimalActionSystem : BaseSystem
}
ref var corpse = ref target.GetComponent();
- Refill(values, AnimalActions.Eat, days);
+ Refill(values, AnimalActions.Eat, days, foodEff);
corpse.Meat -= def.ForageBiteDays * days; // расход мяса той же «силой укуса», что и выедание
if (corpse.Meat <= 0f && !_consumed.Contains(target))
{
@@ -961,8 +1075,9 @@ public sealed class AnimalActionSystem : BaseSystem
}
}
- // Восполняет нужду, которую утоляет действие (по NeedSet), на её FeedPerDay.
- private void Refill(float[] values, string action, float days)
+ // Восполняет нужду, которую утоляет действие (по NeedSet), на её FeedPerDay × эффективность
+ // (питание/пищеварение для еды — слабая челюсть/больной желудок дают меньше сытости с того же корма).
+ private void Refill(float[] values, string action, float days, float efficiency = 1f)
{
var index = _needs.NeedForAction(action);
if (index < 0 || values is null || index >= values.Length)
@@ -970,7 +1085,11 @@ public sealed class AnimalActionSystem : BaseSystem
return;
}
- values[index] = Math.Clamp(values[index] + _needs[index].FeedPerDay * days, 0f, 1f);
+ values[index] = Math.Clamp(
+ values[index] + _needs[index].FeedPerDay * days * efficiency,
+ 0f,
+ 1f
+ );
}
// Двигает позицию к цели на step; возвращает true, если уже у цели (можно исполнять действие).
@@ -1010,6 +1129,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()
+ )
+ {
+ return;
+ }
+
+ ref readonly var traits = ref plant.GetComponent().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()
@@ -1039,14 +1217,24 @@ public sealed class AnimalActionSystem : BaseSystem
if (!mother.HasComponent())
{
ref readonly var mom = ref mother.GetComponent();
- mother.AddComponent(
- new Pregnant
- {
- FatherGenome = father.GetComponent().Genome,
- DueInDays = MathF.Max(1f, mom.Traits.GestationDays),
- Litter = Math.Max(1, (int)MathF.Round(mom.Traits.LitterSize)),
- }
- );
+ // Плодовитость падает со старостью (тот же фактор сенесценса): дряхлая самка приносит
+ // меньший помёт, а после порога пострепродуктивного возраста уже не зачинает.
+ var momAge = mother.HasComponent()
+ ? mother.GetComponent().AgeDays
+ : 0f;
+ var ageFrac = mom.Traits.Lifespan > 0f ? momAge / mom.Traits.Lifespan : 0f;
+ if (ageFrac <= PostReproductiveFrac)
+ {
+ var fert = AnimalHealthSystem.SenescenceFactor(momAge, mom.Traits.Lifespan);
+ mother.AddComponent(
+ new Pregnant
+ {
+ FatherGenome = father.GetComponent().Genome,
+ DueInDays = MathF.Max(1f, mom.Traits.GestationDays),
+ Litter = Math.Max(1, (int)MathF.Round(mom.Traits.LitterSize * fert)),
+ }
+ );
+ }
}
ResetMating(a, mateNeed);
@@ -1468,6 +1656,8 @@ public sealed class AnimalPregnancySystem : BaseSystem
Litter = preg.Litter,
Generation = o[i].Generation + 1,
Position = t[i].Position,
+ Oviparous = o[i].Traits.Oviparous,
+ IncubateDays = MathF.Max(1f, o[i].Traits.GestationDays),
}
);
}
@@ -1497,17 +1687,35 @@ public sealed class AnimalPregnancySystem : BaseSystem
);
var offset =
new Vector2(_rng.NextSingle() - 0.5f, _rng.NextSingle() - 0.5f) * _cellSize;
- AnimalFactory.Create(
- _store,
- _animals,
- _needs,
- birth.Species,
- birth.Position + offset,
- ageDays: 0f,
- child,
- birth.Generation,
- _cellSize
- );
+ if (birth.Oviparous)
+ {
+ // Яйцекладка: вместо живого детёныша откладываем яйцо с его геномом — вылупится позже.
+ AnimalFactory.CreateEgg(
+ _store,
+ _animals,
+ birth.Species,
+ birth.Position + offset,
+ child,
+ birth.Generation,
+ birth.IncubateDays,
+ _cellSize
+ );
+ }
+ else
+ {
+ AnimalFactory.Create(
+ _store,
+ _animals,
+ _needs,
+ birth.Species,
+ birth.Position + offset,
+ ageDays: 0f,
+ child,
+ birth.Generation,
+ _cellSize
+ );
+ }
+
count++;
}
}
@@ -1522,6 +1730,90 @@ public sealed class AnimalPregnancySystem : BaseSystem
public int Litter;
public int Generation;
public Vector2 Position;
+ public bool Oviparous;
+ public float IncubateDays;
+ }
+}
+
+///
+/// Инкубация яиц (тип рождения «яйцекладка»): тикает остаток инкубации каждого яйца; по истечении
+/// вылупляет детёныша ( из хранимого генома, стадия Baby) рядом с
+/// яйцом и удаляет яйцо. Зеркало родов для яйцекладущих видов.
+///
+public sealed class EggSystem : BaseSystem
+{
+ private readonly EntityStore _store;
+ private readonly AnimalSet _animals;
+ private readonly NeedSet _needs;
+ private readonly GameClock _clock;
+ private readonly float _secondsPerDay;
+ private readonly int _cellSize;
+ private readonly Random _rng;
+ private readonly ArchetypeQuery _query;
+ private readonly List _hatched = [];
+
+ public EggSystem(
+ EntityStore store,
+ AnimalSet animals,
+ NeedSet needs,
+ GameClock clock,
+ float secondsPerDay,
+ int cellSize,
+ int seed
+ )
+ {
+ _store = store;
+ _animals = animals;
+ _needs = needs;
+ _clock = clock;
+ _secondsPerDay = secondsPerDay;
+ _cellSize = cellSize;
+ _rng = new Random(seed);
+ _query = store.Query();
+ }
+
+ protected override void OnUpdateGroup()
+ {
+ var days = _clock.DeltaTime / _secondsPerDay;
+ if (days <= 0f)
+ {
+ return;
+ }
+
+ _hatched.Clear();
+ foreach (var (eggs, transforms, entities) in _query.Chunks)
+ {
+ var e = eggs.Span;
+ for (var i = 0; i < e.Length; i++)
+ {
+ ref var egg = ref e[i];
+ egg.IncubateDays -= days;
+ if (egg.IncubateDays > 0f)
+ {
+ continue;
+ }
+
+ var offset =
+ new Vector2(_rng.NextSingle() - 0.5f, _rng.NextSingle() - 0.5f) * _cellSize;
+ AnimalFactory.Create(
+ _store,
+ _animals,
+ _needs,
+ egg.Species,
+ transforms.Span[i].Position + offset,
+ ageDays: 0f,
+ egg.Genome,
+ egg.Generation,
+ _cellSize
+ );
+ _hatched.Add(entities.EntityAt(i));
+ }
+ }
+
+ foreach (var egg in _hatched)
+ {
+ egg.DeleteEntity();
+ }
}
}
@@ -1543,10 +1835,15 @@ public sealed class AnimalHealthSystem : BaseSystem
private readonly float _secondsPerDay;
private readonly HediffDef[] _ambient;
private readonly Random _rng;
- private readonly ArchetypeQuery _query;
+ private readonly ArchetypeQuery _query;
private readonly List _deaths = [];
private float _accum;
+ // Сенесценс (старение): спад способностей начинается с этой доли продолжительности жизни и достигает
+ // максимума к её концу. Дряхлеющий зверь слабее (медленнее, хуже видит/фильтрует кровь) и уязвимее.
+ private const float SenescenceStart = 0.6f;
+ private const float SenescenceMax = 0.45f;
+
public AnimalHealthSystem(
EntityStore store,
AnimalSet animals,
@@ -1564,7 +1861,19 @@ public sealed class AnimalHealthSystem : BaseSystem
_secondsPerDay = secondsPerDay;
_ambient = ambient;
_rng = new Random(seed);
- _query = store.Query();
+ _query = store.Query();
+ }
+
+ /// Множитель способностей от старения (1 — молод/расцвет, <1 — дряхлеет к концу жизни).
+ public static float SenescenceFactor(float ageDays, float lifespan)
+ {
+ if (lifespan <= 0f)
+ {
+ return 1f;
+ }
+
+ var frac = (ageDays / lifespan - SenescenceStart) / MathF.Max(0.01f, 1f - SenescenceStart);
+ return 1f - SenescenceMax * Math.Clamp(frac, 0f, 1f);
}
protected override void OnUpdateGroup()
@@ -1579,9 +1888,11 @@ public sealed class AnimalHealthSystem : BaseSystem
_accum = 0f;
_deaths.Clear();
- foreach (var (healths, organisms, entities) in _query.Chunks)
+ foreach (var (healths, organisms, growths, entities) in _query.Chunks)
{
var h = healths.Span;
+ var o = organisms.Span;
+ var g = growths.Span;
for (var i = 0; i < h.Length; i++)
{
var state = h[i].State;
@@ -1601,7 +1912,12 @@ public sealed class AnimalHealthSystem : BaseSystem
}
}
- var lethal = state.AdvanceHediffs(days);
+ // Фильтрация крови (почки/печень) ускоряет вывод токсинов и набор иммунитета: повреждённый
+ // орган → медленнее выздоровление от яда/болезни (для тела без неё Capacity вернёт 1).
+ var lethal = state.AdvanceHediffs(
+ days,
+ state.Capacity(AnimalCapacities.BloodFiltration)
+ );
// Кровопотеря от ран (кровотечения), иначе — постепенное восстановление крови в покое.
var bleed = 0f;
foreach (var hd in state.Hediffs)
@@ -1622,7 +1938,9 @@ public sealed class AnimalHealthSystem : BaseSystem
);
}
- state.RecomputeCapacities();
+ // Старение: к концу жизни способности дряхлеют (медленнее/слабее, уязвимее к ранам/болезни).
+ var senescence = SenescenceFactor(g[i].AgeDays, o[i].Traits.Lifespan);
+ state.RecomputeCapacities(senescence);
if (
lethal
|| state.BloodLevel <= 0f
diff --git a/src/LittleSim/Sim/CapacityCalc.cs b/src/LittleSim/Sim/CapacityCalc.cs
index 87a1ed9..2e2af92 100644
--- a/src/LittleSim/Sim/CapacityCalc.cs
+++ b/src/LittleSim/Sim/CapacityCalc.cs
@@ -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";
+
+ /// Порядок отображения способностей в UI (сверху вниз). Прочие модовые — после, как есть.
+ public static readonly string[] DisplayOrder =
+ [
+ Consciousness,
+ Moving,
+ Sight,
+ Hearing,
+ Talking,
+ Eating,
+ Breathing,
+ BloodPumping,
+ BloodFiltration,
+ Digestion,
+ ];
}
///
@@ -24,7 +43,11 @@ public static class CapacityCalc
/// Считает карту способностей из инстансов частей, уровня крови (0..1) и боли (0..1).
public static Dictionary Compute(PartInstance[] parts, float blood, float pain)
{
+ // Сырой вклад каждой способности (Σ доля·HP) и НАБОР объявленных телом способностей: вид имеет
+ // только те, к которым его части вообще причастны (у оленя нет манипуляции). Объявленность —
+ // статична (по дефам частей), поэтому уничтоженный орган всё равно покажет способность на 0%.
var raw = new Dictionary(System.StringComparer.Ordinal);
+ var declared = new HashSet(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(System.StringComparer.Ordinal)
+ // Производные способности зависят от сознания (и кровоснабжения для движения/пищеварения).
+ var computed = new Dictionary(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(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;
diff --git a/src/LittleSim/Sim/PlantGrowthSystem.cs b/src/LittleSim/Sim/PlantGrowthSystem.cs
index 33d1f21..28e5a63 100644
--- a/src/LittleSim/Sim/PlantGrowthSystem.cs
+++ b/src/LittleSim/Sim/PlantGrowthSystem.cs
@@ -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(
diff --git a/src/LittleSim/Sim/PlantOrganism.cs b/src/LittleSim/Sim/PlantOrganism.cs
index b6e563b..8860ce4 100644
--- a/src/LittleSim/Sim/PlantOrganism.cs
+++ b/src/LittleSim/Sim/PlantOrganism.cs
@@ -32,6 +32,16 @@ public struct PlantPhenotype
public float HarvestAmount;
public float LeafHue;
+ // --- Защита (коэволюция с травоядными) ---
+ /// Токсичность [0..1]: отравляет поедателя; цена — замедление роста.
+ public float Toxicity;
+
+ /// Шипы [0..1]: травмируют поедателя; цена — замедление роста.
+ public float Thorns;
+
+ /// Вкусность [0..1]: обратная привлекательность для умных травоядных (избегание в C2).
+ public float Palatability;
+
/// Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.
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;
+
+ /// Множитель скорости роста с учётом цены защиты (1 — без защиты, ≥ MinGrowthFactor).
+ public readonly float DefenseGrowthFactor() =>
+ Math.Clamp(
+ 1f - ToxicityGrowthCost * Toxicity - ThornsGrowthCost * Thorns,
+ MinGrowthFactor,
+ 1f
+ );
}
///
diff --git a/src/LittleSim/UI/InspectPanel.cs b/src/LittleSim/UI/InspectPanel.cs
index 7d22afe..a397a85 100644
--- a/src/LittleSim/UI/InspectPanel.cs
+++ b/src/LittleSim/UI/InspectPanel.cs
@@ -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(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. (cap.consciousness, cap.bloodpumping…).
+ private static string CapacityLabel(string capId) => "cap." + capId.ToLowerInvariant();
+
private void AppendCapacity(
StringBuilder text,
HealthState state,