From 880704530398688612fecd8020f9b2e83f5f09b6 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sun, 14 Jun 2026 14:13:51 +0300 Subject: [PATCH 1/5] =?UTF-8?q?Plant=E2=80=93herbivore=20coevolution=20+?= =?UTF-8?q?=20RimWorld-style=20health=20capacities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the first plant↔animal coupling beyond "herbivore eats plant": a toxin/ thorns ↔ tolerance arms race built on the existing gene + hediff engines, plus the fuller RimWorld capacity set on the health scaffold. Coevolution C1 — plant defense → poisons/injures the eater: - Plant genes GeneToxicity/GeneThorns/GenePalatability (GenomeDef + PlantSet template + PlantPhenotype); animal gene GeneToxinTolerance. - Hediff Poisoned (non-progressing; grows from eating via HealthState.Intensify, immunity heals it, overdose is lethal). - AnimalActionSystem.ApplyPlantDefense on each bite: poison dose = toxicity × (1 − tolerance); thorns → ApplyInjury (reuses predator-cluster). - Cost of defense: PlantGrowthSystem growth ×= DefenseGrowthFactor() (1 − 0.4·toxicity − 0.3·thorns) — without a cost defense would max out and coevolution would stall. - Showcase: cactus thorns, poisonous mushroom; grass starts clean so toxicity can evolve on the staple. Deer/boar get small starting tolerance. Coevolution C2 — discrimination + the other half of the arms race: - Smart foraging (reuses A7 intelligence gating): herbivores with effective intelligence ≥ tier-4 weigh plants by palatability − perceived-toxin − distance; reflex grazers (or sick animals with dropped consciousness) eat the nearest plant and risk poison. - Cost of tolerance: detox raises metabolism (need decay ×= 1 + 0.5·tolerance), closing the arms race so tolerance doesn't fix at 1. - popstats reports mean toxinTolerance and a plants line (mean toxicity/thorns/ palatability) to watch the drift. Health capacities (RimWorld parity, user-requested): - New capacities BloodFiltration/Hearing/Talking/Eating; CapacityCalc rewritten to emit only capacities the body declares (a destroyed organ still shows 0%) with the full dependency chain (blood → pumping/breathing/filtration → consciousness → moving/sight/hearing/talking/eating, digestion). - Quadruped body gains kidneys + liver (filtration), ears (hearing), jaw/tongue (eating/talking). Inspector health tab lists all of a body's capacities. Localization (ru/en) for new genes' defense line, Poisoned, and the new capacity labels. Build + --check-content clean (11 def types, 40 genes, 39 traits). Not GUI-verified (headless can't render the world scene). Co-Authored-By: Claude Opus 4.8 --- Mods/Core/Defs/Animals/Animals.json | 6 +- Mods/Core/Defs/Bodies/Bodies.json | 19 ++- Mods/Core/Defs/Genes/Animal.json | 7 + Mods/Core/Defs/Genes/Content.json | 15 +++ Mods/Core/Defs/Hediffs/Hediffs.json | 10 ++ Mods/Core/Defs/Plants/_Bases.json | 6 +- Mods/Core/Languages/en/ui.json | 9 ++ Mods/Core/Languages/ru/ui.json | 9 ++ src/LittleSim/Content/GameDefs.cs | 9 ++ src/LittleSim/Content/PlantSet.cs | 4 + src/LittleSim/Scenes/WorldScene.cs | 42 +++++- src/LittleSim/Sim/AnimalOrganism.cs | 4 + src/LittleSim/Sim/AnimalSystems.cs | 169 ++++++++++++++++++++++++- src/LittleSim/Sim/CapacityCalc.cs | 55 ++++++-- src/LittleSim/Sim/PlantGrowthSystem.cs | 1 + src/LittleSim/Sim/PlantOrganism.cs | 27 ++++ src/LittleSim/UI/InspectPanel.cs | 33 ++++- 17 files changed, 400 insertions(+), 25 deletions(-) diff --git a/Mods/Core/Defs/Animals/Animals.json b/Mods/Core/Defs/Animals/Animals.json index 0b10d67..8ace26b 100644 --- a/Mods/Core/Defs/Animals/Animals.json +++ b/Mods/Core/Defs/Animals/Animals.json @@ -30,7 +30,8 @@ "GeneBreedingSeason": 2, "GeneGestationDays": 30, "GeneLitterSize": 1, - "GeneHerbivory": 1.0 + "GeneHerbivory": 1.0, + "GeneToxinTolerance": 0.1 } }, @@ -97,7 +98,8 @@ "GeneLitterSize": 5, "GeneHerbivory": 0.4, "GeneCarnivory": 0.2, - "GeneOmnivory": 0.85 + "GeneOmnivory": 0.85, + "GeneToxinTolerance": 0.25 } } ] diff --git a/Mods/Core/Defs/Bodies/Bodies.json b/Mods/Core/Defs/Bodies/Bodies.json index 88c1e06..c688659 100644 --- a/Mods/Core/Defs/Bodies/Bodies.json +++ b/Mods/Core/Defs/Bodies/Bodies.json @@ -7,23 +7,36 @@ { "defName": "Quadruped", "parts": [ - { "name": "torso", "coverage": 0.32, "maxHp": 40, "vital": true }, + { "name": "torso", "coverage": 0.3, "maxHp": 40, "vital": true }, { "name": "heart", "parent": "torso", "coverage": 0.02, "maxHp": 12, "vital": true, "capacities": { "BloodPumping": 1.0 } }, { "name": "lungLeft", "parent": "torso", "coverage": 0.03, "maxHp": 12, "capacities": { "Breathing": 0.5 } }, { "name": "lungRight", "parent": "torso", "coverage": 0.03, "maxHp": 12, "capacities": { "Breathing": 0.5 } }, - { "name": "liver", "parent": "torso", "coverage": 0.03, "maxHp": 14, "vital": true }, + { "name": "liver", "parent": "torso", "coverage": 0.03, "maxHp": 14, "vital": true, + "capacities": { "BloodFiltration": 0.4 } }, + { "name": "kidneyLeft", "parent": "torso", "coverage": 0.02, "maxHp": 10, + "capacities": { "BloodFiltration": 0.3 } }, + { "name": "kidneyRight", "parent": "torso", "coverage": 0.02, "maxHp": 10, + "capacities": { "BloodFiltration": 0.3 } }, { "name": "stomach", "parent": "torso", "coverage": 0.03, "maxHp": 12, "capacities": { "Digestion": 1.0 } }, - { "name": "head", "coverage": 0.1, "maxHp": 25 }, + { "name": "head", "coverage": 0.08, "maxHp": 25 }, { "name": "brain", "parent": "head", "coverage": 0.02, "maxHp": 12, "vital": true, "capacities": { "Consciousness": 1.0 } }, { "name": "eyeLeft", "parent": "head", "coverage": 0.015, "maxHp": 8, "capacities": { "Sight": 0.5 } }, { "name": "eyeRight", "parent": "head", "coverage": 0.015, "maxHp": 8, "capacities": { "Sight": 0.5 } }, + { "name": "earLeft", "parent": "head", "coverage": 0.01, "maxHp": 8, + "capacities": { "Hearing": 0.5 } }, + { "name": "earRight", "parent": "head", "coverage": 0.01, "maxHp": 8, + "capacities": { "Hearing": 0.5 } }, + { "name": "jaw", "parent": "head", "coverage": 0.02, "maxHp": 10, + "capacities": { "Eating": 1.0, "Talking": 0.5 } }, + { "name": "tongue", "parent": "head", "coverage": 0.01, "maxHp": 8, + "capacities": { "Talking": 0.5 } }, { "name": "legFrontLeft", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } }, { "name": "legFrontRight", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } }, { "name": "legBackLeft", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } }, diff --git a/Mods/Core/Defs/Genes/Animal.json b/Mods/Core/Defs/Genes/Animal.json index 452e3ef..a5c05fa 100644 --- a/Mods/Core/Defs/Genes/Animal.json +++ b/Mods/Core/Defs/Genes/Animal.json @@ -46,6 +46,13 @@ "default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"], "effects": { "omnivory": "value" } }, + // Устойчивость к растительным ядам (коэволюция): снижает дозу отравления при поедании токсичных + // растений (effectivePoison = toxicity × (1 − toxinTolerance), см. AnimalActionSystem). Растёт под + // давлением ядовитого корма — другая сторона гонки вооружений с GeneToxicity растений. + { "defName": "GeneToxinTolerance", "parent": "BaseNumericGene", "label": "gene.toxinTolerance", + "default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"], + "effects": { "toxinTolerance": "value" } }, + // Пол как локус (фаза A3): аллели {X=0, Y=1}; самка XX, самец XY. Эффекта нет — пол читается из // самих аллелей (наличие Y), не из выраженного значения. mutationChance 0 (X не мутирует в Y). // Генерация особи-основателя задаёт пол явно (XX или XY), чтобы не возник невозможный YY. diff --git a/Mods/Core/Defs/Genes/Content.json b/Mods/Core/Defs/Genes/Content.json index 17d9258..92e5d64 100644 --- a/Mods/Core/Defs/Genes/Content.json +++ b/Mods/Core/Defs/Genes/Content.json @@ -15,6 +15,21 @@ "default": 0.33, "min": 0, "max": 1, "spread": 0.04, "tags": ["morphology", "color"], "effects": { "leafHue": "value" } }, + // --- Защита растения (коэволюция с травоядными) --- + // Токсичность отравляет поедателя (hediff Poisoned, тем сильнее, чем ниже его устойчивость к яду); + // шипы наносят поедателю лёгкую травму; вкусность (palatability) — обратная привлекательность для + // умных травоядных (избегание появится в C2). У защиты ЕСТЬ ЦЕНА: токсичность/шипы тормозят рост + // (см. PlantGrowthSystem), иначе все растения дошли бы до максимума и коэволюция бы встала. + { "defName": "GeneToxicity", "parent": "BaseNumericGene", "label": "gene.toxicity", + "default": 0.0, "min": 0.0, "max": 1.0, "tags": ["defense"], + "effects": { "toxicity": "value" } }, + { "defName": "GeneThorns", "parent": "BaseNumericGene", "label": "gene.thorns", + "default": 0.0, "min": 0.0, "max": 1.0, "tags": ["defense"], + "effects": { "thorns": "value" } }, + { "defName": "GenePalatability", "parent": "BaseNumericGene", "label": "gene.palatability", + "default": 1.0, "min": 0.0, "max": 1.0, "tags": ["defense"], + "effects": { "palatability": "value" } }, + // Производный ген: признак собирается группировкой по регэкспу — сумма всех генов-толерантностей // (демонстрация gsom-функций фазы G5). Собственное значение гена не используется. { "defName": "GeneHardiness", "parent": "BaseNumericGene", "label": "gene.hardiness", diff --git a/Mods/Core/Defs/Hediffs/Hediffs.json b/Mods/Core/Defs/Hediffs/Hediffs.json index 00b001e..f684124 100644 --- a/Mods/Core/Defs/Hediffs/Hediffs.json +++ b/Mods/Core/Defs/Hediffs/Hediffs.json @@ -22,6 +22,16 @@ "defName": "Bleeding", "label": "hediff.bleeding", "initialSeverity": 0.3, "immunityPerDay": 0.5, "bloodLossPerDay": 0.8, "pain": 0.25, "capMods": { "Moving": 0.92, "Consciousness": 0.95 } + }, + + // Отравление растительным ядом (коэволюция): сама не прогрессирует (severityPerDay 0) — тяжесть + // копится при поедании токсичных растений (HealthState.Intensify в AnimalActionSystem). Иммунитет + // её рассасывает (~1.5 дня), но если зверь ест яд быстрее, чем выводит, тяжесть доходит до 1 → смерть. + // Это селективное давление: ядовитый корм опасен для травоядных с низкой устойчивостью. + { + "defName": "Poisoned", "label": "hediff.poisoned", + "initialSeverity": 0.0, "immunityPerDay": 0.65, "lethalSeverity": 1.0, "pain": 0.2, + "capMods": { "Moving": 0.85, "Consciousness": 0.9, "Digestion": 0.7 } } ] } diff --git a/Mods/Core/Defs/Plants/_Bases.json b/Mods/Core/Defs/Plants/_Bases.json index dbbda4c..e5d2935 100644 --- a/Mods/Core/Defs/Plants/_Bases.json +++ b/Mods/Core/Defs/Plants/_Bases.json @@ -66,7 +66,8 @@ "optimalFertility": 0.4, "fertilityTolerance": 0.5, "vigor": 0.5, "lifespan": 300, "dispersalRange": 2, "reproduceInterval": 25, "selfPollination": 0.6, "mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08, - "fruitYield": 3, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.5 }, + "fruitYield": 3, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.5, + "thorns": 0.6, "palatability": 0.5 }, "stages": [ { "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 5, "label": "plant.stage.sprout" }, { "label": "plant.stage.mature" } @@ -81,7 +82,8 @@ "optimalFertility": 1.6, "fertilityTolerance": 1.1, "vigor": 1.2, "lifespan": 25, "dispersalRange": 3, "reproduceInterval": 4, "selfPollination": 0.9, "mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1, - "fruitYield": 0, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.5 }, + "fruitYield": 0, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.5, + "toxicity": 0.5, "palatability": 0.4 }, "stages": [ { "texture": "things/plant/seed_default", "sizeCells": 0.4, "growDays": 2, "label": "plant.stage.sprout" }, { "label": "plant.stage.mature" } diff --git a/Mods/Core/Languages/en/ui.json b/Mods/Core/Languages/en/ui.json index 5f1abda..57f6032 100644 --- a/Mods/Core/Languages/en/ui.json +++ b/Mods/Core/Languages/en/ui.json @@ -30,6 +30,7 @@ "inspect.gene.repro": "Dispersal {0:0.0} · interval {1:0.0} d", "inspect.gene.repro2": "Self-pollin. {0:0.00} · mutation {1:0.00}", "inspect.gene.hardy": "Cold/heat {0:0}/{1:0} · leaf hue {2:0.00}", + "inspect.gene.defense": "Defense: toxin {0:0.00} · thorns {1:0.00}", "inspect.gene.variant": "Morph: recessive variant", "inspect.harvest": "Harvest: {0} ×{1:0.#}", "inspect.fruit": "Fruit: {0} ×{1:0.#} ({2}) · ripe {3:0.#}", @@ -61,9 +62,17 @@ "cap.consciousness": "consciousness", "cap.moving": "moving", "cap.sight": "sight", + "cap.hearing": "hearing", + "cap.talking": "talking", + "cap.eating": "eating", + "cap.breathing": "breathing", + "cap.bloodpumping": "blood pumping", + "cap.bloodfiltration": "blood filtration", + "cap.digestion": "digestion", "hediff.rut": "rut", "hediff.fever": "fever", "hediff.bleeding": "bleeding", + "hediff.poisoned": "poisoned", "inspect.hint": "LMB — select · RMB/Esc — clear", "hud.paused": "PAUSED", "menu.title": "LittleSim", diff --git a/Mods/Core/Languages/ru/ui.json b/Mods/Core/Languages/ru/ui.json index 0c6d660..00b9a3e 100644 --- a/Mods/Core/Languages/ru/ui.json +++ b/Mods/Core/Languages/ru/ui.json @@ -30,6 +30,7 @@ "inspect.gene.repro": "Расселение {0:0.0} кл · период {1:0.0} дн", "inspect.gene.repro2": "Самоопыление {0:0.00} · мутации {1:0.00}", "inspect.gene.hardy": "Морозо/жаро {0:0}/{1:0} · оттенок {2:0.00}", + "inspect.gene.defense": "Защита: яд {0:0.00} · шипы {1:0.00}", "inspect.gene.variant": "Морфа: рецессивный вариант", "inspect.harvest": "Сбор: {0} ×{1:0.#}", "inspect.fruit": "Плоды: {0} ×{1:0.#} ({2}) · зрелых {3:0.#}", @@ -61,9 +62,17 @@ "cap.consciousness": "сознание", "cap.moving": "движение", "cap.sight": "зрение", + "cap.hearing": "слух", + "cap.talking": "речь", + "cap.eating": "питание", + "cap.breathing": "дыхание", + "cap.bloodpumping": "кровоснабжение", + "cap.bloodfiltration": "фильтрация крови", + "cap.digestion": "пищеварение", "hediff.rut": "гон", "hediff.fever": "лихорадка", "hediff.bleeding": "кровотечение", + "hediff.poisoned": "отравление", "inspect.hint": "ЛКМ — выбрать · ПКМ/Esc — снять", "hud.paused": "ПАУЗА", "menu.title": "LittleSim", diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs index 436545e..43c6637 100644 --- a/src/LittleSim/Content/GameDefs.cs +++ b/src/LittleSim/Content/GameDefs.cs @@ -143,6 +143,15 @@ public sealed class GenomeDef /// Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета. public float LeafHue { get; init; } = 0.33f; + + /// Токсичность (0..1): отравляет поедателя (коэволюция); тормозит рост (цена защиты). + public float Toxicity { get; init; } + + /// Шипы (0..1): травмируют поедателя; тормозят рост (цена защиты). + public float Thorns { get; init; } + + /// Вкусность (0..1): обратная привлекательность для умных травоядных (избегание в C2). + public float Palatability { get; init; } = 1f; } /// Растение (Defs/Plants/): текстура, размер, опциональный ствол-препятствие, стадии роста, геном. diff --git a/src/LittleSim/Content/PlantSet.cs b/src/LittleSim/Content/PlantSet.cs index 684b7a1..2f4b894 100644 --- a/src/LittleSim/Content/PlantSet.cs +++ b/src/LittleSim/Content/PlantSet.cs @@ -118,6 +118,10 @@ public sealed class PlantSet new GenomeTemplate.Entry(Gene("GeneFruitSeason"), g.FruitSeason, 0f), // сезон фиксирован по виду Numeric("GeneHarvestAmount", g.HarvestAmount), new GenomeTemplate.Entry(Gene("GeneLeafHue"), g.LeafHue, 0.04f), + // Защита (коэволюция с травоядными): токсичность/шипы/вкусность. + Numeric("GeneToxicity", g.Toxicity), + Numeric("GeneThorns", g.Thorns), + Numeric("GenePalatability", g.Palatability), new GenomeTemplate.Entry( Gene("GeneMorph"), 0f, diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 700ae0b..01bd96d 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -260,6 +260,7 @@ public sealed class WorldScene : Scene ) ); var bleeding = content.Defs.TryGet("Bleeding", out var bl) ? bl : null; + var poisoned = content.Defs.TryGet("Poisoned", out var ps) ? ps : null; UpdateSystems.Add( new AnimalActionSystem( Store, @@ -272,6 +273,7 @@ public sealed class WorldScene : Scene CellSize, _bounds, bleeding, + poisoned, _config.Seed + 0x1B17 ) ); @@ -1070,6 +1072,10 @@ public sealed class WorldScene : Scene $" temp: grows {tMin:0.#}..{tMax:0.#}°C, optimal {tLow:0.#}..{tHigh:0.#}°C " + $"(cold {traits.ColdHardiness:0.#}, heat {traits.HeatHardiness:0.#})" ); + console.WriteLine( + $" defense: toxicity {traits.Toxicity:0.##}, thorns {traits.Thorns:0.##}, " + + $"palatability {traits.Palatability:0.##} (growth ×{traits.DefenseGrowthFactor():0.##})" + ); if (def.HarvestProduct is { } harvest) { @@ -1126,6 +1132,10 @@ public sealed class WorldScene : Scene $" move {traits.MoveSpeed:0.##}, vision {traits.Vision:0.##}, blood {traits.BloodVolume:0.##}, " + $"insulation {traits.Insulation:0.##}, furHue {traits.FurHue:0.##}" ); + console.WriteLine( + $" diet: herb {traits.Herbivory:0.##} / carn {traits.Carnivory:0.##} / omni {traits.Omnivory:0.##}, " + + $"toxinTolerance {traits.ToxinTolerance:0.##}" + ); } // Диета из генов (предатор-кластер): по каждому виду печатает herbivory/carnivory/omnivory, @@ -1187,6 +1197,7 @@ public sealed class WorldScene : Scene var brain = new double[_animals.Count]; var life = new double[_animals.Count]; var move = new double[_animals.Count]; + var tox = new double[_animals.Count]; var maxGen = new int[_animals.Count]; Store @@ -1200,6 +1211,7 @@ public sealed class WorldScene : Scene brain[s] += o.Traits.BrainSize; life[s] += o.Traits.Lifespan; move[s] += o.Traits.MoveSpeed; + tox[s] += o.Traits.ToxinTolerance; if (o.Generation > maxGen[s]) { maxGen[s] = o.Generation; @@ -1227,7 +1239,8 @@ public sealed class WorldScene : Scene var c = count[s]; console.WriteLine( $"{name}: n={c}, gen 0..{maxGen[s]}, body {body[s] / c:0.##}, " - + $"brain {brain[s] / c:0.##}, lifespan {life[s] / c:0} d, move {move[s] / c:0.##}" + + $"brain {brain[s] / c:0.##}, lifespan {life[s] / c:0} d, move {move[s] / c:0.##}, " + + $"toxinTol {tox[s] / c:0.##}" ); } @@ -1235,6 +1248,33 @@ public sealed class WorldScene : Scene { console.WriteLine(filter is null ? "no animals alive" : $"no '{filter}' alive"); } + + // Другая сторона гонки вооружений: средняя защита живых растений (коэволюция C2). Без фильтра. + if (filter is null) + { + var pn = 0; + double pTox = 0, + pThorn = 0, + pPalat = 0; + Store + .Query() + .ForEachEntity( + (ref PlantOrganism o, Entity _) => + { + pn++; + pTox += o.Traits.Toxicity; + pThorn += o.Traits.Thorns; + pPalat += o.Traits.Palatability; + } + ); + if (pn > 0) + { + console.WriteLine( + $"plants: n={pn}, toxicity {pTox / pn:0.###}, thorns {pThorn / pn:0.###}, " + + $"palatability {pPalat / pn:0.###}" + ); + } + } } // Наблюдаемость здоровья (фаза A5): дерево частей тела вида и способности здоровой особи. diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs index 9fd739e..e8a537a 100644 --- a/src/LittleSim/Sim/AnimalOrganism.cs +++ b/src/LittleSim/Sim/AnimalOrganism.cs @@ -48,6 +48,9 @@ public struct AnimalPhenotype /// Всеядность [0..1] — генералист: включает оба источника пищи (растения и мясо). public float Omnivory; + /// Устойчивость к растительным ядам [0..1] — снижает дозу отравления токсичным кормом. + public float ToxinTolerance; + /// Продолжительность жизни (игровых дней). public float Lifespan; @@ -78,6 +81,7 @@ public struct AnimalPhenotype Herbivory = T("herbivory"), Carnivory = T("carnivory"), Omnivory = T("omnivory"), + ToxinTolerance = T("toxinTolerance"), Lifespan = T("lifespan"), BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)), GestationDays = T("gestationDays"), diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs index c51183f..c7f24b2 100644 --- a/src/LittleSim/Sim/AnimalSystems.cs +++ b/src/LittleSim/Sim/AnimalSystems.cs @@ -125,6 +125,9 @@ public readonly struct AnimalContext(float[] values, NeedSet needs, float intell public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, NeedSet needs) : QuerySystem { + // Прибавка к обмену веществ при устойчивости к яду 1.0 (цена детоксикации, коэволюция C2). + private const float ToleranceMetabolicCost = 0.5f; + protected override void OnUpdate() { var days = clock.DeltaTime / secondsPerDay; @@ -146,7 +149,14 @@ public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, Need continue; // защита от старого/пустого состояния } - var metabolism = MathF.Max(0.1f, o[i].Traits.Metabolism); + // Цена устойчивости к яду (коэволюция C2): детоксикация метаболически затратна — чем выше + // toxinTolerance, тем быстрее расходуются нужды (зверь голоднее). Без этой платы устойчивость + // ушла бы к максимуму у всех и гонка вооружений встала бы. Множитель к обмену веществ. + var metabolism = + MathF.Max(0.1f, o[i].Traits.Metabolism) + * ( + 1f + ToleranceMetabolicCost * Math.Clamp(o[i].Traits.ToxinTolerance, 0f, 1f) + ); var lethalEmpty = false; for (var k = 0; k < needs.Count; k++) { @@ -237,7 +247,7 @@ public sealed class AnimalDecisionSystem : BaseSystem AnimalGrowth, Transform2D > _animals; - private readonly ArchetypeQuery _plants; + private readonly ArchetypeQuery _plants; private readonly ArchetypeQuery _corpses; public AnimalDecisionSystem( @@ -264,7 +274,7 @@ public sealed class AnimalDecisionSystem : BaseSystem AnimalGrowth, Transform2D >(); - _plants = store.Query(); + _plants = store.Query(); _corpses = store.Query(); } @@ -377,7 +387,15 @@ public sealed class AnimalDecisionSystem : BaseSystem brain.Target = preyPos; break; case AnimalActions.Eat - when eatsPlants && TryFindPlant(pos, radius, out var plant, out var pp): + when eatsPlants + && TryFindForage( + pos, + radius, + intelligence, + o[i].Traits.ToxinTolerance, + out var plant, + out var pp + ): brain.Action = AnimalActions.Eat; brain.TargetPlant = plant; brain.Target = pp; @@ -586,12 +604,36 @@ public sealed class AnimalDecisionSystem : BaseSystem otherTraits.BodySize <= hunterBody * 1.25f; // Ближайшее растение в радиусе (квадрат расстояния); ничьи — по меньшему id (детерминизм). + // Порог интеллекта, с которого травоядное РАЗБИРАЕТ корм (избегает ядовитого/невкусного). Ниже — + // рефлекторное выедание ближайшего растения (тир мозга 4, как сон/мысли; больной зверь с упавшим + // сознанием падает ниже и снова жрёт что попало — последствие гейтинга A7). + private const float ForageSmartMinBrain = 0.4f; + + // Вес, с которым воспринимаемый яд (с поправкой на устойчивость) отталкивает от растения, и штраф за + // дальность — чтобы умный зверь предпочитал близкий и безопасный корм, а не бежал через всю карту. + private const float ToxinAvoidWeight = 1.2f; + private const float ForageDistanceWeight = 0.5f; + + // Выбор корма: умный (избегает яда/невкусного) если интеллект ≥ порога, иначе рефлекс — ближайшее. + private bool TryFindForage( + Vector2 from, + float radius, + float intelligence, + float toxinTolerance, + out int plantId, + out Vector2 position + ) => + intelligence >= ForageSmartMinBrain + ? TryFindBestPlant(from, radius, toxinTolerance, out plantId, out position) + : TryFindPlant(from, radius, out plantId, out position); + + // Рефлекс: ближайшее растение в радиусе (ничья — меньший id, детерминизм). private bool TryFindPlant(Vector2 from, float radius, out int plantId, out Vector2 position) { var bestSq = radius * radius; plantId = -1; position = default; - foreach (var (transforms, _, entities) in _plants.Chunks) + foreach (var (transforms, _, _, entities) in _plants.Chunks) { var t = transforms.Span; for (var i = 0; i < t.Length; i++) @@ -610,6 +652,54 @@ public sealed class AnimalDecisionSystem : BaseSystem return plantId >= 0; } + // Умный выбор: среди растений в радиусе максимизируем привлекательность = вкусность − + // воспринимаемый_яд − штраф_за_дальность. Воспринимаемый яд = toxicity × (1 − устойчивость): чем + // выше устойчивость зверя, тем меньше его отпугивает токсичный корм. Если всё в округе невкусно/ + // ядовито, голод всё равно заставит выбрать наименее плохой (берётся максимум, даже отрицательный). + private bool TryFindBestPlant( + Vector2 from, + float radius, + float toxinTolerance, + out int plantId, + out Vector2 position + ) + { + var radiusSq = radius * radius; + var tol = Math.Clamp(toxinTolerance, 0f, 1f); + var bestScore = float.NegativeInfinity; + plantId = -1; + position = default; + foreach (var (transforms, _, organisms, entities) in _plants.Chunks) + { + var t = transforms.Span; + var o = organisms.Span; + for (var i = 0; i < t.Length; i++) + { + var sq = Vector2.DistanceSquared(from, t[i].Position); + if (sq > radiusSq) + { + continue; + } + + ref readonly var tr = ref o[i].Traits; + var perceivedToxin = tr.Toxicity * (1f - tol); + var score = + tr.Palatability + - ToxinAvoidWeight * perceivedToxin + - ForageDistanceWeight * (radius > 0f ? MathF.Sqrt(sq) / radius : 0f); + var id = entities.EntityAt(i).Id; + if (score > bestScore || (score == bestScore && id < plantId)) + { + bestScore = score; + plantId = id; + position = t[i].Position; + } + } + } + + return plantId >= 0; + } + // Ближайшая кромка воды (без лимита по зрению — звери «помнят» водопои; упрощение фазы A2). private bool TryFindShore(Vector2 from, out Vector2 position) { @@ -707,6 +797,7 @@ public sealed class AnimalActionSystem : BaseSystem private readonly int _cellSize; private readonly RectF _bounds; private readonly HediffDef? _bleeding; + private readonly HediffDef? _poisoned; private readonly Random _rng; private readonly ArchetypeQuery< AnimalBrain, @@ -731,6 +822,7 @@ public sealed class AnimalActionSystem : BaseSystem int cellSize, RectF bounds, HediffDef? bleeding, + HediffDef? poisoned, int seed ) { @@ -744,6 +836,7 @@ public sealed class AnimalActionSystem : BaseSystem _cellSize = cellSize; _bounds = bounds; _bleeding = bleeding; + _poisoned = poisoned; _rng = new Random(seed); _query = store.Query(); } @@ -792,6 +885,13 @@ public sealed class AnimalActionSystem : BaseSystem { Refill(values, AnimalActions.Eat, days); Graze(brain.TargetPlant, days, def.ForageBiteDays); + // Коэволюция: защита растения бьёт по поедателю (яд/шипы). + ApplyPlantDefense( + brain.TargetPlant, + hh[i].State, + o[i].Traits.ToxinTolerance, + days + ); } break; @@ -1010,6 +1110,65 @@ public sealed class AnimalActionSystem : BaseSystem } } + // Доза яда за день укуса при токсичности 1 и нулевой устойчивости (масштабируется тяжести Poisoned). + private const float PoisonDosePerDay = 1.6f; + + // Порог шипов, выше которого укус о растение травмирует, и масштабы урона/кровопотери от шипов. + private const float ThornThreshold = 0.2f; + private const float ThornPartDamage = 4f; + private const float ThornBloodLoss = 0.02f; + + // Коэволюция: при поедании растение «отвечает» — токсичность отравляет (тем сильнее, чем ниже + // устойчивость зверя к яду), шипы наносят лёгкую травму. Доза яда копится в хедифе Poisoned; если + // зверь ест яд быстрее, чем выводит, тяжесть дойдёт до летальной. Детерминированно (общий _rng). + private void ApplyPlantDefense( + int plantId, + HealthState? health, + float toxinTolerance, + float days + ) + { + if ( + health is null + || plantId < 0 + || !_store.TryGetEntityById(plantId, out var plant) + || plant.IsNull + || !plant.HasComponent() + ) + { + return; + } + + ref readonly var traits = ref plant.GetComponent().Traits; + + var poisoned = false; + if (_poisoned is not null && traits.Toxicity > 0f) + { + var dose = traits.Toxicity * (1f - Math.Clamp(toxinTolerance, 0f, 1f)); + if (dose > 0f) + { + health.Intensify(_poisoned, dose * PoisonDosePerDay * days); + poisoned = true; + } + } + + if (traits.Thorns > ThornThreshold) + { + // ApplyInjury сам пересчитывает способности (учтёт и свежий яд). + health.ApplyInjury( + traits.Thorns * ThornPartDamage * days, + traits.Thorns * ThornBloodLoss * days, + _bleeding, + traits.Thorns * 0.15f * days, + _rng + ); + } + else if (poisoned) + { + health.RecomputeCapacities(); // отравление меняет capacity-моды — применить сразу + } + } + // Применяет спаривания после прохода: самка пары беременеет геномом самца (структурное добавление — // после итерации), влечение обоих обнуляется. Двойные события на одну самку отсеиваются. private void ApplyMatings() diff --git a/src/LittleSim/Sim/CapacityCalc.cs b/src/LittleSim/Sim/CapacityCalc.cs index 87a1ed9..2e2af92 100644 --- a/src/LittleSim/Sim/CapacityCalc.cs +++ b/src/LittleSim/Sim/CapacityCalc.cs @@ -9,8 +9,27 @@ public static class AnimalCapacities public const string Moving = "Moving"; public const string Breathing = "Breathing"; public const string BloodPumping = "BloodPumping"; + public const string BloodFiltration = "BloodFiltration"; public const string Sight = "Sight"; + public const string Hearing = "Hearing"; + public const string Talking = "Talking"; + public const string Eating = "Eating"; public const string Digestion = "Digestion"; + + /// Порядок отображения способностей в UI (сверху вниз). Прочие модовые — после, как есть. + public static readonly string[] DisplayOrder = + [ + Consciousness, + Moving, + Sight, + Hearing, + Talking, + Eating, + Breathing, + BloodPumping, + BloodFiltration, + Digestion, + ]; } /// @@ -24,7 +43,11 @@ public static class CapacityCalc /// Считает карту способностей из инстансов частей, уровня крови (0..1) и боли (0..1). public static Dictionary Compute(PartInstance[] parts, float blood, float pain) { + // Сырой вклад каждой способности (Σ доля·HP) и НАБОР объявленных телом способностей: вид имеет + // только те, к которым его части вообще причастны (у оленя нет манипуляции). Объявленность — + // статична (по дефам частей), поэтому уничтоженный орган всё равно покажет способность на 0%. var raw = new Dictionary(System.StringComparer.Ordinal); + var declared = new HashSet(System.StringComparer.Ordinal); foreach (var part in parts) { if (part.Def is null) @@ -35,36 +58,52 @@ public static class CapacityCalc var fraction = part.Fraction; foreach (var (capacity, contribution) in part.Def.Capacities) { + declared.Add(capacity); raw[capacity] = raw.GetValueOrDefault(capacity) + contribution * fraction; } } float Raw(string id) => raw.GetValueOrDefault(id); - static float Clamp01(float v) => v < 0f ? 0f : v > 1f ? 1f : v; + static float Clamp01(float v) => + v < 0f ? 0f + : v > 1f ? 1f + : v; + // Базовые (независимые) способности: кровоснабжение/дыхание/фильтрация × уровень крови. var bloodLevel = Clamp01(blood); var bloodPumping = Raw(AnimalCapacities.BloodPumping) * bloodLevel; var breathing = Raw(AnimalCapacities.Breathing) * bloodLevel; + var bloodFiltration = Raw(AnimalCapacities.BloodFiltration) * bloodLevel; + // Сознание зависит от кровоснабжения, дыхания и боли (модель RimWorld). var consciousness = Raw(AnimalCapacities.Consciousness) * Clamp01(bloodPumping) * Clamp01(breathing) * (1f - Clamp01(pain)); + var cons = Clamp01(consciousness); + var pump = Clamp01(bloodPumping); - var result = new Dictionary(System.StringComparer.Ordinal) + // Производные способности зависят от сознания (и кровоснабжения для движения/пищеварения). + var computed = new Dictionary(System.StringComparer.Ordinal) { [AnimalCapacities.BloodPumping] = bloodPumping, [AnimalCapacities.Breathing] = breathing, + [AnimalCapacities.BloodFiltration] = bloodFiltration, [AnimalCapacities.Consciousness] = consciousness, - [AnimalCapacities.Moving] = Raw(AnimalCapacities.Moving) * consciousness * Clamp01(bloodPumping), - [AnimalCapacities.Sight] = Raw(AnimalCapacities.Sight) * consciousness, - [AnimalCapacities.Digestion] = Raw(AnimalCapacities.Digestion) * Clamp01(bloodPumping), + [AnimalCapacities.Moving] = Raw(AnimalCapacities.Moving) * cons * pump, + [AnimalCapacities.Sight] = Raw(AnimalCapacities.Sight) * cons, + [AnimalCapacities.Hearing] = Raw(AnimalCapacities.Hearing) * cons, + [AnimalCapacities.Talking] = Raw(AnimalCapacities.Talking) * cons, + [AnimalCapacities.Eating] = Raw(AnimalCapacities.Eating) * cons, + [AnimalCapacities.Digestion] = Raw(AnimalCapacities.Digestion) * pump, }; - // Модовые способности, не охваченные зависимостями, — сырыми. - foreach (var (capacity, value) in raw) + // В результат — только объявленные телом способности: известные по формуле зависимостей, прочие + // (модовые) — сырым вкладом. Так у вида видны ровно его способности, а не весь каталог. + var result = new Dictionary(System.StringComparer.Ordinal); + foreach (var capacity in declared) { - result.TryAdd(capacity, value); + result[capacity] = computed.TryGetValue(capacity, out var v) ? v : Raw(capacity); } return result; diff --git a/src/LittleSim/Sim/PlantGrowthSystem.cs b/src/LittleSim/Sim/PlantGrowthSystem.cs index 33d1f21..28e5a63 100644 --- a/src/LittleSim/Sim/PlantGrowthSystem.cs +++ b/src/LittleSim/Sim/PlantGrowthSystem.cs @@ -57,6 +57,7 @@ public sealed class PlantGrowthSystem( var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand(); var rate = traits.Vigor + * traits.DefenseGrowthFactor() // цена защиты: токсичные/колючие растут медленнее * Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance) * Suitability.Trapezoid(temperature, tMin, tLow, tHigh, tMax) * Suitability.Gaussian( diff --git a/src/LittleSim/Sim/PlantOrganism.cs b/src/LittleSim/Sim/PlantOrganism.cs index b6e563b..8860ce4 100644 --- a/src/LittleSim/Sim/PlantOrganism.cs +++ b/src/LittleSim/Sim/PlantOrganism.cs @@ -32,6 +32,16 @@ public struct PlantPhenotype public float HarvestAmount; public float LeafHue; + // --- Защита (коэволюция с травоядными) --- + /// Токсичность [0..1]: отравляет поедателя; цена — замедление роста. + public float Toxicity; + + /// Шипы [0..1]: травмируют поедателя; цена — замедление роста. + public float Thorns; + + /// Вкусность [0..1]: обратная привлекательность для умных травоядных (избегание в C2). + public float Palatability; + /// Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта. public bool IsVariant; @@ -71,9 +81,26 @@ public struct PlantPhenotype FruitSeason = (int)MathF.Round(Math.Clamp(T("fruitSeason"), 0f, 3f)), HarvestAmount = T("harvestAmount"), LeafHue = T("leafHue"), + Toxicity = T("toxicity"), + Thorns = T("thorns"), + Palatability = T("palatability"), IsVariant = T("variant") >= 0.5f, }; } + + // Цена защиты: токсичность и шипы отнимают ресурсы у роста. Без этой платы отбор гнал бы защиту к + // максимуму у всех растений и коэволюция бы встала — а так под слабым выпасом выгоднее расти быстрее. + private const float ToxicityGrowthCost = 0.4f; + private const float ThornsGrowthCost = 0.3f; + private const float MinGrowthFactor = 0.2f; + + /// Множитель скорости роста с учётом цены защиты (1 — без защиты, ≥ MinGrowthFactor). + public readonly float DefenseGrowthFactor() => + Math.Clamp( + 1f - ToxicityGrowthCost * Toxicity - ThornsGrowthCost * Thorns, + MinGrowthFactor, + 1f + ); } /// diff --git a/src/LittleSim/UI/InspectPanel.cs b/src/LittleSim/UI/InspectPanel.cs index 7d22afe..a397a85 100644 --- a/src/LittleSim/UI/InspectPanel.cs +++ b/src/LittleSim/UI/InspectPanel.cs @@ -331,6 +331,13 @@ internal sealed class InspectPanel traits.LeafHue ) ); + if (traits.Toxicity > 0.01f || traits.Thorns > 0.01f) + { + text.AppendLine( + languages.Format("inspect.gene.defense", traits.Toxicity, traits.Thorns) + ); + } + if (traits.IsVariant) { text.AppendLine(languages.Get("inspect.gene.variant")); @@ -435,12 +442,27 @@ internal sealed class InspectPanel text.AppendLine( languages.Format("inspect.health.blood", state.BloodLevel * 100f, state.Pain * 100f) ); - if (state.Parts.Length > 0) + if (state.Capacities.Count > 0) { text.AppendLine(languages.Get("inspect.health.caps")); - AppendCapacity(text, state, AnimalCapacities.Consciousness, "cap.consciousness"); - AppendCapacity(text, state, AnimalCapacities.Moving, "cap.moving"); - AppendCapacity(text, state, AnimalCapacities.Sight, "cap.sight"); + // Сначала известные способности в порядке отображения, затем прочие (модовые) — как есть. + var shown = new HashSet(StringComparer.Ordinal); + foreach (var capId in AnimalCapacities.DisplayOrder) + { + if (state.Capacities.ContainsKey(capId)) + { + AppendCapacity(text, state, capId, CapacityLabel(capId)); + shown.Add(capId); + } + } + + foreach (var capId in state.Capacities.Keys) + { + if (shown.Add(capId)) + { + AppendCapacity(text, state, capId, CapacityLabel(capId)); + } + } } if (state.Hediffs.Count > 0) @@ -463,6 +485,9 @@ internal sealed class InspectPanel } } + // Ключ локализации метки способности: cap. (cap.consciousness, cap.bloodpumping…). + private static string CapacityLabel(string capId) => "cap." + capId.ToLowerInvariant(); + private void AppendCapacity( StringBuilder text, HealthState state, From e105938a2219074d2b3a829cd39ed371399e77c9 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sun, 14 Jun 2026 14:18:35 +0300 Subject: [PATCH 2/5] =?UTF-8?q?Wire=20health=20capacities=20to=20behaviour?= =?UTF-8?q?:=20filtration=E2=86=92immunity,=20sight/hearing=E2=86=92percep?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The expanded capacities were mostly display-only; make two of them affect the simulation (Moving→speed and Consciousness→intelligence gating already existed): - Blood filtration → toxin/disease clearance: AdvanceHediffs takes an immunity scale (default 1); AnimalHealthSystem passes the BloodFiltration capacity, so damaged kidneys/liver clear Poisoned and diseases slower (RimWorld model). Ties the new capacities to the coevolution work — weak filtration makes toxic plants deadlier. - Sight/hearing → perception radius: foraging, hunting and mate search scale by Sight; threat detection (flee) scales by max(Sight, Hearing) — you see or hear a predator. Injured eyes/ears shrink awareness. Eating/Talking/Digestion remain indicators for now. Build + --check-content clean. Not GUI-verified. Co-Authored-By: Claude Opus 4.8 --- src/LittleSim/Sim/AnimalOrganism.cs | 6 ++++-- src/LittleSim/Sim/AnimalSystems.cs | 31 +++++++++++++++++++---------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs index e8a537a..1d277a9 100644 --- a/src/LittleSim/Sim/AnimalOrganism.cs +++ b/src/LittleSim/Sim/AnimalOrganism.cs @@ -224,8 +224,10 @@ public sealed class HealthState /// /// Прогрессирует болезни (тяжесть и иммунитет растут со временем); снимает выздоровевшие /// (иммунитет ≥ 1); возвращает true, если какая-то болезнь достигла летальной тяжести. + /// — множитель набора иммунитета (способность «фильтрация крови»: + /// почки/печень выводят токсины и помогают бороться с болезнью; повреждённые → медленнее). /// - public bool AdvanceHediffs(float days) + public bool AdvanceHediffs(float days, float immunityScale = 1f) { var lethal = false; for (var i = _hediffs.Count - 1; i >= 0; i--) @@ -237,7 +239,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); // иммунитет победил — выздоровление diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs index c7f24b2..9e4cf6e 100644 --- a/src/LittleSim/Sim/AnimalSystems.cs +++ b/src/LittleSim/Sim/AnimalSystems.cs @@ -330,20 +330,24 @@ public sealed class AnimalDecisionSystem : BaseSystem brain.DecideIn = Interval + _rng.NextSingle() * Interval; // джиттер — расфазировать скан var pos = t[i].Position; var adult = AnimalFactory.IsAdult(_animalSet[o[i].Species], gr[i].Stage); - var radius = VisionRadius(o[i].Species, o[i].Traits.Vision); var self = entities.EntityAt(i); + var health = self.GetComponent().State; // Эффективный интеллект (мозг × сознание) гейтит доступные нужды/действия (фаза A7): // больной/раненый зверь с упавшим сознанием теряет высшие нужды (сон/секс) → рефлексы. - var intelligence = AnimalFactory.Intelligence( - o[i].Traits, - self.GetComponent().State - ); + var intelligence = AnimalFactory.Intelligence(o[i].Traits, health); + // Радиус восприятия по способностям: зрение ищет корм/партнёра/добычу, угрозу замечаем + // зрением ИЛИ слухом (повреждённые глаза/уши сужают восприятие). + var baseRadius = VisionRadius(o[i].Species, o[i].Traits.Vision); + var sight = health?.Capacity(AnimalCapacities.Sight) ?? 1f; + var hearing = health?.Capacity(AnimalCapacities.Hearing) ?? 1f; + var seeRadius = baseRadius * MathF.Max(0.2f, sight); + var senseRadius = baseRadius * MathF.Max(0.2f, MathF.Max(sight, hearing)); // Настроение (A8): стресс/истощение (низкое настроение) подавляет тягу к спариванию. var mood = self.GetComponent(); // Бегство (предатор-кластер): жертва, заметившая рядом хищника, спасается — это важнее // любых нужд (инстинкт выживания). Сам хищник от добычи не бежит (см. IsThreatTo). - if (TryFindThreat(pos, radius, o[i].Species, o[i].Traits, out var threatPos)) + if (TryFindThreat(pos, senseRadius, o[i].Species, o[i].Traits, out var threatPos)) { brain.Action = AnimalActions.Flee; brain.TargetPlant = -1; @@ -352,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; } @@ -372,7 +376,7 @@ public sealed class AnimalDecisionSystem : BaseSystem when eatsMeat && TryFindKill( pos, - radius, + seeRadius, o[i].Species, o[i].Traits, self.Id, @@ -390,7 +394,7 @@ public sealed class AnimalDecisionSystem : BaseSystem when eatsPlants && TryFindForage( pos, - radius, + seeRadius, intelligence, o[i].Traits.ToxinTolerance, out var plant, @@ -415,7 +419,7 @@ public sealed class AnimalDecisionSystem : BaseSystem && TryFindMate( pos, o[i].IsMale, - radius, + seeRadius, out var mateId, out var matePos ): @@ -1760,7 +1764,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) From 249b60349784afc86a07cc2c8c744c29cfd41dfa Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sun, 14 Jun 2026 14:58:10 +0300 Subject: [PATCH 3/5] Wire Eating + Digestion capacities to feeding (option 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eating (jaw) scales graze bite and Digestion×Eating scales satiety gained from food (plant grazing and corpse scavenging via Refill efficiency). A damaged jaw or sick gut means less nutrition per feeding, so the animal must eat more often. Completes wiring the new health capacities to behaviour; Talking remains an indicator (no social system yet). Build clean. Co-Authored-By: Claude Opus 4.8 --- src/LittleSim/Sim/AnimalSystems.cs | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs index 9e4cf6e..92539f5 100644 --- a/src/LittleSim/Sim/AnimalSystems.cs +++ b/src/LittleSim/Sim/AnimalSystems.cs @@ -877,6 +877,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) { @@ -887,8 +891,12 @@ 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, @@ -940,7 +948,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: @@ -987,7 +995,8 @@ public sealed class AnimalActionSystem : BaseSystem float[] values, AnimalDef def, float step, - float days + float days, + float foodEff ) { if ( @@ -1018,7 +1027,7 @@ public sealed class AnimalActionSystem : BaseSystem } ref var corpse = ref target.GetComponent(); - Refill(values, AnimalActions.Eat, days); + Refill(values, AnimalActions.Eat, days, foodEff); corpse.Meat -= def.ForageBiteDays * days; // расход мяса той же «силой укуса», что и выедание if (corpse.Meat <= 0f && !_consumed.Contains(target)) { @@ -1065,8 +1074,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) @@ -1074,7 +1084,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, если уже у цели (можно исполнять действие). From e6b1367a3a77f2348bf044bca751d340f572ad51 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sun, 14 Jun 2026 15:13:26 +0300 Subject: [PATCH 4/5] Senescence: aging degrades capacities and fertility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GeneLifespan already set the moment of death from old age; now aging actually manifests before it. RecomputeCapacities takes a capacityScale; AnimalHealthSystem applies SenescenceFactor(age, lifespan) — capacities fade from 60% of lifespan to ~0.55× near the end, so old animals move/see/filter blood worse and are frailer. Fertility falls with the same factor (smaller litters), and past 90% of lifespan females no longer conceive (post-reproductive). Makes GeneLifespan and the Senior stage meaningful. Build clean. Co-Authored-By: Claude Opus 4.8 --- src/LittleSim/Sim/AnimalOrganism.cs | 15 ++++++-- src/LittleSim/Sim/AnimalSystems.cs | 56 ++++++++++++++++++++++------- 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs index 1d277a9..6523e76 100644 --- a/src/LittleSim/Sim/AnimalOrganism.cs +++ b/src/LittleSim/Sim/AnimalOrganism.cs @@ -191,9 +191,11 @@ public sealed class HealthState /// /// Пересчитывает боль (из хедифов) и способности: базовые из частей/крови/боли, затем модификаторы - /// способностей от хедифов (лерп 1→factor по тяжести). Вызывать после изменения частей/крови/хедифов. + /// способностей от хедифов (лерп 1→factor по тяжести), затем общий множитель + /// (старение/сенесценс: дряхлеющий организм слабее по всем способностям). Вызывать после изменения + /// частей/крови/хедифов; =1 — без возрастного спада. /// - public void RecomputeCapacities() + public void RecomputeCapacities(float capacityScale = 1f) { var pain = 0f; foreach (var h in _hediffs) @@ -218,6 +220,15 @@ public sealed class HealthState } } + if (capacityScale < 1f) + { + var scale = Math.Clamp(capacityScale, 0f, 1f); + foreach (var capacity in new List(caps.Keys)) + { + caps[capacity] *= scale; + } + } + Capacities = caps; } diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs index 92539f5..c7d78bf 100644 --- a/src/LittleSim/Sim/AnimalSystems.cs +++ b/src/LittleSim/Sim/AnimalSystems.cs @@ -790,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; @@ -1216,14 +1217,24 @@ public sealed class AnimalActionSystem : BaseSystem if (!mother.HasComponent()) { ref readonly var mom = ref mother.GetComponent(); - mother.AddComponent( - new Pregnant - { - FatherGenome = father.GetComponent().Genome, - DueInDays = MathF.Max(1f, mom.Traits.GestationDays), - Litter = Math.Max(1, (int)MathF.Round(mom.Traits.LitterSize)), - } - ); + // Плодовитость падает со старостью (тот же фактор сенесценса): дряхлая самка приносит + // меньший помёт, а после порога пострепродуктивного возраста уже не зачинает. + var momAge = mother.HasComponent() + ? mother.GetComponent().AgeDays + : 0f; + var ageFrac = mom.Traits.Lifespan > 0f ? momAge / mom.Traits.Lifespan : 0f; + if (ageFrac <= PostReproductiveFrac) + { + var fert = AnimalHealthSystem.SenescenceFactor(momAge, mom.Traits.Lifespan); + mother.AddComponent( + new Pregnant + { + FatherGenome = father.GetComponent().Genome, + DueInDays = MathF.Max(1f, mom.Traits.GestationDays), + Litter = Math.Max(1, (int)MathF.Round(mom.Traits.LitterSize * fert)), + } + ); + } } ResetMating(a, mateNeed); @@ -1720,10 +1731,15 @@ public sealed class AnimalHealthSystem : BaseSystem private readonly float _secondsPerDay; private readonly HediffDef[] _ambient; private readonly Random _rng; - private readonly ArchetypeQuery _query; + private readonly ArchetypeQuery _query; private readonly List _deaths = []; private float _accum; + // Сенесценс (старение): спад способностей начинается с этой доли продолжительности жизни и достигает + // максимума к её концу. Дряхлеющий зверь слабее (медленнее, хуже видит/фильтрует кровь) и уязвимее. + private const float SenescenceStart = 0.6f; + private const float SenescenceMax = 0.45f; + public AnimalHealthSystem( EntityStore store, AnimalSet animals, @@ -1741,7 +1757,19 @@ public sealed class AnimalHealthSystem : BaseSystem _secondsPerDay = secondsPerDay; _ambient = ambient; _rng = new Random(seed); - _query = store.Query(); + _query = store.Query(); + } + + /// Множитель способностей от старения (1 — молод/расцвет, <1 — дряхлеет к концу жизни). + public static float SenescenceFactor(float ageDays, float lifespan) + { + if (lifespan <= 0f) + { + return 1f; + } + + var frac = (ageDays / lifespan - SenescenceStart) / MathF.Max(0.01f, 1f - SenescenceStart); + return 1f - SenescenceMax * Math.Clamp(frac, 0f, 1f); } protected override void OnUpdateGroup() @@ -1756,9 +1784,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; @@ -1804,7 +1834,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 From b2e3eb490ea7c35f0fa990bbcd3c8e101129c77f Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sun, 14 Jun 2026 15:24:04 +0300 Subject: [PATCH 5/5] Birth type gene (live-bearing vs egg-laying) + hybrid-creation foundation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GeneEggLaying (species-fixed) selects the conception cycle: viviparous animals carry to a live birth (existing path); oviparous animals, after the same gravid period, LAY eggs instead. New Egg entity (carries the already-bred offspring genome) + EggSystem incubates and hatches it into a baby. AnimalPregnancySystem branches on the mother's birth type. Adds an oviparous showcase species, Chicken. Eggs are serialized (WorldSave.Eggs) like corpses, so clutches survive save/load. Also lays the foundation for creating any species or hybrid (full command/UI deferred): AnimalSet.HybridGenome(base, other) produces a cross anchored to a base species' body/gene-set with shared genes blended Mendelian-style — so Create(species, GenerateGenome) makes any species and Create(species, HybridGenome) makes any hybrid. EggTexture on AnimalDef (defaults to the seed dot placeholder); reproduction line added to the `animal` console command. Build + --check-content clean (41 genes, 40 traits). Not GUI-verified. Co-Authored-By: Claude Opus 4.8 --- Mods/Core/Defs/Animals/Animals.json | 30 +++++++ Mods/Core/Defs/Genes/Animal.json | 7 ++ Mods/Core/Languages/en/ui.json | 1 + Mods/Core/Languages/ru/ui.json | 1 + src/LittleSim/App/WorldSave.cs | 25 ++++++ src/LittleSim/Content/AnimalSet.cs | 73 +++++++++++++++- src/LittleSim/Content/GameDefs.cs | 3 + src/LittleSim/Scenes/WorldScene.cs | 59 ++++++++++++- src/LittleSim/Sim/AnimalFactory.cs | 40 +++++++++ src/LittleSim/Sim/AnimalOrganism.cs | 27 ++++++ src/LittleSim/Sim/AnimalSystems.cs | 126 +++++++++++++++++++++++++--- 11 files changed, 375 insertions(+), 17 deletions(-) diff --git a/Mods/Core/Defs/Animals/Animals.json b/Mods/Core/Defs/Animals/Animals.json index 8ace26b..ed10555 100644 --- a/Mods/Core/Defs/Animals/Animals.json +++ b/Mods/Core/Defs/Animals/Animals.json @@ -101,6 +101,36 @@ "GeneOmnivory": 0.85, "GeneToxinTolerance": 0.25 } + }, + + { + "defName": "Chicken", + "label": "pawn.chicken", + "kind": "animal", + "texture": "things/pawn/animal/chicken/Chicken_east", + "body": "Quadruped", + "diet": ["plant"], + "spawnPer1000Cells": 1.5, + "baseSpeed": 22, + "visionCells": 10, + "genome": { + "GeneMaxBodySize": 0.5, + "GeneMetabolism": 1.3, + "GeneMoveSpeed": 0.9, + "GeneBloodVolume": 0.7, + "GeneVision": 1.1, + "GeneBrainSize": 0.3, + "GeneInsulation": 0.45, + "GeneFurColor": 0.6, + "GeneMaturityAge": 50, + "GeneLifespan": 180, + "GeneBreedingSeason": 1, + "GeneGestationDays": 12, + "GeneLitterSize": 6, + "GeneHerbivory": 0.8, + "GeneToxinTolerance": 0.15, + "GeneEggLaying": 1.0 + } } ] } diff --git a/Mods/Core/Defs/Genes/Animal.json b/Mods/Core/Defs/Genes/Animal.json index a5c05fa..cc54db5 100644 --- a/Mods/Core/Defs/Genes/Animal.json +++ b/Mods/Core/Defs/Genes/Animal.json @@ -59,6 +59,13 @@ { "defName": "GeneSex", "kind": "Discrete", "label": "gene.sex", "variants": 2, "variantWeights": [0.5, 0.5], "mutationChance": 0, "tags": ["animal", "sex"] }, + // Тип рождения (цикл зачатия): живородящие (eggLaying < 0.5) вынашивают и рожают живых детёнышей; + // яйцекладущие (≥ 0.5) после вынашивания ОТКЛАДЫВАЮТ яйца-сущности, которые инкубируются и + // вылупляются (см. AnimalPregnancySystem/EggSystem). Признак фиксирован по виду (spread/mutation 0). + { "defName": "GeneEggLaying", "parent": "BaseNumericGene", "label": "gene.eggLaying", + "default": 0.0, "min": 0.0, "max": 1.0, "spread": 0, "mutationChance": 0, "tags": ["animal", "reproduction"], + "effects": { "eggLaying": "value" } }, + // Размножение (фаза A4): сезон гона фиксирован по виду (0 весна … 3 зима), срок вынашивания и помёт. { "defName": "GeneBreedingSeason", "parent": "BaseNumericGene", "label": "gene.breedingSeason", "default": 2, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["animal", "reproduction"], diff --git a/Mods/Core/Languages/en/ui.json b/Mods/Core/Languages/en/ui.json index 57f6032..a63e94b 100644 --- a/Mods/Core/Languages/en/ui.json +++ b/Mods/Core/Languages/en/ui.json @@ -163,6 +163,7 @@ "pawn.hare": "hare", "pawn.boar": "boar", "pawn.wolf": "wolf", + "pawn.chicken": "chicken", "pawn.muffalo": "muffalo", "pawn.squirrel": "squirrel", "thought.gaveBirth": "gave birth", diff --git a/Mods/Core/Languages/ru/ui.json b/Mods/Core/Languages/ru/ui.json index 00b9a3e..8cb7bef 100644 --- a/Mods/Core/Languages/ru/ui.json +++ b/Mods/Core/Languages/ru/ui.json @@ -163,6 +163,7 @@ "pawn.hare": "заяц", "pawn.boar": "кабан", "pawn.wolf": "волк", + "pawn.chicken": "курица", "pawn.muffalo": "муффало", "pawn.squirrel": "белка", "thought.gaveBirth": "родила потомство", diff --git a/src/LittleSim/App/WorldSave.cs b/src/LittleSim/App/WorldSave.cs index ce1b9e8..752672c 100644 --- a/src/LittleSim/App/WorldSave.cs +++ b/src/LittleSim/App/WorldSave.cs @@ -173,6 +173,28 @@ public sealed class CorpseSave public float Meat { get; set; } } +/// Сериализуемое яйцо (тип рождения «яйцекладка»): вид, позиция, геном детёныша, поколение, инкубация. +public sealed class EggSave +{ + /// Имя дефа вида — по нему берётся индекс/спрайт/тело при вылуплении. + public string Species { get; set; } = ""; + + /// Позиция X в мировых координатах. + public float X { get; set; } + + /// Позиция Y в мировых координатах. + public float Y { get; set; } + + /// Поколение будущего детёныша. + public int Generation { get; set; } + + /// Остаток инкубации в игровых днях. + public float IncubateDays { get; set; } + + /// Геном будущего детёныша (уже скрещён на момент кладки). + public Dictionary Genome { get; set; } = new(); +} + /// /// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание), /// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он @@ -216,6 +238,9 @@ public sealed class WorldSave /// Снимок трупов (вид, позиция, таймер/стадия разложения, мясо). public List Corpses { get; set; } = []; + /// Снимок яиц (вид, позиция, геном детёныша, поколение, инкубация). + public List Eggs { get; set; } = []; + /// Конфиг мира для пересоздания сцены. public WorldConfig ToConfig() => new() diff --git a/src/LittleSim/Content/AnimalSet.cs b/src/LittleSim/Content/AnimalSet.cs index 4c6792b..ca3fbb8 100644 --- a/src/LittleSim/Content/AnimalSet.cs +++ b/src/LittleSim/Content/AnimalSet.cs @@ -30,6 +30,9 @@ public sealed class AnimalSet /// Спрайт трупа (fallback — самка). public required Texture2DRegion Corpse { get; init; } + /// Спрайт яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт-точка. + public required Texture2DRegion Egg { get; init; } + /// Стадии роста (из дефа или встроенный дефолт), по возрастанию возраста входа. public required AnimalStageDef[] Stages { get; init; } @@ -43,10 +46,40 @@ public sealed class AnimalSet // Встроенный набор стадий по умолчанию (если вид не задал свой): как было до выноса в данные. private static readonly AnimalStageDef[] DefaultStages = [ - new() { Name = "Baby", EnterAt = 0f, RelativeTo = "maturity", Scale = 0.45f, Texture = "baby" }, - new() { Name = "Juvenile", EnterAt = 0.22f, RelativeTo = "maturity", Scale = 0.7f, Texture = "baby" }, - new() { Name = "Adult", EnterAt = 1f, RelativeTo = "maturity", Scale = 1f, Texture = "adult", Adult = true }, - new() { Name = "Senior", EnterAt = 0.8f, RelativeTo = "lifespan", Scale = 0.92f, Texture = "adult", Adult = true }, + new() + { + Name = "Baby", + EnterAt = 0f, + RelativeTo = "maturity", + Scale = 0.45f, + Texture = "baby", + }, + new() + { + Name = "Juvenile", + EnterAt = 0.22f, + RelativeTo = "maturity", + Scale = 0.7f, + Texture = "baby", + }, + new() + { + Name = "Adult", + EnterAt = 1f, + RelativeTo = "maturity", + Scale = 1f, + Texture = "adult", + Adult = true, + }, + new() + { + Name = "Senior", + EnterAt = 0.8f, + RelativeTo = "lifespan", + Scale = 0.92f, + Texture = "adult", + Adult = true, + }, ]; private readonly Species[] _species; @@ -82,6 +115,9 @@ public sealed class AnimalSet Corpse = string.IsNullOrEmpty(def.CorpseTexture) ? female : atlases.GetRegion(device, def.CorpseTexture), + Egg = string.IsNullOrEmpty(def.EggTexture) + ? female + : atlases.GetRegion(device, def.EggTexture), Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages, Body = content.Defs.TryGet(def.Body, out var body) ? body : null, Template = BuildTemplate(def.Genome, genes), @@ -111,6 +147,35 @@ public sealed class AnimalSet return genome; } + /// + /// ОСНОВА создания гибрида (полноценная команда/UI — позже): геном помеси, привязанный к базовому виду + /// (его тело/спрайт/набор генов), но с общими генами, скрещёнными с + /// (по одной аллели-гамете от каждого родителя, как при размножении). + /// Пол берётся от базового вида; гены, которых нет у базового, игнорируются (его тело их не читает). + /// Сущность создаётся обычной с этим геномом и базовым видом — + /// то есть «создать любой вид» = Create(вид, GenerateGenome), «любой гибрид» = Create(вид, HybridGenome). + /// + public Genome HybridGenome(int baseSpecies, int otherSpecies, Random random) + { + var hybrid = GenerateGenome(baseSpecies, random); + var other = _species[otherSpecies].Template.Generate(random); + foreach (var (geneId, allele) in other.ToDictionary()) + { + if (string.Equals(geneId, "GeneSex", StringComparison.Ordinal) || !hybrid.Has(geneId)) + { + continue; // пол — от базового вида; чужие гены базовое тело не читает + } + + var mine = hybrid[geneId]; + hybrid[geneId] = new Allele( + random.NextSingle() < 0.5f ? mine.A : mine.B, + random.NextSingle() < 0.5f ? allele.A : allele.B + ); + } + + return hybrid; + } + // Строит шаблон генома вида из ДАННЫХ (geneId→base из Defs/Animals/) generically: разброс берётся из // самого гена. Состав открыт — модер добавляет ген строкой JSON, без правки кода. Дискретные гены // (база/разброс не нужны — варианты из GeneDef) тоже поддержаны. diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs index 43c6637..c3fa062 100644 --- a/src/LittleSim/Content/GameDefs.cs +++ b/src/LittleSim/Content/GameDefs.cs @@ -261,6 +261,9 @@ public sealed class AnimalDef : PawnDef /// Текстура трупа (фаза A5); пусто — берётся . public string CorpseTexture { get; init; } = ""; + /// Текстура яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт семени-точки. + public string EggTexture { get; init; } = "things/plant/seed_default"; + /// Имя — анатомия вида (части тела/органы); пусто — без частей тела. public string Body { get; init; } = ""; diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 01bd96d..8ecf0a0 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -294,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) @@ -519,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; } @@ -614,6 +625,23 @@ public sealed class WorldScene : Scene } ) ); + + Store + .Query() + .ForEachEntity( + (ref Egg egg, ref Transform2D transform, Entity _) => + save.Eggs.Add( + new EggSave + { + Species = _animals[egg.Species].Def.DefName, + X = transform.Position.X, + Y = transform.Position.Y, + Generation = egg.Generation, + IncubateDays = egg.IncubateDays, + Genome = egg.Genome?.ToDictionary() ?? new(), + } + ) + ); } private void RestorePlants(GameContent content) @@ -695,8 +723,31 @@ public sealed class WorldScene : Scene data.Meat = saved.Meat; } + foreach (var saved in _save.Eggs) + { + if (!content.Defs.TryGet(saved.Species, out var def)) + { + continue; // вид пропал (мод убрали) — пропускаем яйцо + } + + var index = _animals.IndexOf(def); + var genome = saved.Genome is { Count: > 0 } + ? new Genome(saved.Genome) + : _animals.GenerateGenome(index, fallback); + AnimalFactory.CreateEgg( + Store, + _animals, + index, + new Vector2(saved.X, saved.Y), + genome, + saved.Generation, + saved.IncubateDays, + CellSize + ); + } + Log.Info( - $"Restored {_save.Animals.Count} animals, {_save.Corpses.Count} corpses from save" + $"Restored {_save.Animals.Count} animals, {_save.Corpses.Count} corpses, {_save.Eggs.Count} eggs from save" ); } @@ -1136,6 +1187,10 @@ public sealed class WorldScene : Scene $" 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, diff --git a/src/LittleSim/Sim/AnimalFactory.cs b/src/LittleSim/Sim/AnimalFactory.cs index 6eb5596..268ece5 100644 --- a/src/LittleSim/Sim/AnimalFactory.cs +++ b/src/LittleSim/Sim/AnimalFactory.cs @@ -110,6 +110,46 @@ public static class AnimalFactory ); } + // Тинт яйца (бледно-кремовый) и доля размера клетки, до которой масштабируется спрайт яйца. + private static readonly Color EggTint = new(238, 230, 208); + private const float EggSizeFraction = 0.45f; + + /// + /// Создаёт сущность-яйцо (тип рождения «яйцекладка»): спрайт-яйцо и компонент с уже + /// скрещённым геномом будущего детёныша. Инкубируется и вылупляется в . + /// + public static Entity CreateEgg( + EntityStore store, + AnimalSet animals, + int species, + Vector2 position, + Genome genome, + int generation, + float incubateDays, + int cellSize + ) + { + var sp = animals[species]; + var region = sp.Egg; + var sprite = new Sprite(region, GameLayers.Beings); + sprite.CenterOrigin(); + sprite.Color = EggTint; + return store.CreateEntity( + new Transform2D( + position, + scale: new Vector2(cellSize * EggSizeFraction / region.Width) + ), + sprite, + new Egg + { + Species = species, + Genome = genome, + Generation = generation, + IncubateDays = incubateDays, + } + ); + } + /// Порог выраженности признака диеты, с которого вид реально ест данный корм. public const float DietThreshold = 0.5f; diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs index 6523e76..73acdcc 100644 --- a/src/LittleSim/Sim/AnimalOrganism.cs +++ b/src/LittleSim/Sim/AnimalOrganism.cs @@ -63,6 +63,12 @@ public struct AnimalPhenotype /// Размер помёта. public float LitterSize; + /// Тип рождения [0..1]: ≥0.5 — яйцекладущий (откладывает яйца), иначе живородящий. + public float EggLaying; + + /// Яйцекладущий ли вид (тип рождения) — определяет цикл зачатия. + public readonly bool Oviparous => EggLaying >= 0.5f; + /// Собирает фенотип из карты признаков, посчитанной . public static AnimalPhenotype FromTraits(IReadOnlyDictionary traits) { @@ -86,6 +92,7 @@ public struct AnimalPhenotype BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)), GestationDays = T("gestationDays"), LitterSize = T("litterSize"), + EggLaying = T("eggLaying"), }; } } @@ -457,6 +464,26 @@ public struct Corpse : IComponent public float Meat; } +/// +/// Яйцо (тип рождения «яйцекладка»): отдельная сущность, отложенная яйцекладущей самкой вместо живых +/// родов. Несёт уже скрещённый геном будущего детёныша, вид и поколение; инкубируется во времени и +/// вылупляется в детёныша (). Геном — managed-ссылка (как у трупа/беременности). +/// +public struct Egg : IComponent +{ + /// Индекс вида в (для спрайта при вылуплении и сейва). + public int Species; + + /// Геном будущего детёныша (уже скрещён из родителей на момент кладки). + public Genome Genome; + + /// Поколение будущего детёныша. + public int Generation; + + /// Остаток инкубации в игровых днях; ≤0 — вылупление. + public float IncubateDays; +} + /// Инстанс мысли настроения на особи (фаза A8): деф события + остаток времени до угасания. public struct ThoughtInstance { diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs index c7d78bf..90d2974 100644 --- a/src/LittleSim/Sim/AnimalSystems.cs +++ b/src/LittleSim/Sim/AnimalSystems.cs @@ -1656,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), } ); } @@ -1685,17 +1687,35 @@ public sealed class AnimalPregnancySystem : BaseSystem ); var offset = new Vector2(_rng.NextSingle() - 0.5f, _rng.NextSingle() - 0.5f) * _cellSize; - AnimalFactory.Create( - _store, - _animals, - _needs, - birth.Species, - birth.Position + offset, - ageDays: 0f, - child, - birth.Generation, - _cellSize - ); + if (birth.Oviparous) + { + // Яйцекладка: вместо живого детёныша откладываем яйцо с его геномом — вылупится позже. + AnimalFactory.CreateEgg( + _store, + _animals, + birth.Species, + birth.Position + offset, + child, + birth.Generation, + birth.IncubateDays, + _cellSize + ); + } + else + { + AnimalFactory.Create( + _store, + _animals, + _needs, + birth.Species, + birth.Position + offset, + ageDays: 0f, + child, + birth.Generation, + _cellSize + ); + } + count++; } } @@ -1710,6 +1730,90 @@ public sealed class AnimalPregnancySystem : BaseSystem public int Litter; public int Generation; public Vector2 Position; + public bool Oviparous; + public float IncubateDays; + } +} + +/// +/// Инкубация яиц (тип рождения «яйцекладка»): тикает остаток инкубации каждого яйца; по истечении +/// вылупляет детёныша ( из хранимого генома, стадия Baby) рядом с +/// яйцом и удаляет яйцо. Зеркало родов для яйцекладущих видов. +/// +public sealed class EggSystem : BaseSystem +{ + private readonly EntityStore _store; + private readonly AnimalSet _animals; + private readonly NeedSet _needs; + private readonly GameClock _clock; + private readonly float _secondsPerDay; + private readonly int _cellSize; + private readonly Random _rng; + private readonly ArchetypeQuery _query; + private readonly List _hatched = []; + + public EggSystem( + EntityStore store, + AnimalSet animals, + NeedSet needs, + GameClock clock, + float secondsPerDay, + int cellSize, + int seed + ) + { + _store = store; + _animals = animals; + _needs = needs; + _clock = clock; + _secondsPerDay = secondsPerDay; + _cellSize = cellSize; + _rng = new Random(seed); + _query = store.Query(); + } + + protected override void OnUpdateGroup() + { + var days = _clock.DeltaTime / _secondsPerDay; + if (days <= 0f) + { + return; + } + + _hatched.Clear(); + foreach (var (eggs, transforms, entities) in _query.Chunks) + { + var e = eggs.Span; + for (var i = 0; i < e.Length; i++) + { + ref var egg = ref e[i]; + egg.IncubateDays -= days; + if (egg.IncubateDays > 0f) + { + continue; + } + + var offset = + new Vector2(_rng.NextSingle() - 0.5f, _rng.NextSingle() - 0.5f) * _cellSize; + AnimalFactory.Create( + _store, + _animals, + _needs, + egg.Species, + transforms.Span[i].Position + offset, + ageDays: 0f, + egg.Genome, + egg.Generation, + _cellSize + ); + _hatched.Add(entities.EntityAt(i)); + } + } + + foreach (var egg in _hatched) + { + egg.DeleteEntity(); + } } }