diff --git a/docs/животные.md b/docs/животные.md index d82e7f9..8acaca9 100644 --- a/docs/животные.md +++ b/docs/животные.md @@ -1,8 +1,19 @@ # Животные (зоология) -> Статус: **A1–A8 реализованы целиком** — базовый план зоологии закрыт; дальше экологический горизонт -> (хищники, опека, эпидемии, миграция) и бэклог реализма. Это живой план — стиль и правила как в -> [концепте](concept.md) и [модах](mods.md). +> Статус: **A1–A8 реализованы целиком + сериализация животных** — базовый план зоологии и его сквозное +> требование сейв/лоада закрыты; дальше экологический горизонт (хищники, опека, эпидемии, миграция) и +> бэклог реализма. Это живой план — стиль и правила как в [концепте](concept.md) и [модах](mods.md). +> +> **Сериализация животных (готово): сейв/лоад популяции и трупов.** `WorldSave` получил `Animals` +> (`AnimalSave`: геном→признаки+пол, поколение, стадия/возраст, нужды+истощение, здоровье — кровь/HP +> частей/хедифы тяжесть+иммунитет, настроение+мысли, беременность+геном отца) и `Corpses` (`CorpseSave`: +> вид/позиция/таймер-стадия разложения/мясо). `SaveAnimals` снимает популяцию (Health/Mood/Pregnant — через +> entity, т.к. запрос ≤5 типов); `RestoreAnimals`/`RestoreAnimalState` собирают особь фабрикой из генома и +> накладывают динамическое состояние (ref-правки до структурного `AddComponent(Pregnant)`; труп +> пере-тинтится через `Stage=-1`). `Corpse` получил поле `Species` (для спрайта/разделки); `CreateCorpse` +> теперь принимает индекс вида. На загрузке стадо НЕ пересыпается, если в сейве есть животные; старый/пустой +> сейв — регенерируется из сида (не крашит). Эволюция/болезни/беременности переживают сейв/лоад. Сборка + +> `--check-content` чистые. > > **A8 часть 2 (готово): инспектор животных.** `InspectPanel` теперь осматривает и животных (не только > растения): вкладки **Обзор** (стадия/возраст/пол/поколение/беременность), **Нужды** (значения нужд %), diff --git a/src/LittleSim/App/WorldSave.cs b/src/LittleSim/App/WorldSave.cs index 871c3f3..ce1b9e8 100644 --- a/src/LittleSim/App/WorldSave.cs +++ b/src/LittleSim/App/WorldSave.cs @@ -62,6 +62,117 @@ public sealed class PawnSave public float ChangeIn { get; set; } } +/// Сериализуемый хедиф особи (фаза A6): деф (по имени), тяжесть и набранный иммунитет. +public sealed class HediffSave +{ + /// Имя дефа хедифа (гон/болезнь). + public string DefName { get; set; } = ""; + + /// Текущая тяжесть. + public float Severity { get; set; } + + /// Набранный иммунитет [0,1]. + public float Immunity { get; set; } +} + +/// Сериализуемая событийная мысль (фаза A8): деф (по имени) и остаток времени до угасания. +public sealed class ThoughtSave +{ + /// Имя дефа мысли. + public string DefName { get; set; } = ""; + + /// Сколько игровых дней мысль ещё действует. + public float RemainingDays { get; set; } +} + +/// +/// Сериализуемое состояние одного животного (фазы A1–A8): вид, позиция, геном (источник признаков и +/// пола), поколение, стадия/возраст, нужды, здоровье (кровь, HP частей, хедифы), настроение и +/// беременность. Спрайт пересобирается фабрикой из дефа. Чтобы популяция (эволюция/состояние) +/// переживала сейв/лоад, а не пересыпалась заново из сида. +/// +public sealed class AnimalSave +{ + /// Имя дефа вида — по нему берётся индекс/стадии/текстуры/тело. + public string Species { get; set; } = ""; + + /// Позиция X в мировых координатах. + public float X { get; set; } + + /// Позиция Y в мировых координатах. + public float Y { get; set; } + + /// Номер поколения (для popstats/дрейфа). + public int Generation { get; set; } + + /// Текущая стадия роста. + public int Stage { get; set; } + + /// Накопленный возраст в игровых днях. + public float AgeDays { get; set; } + + /// Геном особи: аллели по id генов (переменного состава; несёт и пол через GeneSex). + public Dictionary Genome { get; set; } = new(); + + /// Значения нужд по индексам NeedSet. + public float[] Needs { get; set; } = []; + + /// Накоплено дней истощения. + public float Starve { get; set; } + + /// Уровень крови [0,1]. + public float BloodLevel { get; set; } = 1f; + + /// HP частей тела по порядку анатомии вида (пусто — у вида нет тела). + public float[] PartHp { get; set; } = []; + + /// Активные хедифы (гон/болезни). + public List Hediffs { get; set; } = []; + + /// Текущее настроение [0,1]. + public float Mood { get; set; } + + /// Активно ли настроение (тир 4+). + public bool MoodActive { get; set; } + + /// Активные событийные мысли. + public List Thoughts { get; set; } = []; + + /// Беременна ли самка. + public bool Pregnant { get; set; } + + /// Геном отца (если беременна) — «слепок» на момент зачатия. + public Dictionary FatherGenome { get; set; } = new(); + + /// Остаток срока вынашивания (игровых дней). + public float DueInDays { get; set; } + + /// Размер помёта. + public int Litter { get; set; } +} + +/// Сериализуемый труп (фаза A5): вид (для спрайта), позиция, таймер/стадия разложения, мясо. +public sealed class CorpseSave +{ + /// Имя дефа вида — по нему берётся спрайт трупа. + public string Species { get; set; } = ""; + + /// Позиция X в мировых координатах. + public float X { get; set; } + + /// Позиция Y в мировых координатах. + public float Y { get; set; } + + /// Накоплено игровых дней разложения. + public float RotDays { get; set; } + + /// Размер тела (для масштаба спрайта). + public float BodySize { get; set; } + + /// Доступное мясо. + public float Meat { get; set; } +} + /// /// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание), /// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он @@ -99,6 +210,12 @@ public sealed class WorldSave /// Снимок всех растений (геномы, позиции, возраст, стадия). public List Plants { get; set; } = []; + /// Снимок всех животных (геном, стадия/возраст, нужды, здоровье, настроение, беременность). + public List Animals { get; set; } = []; + + /// Снимок трупов (вид, позиция, таймер/стадия разложения, мясо). + public List Corpses { get; set; } = []; + /// Конфиг мира для пересоздания сцены. public WorldConfig ToConfig() => new() diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 6a38477..b47bb86 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -126,9 +126,16 @@ public sealed class WorldScene : Scene RestorePlants(content); } - // Животные: детерминированный начальный спавн по данным (плотность из дефов). Пока без сейва — - // регенерируются из сида и для нового, и для загруженного мира. - SpawnAnimals(); + // Животные: из сейва (сохранённая популяция — эволюция/состояние не теряются) либо + // детерминированный спавн из сида (новый мир или старый сейв без животных). + if (_save?.Animals is { Count: > 0 }) + { + RestoreAnimals(content); + } + else + { + SpawnAnimals(); + } // Освещение: лайтмап (день/ночь × окклюзия от гор/крон + точечные) множится поверх мира, // а Lighting.SampleAt даёт локальный свет системе роста (подлесок под кронами растёт хуже). @@ -501,11 +508,108 @@ public sealed class WorldScene : Scene } ); + SaveAnimals(save); + new SaveStore().Write(save); - Log.Info($"World '{_config.Name}' saved ({save.Plants.Count} plants)"); + Log.Info( + $"World '{_config.Name}' saved ({save.Plants.Count} plants, " + + $"{save.Animals.Count} animals, {save.Corpses.Count} corpses)" + ); return _config.Name; } + // Снимок популяции животных и трупов: всё нужное, чтобы эволюция/состояние пережили сейв/лоад + // (а не пересыпались из сида). Health/Mood/Pregnant берём через entity — запрос ограничен 5 типами. + private void SaveAnimals(WorldSave save) + { + Store + .Query() + .ForEachEntity( + ( + ref AnimalOrganism org, + ref AnimalGrowth grow, + ref AnimalNeeds needs, + ref Transform2D transform, + Entity entity + ) => + { + var record = new AnimalSave + { + Species = _animals[org.Species].Def.DefName, + X = transform.Position.X, + Y = transform.Position.Y, + Generation = org.Generation, + Stage = grow.Stage, + AgeDays = grow.AgeDays, + Genome = org.Genome.ToDictionary(), + Needs = needs.Values, + Starve = needs.Starve, + }; + + if (entity.GetComponent().State is { } health) + { + record.BloodLevel = health.BloodLevel; + record.PartHp = health.Parts.Select(p => p.Hp).ToArray(); + foreach (var hediff in health.Hediffs) + { + record.Hediffs.Add( + new HediffSave + { + DefName = hediff.Def.DefName, + Severity = hediff.Severity, + Immunity = hediff.Immunity, + } + ); + } + } + + var mood = entity.GetComponent(); + record.Mood = mood.Value; + record.MoodActive = mood.Active; + if (mood.Thoughts is not null) + { + foreach (var thought in mood.Thoughts) + { + record.Thoughts.Add( + new ThoughtSave + { + DefName = thought.Def.DefName, + RemainingDays = thought.RemainingDays, + } + ); + } + } + + if (entity.TryGetComponent(out var preg)) + { + record.Pregnant = true; + record.FatherGenome = preg.FatherGenome.ToDictionary(); + record.DueInDays = preg.DueInDays; + record.Litter = preg.Litter; + } + + save.Animals.Add(record); + } + ); + + Store + .Query() + .ForEachEntity( + (ref Corpse corpse, ref Transform2D transform, Entity _) => + save.Corpses.Add( + new CorpseSave + { + Species = _animals[corpse.Species].Def.DefName, + X = transform.Position.X, + Y = transform.Position.Y, + RotDays = corpse.RotDays, + BodySize = corpse.Meat / AnimalFactory.MeatPerBodySize, + Meat = corpse.Meat, + } + ) + ); + } + private void RestorePlants(GameContent content) { var fallback = new Random(_config.Seed); // для старых/битых сейвов без генома @@ -533,6 +637,122 @@ public sealed class WorldScene : Scene } } + // Восстанавливает популяцию животных и трупы из сейва: фабрика собирает особь из генома (геном + // несёт и пол), затем динамическое состояние (нужды/стадия/здоровье/настроение/беременность) + // накладывается поверх. Старый/битый сейв без генома — особь регенерируется из шаблона вида. + private void RestoreAnimals(GameContent content) + { + var fallback = new Random(_config.Seed); + foreach (var saved in _save!.Animals) + { + if (!content.Defs.TryGet(saved.Species, out var def)) + { + continue; // вид пропал (мод убрали) — пропускаем особь + } + + var index = _animals.IndexOf(def); + var genome = saved.Genome is { Count: > 0 } + ? new Genome(saved.Genome) + : _animals.GenerateGenome(index, fallback); + var entity = AnimalFactory.Create( + Store, + _animals, + _needs, + index, + new Vector2(saved.X, saved.Y), + saved.AgeDays, + genome, + saved.Generation, + CellSize + ); + RestoreAnimalState(entity, saved, content); + } + + foreach (var saved in _save.Corpses) + { + if (!content.Defs.TryGet(saved.Species, out var def)) + { + continue; + } + + var corpse = AnimalFactory.CreateCorpse( + Store, + _animals, + _animals.IndexOf(def), + new Vector2(saved.X, saved.Y), + saved.BodySize, + CellSize + ); + ref var data = ref corpse.GetComponent(); + data.RotDays = saved.RotDays; + data.Stage = -1; // форсируем пере-тинт стадии в CorpseSystem на следующем тике + data.Meat = saved.Meat; + } + + Log.Info( + $"Restored {_save.Animals.Count} animals, {_save.Corpses.Count} corpses from save" + ); + } + + // Накладывает на свежесозданную особь сохранённое динамическое состояние. Ref-компоненты правим до + // AddComponent(Pregnant) (структурное изменение инвалидирует ссылки); Health — managed, безопасен. + private void RestoreAnimalState(Entity entity, AnimalSave saved, GameContent content) + { + ref var needs = ref entity.GetComponent(); + if (saved.Needs is { Length: > 0 } && saved.Needs.Length == _needs.Count) + { + needs.Values = saved.Needs; // состав совпал — иначе остаются свежие из фабрики + } + + needs.Starve = saved.Starve; + entity.GetComponent().Stage = saved.Stage; + + ref var mood = ref entity.GetComponent(); + mood.Value = saved.Mood; + mood.Active = saved.MoodActive; + mood.Thoughts.Clear(); + foreach (var thought in saved.Thoughts) + { + if (content.Defs.TryGet(thought.DefName, out var tdef)) + { + mood.Thoughts.Add( + new ThoughtInstance { Def = tdef, RemainingDays = thought.RemainingDays } + ); + } + } + + if (entity.GetComponent().State is { } health) + { + health.BloodLevel = saved.BloodLevel; + for (var i = 0; i < health.Parts.Length && i < saved.PartHp.Length; i++) + { + health.Parts[i].Hp = saved.PartHp[i]; + } + + foreach (var hediff in saved.Hediffs) + { + if (content.Defs.TryGet(hediff.DefName, out var hdef)) + { + health.AddHediff(hdef, hediff.Severity, hediff.Immunity); + } + } + + health.RecomputeCapacities(); + } + + if (saved.Pregnant && saved.FatherGenome is { Count: > 0 }) + { + entity.AddComponent( + new Pregnant + { + FatherGenome = new Genome(saved.FatherGenome), + DueInDays = saved.DueInDays, + Litter = saved.Litter, + } + ); + } + } + // Детерминированный начальный спавн животных по данным: каждый вид с SpawnPer1000Cells > 0 // рассыпается по клеткам суши с плотностью из дефа; геном и возраст особи — из шаблона вида. private void SpawnAnimals() diff --git a/src/LittleSim/Sim/AnimalFactory.cs b/src/LittleSim/Sim/AnimalFactory.cs index 47d581a..06c2279 100644 --- a/src/LittleSim/Sim/AnimalFactory.cs +++ b/src/LittleSim/Sim/AnimalFactory.cs @@ -84,12 +84,14 @@ public static class AnimalFactory /// Создаёт сущность-труп после смерти животного: спрайт-падаль, запас мяса (∝ размер тела). public static Entity CreateCorpse( EntityStore store, - AnimalSet.Species sp, + AnimalSet animals, + int species, Vector2 position, float bodySize, int cellSize ) { + var sp = animals[species]; var region = sp.Corpse; var sprite = new Sprite(region, GameLayers.Beings); sprite.CenterOrigin(); @@ -100,6 +102,7 @@ public static class AnimalFactory sprite, new Corpse { + Species = species, RotDays = 0f, Stage = 0, Meat = bodySize * MeatPerBodySize, diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs index 8f61071..5409cfd 100644 --- a/src/LittleSim/Sim/AnimalOrganism.cs +++ b/src/LittleSim/Sim/AnimalOrganism.cs @@ -286,6 +286,22 @@ public sealed class HealthState } } + /// Восстанавливает хедиф с заданной тяжестью и иммунитетом (для загрузки сейва). + public void AddHediff(HediffDef def, float severity, float immunity) + { + if (!Has(def.DefName)) + { + _hediffs.Add( + new Hediff + { + Def = def, + Severity = severity, + Immunity = immunity, + } + ); + } + } + /// Снимает хедиф по имени дефа; возвращает, был ли он. public bool Remove(string defName) { @@ -316,6 +332,9 @@ public struct Health : IComponent /// public struct Corpse : IComponent { + /// Индекс вида в (для спрайта, разделки, сейва). + public int Species; + /// Накоплено игровых дней разложения. public float RotDays; diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs index 1056f91..5cf52ed 100644 --- a/src/LittleSim/Sim/AnimalSystems.cs +++ b/src/LittleSim/Sim/AnimalSystems.cs @@ -994,7 +994,8 @@ public sealed class AnimalMortalitySystem : BaseSystem var position = dead.GetComponent().Position; AnimalFactory.CreateCorpse( _store, - _animals[org.Species], + _animals, + org.Species, position, org.Traits.BodySize, _cellSize @@ -1317,7 +1318,8 @@ public sealed class AnimalHealthSystem : BaseSystem var position = dead.GetComponent().Position; AnimalFactory.CreateCorpse( _store, - _animals[org.Species], + _animals, + org.Species, position, org.Traits.BodySize, _cellSize