diff --git a/Mods/Core/Defs/Animals/Animals.json b/Mods/Core/Defs/Animals/Animals.json
index 0b10d67..8ace26b 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,8 @@
"GeneLitterSize": 5,
"GeneHerbivory": 0.4,
"GeneCarnivory": 0.2,
- "GeneOmnivory": 0.85
+ "GeneOmnivory": 0.85,
+ "GeneToxinTolerance": 0.25
}
}
]
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..a5c05fa 100644
--- a/Mods/Core/Defs/Genes/Animal.json
+++ b/Mods/Core/Defs/Genes/Animal.json
@@ -46,6 +46,13 @@
"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.
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..57f6032 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",
diff --git a/Mods/Core/Languages/ru/ui.json b/Mods/Core/Languages/ru/ui.json
index 0c6d660..00b9a3e 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",
diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs
index 436545e..43c6637 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/): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.
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..01bd96d 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
)
);
@@ -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()
+ .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/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs
index 9fd739e..e8a537a 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;
@@ -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"),
diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs
index c51183f..c7f24b2 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();
}
@@ -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();
}
@@ -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()
+ )
+ {
+ 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()
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,