Merge branch 'coevolution-health' into main

Plant–herbivore coevolution (toxin/thorns ↔ tolerance, smart avoidance, costs),
RimWorld-style health capacities wired to behaviour, senescence, and birth-type
genes (live-bearing vs egg-laying with eggs) + hybrid-creation foundation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-14 16:44:17 +03:00
co-authored by Claude Opus 4.8
20 changed files with 877 additions and 76 deletions
+34 -2
View File
@@ -30,7 +30,8 @@
"GeneBreedingSeason": 2, "GeneBreedingSeason": 2,
"GeneGestationDays": 30, "GeneGestationDays": 30,
"GeneLitterSize": 1, "GeneLitterSize": 1,
"GeneHerbivory": 1.0 "GeneHerbivory": 1.0,
"GeneToxinTolerance": 0.1
} }
}, },
@@ -97,7 +98,38 @@
"GeneLitterSize": 5, "GeneLitterSize": 5,
"GeneHerbivory": 0.4, "GeneHerbivory": 0.4,
"GeneCarnivory": 0.2, "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
} }
} }
] ]
+16 -3
View File
@@ -7,23 +7,36 @@
{ {
"defName": "Quadruped", "defName": "Quadruped",
"parts": [ "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, { "name": "heart", "parent": "torso", "coverage": 0.02, "maxHp": 12, "vital": true,
"capacities": { "BloodPumping": 1.0 } }, "capacities": { "BloodPumping": 1.0 } },
{ "name": "lungLeft", "parent": "torso", "coverage": 0.03, "maxHp": 12, { "name": "lungLeft", "parent": "torso", "coverage": 0.03, "maxHp": 12,
"capacities": { "Breathing": 0.5 } }, "capacities": { "Breathing": 0.5 } },
{ "name": "lungRight", "parent": "torso", "coverage": 0.03, "maxHp": 12, { "name": "lungRight", "parent": "torso", "coverage": 0.03, "maxHp": 12,
"capacities": { "Breathing": 0.5 } }, "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, { "name": "stomach", "parent": "torso", "coverage": 0.03, "maxHp": 12,
"capacities": { "Digestion": 1.0 } }, "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, { "name": "brain", "parent": "head", "coverage": 0.02, "maxHp": 12, "vital": true,
"capacities": { "Consciousness": 1.0 } }, "capacities": { "Consciousness": 1.0 } },
{ "name": "eyeLeft", "parent": "head", "coverage": 0.015, "maxHp": 8, { "name": "eyeLeft", "parent": "head", "coverage": 0.015, "maxHp": 8,
"capacities": { "Sight": 0.5 } }, "capacities": { "Sight": 0.5 } },
{ "name": "eyeRight", "parent": "head", "coverage": 0.015, "maxHp": 8, { "name": "eyeRight", "parent": "head", "coverage": 0.015, "maxHp": 8,
"capacities": { "Sight": 0.5 } }, "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": "legFrontLeft", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } },
{ "name": "legFrontRight", "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 } }, { "name": "legBackLeft", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } },
+14
View File
@@ -46,12 +46,26 @@
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"], "default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"],
"effects": { "omnivory": "value" } }, "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. Эффекта нет — пол читается из // Пол как локус (фаза A3): аллели {X=0, Y=1}; самка XX, самец XY. Эффекта нет — пол читается из
// самих аллелей (наличие Y), не из выраженного значения. mutationChance 0 (X не мутирует в Y). // самих аллелей (наличие Y), не из выраженного значения. mutationChance 0 (X не мутирует в Y).
// Генерация особи-основателя задаёт пол явно (XX или XY), чтобы не возник невозможный YY. // Генерация особи-основателя задаёт пол явно (XX или XY), чтобы не возник невозможный YY.
{ "defName": "GeneSex", "kind": "Discrete", "label": "gene.sex", { "defName": "GeneSex", "kind": "Discrete", "label": "gene.sex",
"variants": 2, "variantWeights": [0.5, 0.5], "mutationChance": 0, "tags": ["animal", "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 зима), срок вынашивания и помёт. // Размножение (фаза A4): сезон гона фиксирован по виду (0 весна … 3 зима), срок вынашивания и помёт.
{ "defName": "GeneBreedingSeason", "parent": "BaseNumericGene", "label": "gene.breedingSeason", { "defName": "GeneBreedingSeason", "parent": "BaseNumericGene", "label": "gene.breedingSeason",
"default": 2, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["animal", "reproduction"], "default": 2, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["animal", "reproduction"],
+15
View File
@@ -15,6 +15,21 @@
"default": 0.33, "min": 0, "max": 1, "spread": 0.04, "tags": ["morphology", "color"], "default": 0.33, "min": 0, "max": 1, "spread": 0.04, "tags": ["morphology", "color"],
"effects": { "leafHue": "value" } }, "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). Собственное значение гена не используется. // (демонстрация gsom-функций фазы G5). Собственное значение гена не используется.
{ "defName": "GeneHardiness", "parent": "BaseNumericGene", "label": "gene.hardiness", { "defName": "GeneHardiness", "parent": "BaseNumericGene", "label": "gene.hardiness",
+10
View File
@@ -22,6 +22,16 @@
"defName": "Bleeding", "label": "hediff.bleeding", "defName": "Bleeding", "label": "hediff.bleeding",
"initialSeverity": 0.3, "immunityPerDay": 0.5, "bloodLossPerDay": 0.8, "initialSeverity": 0.3, "immunityPerDay": 0.5, "bloodLossPerDay": 0.8,
"pain": 0.25, "capMods": { "Moving": 0.92, "Consciousness": 0.95 } "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 }
} }
] ]
} }
+4 -2
View File
@@ -66,7 +66,8 @@
"optimalFertility": 0.4, "fertilityTolerance": 0.5, "vigor": 0.5, "optimalFertility": 0.4, "fertilityTolerance": 0.5, "vigor": 0.5,
"lifespan": 300, "dispersalRange": 2, "reproduceInterval": 25, "selfPollination": 0.6, "lifespan": 300, "dispersalRange": 2, "reproduceInterval": 25, "selfPollination": 0.6,
"mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08, "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": [ "stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 5, "label": "plant.stage.sprout" }, { "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 5, "label": "plant.stage.sprout" },
{ "label": "plant.stage.mature" } { "label": "plant.stage.mature" }
@@ -81,7 +82,8 @@
"optimalFertility": 1.6, "fertilityTolerance": 1.1, "vigor": 1.2, "optimalFertility": 1.6, "fertilityTolerance": 1.1, "vigor": 1.2,
"lifespan": 25, "dispersalRange": 3, "reproduceInterval": 4, "selfPollination": 0.9, "lifespan": 25, "dispersalRange": 3, "reproduceInterval": 4, "selfPollination": 0.9,
"mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1, "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": [ "stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.4, "growDays": 2, "label": "plant.stage.sprout" }, { "texture": "things/plant/seed_default", "sizeCells": 0.4, "growDays": 2, "label": "plant.stage.sprout" },
{ "label": "plant.stage.mature" } { "label": "plant.stage.mature" }
+10
View File
@@ -30,6 +30,7 @@
"inspect.gene.repro": "Dispersal {0:0.0} · interval {1:0.0} d", "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.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.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.gene.variant": "Morph: recessive variant",
"inspect.harvest": "Harvest: {0} ×{1:0.#}", "inspect.harvest": "Harvest: {0} ×{1:0.#}",
"inspect.fruit": "Fruit: {0} ×{1:0.#} ({2}) · ripe {3:0.#}", "inspect.fruit": "Fruit: {0} ×{1:0.#} ({2}) · ripe {3:0.#}",
@@ -61,9 +62,17 @@
"cap.consciousness": "consciousness", "cap.consciousness": "consciousness",
"cap.moving": "moving", "cap.moving": "moving",
"cap.sight": "sight", "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.rut": "rut",
"hediff.fever": "fever", "hediff.fever": "fever",
"hediff.bleeding": "bleeding", "hediff.bleeding": "bleeding",
"hediff.poisoned": "poisoned",
"inspect.hint": "LMB — select · RMB/Esc — clear", "inspect.hint": "LMB — select · RMB/Esc — clear",
"hud.paused": "PAUSED", "hud.paused": "PAUSED",
"menu.title": "LittleSim", "menu.title": "LittleSim",
@@ -154,6 +163,7 @@
"pawn.hare": "hare", "pawn.hare": "hare",
"pawn.boar": "boar", "pawn.boar": "boar",
"pawn.wolf": "wolf", "pawn.wolf": "wolf",
"pawn.chicken": "chicken",
"pawn.muffalo": "muffalo", "pawn.muffalo": "muffalo",
"pawn.squirrel": "squirrel", "pawn.squirrel": "squirrel",
"thought.gaveBirth": "gave birth", "thought.gaveBirth": "gave birth",
+10
View File
@@ -30,6 +30,7 @@
"inspect.gene.repro": "Расселение {0:0.0} кл · период {1:0.0} дн", "inspect.gene.repro": "Расселение {0:0.0} кл · период {1:0.0} дн",
"inspect.gene.repro2": "Самоопыление {0:0.00} · мутации {1:0.00}", "inspect.gene.repro2": "Самоопыление {0:0.00} · мутации {1:0.00}",
"inspect.gene.hardy": "Морозо/жаро {0:0}/{1:0} · оттенок {2: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.gene.variant": "Морфа: рецессивный вариант",
"inspect.harvest": "Сбор: {0} ×{1:0.#}", "inspect.harvest": "Сбор: {0} ×{1:0.#}",
"inspect.fruit": "Плоды: {0} ×{1:0.#} ({2}) · зрелых {3:0.#}", "inspect.fruit": "Плоды: {0} ×{1:0.#} ({2}) · зрелых {3:0.#}",
@@ -61,9 +62,17 @@
"cap.consciousness": "сознание", "cap.consciousness": "сознание",
"cap.moving": "движение", "cap.moving": "движение",
"cap.sight": "зрение", "cap.sight": "зрение",
"cap.hearing": "слух",
"cap.talking": "речь",
"cap.eating": "питание",
"cap.breathing": "дыхание",
"cap.bloodpumping": "кровоснабжение",
"cap.bloodfiltration": "фильтрация крови",
"cap.digestion": "пищеварение",
"hediff.rut": "гон", "hediff.rut": "гон",
"hediff.fever": "лихорадка", "hediff.fever": "лихорадка",
"hediff.bleeding": "кровотечение", "hediff.bleeding": "кровотечение",
"hediff.poisoned": "отравление",
"inspect.hint": "ЛКМ — выбрать · ПКМ/Esc — снять", "inspect.hint": "ЛКМ — выбрать · ПКМ/Esc — снять",
"hud.paused": "ПАУЗА", "hud.paused": "ПАУЗА",
"menu.title": "LittleSim", "menu.title": "LittleSim",
@@ -154,6 +163,7 @@
"pawn.hare": "заяц", "pawn.hare": "заяц",
"pawn.boar": "кабан", "pawn.boar": "кабан",
"pawn.wolf": "волк", "pawn.wolf": "волк",
"pawn.chicken": "курица",
"pawn.muffalo": "муффало", "pawn.muffalo": "муффало",
"pawn.squirrel": "белка", "pawn.squirrel": "белка",
"thought.gaveBirth": "родила потомство", "thought.gaveBirth": "родила потомство",
+25
View File
@@ -173,6 +173,28 @@ public sealed class CorpseSave
public float Meat { get; set; } public float Meat { get; set; }
} }
/// <summary>Сериализуемое яйцо (тип рождения «яйцекладка»): вид, позиция, геном детёныша, поколение, инкубация.</summary>
public sealed class EggSave
{
/// <summary>Имя дефа вида — по нему берётся индекс/спрайт/тело при вылуплении.</summary>
public string Species { get; set; } = "";
/// <summary>Позиция X в мировых координатах.</summary>
public float X { get; set; }
/// <summary>Позиция Y в мировых координатах.</summary>
public float Y { get; set; }
/// <summary>Поколение будущего детёныша.</summary>
public int Generation { get; set; }
/// <summary>Остаток инкубации в игровых днях.</summary>
public float IncubateDays { get; set; }
/// <summary>Геном будущего детёныша (уже скрещён на момент кладки).</summary>
public Dictionary<string, Allele> Genome { get; set; } = new();
}
/// <summary> /// <summary>
/// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание), /// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание),
/// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он /// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он
@@ -216,6 +238,9 @@ public sealed class WorldSave
/// <summary>Снимок трупов (вид, позиция, таймер/стадия разложения, мясо).</summary> /// <summary>Снимок трупов (вид, позиция, таймер/стадия разложения, мясо).</summary>
public List<CorpseSave> Corpses { get; set; } = []; public List<CorpseSave> Corpses { get; set; } = [];
/// <summary>Снимок яиц (вид, позиция, геном детёныша, поколение, инкубация).</summary>
public List<EggSave> Eggs { get; set; } = [];
/// <summary>Конфиг мира для пересоздания сцены.</summary> /// <summary>Конфиг мира для пересоздания сцены.</summary>
public WorldConfig ToConfig() => public WorldConfig ToConfig() =>
new() new()
+69 -4
View File
@@ -30,6 +30,9 @@ public sealed class AnimalSet
/// <summary>Спрайт трупа (fallback — самка).</summary> /// <summary>Спрайт трупа (fallback — самка).</summary>
public required Texture2DRegion Corpse { get; init; } public required Texture2DRegion Corpse { get; init; }
/// <summary>Спрайт яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт-точка.</summary>
public required Texture2DRegion Egg { get; init; }
/// <summary>Стадии роста (из дефа или встроенный дефолт), по возрастанию возраста входа.</summary> /// <summary>Стадии роста (из дефа или встроенный дефолт), по возрастанию возраста входа.</summary>
public required AnimalStageDef[] Stages { get; init; } public required AnimalStageDef[] Stages { get; init; }
@@ -43,10 +46,40 @@ public sealed class AnimalSet
// Встроенный набор стадий по умолчанию (если вид не задал свой): как было до выноса в данные. // Встроенный набор стадий по умолчанию (если вид не задал свой): как было до выноса в данные.
private static readonly AnimalStageDef[] DefaultStages = private static readonly AnimalStageDef[] DefaultStages =
[ [
new() { Name = "Baby", EnterAt = 0f, RelativeTo = "maturity", Scale = 0.45f, Texture = "baby" }, new()
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 }, Name = "Baby",
new() { Name = "Senior", EnterAt = 0.8f, RelativeTo = "lifespan", Scale = 0.92f, Texture = "adult", Adult = true }, 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; private readonly Species[] _species;
@@ -82,6 +115,9 @@ public sealed class AnimalSet
Corpse = string.IsNullOrEmpty(def.CorpseTexture) Corpse = string.IsNullOrEmpty(def.CorpseTexture)
? female ? female
: atlases.GetRegion(device, def.CorpseTexture), : atlases.GetRegion(device, def.CorpseTexture),
Egg = string.IsNullOrEmpty(def.EggTexture)
? female
: atlases.GetRegion(device, def.EggTexture),
Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages, Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages,
Body = content.Defs.TryGet<BodyDef>(def.Body, out var body) ? body : null, Body = content.Defs.TryGet<BodyDef>(def.Body, out var body) ? body : null,
Template = BuildTemplate(def.Genome, genes), Template = BuildTemplate(def.Genome, genes),
@@ -111,6 +147,35 @@ public sealed class AnimalSet
return genome; return genome;
} }
/// <summary>
/// ОСНОВА создания гибрида (полноценная команда/UI — позже): геном помеси, привязанный к базовому виду
/// <paramref name="baseSpecies"/> (его тело/спрайт/набор генов), но с общими генами, скрещёнными с
/// <paramref name="otherSpecies"/> (по одной аллели-гамете от каждого родителя, как при размножении).
/// Пол берётся от базового вида; гены, которых нет у базового, игнорируются (его тело их не читает).
/// Сущность создаётся обычной <see cref="Sim.AnimalFactory.Create"/> с этим геномом и базовым видом —
/// то есть «создать любой вид» = Create(вид, GenerateGenome), «любой гибрид» = Create(вид, HybridGenome).
/// </summary>
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: разброс берётся из // Строит шаблон генома вида из ДАННЫХ (geneId→base из Defs/Animals/) generically: разброс берётся из
// самого гена. Состав открыт — модер добавляет ген строкой JSON, без правки кода. Дискретные гены // самого гена. Состав открыт — модер добавляет ген строкой JSON, без правки кода. Дискретные гены
// (база/разброс не нужны — варианты из GeneDef) тоже поддержаны. // (база/разброс не нужны — варианты из GeneDef) тоже поддержаны.
+12
View File
@@ -143,6 +143,15 @@ public sealed class GenomeDef
/// <summary>Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета.</summary> /// <summary>Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета.</summary>
public float LeafHue { get; init; } = 0.33f; public float LeafHue { get; init; } = 0.33f;
/// <summary>Токсичность (0..1): отравляет поедателя (коэволюция); тормозит рост (цена защиты).</summary>
public float Toxicity { get; init; }
/// <summary>Шипы (0..1): травмируют поедателя; тормозят рост (цена защиты).</summary>
public float Thorns { get; init; }
/// <summary>Вкусность (0..1): обратная привлекательность для умных травоядных (избегание в C2).</summary>
public float Palatability { get; init; } = 1f;
} }
/// <summary>Растение (Defs/Plants/): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary> /// <summary>Растение (Defs/Plants/): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
@@ -252,6 +261,9 @@ public sealed class AnimalDef : PawnDef
/// <summary>Текстура трупа (фаза A5); пусто — берётся <see cref="PawnDef.Texture"/>.</summary> /// <summary>Текстура трупа (фаза A5); пусто — берётся <see cref="PawnDef.Texture"/>.</summary>
public string CorpseTexture { get; init; } = ""; public string CorpseTexture { get; init; } = "";
/// <summary>Текстура яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт семени-точки.</summary>
public string EggTexture { get; init; } = "things/plant/seed_default";
/// <summary>Имя <see cref="BodyDef"/> — анатомия вида (части тела/органы); пусто — без частей тела.</summary> /// <summary>Имя <see cref="BodyDef"/> — анатомия вида (части тела/органы); пусто — без частей тела.</summary>
public string Body { get; init; } = ""; public string Body { get; init; } = "";
+4
View File
@@ -118,6 +118,10 @@ public sealed class PlantSet
new GenomeTemplate.Entry(Gene("GeneFruitSeason"), g.FruitSeason, 0f), // сезон фиксирован по виду new GenomeTemplate.Entry(Gene("GeneFruitSeason"), g.FruitSeason, 0f), // сезон фиксирован по виду
Numeric("GeneHarvestAmount", g.HarvestAmount), Numeric("GeneHarvestAmount", g.HarvestAmount),
new GenomeTemplate.Entry(Gene("GeneLeafHue"), g.LeafHue, 0.04f), new GenomeTemplate.Entry(Gene("GeneLeafHue"), g.LeafHue, 0.04f),
// Защита (коэволюция с травоядными): токсичность/шипы/вкусность.
Numeric("GeneToxicity", g.Toxicity),
Numeric("GeneThorns", g.Thorns),
Numeric("GenePalatability", g.Palatability),
new GenomeTemplate.Entry( new GenomeTemplate.Entry(
Gene("GeneMorph"), Gene("GeneMorph"),
0f, 0f,
+98 -3
View File
@@ -260,6 +260,7 @@ public sealed class WorldScene : Scene
) )
); );
var bleeding = content.Defs.TryGet<HediffDef>("Bleeding", out var bl) ? bl : null; var bleeding = content.Defs.TryGet<HediffDef>("Bleeding", out var bl) ? bl : null;
var poisoned = content.Defs.TryGet<HediffDef>("Poisoned", out var ps) ? ps : null;
UpdateSystems.Add( UpdateSystems.Add(
new AnimalActionSystem( new AnimalActionSystem(
Store, Store,
@@ -272,6 +273,7 @@ public sealed class WorldScene : Scene
CellSize, CellSize,
_bounds, _bounds,
bleeding, bleeding,
poisoned,
_config.Seed + 0x1B17 _config.Seed + 0x1B17
) )
); );
@@ -292,6 +294,17 @@ public sealed class WorldScene : Scene
_config.Seed + 0x4BED _config.Seed + 0x4BED
) )
); );
UpdateSystems.Add(
new EggSystem(
Store,
_animals,
_needs,
Context.Clock,
SecondsPerDay,
CellSize,
_config.Seed + 0x6E66
)
);
UpdateSystems.Add( UpdateSystems.Add(
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen) new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
@@ -517,7 +530,7 @@ public sealed class WorldScene : Scene
new SaveStore().Write(save); new SaveStore().Write(save);
Log.Info( Log.Info(
$"World '{_config.Name}' saved ({save.Plants.Count} plants, " $"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; return _config.Name;
} }
@@ -612,6 +625,23 @@ public sealed class WorldScene : Scene
} }
) )
); );
Store
.Query<Egg, Transform2D>()
.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) private void RestorePlants(GameContent content)
@@ -693,8 +723,31 @@ public sealed class WorldScene : Scene
data.Meat = saved.Meat; data.Meat = saved.Meat;
} }
foreach (var saved in _save.Eggs)
{
if (!content.Defs.TryGet<AnimalDef>(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( 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 " $" temp: grows {tMin:0.#}..{tMax:0.#}°C, optimal {tLow:0.#}..{tHigh:0.#}°C "
+ $"(cold {traits.ColdHardiness:0.#}, heat {traits.HeatHardiness:0.#})" + $"(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) 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.##}, " $" move {traits.MoveSpeed:0.##}, vision {traits.Vision:0.##}, blood {traits.BloodVolume:0.##}, "
+ $"insulation {traits.Insulation:0.##}, furHue {traits.FurHue: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, // Диета из генов (предатор-кластер): по каждому виду печатает herbivory/carnivory/omnivory,
@@ -1187,6 +1252,7 @@ public sealed class WorldScene : Scene
var brain = new double[_animals.Count]; var brain = new double[_animals.Count];
var life = new double[_animals.Count]; var life = new double[_animals.Count];
var move = new double[_animals.Count]; var move = new double[_animals.Count];
var tox = new double[_animals.Count];
var maxGen = new int[_animals.Count]; var maxGen = new int[_animals.Count];
Store Store
@@ -1200,6 +1266,7 @@ public sealed class WorldScene : Scene
brain[s] += o.Traits.BrainSize; brain[s] += o.Traits.BrainSize;
life[s] += o.Traits.Lifespan; life[s] += o.Traits.Lifespan;
move[s] += o.Traits.MoveSpeed; move[s] += o.Traits.MoveSpeed;
tox[s] += o.Traits.ToxinTolerance;
if (o.Generation > maxGen[s]) if (o.Generation > maxGen[s])
{ {
maxGen[s] = o.Generation; maxGen[s] = o.Generation;
@@ -1227,7 +1294,8 @@ public sealed class WorldScene : Scene
var c = count[s]; var c = count[s];
console.WriteLine( console.WriteLine(
$"{name}: n={c}, gen 0..{maxGen[s]}, body {body[s] / c:0.##}, " $"{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"); console.WriteLine(filter is null ? "no animals alive" : $"no '{filter}' alive");
} }
// Другая сторона гонки вооружений: средняя защита живых растений (коэволюция C2). Без фильтра.
if (filter is null)
{
var pn = 0;
double pTox = 0,
pThorn = 0,
pPalat = 0;
Store
.Query<PlantOrganism>()
.ForEachEntity(
(ref PlantOrganism o, Entity _) =>
{
pn++;
pTox += o.Traits.Toxicity;
pThorn += o.Traits.Thorns;
pPalat += o.Traits.Palatability;
}
);
if (pn > 0)
{
console.WriteLine(
$"plants: n={pn}, toxicity {pTox / pn:0.###}, thorns {pThorn / pn:0.###}, "
+ $"palatability {pPalat / pn:0.###}"
);
}
}
} }
// Наблюдаемость здоровья (фаза A5): дерево частей тела вида и способности здоровой особи. // Наблюдаемость здоровья (фаза A5): дерево частей тела вида и способности здоровой особи.
+40
View File
@@ -110,6 +110,46 @@ public static class AnimalFactory
); );
} }
// Тинт яйца (бледно-кремовый) и доля размера клетки, до которой масштабируется спрайт яйца.
private static readonly Color EggTint = new(238, 230, 208);
private const float EggSizeFraction = 0.45f;
/// <summary>
/// Создаёт сущность-яйцо (тип рождения «яйцекладка»): спрайт-яйцо и компонент <see cref="Egg"/> с уже
/// скрещённым геномом будущего детёныша. Инкубируется и вылупляется в <see cref="EggSystem"/>.
/// </summary>
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,
}
);
}
/// <summary>Порог выраженности признака диеты, с которого вид реально ест данный корм.</summary> /// <summary>Порог выраженности признака диеты, с которого вид реально ест данный корм.</summary>
public const float DietThreshold = 0.5f; public const float DietThreshold = 0.5f;
+48 -4
View File
@@ -48,6 +48,9 @@ public struct AnimalPhenotype
/// <summary>Всеядность [0..1] — генералист: включает оба источника пищи (растения и мясо).</summary> /// <summary>Всеядность [0..1] — генералист: включает оба источника пищи (растения и мясо).</summary>
public float Omnivory; public float Omnivory;
/// <summary>Устойчивость к растительным ядам [0..1] — снижает дозу отравления токсичным кормом.</summary>
public float ToxinTolerance;
/// <summary>Продолжительность жизни (игровых дней).</summary> /// <summary>Продолжительность жизни (игровых дней).</summary>
public float Lifespan; public float Lifespan;
@@ -60,6 +63,12 @@ public struct AnimalPhenotype
/// <summary>Размер помёта.</summary> /// <summary>Размер помёта.</summary>
public float LitterSize; public float LitterSize;
/// <summary>Тип рождения [0..1]: ≥0.5 — яйцекладущий (откладывает яйца), иначе живородящий.</summary>
public float EggLaying;
/// <summary>Яйцекладущий ли вид (тип рождения) — определяет цикл зачатия.</summary>
public readonly bool Oviparous => EggLaying >= 0.5f;
/// <summary>Собирает фенотип из карты признаков, посчитанной <see cref="Phenotype.Compute"/>.</summary> /// <summary>Собирает фенотип из карты признаков, посчитанной <see cref="Phenotype.Compute"/>.</summary>
public static AnimalPhenotype FromTraits(IReadOnlyDictionary<string, float> traits) public static AnimalPhenotype FromTraits(IReadOnlyDictionary<string, float> traits)
{ {
@@ -78,10 +87,12 @@ public struct AnimalPhenotype
Herbivory = T("herbivory"), Herbivory = T("herbivory"),
Carnivory = T("carnivory"), Carnivory = T("carnivory"),
Omnivory = T("omnivory"), Omnivory = T("omnivory"),
ToxinTolerance = T("toxinTolerance"),
Lifespan = T("lifespan"), Lifespan = T("lifespan"),
BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)), BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)),
GestationDays = T("gestationDays"), GestationDays = T("gestationDays"),
LitterSize = T("litterSize"), LitterSize = T("litterSize"),
EggLaying = T("eggLaying"),
}; };
} }
} }
@@ -187,9 +198,11 @@ public sealed class HealthState
/// <summary> /// <summary>
/// Пересчитывает боль (из хедифов) и способности: базовые из частей/крови/боли, затем модификаторы /// Пересчитывает боль (из хедифов) и способности: базовые из частей/крови/боли, затем модификаторы
/// способностей от хедифов (лерп 1→factor по тяжести). Вызывать после изменения частей/крови/хедифов. /// способностей от хедифов (лерп 1→factor по тяжести), затем общий множитель <paramref name="capacityScale"/>
/// (старение/сенесценс: дряхлеющий организм слабее по всем способностям). Вызывать после изменения
/// частей/крови/хедифов; <paramref name="capacityScale"/>=1 — без возрастного спада.
/// </summary> /// </summary>
public void RecomputeCapacities() public void RecomputeCapacities(float capacityScale = 1f)
{ {
var pain = 0f; var pain = 0f;
foreach (var h in _hediffs) 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<string>(caps.Keys))
{
caps[capacity] *= scale;
}
}
Capacities = caps; Capacities = caps;
} }
/// <summary> /// <summary>
/// Прогрессирует болезни (тяжесть и иммунитет растут со временем); снимает выздоровевшие /// Прогрессирует болезни (тяжесть и иммунитет растут со временем); снимает выздоровевшие
/// (иммунитет ≥ 1); возвращает true, если какая-то болезнь достигла летальной тяжести. /// (иммунитет ≥ 1); возвращает true, если какая-то болезнь достигла летальной тяжести.
/// <paramref name="immunityScale"/> — множитель набора иммунитета (способность «фильтрация крови»:
/// почки/печень выводят токсины и помогают бороться с болезнью; повреждённые → медленнее).
/// </summary> /// </summary>
public bool AdvanceHediffs(float days) public bool AdvanceHediffs(float days, float immunityScale = 1f)
{ {
var lethal = false; var lethal = false;
for (var i = _hediffs.Count - 1; i >= 0; i--) for (var i = _hediffs.Count - 1; i >= 0; i--)
@@ -233,7 +257,7 @@ public sealed class HealthState
} }
h.Severity += h.Def.SeverityPerDay * days; h.Severity += h.Def.SeverityPerDay * days;
h.Immunity += h.Def.ImmunityPerDay * days; h.Immunity += h.Def.ImmunityPerDay * days * immunityScale;
if (h.Immunity >= 1f) if (h.Immunity >= 1f)
{ {
_hediffs.RemoveAt(i); // иммунитет победил — выздоровление _hediffs.RemoveAt(i); // иммунитет победил — выздоровление
@@ -440,6 +464,26 @@ public struct Corpse : IComponent
public float Meat; public float Meat;
} }
/// <summary>
/// Яйцо (тип рождения «яйцекладка»): отдельная сущность, отложенная яйцекладущей самкой вместо живых
/// родов. Несёт уже скрещённый геном будущего детёныша, вид и поколение; инкубируется во времени и
/// вылупляется в детёныша (<see cref="Sim.EggSystem"/>). Геном — managed-ссылка (как у трупа/беременности).
/// </summary>
public struct Egg : IComponent
{
/// <summary>Индекс вида в <see cref="Content.AnimalSet"/> (для спрайта при вылуплении и сейва).</summary>
public int Species;
/// <summary>Геном будущего детёныша (уже скрещён из родителей на момент кладки).</summary>
public Genome Genome;
/// <summary>Поколение будущего детёныша.</summary>
public int Generation;
/// <summary>Остаток инкубации в игровых днях; ≤0 — вылупление.</summary>
public float IncubateDays;
}
/// <summary>Инстанс мысли настроения на особи (фаза A8): деф события + остаток времени до угасания.</summary> /// <summary>Инстанс мысли настроения на особи (фаза A8): деф события + остаток времени до угасания.</summary>
public struct ThoughtInstance public struct ThoughtInstance
{ {
+346 -28
View File
@@ -125,6 +125,9 @@ public readonly struct AnimalContext(float[] values, NeedSet needs, float intell
public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, NeedSet needs) public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, NeedSet needs)
: QuerySystem<AnimalNeeds, AnimalOrganism, Health> : QuerySystem<AnimalNeeds, AnimalOrganism, Health>
{ {
// Прибавка к обмену веществ при устойчивости к яду 1.0 (цена детоксикации, коэволюция C2).
private const float ToleranceMetabolicCost = 0.5f;
protected override void OnUpdate() protected override void OnUpdate()
{ {
var days = clock.DeltaTime / secondsPerDay; var days = clock.DeltaTime / secondsPerDay;
@@ -146,7 +149,14 @@ public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, Need
continue; // защита от старого/пустого состояния 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; var lethalEmpty = false;
for (var k = 0; k < needs.Count; k++) for (var k = 0; k < needs.Count; k++)
{ {
@@ -237,7 +247,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
AnimalGrowth, AnimalGrowth,
Transform2D Transform2D
> _animals; > _animals;
private readonly ArchetypeQuery<Transform2D, PlantGrowth> _plants; private readonly ArchetypeQuery<Transform2D, PlantGrowth, PlantOrganism> _plants;
private readonly ArchetypeQuery<Transform2D, Corpse> _corpses; private readonly ArchetypeQuery<Transform2D, Corpse> _corpses;
public AnimalDecisionSystem( public AnimalDecisionSystem(
@@ -264,7 +274,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
AnimalGrowth, AnimalGrowth,
Transform2D Transform2D
>(); >();
_plants = store.Query<Transform2D, PlantGrowth>(); _plants = store.Query<Transform2D, PlantGrowth, PlantOrganism>();
_corpses = store.Query<Transform2D, Corpse>(); _corpses = store.Query<Transform2D, Corpse>();
} }
@@ -320,20 +330,24 @@ public sealed class AnimalDecisionSystem : BaseSystem
brain.DecideIn = Interval + _rng.NextSingle() * Interval; // джиттер — расфазировать скан brain.DecideIn = Interval + _rng.NextSingle() * Interval; // джиттер — расфазировать скан
var pos = t[i].Position; var pos = t[i].Position;
var adult = AnimalFactory.IsAdult(_animalSet[o[i].Species], gr[i].Stage); 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 self = entities.EntityAt(i);
var health = self.GetComponent<Health>().State;
// Эффективный интеллект (мозг × сознание) гейтит доступные нужды/действия (фаза A7): // Эффективный интеллект (мозг × сознание) гейтит доступные нужды/действия (фаза A7):
// больной/раненый зверь с упавшим сознанием теряет высшие нужды (сон/секс) → рефлексы. // больной/раненый зверь с упавшим сознанием теряет высшие нужды (сон/секс) → рефлексы.
var intelligence = AnimalFactory.Intelligence( var intelligence = AnimalFactory.Intelligence(o[i].Traits, health);
o[i].Traits, // Радиус восприятия по способностям: зрение ищет корм/партнёра/добычу, угрозу замечаем
self.GetComponent<Health>().State // зрением ИЛИ слухом (повреждённые глаза/уши сужают восприятие).
); 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): стресс/истощение (низкое настроение) подавляет тягу к спариванию. // Настроение (A8): стресс/истощение (низкое настроение) подавляет тягу к спариванию.
var mood = self.GetComponent<Mood>(); var mood = self.GetComponent<Mood>();
// Бегство (предатор-кластер): жертва, заметившая рядом хищника, спасается — это важнее // Бегство (предатор-кластер): жертва, заметившая рядом хищника, спасается — это важнее
// любых нужд (инстинкт выживания). Сам хищник от добычи не бежит (см. IsThreatTo). // любых нужд (инстинкт выживания). Сам хищник от добычи не бежит (см. 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.Action = AnimalActions.Flee;
brain.TargetPlant = -1; brain.TargetPlant = -1;
@@ -342,7 +356,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
var len = away.Length(); var len = away.Length();
brain.Target = brain.Target =
len > 0.001f len > 0.001f
? pos + away / len * (radius + _cellSize) ? pos + away / len * (senseRadius + _cellSize)
: pos + new Vector2(_cellSize, 0f); : pos + new Vector2(_cellSize, 0f);
continue; continue;
} }
@@ -362,7 +376,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
when eatsMeat when eatsMeat
&& TryFindKill( && TryFindKill(
pos, pos,
radius, seeRadius,
o[i].Species, o[i].Species,
o[i].Traits, o[i].Traits,
self.Id, self.Id,
@@ -377,7 +391,15 @@ public sealed class AnimalDecisionSystem : BaseSystem
brain.Target = preyPos; brain.Target = preyPos;
break; break;
case AnimalActions.Eat 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.Action = AnimalActions.Eat;
brain.TargetPlant = plant; brain.TargetPlant = plant;
brain.Target = pp; brain.Target = pp;
@@ -397,7 +419,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
&& TryFindMate( && TryFindMate(
pos, pos,
o[i].IsMale, o[i].IsMale,
radius, seeRadius,
out var mateId, out var mateId,
out var matePos out var matePos
): ):
@@ -586,12 +608,36 @@ public sealed class AnimalDecisionSystem : BaseSystem
otherTraits.BodySize <= hunterBody * 1.25f; otherTraits.BodySize <= hunterBody * 1.25f;
// Ближайшее растение в радиусе (квадрат расстояния); ничьи — по меньшему id (детерминизм). // Ближайшее растение в радиусе (квадрат расстояния); ничьи — по меньшему 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) private bool TryFindPlant(Vector2 from, float radius, out int plantId, out Vector2 position)
{ {
var bestSq = radius * radius; var bestSq = radius * radius;
plantId = -1; plantId = -1;
position = default; position = default;
foreach (var (transforms, _, entities) in _plants.Chunks) foreach (var (transforms, _, _, entities) in _plants.Chunks)
{ {
var t = transforms.Span; var t = transforms.Span;
for (var i = 0; i < t.Length; i++) for (var i = 0; i < t.Length; i++)
@@ -610,6 +656,54 @@ public sealed class AnimalDecisionSystem : BaseSystem
return plantId >= 0; 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). // Ближайшая кромка воды (без лимита по зрению — звери «помнят» водопои; упрощение фазы A2).
private bool TryFindShore(Vector2 from, out Vector2 position) 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 GrazeKillAge = 1f; // трава с возрастом ниже этого после выедания исчезает
private const float ThirstReliefBelow = 0.15f; // ниже этого жажда — питьё даёт мысль «напилась» private const float ThirstReliefBelow = 0.15f; // ниже этого жажда — питьё даёт мысль «напилась»
private const float FleeSpeedMult = 1.2f; // страх подгоняет жертву чуть быстрее обычного шага private const float FleeSpeedMult = 1.2f; // страх подгоняет жертву чуть быстрее обычного шага
private const float PostReproductiveFrac = 0.9f; // доля жизни, после которой самка уже не зачинает
private readonly EntityStore _store; private readonly EntityStore _store;
private readonly PlantSet _plants; private readonly PlantSet _plants;
@@ -707,6 +802,7 @@ public sealed class AnimalActionSystem : BaseSystem
private readonly int _cellSize; private readonly int _cellSize;
private readonly RectF _bounds; private readonly RectF _bounds;
private readonly HediffDef? _bleeding; private readonly HediffDef? _bleeding;
private readonly HediffDef? _poisoned;
private readonly Random _rng; private readonly Random _rng;
private readonly ArchetypeQuery< private readonly ArchetypeQuery<
AnimalBrain, AnimalBrain,
@@ -731,6 +827,7 @@ public sealed class AnimalActionSystem : BaseSystem
int cellSize, int cellSize,
RectF bounds, RectF bounds,
HediffDef? bleeding, HediffDef? bleeding,
HediffDef? poisoned,
int seed int seed
) )
{ {
@@ -744,6 +841,7 @@ public sealed class AnimalActionSystem : BaseSystem
_cellSize = cellSize; _cellSize = cellSize;
_bounds = bounds; _bounds = bounds;
_bleeding = bleeding; _bleeding = bleeding;
_poisoned = poisoned;
_rng = new Random(seed); _rng = new Random(seed);
_query = store.Query<AnimalBrain, AnimalNeeds, AnimalOrganism, Transform2D, Health>(); _query = store.Query<AnimalBrain, AnimalNeeds, AnimalOrganism, Transform2D, Health>();
} }
@@ -780,6 +878,10 @@ public sealed class AnimalActionSystem : BaseSystem
// Способность Moving (падает от ран/болезней) — множитель к скорости: больные медленнее. // Способность Moving (падает от ран/болезней) — множитель к скорости: больные медленнее.
var moving = hh[i].State.Capacity(AnimalCapacities.Moving); var moving = hh[i].State.Capacity(AnimalCapacities.Moving);
var speed = def.BaseSpeed * MathF.Max(0.2f, o[i].Traits.MoveSpeed) * 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) switch (brain.Action)
{ {
@@ -790,8 +892,19 @@ public sealed class AnimalActionSystem : BaseSystem
case AnimalActions.Eat: case AnimalActions.Eat:
if (MoveTo(ref pos, brain.Target, speed * seconds)) if (MoveTo(ref pos, brain.Target, speed * seconds))
{ {
Refill(values, AnimalActions.Eat, days); Refill(values, AnimalActions.Eat, days, foodEff);
Graze(brain.TargetPlant, days, def.ForageBiteDays); Graze(
brain.TargetPlant,
days,
def.ForageBiteDays * MathF.Max(0.2f, eating)
);
// Коэволюция: защита растения бьёт по поедателю (яд/шипы).
ApplyPlantDefense(
brain.TargetPlant,
hh[i].State,
o[i].Traits.ToxinTolerance,
days
);
} }
break; break;
@@ -836,7 +949,7 @@ public sealed class AnimalActionSystem : BaseSystem
break; break;
case AnimalActions.Hunt: 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; break;
case AnimalActions.Flee: case AnimalActions.Flee:
@@ -883,7 +996,8 @@ public sealed class AnimalActionSystem : BaseSystem
float[] values, float[] values,
AnimalDef def, AnimalDef def,
float step, float step,
float days float days,
float foodEff
) )
{ {
if ( if (
@@ -914,7 +1028,7 @@ public sealed class AnimalActionSystem : BaseSystem
} }
ref var corpse = ref target.GetComponent<Corpse>(); ref var corpse = ref target.GetComponent<Corpse>();
Refill(values, AnimalActions.Eat, days); Refill(values, AnimalActions.Eat, days, foodEff);
corpse.Meat -= def.ForageBiteDays * days; // расход мяса той же «силой укуса», что и выедание corpse.Meat -= def.ForageBiteDays * days; // расход мяса той же «силой укуса», что и выедание
if (corpse.Meat <= 0f && !_consumed.Contains(target)) if (corpse.Meat <= 0f && !_consumed.Contains(target))
{ {
@@ -961,8 +1075,9 @@ public sealed class AnimalActionSystem : BaseSystem
} }
} }
// Восполняет нужду, которую утоляет действие (по NeedSet), на её FeedPerDay. // Восполняет нужду, которую утоляет действие (по NeedSet), на её FeedPerDay × эффективность
private void Refill(float[] values, string action, float days) // (питание/пищеварение для еды — слабая челюсть/больной желудок дают меньше сытости с того же корма).
private void Refill(float[] values, string action, float days, float efficiency = 1f)
{ {
var index = _needs.NeedForAction(action); var index = _needs.NeedForAction(action);
if (index < 0 || values is null || index >= values.Length) if (index < 0 || values is null || index >= values.Length)
@@ -970,7 +1085,11 @@ public sealed class AnimalActionSystem : BaseSystem
return; 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, если уже у цели (можно исполнять действие). // Двигает позицию к цели на 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<PlantOrganism>()
)
{
return;
}
ref readonly var traits = ref plant.GetComponent<PlantOrganism>().Traits;
var poisoned = false;
if (_poisoned is not null && traits.Toxicity > 0f)
{
var dose = traits.Toxicity * (1f - Math.Clamp(toxinTolerance, 0f, 1f));
if (dose > 0f)
{
health.Intensify(_poisoned, dose * PoisonDosePerDay * days);
poisoned = true;
}
}
if (traits.Thorns > ThornThreshold)
{
// ApplyInjury сам пересчитывает способности (учтёт и свежий яд).
health.ApplyInjury(
traits.Thorns * ThornPartDamage * days,
traits.Thorns * ThornBloodLoss * days,
_bleeding,
traits.Thorns * 0.15f * days,
_rng
);
}
else if (poisoned)
{
health.RecomputeCapacities(); // отравление меняет capacity-моды — применить сразу
}
}
// Применяет спаривания после прохода: самка пары беременеет геномом самца (структурное добавление — // Применяет спаривания после прохода: самка пары беременеет геномом самца (структурное добавление —
// после итерации), влечение обоих обнуляется. Двойные события на одну самку отсеиваются. // после итерации), влечение обоих обнуляется. Двойные события на одну самку отсеиваются.
private void ApplyMatings() private void ApplyMatings()
@@ -1039,15 +1217,25 @@ public sealed class AnimalActionSystem : BaseSystem
if (!mother.HasComponent<Pregnant>()) if (!mother.HasComponent<Pregnant>())
{ {
ref readonly var mom = ref mother.GetComponent<AnimalOrganism>(); ref readonly var mom = ref mother.GetComponent<AnimalOrganism>();
// Плодовитость падает со старостью (тот же фактор сенесценса): дряхлая самка приносит
// меньший помёт, а после порога пострепродуктивного возраста уже не зачинает.
var momAge = mother.HasComponent<AnimalGrowth>()
? mother.GetComponent<AnimalGrowth>().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( mother.AddComponent(
new Pregnant new Pregnant
{ {
FatherGenome = father.GetComponent<AnimalOrganism>().Genome, FatherGenome = father.GetComponent<AnimalOrganism>().Genome,
DueInDays = MathF.Max(1f, mom.Traits.GestationDays), DueInDays = MathF.Max(1f, mom.Traits.GestationDays),
Litter = Math.Max(1, (int)MathF.Round(mom.Traits.LitterSize)), Litter = Math.Max(1, (int)MathF.Round(mom.Traits.LitterSize * fert)),
} }
); );
} }
}
ResetMating(a, mateNeed); ResetMating(a, mateNeed);
ResetMating(b, mateNeed); ResetMating(b, mateNeed);
@@ -1468,6 +1656,8 @@ public sealed class AnimalPregnancySystem : BaseSystem
Litter = preg.Litter, Litter = preg.Litter,
Generation = o[i].Generation + 1, Generation = o[i].Generation + 1,
Position = t[i].Position, Position = t[i].Position,
Oviparous = o[i].Traits.Oviparous,
IncubateDays = MathF.Max(1f, o[i].Traits.GestationDays),
} }
); );
} }
@@ -1497,6 +1687,22 @@ public sealed class AnimalPregnancySystem : BaseSystem
); );
var offset = var offset =
new Vector2(_rng.NextSingle() - 0.5f, _rng.NextSingle() - 0.5f) * _cellSize; new Vector2(_rng.NextSingle() - 0.5f, _rng.NextSingle() - 0.5f) * _cellSize;
if (birth.Oviparous)
{
// Яйцекладка: вместо живого детёныша откладываем яйцо с его геномом — вылупится позже.
AnimalFactory.CreateEgg(
_store,
_animals,
birth.Species,
birth.Position + offset,
child,
birth.Generation,
birth.IncubateDays,
_cellSize
);
}
else
{
AnimalFactory.Create( AnimalFactory.Create(
_store, _store,
_animals, _animals,
@@ -1508,6 +1714,8 @@ public sealed class AnimalPregnancySystem : BaseSystem
birth.Generation, birth.Generation,
_cellSize _cellSize
); );
}
count++; count++;
} }
} }
@@ -1522,6 +1730,90 @@ public sealed class AnimalPregnancySystem : BaseSystem
public int Litter; public int Litter;
public int Generation; public int Generation;
public Vector2 Position; public Vector2 Position;
public bool Oviparous;
public float IncubateDays;
}
}
/// <summary>
/// Инкубация яиц (тип рождения «яйцекладка»): тикает остаток инкубации каждого яйца; по истечении
/// вылупляет детёныша (<see cref="AnimalFactory.Create"/> из хранимого генома, стадия Baby) рядом с
/// яйцом и удаляет яйцо. Зеркало родов для яйцекладущих видов.
/// </summary>
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<Egg, Transform2D> _query;
private readonly List<Entity> _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<Egg, Transform2D>();
}
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 float _secondsPerDay;
private readonly HediffDef[] _ambient; private readonly HediffDef[] _ambient;
private readonly Random _rng; private readonly Random _rng;
private readonly ArchetypeQuery<Health, AnimalOrganism> _query; private readonly ArchetypeQuery<Health, AnimalOrganism, AnimalGrowth> _query;
private readonly List<Entity> _deaths = []; private readonly List<Entity> _deaths = [];
private float _accum; private float _accum;
// Сенесценс (старение): спад способностей начинается с этой доли продолжительности жизни и достигает
// максимума к её концу. Дряхлеющий зверь слабее (медленнее, хуже видит/фильтрует кровь) и уязвимее.
private const float SenescenceStart = 0.6f;
private const float SenescenceMax = 0.45f;
public AnimalHealthSystem( public AnimalHealthSystem(
EntityStore store, EntityStore store,
AnimalSet animals, AnimalSet animals,
@@ -1564,7 +1861,19 @@ public sealed class AnimalHealthSystem : BaseSystem
_secondsPerDay = secondsPerDay; _secondsPerDay = secondsPerDay;
_ambient = ambient; _ambient = ambient;
_rng = new Random(seed); _rng = new Random(seed);
_query = store.Query<Health, AnimalOrganism>(); _query = store.Query<Health, AnimalOrganism, AnimalGrowth>();
}
/// <summary>Множитель способностей от старения (1 — молод/расцвет, &lt;1 — дряхлеет к концу жизни).</summary>
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() protected override void OnUpdateGroup()
@@ -1579,9 +1888,11 @@ public sealed class AnimalHealthSystem : BaseSystem
_accum = 0f; _accum = 0f;
_deaths.Clear(); _deaths.Clear();
foreach (var (healths, organisms, entities) in _query.Chunks) foreach (var (healths, organisms, growths, entities) in _query.Chunks)
{ {
var h = healths.Span; var h = healths.Span;
var o = organisms.Span;
var g = growths.Span;
for (var i = 0; i < h.Length; i++) for (var i = 0; i < h.Length; i++)
{ {
var state = h[i].State; 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; var bleed = 0f;
foreach (var hd in state.Hediffs) 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 ( if (
lethal lethal
|| state.BloodLevel <= 0f || state.BloodLevel <= 0f
+47 -8
View File
@@ -9,8 +9,27 @@ public static class AnimalCapacities
public const string Moving = "Moving"; public const string Moving = "Moving";
public const string Breathing = "Breathing"; public const string Breathing = "Breathing";
public const string BloodPumping = "BloodPumping"; public const string BloodPumping = "BloodPumping";
public const string BloodFiltration = "BloodFiltration";
public const string Sight = "Sight"; 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"; public const string Digestion = "Digestion";
/// <summary>Порядок отображения способностей в UI (сверху вниз). Прочие модовые — после, как есть.</summary>
public static readonly string[] DisplayOrder =
[
Consciousness,
Moving,
Sight,
Hearing,
Talking,
Eating,
Breathing,
BloodPumping,
BloodFiltration,
Digestion,
];
} }
/// <summary> /// <summary>
@@ -24,7 +43,11 @@ public static class CapacityCalc
/// <summary>Считает карту способностей из инстансов частей, уровня крови (0..1) и боли (0..1).</summary> /// <summary>Считает карту способностей из инстансов частей, уровня крови (0..1) и боли (0..1).</summary>
public static Dictionary<string, float> Compute(PartInstance[] parts, float blood, float pain) public static Dictionary<string, float> Compute(PartInstance[] parts, float blood, float pain)
{ {
// Сырой вклад каждой способности (Σ доля·HP) и НАБОР объявленных телом способностей: вид имеет
// только те, к которым его части вообще причастны (у оленя нет манипуляции). Объявленность —
// статична (по дефам частей), поэтому уничтоженный орган всё равно покажет способность на 0%.
var raw = new Dictionary<string, float>(System.StringComparer.Ordinal); var raw = new Dictionary<string, float>(System.StringComparer.Ordinal);
var declared = new HashSet<string>(System.StringComparer.Ordinal);
foreach (var part in parts) foreach (var part in parts)
{ {
if (part.Def is null) if (part.Def is null)
@@ -35,36 +58,52 @@ public static class CapacityCalc
var fraction = part.Fraction; var fraction = part.Fraction;
foreach (var (capacity, contribution) in part.Def.Capacities) foreach (var (capacity, contribution) in part.Def.Capacities)
{ {
declared.Add(capacity);
raw[capacity] = raw.GetValueOrDefault(capacity) + contribution * fraction; raw[capacity] = raw.GetValueOrDefault(capacity) + contribution * fraction;
} }
} }
float Raw(string id) => raw.GetValueOrDefault(id); 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 bloodLevel = Clamp01(blood);
var bloodPumping = Raw(AnimalCapacities.BloodPumping) * bloodLevel; var bloodPumping = Raw(AnimalCapacities.BloodPumping) * bloodLevel;
var breathing = Raw(AnimalCapacities.Breathing) * bloodLevel; var breathing = Raw(AnimalCapacities.Breathing) * bloodLevel;
var bloodFiltration = Raw(AnimalCapacities.BloodFiltration) * bloodLevel;
// Сознание зависит от кровоснабжения, дыхания и боли (модель RimWorld).
var consciousness = var consciousness =
Raw(AnimalCapacities.Consciousness) Raw(AnimalCapacities.Consciousness)
* Clamp01(bloodPumping) * Clamp01(bloodPumping)
* Clamp01(breathing) * Clamp01(breathing)
* (1f - Clamp01(pain)); * (1f - Clamp01(pain));
var cons = Clamp01(consciousness);
var pump = Clamp01(bloodPumping);
var result = new Dictionary<string, float>(System.StringComparer.Ordinal) // Производные способности зависят от сознания (и кровоснабжения для движения/пищеварения).
var computed = new Dictionary<string, float>(System.StringComparer.Ordinal)
{ {
[AnimalCapacities.BloodPumping] = bloodPumping, [AnimalCapacities.BloodPumping] = bloodPumping,
[AnimalCapacities.Breathing] = breathing, [AnimalCapacities.Breathing] = breathing,
[AnimalCapacities.BloodFiltration] = bloodFiltration,
[AnimalCapacities.Consciousness] = consciousness, [AnimalCapacities.Consciousness] = consciousness,
[AnimalCapacities.Moving] = Raw(AnimalCapacities.Moving) * consciousness * Clamp01(bloodPumping), [AnimalCapacities.Moving] = Raw(AnimalCapacities.Moving) * cons * pump,
[AnimalCapacities.Sight] = Raw(AnimalCapacities.Sight) * consciousness, [AnimalCapacities.Sight] = Raw(AnimalCapacities.Sight) * cons,
[AnimalCapacities.Digestion] = Raw(AnimalCapacities.Digestion) * Clamp01(bloodPumping), [AnimalCapacities.Hearing] = Raw(AnimalCapacities.Hearing) * cons,
[AnimalCapacities.Talking] = Raw(AnimalCapacities.Talking) * cons,
[AnimalCapacities.Eating] = Raw(AnimalCapacities.Eating) * cons,
[AnimalCapacities.Digestion] = Raw(AnimalCapacities.Digestion) * pump,
}; };
// Модовые способности, не охваченные зависимостями, — сырыми. // В результат — только объявленные телом способности: известные по формуле зависимостей, прочие
foreach (var (capacity, value) in raw) // (модовые) — сырым вкладом. Так у вида видны ровно его способности, а не весь каталог.
var result = new Dictionary<string, float>(System.StringComparer.Ordinal);
foreach (var capacity in declared)
{ {
result.TryAdd(capacity, value); result[capacity] = computed.TryGetValue(capacity, out var v) ? v : Raw(capacity);
} }
return result; return result;
+1
View File
@@ -57,6 +57,7 @@ public sealed class PlantGrowthSystem(
var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand(); var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
var rate = var rate =
traits.Vigor traits.Vigor
* traits.DefenseGrowthFactor() // цена защиты: токсичные/колючие растут медленнее
* Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance) * Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance)
* Suitability.Trapezoid(temperature, tMin, tLow, tHigh, tMax) * Suitability.Trapezoid(temperature, tMin, tLow, tHigh, tMax)
* Suitability.Gaussian( * Suitability.Gaussian(
+27
View File
@@ -32,6 +32,16 @@ public struct PlantPhenotype
public float HarvestAmount; public float HarvestAmount;
public float LeafHue; public float LeafHue;
// --- Защита (коэволюция с травоядными) ---
/// <summary>Токсичность [0..1]: отравляет поедателя; цена — замедление роста.</summary>
public float Toxicity;
/// <summary>Шипы [0..1]: травмируют поедателя; цена — замедление роста.</summary>
public float Thorns;
/// <summary>Вкусность [0..1]: обратная привлекательность для умных травоядных (избегание в C2).</summary>
public float Palatability;
/// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary> /// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary>
public bool IsVariant; public bool IsVariant;
@@ -71,9 +81,26 @@ public struct PlantPhenotype
FruitSeason = (int)MathF.Round(Math.Clamp(T("fruitSeason"), 0f, 3f)), FruitSeason = (int)MathF.Round(Math.Clamp(T("fruitSeason"), 0f, 3f)),
HarvestAmount = T("harvestAmount"), HarvestAmount = T("harvestAmount"),
LeafHue = T("leafHue"), LeafHue = T("leafHue"),
Toxicity = T("toxicity"),
Thorns = T("thorns"),
Palatability = T("palatability"),
IsVariant = T("variant") >= 0.5f, IsVariant = T("variant") >= 0.5f,
}; };
} }
// Цена защиты: токсичность и шипы отнимают ресурсы у роста. Без этой платы отбор гнал бы защиту к
// максимуму у всех растений и коэволюция бы встала — а так под слабым выпасом выгоднее расти быстрее.
private const float ToxicityGrowthCost = 0.4f;
private const float ThornsGrowthCost = 0.3f;
private const float MinGrowthFactor = 0.2f;
/// <summary>Множитель скорости роста с учётом цены защиты (1 — без защиты, ≥ <c>MinGrowthFactor</c>).</summary>
public readonly float DefenseGrowthFactor() =>
Math.Clamp(
1f - ToxicityGrowthCost * Toxicity - ThornsGrowthCost * Thorns,
MinGrowthFactor,
1f
);
} }
/// <summary> /// <summary>
+29 -4
View File
@@ -331,6 +331,13 @@ internal sealed class InspectPanel
traits.LeafHue 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) if (traits.IsVariant)
{ {
text.AppendLine(languages.Get("inspect.gene.variant")); text.AppendLine(languages.Get("inspect.gene.variant"));
@@ -435,12 +442,27 @@ internal sealed class InspectPanel
text.AppendLine( text.AppendLine(
languages.Format("inspect.health.blood", state.BloodLevel * 100f, state.Pain * 100f) 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")); text.AppendLine(languages.Get("inspect.health.caps"));
AppendCapacity(text, state, AnimalCapacities.Consciousness, "cap.consciousness"); // Сначала известные способности в порядке отображения, затем прочие (модовые) — как есть.
AppendCapacity(text, state, AnimalCapacities.Moving, "cap.moving"); var shown = new HashSet<string>(StringComparer.Ordinal);
AppendCapacity(text, state, AnimalCapacities.Sight, "cap.sight"); 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) if (state.Hediffs.Count > 0)
@@ -463,6 +485,9 @@ internal sealed class InspectPanel
} }
} }
// Ключ локализации метки способности: cap.<id в нижнем регистре> (cap.consciousness, cap.bloodpumping…).
private static string CapacityLabel(string capId) => "cap." + capId.ToLowerInvariant();
private void AppendCapacity( private void AppendCapacity(
StringBuilder text, StringBuilder text,
HealthState state, HealthState state,