diff --git a/Mods/Core/Defs/animals.json b/Mods/Core/Defs/animals.json index 5ebe8ba..fd69df5 100644 --- a/Mods/Core/Defs/animals.json +++ b/Mods/Core/Defs/animals.json @@ -12,6 +12,7 @@ "maleTexture": "things/pawn/animal/deer/DeerMale_east", "babyTexture": "things/pawn/animal/deer/DeerBaby_east", "diet": ["plant"], + "spawnPer1000Cells": 2.5, "genome": { "GeneMaxBodySize": 1.5, "GeneMetabolism": 1.0, diff --git a/docs/животные.md b/docs/животные.md index 6f7bc95..a7b8bf0 100644 --- a/docs/животные.md +++ b/docs/животные.md @@ -10,12 +10,14 @@ > через `Genome.Breed`, наследование пола без YY, поколение+1, потолок численности). Сборка > чистая, `--check-content` (33 гена). > -> **Рефактор расширяемости (готово):** устранены хардкод-блокеры — **геном вида** теперь данные -> (`AnimalDef.Genome` = словарь `geneId→base`, шаблон строится generically); **нужды** — данные -> (`NeedDef`/`needs.json` + `NeedSet` + динамический `AnimalNeeds.Values` + реестр поведений-действий -> по id); **диета** — данные (`AnimalDef.Diet`). Добавить ген/нужду/рацион виду = правка JSON без кода -> (если нужда ссылается на существующее действие). Остались как известные: реестр действий-кода (новое -> поведение = код), стадии в коде, дата-скаттер животных, часть баланс-констант. +> **Рефактор расширяемости (готово):** устранены хардкод-блокеры — данными стали: **геном вида** +> (`AnimalDef.Genome` = словарь `geneId→base`, шаблон строится generically); **нужды** +> (`NeedDef`/`needs.json` + `NeedSet` + динамический `AnimalNeeds.Values` + реестр поведений по id); +> **диета** (`AnimalDef.Diet`); **стадии роста** (`AnimalStageDef`/`AnimalDef.Stages` — возраст входа, +> масштаб, текстуры, флаг «взрослая»; пусто → встроенный дефолт); **спавн** (`AnimalDef.SpawnPer1000Cells` +> — все виды рассыпаются по суше из данных, не только олень). Добавить ген/нужду/рацион/стадию/спавн +> виду = правка JSON без кода. Остаются как известные: реестр действий-кода (новое *поведение* = код, +> сознательная граница), часть глобальных баланс-констант (скорость/выпас/потолок). > > **A3 (готово):** компонент `AnimalGrowth` (стадия + возраст); ген пола `GeneSex` (XX/XY, > генерация основателя без YY через `AnimalSet.GenerateGenome`) и ген `GeneMaturityAge`; diff --git a/src/LittleSim/Content/AnimalSet.cs b/src/LittleSim/Content/AnimalSet.cs index 569f6f1..5bf007b 100644 --- a/src/LittleSim/Content/AnimalSet.cs +++ b/src/LittleSim/Content/AnimalSet.cs @@ -27,10 +27,22 @@ public sealed class AnimalSet /// Спрайт детёныша/подростка (fallback — самка). public required Texture2DRegion Baby { get; init; } + /// Стадии роста (из дефа или встроенный дефолт), по возрастанию возраста входа. + public required AnimalStageDef[] Stages { get; init; } + /// Набор генов вида: базовые значения для генерации особи. public required GenomeTemplate Template { get; init; } } + // Встроенный набор стадий по умолчанию (если вид не задал свой): как было до выноса в данные. + private static readonly AnimalStageDef[] DefaultStages = + [ + new() { Name = "Baby", EnterAt = 0f, RelativeTo = "maturity", Scale = 0.45f, Texture = "baby" }, + new() { Name = "Juvenile", EnterAt = 0.22f, RelativeTo = "maturity", Scale = 0.7f, Texture = "baby" }, + new() { Name = "Adult", EnterAt = 1f, RelativeTo = "maturity", Scale = 1f, Texture = "adult", Adult = true }, + new() { Name = "Senior", EnterAt = 0.8f, RelativeTo = "lifespan", Scale = 0.92f, Texture = "adult", Adult = true }, + ]; + private readonly Species[] _species; private readonly Dictionary _index = new(); @@ -61,6 +73,7 @@ public sealed class AnimalSet Baby = string.IsNullOrEmpty(def.BabyTexture) ? female : atlases.GetRegion(device, def.BabyTexture), + Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages, Template = BuildTemplate(def.Genome, genes), }; _index[def] = i; diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs index 120e85c..72c75bd 100644 --- a/src/LittleSim/Content/GameDefs.cs +++ b/src/LittleSim/Content/GameDefs.cs @@ -209,6 +209,33 @@ public class PawnDef : Def public string Kind { get; init; } = ""; } +/// +/// Стадия роста животного как ДАННЫЕ (вложенный объект ): возраст входа = +/// × якорь ( — созревание или продолжительность жизни), +/// масштаб тела и набор текстур ("baby"/"adult"). Стадии перечисляются по возрастанию возраста входа. +/// Модер задаёт любой набор стадий (личинка/куколка/имаго и т.п.) без правки кода. +/// +public sealed class AnimalStageDef +{ + /// Имя стадии (для тултипов/отладки). + public string Name { get; init; } = ""; + + /// Множитель к якорю: возраст входа = EnterAt × (созревание | продолжительность жизни). + public float EnterAt { get; init; } + + /// Якорь возраста входа: "maturity" (созревание) или "lifespan" (продолжительность жизни). + public string RelativeTo { get; init; } = "maturity"; + + /// Масштаб размера тела на этой стадии (× ген размера). + public float Scale { get; init; } = 1f; + + /// Набор текстур: "baby" (детёныш) или "adult" (по полу — самец/самка). + public string Texture { get; init; } = "adult"; + + /// Взрослая стадия — способна размножаться. + public bool Adult { get; init; } +} + /// /// Вид-животное (Defs/animals.json): подтип (текстура/размер/вид) плюс геном /// вида. Поведение, здоровье и стадии живут на компонентах сущности (фазы A2+), а не на типе дефа — @@ -235,6 +262,14 @@ public sealed class AnimalDef : PawnDef /// Поиск корма гейтится этим списком — модер задаёт рацион без правки кода. /// public string[] Diet { get; init; } = []; + + /// + /// Стадии роста (данные); пусто — встроенный набор Baby→Juvenile→Adult→Senior по умолчанию. + /// + public AnimalStageDef[] Stages { get; init; } = []; + + /// Плотность стартового спавна: голов на 1000 клеток суши (0 — вид не спавнится сам). + public float SpawnPer1000Cells { get; init; } } /// diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index b62c250..5e5b5e4 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -126,9 +126,9 @@ public sealed class WorldScene : Scene RestorePlants(content); } - // Животные (фаза A1): детерминированный начальный спавн по суше. Пока без сейва/симуляции — + // Животные: детерминированный начальный спавн по данным (плотность из дефов). Пока без сейва — // регенерируются из сида и для нового, и для загруженного мира. - SpawnAnimals(content); + SpawnAnimals(); // Освещение: лайтмап (день/ночь × окклюзия от гор/крон + точечные) множится поверх мира, // а Lighting.SampleAt даёт локальный свет системе роста (подлесок под кронами растёт хуже). @@ -215,7 +215,7 @@ public sealed class WorldScene : Scene // нужду (еда выедает растения — контур ёмкости среды). Внешний вид тускнеет с острой нуждой. var shore = ComputeShore(); UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs)); - UpdateSystems.Add(new AnimalRutSystem(climate, content.Defs.Get("Rut"))); + UpdateSystems.Add(new AnimalRutSystem(_animals, climate, content.Defs.Get("Rut"))); UpdateSystems.Add( new AnimalDecisionSystem( Store, @@ -497,21 +497,10 @@ public sealed class WorldScene : Scene } } - // Детерминированный начальный спавн оленей по клеткам суши (фаза A1): количество — лёгкое стадо, - // масштаб по площади суши; геном каждой особи генерируется из шаблона вида (разброс + аллели). - private void SpawnAnimals(GameContent content) + // Детерминированный начальный спавн животных по данным: каждый вид с SpawnPer1000Cells > 0 + // рассыпается по клеткам суши с плотностью из дефа; геном и возраст особи — из шаблона вида. + private void SpawnAnimals() { - if (_animals.Count == 0 || !content.Defs.TryGet("Deer", out var deerDef)) - { - return; - } - - var species = _animals.IndexOf(deerDef); - // Разновозрастное стадо, но не у самой смерти; продолжительность жизни — из данных генома вида. - var maxAge = - (deerDef.Genome.TryGetValue("GeneLifespan", out var lifespan) ? lifespan : 160f) * 0.85f; - var random = new Random(_config.Seed + 0x5EED); - var landCells = 0; for (var i = 0; i < _cellLand.Length; i++) { @@ -521,42 +510,57 @@ public sealed class WorldScene : Scene } } - if (landCells == 0) + if (_animals.Count == 0 || landCells == 0) { return; } - var count = Math.Clamp(landCells / 400, 6, 40); - var placed = 0; - var attempts = 0; - while (placed < count && attempts < count * 50) + var random = new Random(_config.Seed + 0x5EED); + var spawned = 0; + for (var s = 0; s < _animals.Count; s++) { - attempts++; - var cell = random.Next(_cellLand.Length); - if (!_cellLand[cell]) + var def = _animals[s].Def; + if (def.SpawnPer1000Cells <= 0f) { - continue; + 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, - _needs, - species, - new Vector2(px, py), - ageDays: random.NextSingle() * maxAge, - _animals.GenerateGenome(species, random), - generation: 0, - CellSize - ); - placed++; + var count = Math.Max(1, (int)MathF.Round(landCells / 1000f * def.SpawnPer1000Cells)); + var maxAge = + (def.Genome.TryGetValue("GeneLifespan", out var lifespan) ? lifespan : 160f) * 0.85f; + 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, + _needs, + s, + new Vector2(px, py), + ageDays: random.NextSingle() * maxAge, + _animals.GenerateGenome(s, random), + generation: 0, + CellSize + ); + placed++; + } + + spawned += placed; } - Log.Info($"Spawned {placed} deer"); + Log.Info($"Spawned {spawned} animals ({_animals.Count} species)"); } // Кромка воды: центры клеток суши, граничащих с водой — куда звери ходят пить (вода непроходима). diff --git a/src/LittleSim/Sim/AnimalFactory.cs b/src/LittleSim/Sim/AnimalFactory.cs index 8cf5d40..7d1ca87 100644 --- a/src/LittleSim/Sim/AnimalFactory.cs +++ b/src/LittleSim/Sim/AnimalFactory.cs @@ -18,15 +18,6 @@ public static class AnimalFactory private static readonly Color FurWarm = new(255, 226, 188); private static readonly Color FurCool = new(206, 222, 235); - /// Стадии роста животного (фаза A3) по индексам, как в . - public const int StageBaby = 0; - public const int StageJuvenile = 1; - public const int StageAdult = 2; - public const int StageSenior = 3; - - // Масштаб спрайта по стадии (множитель размера тела): детёныш мельче, старик чуть усох. - private static readonly float[] StageScales = [0.45f, 0.7f, 1f, 0.92f]; - /// Создаёт животное вида заданного возраста, генома и поколения. public static Entity Create( EntityStore store, @@ -43,7 +34,7 @@ public static class AnimalFactory var sp = animals[species]; var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, animals.GeneRegistry)); var isMale = IsMaleGenome(genome); - var stage = StageAt(ageDays, traits.MaturityAge, traits.Lifespan); + var stage = StageAt(ageDays, traits.MaturityAge, traits.Lifespan, sp.Stages); var region = RegionFor(sp, stage, isMale); var sprite = new Sprite(region, GameLayers.Beings); @@ -78,31 +69,33 @@ public static class AnimalFactory public static bool IsMaleGenome(Genome genome) => genome.Has("GeneSex") && (genome["GeneSex"].A > 0.5f || genome["GeneSex"].B > 0.5f); - /// Стадия по возрасту: Baby < 0.2·созревание, Juvenile < созревание, Adult < 0.8·жизнь, иначе Senior. - public static int StageAt(float ageDays, float maturity, float lifespan) + /// Индекс стадии по возрасту: последняя стадия, чей возраст входа (EnterAt × якорь) ≤ возраст. + public static int StageAt(float ageDays, float maturity, float lifespan, AnimalStageDef[] stages) { - var babyEnd = MathF.Max(1f, maturity * 0.2f); - var juvEnd = MathF.Max(babyEnd + 1f, maturity); - var adultEnd = MathF.Max(juvEnd + 1f, lifespan * 0.8f); - if (ageDays < babyEnd) + var stage = 0; + for (var i = 1; i < stages.Length; i++) { - return StageBaby; + var anchor = string.Equals(stages[i].RelativeTo, "lifespan", StringComparison.Ordinal) + ? lifespan + : maturity; + if (ageDays >= stages[i].EnterAt * anchor) + { + stage = i; + } } - if (ageDays < juvEnd) - { - return StageJuvenile; - } - - return ageDays < adultEnd ? StageAdult : StageSenior; + return stage; } - /// Спрайт по стадии и полу: детёныш/подросток — baby-текстура; взрослые — по полу. + /// Спрайт стадии: набор "baby" — текстура детёныша; иначе по полу (самец/самка). public static Texture2DRegion RegionFor(AnimalSet.Species sp, int stage, bool isMale) => - stage <= StageJuvenile ? sp.Baby + string.Equals(sp.Stages[stage].Texture, "baby", StringComparison.Ordinal) ? sp.Baby : isMale ? sp.Male : sp.Female; + /// Взрослая ли стадия (способна размножаться) — флаг из данных стадии. + public static bool IsAdult(AnimalSet.Species sp, int stage) => sp.Stages[stage].Adult; + /// Масштаб Transform2D для спрайта стадии (размер тела × множитель стадии / ширина региона). public static float Scale( AnimalSet.Species sp, @@ -112,7 +105,8 @@ public static class AnimalFactory int cellSize ) { - var sizeCells = (traits.BodySize > 0f ? traits.BodySize : sp.Def.SizeCells) * StageScales[stage]; + var sizeCells = + (traits.BodySize > 0f ? traits.BodySize : sp.Def.SizeCells) * sp.Stages[stage].Scale; return cellSize * sizeCells / region.Width; } diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs index 28ebc7e..b6200de 100644 --- a/src/LittleSim/Sim/AnimalSystems.cs +++ b/src/LittleSim/Sim/AnimalSystems.cs @@ -132,7 +132,7 @@ public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, Need /// под этим хедифом считает (нужда Mating с risesUnder=Rut) — /// сезонность и влечение разнесены: эта система отвечает только за состояние. /// -public sealed class AnimalRutSystem(Climate climate, HediffDef rut) +public sealed class AnimalRutSystem(AnimalSet animals, Climate climate, HediffDef rut) : QuerySystem { protected override void OnUpdate() @@ -146,7 +146,8 @@ public sealed class AnimalRutSystem(Climate climate, HediffDef rut) for (var i = 0; i < g.Length; i++) { var inRut = - g[i].Stage >= AnimalFactory.StageAdult && season == o[i].Traits.BreedingSeason; + AnimalFactory.IsAdult(animals[o[i].Species], g[i].Stage) + && season == o[i].Traits.BreedingSeason; if (inRut) { h[i].State.Add(rut); @@ -255,7 +256,7 @@ public sealed class AnimalDecisionSystem : BaseSystem brain.DecideIn = Interval + _rng.NextSingle() * Interval; // джиттер — расфазировать скан var pos = t[i].Position; - var adult = gr[i].Stage >= AnimalFactory.StageAdult; + var adult = AnimalFactory.IsAdult(_animalSet[o[i].Species], gr[i].Stage); var name = _brain.Select(new AnimalContext(n[i].Values))?.Name ?? AnimalActions.Wander; @@ -391,7 +392,7 @@ public sealed class AnimalDecisionSystem : BaseSystem var tt = transforms.Span; for (var i = 0; i < tt.Length; i++) { - if (oo[i].IsMale == selfMale || gg[i].Stage < AnimalFactory.StageAdult) + if (oo[i].IsMale == selfMale || !AnimalFactory.IsAdult(_animalSet[oo[i].Species], gg[i].Stage)) { continue; // тот же пол / не взрослый (self отсеивается по полу) } @@ -719,10 +720,12 @@ public sealed class AnimalGrowthSystem( ref var grow = ref g[i]; grow.AgeDays += days; ref readonly var org = ref o[i]; + var sp = animals[org.Species]; var stage = AnimalFactory.StageAt( grow.AgeDays, org.Traits.MaturityAge, - org.Traits.Lifespan + org.Traits.Lifespan, + sp.Stages ); if (stage == grow.Stage) { @@ -730,7 +733,6 @@ public sealed class AnimalGrowthSystem( } grow.Stage = stage; - var sp = animals[org.Species]; var region = AnimalFactory.RegionFor(sp, stage, org.IsMale); ref var sprite = ref s[i]; sprite.Region = region;