Рефактор: стадии роста и спавн — данными

Стадии стали данными: AnimalStageDef + AnimalDef.Stages (возраст входа = EnterAt x якорь maturity/lifespan, масштаб, набор текстур baby/adult, флаг adult для размножения); пусто -> встроенный дефолт Baby/Juvenile/Adult/Senior. AnimalFactory.StageAt/RegionFor/Scale/IsAdult работают по данным стадий; системы роста/решения/гона используют флаг adult вместо индекса. Спавн стал данными: AnimalDef.SpawnPer1000Cells — WorldScene рассыпает ВСЕ виды по плотности, не только захардкоженного оленя. Устраняет блокеры расширяемости #5 и #7.

Сборка чистая, --check-content (9 типов дефов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-14 04:57:44 +03:00
co-authored by Claude Opus 4.8
parent f12b0f170c
commit 5d0cbd2115
7 changed files with 132 additions and 81 deletions
+1
View File
@@ -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,
+8 -6
View File
@@ -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`;
+13
View File
@@ -27,10 +27,22 @@ public sealed class AnimalSet
/// <summary>Спрайт детёныша/подростка (fallback — самка).</summary>
public required Texture2DRegion Baby { get; init; }
/// <summary>Стадии роста (из дефа или встроенный дефолт), по возрастанию возраста входа.</summary>
public required AnimalStageDef[] Stages { get; init; }
/// <summary>Набор генов вида: базовые значения для генерации особи.</summary>
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<AnimalDef, int> _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;
+35
View File
@@ -209,6 +209,33 @@ public class PawnDef : Def
public string Kind { get; init; } = "";
}
/// <summary>
/// Стадия роста животного как ДАННЫЕ (вложенный объект <see cref="AnimalDef.Stages"/>): возраст входа =
/// <see cref="EnterAt"/> × якорь (<see cref="RelativeTo"/> — созревание или продолжительность жизни),
/// масштаб тела и набор текстур ("baby"/"adult"). Стадии перечисляются по возрастанию возраста входа.
/// Модер задаёт любой набор стадий (личинка/куколка/имаго и т.п.) без правки кода.
/// </summary>
public sealed class AnimalStageDef
{
/// <summary>Имя стадии (для тултипов/отладки).</summary>
public string Name { get; init; } = "";
/// <summary>Множитель к якорю: возраст входа = EnterAt × (созревание | продолжительность жизни).</summary>
public float EnterAt { get; init; }
/// <summary>Якорь возраста входа: "maturity" (созревание) или "lifespan" (продолжительность жизни).</summary>
public string RelativeTo { get; init; } = "maturity";
/// <summary>Масштаб размера тела на этой стадии (× ген размера).</summary>
public float Scale { get; init; } = 1f;
/// <summary>Набор текстур: "baby" (детёныш) или "adult" (по полу — самец/самка).</summary>
public string Texture { get; init; } = "adult";
/// <summary>Взрослая стадия — способна размножаться.</summary>
public bool Adult { get; init; }
}
/// <summary>
/// Вид-животное (Defs/animals.json): подтип <see cref="PawnDef"/> (текстура/размер/вид) плюс геном
/// вида. Поведение, здоровье и стадии живут на компонентах сущности (фазы A2+), а не на типе дефа —
@@ -235,6 +262,14 @@ public sealed class AnimalDef : PawnDef
/// Поиск корма гейтится этим списком — модер задаёт рацион без правки кода.
/// </summary>
public string[] Diet { get; init; } = [];
/// <summary>
/// Стадии роста (данные); пусто — встроенный набор Baby→Juvenile→Adult→Senior по умолчанию.
/// </summary>
public AnimalStageDef[] Stages { get; init; } = [];
/// <summary>Плотность стартового спавна: голов на 1000 клеток суши (0 — вид не спавнится сам).</summary>
public float SpawnPer1000Cells { get; init; }
}
/// <summary>
+26 -22
View File
@@ -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<HediffDef>("Rut")));
UpdateSystems.Add(new AnimalRutSystem(_animals, climate, content.Defs.Get<HediffDef>("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<AnimalDef>("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,12 +510,24 @@ public sealed class WorldScene : Scene
}
}
if (landCells == 0)
if (_animals.Count == 0 || landCells == 0)
{
return;
}
var count = Math.Clamp(landCells / 400, 6, 40);
var random = new Random(_config.Seed + 0x5EED);
var spawned = 0;
for (var s = 0; s < _animals.Count; s++)
{
var def = _animals[s].Def;
if (def.SpawnPer1000Cells <= 0f)
{
continue; // вид не спавнится сам (напр. появляется только в сейве/событием)
}
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)
@@ -546,17 +547,20 @@ public sealed class WorldScene : Scene
Store,
_animals,
_needs,
species,
s,
new Vector2(px, py),
ageDays: random.NextSingle() * maxAge,
_animals.GenerateGenome(species, random),
_animals.GenerateGenome(s, random),
generation: 0,
CellSize
);
placed++;
}
Log.Info($"Spawned {placed} deer");
spawned += placed;
}
Log.Info($"Spawned {spawned} animals ({_animals.Count} species)");
}
// Кромка воды: центры клеток суши, граничащих с водой — куда звери ходят пить (вода непроходима).
+20 -26
View File
@@ -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);
/// <summary>Стадии роста животного (фаза A3) по индексам, как в <see cref="AnimalGrowth.Stage"/>.</summary>
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];
/// <summary>Создаёт животное вида <paramref name="species"/> заданного возраста, генома и поколения.</summary>
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);
/// <summary>Стадия по возрасту: Baby &lt; 0.2·созревание, Juvenile &lt; созревание, Adult &lt; 0.8·жизнь, иначе Senior.</summary>
public static int StageAt(float ageDays, float maturity, float lifespan)
/// <summary>Индекс стадии по возрасту: последняя стадия, чей возраст входа (EnterAt × якорь) ≤ возраст.</summary>
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 stage;
}
return ageDays < adultEnd ? StageAdult : StageSenior;
}
/// <summary>Спрайт по стадии и полу: детёныш/подросток — baby-текстура; взрослые — по полу.</summary>
/// <summary>Спрайт стадии: набор "baby" — текстура детёныша; иначе по полу (самец/самка).</summary>
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;
/// <summary>Взрослая ли стадия (способна размножаться) — флаг из данных стадии.</summary>
public static bool IsAdult(AnimalSet.Species sp, int stage) => sp.Stages[stage].Adult;
/// <summary>Масштаб Transform2D для спрайта стадии (размер тела × множитель стадии / ширина региона).</summary>
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;
}
+8 -6
View File
@@ -132,7 +132,7 @@ public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, Need
/// под этим хедифом считает <see cref="AnimalNeedsSystem"/> (нужда Mating с <c>risesUnder=Rut</c>) —
/// сезонность и влечение разнесены: эта система отвечает только за состояние.
/// </summary>
public sealed class AnimalRutSystem(Climate climate, HediffDef rut)
public sealed class AnimalRutSystem(AnimalSet animals, Climate climate, HediffDef rut)
: QuerySystem<AnimalGrowth, AnimalOrganism, Health>
{
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;