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:
@@ -30,7 +30,8 @@
|
||||
"GeneBreedingSeason": 2,
|
||||
"GeneGestationDays": 30,
|
||||
"GeneLitterSize": 1,
|
||||
"GeneHerbivory": 1.0
|
||||
"GeneHerbivory": 1.0,
|
||||
"GeneToxinTolerance": 0.1
|
||||
}
|
||||
},
|
||||
|
||||
@@ -97,7 +98,38 @@
|
||||
"GeneLitterSize": 5,
|
||||
"GeneHerbivory": 0.4,
|
||||
"GeneCarnivory": 0.2,
|
||||
"GeneOmnivory": 0.85
|
||||
"GeneOmnivory": 0.85,
|
||||
"GeneToxinTolerance": 0.25
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"defName": "Chicken",
|
||||
"label": "pawn.chicken",
|
||||
"kind": "animal",
|
||||
"texture": "things/pawn/animal/chicken/Chicken_east",
|
||||
"body": "Quadruped",
|
||||
"diet": ["plant"],
|
||||
"spawnPer1000Cells": 1.5,
|
||||
"baseSpeed": 22,
|
||||
"visionCells": 10,
|
||||
"genome": {
|
||||
"GeneMaxBodySize": 0.5,
|
||||
"GeneMetabolism": 1.3,
|
||||
"GeneMoveSpeed": 0.9,
|
||||
"GeneBloodVolume": 0.7,
|
||||
"GeneVision": 1.1,
|
||||
"GeneBrainSize": 0.3,
|
||||
"GeneInsulation": 0.45,
|
||||
"GeneFurColor": 0.6,
|
||||
"GeneMaturityAge": 50,
|
||||
"GeneLifespan": 180,
|
||||
"GeneBreedingSeason": 1,
|
||||
"GeneGestationDays": 12,
|
||||
"GeneLitterSize": 6,
|
||||
"GeneHerbivory": 0.8,
|
||||
"GeneToxinTolerance": 0.15,
|
||||
"GeneEggLaying": 1.0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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 } },
|
||||
|
||||
@@ -46,12 +46,26 @@
|
||||
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"],
|
||||
"effects": { "omnivory": "value" } },
|
||||
|
||||
// Устойчивость к растительным ядам (коэволюция): снижает дозу отравления при поедании токсичных
|
||||
// растений (effectivePoison = toxicity × (1 − toxinTolerance), см. AnimalActionSystem). Растёт под
|
||||
// давлением ядовитого корма — другая сторона гонки вооружений с GeneToxicity растений.
|
||||
{ "defName": "GeneToxinTolerance", "parent": "BaseNumericGene", "label": "gene.toxinTolerance",
|
||||
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"],
|
||||
"effects": { "toxinTolerance": "value" } },
|
||||
|
||||
// Пол как локус (фаза A3): аллели {X=0, Y=1}; самка XX, самец XY. Эффекта нет — пол читается из
|
||||
// самих аллелей (наличие Y), не из выраженного значения. mutationChance 0 (X не мутирует в Y).
|
||||
// Генерация особи-основателя задаёт пол явно (XX или XY), чтобы не возник невозможный YY.
|
||||
{ "defName": "GeneSex", "kind": "Discrete", "label": "gene.sex",
|
||||
"variants": 2, "variantWeights": [0.5, 0.5], "mutationChance": 0, "tags": ["animal", "sex"] },
|
||||
|
||||
// Тип рождения (цикл зачатия): живородящие (eggLaying < 0.5) вынашивают и рожают живых детёнышей;
|
||||
// яйцекладущие (≥ 0.5) после вынашивания ОТКЛАДЫВАЮТ яйца-сущности, которые инкубируются и
|
||||
// вылупляются (см. AnimalPregnancySystem/EggSystem). Признак фиксирован по виду (spread/mutation 0).
|
||||
{ "defName": "GeneEggLaying", "parent": "BaseNumericGene", "label": "gene.eggLaying",
|
||||
"default": 0.0, "min": 0.0, "max": 1.0, "spread": 0, "mutationChance": 0, "tags": ["animal", "reproduction"],
|
||||
"effects": { "eggLaying": "value" } },
|
||||
|
||||
// Размножение (фаза A4): сезон гона фиксирован по виду (0 весна … 3 зима), срок вынашивания и помёт.
|
||||
{ "defName": "GeneBreedingSeason", "parent": "BaseNumericGene", "label": "gene.breedingSeason",
|
||||
"default": 2, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["animal", "reproduction"],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"inspect.gene.repro": "Dispersal {0:0.0} · interval {1:0.0} d",
|
||||
"inspect.gene.repro2": "Self-pollin. {0:0.00} · mutation {1:0.00}",
|
||||
"inspect.gene.hardy": "Cold/heat {0:0}/{1:0} · leaf hue {2:0.00}",
|
||||
"inspect.gene.defense": "Defense: toxin {0:0.00} · thorns {1:0.00}",
|
||||
"inspect.gene.variant": "Morph: recessive variant",
|
||||
"inspect.harvest": "Harvest: {0} ×{1:0.#}",
|
||||
"inspect.fruit": "Fruit: {0} ×{1:0.#} ({2}) · ripe {3:0.#}",
|
||||
@@ -61,9 +62,17 @@
|
||||
"cap.consciousness": "consciousness",
|
||||
"cap.moving": "moving",
|
||||
"cap.sight": "sight",
|
||||
"cap.hearing": "hearing",
|
||||
"cap.talking": "talking",
|
||||
"cap.eating": "eating",
|
||||
"cap.breathing": "breathing",
|
||||
"cap.bloodpumping": "blood pumping",
|
||||
"cap.bloodfiltration": "blood filtration",
|
||||
"cap.digestion": "digestion",
|
||||
"hediff.rut": "rut",
|
||||
"hediff.fever": "fever",
|
||||
"hediff.bleeding": "bleeding",
|
||||
"hediff.poisoned": "poisoned",
|
||||
"inspect.hint": "LMB — select · RMB/Esc — clear",
|
||||
"hud.paused": "PAUSED",
|
||||
"menu.title": "LittleSim",
|
||||
@@ -154,6 +163,7 @@
|
||||
"pawn.hare": "hare",
|
||||
"pawn.boar": "boar",
|
||||
"pawn.wolf": "wolf",
|
||||
"pawn.chicken": "chicken",
|
||||
"pawn.muffalo": "muffalo",
|
||||
"pawn.squirrel": "squirrel",
|
||||
"thought.gaveBirth": "gave birth",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"inspect.gene.repro": "Расселение {0:0.0} кл · период {1:0.0} дн",
|
||||
"inspect.gene.repro2": "Самоопыление {0:0.00} · мутации {1:0.00}",
|
||||
"inspect.gene.hardy": "Морозо/жаро {0:0}/{1:0} · оттенок {2:0.00}",
|
||||
"inspect.gene.defense": "Защита: яд {0:0.00} · шипы {1:0.00}",
|
||||
"inspect.gene.variant": "Морфа: рецессивный вариант",
|
||||
"inspect.harvest": "Сбор: {0} ×{1:0.#}",
|
||||
"inspect.fruit": "Плоды: {0} ×{1:0.#} ({2}) · зрелых {3:0.#}",
|
||||
@@ -61,9 +62,17 @@
|
||||
"cap.consciousness": "сознание",
|
||||
"cap.moving": "движение",
|
||||
"cap.sight": "зрение",
|
||||
"cap.hearing": "слух",
|
||||
"cap.talking": "речь",
|
||||
"cap.eating": "питание",
|
||||
"cap.breathing": "дыхание",
|
||||
"cap.bloodpumping": "кровоснабжение",
|
||||
"cap.bloodfiltration": "фильтрация крови",
|
||||
"cap.digestion": "пищеварение",
|
||||
"hediff.rut": "гон",
|
||||
"hediff.fever": "лихорадка",
|
||||
"hediff.bleeding": "кровотечение",
|
||||
"hediff.poisoned": "отравление",
|
||||
"inspect.hint": "ЛКМ — выбрать · ПКМ/Esc — снять",
|
||||
"hud.paused": "ПАУЗА",
|
||||
"menu.title": "LittleSim",
|
||||
@@ -154,6 +163,7 @@
|
||||
"pawn.hare": "заяц",
|
||||
"pawn.boar": "кабан",
|
||||
"pawn.wolf": "волк",
|
||||
"pawn.chicken": "курица",
|
||||
"pawn.muffalo": "муффало",
|
||||
"pawn.squirrel": "белка",
|
||||
"thought.gaveBirth": "родила потомство",
|
||||
|
||||
@@ -173,6 +173,28 @@ public sealed class CorpseSave
|
||||
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>
|
||||
/// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание),
|
||||
/// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он
|
||||
@@ -216,6 +238,9 @@ public sealed class WorldSave
|
||||
/// <summary>Снимок трупов (вид, позиция, таймер/стадия разложения, мясо).</summary>
|
||||
public List<CorpseSave> Corpses { get; set; } = [];
|
||||
|
||||
/// <summary>Снимок яиц (вид, позиция, геном детёныша, поколение, инкубация).</summary>
|
||||
public List<EggSave> Eggs { get; set; } = [];
|
||||
|
||||
/// <summary>Конфиг мира для пересоздания сцены.</summary>
|
||||
public WorldConfig ToConfig() =>
|
||||
new()
|
||||
|
||||
@@ -30,6 +30,9 @@ public sealed class AnimalSet
|
||||
/// <summary>Спрайт трупа (fallback — самка).</summary>
|
||||
public required Texture2DRegion Corpse { get; init; }
|
||||
|
||||
/// <summary>Спрайт яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт-точка.</summary>
|
||||
public required Texture2DRegion Egg { get; init; }
|
||||
|
||||
/// <summary>Стадии роста (из дефа или встроенный дефолт), по возрастанию возраста входа.</summary>
|
||||
public required AnimalStageDef[] Stages { get; init; }
|
||||
|
||||
@@ -43,10 +46,40 @@ public sealed class AnimalSet
|
||||
// Встроенный набор стадий по умолчанию (если вид не задал свой): как было до выноса в данные.
|
||||
private static readonly AnimalStageDef[] DefaultStages =
|
||||
[
|
||||
new() { Name = "Baby", EnterAt = 0f, RelativeTo = "maturity", Scale = 0.45f, Texture = "baby" },
|
||||
new() { Name = "Juvenile", EnterAt = 0.22f, RelativeTo = "maturity", Scale = 0.7f, Texture = "baby" },
|
||||
new() { Name = "Adult", EnterAt = 1f, RelativeTo = "maturity", Scale = 1f, Texture = "adult", Adult = true },
|
||||
new() { Name = "Senior", EnterAt = 0.8f, RelativeTo = "lifespan", Scale = 0.92f, Texture = "adult", Adult = true },
|
||||
new()
|
||||
{
|
||||
Name = "Baby",
|
||||
EnterAt = 0f,
|
||||
RelativeTo = "maturity",
|
||||
Scale = 0.45f,
|
||||
Texture = "baby",
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Juvenile",
|
||||
EnterAt = 0.22f,
|
||||
RelativeTo = "maturity",
|
||||
Scale = 0.7f,
|
||||
Texture = "baby",
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Adult",
|
||||
EnterAt = 1f,
|
||||
RelativeTo = "maturity",
|
||||
Scale = 1f,
|
||||
Texture = "adult",
|
||||
Adult = true,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Senior",
|
||||
EnterAt = 0.8f,
|
||||
RelativeTo = "lifespan",
|
||||
Scale = 0.92f,
|
||||
Texture = "adult",
|
||||
Adult = true,
|
||||
},
|
||||
];
|
||||
|
||||
private readonly Species[] _species;
|
||||
@@ -82,6 +115,9 @@ public sealed class AnimalSet
|
||||
Corpse = string.IsNullOrEmpty(def.CorpseTexture)
|
||||
? female
|
||||
: atlases.GetRegion(device, def.CorpseTexture),
|
||||
Egg = string.IsNullOrEmpty(def.EggTexture)
|
||||
? female
|
||||
: atlases.GetRegion(device, def.EggTexture),
|
||||
Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages,
|
||||
Body = content.Defs.TryGet<BodyDef>(def.Body, out var body) ? body : null,
|
||||
Template = BuildTemplate(def.Genome, genes),
|
||||
@@ -111,6 +147,35 @@ public sealed class AnimalSet
|
||||
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: разброс берётся из
|
||||
// самого гена. Состав открыт — модер добавляет ген строкой JSON, без правки кода. Дискретные гены
|
||||
// (база/разброс не нужны — варианты из GeneDef) тоже поддержаны.
|
||||
|
||||
@@ -143,6 +143,15 @@ public sealed class GenomeDef
|
||||
|
||||
/// <summary>Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета.</summary>
|
||||
public float LeafHue { get; init; } = 0.33f;
|
||||
|
||||
/// <summary>Токсичность (0..1): отравляет поедателя (коэволюция); тормозит рост (цена защиты).</summary>
|
||||
public float Toxicity { get; init; }
|
||||
|
||||
/// <summary>Шипы (0..1): травмируют поедателя; тормозят рост (цена защиты).</summary>
|
||||
public float Thorns { get; init; }
|
||||
|
||||
/// <summary>Вкусность (0..1): обратная привлекательность для умных травоядных (избегание в C2).</summary>
|
||||
public float Palatability { get; init; } = 1f;
|
||||
}
|
||||
|
||||
/// <summary>Растение (Defs/Plants/): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
|
||||
@@ -252,6 +261,9 @@ public sealed class AnimalDef : PawnDef
|
||||
/// <summary>Текстура трупа (фаза A5); пусто — берётся <see cref="PawnDef.Texture"/>.</summary>
|
||||
public string CorpseTexture { get; init; } = "";
|
||||
|
||||
/// <summary>Текстура яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт семени-точки.</summary>
|
||||
public string EggTexture { get; init; } = "things/plant/seed_default";
|
||||
|
||||
/// <summary>Имя <see cref="BodyDef"/> — анатомия вида (части тела/органы); пусто — без частей тела.</summary>
|
||||
public string Body { get; init; } = "";
|
||||
|
||||
|
||||
@@ -118,6 +118,10 @@ public sealed class PlantSet
|
||||
new GenomeTemplate.Entry(Gene("GeneFruitSeason"), g.FruitSeason, 0f), // сезон фиксирован по виду
|
||||
Numeric("GeneHarvestAmount", g.HarvestAmount),
|
||||
new GenomeTemplate.Entry(Gene("GeneLeafHue"), g.LeafHue, 0.04f),
|
||||
// Защита (коэволюция с травоядными): токсичность/шипы/вкусность.
|
||||
Numeric("GeneToxicity", g.Toxicity),
|
||||
Numeric("GeneThorns", g.Thorns),
|
||||
Numeric("GenePalatability", g.Palatability),
|
||||
new GenomeTemplate.Entry(
|
||||
Gene("GeneMorph"),
|
||||
0f,
|
||||
|
||||
@@ -260,6 +260,7 @@ public sealed class WorldScene : Scene
|
||||
)
|
||||
);
|
||||
var bleeding = content.Defs.TryGet<HediffDef>("Bleeding", out var bl) ? bl : null;
|
||||
var poisoned = content.Defs.TryGet<HediffDef>("Poisoned", out var ps) ? ps : null;
|
||||
UpdateSystems.Add(
|
||||
new AnimalActionSystem(
|
||||
Store,
|
||||
@@ -272,6 +273,7 @@ public sealed class WorldScene : Scene
|
||||
CellSize,
|
||||
_bounds,
|
||||
bleeding,
|
||||
poisoned,
|
||||
_config.Seed + 0x1B17
|
||||
)
|
||||
);
|
||||
@@ -292,6 +294,17 @@ public sealed class WorldScene : Scene
|
||||
_config.Seed + 0x4BED
|
||||
)
|
||||
);
|
||||
UpdateSystems.Add(
|
||||
new EggSystem(
|
||||
Store,
|
||||
_animals,
|
||||
_needs,
|
||||
Context.Clock,
|
||||
SecondsPerDay,
|
||||
CellSize,
|
||||
_config.Seed + 0x6E66
|
||||
)
|
||||
);
|
||||
|
||||
UpdateSystems.Add(
|
||||
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
|
||||
@@ -517,7 +530,7 @@ public sealed class WorldScene : Scene
|
||||
new SaveStore().Write(save);
|
||||
Log.Info(
|
||||
$"World '{_config.Name}' saved ({save.Plants.Count} plants, "
|
||||
+ $"{save.Animals.Count} animals, {save.Corpses.Count} corpses)"
|
||||
+ $"{save.Animals.Count} animals, {save.Corpses.Count} corpses, {save.Eggs.Count} eggs)"
|
||||
);
|
||||
return _config.Name;
|
||||
}
|
||||
@@ -612,6 +625,23 @@ public sealed class WorldScene : Scene
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
Store
|
||||
.Query<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)
|
||||
@@ -693,8 +723,31 @@ public sealed class WorldScene : Scene
|
||||
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(
|
||||
$"Restored {_save.Animals.Count} animals, {_save.Corpses.Count} corpses from save"
|
||||
$"Restored {_save.Animals.Count} animals, {_save.Corpses.Count} corpses, {_save.Eggs.Count} eggs from save"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1070,6 +1123,10 @@ public sealed class WorldScene : Scene
|
||||
$" temp: grows {tMin:0.#}..{tMax:0.#}°C, optimal {tLow:0.#}..{tHigh:0.#}°C "
|
||||
+ $"(cold {traits.ColdHardiness:0.#}, heat {traits.HeatHardiness:0.#})"
|
||||
);
|
||||
console.WriteLine(
|
||||
$" defense: toxicity {traits.Toxicity:0.##}, thorns {traits.Thorns:0.##}, "
|
||||
+ $"palatability {traits.Palatability:0.##} (growth ×{traits.DefenseGrowthFactor():0.##})"
|
||||
);
|
||||
|
||||
if (def.HarvestProduct is { } harvest)
|
||||
{
|
||||
@@ -1126,6 +1183,14 @@ public sealed class WorldScene : Scene
|
||||
$" move {traits.MoveSpeed:0.##}, vision {traits.Vision:0.##}, blood {traits.BloodVolume:0.##}, "
|
||||
+ $"insulation {traits.Insulation:0.##}, furHue {traits.FurHue:0.##}"
|
||||
);
|
||||
console.WriteLine(
|
||||
$" diet: herb {traits.Herbivory:0.##} / carn {traits.Carnivory:0.##} / omni {traits.Omnivory:0.##}, "
|
||||
+ $"toxinTolerance {traits.ToxinTolerance:0.##}"
|
||||
);
|
||||
console.WriteLine(
|
||||
$" reproduction: {(traits.Oviparous ? "oviparous (lays eggs)" : "viviparous (live birth)")}, "
|
||||
+ $"gestation {traits.GestationDays:0} d, litter {traits.LitterSize:0.#}"
|
||||
);
|
||||
}
|
||||
|
||||
// Диета из генов (предатор-кластер): по каждому виду печатает herbivory/carnivory/omnivory,
|
||||
@@ -1187,6 +1252,7 @@ public sealed class WorldScene : Scene
|
||||
var brain = new double[_animals.Count];
|
||||
var life = new double[_animals.Count];
|
||||
var move = new double[_animals.Count];
|
||||
var tox = new double[_animals.Count];
|
||||
var maxGen = new int[_animals.Count];
|
||||
|
||||
Store
|
||||
@@ -1200,6 +1266,7 @@ public sealed class WorldScene : Scene
|
||||
brain[s] += o.Traits.BrainSize;
|
||||
life[s] += o.Traits.Lifespan;
|
||||
move[s] += o.Traits.MoveSpeed;
|
||||
tox[s] += o.Traits.ToxinTolerance;
|
||||
if (o.Generation > maxGen[s])
|
||||
{
|
||||
maxGen[s] = o.Generation;
|
||||
@@ -1227,7 +1294,8 @@ public sealed class WorldScene : Scene
|
||||
var c = count[s];
|
||||
console.WriteLine(
|
||||
$"{name}: n={c}, gen 0..{maxGen[s]}, body {body[s] / c:0.##}, "
|
||||
+ $"brain {brain[s] / c:0.##}, lifespan {life[s] / c:0} d, move {move[s] / c:0.##}"
|
||||
+ $"brain {brain[s] / c:0.##}, lifespan {life[s] / c:0} d, move {move[s] / c:0.##}, "
|
||||
+ $"toxinTol {tox[s] / c:0.##}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1235,6 +1303,33 @@ public sealed class WorldScene : Scene
|
||||
{
|
||||
console.WriteLine(filter is null ? "no animals alive" : $"no '{filter}' alive");
|
||||
}
|
||||
|
||||
// Другая сторона гонки вооружений: средняя защита живых растений (коэволюция C2). Без фильтра.
|
||||
if (filter is null)
|
||||
{
|
||||
var pn = 0;
|
||||
double pTox = 0,
|
||||
pThorn = 0,
|
||||
pPalat = 0;
|
||||
Store
|
||||
.Query<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): дерево частей тела вида и способности здоровой особи.
|
||||
|
||||
@@ -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>
|
||||
public const float DietThreshold = 0.5f;
|
||||
|
||||
|
||||
@@ -48,6 +48,9 @@ public struct AnimalPhenotype
|
||||
/// <summary>Всеядность [0..1] — генералист: включает оба источника пищи (растения и мясо).</summary>
|
||||
public float Omnivory;
|
||||
|
||||
/// <summary>Устойчивость к растительным ядам [0..1] — снижает дозу отравления токсичным кормом.</summary>
|
||||
public float ToxinTolerance;
|
||||
|
||||
/// <summary>Продолжительность жизни (игровых дней).</summary>
|
||||
public float Lifespan;
|
||||
|
||||
@@ -60,6 +63,12 @@ public struct AnimalPhenotype
|
||||
/// <summary>Размер помёта.</summary>
|
||||
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>
|
||||
public static AnimalPhenotype FromTraits(IReadOnlyDictionary<string, float> traits)
|
||||
{
|
||||
@@ -78,10 +87,12 @@ public struct AnimalPhenotype
|
||||
Herbivory = T("herbivory"),
|
||||
Carnivory = T("carnivory"),
|
||||
Omnivory = T("omnivory"),
|
||||
ToxinTolerance = T("toxinTolerance"),
|
||||
Lifespan = T("lifespan"),
|
||||
BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)),
|
||||
GestationDays = T("gestationDays"),
|
||||
LitterSize = T("litterSize"),
|
||||
EggLaying = T("eggLaying"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -187,9 +198,11 @@ public sealed class HealthState
|
||||
|
||||
/// <summary>
|
||||
/// Пересчитывает боль (из хедифов) и способности: базовые из частей/крови/боли, затем модификаторы
|
||||
/// способностей от хедифов (лерп 1→factor по тяжести). Вызывать после изменения частей/крови/хедифов.
|
||||
/// способностей от хедифов (лерп 1→factor по тяжести), затем общий множитель <paramref name="capacityScale"/>
|
||||
/// (старение/сенесценс: дряхлеющий организм слабее по всем способностям). Вызывать после изменения
|
||||
/// частей/крови/хедифов; <paramref name="capacityScale"/>=1 — без возрастного спада.
|
||||
/// </summary>
|
||||
public void RecomputeCapacities()
|
||||
public void RecomputeCapacities(float capacityScale = 1f)
|
||||
{
|
||||
var pain = 0f;
|
||||
foreach (var h in _hediffs)
|
||||
@@ -214,14 +227,25 @@ public sealed class HealthState
|
||||
}
|
||||
}
|
||||
|
||||
if (capacityScale < 1f)
|
||||
{
|
||||
var scale = Math.Clamp(capacityScale, 0f, 1f);
|
||||
foreach (var capacity in new List<string>(caps.Keys))
|
||||
{
|
||||
caps[capacity] *= scale;
|
||||
}
|
||||
}
|
||||
|
||||
Capacities = caps;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Прогрессирует болезни (тяжесть и иммунитет растут со временем); снимает выздоровевшие
|
||||
/// (иммунитет ≥ 1); возвращает true, если какая-то болезнь достигла летальной тяжести.
|
||||
/// <paramref name="immunityScale"/> — множитель набора иммунитета (способность «фильтрация крови»:
|
||||
/// почки/печень выводят токсины и помогают бороться с болезнью; повреждённые → медленнее).
|
||||
/// </summary>
|
||||
public bool AdvanceHediffs(float days)
|
||||
public bool AdvanceHediffs(float days, float immunityScale = 1f)
|
||||
{
|
||||
var lethal = false;
|
||||
for (var i = _hediffs.Count - 1; i >= 0; i--)
|
||||
@@ -233,7 +257,7 @@ public sealed class HealthState
|
||||
}
|
||||
|
||||
h.Severity += h.Def.SeverityPerDay * days;
|
||||
h.Immunity += h.Def.ImmunityPerDay * days;
|
||||
h.Immunity += h.Def.ImmunityPerDay * days * immunityScale;
|
||||
if (h.Immunity >= 1f)
|
||||
{
|
||||
_hediffs.RemoveAt(i); // иммунитет победил — выздоровление
|
||||
@@ -440,6 +464,26 @@ public struct Corpse : IComponent
|
||||
public float Meat;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public struct ThoughtInstance
|
||||
{
|
||||
|
||||
@@ -125,6 +125,9 @@ public readonly struct AnimalContext(float[] values, NeedSet needs, float intell
|
||||
public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, NeedSet needs)
|
||||
: QuerySystem<AnimalNeeds, AnimalOrganism, Health>
|
||||
{
|
||||
// Прибавка к обмену веществ при устойчивости к яду 1.0 (цена детоксикации, коэволюция C2).
|
||||
private const float ToleranceMetabolicCost = 0.5f;
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
var days = clock.DeltaTime / secondsPerDay;
|
||||
@@ -146,7 +149,14 @@ public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, Need
|
||||
continue; // защита от старого/пустого состояния
|
||||
}
|
||||
|
||||
var metabolism = MathF.Max(0.1f, o[i].Traits.Metabolism);
|
||||
// Цена устойчивости к яду (коэволюция C2): детоксикация метаболически затратна — чем выше
|
||||
// toxinTolerance, тем быстрее расходуются нужды (зверь голоднее). Без этой платы устойчивость
|
||||
// ушла бы к максимуму у всех и гонка вооружений встала бы. Множитель к обмену веществ.
|
||||
var metabolism =
|
||||
MathF.Max(0.1f, o[i].Traits.Metabolism)
|
||||
* (
|
||||
1f + ToleranceMetabolicCost * Math.Clamp(o[i].Traits.ToxinTolerance, 0f, 1f)
|
||||
);
|
||||
var lethalEmpty = false;
|
||||
for (var k = 0; k < needs.Count; k++)
|
||||
{
|
||||
@@ -237,7 +247,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
||||
AnimalGrowth,
|
||||
Transform2D
|
||||
> _animals;
|
||||
private readonly ArchetypeQuery<Transform2D, PlantGrowth> _plants;
|
||||
private readonly ArchetypeQuery<Transform2D, PlantGrowth, PlantOrganism> _plants;
|
||||
private readonly ArchetypeQuery<Transform2D, Corpse> _corpses;
|
||||
|
||||
public AnimalDecisionSystem(
|
||||
@@ -264,7 +274,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
||||
AnimalGrowth,
|
||||
Transform2D
|
||||
>();
|
||||
_plants = store.Query<Transform2D, PlantGrowth>();
|
||||
_plants = store.Query<Transform2D, PlantGrowth, PlantOrganism>();
|
||||
_corpses = store.Query<Transform2D, Corpse>();
|
||||
}
|
||||
|
||||
@@ -320,20 +330,24 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
||||
brain.DecideIn = Interval + _rng.NextSingle() * Interval; // джиттер — расфазировать скан
|
||||
var pos = t[i].Position;
|
||||
var adult = AnimalFactory.IsAdult(_animalSet[o[i].Species], gr[i].Stage);
|
||||
var radius = VisionRadius(o[i].Species, o[i].Traits.Vision);
|
||||
var self = entities.EntityAt(i);
|
||||
var health = self.GetComponent<Health>().State;
|
||||
// Эффективный интеллект (мозг × сознание) гейтит доступные нужды/действия (фаза A7):
|
||||
// больной/раненый зверь с упавшим сознанием теряет высшие нужды (сон/секс) → рефлексы.
|
||||
var intelligence = AnimalFactory.Intelligence(
|
||||
o[i].Traits,
|
||||
self.GetComponent<Health>().State
|
||||
);
|
||||
var intelligence = AnimalFactory.Intelligence(o[i].Traits, health);
|
||||
// Радиус восприятия по способностям: зрение ищет корм/партнёра/добычу, угрозу замечаем
|
||||
// зрением ИЛИ слухом (повреждённые глаза/уши сужают восприятие).
|
||||
var baseRadius = VisionRadius(o[i].Species, o[i].Traits.Vision);
|
||||
var sight = health?.Capacity(AnimalCapacities.Sight) ?? 1f;
|
||||
var hearing = health?.Capacity(AnimalCapacities.Hearing) ?? 1f;
|
||||
var seeRadius = baseRadius * MathF.Max(0.2f, sight);
|
||||
var senseRadius = baseRadius * MathF.Max(0.2f, MathF.Max(sight, hearing));
|
||||
// Настроение (A8): стресс/истощение (низкое настроение) подавляет тягу к спариванию.
|
||||
var mood = self.GetComponent<Mood>();
|
||||
|
||||
// Бегство (предатор-кластер): жертва, заметившая рядом хищника, спасается — это важнее
|
||||
// любых нужд (инстинкт выживания). Сам хищник от добычи не бежит (см. IsThreatTo).
|
||||
if (TryFindThreat(pos, radius, o[i].Species, o[i].Traits, out var threatPos))
|
||||
if (TryFindThreat(pos, senseRadius, o[i].Species, o[i].Traits, out var threatPos))
|
||||
{
|
||||
brain.Action = AnimalActions.Flee;
|
||||
brain.TargetPlant = -1;
|
||||
@@ -342,7 +356,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
||||
var len = away.Length();
|
||||
brain.Target =
|
||||
len > 0.001f
|
||||
? pos + away / len * (radius + _cellSize)
|
||||
? pos + away / len * (senseRadius + _cellSize)
|
||||
: pos + new Vector2(_cellSize, 0f);
|
||||
continue;
|
||||
}
|
||||
@@ -362,7 +376,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
||||
when eatsMeat
|
||||
&& TryFindKill(
|
||||
pos,
|
||||
radius,
|
||||
seeRadius,
|
||||
o[i].Species,
|
||||
o[i].Traits,
|
||||
self.Id,
|
||||
@@ -377,7 +391,15 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
||||
brain.Target = preyPos;
|
||||
break;
|
||||
case AnimalActions.Eat
|
||||
when eatsPlants && TryFindPlant(pos, radius, out var plant, out var pp):
|
||||
when eatsPlants
|
||||
&& TryFindForage(
|
||||
pos,
|
||||
seeRadius,
|
||||
intelligence,
|
||||
o[i].Traits.ToxinTolerance,
|
||||
out var plant,
|
||||
out var pp
|
||||
):
|
||||
brain.Action = AnimalActions.Eat;
|
||||
brain.TargetPlant = plant;
|
||||
brain.Target = pp;
|
||||
@@ -397,7 +419,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
||||
&& TryFindMate(
|
||||
pos,
|
||||
o[i].IsMale,
|
||||
radius,
|
||||
seeRadius,
|
||||
out var mateId,
|
||||
out var matePos
|
||||
):
|
||||
@@ -586,12 +608,36 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
||||
otherTraits.BodySize <= hunterBody * 1.25f;
|
||||
|
||||
// Ближайшее растение в радиусе (квадрат расстояния); ничьи — по меньшему id (детерминизм).
|
||||
// Порог интеллекта, с которого травоядное РАЗБИРАЕТ корм (избегает ядовитого/невкусного). Ниже —
|
||||
// рефлекторное выедание ближайшего растения (тир мозга 4, как сон/мысли; больной зверь с упавшим
|
||||
// сознанием падает ниже и снова жрёт что попало — последствие гейтинга A7).
|
||||
private const float ForageSmartMinBrain = 0.4f;
|
||||
|
||||
// Вес, с которым воспринимаемый яд (с поправкой на устойчивость) отталкивает от растения, и штраф за
|
||||
// дальность — чтобы умный зверь предпочитал близкий и безопасный корм, а не бежал через всю карту.
|
||||
private const float ToxinAvoidWeight = 1.2f;
|
||||
private const float ForageDistanceWeight = 0.5f;
|
||||
|
||||
// Выбор корма: умный (избегает яда/невкусного) если интеллект ≥ порога, иначе рефлекс — ближайшее.
|
||||
private bool TryFindForage(
|
||||
Vector2 from,
|
||||
float radius,
|
||||
float intelligence,
|
||||
float toxinTolerance,
|
||||
out int plantId,
|
||||
out Vector2 position
|
||||
) =>
|
||||
intelligence >= ForageSmartMinBrain
|
||||
? TryFindBestPlant(from, radius, toxinTolerance, out plantId, out position)
|
||||
: TryFindPlant(from, radius, out plantId, out position);
|
||||
|
||||
// Рефлекс: ближайшее растение в радиусе (ничья — меньший id, детерминизм).
|
||||
private bool TryFindPlant(Vector2 from, float radius, out int plantId, out Vector2 position)
|
||||
{
|
||||
var bestSq = radius * radius;
|
||||
plantId = -1;
|
||||
position = default;
|
||||
foreach (var (transforms, _, entities) in _plants.Chunks)
|
||||
foreach (var (transforms, _, _, entities) in _plants.Chunks)
|
||||
{
|
||||
var t = transforms.Span;
|
||||
for (var i = 0; i < t.Length; i++)
|
||||
@@ -610,6 +656,54 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
||||
return plantId >= 0;
|
||||
}
|
||||
|
||||
// Умный выбор: среди растений в радиусе максимизируем привлекательность = вкусность −
|
||||
// воспринимаемый_яд − штраф_за_дальность. Воспринимаемый яд = toxicity × (1 − устойчивость): чем
|
||||
// выше устойчивость зверя, тем меньше его отпугивает токсичный корм. Если всё в округе невкусно/
|
||||
// ядовито, голод всё равно заставит выбрать наименее плохой (берётся максимум, даже отрицательный).
|
||||
private bool TryFindBestPlant(
|
||||
Vector2 from,
|
||||
float radius,
|
||||
float toxinTolerance,
|
||||
out int plantId,
|
||||
out Vector2 position
|
||||
)
|
||||
{
|
||||
var radiusSq = radius * radius;
|
||||
var tol = Math.Clamp(toxinTolerance, 0f, 1f);
|
||||
var bestScore = float.NegativeInfinity;
|
||||
plantId = -1;
|
||||
position = default;
|
||||
foreach (var (transforms, _, organisms, entities) in _plants.Chunks)
|
||||
{
|
||||
var t = transforms.Span;
|
||||
var o = organisms.Span;
|
||||
for (var i = 0; i < t.Length; i++)
|
||||
{
|
||||
var sq = Vector2.DistanceSquared(from, t[i].Position);
|
||||
if (sq > radiusSq)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ref readonly var tr = ref o[i].Traits;
|
||||
var perceivedToxin = tr.Toxicity * (1f - tol);
|
||||
var score =
|
||||
tr.Palatability
|
||||
- ToxinAvoidWeight * perceivedToxin
|
||||
- ForageDistanceWeight * (radius > 0f ? MathF.Sqrt(sq) / radius : 0f);
|
||||
var id = entities.EntityAt(i).Id;
|
||||
if (score > bestScore || (score == bestScore && id < plantId))
|
||||
{
|
||||
bestScore = score;
|
||||
plantId = id;
|
||||
position = t[i].Position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return plantId >= 0;
|
||||
}
|
||||
|
||||
// Ближайшая кромка воды (без лимита по зрению — звери «помнят» водопои; упрощение фазы A2).
|
||||
private bool TryFindShore(Vector2 from, out Vector2 position)
|
||||
{
|
||||
@@ -696,6 +790,7 @@ public sealed class AnimalActionSystem : BaseSystem
|
||||
private const float GrazeKillAge = 1f; // трава с возрастом ниже этого после выедания исчезает
|
||||
private const float ThirstReliefBelow = 0.15f; // ниже этого жажда — питьё даёт мысль «напилась»
|
||||
private const float FleeSpeedMult = 1.2f; // страх подгоняет жертву чуть быстрее обычного шага
|
||||
private const float PostReproductiveFrac = 0.9f; // доля жизни, после которой самка уже не зачинает
|
||||
|
||||
private readonly EntityStore _store;
|
||||
private readonly PlantSet _plants;
|
||||
@@ -707,6 +802,7 @@ public sealed class AnimalActionSystem : BaseSystem
|
||||
private readonly int _cellSize;
|
||||
private readonly RectF _bounds;
|
||||
private readonly HediffDef? _bleeding;
|
||||
private readonly HediffDef? _poisoned;
|
||||
private readonly Random _rng;
|
||||
private readonly ArchetypeQuery<
|
||||
AnimalBrain,
|
||||
@@ -731,6 +827,7 @@ public sealed class AnimalActionSystem : BaseSystem
|
||||
int cellSize,
|
||||
RectF bounds,
|
||||
HediffDef? bleeding,
|
||||
HediffDef? poisoned,
|
||||
int seed
|
||||
)
|
||||
{
|
||||
@@ -744,6 +841,7 @@ public sealed class AnimalActionSystem : BaseSystem
|
||||
_cellSize = cellSize;
|
||||
_bounds = bounds;
|
||||
_bleeding = bleeding;
|
||||
_poisoned = poisoned;
|
||||
_rng = new Random(seed);
|
||||
_query = store.Query<AnimalBrain, AnimalNeeds, AnimalOrganism, Transform2D, Health>();
|
||||
}
|
||||
@@ -780,6 +878,10 @@ public sealed class AnimalActionSystem : BaseSystem
|
||||
// Способность Moving (падает от ран/болезней) — множитель к скорости: больные медленнее.
|
||||
var moving = hh[i].State.Capacity(AnimalCapacities.Moving);
|
||||
var speed = def.BaseSpeed * MathF.Max(0.2f, o[i].Traits.MoveSpeed) * moving;
|
||||
// Питание (челюсть) масштабирует укус, пищеварение (желудок) — усвоение: их произведение
|
||||
// даёт сытость с корма; раненая челюсть/больной желудок → зверь голоднее (опция 1).
|
||||
var eating = hh[i].State.Capacity(AnimalCapacities.Eating);
|
||||
var foodEff = eating * hh[i].State.Capacity(AnimalCapacities.Digestion);
|
||||
|
||||
switch (brain.Action)
|
||||
{
|
||||
@@ -790,8 +892,19 @@ public sealed class AnimalActionSystem : BaseSystem
|
||||
case AnimalActions.Eat:
|
||||
if (MoveTo(ref pos, brain.Target, speed * seconds))
|
||||
{
|
||||
Refill(values, AnimalActions.Eat, days);
|
||||
Graze(brain.TargetPlant, days, def.ForageBiteDays);
|
||||
Refill(values, AnimalActions.Eat, days, foodEff);
|
||||
Graze(
|
||||
brain.TargetPlant,
|
||||
days,
|
||||
def.ForageBiteDays * MathF.Max(0.2f, eating)
|
||||
);
|
||||
// Коэволюция: защита растения бьёт по поедателю (яд/шипы).
|
||||
ApplyPlantDefense(
|
||||
brain.TargetPlant,
|
||||
hh[i].State,
|
||||
o[i].Traits.ToxinTolerance,
|
||||
days
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -836,7 +949,7 @@ public sealed class AnimalActionSystem : BaseSystem
|
||||
break;
|
||||
|
||||
case AnimalActions.Hunt:
|
||||
Hunt(ref brain, ref pos, values, def, speed * seconds, days);
|
||||
Hunt(ref brain, ref pos, values, def, speed * seconds, days, foodEff);
|
||||
break;
|
||||
|
||||
case AnimalActions.Flee:
|
||||
@@ -883,7 +996,8 @@ public sealed class AnimalActionSystem : BaseSystem
|
||||
float[] values,
|
||||
AnimalDef def,
|
||||
float step,
|
||||
float days
|
||||
float days,
|
||||
float foodEff
|
||||
)
|
||||
{
|
||||
if (
|
||||
@@ -914,7 +1028,7 @@ public sealed class AnimalActionSystem : BaseSystem
|
||||
}
|
||||
|
||||
ref var corpse = ref target.GetComponent<Corpse>();
|
||||
Refill(values, AnimalActions.Eat, days);
|
||||
Refill(values, AnimalActions.Eat, days, foodEff);
|
||||
corpse.Meat -= def.ForageBiteDays * days; // расход мяса той же «силой укуса», что и выедание
|
||||
if (corpse.Meat <= 0f && !_consumed.Contains(target))
|
||||
{
|
||||
@@ -961,8 +1075,9 @@ public sealed class AnimalActionSystem : BaseSystem
|
||||
}
|
||||
}
|
||||
|
||||
// Восполняет нужду, которую утоляет действие (по NeedSet), на её FeedPerDay.
|
||||
private void Refill(float[] values, string action, float days)
|
||||
// Восполняет нужду, которую утоляет действие (по NeedSet), на её FeedPerDay × эффективность
|
||||
// (питание/пищеварение для еды — слабая челюсть/больной желудок дают меньше сытости с того же корма).
|
||||
private void Refill(float[] values, string action, float days, float efficiency = 1f)
|
||||
{
|
||||
var index = _needs.NeedForAction(action);
|
||||
if (index < 0 || values is null || index >= values.Length)
|
||||
@@ -970,7 +1085,11 @@ public sealed class AnimalActionSystem : BaseSystem
|
||||
return;
|
||||
}
|
||||
|
||||
values[index] = Math.Clamp(values[index] + _needs[index].FeedPerDay * days, 0f, 1f);
|
||||
values[index] = Math.Clamp(
|
||||
values[index] + _needs[index].FeedPerDay * days * efficiency,
|
||||
0f,
|
||||
1f
|
||||
);
|
||||
}
|
||||
|
||||
// Двигает позицию к цели на step; возвращает true, если уже у цели (можно исполнять действие).
|
||||
@@ -1010,6 +1129,65 @@ public sealed class AnimalActionSystem : BaseSystem
|
||||
}
|
||||
}
|
||||
|
||||
// Доза яда за день укуса при токсичности 1 и нулевой устойчивости (масштабируется тяжести Poisoned).
|
||||
private const float PoisonDosePerDay = 1.6f;
|
||||
|
||||
// Порог шипов, выше которого укус о растение травмирует, и масштабы урона/кровопотери от шипов.
|
||||
private const float ThornThreshold = 0.2f;
|
||||
private const float ThornPartDamage = 4f;
|
||||
private const float ThornBloodLoss = 0.02f;
|
||||
|
||||
// Коэволюция: при поедании растение «отвечает» — токсичность отравляет (тем сильнее, чем ниже
|
||||
// устойчивость зверя к яду), шипы наносят лёгкую травму. Доза яда копится в хедифе Poisoned; если
|
||||
// зверь ест яд быстрее, чем выводит, тяжесть дойдёт до летальной. Детерминированно (общий _rng).
|
||||
private void ApplyPlantDefense(
|
||||
int plantId,
|
||||
HealthState? health,
|
||||
float toxinTolerance,
|
||||
float days
|
||||
)
|
||||
{
|
||||
if (
|
||||
health is null
|
||||
|| plantId < 0
|
||||
|| !_store.TryGetEntityById(plantId, out var plant)
|
||||
|| plant.IsNull
|
||||
|| !plant.HasComponent<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()
|
||||
@@ -1039,15 +1217,25 @@ public sealed class AnimalActionSystem : BaseSystem
|
||||
if (!mother.HasComponent<Pregnant>())
|
||||
{
|
||||
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(
|
||||
new Pregnant
|
||||
{
|
||||
FatherGenome = father.GetComponent<AnimalOrganism>().Genome,
|
||||
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(b, mateNeed);
|
||||
@@ -1468,6 +1656,8 @@ public sealed class AnimalPregnancySystem : BaseSystem
|
||||
Litter = preg.Litter,
|
||||
Generation = o[i].Generation + 1,
|
||||
Position = t[i].Position,
|
||||
Oviparous = o[i].Traits.Oviparous,
|
||||
IncubateDays = MathF.Max(1f, o[i].Traits.GestationDays),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1497,6 +1687,22 @@ public sealed class AnimalPregnancySystem : BaseSystem
|
||||
);
|
||||
var offset =
|
||||
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(
|
||||
_store,
|
||||
_animals,
|
||||
@@ -1508,6 +1714,8 @@ public sealed class AnimalPregnancySystem : BaseSystem
|
||||
birth.Generation,
|
||||
_cellSize
|
||||
);
|
||||
}
|
||||
|
||||
count++;
|
||||
}
|
||||
}
|
||||
@@ -1522,6 +1730,90 @@ public sealed class AnimalPregnancySystem : BaseSystem
|
||||
public int Litter;
|
||||
public int Generation;
|
||||
public Vector2 Position;
|
||||
public bool Oviparous;
|
||||
public float IncubateDays;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 HediffDef[] _ambient;
|
||||
private readonly Random _rng;
|
||||
private readonly ArchetypeQuery<Health, AnimalOrganism> _query;
|
||||
private readonly ArchetypeQuery<Health, AnimalOrganism, AnimalGrowth> _query;
|
||||
private readonly List<Entity> _deaths = [];
|
||||
private float _accum;
|
||||
|
||||
// Сенесценс (старение): спад способностей начинается с этой доли продолжительности жизни и достигает
|
||||
// максимума к её концу. Дряхлеющий зверь слабее (медленнее, хуже видит/фильтрует кровь) и уязвимее.
|
||||
private const float SenescenceStart = 0.6f;
|
||||
private const float SenescenceMax = 0.45f;
|
||||
|
||||
public AnimalHealthSystem(
|
||||
EntityStore store,
|
||||
AnimalSet animals,
|
||||
@@ -1564,7 +1861,19 @@ public sealed class AnimalHealthSystem : BaseSystem
|
||||
_secondsPerDay = secondsPerDay;
|
||||
_ambient = ambient;
|
||||
_rng = new Random(seed);
|
||||
_query = store.Query<Health, AnimalOrganism>();
|
||||
_query = store.Query<Health, AnimalOrganism, AnimalGrowth>();
|
||||
}
|
||||
|
||||
/// <summary>Множитель способностей от старения (1 — молод/расцвет, <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()
|
||||
@@ -1579,9 +1888,11 @@ public sealed class AnimalHealthSystem : BaseSystem
|
||||
_accum = 0f;
|
||||
_deaths.Clear();
|
||||
|
||||
foreach (var (healths, organisms, entities) in _query.Chunks)
|
||||
foreach (var (healths, organisms, growths, entities) in _query.Chunks)
|
||||
{
|
||||
var h = healths.Span;
|
||||
var o = organisms.Span;
|
||||
var g = growths.Span;
|
||||
for (var i = 0; i < h.Length; i++)
|
||||
{
|
||||
var state = h[i].State;
|
||||
@@ -1601,7 +1912,12 @@ public sealed class AnimalHealthSystem : BaseSystem
|
||||
}
|
||||
}
|
||||
|
||||
var lethal = state.AdvanceHediffs(days);
|
||||
// Фильтрация крови (почки/печень) ускоряет вывод токсинов и набор иммунитета: повреждённый
|
||||
// орган → медленнее выздоровление от яда/болезни (для тела без неё Capacity вернёт 1).
|
||||
var lethal = state.AdvanceHediffs(
|
||||
days,
|
||||
state.Capacity(AnimalCapacities.BloodFiltration)
|
||||
);
|
||||
// Кровопотеря от ран (кровотечения), иначе — постепенное восстановление крови в покое.
|
||||
var bleed = 0f;
|
||||
foreach (var hd in state.Hediffs)
|
||||
@@ -1622,7 +1938,9 @@ public sealed class AnimalHealthSystem : BaseSystem
|
||||
);
|
||||
}
|
||||
|
||||
state.RecomputeCapacities();
|
||||
// Старение: к концу жизни способности дряхлеют (медленнее/слабее, уязвимее к ранам/болезни).
|
||||
var senescence = SenescenceFactor(g[i].AgeDays, o[i].Traits.Lifespan);
|
||||
state.RecomputeCapacities(senescence);
|
||||
if (
|
||||
lethal
|
||||
|| state.BloodLevel <= 0f
|
||||
|
||||
@@ -9,8 +9,27 @@ public static class AnimalCapacities
|
||||
public const string Moving = "Moving";
|
||||
public const string Breathing = "Breathing";
|
||||
public const string BloodPumping = "BloodPumping";
|
||||
public const string BloodFiltration = "BloodFiltration";
|
||||
public const string Sight = "Sight";
|
||||
public const string Hearing = "Hearing";
|
||||
public const string Talking = "Talking";
|
||||
public const string Eating = "Eating";
|
||||
public const string Digestion = "Digestion";
|
||||
|
||||
/// <summary>Порядок отображения способностей в UI (сверху вниз). Прочие модовые — после, как есть.</summary>
|
||||
public static readonly string[] DisplayOrder =
|
||||
[
|
||||
Consciousness,
|
||||
Moving,
|
||||
Sight,
|
||||
Hearing,
|
||||
Talking,
|
||||
Eating,
|
||||
Breathing,
|
||||
BloodPumping,
|
||||
BloodFiltration,
|
||||
Digestion,
|
||||
];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -24,7 +43,11 @@ public static class CapacityCalc
|
||||
/// <summary>Считает карту способностей из инстансов частей, уровня крови (0..1) и боли (0..1).</summary>
|
||||
public static Dictionary<string, float> Compute(PartInstance[] parts, float blood, float pain)
|
||||
{
|
||||
// Сырой вклад каждой способности (Σ доля·HP) и НАБОР объявленных телом способностей: вид имеет
|
||||
// только те, к которым его части вообще причастны (у оленя нет манипуляции). Объявленность —
|
||||
// статична (по дефам частей), поэтому уничтоженный орган всё равно покажет способность на 0%.
|
||||
var raw = new Dictionary<string, float>(System.StringComparer.Ordinal);
|
||||
var declared = new HashSet<string>(System.StringComparer.Ordinal);
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (part.Def is null)
|
||||
@@ -35,36 +58,52 @@ public static class CapacityCalc
|
||||
var fraction = part.Fraction;
|
||||
foreach (var (capacity, contribution) in part.Def.Capacities)
|
||||
{
|
||||
declared.Add(capacity);
|
||||
raw[capacity] = raw.GetValueOrDefault(capacity) + contribution * fraction;
|
||||
}
|
||||
}
|
||||
|
||||
float Raw(string id) => raw.GetValueOrDefault(id);
|
||||
static float Clamp01(float v) => v < 0f ? 0f : v > 1f ? 1f : v;
|
||||
static float Clamp01(float v) =>
|
||||
v < 0f ? 0f
|
||||
: v > 1f ? 1f
|
||||
: v;
|
||||
|
||||
// Базовые (независимые) способности: кровоснабжение/дыхание/фильтрация × уровень крови.
|
||||
var bloodLevel = Clamp01(blood);
|
||||
var bloodPumping = Raw(AnimalCapacities.BloodPumping) * bloodLevel;
|
||||
var breathing = Raw(AnimalCapacities.Breathing) * bloodLevel;
|
||||
var bloodFiltration = Raw(AnimalCapacities.BloodFiltration) * bloodLevel;
|
||||
// Сознание зависит от кровоснабжения, дыхания и боли (модель RimWorld).
|
||||
var consciousness =
|
||||
Raw(AnimalCapacities.Consciousness)
|
||||
* Clamp01(bloodPumping)
|
||||
* Clamp01(breathing)
|
||||
* (1f - Clamp01(pain));
|
||||
var cons = Clamp01(consciousness);
|
||||
var pump = Clamp01(bloodPumping);
|
||||
|
||||
var result = new Dictionary<string, float>(System.StringComparer.Ordinal)
|
||||
// Производные способности зависят от сознания (и кровоснабжения для движения/пищеварения).
|
||||
var computed = new Dictionary<string, float>(System.StringComparer.Ordinal)
|
||||
{
|
||||
[AnimalCapacities.BloodPumping] = bloodPumping,
|
||||
[AnimalCapacities.Breathing] = breathing,
|
||||
[AnimalCapacities.BloodFiltration] = bloodFiltration,
|
||||
[AnimalCapacities.Consciousness] = consciousness,
|
||||
[AnimalCapacities.Moving] = Raw(AnimalCapacities.Moving) * consciousness * Clamp01(bloodPumping),
|
||||
[AnimalCapacities.Sight] = Raw(AnimalCapacities.Sight) * consciousness,
|
||||
[AnimalCapacities.Digestion] = Raw(AnimalCapacities.Digestion) * Clamp01(bloodPumping),
|
||||
[AnimalCapacities.Moving] = Raw(AnimalCapacities.Moving) * cons * pump,
|
||||
[AnimalCapacities.Sight] = Raw(AnimalCapacities.Sight) * cons,
|
||||
[AnimalCapacities.Hearing] = Raw(AnimalCapacities.Hearing) * cons,
|
||||
[AnimalCapacities.Talking] = Raw(AnimalCapacities.Talking) * cons,
|
||||
[AnimalCapacities.Eating] = Raw(AnimalCapacities.Eating) * cons,
|
||||
[AnimalCapacities.Digestion] = Raw(AnimalCapacities.Digestion) * pump,
|
||||
};
|
||||
|
||||
// Модовые способности, не охваченные зависимостями, — сырыми.
|
||||
foreach (var (capacity, value) in raw)
|
||||
// В результат — только объявленные телом способности: известные по формуле зависимостей, прочие
|
||||
// (модовые) — сырым вкладом. Так у вида видны ровно его способности, а не весь каталог.
|
||||
var result = new Dictionary<string, float>(System.StringComparer.Ordinal);
|
||||
foreach (var capacity in declared)
|
||||
{
|
||||
result.TryAdd(capacity, value);
|
||||
result[capacity] = computed.TryGetValue(capacity, out var v) ? v : Raw(capacity);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -57,6 +57,7 @@ public sealed class PlantGrowthSystem(
|
||||
var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
|
||||
var rate =
|
||||
traits.Vigor
|
||||
* traits.DefenseGrowthFactor() // цена защиты: токсичные/колючие растут медленнее
|
||||
* Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance)
|
||||
* Suitability.Trapezoid(temperature, tMin, tLow, tHigh, tMax)
|
||||
* Suitability.Gaussian(
|
||||
|
||||
@@ -32,6 +32,16 @@ public struct PlantPhenotype
|
||||
public float HarvestAmount;
|
||||
public float LeafHue;
|
||||
|
||||
// --- Защита (коэволюция с травоядными) ---
|
||||
/// <summary>Токсичность [0..1]: отравляет поедателя; цена — замедление роста.</summary>
|
||||
public float Toxicity;
|
||||
|
||||
/// <summary>Шипы [0..1]: травмируют поедателя; цена — замедление роста.</summary>
|
||||
public float Thorns;
|
||||
|
||||
/// <summary>Вкусность [0..1]: обратная привлекательность для умных травоядных (избегание в C2).</summary>
|
||||
public float Palatability;
|
||||
|
||||
/// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary>
|
||||
public bool IsVariant;
|
||||
|
||||
@@ -71,9 +81,26 @@ public struct PlantPhenotype
|
||||
FruitSeason = (int)MathF.Round(Math.Clamp(T("fruitSeason"), 0f, 3f)),
|
||||
HarvestAmount = T("harvestAmount"),
|
||||
LeafHue = T("leafHue"),
|
||||
Toxicity = T("toxicity"),
|
||||
Thorns = T("thorns"),
|
||||
Palatability = T("palatability"),
|
||||
IsVariant = T("variant") >= 0.5f,
|
||||
};
|
||||
}
|
||||
|
||||
// Цена защиты: токсичность и шипы отнимают ресурсы у роста. Без этой платы отбор гнал бы защиту к
|
||||
// максимуму у всех растений и коэволюция бы встала — а так под слабым выпасом выгоднее расти быстрее.
|
||||
private const float ToxicityGrowthCost = 0.4f;
|
||||
private const float ThornsGrowthCost = 0.3f;
|
||||
private const float MinGrowthFactor = 0.2f;
|
||||
|
||||
/// <summary>Множитель скорости роста с учётом цены защиты (1 — без защиты, ≥ <c>MinGrowthFactor</c>).</summary>
|
||||
public readonly float DefenseGrowthFactor() =>
|
||||
Math.Clamp(
|
||||
1f - ToxicityGrowthCost * Toxicity - ThornsGrowthCost * Thorns,
|
||||
MinGrowthFactor,
|
||||
1f
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -331,6 +331,13 @@ internal sealed class InspectPanel
|
||||
traits.LeafHue
|
||||
)
|
||||
);
|
||||
if (traits.Toxicity > 0.01f || traits.Thorns > 0.01f)
|
||||
{
|
||||
text.AppendLine(
|
||||
languages.Format("inspect.gene.defense", traits.Toxicity, traits.Thorns)
|
||||
);
|
||||
}
|
||||
|
||||
if (traits.IsVariant)
|
||||
{
|
||||
text.AppendLine(languages.Get("inspect.gene.variant"));
|
||||
@@ -435,12 +442,27 @@ internal sealed class InspectPanel
|
||||
text.AppendLine(
|
||||
languages.Format("inspect.health.blood", state.BloodLevel * 100f, state.Pain * 100f)
|
||||
);
|
||||
if (state.Parts.Length > 0)
|
||||
if (state.Capacities.Count > 0)
|
||||
{
|
||||
text.AppendLine(languages.Get("inspect.health.caps"));
|
||||
AppendCapacity(text, state, AnimalCapacities.Consciousness, "cap.consciousness");
|
||||
AppendCapacity(text, state, AnimalCapacities.Moving, "cap.moving");
|
||||
AppendCapacity(text, state, AnimalCapacities.Sight, "cap.sight");
|
||||
// Сначала известные способности в порядке отображения, затем прочие (модовые) — как есть.
|
||||
var shown = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var capId in AnimalCapacities.DisplayOrder)
|
||||
{
|
||||
if (state.Capacities.ContainsKey(capId))
|
||||
{
|
||||
AppendCapacity(text, state, capId, CapacityLabel(capId));
|
||||
shown.Add(capId);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var capId in state.Capacities.Keys)
|
||||
{
|
||||
if (shown.Add(capId))
|
||||
{
|
||||
AppendCapacity(text, state, capId, CapacityLabel(capId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.Hediffs.Count > 0)
|
||||
@@ -463,6 +485,9 @@ internal sealed class InspectPanel
|
||||
}
|
||||
}
|
||||
|
||||
// Ключ локализации метки способности: cap.<id в нижнем регистре> (cap.consciousness, cap.bloodpumping…).
|
||||
private static string CapacityLabel(string capId) => "cap." + capId.ToLowerInvariant();
|
||||
|
||||
private void AppendCapacity(
|
||||
StringBuilder text,
|
||||
HealthState state,
|
||||
|
||||
Reference in New Issue
Block a user