Животные: сериализация — сейв/лоад популяции и трупов
Закрывает сквозное требование плана (сейв/лоад данных каждой фазы). WorldSave получил Animals (AnimalSave: геном+пол, поколение, стадия/возраст, нужды, здоровье — кровь/HP частей/хедифы, настроение+мысли, беременность+геном отца) и Corpses (CorpseSave). SaveAnimals снимает популяцию (Health/Mood/Pregnant через entity); RestoreAnimals/RestoreAnimalState собирают особь фабрикой из генома + накладывают состояние (ref-правки до AddComponent(Pregnant); труп пере-тинтится через Stage=-1). Corpse получил Species; CreateCorpse принимает индекс вида + HealthState.AddHediff(def,severity,immunity). На загрузке стадо не пересыпается при наличии животных в сейве; старый/пустой сейв регенерируется из сида. Сборка + --check-content чистые. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
003f235440
commit
4b53eb8984
+14
-3
@@ -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` теперь осматривает и животных (не только
|
||||
> растения): вкладки **Обзор** (стадия/возраст/пол/поколение/беременность), **Нужды** (значения нужд %),
|
||||
|
||||
@@ -62,6 +62,117 @@ public sealed class PawnSave
|
||||
public float ChangeIn { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Сериализуемый хедиф особи (фаза A6): деф (по имени), тяжесть и набранный иммунитет.</summary>
|
||||
public sealed class HediffSave
|
||||
{
|
||||
/// <summary>Имя дефа хедифа (гон/болезнь).</summary>
|
||||
public string DefName { get; set; } = "";
|
||||
|
||||
/// <summary>Текущая тяжесть.</summary>
|
||||
public float Severity { get; set; }
|
||||
|
||||
/// <summary>Набранный иммунитет [0,1].</summary>
|
||||
public float Immunity { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Сериализуемая событийная мысль (фаза A8): деф (по имени) и остаток времени до угасания.</summary>
|
||||
public sealed class ThoughtSave
|
||||
{
|
||||
/// <summary>Имя дефа мысли.</summary>
|
||||
public string DefName { get; set; } = "";
|
||||
|
||||
/// <summary>Сколько игровых дней мысль ещё действует.</summary>
|
||||
public float RemainingDays { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Сериализуемое состояние одного животного (фазы A1–A8): вид, позиция, геном (источник признаков и
|
||||
/// пола), поколение, стадия/возраст, нужды, здоровье (кровь, HP частей, хедифы), настроение и
|
||||
/// беременность. Спрайт пересобирается фабрикой из дефа. Чтобы популяция (эволюция/состояние)
|
||||
/// переживала сейв/лоад, а не пересыпалась заново из сида.
|
||||
/// </summary>
|
||||
public sealed class AnimalSave
|
||||
{
|
||||
/// <summary>Имя дефа вида — по нему берётся индекс/стадии/текстуры/тело.</summary>
|
||||
public string Species { get; set; } = "";
|
||||
|
||||
/// <summary>Позиция X в мировых координатах.</summary>
|
||||
public float X { get; set; }
|
||||
|
||||
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||
public float Y { get; set; }
|
||||
|
||||
/// <summary>Номер поколения (для popstats/дрейфа).</summary>
|
||||
public int Generation { get; set; }
|
||||
|
||||
/// <summary>Текущая стадия роста.</summary>
|
||||
public int Stage { get; set; }
|
||||
|
||||
/// <summary>Накопленный возраст в игровых днях.</summary>
|
||||
public float AgeDays { get; set; }
|
||||
|
||||
/// <summary>Геном особи: аллели по id генов (переменного состава; несёт и пол через GeneSex).</summary>
|
||||
public Dictionary<string, Allele> Genome { get; set; } = new();
|
||||
|
||||
/// <summary>Значения нужд по индексам NeedSet.</summary>
|
||||
public float[] Needs { get; set; } = [];
|
||||
|
||||
/// <summary>Накоплено дней истощения.</summary>
|
||||
public float Starve { get; set; }
|
||||
|
||||
/// <summary>Уровень крови [0,1].</summary>
|
||||
public float BloodLevel { get; set; } = 1f;
|
||||
|
||||
/// <summary>HP частей тела по порядку анатомии вида (пусто — у вида нет тела).</summary>
|
||||
public float[] PartHp { get; set; } = [];
|
||||
|
||||
/// <summary>Активные хедифы (гон/болезни).</summary>
|
||||
public List<HediffSave> Hediffs { get; set; } = [];
|
||||
|
||||
/// <summary>Текущее настроение [0,1].</summary>
|
||||
public float Mood { get; set; }
|
||||
|
||||
/// <summary>Активно ли настроение (тир 4+).</summary>
|
||||
public bool MoodActive { get; set; }
|
||||
|
||||
/// <summary>Активные событийные мысли.</summary>
|
||||
public List<ThoughtSave> Thoughts { get; set; } = [];
|
||||
|
||||
/// <summary>Беременна ли самка.</summary>
|
||||
public bool Pregnant { get; set; }
|
||||
|
||||
/// <summary>Геном отца (если беременна) — «слепок» на момент зачатия.</summary>
|
||||
public Dictionary<string, Allele> FatherGenome { get; set; } = new();
|
||||
|
||||
/// <summary>Остаток срока вынашивания (игровых дней).</summary>
|
||||
public float DueInDays { get; set; }
|
||||
|
||||
/// <summary>Размер помёта.</summary>
|
||||
public int Litter { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Сериализуемый труп (фаза A5): вид (для спрайта), позиция, таймер/стадия разложения, мясо.</summary>
|
||||
public sealed class CorpseSave
|
||||
{
|
||||
/// <summary>Имя дефа вида — по нему берётся спрайт трупа.</summary>
|
||||
public string Species { get; set; } = "";
|
||||
|
||||
/// <summary>Позиция X в мировых координатах.</summary>
|
||||
public float X { get; set; }
|
||||
|
||||
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||
public float Y { get; set; }
|
||||
|
||||
/// <summary>Накоплено игровых дней разложения.</summary>
|
||||
public float RotDays { get; set; }
|
||||
|
||||
/// <summary>Размер тела (для масштаба спрайта).</summary>
|
||||
public float BodySize { get; set; }
|
||||
|
||||
/// <summary>Доступное мясо.</summary>
|
||||
public float Meat { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание),
|
||||
/// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он
|
||||
@@ -99,6 +210,12 @@ public sealed class WorldSave
|
||||
/// <summary>Снимок всех растений (геномы, позиции, возраст, стадия).</summary>
|
||||
public List<PlantSave> Plants { get; set; } = [];
|
||||
|
||||
/// <summary>Снимок всех животных (геном, стадия/возраст, нужды, здоровье, настроение, беременность).</summary>
|
||||
public List<AnimalSave> Animals { get; set; } = [];
|
||||
|
||||
/// <summary>Снимок трупов (вид, позиция, таймер/стадия разложения, мясо).</summary>
|
||||
public List<CorpseSave> Corpses { get; set; } = [];
|
||||
|
||||
/// <summary>Конфиг мира для пересоздания сцены.</summary>
|
||||
public WorldConfig ToConfig() =>
|
||||
new()
|
||||
|
||||
@@ -126,9 +126,16 @@ public sealed class WorldScene : Scene
|
||||
RestorePlants(content);
|
||||
}
|
||||
|
||||
// Животные: детерминированный начальный спавн по данным (плотность из дефов). Пока без сейва —
|
||||
// регенерируются из сида и для нового, и для загруженного мира.
|
||||
// Животные: из сейва (сохранённая популяция — эволюция/состояние не теряются) либо
|
||||
// детерминированный спавн из сида (новый мир или старый сейв без животных).
|
||||
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<AnimalOrganism, AnimalGrowth, AnimalNeeds, Transform2D>()
|
||||
.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<Health>().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<Mood>();
|
||||
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<Pregnant>(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<Corpse, Transform2D>()
|
||||
.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<AnimalDef>(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<AnimalDef>(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<Corpse>();
|
||||
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<AnimalNeeds>();
|
||||
if (saved.Needs is { Length: > 0 } && saved.Needs.Length == _needs.Count)
|
||||
{
|
||||
needs.Values = saved.Needs; // состав совпал — иначе остаются свежие из фабрики
|
||||
}
|
||||
|
||||
needs.Starve = saved.Starve;
|
||||
entity.GetComponent<AnimalGrowth>().Stage = saved.Stage;
|
||||
|
||||
ref var mood = ref entity.GetComponent<Mood>();
|
||||
mood.Value = saved.Mood;
|
||||
mood.Active = saved.MoodActive;
|
||||
mood.Thoughts.Clear();
|
||||
foreach (var thought in saved.Thoughts)
|
||||
{
|
||||
if (content.Defs.TryGet<ThoughtDef>(thought.DefName, out var tdef))
|
||||
{
|
||||
mood.Thoughts.Add(
|
||||
new ThoughtInstance { Def = tdef, RemainingDays = thought.RemainingDays }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.GetComponent<Health>().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<HediffDef>(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()
|
||||
|
||||
@@ -84,12 +84,14 @@ public static class AnimalFactory
|
||||
/// <summary>Создаёт сущность-труп после смерти животного: спрайт-падаль, запас мяса (∝ размер тела).</summary>
|
||||
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,
|
||||
|
||||
@@ -286,6 +286,22 @@ public sealed class HealthState
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Восстанавливает хедиф с заданной тяжестью и иммунитетом (для загрузки сейва).</summary>
|
||||
public void AddHediff(HediffDef def, float severity, float immunity)
|
||||
{
|
||||
if (!Has(def.DefName))
|
||||
{
|
||||
_hediffs.Add(
|
||||
new Hediff
|
||||
{
|
||||
Def = def,
|
||||
Severity = severity,
|
||||
Immunity = immunity,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Снимает хедиф по имени дефа; возвращает, был ли он.</summary>
|
||||
public bool Remove(string defName)
|
||||
{
|
||||
@@ -316,6 +332,9 @@ public struct Health : IComponent
|
||||
/// </summary>
|
||||
public struct Corpse : IComponent
|
||||
{
|
||||
/// <summary>Индекс вида в <see cref="Content.AnimalSet"/> (для спрайта, разделки, сейва).</summary>
|
||||
public int Species;
|
||||
|
||||
/// <summary>Накоплено игровых дней разложения.</summary>
|
||||
public float RotDays;
|
||||
|
||||
|
||||
@@ -994,7 +994,8 @@ public sealed class AnimalMortalitySystem : BaseSystem
|
||||
var position = dead.GetComponent<Transform2D>().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<Transform2D>().Position;
|
||||
AnimalFactory.CreateCorpse(
|
||||
_store,
|
||||
_animals[org.Species],
|
||||
_animals,
|
||||
org.Species,
|
||||
position,
|
||||
org.Traits.BodySize,
|
||||
_cellSize
|
||||
|
||||
Reference in New Issue
Block a user