From 7fc4bcd3015f139c84d918fdf010ace2b731ee7d Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sat, 13 Jun 2026 03:50:03 +0300 Subject: [PATCH] =?UTF-8?q?G4:=20gene-driven=20content=20=E2=80=94=20fruit?= =?UTF-8?q?ing,=20harvest=20products,=20leaf=20color?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 [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 --- Mods/Core/Defs/genes.json | 14 ++++++ Mods/Core/Defs/plants.json | 12 +++-- Mods/Core/Defs/products.json | 11 ++++ Mods/Core/Languages/en/ui.json | 5 ++ Mods/Core/Languages/ru/ui.json | 5 ++ src/LittleSim/Content/GameContent.cs | 1 + src/LittleSim/Content/GameDefs.cs | 29 +++++++++++ src/LittleSim/Content/PlantSet.cs | 5 ++ src/LittleSim/Scenes/WorldScene.cs | 64 ++++++++++++++++++++++++ src/LittleSim/Sim/PlantFactory.cs | 18 ++++++- src/LittleSim/Sim/PlantFruitingSystem.cs | 62 +++++++++++++++++++++++ src/LittleSim/Sim/PlantOrganism.cs | 21 ++++++++ 12 files changed, 242 insertions(+), 5 deletions(-) create mode 100644 Mods/Core/Defs/products.json create mode 100644 src/LittleSim/Sim/PlantFruitingSystem.cs diff --git a/Mods/Core/Defs/genes.json b/Mods/Core/Defs/genes.json index de74c8c..4252775 100644 --- a/Mods/Core/Defs/genes.json +++ b/Mods/Core/Defs/genes.json @@ -45,6 +45,20 @@ "default": 0.05, "min": 0.0, "max": 1.0, "tags": ["reproduction"], "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 (рецессивный) виден только в гомозиготе. { "defName": "GeneMorph", "kind": "Discrete", "label": "gene.morph", "variants": 2, "variantWeights": [0.82, 0.18], "mutationChance": 0.05, "tags": ["morphology"], diff --git a/Mods/Core/Defs/plants.json b/Mods/Core/Defs/plants.json index 7014534..d39427c 100644 --- a/Mods/Core/Defs/plants.json +++ b/Mods/Core/Defs/plants.json @@ -2,10 +2,12 @@ "type": "Plant", "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, "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 }, + "mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08, + "fruitYield": 5, "fruitSeason": 2, "harvestAmount": 10, "leafHue": 0.30 }, "stages": [ { "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" }, { "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": "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, "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 }, + "mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1, + "harvestAmount": 2, "leafHue": 0.36 }, "stages": [ { "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 2, "label": "plant.stage.sprout" }, { "sizeCells": 1.2, "label": "plant.stage.mature" } ] }, { "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4, + "fruitProduct": "ProductBerry", "genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 13, "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 }, + "mutationRate": 0.05, "variantChance": 0.18, "spread": 0.1, + "fruitYield": 3, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.32 }, "stages": [ { "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 3, "label": "plant.stage.sprout" }, { "sizeCells": 1.4, "label": "plant.stage.mature" } diff --git a/Mods/Core/Defs/products.json b/Mods/Core/Defs/products.json new file mode 100644 index 0000000..f43d352 --- /dev/null +++ b/Mods/Core/Defs/products.json @@ -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" } + ] +} diff --git a/Mods/Core/Languages/en/ui.json b/Mods/Core/Languages/en/ui.json index db6ac32..2ca64af 100644 --- a/Mods/Core/Languages/en/ui.json +++ b/Mods/Core/Languages/en/ui.json @@ -78,6 +78,11 @@ "plant.stage.sapling": "sapling", "plant.stage.mature": "mature", + "product.wood": "wood", + "product.grass": "grass", + "product.berry": "berries", + "product.acorn": "acorn", + "pawn.being": "being", "pawn.bear": "bear", "pawn.deer": "deer", diff --git a/Mods/Core/Languages/ru/ui.json b/Mods/Core/Languages/ru/ui.json index 138d77e..f2602cd 100644 --- a/Mods/Core/Languages/ru/ui.json +++ b/Mods/Core/Languages/ru/ui.json @@ -78,6 +78,11 @@ "plant.stage.sapling": "саженец", "plant.stage.mature": "взрослое", + "product.wood": "древесина", + "product.grass": "трава", + "product.berry": "ягоды", + "product.acorn": "жёлудь", + "pawn.being": "житель", "pawn.bear": "медведь", "pawn.deer": "олень", diff --git a/src/LittleSim/Content/GameContent.cs b/src/LittleSim/Content/GameContent.cs index 347da4c..2ec2b73 100644 --- a/src/LittleSim/Content/GameContent.cs +++ b/src/LittleSim/Content/GameContent.cs @@ -58,6 +58,7 @@ public sealed class GameContent var defs = new DefDatabase(); defs.RegisterType("Terrain"); defs.RegisterType("Gene"); + defs.RegisterType("Product"); defs.RegisterType("Plant"); defs.RegisterType("Pawn"); defs.RegisterType("WorldPreset"); diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs index 443f94f..781b6a0 100644 --- a/src/LittleSim/Content/GameDefs.cs +++ b/src/LittleSim/Content/GameDefs.cs @@ -66,6 +66,17 @@ public sealed class PlantGrowthStage public string? Label { get; init; } } +/// +/// Продукт (Defs/products.json): что растение даёт при сборе (древесина, трава) или плодоношении +/// (ягоды, жёлудь). Источник истины — деф; растения ссылаются на него по имени, а количество +/// определяют гены. — ключ локализации (product.*). +/// +public sealed class ProductDef : Def +{ + /// Категория продукта (например "material" или "food") — для будущей экономики/инвентаря. + public string Kind { get; init; } = ""; +} + /// /// Базовый геном вида (вложенный объект ): оптимумы и толерантности по /// факторам среды + базовая бодрость роста. Особь при спавне получает аллели рядом с этими базами @@ -114,6 +125,18 @@ public sealed class GenomeDef /// Доля разброса аллелей вокруг базы при генерации особи. public float Spread { get; init; } = 0.08f; + + /// Плодовитость: сколько плодов несёт зрелое растение в сезон (0 — не плодоносит). + public float FruitYield { get; init; } + + /// Сезон плодоношения (0 — весна, 1 — лето, 2 — осень, 3 — зима). + public float FruitSeason { get; init; } = 1f; + + /// Сколько продукта даёт сбор растения (масштаб ). + public float HarvestAmount { get; init; } = 1f; + + /// Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета. + public float LeafHue { get; init; } = 0.33f; } /// Растение (Defs/plants.json): текстура, размер, опциональный ствол-препятствие, стадии роста, геном. @@ -133,6 +156,12 @@ public sealed class PlantDef : Def /// Базовый геном вида: оптимумы/толерантности по среде и бодрость роста. public GenomeDef Genome { get; init; } = new(); + + /// Имя , выдаваемого при сборе растения (дерево/трава); null — несборное. + public string? HarvestProduct { get; init; } + + /// Имя , выдаваемого плодами (ягоды/жёлудь); null — не плодоносит. + public string? FruitProduct { get; init; } } /// diff --git a/src/LittleSim/Content/PlantSet.cs b/src/LittleSim/Content/PlantSet.cs index 1a57f8b..b92622e 100644 --- a/src/LittleSim/Content/PlantSet.cs +++ b/src/LittleSim/Content/PlantSet.cs @@ -111,6 +111,11 @@ public sealed class PlantSet Numeric("GeneReproduceInterval", g.ReproduceInterval), Numeric("GeneSelfPollination", g.SelfPollination), 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( Gene("GeneMorph"), 0f, diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index b4788c5..eba5384 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -162,6 +162,7 @@ public sealed class WorldScene : Scene ); UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, lighting, CellSize)); + UpdateSystems.Add(new PlantFruitingSystem(_plants, calendar, climate)); UpdateSystems.Add( new PlantLifecycleSystem( 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", (c, args) => RunGeneDemo(c, content, args) ); + console.Register( + "plant", + "plant [seed] — sample a species genome and show its gene-driven traits/products", + (c, args) => RunPlantDemo(c, content, args) + ); console.Register( "menu", "menu — return to the main menu", @@ -550,4 +556,62 @@ public sealed class WorldScene : Scene + $"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 [seed] e.g. plant TreeOakA"); + console.WriteLine($"species: {string.Join(", ", species)}"); + return; + } + + if (!content.Defs.TryGet(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(productDefName, out var product) + ? content.Languages.Get(product.Label) + : productDefName; } diff --git a/src/LittleSim/Sim/PlantFactory.cs b/src/LittleSim/Sim/PlantFactory.cs index d15fb17..412247a 100644 --- a/src/LittleSim/Sim/PlantFactory.cs +++ b/src/LittleSim/Sim/PlantFactory.cs @@ -17,6 +17,10 @@ public static class PlantFactory // Тинт рецессивной морфы (домножается на текстуру) — делает вариант визуально отличимым. 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); + /// Создаёт растение вида с заданным возрастом и геномом. public static Entity Create( EntityStore store, @@ -36,7 +40,7 @@ public static class PlantFactory var sprite = new Sprite(resolved.Region, GameLayers.Beings); sprite.CenterOrigin(); - sprite.Color = traits.IsVariant ? VariantTint : Color.White; + sprite.Color = Tint(traits); return store.CreateEntity( new Transform2D( @@ -51,7 +55,17 @@ public static class PlantFactory AgeDays = ageDays, 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; + } } diff --git a/src/LittleSim/Sim/PlantFruitingSystem.cs b/src/LittleSim/Sim/PlantFruitingSystem.cs new file mode 100644 index 0000000..8579d00 --- /dev/null +++ b/src/LittleSim/Sim/PlantFruitingSystem.cs @@ -0,0 +1,62 @@ +using Friflo.Engine.ECS.Systems; +using LittleSim.Content; +using MrGameEng.AI; +using MrGameEng.Core; + +namespace LittleSim.Sim; + +/// +/// Плодоношение (фаза G4): у зрелого растения с признаком > 0 +/// в его генетический сезон () копятся зрелые плоды до потолка +/// ; вне сезона плоды опадают. Полностью управляется генами через +/// признаки — система лишь применяет их к компоненту . Незрелые/бесплодные +/// растения остаются с нулём. +/// +public sealed class PlantFruitingSystem(PlantSet plants, Calendar calendar, Climate climate) + : QuerySystem +{ + // За сколько игровых дней набирается/опадает полный урожай. + 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)); + } + } + } + } +} diff --git a/src/LittleSim/Sim/PlantOrganism.cs b/src/LittleSim/Sim/PlantOrganism.cs index 69c26e5..8376415 100644 --- a/src/LittleSim/Sim/PlantOrganism.cs +++ b/src/LittleSim/Sim/PlantOrganism.cs @@ -24,6 +24,12 @@ public struct PlantPhenotype public float SelfPollination; public float MutationRate; + // --- Контент (G4): плодоношение, добыча, цвет --- + public float FruitYield; + public int FruitSeason; + public float HarvestAmount; + public float LeafHue; + /// Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта. public bool IsVariant; @@ -45,6 +51,10 @@ public struct PlantPhenotype ReproduceInterval = T("reproduceInterval"), SelfPollination = T("selfPollination"), 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, }; } @@ -64,3 +74,14 @@ public struct PlantOrganism : IComponent /// Выраженные признаки — то, что читают системы роста/жизненного цикла. public PlantPhenotype Traits; } + +/// +/// Плодоношение растения: сколько зрелых плодов сейчас на нём. Копится у зрелого растения в его +/// сезон плодоношения (признак ) до потолка +/// , вне сезона опадает. Управляется PlantFruitingSystem. +/// +public struct Fruiting : IComponent +{ + /// Накоплено зрелых плодов (0..FruitYield). + public float RipeFruit; +}