From 5e61491960a2ec78ea88efe6e4c107b37fd18284 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sun, 14 Jun 2026 04:03:51 +0300 Subject: [PATCH] =?UTF-8?q?=D0=96=D0=B8=D0=B2=D0=BE=D1=82=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20A1:=20=D1=84=D1=83=D0=BD=D0=B4=D0=B0=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=20=E2=80=94=20AnimalDef,=20=D0=B3=D0=B5=D0=BD=D1=8B=20?= =?UTF-8?q?=D0=B7=D0=B2=D0=B5=D1=80=D1=8F,=20=D1=81=D0=BF=D0=B0=D0=B2?= =?UTF-8?q?=D0=BD=20=D0=BE=D0=BB=D0=B5=D0=BD=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Новый тип дефа Animal (AnimalDef : PawnDef) + animals.json (Deer). 8 организм-агностичных животных генов в genes.json. AnimalPhenotype/AnimalOrganism, AnimalSet (шаблон генома вида) и AnimalFactory — зеркала растительных PlantSet/PlantFactory. Детерминированный спавн стада оленей по суше в WorldScene с рендером и тинтом меха по гену; консольная команда 'animal '. Проверено: сборка чистая, --check-content (7 типов дефов, 28 генов). Co-Authored-By: Claude Opus 4.8 --- Mods/Core/Defs/animals.json | 26 +++++++ Mods/Core/Defs/genes.json | 26 +++++++ src/LittleSim/Content/AnimalSet.cs | 97 +++++++++++++++++++++++++ src/LittleSim/Content/GameContent.cs | 1 + src/LittleSim/Content/GameDefs.cs | 52 +++++++++++++- src/LittleSim/Scenes/WorldScene.cs | 103 +++++++++++++++++++++++++++ src/LittleSim/Sim/AnimalFactory.cs | 62 ++++++++++++++++ src/LittleSim/Sim/AnimalOrganism.cs | 79 ++++++++++++++++++++ 8 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 Mods/Core/Defs/animals.json create mode 100644 src/LittleSim/Content/AnimalSet.cs create mode 100644 src/LittleSim/Sim/AnimalFactory.cs create mode 100644 src/LittleSim/Sim/AnimalOrganism.cs diff --git a/Mods/Core/Defs/animals.json b/Mods/Core/Defs/animals.json new file mode 100644 index 0000000..f7a8a4d --- /dev/null +++ b/Mods/Core/Defs/animals.json @@ -0,0 +1,26 @@ +{ + "type": "Animal", + // Виды-животные (фаза A1): подтип Pawn (kind=animal) + базовый геном вида из общих Gene-дефов + // (см. genes.json). Особь при спавне получает аллели вокруг этих баз, фенотип — формулами генов. + // Пол, стадии роста, размножение, здоровье и поведение приходят в фазах A2+. + "defs": [ + { + "defName": "Deer", + "label": "pawn.deer", + "kind": "animal", + "texture": "things/pawn/animal/deer/DeerFemale_east", + "genome": { + "bodySize": 1.5, + "metabolism": 1.0, + "moveSpeed": 1.2, + "bloodVolume": 1.4, + "vision": 1.3, + "brainSize": 0.45, + "insulation": 0.55, + "furColor": 0.45, + "lifespan": 360, + "spread": 0.08 + } + } + ] +} diff --git a/Mods/Core/Defs/genes.json b/Mods/Core/Defs/genes.json index 5610511..cc17691 100644 --- a/Mods/Core/Defs/genes.json +++ b/Mods/Core/Defs/genes.json @@ -74,6 +74,32 @@ "default": 0, "min": 0, "max": 1, "spread": 0, "mutationChance": 0, "tags": ["derived"], "effects": { "hardiness": "gsum('Gene.*Tolerance')" } }, + // --- Животные (фаза A1): организм-агностичные гены зверя (их несёт геном животного) --- + { "defName": "GeneMaxBodySize", "parent": "BaseNumericGene", "label": "gene.bodySize", + "default": 1.0, "min": 0.1, "max": 4.0, "tags": ["animal", "body"], + "effects": { "bodySize": "value" } }, + { "defName": "GeneMetabolism", "parent": "BaseNumericGene", "label": "gene.metabolism", + "default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "metabolism"], + "effects": { "metabolism": "value" } }, + { "defName": "GeneMoveSpeed", "parent": "BaseNumericGene", "label": "gene.moveSpeed", + "default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "movement"], + "effects": { "moveSpeed": "value" } }, + { "defName": "GeneBloodVolume", "parent": "BaseNumericGene", "label": "gene.bloodVolume", + "default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "health"], + "effects": { "bloodVolume": "value" } }, + { "defName": "GeneVision", "parent": "BaseNumericGene", "label": "gene.vision", + "default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "senses"], + "effects": { "vision": "value" } }, + { "defName": "GeneBrainSize", "parent": "BaseNumericGene", "label": "gene.brainSize", + "default": 0.45, "min": 0.0, "max": 1.0, "tags": ["animal", "brain"], + "effects": { "brainSize": "value" } }, + { "defName": "GeneInsulation", "parent": "BaseNumericGene", "label": "gene.insulation", + "default": 0.5, "min": 0.0, "max": 1.0, "tags": ["animal", "temperature"], + "effects": { "insulation": "value" } }, + { "defName": "GeneFurColor", "parent": "BaseNumericGene", "label": "gene.furColor", + "default": 0.5, "min": 0.0, "max": 1.0, "spread": 0.06, "tags": ["animal", "morphology", "color"], + "effects": { "furHue": "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/src/LittleSim/Content/AnimalSet.cs b/src/LittleSim/Content/AnimalSet.cs new file mode 100644 index 0000000..f9f66aa --- /dev/null +++ b/src/LittleSim/Content/AnimalSet.cs @@ -0,0 +1,97 @@ +using Microsoft.Xna.Framework.Graphics; +using MrGameEng.Genetics; +using MrGameEng.Graphics; + +namespace LittleSim.Content; + +/// +/// Готовая (resolved) таблица животных для сцены: по каждому — загруженный +/// регион атласа и набор генов вида () для генерации особей. Строится +/// один раз при загрузке сцены (нужны атлас и устройство), как . Системы и +/// фабрика индексируются в неё без словарей. Фаза A1 — один спрайт на вид; стадии/пол придут позже. +/// +public sealed class AnimalSet +{ + /// Вид животного: его деф, спрайт и набор генов для генерации особи. + public sealed class Species + { + /// Исходный деф животного. + public required AnimalDef Def { get; init; } + + /// Спрайт вида (фаза A1 — единый; стадии/пол позже). + public required Texture2DRegion Region { get; init; } + + /// Набор генов вида: базовые значения для генерации особи. + public required GenomeTemplate Template { get; init; } + } + + private readonly Species[] _species; + private readonly Dictionary _index = new(); + + /// Общий реестр генов (по id) для скрещивания и вычисления признаков животных. + public IReadOnlyDictionary GeneRegistry { get; } + + /// Строит таблицу из всех (не-abstract) мода. + public AnimalSet(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, + Region = atlases.GetRegion(device, def.Texture), + Template = BuildTemplate(def.Genome, genes), + }; + _index[def] = i; + } + } + + /// Число видов в таблице. + public int Count => _species.Length; + + /// Вид по индексу. + public Species this[int index] => _species[index]; + + /// Индекс вида по дефу. + public int IndexOf(AnimalDef def) => _index[def]; + + // Переводит базовый геном вида (числа из animals.json) в набор генов: каждое число — центр + // аллелей соответствующего общего GeneDef. Зеркало PlantSet.BuildTemplate. + private static GenomeTemplate BuildTemplate( + AnimalGenomeDef 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("GeneMaxBodySize", g.BodySize), + Numeric("GeneMetabolism", g.Metabolism), + Numeric("GeneMoveSpeed", g.MoveSpeed), + Numeric("GeneBloodVolume", g.BloodVolume), + Numeric("GeneVision", g.Vision), + new GenomeTemplate.Entry(Gene("GeneBrainSize"), g.BrainSize, 0.03f), + Numeric("GeneInsulation", g.Insulation), + new GenomeTemplate.Entry(Gene("GeneFurColor"), g.FurColor, 0.06f), + Numeric("GeneLifespan", g.Lifespan), + ] + ); + } +} diff --git a/src/LittleSim/Content/GameContent.cs b/src/LittleSim/Content/GameContent.cs index 07fdee2..7643072 100644 --- a/src/LittleSim/Content/GameContent.cs +++ b/src/LittleSim/Content/GameContent.cs @@ -63,6 +63,7 @@ public sealed class GameContent defs.RegisterType("Product"); defs.RegisterType("Plant"); defs.RegisterType("Pawn"); + defs.RegisterType("Animal"); defs.RegisterType("WorldPreset"); // Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа. defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'"); diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs index 81b2442..9842294 100644 --- a/src/LittleSim/Content/GameDefs.cs +++ b/src/LittleSim/Content/GameDefs.cs @@ -191,7 +191,7 @@ public sealed class WorldPresetDef : Def } /// Существо (Defs/pawns.json): текстура, размер и вид (житель/животное). -public sealed class PawnDef : Def +public class PawnDef : Def { /// Вид существа: жители ("being") живут в мировой сцене, животные ("animal") — в террейне. public const string KindBeing = "being"; @@ -208,3 +208,53 @@ public sealed class PawnDef : Def /// Вид: или . public string Kind { get; init; } = ""; } + +/// +/// Базовый геном вида-животного (вложенный объект ): центры аллелей +/// животных генов (см. genes.json). Особь при спавне получает аллели рядом с этими базами +/// (разброс — ), а фенотип считается формулами генов над аллелями. Зеркало +/// растительного , только для зверя. +/// +public sealed class AnimalGenomeDef +{ + /// Размер тела в клетках — масштаб спрайта и (позже) HP/расхода энергии. + public float BodySize { get; init; } = 1f; + + /// Скорость обмена веществ — множитель расхода нужд (голод/жажда). + public float Metabolism { get; init; } = 1f; + + /// Скорость передвижения (множитель). + public float MoveSpeed { get; init; } = 1f; + + /// Объём крови (множитель) — масштаб скорости кровопотери в системе здоровья. + public float BloodVolume { get; init; } = 1f; + + /// Дальность зрения (множитель радиуса поиска корма/воды/партнёра). + public float Vision { get; init; } = 1f; + + /// Размер мозга (0..1) — основа интеллекта (гейт нужд/действий). + public float BrainSize { get; init; } = 0.45f; + + /// Теплоизоляция мехом (0..1) — терморегуляция. + public float Insulation { get; init; } = 0.5f; + + /// Оттенок меха (0..1) — сдвиг тинта спрайта (как leafHue у растений). + public float FurColor { get; init; } = 0.5f; + + /// Продолжительность жизни (игровых дней) до смерти от старости. + public float Lifespan { get; init; } = 160f; + + /// Доля разброса аллелей вокруг базы при генерации особи. + public float Spread { get; init; } = 0.08f; +} + +/// +/// Вид-животное (Defs/animals.json): подтип (текстура/размер/вид) плюс базовый +/// геном вида. Поведение, здоровье и стадии живут на компонентах сущности (фазы A2+), а не на типе +/// дефа — существующие 9 животных- остаются валидными. +/// +public sealed class AnimalDef : PawnDef +{ + /// Базовый геном вида: центры аллелей животных генов. + public AnimalGenomeDef Genome { get; init; } = new(); +} diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 5b56a1b..238cfb8 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -65,6 +65,7 @@ public sealed class WorldScene : Scene private readonly List _speedRefreshers = []; private PlantSet _plants = null!; + private AnimalSet _animals = null!; private float[] _cellFertility = []; private bool[] _cellLand = []; private bool[] _cellOccluderBase = []; // горы (статично из террейна) @@ -109,6 +110,7 @@ public sealed class WorldScene : Scene // Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва. _plants = new PlantSet(content, atlases, device); + _animals = new AnimalSet(content, atlases, device); var loadingPlants = _save?.Plants is { Count: > 0 }; BuildTerrain( content, @@ -122,6 +124,10 @@ public sealed class WorldScene : Scene RestorePlants(content); } + // Животные (фаза A1): детерминированный начальный спавн по суше. Пока без сейва/симуляции — + // регенерируются из сида и для нового, и для загруженного мира. + SpawnAnimals(content); + // Освещение: лайтмап (день/ночь × окклюзия от гор/крон + точечные) множится поверх мира, // а Lighting.SampleAt даёт локальный свет системе роста (подлесок под кронами растёт хуже). var lighting = this.UseLighting( @@ -454,6 +460,64 @@ public sealed class WorldScene : Scene } } + // Детерминированный начальный спавн оленей по клеткам суши (фаза A1): количество — лёгкое стадо, + // масштаб по площади суши; геном каждой особи генерируется из шаблона вида (разброс + аллели). + private void SpawnAnimals(GameContent content) + { + if (_animals.Count == 0 || !content.Defs.TryGet("Deer", out var deerDef)) + { + return; + } + + var species = _animals.IndexOf(deerDef); + var template = _animals[species].Template; + var random = new Random(_config.Seed + 0x5EED); + + var landCells = 0; + for (var i = 0; i < _cellLand.Length; i++) + { + if (_cellLand[i]) + { + landCells++; + } + } + + if (landCells == 0) + { + return; + } + + var count = Math.Clamp(landCells / 400, 6, 40); + var placed = 0; + var attempts = 0; + while (placed < count && attempts < count * 50) + { + attempts++; + var cell = random.Next(_cellLand.Length); + if (!_cellLand[cell]) + { + continue; + } + + var cx = cell % _config.Width; + var cy = cell / _config.Width; + var px = (cx + 0.2f + random.NextSingle() * 0.6f) * CellSize; + var py = (cy + 0.2f + random.NextSingle() * 0.6f) * CellSize; + AnimalFactory.Create( + Store, + _animals, + species, + new Vector2(px, py), + template.Generate(random), + generation: 0, + CellSize + ); + placed++; + } + + Log.Info($"Spawned {placed} deer"); + } + // Текущая сетка окклюдеров для лайтмапа: статичные горы + клетки со зрелыми деревьями. private bool[] BuildOccluders() { @@ -552,6 +616,11 @@ public sealed class WorldScene : Scene "plant [seed] — sample a species genome and show its gene-driven traits/products", (c, args) => RunPlantDemo(c, content, args) ); + console.Register( + "animal", + "animal [seed] — sample an animal genome and show its gene-driven traits", + (c, args) => RunAnimalDemo(c, content, args) + ); console.Register( "menu", "menu — return to the main menu", @@ -659,6 +728,40 @@ public sealed class WorldScene : Scene } } + // Демонстрация животного (фаза A1): по виду берём его набор генов, генерируем особь и печатаем + // признаки, вычисленные формулами генов (тело/мозг/метаболизм/чувства/мех). + private void RunAnimalDemo(DevConsole console, GameContent content, string[] args) + { + var species = content.Defs.NamesOf("Animal"); + if (args.Length == 0) + { + console.WriteLine("usage: animal [seed] e.g. animal Deer"); + console.WriteLine($"species: {string.Join(", ", species)}"); + return; + } + + if (!content.Defs.TryGet(args[0], out var def)) + { + console.WriteLine($"no animal '{args[0]}'; species: {string.Join(", ", species)}"); + return; + } + + var index = _animals.IndexOf(def); + var seed = args.Length > 1 ? int.Parse(args[1]) : Random.Shared.Next(); + var genome = _animals[index].Template.Generate(new Random(seed)); + var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, _animals.GeneRegistry)); + + console.WriteLine($"{def.DefName} (seed {seed}):"); + console.WriteLine( + $" body {traits.BodySize:0.##} cells, lifespan {traits.Lifespan:0} d, " + + $"brain {traits.BrainSize:0.##}, metabolism {traits.Metabolism:0.##}" + ); + console.WriteLine( + $" move {traits.MoveSpeed:0.##}, vision {traits.Vision:0.##}, blood {traits.BloodVolume:0.##}, " + + $"insulation {traits.Insulation:0.##}, furHue {traits.FurHue:0.##}" + ); + } + private static string ProductLabel(GameContent content, string productDefName) => content.Defs.TryGet(productDefName, out var product) ? content.Languages.Get(product.Label) diff --git a/src/LittleSim/Sim/AnimalFactory.cs b/src/LittleSim/Sim/AnimalFactory.cs new file mode 100644 index 0000000..ea7d312 --- /dev/null +++ b/src/LittleSim/Sim/AnimalFactory.cs @@ -0,0 +1,62 @@ +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 AnimalFactory +{ + // Якоря оттенка меха: тёплый рыжевато-песочный → холодный серо-голубой (по гену furColor). + private static readonly Color FurWarm = new(255, 226, 188); + private static readonly Color FurCool = new(206, 222, 235); + + /// Создаёт животное вида с заданным геномом и поколением. + public static Entity Create( + EntityStore store, + AnimalSet animals, + int species, + Vector2 position, + Genome genome, + int generation, + int cellSize + ) + { + var sp = animals[species]; + var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, animals.GeneRegistry)); + + var sprite = new Sprite(sp.Region, GameLayers.Beings); + sprite.CenterOrigin(); + sprite.Color = Tint(traits); + + // Размер спрайта задаёт ген размера тела; деф-fallback — на случай отсутствия гена. + var sizeCells = traits.BodySize > 0f ? traits.BodySize : sp.Def.SizeCells; + + return store.CreateEntity( + new Transform2D(position, scale: new Vector2(cellSize * sizeCells / sp.Region.Width)), + sprite, + new AnimalOrganism + { + Genome = genome, + Traits = traits, + Species = species, + Generation = generation, + } + ); + } + + // Цвет животного из генов: лёгкий тёплый/холодный сдвиг меха (ген furHue) поверх текстуры — + // субтильный, чтобы не перекрашивать уже цветной спрайт зверя (как leafHue у растений). + private static Color Tint(in AnimalPhenotype traits) + { + var fur = Color.Lerp(FurWarm, FurCool, Math.Clamp(traits.FurHue, 0f, 1f)); + return Color.Lerp(Color.White, fur, 0.5f); + } +} diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs new file mode 100644 index 0000000..80b3387 --- /dev/null +++ b/src/LittleSim/Sim/AnimalOrganism.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using Friflo.Engine.ECS; +using MrGameEng.Genetics; + +namespace LittleSim.Sim; + +/// +/// Признаки (фенотип) животного, вычисленные из генома формулами генов (см. genes.json): +/// готовые значения, которые читают системы, не касаясь генов напрямую. Зеркало +/// . Имена полей соответствуют именам признаков в эффектах генов. +/// +public struct AnimalPhenotype +{ + /// Размер тела в клетках (масштаб спрайта; позже — HP/расход энергии). + public float BodySize; + + /// Скорость обмена веществ — множитель расхода нужд. + public float Metabolism; + + /// Скорость передвижения (множитель). + public float MoveSpeed; + + /// Объём крови (множитель) — масштаб скорости кровопотери. + public float BloodVolume; + + /// Дальность зрения (множитель радиуса поиска). + public float Vision; + + /// Размер мозга (0..1) — основа интеллекта. + public float BrainSize; + + /// Теплоизоляция мехом (0..1). + public float Insulation; + + /// Оттенок меха (0..1) — сдвиг тинта спрайта. + public float FurHue; + + /// Продолжительность жизни (игровых дней). + public float Lifespan; + + /// Собирает фенотип из карты признаков, посчитанной . + public static AnimalPhenotype FromTraits(IReadOnlyDictionary traits) + { + float T(string name) => traits.GetValueOrDefault(name); + return new AnimalPhenotype + { + BodySize = T("bodySize"), + Metabolism = T("metabolism"), + MoveSpeed = T("moveSpeed"), + BloodVolume = T("bloodVolume"), + Vision = T("vision"), + BrainSize = T("brainSize"), + Insulation = T("insulation"), + FurHue = T("furHue"), + Lifespan = T("lifespan"), + }; + } +} + +/// +/// Компонент-организм животного: управляемый (для размножения, сейва и +/// пересчёта), выраженный фенотип , индекс вида в +/// и номер поколения (для popstats и видимого дрейфа). Геном — ссылочный объект, поэтому +/// компонент несёт ссылку, а не копию (как у растений). Зеркало . +/// +public struct AnimalOrganism : IComponent +{ + /// Геном особи (аллели по генам); источник истины для признаков и размножения. + public Genome Genome; + + /// Выраженные признаки — то, что читают системы. + public AnimalPhenotype Traits; + + /// Индекс вида в . + public int Species; + + /// Номер поколения: max(родителей) + 1; у основателей 0. + public int Generation; +}