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;