Compare commits

..
11 changed files with 113 additions and 143 deletions
+9
View File
@@ -19,6 +19,15 @@
{ "defName": "GeneTemperatureTolerance", "parent": "BaseNumericGene", "label": "gene.temperatureTolerance", { "defName": "GeneTemperatureTolerance", "parent": "BaseNumericGene", "label": "gene.temperatureTolerance",
"default": 16, "min": 2, "max": 40, "tags": ["environment", "temperature"], "default": 16, "min": 2, "max": 40, "tags": ["environment", "temperature"],
"effects": { "temperatureTolerance": "value" } }, "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", { "defName": "GeneOptimalFertility", "parent": "BaseNumericGene", "label": "gene.optimalFertility",
"default": 1.4, "min": 0.1, "max": 3.0, "tags": ["environment", "soil"], "default": 1.4, "min": 0.1, "max": 3.0, "tags": ["environment", "soil"],
"effects": { "optimalFertility": "value" } }, "effects": { "optimalFertility": "value" } },
+6 -3
View File
@@ -3,7 +3,8 @@
"defs": [ "defs": [
{ "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28, { "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28,
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn", "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, "optimalFertility": 1.4, "fertilityTolerance": 0.9, "vigor": 1.0,
"lifespan": 160, "dispersalRange": 2, "reproduceInterval": 14, "selfPollination": 0.2, "lifespan": 160, "dispersalRange": 2, "reproduceInterval": 14, "selfPollination": 0.2,
"mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08, "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, { "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.2,
"harvestProduct": "ProductGrass", "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, "optimalFertility": 1.0, "fertilityTolerance": 1.0, "vigor": 1.3,
"lifespan": 22, "dispersalRange": 3, "reproduceInterval": 3, "selfPollination": 0.8, "lifespan": 22, "dispersalRange": 3, "reproduceInterval": 3, "selfPollination": 0.8,
"mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1, "mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1,
@@ -32,7 +34,8 @@
{ "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4, { "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4,
"fruitProduct": "ProductBerry", "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, "optimalFertility": 1.1, "fertilityTolerance": 1.0, "vigor": 1.0,
"lifespan": 60, "dispersalRange": 2, "reproduceInterval": 7, "selfPollination": 0.5, "lifespan": 60, "dispersalRange": 2, "reproduceInterval": 7, "selfPollination": 0.5,
"mutationRate": 0.05, "variantChance": 0.18, "spread": 0.1, "mutationRate": 0.05, "variantChance": 0.18, "spread": 0.1,
-36
View File
@@ -1,36 +0,0 @@
# Roadmap LittleSim
Этап закрыт, когда механика работает, наблюдаема (HUD/консоль/визуал)
и управляется командой консоли.
## Этап 0 — Каркас ✅
- [x] Репозиторий с движком-сабмодулем, общий solution
- [x] Генерация рельефа по сиду (вода/песок/трава/лес/горы)
- [x] Жители-заглушки (блуждание), камера бога, HUD, консоль (`regen`)
## Этап 1 — Жители как агенты
- [~] Потребности: голод, усталость (тикают со временем) — усталость/энергия готова (`PawnNeeds`), голод впереди
- [~] Utility-выбор действия: искать еду / отдыхать / бродить — каркас на движке (`MrGameEng.AI`), выбор «отдыхать/бродить» работает; «искать еду» впереди
- [ ] Еда на траве/в лесу: растёт, собирается, истощается
- [ ] Смерть от голода; визуальное состояние жителя (цвет/прозрачность)
- [ ] Консоль: `spawn <n>`, `feed`, `starve` для тестов
## Этап 2 — Общество
- [ ] Дома и поселения (житель несёт еду домой, спит дома)
- [ ] Размножение при сытости; рост населения
- [ ] Простые профессии: собиратель, строитель
## Этап 3 — Бог
- [ ] Вера как ресурс (генерируется жителями)
- [ ] Первые силы: дождь (ускоряет рост еды), молния (поджигает лес)
- [ ] Терраформинг клеток
## Этап 4 — Экология и время
- [ ] День/ночь, сезоны
- [ ] Восстановление/деградация ресурсов, пожары
- [ ] Миграции при нехватке ресурсов
## Бэклог
- Сохранение/загрузка мира (сериализация Friflo)
- Фиксированный тик симуляции, независимый от рендера
- Исторические графики (население, еда) в дев-оверлее
-84
View File
@@ -1,84 +0,0 @@
# Растения как экосистема
Дизайн системы растений: рост зависит от среды, среда описана факторами, реакция растения на
факторы задаётся **генами**, а размножение передаёт гены потомкам с мутацией — появляется
естественный отбор. Документ — единый ориентир для фаз A–D; детали каждой фазы уточняются её
отдельным планом.
## Модель роста
Скорость роста растения в данный момент:
```
скорость = база × пригодность(свет) × пригодность(температура) × пригодность(плодородность)
```
Каждая «пригодность» — число в [0..1] из кривой отклика (движковый `ResponseCurve`, модуль
`MrGameEng.AI`) с **оптимумом** и **шириной толерантности**, которые берутся из генов растения.
Растение вне своих оптимумов растёт медленно или стоит; в своих — быстро. Так все факторы среды
сводятся к одному механизму, а различие видов/особей — целиком в генах.
Фактор «возраст/стадия» остаётся как сейчас (`PlantGrowthStage` в дефах): пригодности умножают
прирост `AgeDays`, стадии переключают спрайт/размер по накопленному возрасту.
## Факторы среды
- **Свет** — амбиент день/ночь (`MrGameEng.Lighting.DayNight`), сэмплируемый в точке
(`SampleAt(world)`); глобальный в фазе A, локальный (с тенями) в фазе D.
- **Температура** — непрерывная, из климата (`MrGameEng.Core.Climate`): сезонный синус по дню года
+ суточные колебания (холоднее ночью).
- **Плодородность** — пока константа типа террейна (`TerrainDef.Fertility`); позже возможна
по-клеточная почва с истощением/восстановлением.
## Гены (диплоидный геном, гибридная экспрессия)
Каждое растение несёт **геном**: по паре аллелей на ген. Экспрессия гибридная:
- **Числовые гены** (оптимум света, толерантность к свету, оптимум температуры, толерантность,
потребность в плодородности, макс. размер, базовая скорость, продолжительность жизни, дальность
расселения, плодовитость, темп мутаций, способность к самоопылению) — фенотип = **смешение**
(среднее двух аллелей). Реализм отбора: промежуточные значения, плавный дрейф.
- **Дискретные гены** (например, цвет/морфа, наличие признака) — **доминант/рецессив**: рецессивный
фенотип виден только в гомозиготе (aa).
Наследование (мейоз): каждый родитель отдаёт по одному случайному аллелю на ген; потомок получает
по одному от каждого. Затем — мутация (с вероятностью из гена темпа мутаций сдвигаем числовой
аллель/переключаем дискретный). Всё детерминировано сидом мира.
Стартовая популяция: геномы генерируются из сида (вид задаёт базовый геном и разброс аллелей в
дефе; особи отклоняются).
## Размножение (полуреалистичное)
- **Перекрёстное опыление (приоритет):** зрелое растение ищет зрелого соседа того же вида в радиусе
(ген дальности; запрос через spatial-hash движка). Есть партнёр → семя с геномом от двух
родителей (Мендель + мутация).
- **Самоопыление (запас):** нет партнёра и ген «способность к самоопылению» позволяет → семя из
одного родителя (само-скрещивание аллелей + мутация).
- **Расселение:** семя падает в клетку в радиусе расселения; прорастает, если клетка подходит
(суша, не занято сверх лимита, пригодность не нулевая).
- **Лимит плотности:** не больше N растений на клетку/площадь — иначе взрыв численности.
- **Старение/смерть:** возраст превышает ген жизни → смерть; или длительный стресс (пригодность
около нуля) → гибель. Смерть освобождает место — баланс с размножением и основа отбора.
## Производительность и сейв
- Представление — **по сущности на растение** (видно в рендере и ECS-инспекторе, индивидуальные
гены). Защита от взрыва: лимит плотности, смерть, разумные радиусы, проверки размножения по
таймеру/сезону (не каждый кадр на каждое растение), spatial-hash для соседей.
- **Персист:** геномы, позиции, возраст и стадия растений сохраняются в `WorldSave` (мир больше не
воспроизводится только из сида — эволюция переживает загрузку).
- **Детерминизм:** мутации/расселение/опыление — на сидированном `Random`, как в остальной симуляции.
## Фазы реализации
- **A — климат и день/ночь (движок).** `Climate` (температура/сезон поверх `Calendar`),
`DayNight` (амбиент, `Renderer2D.AmbientLight` на World-слоях, сэмплируемый `SampleAt`),
затемнение мира ночью, HUD: сезон/температура/время. Фундамент для влияния среды на рост.
- **B — гены и рост по пригодностям (игра).** Компонент `PlantGenome`, расчёт пригодностей
(свет из `DayNight.SampleAt`, температура из `Climate`, плодородность из террейна), замена
плоского `GrowthRate`. Геном виден/правится в инспекторе.
- **C — размножение и смерть (игра).** Перекрёстное/самоопыление, расселение, лимит плотности,
старение/стресс-смерть, персист популяции в сейв.
- **D — полноценное 2D-освещение (движок).** Точечные источники + тени (окклюзия от препятствий);
`DayNight.SampleAt` начинает возвращать локальную освещённость, и затенённые растения растут хуже.
+7 -1
View File
@@ -93,9 +93,15 @@ public sealed class GenomeDef
/// <summary>Оптимальная температура (°C) и её толерантность.</summary> /// <summary>Оптимальная температура (°C) и её толерантность.</summary>
public float OptimalTemperature { get; init; } = 18f; public float OptimalTemperature { get; init; } = 18f;
/// <summary>Ширина толерантности по температуре (°C).</summary> /// <summary>Ширина толерантности по температуре (°C) — половина плато полного роста вокруг оптимума.</summary>
public float TemperatureTolerance { get; init; } = 12f; 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> /// <summary>Оптимальная плодородность почвы и её толерантность.</summary>
public float OptimalFertility { get; init; } = 1f; public float OptimalFertility { get; init; } = 1f;
+2
View File
@@ -103,6 +103,8 @@ public sealed class PlantSet
Numeric("GeneLightTolerance", g.LightTolerance), Numeric("GeneLightTolerance", g.LightTolerance),
Numeric("GeneOptimalTemperature", g.OptimalTemperature), Numeric("GeneOptimalTemperature", g.OptimalTemperature),
Numeric("GeneTemperatureTolerance", g.TemperatureTolerance), Numeric("GeneTemperatureTolerance", g.TemperatureTolerance),
Numeric("GeneColdHardiness", g.ColdHardiness),
Numeric("GeneHeatHardiness", g.HeatHardiness),
Numeric("GeneOptimalFertility", g.OptimalFertility), Numeric("GeneOptimalFertility", g.OptimalFertility),
Numeric("GeneFertilityTolerance", g.FertilityTolerance), Numeric("GeneFertilityTolerance", g.FertilityTolerance),
Numeric("GeneVigor", g.Vigor), Numeric("GeneVigor", g.Vigor),
+6
View File
@@ -168,6 +168,7 @@ public sealed class WorldScene : Scene
Store, Store,
_plants, _plants,
calendar, calendar,
climate,
Context.Clock, Context.Clock,
_config.Width, _config.Width,
_config.Height, _config.Height,
@@ -587,6 +588,11 @@ public sealed class WorldScene : Scene
$" vigor {traits.Vigor:0.##}, lifespan {traits.Lifespan:0} d, " $" vigor {traits.Vigor:0.##}, lifespan {traits.Lifespan:0} d, "
+ $"optimalLight {traits.OptimalLight:0.##}, leafHue {traits.LeafHue:0.##}" + $"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) if (def.HarvestProduct is { } harvest)
{ {
+2 -1
View File
@@ -56,7 +56,8 @@ public static class PlantFactory
CellFertility = cellFertility, CellFertility = cellFertility,
}, },
new PlantOrganism { Genome = genome, Traits = traits }, 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; ref var traits = ref orgs[i].Traits;
var light = lighting.SampleAt(t[i].Position); var light = lighting.SampleAt(t[i].Position);
// Температура — трапеция RimWorld: плато полного роста, жёсткие края (дормантность вне).
var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
var rate = var rate =
traits.Vigor traits.Vigor
* Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance) * Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance)
* Suitability.Gaussian( * Suitability.Trapezoid(temperature, tMin, tLow, tHigh, tMax)
temperature,
traits.OptimalTemperature,
traits.TemperatureTolerance
)
* Suitability.Gaussian( * Suitability.Gaussian(
grow.CellFertility, grow.CellFertility,
traits.OptimalFertility, traits.OptimalFertility,
+50 -13
View File
@@ -2,6 +2,7 @@ using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems; using Friflo.Engine.ECS.Systems;
using LittleSim.Content; using LittleSim.Content;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.Genetics; using MrGameEng.Genetics;
using MrGameEng.Graphics; using MrGameEng.Graphics;
@@ -9,21 +10,30 @@ using MrGameEng.Graphics;
namespace LittleSim.Sim; namespace LittleSim.Sim;
/// <summary> /// <summary>
/// Жизненный цикл растений: размножение (наследование по Менделю) и смерть от старости. Запускается /// Жизненный цикл растений: размножение (наследование по Менделю), смерть от старости и от
/// не каждый кадр, а раз в <see cref="IntervalDays"/> игрового дня. За тик: строит по-клеточную сетку /// температурного стресса (вымерзание/перегрев). Запускается не каждый кадр, а раз в
/// (счётчик плотности + «представитель» вида/генома), собирает умерших по возрасту, затем зрелые с /// <see cref="IntervalDays"/> игрового дня. За тик: строит по-клеточную сетку (счётчик плотности +
/// вероятностью <c>elapsed/ReproduceInterval</c> сеют потомка — приоритет перекрёстного опыления /// «представитель» вида/генома), копит/рассасывает температурный стресс по текущей температуре,
/// (зрелый сосед того же вида в радиусе расселения), иначе самоопыление по гену. Семя падает в /// собирает умерших (старость или превышенный стресс), затем зрелые с вероятностью
/// случайную клетку радиуса, если это суша и не превышен лимит плотности. Структурные изменения /// <c>elapsed/ReproduceInterval</c> сеют потомка — приоритет перекрёстного опыления (зрелый сосед
/// (создание/удаление сущностей) применяются после проходов по запросу — это безопасно. /// того же вида в радиусе расселения), иначе самоопыление по гену. Семя падает в случайную клетку
/// радиуса, если это суша и не превышен лимит плотности. Структурные изменения (создание/удаление
/// сущностей) применяются после проходов по запросу — это безопасно.
/// </summary> /// </summary>
public sealed class PlantLifecycleSystem : BaseSystem public sealed class PlantLifecycleSystem : BaseSystem
{ {
private const float IntervalDays = 0.25f; 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 EntityStore _store;
private readonly PlantSet _plants; private readonly PlantSet _plants;
private readonly Calendar _calendar; private readonly Calendar _calendar;
private readonly Climate _climate;
private readonly GameClock _clock; private readonly GameClock _clock;
private readonly int _width; private readonly int _width;
private readonly int _height; private readonly int _height;
@@ -32,7 +42,12 @@ public sealed class PlantLifecycleSystem : BaseSystem
private readonly bool[] _cellLand; private readonly bool[] _cellLand;
private readonly int _densityCap; private readonly int _densityCap;
private readonly Random _rng; 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 int[] _count;
private readonly CellRep[] _rep; private readonly CellRep[] _rep;
@@ -45,6 +60,7 @@ public sealed class PlantLifecycleSystem : BaseSystem
EntityStore store, EntityStore store,
PlantSet plants, PlantSet plants,
Calendar calendar, Calendar calendar,
Climate climate,
GameClock clock, GameClock clock,
int width, int width,
int height, int height,
@@ -58,6 +74,7 @@ public sealed class PlantLifecycleSystem : BaseSystem
_store = store; _store = store;
_plants = plants; _plants = plants;
_calendar = calendar; _calendar = calendar;
_climate = climate;
_clock = clock; _clock = clock;
_width = width; _width = width;
_height = height; _height = height;
@@ -66,7 +83,7 @@ public sealed class PlantLifecycleSystem : BaseSystem
_cellLand = cellLand; _cellLand = cellLand;
_densityCap = densityCap; _densityCap = densityCap;
_rng = new Random(seed); _rng = new Random(seed);
_query = store.Query<PlantGrowth, PlantOrganism, Transform2D>(); _query = store.Query<PlantGrowth, PlantOrganism, Transform2D, TemperatureStress>();
_count = new int[width * height]; _count = new int[width * height];
_rep = new CellRep[width * height]; _rep = new CellRep[width * height];
} }
@@ -82,13 +99,13 @@ public sealed class PlantLifecycleSystem : BaseSystem
var elapsed = _accumulator; var elapsed = _accumulator;
_accumulator = 0f; _accumulator = 0f;
BuildGridAndCollect(); BuildGridAndCollect(elapsed);
Reproduce(elapsed); Reproduce(elapsed);
Apply(); Apply();
} }
// Проход 1: сетка плотности + представители, сбор умерших и зрелых-кандидатов. // Проход 1: сетка плотности + представители, температурный стресс, сбор умерших и зрелых-кандидатов.
private void BuildGridAndCollect() private void BuildGridAndCollect(float elapsed)
{ {
Array.Clear(_count); Array.Clear(_count);
for (var c = 0; c < _rep.Length; c++) for (var c = 0; c < _rep.Length; c++)
@@ -100,11 +117,14 @@ public sealed class PlantLifecycleSystem : BaseSystem
_candidates.Clear(); _candidates.Clear();
_births.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 g = growths.Span;
var orgs = organisms.Span; var orgs = organisms.Span;
var t = transforms.Span; var t = transforms.Span;
var st = stresses.Span;
for (var i = 0; i < g.Length; i++) for (var i = 0; i < g.Length; i++)
{ {
ref var grow = ref g[i]; ref var grow = ref g[i];
@@ -115,6 +135,23 @@ public sealed class PlantLifecycleSystem : BaseSystem
continue; 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 pos = t[i].Position;
var cx = Math.Clamp((int)(pos.X / _cellSize), 0, _width - 1); var cx = Math.Clamp((int)(pos.X / _cellSize), 0, _width - 1);
var cy = Math.Clamp((int)(pos.Y / _cellSize), 0, _height - 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 LightTolerance;
public float OptimalTemperature; public float OptimalTemperature;
public float TemperatureTolerance; public float TemperatureTolerance;
public float ColdHardiness;
public float HeatHardiness;
public float OptimalFertility; public float OptimalFertility;
public float FertilityTolerance; public float FertilityTolerance;
public float Vigor; public float Vigor;
@@ -33,6 +35,18 @@ public struct PlantPhenotype
/// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary> /// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary>
public bool IsVariant; 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> /// <summary>Собирает фенотип из карты признаков, посчитанной <see cref="Phenotype.Compute"/>.</summary>
public static PlantPhenotype FromTraits(IReadOnlyDictionary<string, float> traits) public static PlantPhenotype FromTraits(IReadOnlyDictionary<string, float> traits)
{ {
@@ -43,6 +57,8 @@ public struct PlantPhenotype
LightTolerance = T("lightTolerance"), LightTolerance = T("lightTolerance"),
OptimalTemperature = T("optimalTemperature"), OptimalTemperature = T("optimalTemperature"),
TemperatureTolerance = T("temperatureTolerance"), TemperatureTolerance = T("temperatureTolerance"),
ColdHardiness = T("coldHardiness"),
HeatHardiness = T("heatHardiness"),
OptimalFertility = T("optimalFertility"), OptimalFertility = T("optimalFertility"),
FertilityTolerance = T("fertilityTolerance"), FertilityTolerance = T("fertilityTolerance"),
Vigor = T("vigor"), Vigor = T("vigor"),
@@ -85,3 +101,15 @@ public struct Fruiting : IComponent
/// <summary>Накоплено зрелых плодов (0..FruitYield).</summary> /// <summary>Накоплено зрелых плодов (0..FruitYield).</summary>
public float RipeFruit; public float RipeFruit;
} }
/// <summary>
/// Накопленный температурный стресс растения в игровых днях. Пока температура вне диапазона роста
/// (трапеция <see cref="PlantPhenotype.TemperatureBand"/> даёт ноль — слишком холодно или жарко),
/// стресс копится; внутри диапазона — рассасывается. Превышение порога убивает растение
/// (вымерзание/перегрев). Управляется <c>PlantLifecycleSystem</c>.
/// </summary>
public struct TemperatureStress : IComponent
{
/// <summary>Накоплено дней дормантности вне диапазона роста; при превышении лимита — гибель.</summary>
public float Days;
}