From cc1841c2e27b081188d06b073d8def648fccedc6 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sat, 13 Jun 2026 04:34:03 +0300 Subject: [PATCH] Enhance plant genetics system with cold and heat hardiness traits. Updated genes.json to include GeneColdHardiness and GeneHeatHardiness, and modified plant definitions to incorporate these traits. Adjusted temperature tolerance calculations in PlantPhenotype and PlantLifecycleSystem to account for temperature stress. Improved documentation and added temperature stress management in the lifecycle system. --- Mods/Core/Defs/genes.json | 9 ++++ Mods/Core/Defs/plants.json | 9 ++-- src/LittleSim/Content/GameDefs.cs | 8 ++- src/LittleSim/Content/PlantSet.cs | 2 + src/LittleSim/Scenes/WorldScene.cs | 6 +++ src/LittleSim/Sim/PlantFactory.cs | 3 +- src/LittleSim/Sim/PlantGrowthSystem.cs | 8 ++- src/LittleSim/Sim/PlantLifecycleSystem.cs | 63 ++++++++++++++++++----- src/LittleSim/Sim/PlantOrganism.cs | 28 ++++++++++ 9 files changed, 113 insertions(+), 23 deletions(-) diff --git a/Mods/Core/Defs/genes.json b/Mods/Core/Defs/genes.json index 247801b..5610511 100644 --- a/Mods/Core/Defs/genes.json +++ b/Mods/Core/Defs/genes.json @@ -19,6 +19,15 @@ { "defName": "GeneTemperatureTolerance", "parent": "BaseNumericGene", "label": "gene.temperatureTolerance", "default": 16, "min": 2, "max": 40, "tags": ["environment", "temperature"], "effects": { "temperatureTolerance": "value" } }, + // Жёсткие края диапазона роста (модель RimWorld): на сколько °C ниже/выше плато оптимума рост + // спадает до нуля. Ниже (optimal-tolerance-coldHardiness) или выше (optimal+tolerance+heatHardiness) + // растение дормантно и копит температурный стресс. Асимметрично: вид может терпеть жару лучше холода. + { "defName": "GeneColdHardiness", "parent": "BaseNumericGene", "label": "gene.coldHardiness", + "default": 8, "min": 0, "max": 40, "tags": ["environment", "temperature"], + "effects": { "coldHardiness": "value" } }, + { "defName": "GeneHeatHardiness", "parent": "BaseNumericGene", "label": "gene.heatHardiness", + "default": 10, "min": 0, "max": 40, "tags": ["environment", "temperature"], + "effects": { "heatHardiness": "value" } }, { "defName": "GeneOptimalFertility", "parent": "BaseNumericGene", "label": "gene.optimalFertility", "default": 1.4, "min": 0.1, "max": 3.0, "tags": ["environment", "soil"], "effects": { "optimalFertility": "value" } }, diff --git a/Mods/Core/Defs/plants.json b/Mods/Core/Defs/plants.json index d39427c..e520e4b 100644 --- a/Mods/Core/Defs/plants.json +++ b/Mods/Core/Defs/plants.json @@ -3,7 +3,8 @@ "defs": [ { "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28, "harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn", - "genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 16, + "genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 10, + "coldHardiness": 14, "heatHardiness": 12, "optimalFertility": 1.4, "fertilityTolerance": 0.9, "vigor": 1.0, "lifespan": 160, "dispersalRange": 2, "reproduceInterval": 14, "selfPollination": 0.2, "mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08, @@ -20,7 +21,8 @@ { "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.2, "harvestProduct": "ProductGrass", - "genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 20, "temperatureTolerance": 12, + "genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 22, "temperatureTolerance": 8, + "coldHardiness": 8, "heatHardiness": 6, "optimalFertility": 1.0, "fertilityTolerance": 1.0, "vigor": 1.3, "lifespan": 22, "dispersalRange": 3, "reproduceInterval": 3, "selfPollination": 0.8, "mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1, @@ -32,7 +34,8 @@ { "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4, "fruitProduct": "ProductBerry", - "genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 13, + "genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 9, + "coldHardiness": 10, "heatHardiness": 8, "optimalFertility": 1.1, "fertilityTolerance": 1.0, "vigor": 1.0, "lifespan": 60, "dispersalRange": 2, "reproduceInterval": 7, "selfPollination": 0.5, "mutationRate": 0.05, "variantChance": 0.18, "spread": 0.1, diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs index 781b6a0..81b2442 100644 --- a/src/LittleSim/Content/GameDefs.cs +++ b/src/LittleSim/Content/GameDefs.cs @@ -93,9 +93,15 @@ public sealed class GenomeDef /// Оптимальная температура (°C) и её толерантность. public float OptimalTemperature { get; init; } = 18f; - /// Ширина толерантности по температуре (°C). + /// Ширина толерантности по температуре (°C) — половина плато полного роста вокруг оптимума. public float TemperatureTolerance { get; init; } = 12f; + /// Морозостойкость (°C): на сколько ниже плато рост спадает до нуля (жёсткая нижняя граница). + public float ColdHardiness { get; init; } = 8f; + + /// Жаростойкость (°C): на сколько выше плато рост спадает до нуля (жёсткая верхняя граница). + public float HeatHardiness { get; init; } = 10f; + /// Оптимальная плодородность почвы и её толерантность. public float OptimalFertility { get; init; } = 1f; diff --git a/src/LittleSim/Content/PlantSet.cs b/src/LittleSim/Content/PlantSet.cs index b92622e..0334af7 100644 --- a/src/LittleSim/Content/PlantSet.cs +++ b/src/LittleSim/Content/PlantSet.cs @@ -103,6 +103,8 @@ public sealed class PlantSet Numeric("GeneLightTolerance", g.LightTolerance), Numeric("GeneOptimalTemperature", g.OptimalTemperature), Numeric("GeneTemperatureTolerance", g.TemperatureTolerance), + Numeric("GeneColdHardiness", g.ColdHardiness), + Numeric("GeneHeatHardiness", g.HeatHardiness), Numeric("GeneOptimalFertility", g.OptimalFertility), Numeric("GeneFertilityTolerance", g.FertilityTolerance), Numeric("GeneVigor", g.Vigor), diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index eba5384..9e7413a 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -168,6 +168,7 @@ public sealed class WorldScene : Scene Store, _plants, calendar, + climate, Context.Clock, _config.Width, _config.Height, @@ -587,6 +588,11 @@ public sealed class WorldScene : Scene $" vigor {traits.Vigor:0.##}, lifespan {traits.Lifespan:0} d, " + $"optimalLight {traits.OptimalLight:0.##}, leafHue {traits.LeafHue:0.##}" ); + var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand(); + console.WriteLine( + $" temp: grows {tMin:0.#}..{tMax:0.#}°C, optimal {tLow:0.#}..{tHigh:0.#}°C " + + $"(cold {traits.ColdHardiness:0.#}, heat {traits.HeatHardiness:0.#})" + ); if (def.HarvestProduct is { } harvest) { diff --git a/src/LittleSim/Sim/PlantFactory.cs b/src/LittleSim/Sim/PlantFactory.cs index 412247a..bdd8cb1 100644 --- a/src/LittleSim/Sim/PlantFactory.cs +++ b/src/LittleSim/Sim/PlantFactory.cs @@ -56,7 +56,8 @@ public static class PlantFactory CellFertility = cellFertility, }, new PlantOrganism { Genome = genome, Traits = traits }, - new Fruiting() + new Fruiting(), + new TemperatureStress() ); } diff --git a/src/LittleSim/Sim/PlantGrowthSystem.cs b/src/LittleSim/Sim/PlantGrowthSystem.cs index e7f200c..33d1f21 100644 --- a/src/LittleSim/Sim/PlantGrowthSystem.cs +++ b/src/LittleSim/Sim/PlantGrowthSystem.cs @@ -53,14 +53,12 @@ public sealed class PlantGrowthSystem( ref var traits = ref orgs[i].Traits; var light = lighting.SampleAt(t[i].Position); + // Температура — трапеция RimWorld: плато полного роста, жёсткие края (дормантность вне). + var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand(); var rate = traits.Vigor * Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance) - * Suitability.Gaussian( - temperature, - traits.OptimalTemperature, - traits.TemperatureTolerance - ) + * Suitability.Trapezoid(temperature, tMin, tLow, tHigh, tMax) * Suitability.Gaussian( grow.CellFertility, traits.OptimalFertility, diff --git a/src/LittleSim/Sim/PlantLifecycleSystem.cs b/src/LittleSim/Sim/PlantLifecycleSystem.cs index 1f6ff2c..0142022 100644 --- a/src/LittleSim/Sim/PlantLifecycleSystem.cs +++ b/src/LittleSim/Sim/PlantLifecycleSystem.cs @@ -2,6 +2,7 @@ using Friflo.Engine.ECS; using Friflo.Engine.ECS.Systems; using LittleSim.Content; using Microsoft.Xna.Framework; +using MrGameEng.AI; using MrGameEng.Core; using MrGameEng.Genetics; using MrGameEng.Graphics; @@ -9,21 +10,30 @@ using MrGameEng.Graphics; namespace LittleSim.Sim; /// -/// Жизненный цикл растений: размножение (наследование по Менделю) и смерть от старости. Запускается -/// не каждый кадр, а раз в игрового дня. За тик: строит по-клеточную сетку -/// (счётчик плотности + «представитель» вида/генома), собирает умерших по возрасту, затем зрелые с -/// вероятностью elapsed/ReproduceInterval сеют потомка — приоритет перекрёстного опыления -/// (зрелый сосед того же вида в радиусе расселения), иначе самоопыление по гену. Семя падает в -/// случайную клетку радиуса, если это суша и не превышен лимит плотности. Структурные изменения -/// (создание/удаление сущностей) применяются после проходов по запросу — это безопасно. +/// Жизненный цикл растений: размножение (наследование по Менделю), смерть от старости и от +/// температурного стресса (вымерзание/перегрев). Запускается не каждый кадр, а раз в +/// игрового дня. За тик: строит по-клеточную сетку (счётчик плотности + +/// «представитель» вида/генома), копит/рассасывает температурный стресс по текущей температуре, +/// собирает умерших (старость или превышенный стресс), затем зрелые с вероятностью +/// elapsed/ReproduceInterval сеют потомка — приоритет перекрёстного опыления (зрелый сосед +/// того же вида в радиусе расселения), иначе самоопыление по гену. Семя падает в случайную клетку +/// радиуса, если это суша и не превышен лимит плотности. Структурные изменения (создание/удаление +/// сущностей) применяются после проходов по запросу — это безопасно. /// public sealed class PlantLifecycleSystem : BaseSystem { private const float IntervalDays = 0.25f; + /// Сколько игровых дней дормантности вне диапазона роста убивают растение. + private const float LethalStressDays = 6f; + + /// Во сколько раз стресс рассасывается быстрее в пригодной температуре, чем копится вне. + private const float StressRecovery = 1.5f; + private readonly EntityStore _store; private readonly PlantSet _plants; private readonly Calendar _calendar; + private readonly Climate _climate; private readonly GameClock _clock; private readonly int _width; private readonly int _height; @@ -32,7 +42,12 @@ public sealed class PlantLifecycleSystem : BaseSystem private readonly bool[] _cellLand; private readonly int _densityCap; private readonly Random _rng; - private readonly ArchetypeQuery _query; + private readonly ArchetypeQuery< + PlantGrowth, + PlantOrganism, + Transform2D, + TemperatureStress + > _query; private readonly int[] _count; private readonly CellRep[] _rep; @@ -45,6 +60,7 @@ public sealed class PlantLifecycleSystem : BaseSystem EntityStore store, PlantSet plants, Calendar calendar, + Climate climate, GameClock clock, int width, int height, @@ -58,6 +74,7 @@ public sealed class PlantLifecycleSystem : BaseSystem _store = store; _plants = plants; _calendar = calendar; + _climate = climate; _clock = clock; _width = width; _height = height; @@ -66,7 +83,7 @@ public sealed class PlantLifecycleSystem : BaseSystem _cellLand = cellLand; _densityCap = densityCap; _rng = new Random(seed); - _query = store.Query(); + _query = store.Query(); _count = new int[width * height]; _rep = new CellRep[width * height]; } @@ -82,13 +99,13 @@ public sealed class PlantLifecycleSystem : BaseSystem var elapsed = _accumulator; _accumulator = 0f; - BuildGridAndCollect(); + BuildGridAndCollect(elapsed); Reproduce(elapsed); Apply(); } - // Проход 1: сетка плотности + представители, сбор умерших и зрелых-кандидатов. - private void BuildGridAndCollect() + // Проход 1: сетка плотности + представители, температурный стресс, сбор умерших и зрелых-кандидатов. + private void BuildGridAndCollect(float elapsed) { Array.Clear(_count); for (var c = 0; c < _rep.Length; c++) @@ -100,11 +117,14 @@ public sealed class PlantLifecycleSystem : BaseSystem _candidates.Clear(); _births.Clear(); - foreach (var (growths, organisms, transforms, entities) in _query.Chunks) + var temperature = _climate.Temperature; // глобальная — едина для всех растений в этот тик + + foreach (var (growths, organisms, transforms, stresses, entities) in _query.Chunks) { var g = growths.Span; var orgs = organisms.Span; var t = transforms.Span; + var st = stresses.Span; for (var i = 0; i < g.Length; i++) { ref var grow = ref g[i]; @@ -115,6 +135,23 @@ public sealed class PlantLifecycleSystem : BaseSystem continue; } + // Температурный стресс: вне диапазона роста (трапеция = 0) копится, внутри — рассасывается. + var (tMin, tLow, tHigh, tMax) = org.Traits.TemperatureBand(); + ref var stress = ref st[i].Days; + if (Suitability.Trapezoid(temperature, tMin, tLow, tHigh, tMax) <= 0f) + { + stress += elapsed; + if (stress >= LethalStressDays) + { + _deaths.Add(entities.EntityAt(i)); // вымерзло/перегрелось + continue; + } + } + else + { + stress = MathF.Max(0f, stress - elapsed * StressRecovery); + } + var pos = t[i].Position; var cx = Math.Clamp((int)(pos.X / _cellSize), 0, _width - 1); var cy = Math.Clamp((int)(pos.Y / _cellSize), 0, _height - 1); diff --git a/src/LittleSim/Sim/PlantOrganism.cs b/src/LittleSim/Sim/PlantOrganism.cs index 8376415..78aaaca 100644 --- a/src/LittleSim/Sim/PlantOrganism.cs +++ b/src/LittleSim/Sim/PlantOrganism.cs @@ -15,6 +15,8 @@ public struct PlantPhenotype public float LightTolerance; public float OptimalTemperature; public float TemperatureTolerance; + public float ColdHardiness; + public float HeatHardiness; public float OptimalFertility; public float FertilityTolerance; public float Vigor; @@ -33,6 +35,18 @@ public struct PlantPhenotype /// Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта. public bool IsVariant; + /// + /// Четыре точки температурной трапеции роста (модель RimWorld): жёсткий минимум, нижний край плато, + /// верхний край плато, жёсткий максимум. Плато — оптимум ± толерантность; жёсткие края отстоят от + /// плато на морозо-/жаростойкость. Кормит . + /// + public readonly (float Min, float OptimalLow, float OptimalHigh, float Max) TemperatureBand() + { + var low = OptimalTemperature - TemperatureTolerance; + var high = OptimalTemperature + TemperatureTolerance; + return (low - ColdHardiness, low, high, high + HeatHardiness); + } + /// Собирает фенотип из карты признаков, посчитанной . public static PlantPhenotype FromTraits(IReadOnlyDictionary traits) { @@ -43,6 +57,8 @@ public struct PlantPhenotype LightTolerance = T("lightTolerance"), OptimalTemperature = T("optimalTemperature"), TemperatureTolerance = T("temperatureTolerance"), + ColdHardiness = T("coldHardiness"), + HeatHardiness = T("heatHardiness"), OptimalFertility = T("optimalFertility"), FertilityTolerance = T("fertilityTolerance"), Vigor = T("vigor"), @@ -85,3 +101,15 @@ public struct Fruiting : IComponent /// Накоплено зрелых плодов (0..FruitYield). public float RipeFruit; } + +/// +/// Накопленный температурный стресс растения в игровых днях. Пока температура вне диапазона роста +/// (трапеция даёт ноль — слишком холодно или жарко), +/// стресс копится; внутри диапазона — рассасывается. Превышение порога убивает растение +/// (вымерзание/перегрев). Управляется PlantLifecycleSystem. +/// +public struct TemperatureStress : IComponent +{ + /// Накоплено дней дормантности вне диапазона роста; при превышении лимита — гибель. + public float Days; +}