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,