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.

This commit is contained in:
Leonid Pershin
2026-06-13 04:34:03 +03:00
parent 4fda693994
commit cc1841c2e2
9 changed files with 113 additions and 23 deletions
+9
View File
@@ -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" } },
+6 -3
View File
@@ -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,
+7 -1
View File
@@ -93,9 +93,15 @@ public sealed class GenomeDef
/// <summary>Оптимальная температура (°C) и её толерантность.</summary>
public float OptimalTemperature { get; init; } = 18f;
/// <summary>Ширина толерантности по температуре (°C).</summary>
/// <summary>Ширина толерантности по температуре (°C) — половина плато полного роста вокруг оптимума.</summary>
public float TemperatureTolerance { get; init; } = 12f;
/// <summary>Морозостойкость (°C): на сколько ниже плато рост спадает до нуля (жёсткая нижняя граница).</summary>
public float ColdHardiness { get; init; } = 8f;
/// <summary>Жаростойкость (°C): на сколько выше плато рост спадает до нуля (жёсткая верхняя граница).</summary>
public float HeatHardiness { get; init; } = 10f;
/// <summary>Оптимальная плодородность почвы и её толерантность.</summary>
public float OptimalFertility { get; init; } = 1f;
+2
View File
@@ -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),
+6
View File
@@ -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)
{
+2 -1
View File
@@ -56,7 +56,8 @@ public static class PlantFactory
CellFertility = cellFertility,
},
new PlantOrganism { Genome = genome, Traits = traits },
new Fruiting()
new Fruiting(),
new TemperatureStress()
);
}
+3 -5
View File
@@ -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,
+50 -13
View File
@@ -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;
/// <summary>
/// Жизненный цикл растений: размножение (наследование по Менделю) и смерть от старости. Запускается
/// не каждый кадр, а раз в <see cref="IntervalDays"/> игрового дня. За тик: строит по-клеточную сетку
/// (счётчик плотности + «представитель» вида/генома), собирает умерших по возрасту, затем зрелые с
/// вероятностью <c>elapsed/ReproduceInterval</c> сеют потомка — приоритет перекрёстного опыления
/// (зрелый сосед того же вида в радиусе расселения), иначе самоопыление по гену. Семя падает в
/// случайную клетку радиуса, если это суша и не превышен лимит плотности. Структурные изменения
/// (создание/удаление сущностей) применяются после проходов по запросу — это безопасно.
/// Жизненный цикл растений: размножение (наследование по Менделю), смерть от старости и от
/// температурного стресса (вымерзание/перегрев). Запускается не каждый кадр, а раз в
/// <see cref="IntervalDays"/> игрового дня. За тик: строит по-клеточную сетку (счётчик плотности +
/// «представитель» вида/генома), копит/рассасывает температурный стресс по текущей температуре,
/// собирает умерших (старость или превышенный стресс), затем зрелые с вероятностью
/// <c>elapsed/ReproduceInterval</c> сеют потомка — приоритет перекрёстного опыления (зрелый сосед
/// того же вида в радиусе расселения), иначе самоопыление по гену. Семя падает в случайную клетку
/// радиуса, если это суша и не превышен лимит плотности. Структурные изменения (создание/удаление
/// сущностей) применяются после проходов по запросу — это безопасно.
/// </summary>
public sealed class PlantLifecycleSystem : BaseSystem
{
private const float IntervalDays = 0.25f;
/// <summary>Сколько игровых дней дормантности вне диапазона роста убивают растение.</summary>
private const float LethalStressDays = 6f;
/// <summary>Во сколько раз стресс рассасывается быстрее в пригодной температуре, чем копится вне.</summary>
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<PlantGrowth, PlantOrganism, Transform2D> _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<PlantGrowth, PlantOrganism, Transform2D>();
_query = store.Query<PlantGrowth, PlantOrganism, Transform2D, TemperatureStress>();
_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);
+28
View File
@@ -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
/// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary>
public bool IsVariant;
/// <summary>
/// Четыре точки температурной трапеции роста (модель RimWorld): жёсткий минимум, нижний край плато,
/// верхний край плато, жёсткий максимум. Плато — оптимум ± толерантность; жёсткие края отстоят от
/// плато на морозо-/жаростойкость. Кормит <see cref="MrGameEng.AI.Suitability.Trapezoid"/>.
/// </summary>
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);
}
/// <summary>Собирает фенотип из карты признаков, посчитанной <see cref="Phenotype.Compute"/>.</summary>
public static PlantPhenotype FromTraits(IReadOnlyDictionary<string, float> 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
/// <summary>Накоплено зрелых плодов (0..FruitYield).</summary>
public float RipeFruit;
}
/// <summary>
/// Накопленный температурный стресс растения в игровых днях. Пока температура вне диапазона роста
/// (трапеция <see cref="PlantPhenotype.TemperatureBand"/> даёт ноль — слишком холодно или жарко),
/// стресс копится; внутри диапазона — рассасывается. Превышение порога убивает растение
/// (вымерзание/перегрев). Управляется <c>PlantLifecycleSystem</c>.
/// </summary>
public struct TemperatureStress : IComponent
{
/// <summary>Накоплено дней дормантности вне диапазона роста; при превышении лимита — гибель.</summary>
public float Days;
}