G4: gene-driven content — fruiting, harvest products, leaf color

New plant content, all driven by genes through the trait layer (no engine
change — the genetics machinery already supports it).

- ProductDef + products.json (wood, grass, berries, acorn) — what plants
  yield; localized labels (ru/en).
- New numeric genes (genes.json): GeneFruitYield, GeneFruitSeason,
  GeneHarvestAmount, GeneLeafHue, each declaring its trait via formula.
  GenomeDef carries the per-species bases (PlantSet maps them into the
  species template); PlantDef gains harvestProduct / fruitProduct.
- PlantPhenotype gains FruitYield/FruitSeason/HarvestAmount/LeafHue.
  PlantFactory tints the sprite from the leaf-hue gene (combined with the
  morph variant). Plants carry a Fruiting component; PlantFruitingSystem
  ripens fruit on mature plants during their gene-chosen season and drops
  it off-season.
- plants.json: trees give wood + acorns (autumn), bushes berries (summer),
  grass gives grass — amounts/season/hue from genes.
- 'plant <species> [seed]' console command samples a species genome and
  prints its gene-driven traits and products.

Build clean; def JSON validated; boot smoke loads the new content.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-13 03:50:03 +03:00
co-authored by Claude Opus 4.8
parent 9c2c2d7fd0
commit 7fc4bcd301
12 changed files with 242 additions and 5 deletions
+14
View File
@@ -45,6 +45,20 @@
"default": 0.05, "min": 0.0, "max": 1.0, "tags": ["reproduction"], "default": 0.05, "min": 0.0, "max": 1.0, "tags": ["reproduction"],
"effects": { "mutationRate": "value" } }, "effects": { "mutationRate": "value" } },
// --- Контент (фаза G4): плодоношение, добыча, цвет ---
{ "defName": "GeneFruitYield", "parent": "BaseNumericGene", "label": "gene.fruitYield",
"default": 0, "min": 0, "max": 12, "tags": ["fruiting"],
"effects": { "fruitYield": "value" } },
{ "defName": "GeneFruitSeason", "parent": "BaseNumericGene", "label": "gene.fruitSeason",
"default": 1, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["fruiting"],
"effects": { "fruitSeason": "value" } },
{ "defName": "GeneHarvestAmount", "parent": "BaseNumericGene", "label": "gene.harvestAmount",
"default": 1, "min": 0, "max": 50, "tags": ["harvest"],
"effects": { "harvestAmount": "value" } },
{ "defName": "GeneLeafHue", "parent": "BaseNumericGene", "label": "gene.leafHue",
"default": 0.33, "min": 0, "max": 1, "spread": 0.04, "tags": ["morphology", "color"],
"effects": { "leafHue": "value" } },
// Дискретный ген морфы: вариант 0 доминирует, 1 (рецессивный) виден только в гомозиготе. // Дискретный ген морфы: вариант 0 доминирует, 1 (рецессивный) виден только в гомозиготе.
{ "defName": "GeneMorph", "kind": "Discrete", "label": "gene.morph", { "defName": "GeneMorph", "kind": "Discrete", "label": "gene.morph",
"variants": 2, "variantWeights": [0.82, 0.18], "mutationChance": 0.05, "tags": ["morphology"], "variants": 2, "variantWeights": [0.82, 0.18], "mutationChance": 0.05, "tags": ["morphology"],
+9 -3
View File
@@ -2,10 +2,12 @@
"type": "Plant", "type": "Plant",
"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",
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 16, "genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 16,
"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,
"fruitYield": 5, "fruitSeason": 2, "harvestAmount": 10, "leafHue": 0.30 },
"stages": [ "stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" }, { "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" },
{ "sizeCells": 1.2, "growDays": 9, "label": "plant.stage.sapling" }, { "sizeCells": 1.2, "growDays": 9, "label": "plant.stage.sapling" },
@@ -17,20 +19,24 @@
{ "defName": "TreeGrayPineA", "parent": "BaseTree", "label": "plant.pine", "texture": "things/plant/treegraypine/TreeGrayPineA" }, { "defName": "TreeGrayPineA", "parent": "BaseTree", "label": "plant.pine", "texture": "things/plant/treegraypine/TreeGrayPineA" },
{ "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",
"genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 20, "temperatureTolerance": 12, "genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 20, "temperatureTolerance": 12,
"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,
"harvestAmount": 2, "leafHue": 0.36 },
"stages": [ "stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 2, "label": "plant.stage.sprout" }, { "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 2, "label": "plant.stage.sprout" },
{ "sizeCells": 1.2, "label": "plant.stage.mature" } { "sizeCells": 1.2, "label": "plant.stage.mature" }
] }, ] },
{ "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4, { "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": 13,
"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,
"fruitYield": 3, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.32 },
"stages": [ "stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 3, "label": "plant.stage.sprout" }, { "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 3, "label": "plant.stage.sprout" },
{ "sizeCells": 1.4, "label": "plant.stage.mature" } { "sizeCells": 1.4, "label": "plant.stage.mature" }
+11
View File
@@ -0,0 +1,11 @@
{
"type": "Product",
// Продукты сбора/плодоношения растений. На них ссылаются PlantDef.harvestProduct / fruitProduct;
// количество задаётся генами (harvestAmount / fruitYield).
"defs": [
{ "defName": "ProductWood", "label": "product.wood", "kind": "material" },
{ "defName": "ProductGrass", "label": "product.grass", "kind": "material" },
{ "defName": "ProductBerry", "label": "product.berry", "kind": "food" },
{ "defName": "ProductAcorn", "label": "product.acorn", "kind": "food" }
]
}
+5
View File
@@ -78,6 +78,11 @@
"plant.stage.sapling": "sapling", "plant.stage.sapling": "sapling",
"plant.stage.mature": "mature", "plant.stage.mature": "mature",
"product.wood": "wood",
"product.grass": "grass",
"product.berry": "berries",
"product.acorn": "acorn",
"pawn.being": "being", "pawn.being": "being",
"pawn.bear": "bear", "pawn.bear": "bear",
"pawn.deer": "deer", "pawn.deer": "deer",
+5
View File
@@ -78,6 +78,11 @@
"plant.stage.sapling": "саженец", "plant.stage.sapling": "саженец",
"plant.stage.mature": "взрослое", "plant.stage.mature": "взрослое",
"product.wood": "древесина",
"product.grass": "трава",
"product.berry": "ягоды",
"product.acorn": "жёлудь",
"pawn.being": "житель", "pawn.being": "житель",
"pawn.bear": "медведь", "pawn.bear": "медведь",
"pawn.deer": "олень", "pawn.deer": "олень",
+1
View File
@@ -58,6 +58,7 @@ public sealed class GameContent
var defs = new DefDatabase(); var defs = new DefDatabase();
defs.RegisterType<TerrainDef>("Terrain"); defs.RegisterType<TerrainDef>("Terrain");
defs.RegisterType<GeneDef>("Gene"); defs.RegisterType<GeneDef>("Gene");
defs.RegisterType<ProductDef>("Product");
defs.RegisterType<PlantDef>("Plant"); defs.RegisterType<PlantDef>("Plant");
defs.RegisterType<PawnDef>("Pawn"); defs.RegisterType<PawnDef>("Pawn");
defs.RegisterType<WorldPresetDef>("WorldPreset"); defs.RegisterType<WorldPresetDef>("WorldPreset");
+29
View File
@@ -66,6 +66,17 @@ public sealed class PlantGrowthStage
public string? Label { get; init; } public string? Label { get; init; }
} }
/// <summary>
/// Продукт (Defs/products.json): что растение даёт при сборе (древесина, трава) или плодоношении
/// (ягоды, жёлудь). Источник истины — деф; растения ссылаются на него по имени, а количество
/// определяют гены. <see cref="Def.Label"/> — ключ локализации (product.*).
/// </summary>
public sealed class ProductDef : Def
{
/// <summary>Категория продукта (например "material" или "food") — для будущей экономики/инвентаря.</summary>
public string Kind { get; init; } = "";
}
/// <summary> /// <summary>
/// Базовый геном вида (вложенный объект <see cref="PlantDef.Genome"/>): оптимумы и толерантности по /// Базовый геном вида (вложенный объект <see cref="PlantDef.Genome"/>): оптимумы и толерантности по
/// факторам среды + базовая бодрость роста. Особь при спавне получает аллели рядом с этими базами /// факторам среды + базовая бодрость роста. Особь при спавне получает аллели рядом с этими базами
@@ -114,6 +125,18 @@ public sealed class GenomeDef
/// <summary>Доля разброса аллелей вокруг базы при генерации особи.</summary> /// <summary>Доля разброса аллелей вокруг базы при генерации особи.</summary>
public float Spread { get; init; } = 0.08f; public float Spread { get; init; } = 0.08f;
/// <summary>Плодовитость: сколько плодов несёт зрелое растение в сезон (0 — не плодоносит).</summary>
public float FruitYield { get; init; }
/// <summary>Сезон плодоношения (0 — весна, 1 — лето, 2 — осень, 3 — зима).</summary>
public float FruitSeason { get; init; } = 1f;
/// <summary>Сколько продукта даёт сбор растения (масштаб <see cref="PlantDef.HarvestProduct"/>).</summary>
public float HarvestAmount { get; init; } = 1f;
/// <summary>Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета.</summary>
public float LeafHue { get; init; } = 0.33f;
} }
/// <summary>Растение (Defs/plants.json): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary> /// <summary>Растение (Defs/plants.json): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
@@ -133,6 +156,12 @@ public sealed class PlantDef : Def
/// <summary>Базовый геном вида: оптимумы/толерантности по среде и бодрость роста.</summary> /// <summary>Базовый геном вида: оптимумы/толерантности по среде и бодрость роста.</summary>
public GenomeDef Genome { get; init; } = new(); public GenomeDef Genome { get; init; } = new();
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого при сборе растения (дерево/трава); null — несборное.</summary>
public string? HarvestProduct { get; init; }
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого плодами (ягоды/жёлудь); null — не плодоносит.</summary>
public string? FruitProduct { get; init; }
} }
/// <summary> /// <summary>
+5
View File
@@ -111,6 +111,11 @@ public sealed class PlantSet
Numeric("GeneReproduceInterval", g.ReproduceInterval), Numeric("GeneReproduceInterval", g.ReproduceInterval),
Numeric("GeneSelfPollination", g.SelfPollination), Numeric("GeneSelfPollination", g.SelfPollination),
Numeric("GeneMutationRate", g.MutationRate), Numeric("GeneMutationRate", g.MutationRate),
// Контент (G4): плодоношение, добыча, цвет.
Numeric("GeneFruitYield", g.FruitYield),
new GenomeTemplate.Entry(Gene("GeneFruitSeason"), g.FruitSeason, 0f), // сезон фиксирован по виду
Numeric("GeneHarvestAmount", g.HarvestAmount),
new GenomeTemplate.Entry(Gene("GeneLeafHue"), g.LeafHue, 0.04f),
new GenomeTemplate.Entry( new GenomeTemplate.Entry(
Gene("GeneMorph"), Gene("GeneMorph"),
0f, 0f,
+64
View File
@@ -162,6 +162,7 @@ public sealed class WorldScene : Scene
); );
UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, lighting, CellSize)); UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, lighting, CellSize));
UpdateSystems.Add(new PlantFruitingSystem(_plants, calendar, climate));
UpdateSystems.Add( UpdateSystems.Add(
new PlantLifecycleSystem( new PlantLifecycleSystem(
Store, Store,
@@ -502,6 +503,11 @@ public sealed class WorldScene : Scene
"gene [seed] — generate a genome from Gene defs, show alleles/traits and a bred child", "gene [seed] — generate a genome from Gene defs, show alleles/traits and a bred child",
(c, args) => RunGeneDemo(c, content, args) (c, args) => RunGeneDemo(c, content, args)
); );
console.Register(
"plant",
"plant <species> [seed] — sample a species genome and show its gene-driven traits/products",
(c, args) => RunPlantDemo(c, content, args)
);
console.Register( console.Register(
"menu", "menu",
"menu — return to the main menu", "menu — return to the main menu",
@@ -550,4 +556,62 @@ public sealed class WorldScene : Scene
+ $"variant={childTraits.GetValueOrDefault("variant"):0}" + $"variant={childTraits.GetValueOrDefault("variant"):0}"
); );
} }
// Демонстрация генного контента (фаза G4): по виду берём его набор генов, генерируем особь и
// печатаем признаки роста/жизни и продукты (плоды/добыча с количеством от генов).
private void RunPlantDemo(DevConsole console, GameContent content, string[] args)
{
var species = content.Defs.NamesOf("Plant");
if (args.Length == 0)
{
console.WriteLine("usage: plant <species> [seed] e.g. plant TreeOakA");
console.WriteLine($"species: {string.Join(", ", species)}");
return;
}
if (!content.Defs.TryGet<PlantDef>(args[0], out var def))
{
console.WriteLine($"no plant '{args[0]}'; species: {string.Join(", ", species)}");
return;
}
var index = _plants.IndexOf(def);
var seed = args.Length > 1 ? int.Parse(args[1]) : Random.Shared.Next();
var genome = _plants[index].Template.Generate(new Random(seed));
var traits = PlantPhenotype.FromTraits(Phenotype.Compute(genome, _plants.GeneRegistry));
console.WriteLine(
$"{def.DefName} (seed {seed}){(traits.IsVariant ? " [variant morph]" : "")}:"
);
console.WriteLine(
$" vigor {traits.Vigor:0.##}, lifespan {traits.Lifespan:0} d, "
+ $"optimalLight {traits.OptimalLight:0.##}, leafHue {traits.LeafHue:0.##}"
);
if (def.HarvestProduct is { } harvest)
{
console.WriteLine(
$" harvest: {ProductLabel(content, harvest)} x{traits.HarvestAmount:0.#}"
);
}
if (def.FruitProduct is { } fruit && traits.FruitYield >= 1f)
{
var season = content.Languages.Get(
$"season.{((Season)traits.FruitSeason).ToString().ToLowerInvariant()}"
);
console.WriteLine(
$" fruit: {ProductLabel(content, fruit)} x{traits.FruitYield:0.#} in {season}"
);
}
else
{
console.WriteLine(" barren (no fruit)");
}
}
private static string ProductLabel(GameContent content, string productDefName) =>
content.Defs.TryGet<ProductDef>(productDefName, out var product)
? content.Languages.Get(product.Label)
: productDefName;
} }
+16 -2
View File
@@ -17,6 +17,10 @@ public static class PlantFactory
// Тинт рецессивной морфы (домножается на текстуру) — делает вариант визуально отличимым. // Тинт рецессивной морфы (домножается на текстуру) — делает вариант визуально отличимым.
private static readonly Color VariantTint = new(214, 150, 176); private static readonly Color VariantTint = new(214, 150, 176);
// Якоря оттенка листвы: тёплый жёлто-зелёный → холодный сине-зелёный (по гену leafHue).
private static readonly Color LeafWarm = new(170, 200, 90);
private static readonly Color LeafCool = new(90, 170, 150);
/// <summary>Создаёт растение вида <paramref name="species"/> с заданным возрастом и геномом.</summary> /// <summary>Создаёт растение вида <paramref name="species"/> с заданным возрастом и геномом.</summary>
public static Entity Create( public static Entity Create(
EntityStore store, EntityStore store,
@@ -36,7 +40,7 @@ public static class PlantFactory
var sprite = new Sprite(resolved.Region, GameLayers.Beings); var sprite = new Sprite(resolved.Region, GameLayers.Beings);
sprite.CenterOrigin(); sprite.CenterOrigin();
sprite.Color = traits.IsVariant ? VariantTint : Color.White; sprite.Color = Tint(traits);
return store.CreateEntity( return store.CreateEntity(
new Transform2D( new Transform2D(
@@ -51,7 +55,17 @@ public static class PlantFactory
AgeDays = ageDays, AgeDays = ageDays,
CellFertility = cellFertility, CellFertility = cellFertility,
}, },
new PlantOrganism { Genome = genome, Traits = traits } new PlantOrganism { Genome = genome, Traits = traits },
new Fruiting()
); );
} }
// Цвет растения из генов: лёгкий оттенок листвы (ген leafHue) поверх текстуры; рецессивная морфа
// дополнительно сдвигает тинт в свою сторону.
private static Color Tint(in PlantPhenotype traits)
{
var leaf = Color.Lerp(LeafWarm, LeafCool, Math.Clamp(traits.LeafHue, 0f, 1f));
var tint = Color.Lerp(Color.White, leaf, 0.35f);
return traits.IsVariant ? Color.Lerp(tint, VariantTint, 0.6f) : tint;
}
} }
+62
View File
@@ -0,0 +1,62 @@
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using MrGameEng.AI;
using MrGameEng.Core;
namespace LittleSim.Sim;
/// <summary>
/// Плодоношение (фаза G4): у зрелого растения с признаком <see cref="PlantPhenotype.FruitYield"/> &gt; 0
/// в его генетический сезон (<see cref="PlantPhenotype.FruitSeason"/>) копятся зрелые плоды до потолка
/// <see cref="PlantPhenotype.FruitYield"/>; вне сезона плоды опадают. Полностью управляется генами через
/// признаки — система лишь применяет их к компоненту <see cref="Fruiting"/>. Незрелые/бесплодные
/// растения остаются с нулём.
/// </summary>
public sealed class PlantFruitingSystem(PlantSet plants, Calendar calendar, Climate climate)
: QuerySystem<PlantGrowth, PlantOrganism, Fruiting>
{
// За сколько игровых дней набирается/опадает полный урожай.
private const float RipenDays = 10f;
private const float DropDays = 20f;
protected override void OnUpdate()
{
var deltaDays = Tick.deltaTime / calendar.SecondsPerDay;
if (deltaDays <= 0f)
{
return; // пауза
}
var season = (int)climate.Season;
foreach (var (growths, organisms, fruits, _) in Query.Chunks)
{
var g = growths.Span;
var orgs = organisms.Span;
var f = fruits.Span;
for (var i = 0; i < g.Length; i++)
{
ref var traits = ref orgs[i].Traits;
var mature = g[i].Stage >= plants[g[i].Species].Stages.Length - 1;
if (!mature || traits.FruitYield < 1f)
{
f[i].RipeFruit = 0f; // незрелое или бесплодное — плодов нет
continue;
}
ref var ripe = ref f[i].RipeFruit;
if (season == traits.FruitSeason)
{
ripe = MathF.Min(
traits.FruitYield,
ripe + deltaDays * (traits.FruitYield / RipenDays)
);
}
else
{
ripe = MathF.Max(0f, ripe - deltaDays * (traits.FruitYield / DropDays));
}
}
}
}
}
+21
View File
@@ -24,6 +24,12 @@ public struct PlantPhenotype
public float SelfPollination; public float SelfPollination;
public float MutationRate; public float MutationRate;
// --- Контент (G4): плодоношение, добыча, цвет ---
public float FruitYield;
public int FruitSeason;
public float HarvestAmount;
public float LeafHue;
/// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary> /// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary>
public bool IsVariant; public bool IsVariant;
@@ -45,6 +51,10 @@ public struct PlantPhenotype
ReproduceInterval = T("reproduceInterval"), ReproduceInterval = T("reproduceInterval"),
SelfPollination = T("selfPollination"), SelfPollination = T("selfPollination"),
MutationRate = T("mutationRate"), MutationRate = T("mutationRate"),
FruitYield = T("fruitYield"),
FruitSeason = (int)MathF.Round(Math.Clamp(T("fruitSeason"), 0f, 3f)),
HarvestAmount = T("harvestAmount"),
LeafHue = T("leafHue"),
IsVariant = T("variant") >= 0.5f, IsVariant = T("variant") >= 0.5f,
}; };
} }
@@ -64,3 +74,14 @@ public struct PlantOrganism : IComponent
/// <summary>Выраженные признаки — то, что читают системы роста/жизненного цикла.</summary> /// <summary>Выраженные признаки — то, что читают системы роста/жизненного цикла.</summary>
public PlantPhenotype Traits; public PlantPhenotype Traits;
} }
/// <summary>
/// Плодоношение растения: сколько зрелых плодов сейчас на нём. Копится у зрелого растения в его
/// сезон плодоношения (признак <see cref="PlantPhenotype.FruitSeason"/>) до потолка
/// <see cref="PlantPhenotype.FruitYield"/>, вне сезона опадает. Управляется <c>PlantFruitingSystem</c>.
/// </summary>
public struct Fruiting : IComponent
{
/// <summary>Накоплено зрелых плодов (0..FruitYield).</summary>
public float RipeFruit;
}