Животные A5 (часть 1): смерть → труп → разложение

Компонент Corpse + CorpseSystem: после смерти животное оставляет труп (AnimalFactory.CreateCorpse, мясо ∝ размер тела) вместо деспавна; труп разлагается Fresh→Rotting→Skeleton→исчезает со скоростью от температуры, тинтуясь по стадии. AnimalDef.CorpseTexture + спрайт трупа в AnimalSet; смертность спавнит труп. Продукты мясо/кости/шкура заданы (разделка/падальщики — позже).

Сборка чистая, --check-content (9 типов дефов). Части тела/кровь/capacities — часть 2 A5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-14 05:22:11 +03:00
co-authored by Claude Opus 4.8
parent cbc74c0794
commit c90bf32460
8 changed files with 173 additions and 8 deletions
+103 -5
View File
@@ -742,19 +742,25 @@ public sealed class AnimalGrowthSystem(
}
/// <summary>
/// Смертность (фаза A3): простая гибель с деспавном — от старости (возраст превысил продолжительность
/// жизни) или от истощения (счётчик голода/жажды превысил порог). Замыкает контур ёмкости среды.
/// На A5 «апгрейдится» в смерть через здоровье и оставит труп вместо деспавна.
/// Смертность (фаза A3/A5): гибель от старости (возраст превысил продолжительность жизни) или истощения
/// (счётчик голода/жажды превысил порог). Замыкает контур ёмкости среды. Фаза A5: вместо деспавна
/// оставляет <see cref="Corpse"/> (через <see cref="AnimalFactory.CreateCorpse"/>), затем удаляет особь.
/// </summary>
public sealed class AnimalMortalitySystem : BaseSystem
{
private const float LethalStarveDays = 3f;
private readonly EntityStore _store;
private readonly AnimalSet _animals;
private readonly int _cellSize;
private readonly ArchetypeQuery<AnimalGrowth, AnimalOrganism, AnimalNeeds> _query;
private readonly List<Entity> _deaths = [];
public AnimalMortalitySystem(EntityStore store)
public AnimalMortalitySystem(EntityStore store, AnimalSet animals, int cellSize)
{
_store = store;
_animals = animals;
_cellSize = cellSize;
_query = store.Query<AnimalGrowth, AnimalOrganism, AnimalNeeds>();
}
@@ -775,7 +781,99 @@ public sealed class AnimalMortalitySystem : BaseSystem
}
}
foreach (var entity in _deaths)
foreach (var dead in _deaths)
{
if (dead.IsNull || !dead.HasComponent<AnimalOrganism>())
{
continue;
}
ref readonly var org = ref dead.GetComponent<AnimalOrganism>();
var position = dead.GetComponent<Transform2D>().Position;
AnimalFactory.CreateCorpse(
_store,
_animals[org.Species],
position,
org.Traits.BodySize,
_cellSize
);
dead.DeleteEntity();
}
}
}
/// <summary>
/// Разложение трупа (фаза A5): копит дни гниения (ускоряется в тепле, замедляется на морозе),
/// переключает стадии Fresh→Rotting→Skeleton по порогам и тинтит спрайт; по достижении предела труп
/// исчезает. Скелет рисуется бледным тинтом падали (отдельного спрайта пока нет — см. docs).
/// </summary>
public sealed class CorpseSystem : BaseSystem
{
private const float RottingDay = 2f;
private const float SkeletonDay = 6f;
private const float GoneDay = 14f;
private static readonly Color FreshTint = new(176, 148, 128);
private static readonly Color RottingTint = new(120, 118, 98);
private static readonly Color SkeletonTint = new(228, 226, 214);
private readonly GameClock _clock;
private readonly float _secondsPerDay;
private readonly Climate _climate;
private readonly ArchetypeQuery<Corpse, Sprite> _query;
private readonly List<Entity> _gone = [];
public CorpseSystem(EntityStore store, GameClock clock, float secondsPerDay, Climate climate)
{
_clock = clock;
_secondsPerDay = secondsPerDay;
_climate = climate;
_query = store.Query<Corpse, Sprite>();
}
protected override void OnUpdateGroup()
{
var days = _clock.DeltaTime / _secondsPerDay;
if (days <= 0f)
{
return;
}
// Тепло ускоряет гниение, около нуля и ниже — резко замедляет (заморозка).
var factor = Math.Clamp((_climate.Temperature + 5f) / 20f, 0.1f, 2.5f);
var advance = days * factor;
_gone.Clear();
foreach (var (corpses, sprites, entities) in _query.Chunks)
{
var c = corpses.Span;
var s = sprites.Span;
for (var i = 0; i < c.Length; i++)
{
ref var corpse = ref c[i];
corpse.RotDays += advance;
if (corpse.RotDays >= GoneDay)
{
_gone.Add(entities.EntityAt(i));
continue;
}
var stage =
corpse.RotDays >= SkeletonDay ? 2
: corpse.RotDays >= RottingDay ? 1
: 0;
if (stage != corpse.Stage)
{
corpse.Stage = stage;
s[i].Color =
stage == 2 ? SkeletonTint
: stage == 1 ? RottingTint
: FreshTint;
}
}
}
foreach (var entity in _gone)
{
entity.DeleteEntity();
}