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;
+}