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; +}