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