From a04e867e873bed6c98b77f0e726a0f5e51fb0801 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Fri, 12 Jun 2026 18:21:38 +0300 Subject: [PATCH 1/8] Phase C: plant reproduction (Mendel), death and persistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plants now live full lives. The genome gains lifespan, dispersal, fecundity, self-pollination, mutation-rate and a discrete dominant/recessive Morph gene; PlantGenome.Breed models meiosis (one allele per parent) plus mutation. A gated PlantLifecycleSystem builds a per-cell grid (density + a species/genome representative), reaps plants past their lifespan, and lets mature plants seed offspring — cross-pollinating with a mature same-species neighbour in dispersal range (falling back to self-pollination by gene) into a nearby land cell under the density cap. A shared PlantFactory creates every plant (initial scatter, births, save restore) and tints recessive-morph plants. Growth keeps aging mature plants so they can die of old age. The whole population (genomes, positions, age, stage) is saved to WorldSave and restored on load instead of re-scattering from the seed. plants.json carries the new genes. Co-Authored-By: Claude Opus 4.8 --- Mods/Core/Defs/plants.json | 12 +- src/LittleSim/App/WorldSave.cs | 30 +++ src/LittleSim/Content/GameDefs.cs | 18 ++ src/LittleSim/Scenes/ContentHelpers.cs | 30 +-- src/LittleSim/Scenes/WorldScene.cs | 129 ++++++++-- src/LittleSim/Sim/PlantFactory.cs | 55 ++++ src/LittleSim/Sim/PlantGenome.cs | 117 +++++++-- src/LittleSim/Sim/PlantGrowthSystem.cs | 3 +- src/LittleSim/Sim/PlantLifecycleSystem.cs | 292 ++++++++++++++++++++++ 9 files changed, 621 insertions(+), 65 deletions(-) create mode 100644 src/LittleSim/Sim/PlantFactory.cs create mode 100644 src/LittleSim/Sim/PlantLifecycleSystem.cs diff --git a/Mods/Core/Defs/plants.json b/Mods/Core/Defs/plants.json index 1a83da0..7014534 100644 --- a/Mods/Core/Defs/plants.json +++ b/Mods/Core/Defs/plants.json @@ -3,7 +3,9 @@ "defs": [ { "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28, "genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 16, - "optimalFertility": 1.4, "fertilityTolerance": 0.9, "vigor": 1.0, "spread": 0.08 }, + "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 }, "stages": [ { "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" }, { "sizeCells": 1.2, "growDays": 9, "label": "plant.stage.sapling" }, @@ -16,7 +18,9 @@ { "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.2, "genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 20, "temperatureTolerance": 12, - "optimalFertility": 1.0, "fertilityTolerance": 1.0, "vigor": 1.3, "spread": 0.1 }, + "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 }, "stages": [ { "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 2, "label": "plant.stage.sprout" }, { "sizeCells": 1.2, "label": "plant.stage.mature" } @@ -24,7 +28,9 @@ { "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4, "genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 13, - "optimalFertility": 1.1, "fertilityTolerance": 1.0, "vigor": 1.0, "spread": 0.1 }, + "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 }, "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/src/LittleSim/App/WorldSave.cs b/src/LittleSim/App/WorldSave.cs index 2b4fac9..6fe6158 100644 --- a/src/LittleSim/App/WorldSave.cs +++ b/src/LittleSim/App/WorldSave.cs @@ -1,7 +1,33 @@ using System.Text.Json; +using LittleSim.Sim; namespace LittleSim.App; +/// Сериализуемое состояние одного растения: вид (деф), позиция, возраст, стадия, почва и геном. +public sealed class PlantSave +{ + /// Имя дефа вида — по нему при загрузке берётся индекс/стадии/текстуры. + public string Species { get; set; } = ""; + + /// Позиция X в мировых координатах. + public float X { get; set; } + + /// Позиция Y в мировых координатах. + public float Y { get; set; } + + /// Накопленный возраст в игровых днях. + public float AgeDays { get; set; } + + /// Текущая стадия роста. + public int Stage { get; set; } + + /// Плодородность клетки. + public float CellFertility { get; set; } + + /// Геном особи (аллели сериализуются как поля структуры). + public PlantGenome Genome { get; set; } +} + /// Сериализуемое состояние одного жителя (sim-компоненты; спрайт пересобирается из дефа по ). public sealed class PawnSave { @@ -70,6 +96,9 @@ public sealed class WorldSave /// Снимок жителей. public List Pawns { get; set; } = []; + /// Снимок всех растений (геномы, позиции, возраст, стадия). + public List Plants { get; set; } = []; + /// Конфиг мира для пересоздания сцены. public WorldConfig ToConfig() => new() @@ -90,6 +119,7 @@ public sealed class SaveStore { WriteIndented = true, PropertyNameCaseInsensitive = true, + IncludeFields = true, // аллели генома (PlantGenome) — публичные поля структуры }; private readonly string _directory; diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs index a33e1d7..41c8de0 100644 --- a/src/LittleSim/Content/GameDefs.cs +++ b/src/LittleSim/Content/GameDefs.cs @@ -91,6 +91,24 @@ public sealed class GenomeDef /// Базовая бодрость роста (множитель скорости в идеальных условиях). public float Vigor { get; init; } = 1f; + /// Продолжительность жизни (игровых дней) до смерти от старости. + public float Lifespan { get; init; } = 80f; + + /// Дальность расселения семян (в клетках). + public float DispersalRange { get; init; } = 2f; + + /// Средний интервал между попытками дать семя (игровых дней). + public float ReproduceInterval { get; init; } = 8f; + + /// Способность к самоопылению (0..1): шанс дать семя без партнёра. + public float SelfPollination { get; init; } = 0.4f; + + /// Темп мутаций (0..1): вероятность сдвига аллеля у потомка. + public float MutationRate { get; init; } = 0.05f; + + /// Доля рецессивного аллеля морфы в стартовой популяции (0..1). + public float VariantChance { get; init; } = 0.15f; + /// Доля разброса аллелей вокруг базы при генерации особи. public float Spread { get; init; } = 0.08f; } diff --git a/src/LittleSim/Scenes/ContentHelpers.cs b/src/LittleSim/Scenes/ContentHelpers.cs index 3f43486..1003ffa 100644 --- a/src/LittleSim/Scenes/ContentHelpers.cs +++ b/src/LittleSim/Scenes/ContentHelpers.cs @@ -36,29 +36,19 @@ internal static class ScatterSpawner var def = content.Defs.Get(entry.Options[random.Next(entry.Options.Count)]); var index = plants.IndexOf(def); - var species = plants[index]; var px = (cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize; var py = (cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize; - var age = random.NextSingle() * species.MaturityDays; - var stage = species.StageAt(age); - var resolved = species.Stages[stage]; + var age = random.NextSingle() * plants[index].MaturityDays; - var sprite = new Sprite(resolved.Region, GameLayers.Beings); - sprite.CenterOrigin(); - scene.Store.CreateEntity( - new Transform2D( - new Vector2(px, py), - scale: new Vector2(cellSize * resolved.SizeCells / resolved.Region.Width) - ), - sprite, - new PlantGrowth - { - Species = index, - Stage = stage, - AgeDays = age, - CellFertility = terrain.Fertility, - }, - PlantGenome.FromDef(def.Genome, random) + PlantFactory.Create( + scene.Store, + plants, + index, + new Vector2(px, py), + age, + PlantGenome.FromDef(def.Genome, random), + terrain.Fertility, + cellSize ); } } diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 1afd90f..9727d26 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -25,8 +25,9 @@ namespace LittleSim.Scenes; /// Мир LittleSim: тайловый рельеф целиком описан дефами Core-мода и строится из /// (размер/сид/масштаб деталей) процедурной генерацией движка /// (). Каждый тип клетки рисуется текстурой-поверхностью из атласа -/// (вода — тонированным тайлом). Поверх мира — HUD, полоса скорости (пауза/x1/x3/x6 + горячие -/// клавиши), меню-пауза (Esc) и дев-консоль. Жителей/растений пока нет — только террейн. +/// (вода — тонированным тайлом). Поверх растёт растительность с геномом (рост по свету/температуре/ +/// почве, размножение по Менделю и смерть), климат и день/ночь. UI: HUD, полоса скорости +/// (пауза/x1/x3/x6 + горячие клавиши), меню-пауза (Esc), дев-консоль и ECS-инспектор (F1). /// public sealed class WorldScene : Scene { @@ -38,6 +39,9 @@ public sealed class WorldScene : Scene /// public const float SecondsPerDay = 480f; + /// Максимум растений на клетку — потолок плотности для размножения. + public const int DensityCap = 4; + private readonly WorldConfig _config; private readonly WorldSave? _save; private readonly RectF _bounds; @@ -46,6 +50,10 @@ public sealed class WorldScene : Scene private PauseMenu _pause = null!; private readonly List _speedRefreshers = []; + private PlantSet _plants = null!; + private float[] _cellFertility = []; + private bool[] _cellLand = []; + /// Новый мир из конфига. public WorldScene(WorldConfig config) : this(config, null) { } @@ -78,10 +86,20 @@ public sealed class WorldScene : Scene var climate = Context.UseClimate(ClimateSettings.Default); var dayNight = this.UseDayNight(renderer); // мир темнеет ночью — амбиент идёт в рендер - // Рельеф и расстановка растений детерминированы сидом мира (независимые потоки seed). - var plants = new PlantSet(content, atlases, device); - var random = new Random(_config.Seed); - BuildTerrain(content, atlases, device, assets, plants, random); + // Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва. + _plants = new PlantSet(content, atlases, device); + var loadingPlants = _save?.Plants is { Count: > 0 }; + BuildTerrain( + content, + atlases, + device, + assets, + loadingPlants ? null : new Random(_config.Seed) + ); + if (loadingPlants) + { + RestorePlants(content); + } var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds)); @@ -103,7 +121,22 @@ public sealed class WorldScene : Scene var console = this.UseDevConsole(); RegisterCommands(console, content, atlases); - UpdateSystems.Add(new PlantGrowthSystem(plants, calendar, climate, dayNight, CellSize)); + UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, dayNight, CellSize)); + UpdateSystems.Add( + new PlantLifecycleSystem( + Store, + _plants, + calendar, + Context.Clock, + _config.Width, + _config.Height, + CellSize, + _cellFertility, + _cellLand, + DensityCap, + _config.Seed + ) + ); UpdateSystems.Add( new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen) ); @@ -127,17 +160,17 @@ public sealed class WorldScene : Scene } /// - /// Строит одну сущность-: тайлсет из дефов рельефа (поверхность из - /// атласа либо тонированный тайл) и сетка тайлов по карте высот процедурной генерации, а - /// поверх — растительность по скаттеру дефов (с компонентом роста, разной зрелости). + /// Строит сущность- (тайлсет из дефов рельефа + сетка по карте высот) и + /// заполняет по-клеточные массивы плодородности/суши для жизненного цикла. Если + /// задан (новый мир) — рассыпает растительность по дефам; + /// при загрузке передаётся null (растения восстанавливаются из сейва отдельно). /// private void BuildTerrain( GameContent content, ModAtlases atlases, Microsoft.Xna.Framework.Graphics.GraphicsDevice device, AssetManager assets, - PlantSet plants, - Random random + Random? scatterRandom ) { var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White)); @@ -160,21 +193,29 @@ public sealed class WorldScene : Scene _config.SmoothPasses ); var grid = new TileGrid(_config.Width, _config.Height); + _cellFertility = new float[_config.Width * _config.Height]; + _cellLand = new bool[_config.Width * _config.Height]; for (var x = 0; x < _config.Width; x++) { for (var y = 0; y < _config.Height; y++) { var terrain = content.Terrains.Classify(heights[x, y]); grid[x, y] = tileByDef[terrain]; - ScatterSpawner.Spawn( - this, - content, - plants, - terrain, - new Point(x, y), - CellSize, - random - ); + var cell = y * _config.Width + x; + _cellFertility[cell] = terrain.Fertility; + _cellLand[cell] = terrain.IsLand; + if (scatterRandom is not null) + { + ScatterSpawner.Spawn( + this, + content, + _plants, + terrain, + new Point(x, y), + CellSize, + scatterRandom + ); + } } } @@ -254,7 +295,6 @@ public sealed class WorldScene : Scene private string SaveWorld() { - // Жителей пока нет — сохраняем только конфиг мира; рельеф воспроизводится из сида. var save = new WorldSave { Name = _config.Name, @@ -267,11 +307,54 @@ public sealed class WorldScene : Scene ElapsedSeconds = Context.Clock.TotalTime, }; + // Снимок всей популяции растений: вид (деф), позиция, возраст, стадия, почва и геном. + Store + .Query() + .ForEachEntity( + (ref PlantGrowth grow, ref PlantGenome gene, ref Transform2D transform, Entity _) => + { + save.Plants.Add( + new PlantSave + { + Species = _plants[grow.Species].Def.DefName, + X = transform.Position.X, + Y = transform.Position.Y, + AgeDays = grow.AgeDays, + Stage = grow.Stage, + CellFertility = grow.CellFertility, + Genome = gene, + } + ); + } + ); + new SaveStore().Write(save); - Log.Info($"World '{_config.Name}' saved"); + Log.Info($"World '{_config.Name}' saved ({save.Plants.Count} plants)"); return _config.Name; } + private void RestorePlants(GameContent content) + { + foreach (var plant in _save!.Plants) + { + if (!content.Defs.TryGet(plant.Species, out var def)) + { + continue; // вид пропал (мод убрали) — пропускаем растение + } + + PlantFactory.Create( + Store, + _plants, + _plants.IndexOf(def), + new Vector2(plant.X, plant.Y), + plant.AgeDays, + plant.Genome, + plant.CellFertility, + CellSize + ); + } + } + private void Switch(Scene scene) { if (!Context.Scenes.IsTransitioning) diff --git a/src/LittleSim/Sim/PlantFactory.cs b/src/LittleSim/Sim/PlantFactory.cs new file mode 100644 index 0000000..ebeb22a --- /dev/null +++ b/src/LittleSim/Sim/PlantFactory.cs @@ -0,0 +1,55 @@ +using Friflo.Engine.ECS; +using LittleSim.Content; +using LittleSim.Scenes; +using Microsoft.Xna.Framework; +using MrGameEng.Graphics; + +namespace LittleSim.Sim; + +/// +/// Единая точка создания сущности-растения: спрайт стадии по возрасту, тинт по морфе генома и +/// компоненты роста/генома. Переиспользуется начальным скаттером, размножением и загрузкой сейва — +/// чтобы все растения собирались одинаково. +/// +public static class PlantFactory +{ + // Тинт рецессивной морфы (домножается на текстуру) — делает вариант визуально отличимым. + private static readonly Color VariantTint = new(214, 150, 176); + + /// Создаёт растение вида с заданным возрастом и геномом. + public static Entity Create( + EntityStore store, + PlantSet plants, + int species, + Vector2 position, + float ageDays, + in PlantGenome genome, + float cellFertility, + int cellSize + ) + { + var sp = plants[species]; + var stage = sp.StageAt(ageDays); + var resolved = sp.Stages[stage]; + + var sprite = new Sprite(resolved.Region, GameLayers.Beings); + sprite.CenterOrigin(); + sprite.Color = genome.IsVariant ? VariantTint : Color.White; + + return store.CreateEntity( + new Transform2D( + position, + scale: new Vector2(cellSize * resolved.SizeCells / resolved.Region.Width) + ), + sprite, + new PlantGrowth + { + Species = species, + Stage = stage, + AgeDays = ageDays, + CellFertility = cellFertility, + }, + genome + ); + } +} diff --git a/src/LittleSim/Sim/PlantGenome.cs b/src/LittleSim/Sim/PlantGenome.cs index a7971db..b697088 100644 --- a/src/LittleSim/Sim/PlantGenome.cs +++ b/src/LittleSim/Sim/PlantGenome.cs @@ -4,39 +4,42 @@ using LittleSim.Content; namespace LittleSim.Sim; /// -/// Диплоидный геном растения: по паре аллелей на ген. Для числовых генов экспрессия гибридная — -/// фенотип есть среднее двух аллелей (дискретные доминант/рецессивные гены добавятся с размножением). -/// Аллели — наследуемые данные (правятся в инспекторе); фенотип-свойства читает система роста. +/// Диплоидный геном растения: по паре аллелей на ген. Числовые гены экспрессируются гибридно — +/// фенотип есть среднее двух аллелей; дискретный ген морфы — по Менделю (рецессив виден только в +/// гомозиготе). Аллели наследуются (правятся в инспекторе); фенотип-свойства читают системы роста и +/// жизненного цикла. моделирует мейоз и мутацию. /// public struct PlantGenome : IComponent { - /// Аллели оптимума освещённости (0..1). + // --- Числовые гены (аллель A/B), фенотип = среднее ------------------------------------------ public float OptimalLightA; public float OptimalLightB; - - /// Аллели толерантности по свету. public float LightToleranceA; public float LightToleranceB; - - /// Аллели оптимума температуры (°C). public float OptimalTemperatureA; public float OptimalTemperatureB; - - /// Аллели толерантности по температуре (°C). public float TemperatureToleranceA; public float TemperatureToleranceB; - - /// Аллели оптимума плодородности. public float OptimalFertilityA; public float OptimalFertilityB; - - /// Аллели толерантности по плодородности. public float FertilityToleranceA; public float FertilityToleranceB; - - /// Аллели бодрости роста. public float VigorA; public float VigorB; + public float LifespanA; + public float LifespanB; + public float DispersalRangeA; + public float DispersalRangeB; + public float ReproduceIntervalA; + public float ReproduceIntervalB; + public float SelfPollinationA; + public float SelfPollinationB; + public float MutationRateA; + public float MutationRateB; + + // --- Дискретный ген морфы (0 — доминантный, 1 — рецессивный) -------------------------------- + public int MorphA; + public int MorphB; /// Фенотип: оптимальная освещённость. public readonly float OptimalLight => (OptimalLightA + OptimalLightB) * 0.5f; @@ -60,15 +63,36 @@ public struct PlantGenome : IComponent /// Фенотип: бодрость роста. public readonly float Vigor => (VigorA + VigorB) * 0.5f; + /// Фенотип: продолжительность жизни (дней). + public readonly float Lifespan => (LifespanA + LifespanB) * 0.5f; + + /// Фенотип: дальность расселения (клеток). + public readonly float DispersalRange => (DispersalRangeA + DispersalRangeB) * 0.5f; + + /// Фенотип: интервал размножения (дней). + public readonly float ReproduceInterval => (ReproduceIntervalA + ReproduceIntervalB) * 0.5f; + + /// Фенотип: способность к самоопылению (0..1). + public readonly float SelfPollination => (SelfPollinationA + SelfPollinationB) * 0.5f; + + /// Фенотип: темп мутаций (0..1). + public readonly float MutationRate => (MutationRateA + MutationRateB) * 0.5f; + + /// Фенотип морфы: рецессивный вариант проявляется только в гомозиготе (1,1). + public readonly bool IsVariant => MorphA == 1 && MorphB == 1; + /// - /// Генерирует особь из базового генома вида: каждый аллель — база ± относительный разброс - /// (). Сидируется переданным — детерминизм. + /// Генерирует особь из базового генома вида: числовые аллели — база ± относительный разброс + /// (); аллель морфы = рецессивный (1) с вероятностью + /// . Сидируется — детерминизм. /// public static PlantGenome FromDef(GenomeDef def, Random random) { float Allele(float baseValue) => baseValue + (random.NextSingle() * 2f - 1f) * def.Spread * baseValue; + int MorphAllele() => random.NextSingle() < def.VariantChance ? 1 : 0; + return new PlantGenome { OptimalLightA = Allele(def.OptimalLight), @@ -85,6 +109,63 @@ public struct PlantGenome : IComponent FertilityToleranceB = Allele(def.FertilityTolerance), VigorA = Allele(def.Vigor), VigorB = Allele(def.Vigor), + LifespanA = Allele(def.Lifespan), + LifespanB = Allele(def.Lifespan), + DispersalRangeA = Allele(def.DispersalRange), + DispersalRangeB = Allele(def.DispersalRange), + ReproduceIntervalA = Allele(def.ReproduceInterval), + ReproduceIntervalB = Allele(def.ReproduceInterval), + SelfPollinationA = Allele(def.SelfPollination), + SelfPollinationB = Allele(def.SelfPollination), + MutationRateA = Allele(def.MutationRate), + MutationRateB = Allele(def.MutationRate), + MorphA = MorphAllele(), + MorphB = MorphAllele(), + }; + } + + /// + /// Потомок двух родителей (мейоз): аллель A — случайный из родителя , аллель + /// B — из ; затем мутация (с вероятностью среднего темпа мутаций родителей — + /// сдвиг числового аллеля на малую долю / переключение морфы). Самоопыление = Breed(self, self). + /// + public static PlantGenome Breed(in PlantGenome a, in PlantGenome b, Random rng) + { + var rate = (a.MutationRate + b.MutationRate) * 0.5f; + + float Mut(float v) => + rng.NextSingle() < rate ? v + (rng.NextSingle() * 2f - 1f) * 0.12f * v : v; + float FromA(float x, float y) => Mut(rng.NextSingle() < 0.5f ? x : y); + int MutMorph(int m) => rng.NextSingle() < rate ? 1 - m : m; + + return new PlantGenome + { + OptimalLightA = FromA(a.OptimalLightA, a.OptimalLightB), + OptimalLightB = FromA(b.OptimalLightA, b.OptimalLightB), + LightToleranceA = FromA(a.LightToleranceA, a.LightToleranceB), + LightToleranceB = FromA(b.LightToleranceA, b.LightToleranceB), + OptimalTemperatureA = FromA(a.OptimalTemperatureA, a.OptimalTemperatureB), + OptimalTemperatureB = FromA(b.OptimalTemperatureA, b.OptimalTemperatureB), + TemperatureToleranceA = FromA(a.TemperatureToleranceA, a.TemperatureToleranceB), + TemperatureToleranceB = FromA(b.TemperatureToleranceA, b.TemperatureToleranceB), + OptimalFertilityA = FromA(a.OptimalFertilityA, a.OptimalFertilityB), + OptimalFertilityB = FromA(b.OptimalFertilityA, b.OptimalFertilityB), + FertilityToleranceA = FromA(a.FertilityToleranceA, a.FertilityToleranceB), + FertilityToleranceB = FromA(b.FertilityToleranceA, b.FertilityToleranceB), + VigorA = FromA(a.VigorA, a.VigorB), + VigorB = FromA(b.VigorA, b.VigorB), + LifespanA = FromA(a.LifespanA, a.LifespanB), + LifespanB = FromA(b.LifespanA, b.LifespanB), + DispersalRangeA = FromA(a.DispersalRangeA, a.DispersalRangeB), + DispersalRangeB = FromA(b.DispersalRangeA, b.DispersalRangeB), + ReproduceIntervalA = FromA(a.ReproduceIntervalA, a.ReproduceIntervalB), + ReproduceIntervalB = FromA(b.ReproduceIntervalA, b.ReproduceIntervalB), + SelfPollinationA = FromA(a.SelfPollinationA, a.SelfPollinationB), + SelfPollinationB = FromA(b.SelfPollinationA, b.SelfPollinationB), + MutationRateA = FromA(a.MutationRateA, a.MutationRateB), + MutationRateB = FromA(b.MutationRateA, b.MutationRateB), + MorphA = MutMorph(rng.NextSingle() < 0.5f ? a.MorphA : a.MorphB), + MorphB = MutMorph(rng.NextSingle() < 0.5f ? b.MorphA : b.MorphB), }; } } diff --git a/src/LittleSim/Sim/PlantGrowthSystem.cs b/src/LittleSim/Sim/PlantGrowthSystem.cs index aafdc72..54e8fb9 100644 --- a/src/LittleSim/Sim/PlantGrowthSystem.cs +++ b/src/LittleSim/Sim/PlantGrowthSystem.cs @@ -47,7 +47,8 @@ public sealed class PlantGrowthSystem( var stages = plants[grow.Species].Stages; if (grow.Stage >= stages.Length - 1) { - continue; // уже зрелое — рост окончен + grow.AgeDays += deltaDays; // зрелое: старение в реальном времени → смерть по старости + continue; } ref var gene = ref dna[i]; diff --git a/src/LittleSim/Sim/PlantLifecycleSystem.cs b/src/LittleSim/Sim/PlantLifecycleSystem.cs new file mode 100644 index 0000000..7d49d00 --- /dev/null +++ b/src/LittleSim/Sim/PlantLifecycleSystem.cs @@ -0,0 +1,292 @@ +using Friflo.Engine.ECS; +using Friflo.Engine.ECS.Systems; +using LittleSim.Content; +using Microsoft.Xna.Framework; +using MrGameEng.Core; +using MrGameEng.Graphics; + +namespace LittleSim.Sim; + +/// +/// Жизненный цикл растений: размножение (наследование по Менделю) и смерть от старости. Запускается +/// не каждый кадр, а раз в игрового дня. За тик: строит по-клеточную сетку +/// (счётчик плотности + «представитель» вида/генома), собирает умерших по возрасту, затем зрелые с +/// вероятностью elapsed/ReproduceInterval сеют потомка — приоритет перекрёстного опыления +/// (зрелый сосед того же вида в радиусе расселения), иначе самоопыление по гену. Семя падает в +/// случайную клетку радиуса, если это суша и не превышен лимит плотности. Структурные изменения +/// (создание/удаление сущностей) применяются после проходов по запросу — это безопасно. +/// +public sealed class PlantLifecycleSystem : BaseSystem +{ + private const float IntervalDays = 0.25f; + + private readonly EntityStore _store; + private readonly PlantSet _plants; + private readonly Calendar _calendar; + private readonly GameClock _clock; + private readonly int _width; + private readonly int _height; + private readonly int _cellSize; + private readonly float[] _cellFertility; + private readonly bool[] _cellLand; + private readonly int _densityCap; + private readonly Random _rng; + private readonly ArchetypeQuery _query; + + private readonly int[] _count; + private readonly CellRep[] _rep; + private readonly List _deaths = []; + private readonly List _candidates = []; + private readonly List _births = []; + private float _accumulator; + + public PlantLifecycleSystem( + EntityStore store, + PlantSet plants, + Calendar calendar, + GameClock clock, + int width, + int height, + int cellSize, + float[] cellFertility, + bool[] cellLand, + int densityCap, + int seed + ) + { + _store = store; + _plants = plants; + _calendar = calendar; + _clock = clock; + _width = width; + _height = height; + _cellSize = cellSize; + _cellFertility = cellFertility; + _cellLand = cellLand; + _densityCap = densityCap; + _rng = new Random(seed); + _query = store.Query(); + _count = new int[width * height]; + _rep = new CellRep[width * height]; + } + + protected override void OnUpdateGroup() + { + _accumulator += _clock.DeltaTime / _calendar.SecondsPerDay; // прошло игровых дней (с учётом паузы/скорости) + if (_accumulator < IntervalDays) + { + return; + } + + var elapsed = _accumulator; + _accumulator = 0f; + + BuildGridAndCollect(); + Reproduce(elapsed); + Apply(); + } + + // Проход 1: сетка плотности + представители, сбор умерших и зрелых-кандидатов. + private void BuildGridAndCollect() + { + Array.Clear(_count); + for (var c = 0; c < _rep.Length; c++) + { + _rep[c].Species = -1; + } + + _deaths.Clear(); + _candidates.Clear(); + _births.Clear(); + + foreach (var (growths, genomes, transforms, entities) in _query.Chunks) + { + var g = growths.Span; + var dna = genomes.Span; + var t = transforms.Span; + for (var i = 0; i < g.Length; i++) + { + ref var grow = ref g[i]; + ref var gene = ref dna[i]; + if (grow.AgeDays > gene.Lifespan) + { + _deaths.Add(entities.EntityAt(i)); // умер от старости + continue; + } + + 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); + var cell = cy * _width + cx; + _count[cell]++; + + var mature = grow.Stage >= _plants[grow.Species].Stages.Length - 1; + _rep[cell] = new CellRep + { + Species = grow.Species, + Mature = mature, + Genome = gene, + }; + if (mature) + { + _candidates.Add( + new Candidate + { + Cell = cell, + Species = grow.Species, + Genome = gene, + } + ); + } + } + } + } + + // Проход 2: зрелые сеют потомка (перекрёстно или само-) в подходящую клетку радиуса. + private void Reproduce(float elapsed) + { + foreach (var cand in _candidates) + { + var interval = MathF.Max(0.5f, cand.Genome.ReproduceInterval); + if (_rng.NextSingle() >= elapsed / interval) + { + continue; // в этот тик не сеет + } + + var range = Math.Clamp((int)MathF.Round(cand.Genome.DispersalRange), 1, 6); + var cx = cand.Cell % _width; + var cy = cand.Cell / _width; + + if (!TryFindPartner(cx, cy, range, cand.Species, out var partner)) + { + if (_rng.NextSingle() >= cand.Genome.SelfPollination) + { + continue; // нет партнёра и самоопыление не удалось + } + + partner = cand.Genome; // самоопыление + } + + if (!TryPickTarget(cx, cy, range, out var tcell)) + { + continue; // некуда сеять (нет суши/свободного места рядом) + } + + var child = PlantGenome.Breed(cand.Genome, partner, _rng); + var tx = tcell % _width; + var ty = tcell / _width; + var position = new Vector2( + (tx + 0.3f + _rng.NextSingle() * 0.4f) * _cellSize, + (ty + 0.3f + _rng.NextSingle() * 0.4f) * _cellSize + ); + _births.Add( + new Birth + { + Species = cand.Species, + Position = position, + Genome = child, + Fertility = _cellFertility[tcell], + } + ); + _count[tcell]++; // резервируем место, чтобы не переполнить клетку за тик + } + } + + private bool TryFindPartner(int cx, int cy, int range, int species, out PlantGenome partner) + { + for (var dy = -range; dy <= range; dy++) + { + for (var dx = -range; dx <= range; dx++) + { + if (dx == 0 && dy == 0) + { + continue; // партнёр — другая клетка (приоритет перекрёстного) + } + + var nx = cx + dx; + var ny = cy + dy; + if (nx < 0 || nx >= _width || ny < 0 || ny >= _height) + { + continue; + } + + ref var rep = ref _rep[ny * _width + nx]; + if (rep.Species == species && rep.Mature) + { + partner = rep.Genome; + return true; + } + } + } + + partner = default; + return false; + } + + private bool TryPickTarget(int cx, int cy, int range, out int cell) + { + for (var attempt = 0; attempt < 6; attempt++) + { + var tx = cx + _rng.Next(-range, range + 1); + var ty = cy + _rng.Next(-range, range + 1); + if (tx < 0 || tx >= _width || ty < 0 || ty >= _height) + { + continue; + } + + var candidate = ty * _width + tx; + if (_cellLand[candidate] && _count[candidate] < _densityCap) + { + cell = candidate; + return true; + } + } + + cell = -1; + return false; + } + + private void Apply() + { + foreach (var entity in _deaths) + { + entity.DeleteEntity(); + } + + foreach (var birth in _births) + { + PlantFactory.Create( + _store, + _plants, + birth.Species, + birth.Position, + ageDays: 0f, + birth.Genome, + birth.Fertility, + _cellSize + ); + } + } + + private struct CellRep + { + public int Species; + public bool Mature; + public PlantGenome Genome; + } + + private struct Candidate + { + public int Cell; + public int Species; + public PlantGenome Genome; + } + + private struct Birth + { + public int Species; + public Vector2 Position; + public PlantGenome Genome; + public float Fertility; + } +} From 49936a1de35df4c8b60714f687ae68766671bfdf Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Fri, 12 Jun 2026 18:40:31 +0300 Subject: [PATCH 2/8] Phase D: spatial lighting with shadows; local light drives growth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump the engine submodule to 79d406a (2D lightmap). The world scene now uses UseLighting instead of the per-sprite ambient: a lightmap multiplies day/night × occlusion over the world, so forests cast soft shade and the world darkens spatially toward night. Occluders are mountains (new TerrainDef.BlocksLight) plus mature trees, rebuilt from the live population. PlantGrowthSystem samples the local light per plant (Lighting.SampleAt), so undergrowth under canopy grows slower. A dev-console 'light' command places a point light at the cursor to show cast shadows at night. Completes the plant ecosystem (A–D). Co-Authored-By: Claude Opus 4.8 --- Mods/Core/Defs/terrain.json | 2 +- engine | 2 +- src/LittleSim/Content/GameDefs.cs | 3 + src/LittleSim/Scenes/WorldScene.cs | 83 ++++++++++++++++++++++++-- src/LittleSim/Sim/PlantGrowthSystem.cs | 6 +- 5 files changed, 87 insertions(+), 9 deletions(-) diff --git a/Mods/Core/Defs/terrain.json b/Mods/Core/Defs/terrain.json index f2caabf..a53f681 100644 --- a/Mods/Core/Defs/terrain.json +++ b/Mods/Core/Defs/terrain.json @@ -17,6 +17,6 @@ { "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA"] } ] }, { "defName": "Mountain", "label": "terrain.mountain", "maxHeight": 1.01, "color": [136, 132, 128], - "surface": "terrain/surfaces/roughhewnrock", "fertility": 0.2 } + "surface": "terrain/surfaces/roughhewnrock", "fertility": 0.2, "blocksLight": true } ] } diff --git a/engine b/engine index d0104df..79d406a 160000 --- a/engine +++ b/engine @@ -1 +1 @@ -Subproject commit d0104df3044961f74bda68d3e6cc97dd0d34b326 +Subproject commit 79d406a9f4687fdb47a059a6b1e0593eb33e3061 diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs index 41c8de0..443f94f 100644 --- a/src/LittleSim/Content/GameDefs.cs +++ b/src/LittleSim/Content/GameDefs.cs @@ -27,6 +27,9 @@ public sealed class TerrainDef : Def /// Суша: здесь появляются жители, животные и растения. public bool IsLand { get; init; } + /// Загораживает свет (горы) — базовый окклюдер для лайтмапа/теней. + public bool BlocksLight { get; init; } + /// Ключ текстуры поверхности для тайловой сцены; null — тонированный тайл. public string? Surface { get; init; } diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 9727d26..31dcfaf 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -53,6 +53,8 @@ public sealed class WorldScene : Scene private PlantSet _plants = null!; private float[] _cellFertility = []; private bool[] _cellLand = []; + private bool[] _cellOccluderBase = []; // горы (статично из террейна) + private bool[] _cellOccluder = []; // горы + зрелые деревья (пересобирается лайтмапом) /// Новый мир из конфига. public WorldScene(WorldConfig config) @@ -84,7 +86,7 @@ public sealed class WorldScene : Scene var calendar = Context.UseCalendar(SecondsPerDay); var climate = Context.UseClimate(ClimateSettings.Default); - var dayNight = this.UseDayNight(renderer); // мир темнеет ночью — амбиент идёт в рендер + var dayNight = new DayNight(calendar, DayNightSettings.Default); // Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва. _plants = new PlantSet(content, atlases, device); @@ -101,6 +103,18 @@ public sealed class WorldScene : Scene RestorePlants(content); } + // Освещение: лайтмап (день/ночь × окклюзия от гор/крон + точечные) множится поверх мира, + // а Lighting.SampleAt даёт локальный свет системе роста (подлесок под кронами растёт хуже). + var lighting = this.UseLighting( + renderer, + dayNight, + _config.Width, + _config.Height, + CellSize, + Vector2.Zero, + BuildOccluders + ); + var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds)); // HUD, полоса скорости и меню-пауза в одной корневой панели. @@ -120,8 +134,31 @@ public sealed class WorldScene : Scene this.UseInspector(renderer); var console = this.UseDevConsole(); RegisterCommands(console, content, atlases); + console.Register( + "light", + "light [radius] — place a point light at the cursor (night shadows demo)", + (c, args) => + { + var radius = + args.Length > 0 + ? float.Parse(args[0], System.Globalization.CultureInfo.InvariantCulture) + : 120f; + var mouse = Mouse.GetState(); + var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y)); + Store.CreateEntity( + Transform2D.At(world), + new PointLight + { + Radius = radius, + Color = Color.White, + Intensity = 0.9f, + } + ); + c.WriteLine($"light at {world.X:0},{world.Y:0} r{radius:0}"); + } + ); - UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, dayNight, CellSize)); + UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, lighting, CellSize)); UpdateSystems.Add( new PlantLifecycleSystem( Store, @@ -193,8 +230,11 @@ public sealed class WorldScene : Scene _config.SmoothPasses ); var grid = new TileGrid(_config.Width, _config.Height); - _cellFertility = new float[_config.Width * _config.Height]; - _cellLand = new bool[_config.Width * _config.Height]; + var cells = _config.Width * _config.Height; + _cellFertility = new float[cells]; + _cellLand = new bool[cells]; + _cellOccluderBase = new bool[cells]; + _cellOccluder = new bool[cells]; for (var x = 0; x < _config.Width; x++) { for (var y = 0; y < _config.Height; y++) @@ -204,6 +244,7 @@ public sealed class WorldScene : Scene var cell = y * _config.Width + x; _cellFertility[cell] = terrain.Fertility; _cellLand[cell] = terrain.IsLand; + _cellOccluderBase[cell] = terrain.BlocksLight; if (scatterRandom is not null) { ScatterSpawner.Spawn( @@ -355,6 +396,40 @@ public sealed class WorldScene : Scene } } + // Текущая сетка окклюдеров для лайтмапа: статичные горы + клетки со зрелыми деревьями. + private bool[] BuildOccluders() + { + Array.Copy(_cellOccluderBase, _cellOccluder, _cellOccluder.Length); + Store + .Query() + .ForEachEntity( + (ref PlantGrowth grow, ref Transform2D transform, Entity _) => + { + var species = _plants[grow.Species]; + if ( + species.Def.TrunkRadiusCells <= 0f + || grow.Stage < species.Stages.Length - 1 + ) + { + return; // затеняют только зрелые деревья (со стволом) + } + + var cx = Math.Clamp( + (int)(transform.Position.X / CellSize), + 0, + _config.Width - 1 + ); + var cy = Math.Clamp( + (int)(transform.Position.Y / CellSize), + 0, + _config.Height - 1 + ); + _cellOccluder[cy * _config.Width + cx] = true; + } + ); + return _cellOccluder; + } + private void Switch(Scene scene) { if (!Context.Scenes.IsTransitioning) diff --git a/src/LittleSim/Sim/PlantGrowthSystem.cs b/src/LittleSim/Sim/PlantGrowthSystem.cs index 54e8fb9..c2f4260 100644 --- a/src/LittleSim/Sim/PlantGrowthSystem.cs +++ b/src/LittleSim/Sim/PlantGrowthSystem.cs @@ -19,7 +19,7 @@ public sealed class PlantGrowthSystem( PlantSet plants, Calendar calendar, Climate climate, - DayNight dayNight, + Lighting lighting, int cellSize ) : QuerySystem { @@ -31,8 +31,7 @@ public sealed class PlantGrowthSystem( return; // пауза — рост стоит } - // В фазе B свет и температура глобальные — считаем раз за кадр (фаза D даст локальный свет). - var light = dayNight.Intensity; + // Температура глобальная; свет — локальный (лайтмап: подлесок под кронами темнее). var temperature = climate.Temperature; foreach (var (growths, genomes, sprites, transforms, _) in Query.Chunks) @@ -52,6 +51,7 @@ public sealed class PlantGrowthSystem( } ref var gene = ref dna[i]; + var light = lighting.SampleAt(t[i].Position); var rate = gene.Vigor * Suitability.Gaussian(light, gene.OptimalLight, gene.LightTolerance) From 32bae03869c1195986c0804eeabe6be0073203a2 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Fri, 12 Jun 2026 23:30:44 +0300 Subject: [PATCH 3/8] G1: bump engine (formula engine + Host split), wire formula demo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Engine pointer -> 10898b0, which brings in two engine changes: - the gene-system formula engine (Core): a data-driven expression evaluator that compiles a def string once and evaluates it allocation-free against a variable context; - a parallel refactor splitting the platform out of Core into a new MrGameEng.Host library. Game migration for the Host split: reference MrGameEng.Host (+ add it and its test project to the solution), import MrGameEng.Host where GameHost/GameHostOptions/Input are used, switch Transition.Fade -> Transitions.Fade, and read the device via context.GetGraphicsDevice() now that EngineContext is platform-free. Showcase the formula engine with a dev-console 'formula ' command that compiles and evaluates an expression. Also adds docs/гены.md, the gene-system design doc (phases G1-G5). Co-Authored-By: Claude Opus 4.8 --- LittleSim.sln | 30 ++++++++ docs/гены.md | 104 ++++++++++++++++++++++++++ engine | 2 +- src/LittleSim/LittleSim.csproj | 1 + src/LittleSim/Program.cs | 1 + src/LittleSim/Scenes/BootScene.cs | 3 +- src/LittleSim/Scenes/CreditsScene.cs | 3 +- src/LittleSim/Scenes/LoadGameScene.cs | 5 +- src/LittleSim/Scenes/MainMenuScene.cs | 3 +- src/LittleSim/Scenes/NewWorldScene.cs | 5 +- src/LittleSim/Scenes/PauseMenu.cs | 1 + src/LittleSim/Scenes/SettingsScene.cs | 3 +- src/LittleSim/Scenes/WorldScene.cs | 32 +++++++- 13 files changed, 181 insertions(+), 12 deletions(-) create mode 100644 docs/гены.md diff --git a/LittleSim.sln b/LittleSim.sln index 90a0b1e..15d0529 100644 --- a/LittleSim.sln +++ b/LittleSim.sln @@ -41,6 +41,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.UI.Tests", "engin EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Audio.Tests", "engine\tests\MrGameEng.Audio.Tests\MrGameEng.Audio.Tests.csproj", "{E0E37D87-4F62-41E9-9CD1-3E7432301508}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Host", "engine\src\MrGameEng.Host\MrGameEng.Host.csproj", "{6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Host.Tests", "engine\tests\MrGameEng.Host.Tests\MrGameEng.Host.Tests.csproj", "{BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -231,6 +235,30 @@ Global {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x64.Build.0 = Release|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x86.ActiveCfg = Release|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x86.Build.0 = Release|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x64.ActiveCfg = Debug|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x64.Build.0 = Debug|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x86.ActiveCfg = Debug|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x86.Build.0 = Debug|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|Any CPU.Build.0 = Release|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x64.ActiveCfg = Release|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x64.Build.0 = Release|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x86.ActiveCfg = Release|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x86.Build.0 = Release|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x64.ActiveCfg = Debug|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x64.Build.0 = Debug|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x86.ActiveCfg = Debug|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x86.Build.0 = Debug|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|Any CPU.Build.0 = Release|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x64.ActiveCfg = Release|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x64.Build.0 = Release|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x86.ActiveCfg = Release|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -253,5 +281,7 @@ Global {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {E0E37D87-4F62-41E9-9CD1-3E7432301508} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} EndGlobalSection EndGlobal diff --git a/docs/гены.md b/docs/гены.md new file mode 100644 index 0000000..60bed8d --- /dev/null +++ b/docs/гены.md @@ -0,0 +1,104 @@ +# Гены как фундамент + +Гены — **основополагающая** система LittleSim, общая для растений, животных и людей, а не +структура под один вид. Геном — переменного состава, поэтому возможны произвольные **гибриды** +(хоть человек с генами растения). Поведение организма не зашито в код, а **вычисляется из генов +формулами** через слой признаков. Этот документ описывает модель и порядок реализации (фазы G1–G5). +Дизайн потребителя-растений — в [растения.md](растения.md); гены заменяют его жёсткий `PlantGenome`. + +## Ключевые понятия + +- **`GeneDef`** — отдельный деф на ген, организм-агностичный: id, тип (числовой/дискретный/флаг), + диапазон/дефолт/разброс (для генерации особи), доминантность (дискретные — доминант/рецессив; + числовые — смешение), мутации (вероятность/величина), теги/категория и **эффекты** — вклады гена + в признаки через формулы. +- **Геном** — управляемый объект на сущности: набор генов **переменного состава** (id гена → пара + аллелей, диплоид). Поддерживает добавление/удаление/смешивание генов → гибриды. Скрещивание — + помейозно по объединению генов родителей + мутация. Сериализуется в сейв. +- **Признаки (traits)** — фенотип, вычисляемый из генома + среды: `growthSpeed`, `fruitYield`, + продукт добычи, цвет, период плодоношения, жизнь, расселение… **Системы читают готовые признаки**, + а не гены напрямую. Поэтому одна система плодоношения работает и для дерева, и для человека с + «плодовым» геном; и так «учитываются гены во всех расчётах». +- **Движок формул** — свой (без внешних библиотек) вычислитель выражений в дефах: числа, операции, + функции (`min/max/clamp/lerp/pow/exp/…`), переменные (значения генов/среды/других признаков). + Парсинг → AST → компиляция в делегат; детерминизм; покрыт тестами. +- **Регэкспы в дефах** — три применения: выбор/группировка генов по шаблону имени в формулах + (напр. «сумма всех `leaf_*`»); правила/патчи контента (применять патч ко всем дефам по шаблону); + валидация строковых полей дефов при загрузке. + +## Модель + +``` +GeneDef (данные) ──┐ + ├─► Геном особи (id гена → аллели) ──► [формулы эффектов + среда] ──► Признаки ──► Системы +GeneDef.effects ───┘ (движок формул) (traits) +``` + +- Генерация особи: из базового набора генов вида (деф организма ссылается на `GeneDef` с базовыми + значениями/разбросом) создаются аллели; всё сидируется `Random` мира — детерминизм. +- Признаки кэшируются на особи; пересчитываются при изменении генома (статические признаки) и/или по + среде (динамические — напр. итоговая скорость роста зависит ещё от света/температуры в точке). +- Скрещивание: для каждого гена потомок берёт по аллелю от каждого родителя (если ген есть у обоих; + если только у одного — наследуется с шансом), затем мутация по `GeneDef`. + +## Фазы + +Каждая фаза — отдельным планом (план → утверждение → коммит), как остальные работы. + +### G1 — движок формул (Core) + +Самодостаточный keystone, нужен всему остальному. +- Свой вычислитель выражений: лексер → парсер → AST → компиляция в `Func`. + Поддержка: числа, `+ - * / %`, скобки, унарный минус, сравнения/тернарник (для условий вроде + «плодоносит ли»), функции (`min,max,clamp,lerp,pow,exp,floor,round,abs,…`), переменные по имени. +- Контекст переменных — провайдер `name → value` (значения генов, среды, признаков). +- Детерминизм, без аллокаций при вычислении (компиляция один раз). Тесты на парсинг/вычисление/ошибки. +- Локация: `engine/src/MrGameEng.Core` (или `MrGameEng.Content`, ближе к дефам) — уточнится в плане. + +### G2 — фундамент генов (движок + контент-слой) + +- `GeneDef` (тип дефа) и реестр генов; парсинг эффектов как формул (зависит от G1). +- Управляемый `Genome` (компонент-объект): набор генов с аллелями; генерация из набора вида; + скрещивание + мутация; (де)сериализация. +- Слой признаков: вычисление `traits` из генома и эффектов (формулы) + провайдера среды; кэш и + инвалидация. API `Trait(name)` для систем. +- Без растительной специфики — общая машинерия для любых организмов. + +### G3 — миграция растений на гены/признаки + +- Рост/размножение/жизненный цикл растений читают **признаки** (`growthSpeed`, оптимум/толерантность + света/температуры/почвы, жизнь, расселение, самоопыление, мутации) вместо полей фиксированного + `PlantGenome`. +- `plants.json`/`GeneDef`: набор генов вида описывается данными; убираем жёсткий `PlantGenome`/`Breed`. +- Поведение эквивалентно текущему (фазы A–D), но теперь полностью data-driven через гены/формулы. +- Сейв популяции хранит динамический геном. + +### G4 — генный контент растений + +Новые механики как признаки + небольшие системы: +- **Плодоношение:** признаки «плодоносит ли» (флаг), тип плода (дискретный ген → продукт-деф), число + плодов, период/сезон плодоношения; система создаёт плоды у зрелых растений. +- **Добыча:** что даёт при сборе (дерево/грибы/трава/…) — дискретный ген → продукт; количество — числом. +- **Цвета/морфология:** тинт и вариации спрайта из генов; периоды и скорость роста — уже признаки. + +### G5 — регэкспы и контент-инструменты + +- Группировка генов по шаблону имени в формулах/эффектах (часть G1/G2, выносится сюда как фича). +- **Контент-патчи:** применять патч-деф ко всем дефам, чьё имя подходит под регэксп (массовые правки + модами поверх Core). +- **Валидация:** проверка строковых полей дефов (id/ключи/форматы) по шаблонам при загрузке мода. + +### Позже — животные и люди, гибриды + +Та же машинерия генома/признаков переиспользуется для животных и людей; гибриды — добавлением +«чужих» генов в геном (напр. фотосинтез человеку «растительным» геном, дающим признак, который +читает существующая система). Никаких спец-веток в коде — только новые `GeneDef` и формулы. + +## Принципы + +- **Гены — источник истины; формулы — мост; признаки — то, что читают системы.** Системы не знают + о конкретных видах, только о признаках. +- **Data-driven:** новый ген/эффект/признак — это деф и формула, а не код. +- **Детерминизм:** генерация, скрещивание и мутации — на сидированном `Random`; формулы чисты. +- **Движок vs игра:** движок даёт формулы, `GeneDef`, геном, вычисление признаков, регэксп-инструменты; + игра задаёт конкретные гены/формулы/системы-потребители (плодоношение, добыча) данными и кодом. diff --git a/engine b/engine index 79d406a..10898b0 160000 --- a/engine +++ b/engine @@ -1 +1 @@ -Subproject commit 79d406a9f4687fdb47a059a6b1e0593eb33e3061 +Subproject commit 10898b08a065ffc564c00473069e02b0e5e06c2a diff --git a/src/LittleSim/LittleSim.csproj b/src/LittleSim/LittleSim.csproj index c969813..0ad0e36 100644 --- a/src/LittleSim/LittleSim.csproj +++ b/src/LittleSim/LittleSim.csproj @@ -6,6 +6,7 @@ + diff --git a/src/LittleSim/Program.cs b/src/LittleSim/Program.cs index 80779f4..597fc93 100644 --- a/src/LittleSim/Program.cs +++ b/src/LittleSim/Program.cs @@ -1,6 +1,7 @@ using LittleSim.Scenes; using Microsoft.Xna.Framework; using MrGameEng.Core; +using MrGameEng.Host; // Контент Core-мода грузится уже в окне — на загрузочном экране (BootScene), в фоне. using var host = new GameHost( diff --git a/src/LittleSim/Scenes/BootScene.cs b/src/LittleSim/Scenes/BootScene.cs index b0597d7..9aa9832 100644 --- a/src/LittleSim/Scenes/BootScene.cs +++ b/src/LittleSim/Scenes/BootScene.cs @@ -5,6 +5,7 @@ using LittleSim.UI; using Microsoft.Xna.Framework; using MrGameEng.Audio; using MrGameEng.Core; +using MrGameEng.Host; using MrGameEng.UI; using Myra.Graphics2D.UI; @@ -73,6 +74,6 @@ public sealed class BootScene : Scene Context.Services.Add(content); Context.Services.Add(new ModAtlases(content.AtlasCacheDirectory)); content.Languages.SetLanguage(_settings.Language); - Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.6f)); + Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.6f)); } } diff --git a/src/LittleSim/Scenes/CreditsScene.cs b/src/LittleSim/Scenes/CreditsScene.cs index d6a6848..b4e63e5 100644 --- a/src/LittleSim/Scenes/CreditsScene.cs +++ b/src/LittleSim/Scenes/CreditsScene.cs @@ -2,6 +2,7 @@ using LittleSim.Content; using LittleSim.UI; using Microsoft.Xna.Framework.Input; using MrGameEng.Core; +using MrGameEng.Host; using MrGameEng.Input; using MrGameEng.UI; @@ -40,7 +41,7 @@ public sealed class CreditsScene : Scene { if (!Context.Scenes.IsTransitioning) { - Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.4f)); + Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f)); } } } diff --git a/src/LittleSim/Scenes/LoadGameScene.cs b/src/LittleSim/Scenes/LoadGameScene.cs index 9e502fd..be85763 100644 --- a/src/LittleSim/Scenes/LoadGameScene.cs +++ b/src/LittleSim/Scenes/LoadGameScene.cs @@ -3,6 +3,7 @@ using LittleSim.Content; using LittleSim.UI; using Microsoft.Xna.Framework.Input; using MrGameEng.Core; +using MrGameEng.Host; using MrGameEng.Input; using MrGameEng.UI; using Myra.Graphics2D.UI; @@ -73,7 +74,7 @@ public sealed class LoadGameScene : Scene { Context.Scenes.Switch( new WorldScene(save.ToConfig(), save), - Transition.Fade(0.6f) + Transitions.Fade(0.6f) ); } }, @@ -104,7 +105,7 @@ public sealed class LoadGameScene : Scene { if (!Context.Scenes.IsTransitioning) { - Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.4f)); + Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f)); } } } diff --git a/src/LittleSim/Scenes/MainMenuScene.cs b/src/LittleSim/Scenes/MainMenuScene.cs index 5800983..64dbc57 100644 --- a/src/LittleSim/Scenes/MainMenuScene.cs +++ b/src/LittleSim/Scenes/MainMenuScene.cs @@ -2,6 +2,7 @@ using LittleSim.Content; using LittleSim.UI; using Microsoft.Xna.Framework; using MrGameEng.Core; +using MrGameEng.Host; using MrGameEng.UI; namespace LittleSim.Scenes; @@ -37,7 +38,7 @@ public sealed class MainMenuScene : Scene { if (!Context.Scenes.IsTransitioning) { - Context.Scenes.Switch(scene, Transition.Fade(0.4f)); + Context.Scenes.Switch(scene, Transitions.Fade(0.4f)); } } } diff --git a/src/LittleSim/Scenes/NewWorldScene.cs b/src/LittleSim/Scenes/NewWorldScene.cs index 70d53d8..a379f2a 100644 --- a/src/LittleSim/Scenes/NewWorldScene.cs +++ b/src/LittleSim/Scenes/NewWorldScene.cs @@ -6,6 +6,7 @@ using LittleSim.Content; using LittleSim.UI; using Microsoft.Xna.Framework.Input; using MrGameEng.Core; +using MrGameEng.Host; using MrGameEng.Input; using MrGameEng.UI; using Myra.Graphics2D.UI; @@ -153,14 +154,14 @@ public sealed class NewWorldScene : Scene Seed = seed, SmoothPasses = _smoothing, }; - Context.Scenes.Switch(new WorldScene(config), Transition.Fade(0.6f)); + Context.Scenes.Switch(new WorldScene(config), Transitions.Fade(0.6f)); } private void Back() { if (!Context.Scenes.IsTransitioning) { - Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.4f)); + Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f)); } } } diff --git a/src/LittleSim/Scenes/PauseMenu.cs b/src/LittleSim/Scenes/PauseMenu.cs index 0395c9d..6071706 100644 --- a/src/LittleSim/Scenes/PauseMenu.cs +++ b/src/LittleSim/Scenes/PauseMenu.cs @@ -5,6 +5,7 @@ using LittleSim.UI; using Microsoft.Xna.Framework; using MrGameEng.Audio; using MrGameEng.Core; +using MrGameEng.Host; using Myra.Graphics2D.Brushes; using Myra.Graphics2D.UI; diff --git a/src/LittleSim/Scenes/SettingsScene.cs b/src/LittleSim/Scenes/SettingsScene.cs index d03ad04..ce1e79f 100644 --- a/src/LittleSim/Scenes/SettingsScene.cs +++ b/src/LittleSim/Scenes/SettingsScene.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using MrGameEng.Audio; using MrGameEng.Core; +using MrGameEng.Host; using MrGameEng.Input; using MrGameEng.UI; using Myra.Graphics2D.UI; @@ -59,7 +60,7 @@ public sealed class SettingsScene : Scene { if (!Context.Scenes.IsTransitioning) { - Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.4f)); + Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f)); } } } diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 31dcfaf..84461fd 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -10,7 +10,9 @@ using Microsoft.Xna.Framework.Input; using MrGameEng.Assets; using MrGameEng.Core; using MrGameEng.DevConsole; +using MrGameEng.Formulas; using MrGameEng.Graphics; +using MrGameEng.Host; using MrGameEng.Input; using MrGameEng.Inspector; using MrGameEng.Lighting; @@ -73,7 +75,7 @@ public sealed class WorldScene : Scene var assets = Context.Services.GetOrDefault() ?? Context.UseAssets(); var content = Context.Services.Get(); var atlases = Context.Services.Get(); - var device = Context.GraphicsDevice; + var device = Context.GetGraphicsDevice(); var input = this.UseInput(); _speed = Context.Services.Get(); _speed.SetStep(0); // новый мир/загрузка стартуют на x1 @@ -435,7 +437,7 @@ public sealed class WorldScene : Scene if (!Context.Scenes.IsTransitioning) { _speed.Resume(); - Context.Scenes.Switch(scene, Transition.Fade(0.5f)); + Context.Scenes.Switch(scene, Transitions.Fade(0.5f)); } } @@ -456,10 +458,34 @@ public sealed class WorldScene : Scene c.WriteLine($"regenerating world, seed {seed}"); Context.Scenes.Switch( new WorldScene(_config with { Seed = seed }), - Transition.Fade(0.6f) + Transitions.Fade(0.6f) ); } ); + console.Register( + "formula", + "formula — compile and evaluate an expression (gene-formula engine demo)", + (c, args) => + { + if (args.Length == 0) + { + c.WriteLine( + "usage: formula e.g. formula clamp(lerp(0, 10, 0.5), 0, 8)" + ); + return; + } + + var expression = string.Join(' ', args); + try + { + c.WriteLine($"{expression} = {Formula.Compile(expression).Evaluate()}"); + } + catch (FormulaException e) + { + c.WriteLine($"error: {e.Message}"); + } + } + ); console.Register( "menu", "menu — return to the main menu", From 91a00b4305f92e4a4282331196cf614aed4f1e39 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Fri, 12 Jun 2026 23:43:50 +0300 Subject: [PATCH 4/8] G2: bump engine (gene foundation), load genes + gene demo command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Engine pointer -> bc18db5: brings in the organism-agnostic gene foundation (GeneDef, managed Genome, Phenotype trait computation) plus a parallel MrGameEng.Net library the game does not use. Game wiring: register the "Gene" def type and ship Mods/Core/Defs/ genes.json — a full gene set covering the plant phenotype dimensions (environment optima/tolerances, vigor, lifecycle, reproduction) plus a discrete morph gene, each declaring its trait effects as formulas. A dev-console 'gene [seed]' command generates a genome from these defs, prints its alleles, expressed values and computed traits, then breeds a child — demonstrating the whole pipeline end to end. These genes seed the G3 migration of plants onto traits. Co-Authored-By: Claude Opus 4.8 --- Mods/Core/Defs/genes.json | 53 ++++++++++++++++++++++++++++ engine | 2 +- src/LittleSim/Content/GameContent.cs | 2 ++ src/LittleSim/Scenes/WorldScene.cs | 48 +++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 Mods/Core/Defs/genes.json diff --git a/Mods/Core/Defs/genes.json b/Mods/Core/Defs/genes.json new file mode 100644 index 0000000..de74c8c --- /dev/null +++ b/Mods/Core/Defs/genes.json @@ -0,0 +1,53 @@ +{ + "type": "Gene", + // Организм-агностичные гены: каждый ген задаёт, как генерируются/мутируют аллели и как ген + // вкладывается в признаки (effects: имя признака → формула; value — выраженное значение гена). + // Этот набор покрывает измерения генома растения (его используют системы роста/жизненного цикла). + "defs": [ + { "defName": "BaseNumericGene", "abstract": true, "kind": "Numeric", + "spread": 0.08, "mutationChance": 0.05, "mutationMagnitude": 0.12 }, + + { "defName": "GeneOptimalLight", "parent": "BaseNumericGene", "label": "gene.optimalLight", + "default": 0.6, "min": 0.0, "max": 1.0, "tags": ["environment", "light"], + "effects": { "optimalLight": "value" } }, + { "defName": "GeneLightTolerance", "parent": "BaseNumericGene", "label": "gene.lightTolerance", + "default": 0.5, "min": 0.05, "max": 1.0, "tags": ["environment", "light"], + "effects": { "lightTolerance": "value" } }, + { "defName": "GeneOptimalTemperature", "parent": "BaseNumericGene", "label": "gene.optimalTemperature", + "default": 14, "min": -20, "max": 45, "tags": ["environment", "temperature"], + "effects": { "optimalTemperature": "value" } }, + { "defName": "GeneTemperatureTolerance", "parent": "BaseNumericGene", "label": "gene.temperatureTolerance", + "default": 16, "min": 2, "max": 40, "tags": ["environment", "temperature"], + "effects": { "temperatureTolerance": "value" } }, + { "defName": "GeneOptimalFertility", "parent": "BaseNumericGene", "label": "gene.optimalFertility", + "default": 1.4, "min": 0.1, "max": 3.0, "tags": ["environment", "soil"], + "effects": { "optimalFertility": "value" } }, + { "defName": "GeneFertilityTolerance", "parent": "BaseNumericGene", "label": "gene.fertilityTolerance", + "default": 0.9, "min": 0.1, "max": 3.0, "tags": ["environment", "soil"], + "effects": { "fertilityTolerance": "value" } }, + + { "defName": "GeneVigor", "parent": "BaseNumericGene", "label": "gene.vigor", + "default": 1.0, "min": 0.1, "max": 3.0, "tags": ["growth"], + "effects": { "vigor": "value" } }, + { "defName": "GeneLifespan", "parent": "BaseNumericGene", "label": "gene.lifespan", + "default": 160, "min": 5, "max": 500, "tags": ["lifecycle"], + "effects": { "lifespan": "value" } }, + { "defName": "GeneDispersalRange", "parent": "BaseNumericGene", "label": "gene.dispersalRange", + "default": 2, "min": 1, "max": 8, "tags": ["lifecycle", "reproduction"], + "effects": { "dispersalRange": "value" } }, + { "defName": "GeneReproduceInterval", "parent": "BaseNumericGene", "label": "gene.reproduceInterval", + "default": 14, "min": 1, "max": 60, "tags": ["lifecycle", "reproduction"], + "effects": { "reproduceInterval": "value" } }, + { "defName": "GeneSelfPollination", "parent": "BaseNumericGene", "label": "gene.selfPollination", + "default": 0.2, "min": 0.0, "max": 1.0, "tags": ["reproduction"], + "effects": { "selfPollination": "value" } }, + { "defName": "GeneMutationRate", "parent": "BaseNumericGene", "label": "gene.mutationRate", + "default": 0.05, "min": 0.0, "max": 1.0, "tags": ["reproduction"], + "effects": { "mutationRate": "value" } }, + + // Дискретный ген морфы: вариант 0 доминирует, 1 (рецессивный) виден только в гомозиготе. + { "defName": "GeneMorph", "kind": "Discrete", "label": "gene.morph", + "variants": 2, "variantWeights": [0.82, 0.18], "mutationChance": 0.05, "tags": ["morphology"], + "effects": { "variant": "value" } } + ] +} diff --git a/engine b/engine index 10898b0..bc18db5 160000 --- a/engine +++ b/engine @@ -1 +1 @@ -Subproject commit 10898b08a065ffc564c00473069e02b0e5e06c2a +Subproject commit bc18db5df637d7683e22e4306834ba355033a612 diff --git a/src/LittleSim/Content/GameContent.cs b/src/LittleSim/Content/GameContent.cs index ed41727..347da4c 100644 --- a/src/LittleSim/Content/GameContent.cs +++ b/src/LittleSim/Content/GameContent.cs @@ -1,5 +1,6 @@ using MrGameEng.Atlases; using MrGameEng.Core; +using MrGameEng.Genetics; using MrGameEng.Mods; namespace LittleSim.Content; @@ -56,6 +57,7 @@ public sealed class GameContent var defs = new DefDatabase(); defs.RegisterType("Terrain"); + defs.RegisterType("Gene"); defs.RegisterType("Plant"); defs.RegisterType("Pawn"); defs.RegisterType("WorldPreset"); diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 84461fd..34b2a0f 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -11,6 +11,7 @@ using MrGameEng.Assets; using MrGameEng.Core; using MrGameEng.DevConsole; using MrGameEng.Formulas; +using MrGameEng.Genetics; using MrGameEng.Graphics; using MrGameEng.Host; using MrGameEng.Input; @@ -486,10 +487,57 @@ public sealed class WorldScene : Scene } } ); + console.Register( + "gene", + "gene [seed] — generate a genome from Gene defs, show alleles/traits and a bred child", + (c, args) => RunGeneDemo(c, content, args) + ); console.Register( "menu", "menu — return to the main menu", (_, _) => Switch(new MainMenuScene()) ); } + + // Демонстрация генной системы (фаза G2): из Gene-дефов генерируем геном, печатаем аллели, + // выраженные значения и признаки (вычисленные формулами), затем скрещиваем двух особей. + private static void RunGeneDemo(DevConsole console, GameContent content, string[] args) + { + var genes = content.Defs.All(); + if (genes.Count == 0) + { + console.WriteLine("no Gene defs loaded"); + return; + } + + var registry = genes.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal); + var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next(); + var random = new Random(seed); + + console.WriteLine($"genome from {genes.Count} genes, seed {seed}:"); + var parentA = Genome.Generate(genes, random); + foreach (var gene in genes) + { + var allele = parentA[gene.DefName]; + console.WriteLine( + $" {gene.DefName}: [{allele.A:0.##}, {allele.B:0.##}] -> {parentA.Express(gene):0.###}" + ); + } + + console.WriteLine("traits:"); + foreach (var (trait, value) in Phenotype.Compute(parentA, registry).OrderBy(t => t.Key)) + { + console.WriteLine($" {trait} = {value:0.###}"); + } + + var parentB = Genome.Generate(genes, random); + var child = Genome.Breed(parentA, parentB, registry, random); + var childTraits = Phenotype.Compute(child, registry); + console.WriteLine( + $"bred child: {child.Alleles.Count} genes, " + + $"vigor={childTraits.GetValueOrDefault("vigor"):0.###}, " + + $"lifespan={childTraits.GetValueOrDefault("lifespan"):0.#}, " + + $"variant={childTraits.GetValueOrDefault("variant"):0}" + ); + } } From 9c2c2d7fd06b8c1b4f02b22e565dd104fadebcc5 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Fri, 12 Jun 2026 23:59:46 +0300 Subject: [PATCH 5/8] G3: migrate plants onto genes/traits (drop fixed PlantGenome) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plants now carry a managed Genome and read formula-computed traits instead of the hardcoded PlantGenome struct. Engine pointer -> ead2251 (GenomeTemplate + Breed mutationChance override). - PlantOrganism { Genome; PlantPhenotype Traits } replaces PlantGenome. Traits (optima/tolerances, vigor, lifespan, dispersal, reproduce interval, self-pollination, mutation rate, morph variant) come from Phenotype.Compute over the gene effect formulas in genes.json. - PlantSet builds a per-species GenomeTemplate by mapping each species' existing plants.json genome numbers onto the shared GeneDefs, and exposes the gene registry. Species values unchanged — only reinterpreted through genes now. - Growth and lifecycle systems read PlantOrganism.Traits; breeding goes through Genome.Breed with the registry and a mutation chance averaged from the parents' evolvable mutationRate trait. Scatter generates from the species template. - Save stores the genome as a geneId->Allele map; load rebuilds it (empty/legacy saves regenerate from the template — dev saves disposable). Behaviour matches phases A-D, now fully data-driven through genes and formulas. Full suite green; boot smoke test loads genes + plants cleanly. Co-Authored-By: Claude Opus 4.8 --- engine | 2 +- src/LittleSim/App/WorldSave.cs | 8 +- src/LittleSim/Content/PlantSet.cs | 58 +++++++- src/LittleSim/Scenes/ContentHelpers.cs | 2 +- src/LittleSim/Scenes/WorldScene.cs | 20 ++- src/LittleSim/Sim/PlantFactory.cs | 14 +- src/LittleSim/Sim/PlantGenome.cs | 171 ---------------------- src/LittleSim/Sim/PlantGrowthSystem.cs | 27 ++-- src/LittleSim/Sim/PlantLifecycleSystem.cs | 60 +++++--- src/LittleSim/Sim/PlantOrganism.cs | 66 +++++++++ 10 files changed, 208 insertions(+), 220 deletions(-) delete mode 100644 src/LittleSim/Sim/PlantGenome.cs create mode 100644 src/LittleSim/Sim/PlantOrganism.cs diff --git a/engine b/engine index bc18db5..ead2251 160000 --- a/engine +++ b/engine @@ -1 +1 @@ -Subproject commit bc18db5df637d7683e22e4306834ba355033a612 +Subproject commit ead22517ee7a3da4d5d80c29f4061fc36f69089f diff --git a/src/LittleSim/App/WorldSave.cs b/src/LittleSim/App/WorldSave.cs index 6fe6158..871c3f3 100644 --- a/src/LittleSim/App/WorldSave.cs +++ b/src/LittleSim/App/WorldSave.cs @@ -1,5 +1,5 @@ using System.Text.Json; -using LittleSim.Sim; +using MrGameEng.Genetics; namespace LittleSim.App; @@ -24,8 +24,8 @@ public sealed class PlantSave /// Плодородность клетки. public float CellFertility { get; set; } - /// Геном особи (аллели сериализуются как поля структуры). - public PlantGenome Genome { get; set; } + /// Геном особи: аллели по id генов (переменного состава). + public Dictionary Genome { get; set; } = new(); } /// Сериализуемое состояние одного жителя (sim-компоненты; спрайт пересобирается из дефа по ). @@ -119,7 +119,7 @@ public sealed class SaveStore { WriteIndented = true, PropertyNameCaseInsensitive = true, - IncludeFields = true, // аллели генома (PlantGenome) — публичные поля структуры + IncludeFields = true, // на случай публичных полей в сохраняемых sim-структурах }; private readonly string _directory; diff --git a/src/LittleSim/Content/PlantSet.cs b/src/LittleSim/Content/PlantSet.cs index 74872cb..1a57f8b 100644 --- a/src/LittleSim/Content/PlantSet.cs +++ b/src/LittleSim/Content/PlantSet.cs @@ -1,4 +1,5 @@ using Microsoft.Xna.Framework.Graphics; +using MrGameEng.Genetics; using MrGameEng.Graphics; namespace LittleSim.Content; @@ -23,6 +24,9 @@ public sealed class PlantSet /// Стадии роста по порядку; последняя — терминальная. public required Stage[] Stages { get; init; } + /// Набор генов вида: базовые значения для генерации особи (геном из общих ). + public required GenomeTemplate Template { get; init; } + /// День, с которого растение достигает последней (зрелой) стадии. public float MaturityDays => Stages[^1].EnterDay; @@ -42,15 +46,28 @@ public sealed class PlantSet private readonly Species[] _species; private readonly Dictionary _index = new(); + /// Общий реестр генов (по id) для скрещивания и вычисления признаков растений. + public IReadOnlyDictionary GeneRegistry { get; } + /// Строит таблицу из всех (не-abstract) мода. public PlantSet(GameContent content, ModAtlases atlases, GraphicsDevice device) { + var genes = content + .Defs.All() + .ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal); + GeneRegistry = genes; + var defs = content.Defs.All().ToArray(); _species = new Species[defs.Length]; for (var i = 0; i < defs.Length; i++) { var def = defs[i]; - _species[i] = new Species { Def = def, Stages = BuildStages(def, atlases, device) }; + _species[i] = new Species + { + Def = def, + Stages = BuildStages(def, atlases, device), + Template = BuildTemplate(def.Genome, genes), + }; _index[def] = i; } } @@ -64,6 +81,45 @@ public sealed class PlantSet /// Индекс вида по дефу. public int IndexOf(PlantDef def) => _index[def]; + // Переводит базовый геном вида (числа из plants.json) в набор генов: каждое число — центр + // аллелей соответствующего общего GeneDef; морфа — дискретный ген с долей рецессива из вида. + private static GenomeTemplate BuildTemplate( + GenomeDef g, + IReadOnlyDictionary genes + ) + { + GeneDef Gene(string id) => + genes.TryGetValue(id, out var def) + ? def + : throw new KeyNotFoundException( + $"Gene def '{id}' not found — is Mods/Core/Defs/genes.json loaded?" + ); + + GenomeTemplate.Entry Numeric(string id, float baseValue) => + new(Gene(id), baseValue, g.Spread); + + return new GenomeTemplate([ + Numeric("GeneOptimalLight", g.OptimalLight), + Numeric("GeneLightTolerance", g.LightTolerance), + Numeric("GeneOptimalTemperature", g.OptimalTemperature), + Numeric("GeneTemperatureTolerance", g.TemperatureTolerance), + Numeric("GeneOptimalFertility", g.OptimalFertility), + Numeric("GeneFertilityTolerance", g.FertilityTolerance), + Numeric("GeneVigor", g.Vigor), + Numeric("GeneLifespan", g.Lifespan), + Numeric("GeneDispersalRange", g.DispersalRange), + Numeric("GeneReproduceInterval", g.ReproduceInterval), + Numeric("GeneSelfPollination", g.SelfPollination), + Numeric("GeneMutationRate", g.MutationRate), + new GenomeTemplate.Entry( + Gene("GeneMorph"), + 0f, + 0f, + [1f - g.VariantChance, g.VariantChance] + ), + ]); + } + private static Stage[] BuildStages(PlantDef def, ModAtlases atlases, GraphicsDevice device) { // Без явных стадий растение существует как одна терминальная стадия из базовой текстуры. diff --git a/src/LittleSim/Scenes/ContentHelpers.cs b/src/LittleSim/Scenes/ContentHelpers.cs index 1003ffa..07f90f0 100644 --- a/src/LittleSim/Scenes/ContentHelpers.cs +++ b/src/LittleSim/Scenes/ContentHelpers.cs @@ -46,7 +46,7 @@ internal static class ScatterSpawner index, new Vector2(px, py), age, - PlantGenome.FromDef(def.Genome, random), + plants[index].Template.Generate(random), terrain.Fertility, cellSize ); diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 34b2a0f..b4788c5 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -353,9 +353,14 @@ public sealed class WorldScene : Scene // Снимок всей популяции растений: вид (деф), позиция, возраст, стадия, почва и геном. Store - .Query() + .Query() .ForEachEntity( - (ref PlantGrowth grow, ref PlantGenome gene, ref Transform2D transform, Entity _) => + ( + ref PlantGrowth grow, + ref PlantOrganism org, + ref Transform2D transform, + Entity _ + ) => { save.Plants.Add( new PlantSave @@ -366,7 +371,7 @@ public sealed class WorldScene : Scene AgeDays = grow.AgeDays, Stage = grow.Stage, CellFertility = grow.CellFertility, - Genome = gene, + Genome = org.Genome.ToDictionary(), } ); } @@ -379,6 +384,7 @@ public sealed class WorldScene : Scene private void RestorePlants(GameContent content) { + var fallback = new Random(_config.Seed); // для старых/битых сейвов без генома foreach (var plant in _save!.Plants) { if (!content.Defs.TryGet(plant.Species, out var def)) @@ -386,13 +392,17 @@ public sealed class WorldScene : Scene continue; // вид пропал (мод убрали) — пропускаем растение } + var index = _plants.IndexOf(def); + var genome = plant.Genome is { Count: > 0 } + ? new Genome(plant.Genome) + : _plants[index].Template.Generate(fallback); PlantFactory.Create( Store, _plants, - _plants.IndexOf(def), + index, new Vector2(plant.X, plant.Y), plant.AgeDays, - plant.Genome, + genome, plant.CellFertility, CellSize ); diff --git a/src/LittleSim/Sim/PlantFactory.cs b/src/LittleSim/Sim/PlantFactory.cs index ebeb22a..d15fb17 100644 --- a/src/LittleSim/Sim/PlantFactory.cs +++ b/src/LittleSim/Sim/PlantFactory.cs @@ -2,14 +2,15 @@ using Friflo.Engine.ECS; using LittleSim.Content; using LittleSim.Scenes; using Microsoft.Xna.Framework; +using MrGameEng.Genetics; using MrGameEng.Graphics; namespace LittleSim.Sim; /// -/// Единая точка создания сущности-растения: спрайт стадии по возрасту, тинт по морфе генома и -/// компоненты роста/генома. Переиспользуется начальным скаттером, размножением и загрузкой сейва — -/// чтобы все растения собирались одинаково. +/// Единая точка создания сущности-растения: спрайт стадии по возрасту, признаки из генома формулами +/// генов, тинт по морфе и компоненты роста/организма. Переиспользуется начальным скаттером, +/// размножением и загрузкой сейва — чтобы все растения собирались одинаково. /// public static class PlantFactory { @@ -23,7 +24,7 @@ public static class PlantFactory int species, Vector2 position, float ageDays, - in PlantGenome genome, + Genome genome, float cellFertility, int cellSize ) @@ -31,10 +32,11 @@ public static class PlantFactory var sp = plants[species]; var stage = sp.StageAt(ageDays); var resolved = sp.Stages[stage]; + var traits = PlantPhenotype.FromTraits(Phenotype.Compute(genome, plants.GeneRegistry)); var sprite = new Sprite(resolved.Region, GameLayers.Beings); sprite.CenterOrigin(); - sprite.Color = genome.IsVariant ? VariantTint : Color.White; + sprite.Color = traits.IsVariant ? VariantTint : Color.White; return store.CreateEntity( new Transform2D( @@ -49,7 +51,7 @@ public static class PlantFactory AgeDays = ageDays, CellFertility = cellFertility, }, - genome + new PlantOrganism { Genome = genome, Traits = traits } ); } } diff --git a/src/LittleSim/Sim/PlantGenome.cs b/src/LittleSim/Sim/PlantGenome.cs deleted file mode 100644 index b697088..0000000 --- a/src/LittleSim/Sim/PlantGenome.cs +++ /dev/null @@ -1,171 +0,0 @@ -using Friflo.Engine.ECS; -using LittleSim.Content; - -namespace LittleSim.Sim; - -/// -/// Диплоидный геном растения: по паре аллелей на ген. Числовые гены экспрессируются гибридно — -/// фенотип есть среднее двух аллелей; дискретный ген морфы — по Менделю (рецессив виден только в -/// гомозиготе). Аллели наследуются (правятся в инспекторе); фенотип-свойства читают системы роста и -/// жизненного цикла. моделирует мейоз и мутацию. -/// -public struct PlantGenome : IComponent -{ - // --- Числовые гены (аллель A/B), фенотип = среднее ------------------------------------------ - public float OptimalLightA; - public float OptimalLightB; - public float LightToleranceA; - public float LightToleranceB; - public float OptimalTemperatureA; - public float OptimalTemperatureB; - public float TemperatureToleranceA; - public float TemperatureToleranceB; - public float OptimalFertilityA; - public float OptimalFertilityB; - public float FertilityToleranceA; - public float FertilityToleranceB; - public float VigorA; - public float VigorB; - public float LifespanA; - public float LifespanB; - public float DispersalRangeA; - public float DispersalRangeB; - public float ReproduceIntervalA; - public float ReproduceIntervalB; - public float SelfPollinationA; - public float SelfPollinationB; - public float MutationRateA; - public float MutationRateB; - - // --- Дискретный ген морфы (0 — доминантный, 1 — рецессивный) -------------------------------- - public int MorphA; - public int MorphB; - - /// Фенотип: оптимальная освещённость. - public readonly float OptimalLight => (OptimalLightA + OptimalLightB) * 0.5f; - - /// Фенотип: толерантность по свету. - public readonly float LightTolerance => (LightToleranceA + LightToleranceB) * 0.5f; - - /// Фенотип: оптимальная температура. - public readonly float OptimalTemperature => (OptimalTemperatureA + OptimalTemperatureB) * 0.5f; - - /// Фенотип: толерантность по температуре. - public readonly float TemperatureTolerance => - (TemperatureToleranceA + TemperatureToleranceB) * 0.5f; - - /// Фенотип: оптимальная плодородность. - public readonly float OptimalFertility => (OptimalFertilityA + OptimalFertilityB) * 0.5f; - - /// Фенотип: толерантность по плодородности. - public readonly float FertilityTolerance => (FertilityToleranceA + FertilityToleranceB) * 0.5f; - - /// Фенотип: бодрость роста. - public readonly float Vigor => (VigorA + VigorB) * 0.5f; - - /// Фенотип: продолжительность жизни (дней). - public readonly float Lifespan => (LifespanA + LifespanB) * 0.5f; - - /// Фенотип: дальность расселения (клеток). - public readonly float DispersalRange => (DispersalRangeA + DispersalRangeB) * 0.5f; - - /// Фенотип: интервал размножения (дней). - public readonly float ReproduceInterval => (ReproduceIntervalA + ReproduceIntervalB) * 0.5f; - - /// Фенотип: способность к самоопылению (0..1). - public readonly float SelfPollination => (SelfPollinationA + SelfPollinationB) * 0.5f; - - /// Фенотип: темп мутаций (0..1). - public readonly float MutationRate => (MutationRateA + MutationRateB) * 0.5f; - - /// Фенотип морфы: рецессивный вариант проявляется только в гомозиготе (1,1). - public readonly bool IsVariant => MorphA == 1 && MorphB == 1; - - /// - /// Генерирует особь из базового генома вида: числовые аллели — база ± относительный разброс - /// (); аллель морфы = рецессивный (1) с вероятностью - /// . Сидируется — детерминизм. - /// - public static PlantGenome FromDef(GenomeDef def, Random random) - { - float Allele(float baseValue) => - baseValue + (random.NextSingle() * 2f - 1f) * def.Spread * baseValue; - - int MorphAllele() => random.NextSingle() < def.VariantChance ? 1 : 0; - - return new PlantGenome - { - OptimalLightA = Allele(def.OptimalLight), - OptimalLightB = Allele(def.OptimalLight), - LightToleranceA = Allele(def.LightTolerance), - LightToleranceB = Allele(def.LightTolerance), - OptimalTemperatureA = Allele(def.OptimalTemperature), - OptimalTemperatureB = Allele(def.OptimalTemperature), - TemperatureToleranceA = Allele(def.TemperatureTolerance), - TemperatureToleranceB = Allele(def.TemperatureTolerance), - OptimalFertilityA = Allele(def.OptimalFertility), - OptimalFertilityB = Allele(def.OptimalFertility), - FertilityToleranceA = Allele(def.FertilityTolerance), - FertilityToleranceB = Allele(def.FertilityTolerance), - VigorA = Allele(def.Vigor), - VigorB = Allele(def.Vigor), - LifespanA = Allele(def.Lifespan), - LifespanB = Allele(def.Lifespan), - DispersalRangeA = Allele(def.DispersalRange), - DispersalRangeB = Allele(def.DispersalRange), - ReproduceIntervalA = Allele(def.ReproduceInterval), - ReproduceIntervalB = Allele(def.ReproduceInterval), - SelfPollinationA = Allele(def.SelfPollination), - SelfPollinationB = Allele(def.SelfPollination), - MutationRateA = Allele(def.MutationRate), - MutationRateB = Allele(def.MutationRate), - MorphA = MorphAllele(), - MorphB = MorphAllele(), - }; - } - - /// - /// Потомок двух родителей (мейоз): аллель A — случайный из родителя , аллель - /// B — из ; затем мутация (с вероятностью среднего темпа мутаций родителей — - /// сдвиг числового аллеля на малую долю / переключение морфы). Самоопыление = Breed(self, self). - /// - public static PlantGenome Breed(in PlantGenome a, in PlantGenome b, Random rng) - { - var rate = (a.MutationRate + b.MutationRate) * 0.5f; - - float Mut(float v) => - rng.NextSingle() < rate ? v + (rng.NextSingle() * 2f - 1f) * 0.12f * v : v; - float FromA(float x, float y) => Mut(rng.NextSingle() < 0.5f ? x : y); - int MutMorph(int m) => rng.NextSingle() < rate ? 1 - m : m; - - return new PlantGenome - { - OptimalLightA = FromA(a.OptimalLightA, a.OptimalLightB), - OptimalLightB = FromA(b.OptimalLightA, b.OptimalLightB), - LightToleranceA = FromA(a.LightToleranceA, a.LightToleranceB), - LightToleranceB = FromA(b.LightToleranceA, b.LightToleranceB), - OptimalTemperatureA = FromA(a.OptimalTemperatureA, a.OptimalTemperatureB), - OptimalTemperatureB = FromA(b.OptimalTemperatureA, b.OptimalTemperatureB), - TemperatureToleranceA = FromA(a.TemperatureToleranceA, a.TemperatureToleranceB), - TemperatureToleranceB = FromA(b.TemperatureToleranceA, b.TemperatureToleranceB), - OptimalFertilityA = FromA(a.OptimalFertilityA, a.OptimalFertilityB), - OptimalFertilityB = FromA(b.OptimalFertilityA, b.OptimalFertilityB), - FertilityToleranceA = FromA(a.FertilityToleranceA, a.FertilityToleranceB), - FertilityToleranceB = FromA(b.FertilityToleranceA, b.FertilityToleranceB), - VigorA = FromA(a.VigorA, a.VigorB), - VigorB = FromA(b.VigorA, b.VigorB), - LifespanA = FromA(a.LifespanA, a.LifespanB), - LifespanB = FromA(b.LifespanA, b.LifespanB), - DispersalRangeA = FromA(a.DispersalRangeA, a.DispersalRangeB), - DispersalRangeB = FromA(b.DispersalRangeA, b.DispersalRangeB), - ReproduceIntervalA = FromA(a.ReproduceIntervalA, a.ReproduceIntervalB), - ReproduceIntervalB = FromA(b.ReproduceIntervalA, b.ReproduceIntervalB), - SelfPollinationA = FromA(a.SelfPollinationA, a.SelfPollinationB), - SelfPollinationB = FromA(b.SelfPollinationA, b.SelfPollinationB), - MutationRateA = FromA(a.MutationRateA, a.MutationRateB), - MutationRateB = FromA(b.MutationRateA, b.MutationRateB), - MorphA = MutMorph(rng.NextSingle() < 0.5f ? a.MorphA : a.MorphB), - MorphB = MutMorph(rng.NextSingle() < 0.5f ? b.MorphA : b.MorphB), - }; - } -} diff --git a/src/LittleSim/Sim/PlantGrowthSystem.cs b/src/LittleSim/Sim/PlantGrowthSystem.cs index c2f4260..e7f200c 100644 --- a/src/LittleSim/Sim/PlantGrowthSystem.cs +++ b/src/LittleSim/Sim/PlantGrowthSystem.cs @@ -11,9 +11,10 @@ namespace LittleSim.Sim; /// /// Двигает рост растений по игровому календарю: прирост возраста = пригодность среды для генов /// растения. Скорость = бодрость · пригодность(свет) · пригодность(температура) · -/// пригодность(плодородность), каждая пригодность — гауссов «колокол» вокруг оптимума из генов -/// (см. ). Свет/температура в фазе B глобальные (день/ночь и сезон); стадии -/// и спрайт переключаются по накопленному возрасту как прежде. Терминальную стадию пропускает. +/// пригодность(плодородность), каждая пригодность — гауссов «колокол» вокруг оптимума из +/// признаков (), вычисленных формулами генов. Свет локальный (лайтмап), +/// температура глобальная (сезон/день-ночь); стадии и спрайт переключаются по накопленному +/// возрасту как прежде. Терминальную стадию пропускает. /// public sealed class PlantGrowthSystem( PlantSet plants, @@ -21,7 +22,7 @@ public sealed class PlantGrowthSystem( Climate climate, Lighting lighting, int cellSize -) : QuerySystem +) : QuerySystem { protected override void OnUpdate() { @@ -34,10 +35,10 @@ public sealed class PlantGrowthSystem( // Температура глобальная; свет — локальный (лайтмап: подлесок под кронами темнее). var temperature = climate.Temperature; - foreach (var (growths, genomes, sprites, transforms, _) in Query.Chunks) + foreach (var (growths, organisms, sprites, transforms, _) in Query.Chunks) { var g = growths.Span; - var dna = genomes.Span; + var orgs = organisms.Span; var s = sprites.Span; var t = transforms.Span; for (var i = 0; i < g.Length; i++) @@ -50,20 +51,20 @@ public sealed class PlantGrowthSystem( continue; } - ref var gene = ref dna[i]; + ref var traits = ref orgs[i].Traits; var light = lighting.SampleAt(t[i].Position); var rate = - gene.Vigor - * Suitability.Gaussian(light, gene.OptimalLight, gene.LightTolerance) + traits.Vigor + * Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance) * Suitability.Gaussian( temperature, - gene.OptimalTemperature, - gene.TemperatureTolerance + traits.OptimalTemperature, + traits.TemperatureTolerance ) * Suitability.Gaussian( grow.CellFertility, - gene.OptimalFertility, - gene.FertilityTolerance + traits.OptimalFertility, + traits.FertilityTolerance ); grow.AgeDays += deltaDays * rate; diff --git a/src/LittleSim/Sim/PlantLifecycleSystem.cs b/src/LittleSim/Sim/PlantLifecycleSystem.cs index 7d49d00..1f6ff2c 100644 --- a/src/LittleSim/Sim/PlantLifecycleSystem.cs +++ b/src/LittleSim/Sim/PlantLifecycleSystem.cs @@ -3,6 +3,7 @@ using Friflo.Engine.ECS.Systems; using LittleSim.Content; using Microsoft.Xna.Framework; using MrGameEng.Core; +using MrGameEng.Genetics; using MrGameEng.Graphics; namespace LittleSim.Sim; @@ -31,7 +32,7 @@ public sealed class PlantLifecycleSystem : BaseSystem private readonly bool[] _cellLand; private readonly int _densityCap; private readonly Random _rng; - private readonly ArchetypeQuery _query; + private readonly ArchetypeQuery _query; private readonly int[] _count; private readonly CellRep[] _rep; @@ -65,7 +66,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]; } @@ -99,16 +100,16 @@ public sealed class PlantLifecycleSystem : BaseSystem _candidates.Clear(); _births.Clear(); - foreach (var (growths, genomes, transforms, entities) in _query.Chunks) + foreach (var (growths, organisms, transforms, entities) in _query.Chunks) { var g = growths.Span; - var dna = genomes.Span; + var orgs = organisms.Span; var t = transforms.Span; for (var i = 0; i < g.Length; i++) { ref var grow = ref g[i]; - ref var gene = ref dna[i]; - if (grow.AgeDays > gene.Lifespan) + ref var org = ref orgs[i]; + if (grow.AgeDays > org.Traits.Lifespan) { _deaths.Add(entities.EntityAt(i)); // умер от старости continue; @@ -125,7 +126,8 @@ public sealed class PlantLifecycleSystem : BaseSystem { Species = grow.Species, Mature = mature, - Genome = gene, + Genome = org.Genome, + MutationRate = org.Traits.MutationRate, }; if (mature) { @@ -134,7 +136,8 @@ public sealed class PlantLifecycleSystem : BaseSystem { Cell = cell, Species = grow.Species, - Genome = gene, + Genome = org.Genome, + Traits = org.Traits, } ); } @@ -147,24 +150,29 @@ public sealed class PlantLifecycleSystem : BaseSystem { foreach (var cand in _candidates) { - var interval = MathF.Max(0.5f, cand.Genome.ReproduceInterval); + var interval = MathF.Max(0.5f, cand.Traits.ReproduceInterval); if (_rng.NextSingle() >= elapsed / interval) { continue; // в этот тик не сеет } - var range = Math.Clamp((int)MathF.Round(cand.Genome.DispersalRange), 1, 6); + var range = Math.Clamp((int)MathF.Round(cand.Traits.DispersalRange), 1, 6); var cx = cand.Cell % _width; var cy = cand.Cell / _width; if (!TryFindPartner(cx, cy, range, cand.Species, out var partner)) { - if (_rng.NextSingle() >= cand.Genome.SelfPollination) + if (_rng.NextSingle() >= cand.Traits.SelfPollination) { continue; // нет партнёра и самоопыление не удалось } - partner = cand.Genome; // самоопыление + // самоопыление — партнёр это сам кандидат + partner = new Partner + { + Genome = cand.Genome, + MutationRate = cand.Traits.MutationRate, + }; } if (!TryPickTarget(cx, cy, range, out var tcell)) @@ -172,7 +180,15 @@ public sealed class PlantLifecycleSystem : BaseSystem continue; // некуда сеять (нет суши/свободного места рядом) } - var child = PlantGenome.Breed(cand.Genome, partner, _rng); + // Темп мутаций потомка — средний эволюционирующий признак родителей (ген управляет мутацией). + var mutationChance = (cand.Traits.MutationRate + partner.MutationRate) * 0.5f; + var child = Genome.Breed( + cand.Genome, + partner.Genome, + _plants.GeneRegistry, + _rng, + mutationChance + ); var tx = tcell % _width; var ty = tcell / _width; var position = new Vector2( @@ -192,7 +208,7 @@ public sealed class PlantLifecycleSystem : BaseSystem } } - private bool TryFindPartner(int cx, int cy, int range, int species, out PlantGenome partner) + private bool TryFindPartner(int cx, int cy, int range, int species, out Partner partner) { for (var dy = -range; dy <= range; dy++) { @@ -213,7 +229,7 @@ public sealed class PlantLifecycleSystem : BaseSystem ref var rep = ref _rep[ny * _width + nx]; if (rep.Species == species && rep.Mature) { - partner = rep.Genome; + partner = new Partner { Genome = rep.Genome, MutationRate = rep.MutationRate }; return true; } } @@ -272,21 +288,29 @@ public sealed class PlantLifecycleSystem : BaseSystem { public int Species; public bool Mature; - public PlantGenome Genome; + public Genome Genome; + public float MutationRate; } private struct Candidate { public int Cell; public int Species; - public PlantGenome Genome; + public Genome Genome; + public PlantPhenotype Traits; + } + + private struct Partner + { + public Genome Genome; + public float MutationRate; } private struct Birth { public int Species; public Vector2 Position; - public PlantGenome Genome; + public Genome Genome; public float Fertility; } } diff --git a/src/LittleSim/Sim/PlantOrganism.cs b/src/LittleSim/Sim/PlantOrganism.cs new file mode 100644 index 0000000..69c26e5 --- /dev/null +++ b/src/LittleSim/Sim/PlantOrganism.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using Friflo.Engine.ECS; +using MrGameEng.Genetics; + +namespace LittleSim.Sim; + +/// +/// Растительные признаки (фенотип), вычисленные из генома формулами генов (см. genes.json): +/// готовые значения, которые читают системы роста и жизненного цикла, не касаясь генов напрямую. +/// Имена полей соответствуют именам признаков в эффектах генов. +/// +public struct PlantPhenotype +{ + public float OptimalLight; + public float LightTolerance; + public float OptimalTemperature; + public float TemperatureTolerance; + public float OptimalFertility; + public float FertilityTolerance; + public float Vigor; + public float Lifespan; + public float DispersalRange; + public float ReproduceInterval; + public float SelfPollination; + public float MutationRate; + + /// Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта. + public bool IsVariant; + + /// Собирает фенотип из карты признаков, посчитанной . + public static PlantPhenotype FromTraits(IReadOnlyDictionary traits) + { + float T(string name) => traits.GetValueOrDefault(name); + return new PlantPhenotype + { + OptimalLight = T("optimalLight"), + LightTolerance = T("lightTolerance"), + OptimalTemperature = T("optimalTemperature"), + TemperatureTolerance = T("temperatureTolerance"), + OptimalFertility = T("optimalFertility"), + FertilityTolerance = T("fertilityTolerance"), + Vigor = T("vigor"), + Lifespan = T("lifespan"), + DispersalRange = T("dispersalRange"), + ReproduceInterval = T("reproduceInterval"), + SelfPollination = T("selfPollination"), + MutationRate = T("mutationRate"), + IsVariant = T("variant") >= 0.5f, + }; + } +} + +/// +/// Компонент-организм растения: его управляемый (для размножения, сейва и +/// пересчёта) и выраженный фенотип , по которому работают системы. Заменяет +/// прежний фиксированный PlantGenome — теперь геном переменного состава и признаки из формул +/// генов (фаза G3). Геном — ссылочный объект, поэтому компонент несёт ссылку, а не копию. +/// +public struct PlantOrganism : IComponent +{ + /// Геном особи (аллели по генам); общий источник истины для признаков и размножения. + public Genome Genome; + + /// Выраженные признаки — то, что читают системы роста/жизненного цикла. + public PlantPhenotype Traits; +} From 7fc4bcd3015f139c84d918fdf010ace2b731ee7d Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sat, 13 Jun 2026 03:50:03 +0300 Subject: [PATCH 6/8] =?UTF-8?q?G4:=20gene-driven=20content=20=E2=80=94=20f?= =?UTF-8?q?ruiting,=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; +} From 37d8c976a3c71c5d17d13ebf88de8b3d8fa5a85f Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sat, 13 Jun 2026 04:00:26 +0300 Subject: [PATCH 7/8] G5: bump engine (regex tooling), showcase grouping/patch/validators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Engine pointer -> d044caf (formula gene grouping, content patches, def field validation). Game-side demonstration of all three: - genes.json: GeneHardiness whose effect is gsum('Gene.*Tolerance') — a derived trait summing all tolerance genes via regex grouping. - patches.json: a content patch giving every Bush* plant a wood harvest product, applied to Core's own defs at load. - GameContent registers load-time validators: Gene/Product defNames must carry their type prefix. - Program: a --check-content headless mode that loads all mod defs (running patches + validators) and computes a sample genome's traits (compiling every gene formula, grouping included) — a CI-friendly content lint. Verified: --check-content reports 6 def types, 18 genes, 8 plants, 18 traits with hardiness computed from the gene group. Full suite green. Co-Authored-By: Claude Opus 4.8 --- Mods/Core/Defs/genes.json | 6 ++++++ Mods/Core/Defs/patches.json | 8 ++++++++ engine | 2 +- src/LittleSim/Content/GameContent.cs | 8 ++++++++ src/LittleSim/Program.cs | 27 +++++++++++++++++++++++++++ 5 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 Mods/Core/Defs/patches.json diff --git a/Mods/Core/Defs/genes.json b/Mods/Core/Defs/genes.json index 4252775..247801b 100644 --- a/Mods/Core/Defs/genes.json +++ b/Mods/Core/Defs/genes.json @@ -59,6 +59,12 @@ "default": 0.33, "min": 0, "max": 1, "spread": 0.04, "tags": ["morphology", "color"], "effects": { "leafHue": "value" } }, + // Производный ген: признак собирается группировкой по регэкспу — сумма всех генов-толерантностей + // (демонстрация gsom-функций фазы G5). Собственное значение гена не используется. + { "defName": "GeneHardiness", "parent": "BaseNumericGene", "label": "gene.hardiness", + "default": 0, "min": 0, "max": 1, "spread": 0, "mutationChance": 0, "tags": ["derived"], + "effects": { "hardiness": "gsum('Gene.*Tolerance')" } }, + // Дискретный ген морфы: вариант 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/patches.json b/Mods/Core/Defs/patches.json new file mode 100644 index 0000000..81677c8 --- /dev/null +++ b/Mods/Core/Defs/patches.json @@ -0,0 +1,8 @@ +{ + "type": "Patch", + // Контент-патчи (фаза G5): применяются ко всем дефам типа defType, чьё имя подходит под regex match, + // проставляя поля set. Демонстрация: даём кустам древесину при сборе (веточки). + "patches": [ + { "defType": "Plant", "match": "Bush.*", "set": { "harvestProduct": "ProductWood" } } + ] +} diff --git a/engine b/engine index ead2251..d044caf 160000 --- a/engine +++ b/engine @@ -1 +1 @@ -Subproject commit ead22517ee7a3da4d5d80c29f4061fc36f69089f +Subproject commit d044cafad93177e0fbffb79872bde12afcf03f9c diff --git a/src/LittleSim/Content/GameContent.cs b/src/LittleSim/Content/GameContent.cs index 2ec2b73..9fef03d 100644 --- a/src/LittleSim/Content/GameContent.cs +++ b/src/LittleSim/Content/GameContent.cs @@ -62,6 +62,14 @@ public sealed class GameContent defs.RegisterType("Plant"); defs.RegisterType("Pawn"); defs.RegisterType("WorldPreset"); + // Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа. + defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'"); + defs.RegisterValidator( + "Product", + "defName", + "^Product", + "product defName must start with 'Product'" + ); defs.Load(mods); var languages = new LanguageManager(defaultLanguage: "ru"); diff --git a/src/LittleSim/Program.cs b/src/LittleSim/Program.cs index 597fc93..e663265 100644 --- a/src/LittleSim/Program.cs +++ b/src/LittleSim/Program.cs @@ -1,8 +1,35 @@ +using LittleSim.Content; using LittleSim.Scenes; using Microsoft.Xna.Framework; using MrGameEng.Core; +using MrGameEng.Genetics; using MrGameEng.Host; +// Контент-линт без окна/GPU: грузит моды/дефы (патчи и валидаторы тоже), компилирует формулы генов +// (включая группировку gsum) и выходит. Удобно для CI. +if (args.Contains("--check-content")) +{ + try + { + var content = GameContent.Load(); + var genes = content.Defs.All(); + var registry = genes.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal); + var traits = Phenotype.Compute(Genome.Generate(genes, new Random(1)), registry); + Console.WriteLine( + $"content ok: {content.Defs.TypeKeys.Count} def types, {genes.Count} genes, " + + $"{content.Defs.NamesOf("Plant").Count} plants, {traits.Count} traits " + + $"(hardiness={traits.GetValueOrDefault("hardiness"):0.##})" + ); + return; + } + catch (Exception error) + { + Console.Error.WriteLine($"content FAILED: {error.Message}"); + Environment.Exit(1); + return; + } +} + // Контент Core-мода грузится уже в окне — на загрузочном экране (BootScene), в фоне. using var host = new GameHost( new GameHostOptions From d52eff07f8d7330901233171de9cf0d976272fa5 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sat, 13 Jun 2026 04:09:45 +0300 Subject: [PATCH 8/8] =?UTF-8?q?Remove=20docs/=D0=B3=D0=B5=D0=BD=D1=8B.md?= =?UTF-8?q?=20(gene-system=20design=20doc)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gene system it described (phases G1–G5) is fully implemented; the design doc is no longer needed. Co-Authored-By: Claude Opus 4.8 --- docs/гены.md | 104 --------------------------------------------------- 1 file changed, 104 deletions(-) delete mode 100644 docs/гены.md diff --git a/docs/гены.md b/docs/гены.md deleted file mode 100644 index 60bed8d..0000000 --- a/docs/гены.md +++ /dev/null @@ -1,104 +0,0 @@ -# Гены как фундамент - -Гены — **основополагающая** система LittleSim, общая для растений, животных и людей, а не -структура под один вид. Геном — переменного состава, поэтому возможны произвольные **гибриды** -(хоть человек с генами растения). Поведение организма не зашито в код, а **вычисляется из генов -формулами** через слой признаков. Этот документ описывает модель и порядок реализации (фазы G1–G5). -Дизайн потребителя-растений — в [растения.md](растения.md); гены заменяют его жёсткий `PlantGenome`. - -## Ключевые понятия - -- **`GeneDef`** — отдельный деф на ген, организм-агностичный: id, тип (числовой/дискретный/флаг), - диапазон/дефолт/разброс (для генерации особи), доминантность (дискретные — доминант/рецессив; - числовые — смешение), мутации (вероятность/величина), теги/категория и **эффекты** — вклады гена - в признаки через формулы. -- **Геном** — управляемый объект на сущности: набор генов **переменного состава** (id гена → пара - аллелей, диплоид). Поддерживает добавление/удаление/смешивание генов → гибриды. Скрещивание — - помейозно по объединению генов родителей + мутация. Сериализуется в сейв. -- **Признаки (traits)** — фенотип, вычисляемый из генома + среды: `growthSpeed`, `fruitYield`, - продукт добычи, цвет, период плодоношения, жизнь, расселение… **Системы читают готовые признаки**, - а не гены напрямую. Поэтому одна система плодоношения работает и для дерева, и для человека с - «плодовым» геном; и так «учитываются гены во всех расчётах». -- **Движок формул** — свой (без внешних библиотек) вычислитель выражений в дефах: числа, операции, - функции (`min/max/clamp/lerp/pow/exp/…`), переменные (значения генов/среды/других признаков). - Парсинг → AST → компиляция в делегат; детерминизм; покрыт тестами. -- **Регэкспы в дефах** — три применения: выбор/группировка генов по шаблону имени в формулах - (напр. «сумма всех `leaf_*`»); правила/патчи контента (применять патч ко всем дефам по шаблону); - валидация строковых полей дефов при загрузке. - -## Модель - -``` -GeneDef (данные) ──┐ - ├─► Геном особи (id гена → аллели) ──► [формулы эффектов + среда] ──► Признаки ──► Системы -GeneDef.effects ───┘ (движок формул) (traits) -``` - -- Генерация особи: из базового набора генов вида (деф организма ссылается на `GeneDef` с базовыми - значениями/разбросом) создаются аллели; всё сидируется `Random` мира — детерминизм. -- Признаки кэшируются на особи; пересчитываются при изменении генома (статические признаки) и/или по - среде (динамические — напр. итоговая скорость роста зависит ещё от света/температуры в точке). -- Скрещивание: для каждого гена потомок берёт по аллелю от каждого родителя (если ген есть у обоих; - если только у одного — наследуется с шансом), затем мутация по `GeneDef`. - -## Фазы - -Каждая фаза — отдельным планом (план → утверждение → коммит), как остальные работы. - -### G1 — движок формул (Core) - -Самодостаточный keystone, нужен всему остальному. -- Свой вычислитель выражений: лексер → парсер → AST → компиляция в `Func`. - Поддержка: числа, `+ - * / %`, скобки, унарный минус, сравнения/тернарник (для условий вроде - «плодоносит ли»), функции (`min,max,clamp,lerp,pow,exp,floor,round,abs,…`), переменные по имени. -- Контекст переменных — провайдер `name → value` (значения генов, среды, признаков). -- Детерминизм, без аллокаций при вычислении (компиляция один раз). Тесты на парсинг/вычисление/ошибки. -- Локация: `engine/src/MrGameEng.Core` (или `MrGameEng.Content`, ближе к дефам) — уточнится в плане. - -### G2 — фундамент генов (движок + контент-слой) - -- `GeneDef` (тип дефа) и реестр генов; парсинг эффектов как формул (зависит от G1). -- Управляемый `Genome` (компонент-объект): набор генов с аллелями; генерация из набора вида; - скрещивание + мутация; (де)сериализация. -- Слой признаков: вычисление `traits` из генома и эффектов (формулы) + провайдера среды; кэш и - инвалидация. API `Trait(name)` для систем. -- Без растительной специфики — общая машинерия для любых организмов. - -### G3 — миграция растений на гены/признаки - -- Рост/размножение/жизненный цикл растений читают **признаки** (`growthSpeed`, оптимум/толерантность - света/температуры/почвы, жизнь, расселение, самоопыление, мутации) вместо полей фиксированного - `PlantGenome`. -- `plants.json`/`GeneDef`: набор генов вида описывается данными; убираем жёсткий `PlantGenome`/`Breed`. -- Поведение эквивалентно текущему (фазы A–D), но теперь полностью data-driven через гены/формулы. -- Сейв популяции хранит динамический геном. - -### G4 — генный контент растений - -Новые механики как признаки + небольшие системы: -- **Плодоношение:** признаки «плодоносит ли» (флаг), тип плода (дискретный ген → продукт-деф), число - плодов, период/сезон плодоношения; система создаёт плоды у зрелых растений. -- **Добыча:** что даёт при сборе (дерево/грибы/трава/…) — дискретный ген → продукт; количество — числом. -- **Цвета/морфология:** тинт и вариации спрайта из генов; периоды и скорость роста — уже признаки. - -### G5 — регэкспы и контент-инструменты - -- Группировка генов по шаблону имени в формулах/эффектах (часть G1/G2, выносится сюда как фича). -- **Контент-патчи:** применять патч-деф ко всем дефам, чьё имя подходит под регэксп (массовые правки - модами поверх Core). -- **Валидация:** проверка строковых полей дефов (id/ключи/форматы) по шаблонам при загрузке мода. - -### Позже — животные и люди, гибриды - -Та же машинерия генома/признаков переиспользуется для животных и людей; гибриды — добавлением -«чужих» генов в геном (напр. фотосинтез человеку «растительным» геном, дающим признак, который -читает существующая система). Никаких спец-веток в коде — только новые `GeneDef` и формулы. - -## Принципы - -- **Гены — источник истины; формулы — мост; признаки — то, что читают системы.** Системы не знают - о конкретных видах, только о признаках. -- **Data-driven:** новый ген/эффект/признак — это деф и формула, а не код. -- **Детерминизм:** генерация, скрещивание и мутации — на сидированном `Random`; формулы чисты. -- **Движок vs игра:** движок даёт формулы, `GeneDef`, геном, вычисление признаков, регэксп-инструменты; - игра задаёт конкретные гены/формулы/системы-потребители (плодоношение, добыча) данными и кодом.