Животные 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
+6
View File
@@ -27,6 +27,9 @@ public sealed class AnimalSet
/// <summary>Спрайт детёныша/подростка (fallback — самка).</summary>
public required Texture2DRegion Baby { get; init; }
/// <summary>Спрайт трупа (fallback — самка).</summary>
public required Texture2DRegion Corpse { get; init; }
/// <summary>Стадии роста (из дефа или встроенный дефолт), по возрастанию возраста входа.</summary>
public required AnimalStageDef[] Stages { get; init; }
@@ -73,6 +76,9 @@ public sealed class AnimalSet
Baby = string.IsNullOrEmpty(def.BabyTexture)
? female
: atlases.GetRegion(device, def.BabyTexture),
Corpse = string.IsNullOrEmpty(def.CorpseTexture)
? female
: atlases.GetRegion(device, def.CorpseTexture),
Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages,
Template = BuildTemplate(def.Genome, genes),
};
+3
View File
@@ -249,6 +249,9 @@ public sealed class AnimalDef : PawnDef
/// <summary>Текстура детёныша/подростка (фаза A3); пусто — берётся <see cref="PawnDef.Texture"/>.</summary>
public string BabyTexture { get; init; } = "";
/// <summary>Текстура трупа (фаза A5); пусто — берётся <see cref="PawnDef.Texture"/>.</summary>
public string CorpseTexture { get; init; } = "";
/// <summary>
/// Геном вида как ДАННЫЕ: ключ — id гена (<see cref="MrGameEng.Genetics.GeneDef"/>), значение —
/// центр аллелей особи. Состав открыт — модер добавляет/убирает ген одной строкой JSON, без правки
+2 -1
View File
@@ -240,7 +240,8 @@ public sealed class WorldScene : Scene
);
UpdateSystems.Add(new AnimalAppearanceSystem(_needs));
UpdateSystems.Add(new AnimalGrowthSystem(Context.Clock, SecondsPerDay, _animals, CellSize));
UpdateSystems.Add(new AnimalMortalitySystem(Store));
UpdateSystems.Add(new AnimalMortalitySystem(Store, _animals, CellSize));
UpdateSystems.Add(new CorpseSystem(Store, Context.Clock, SecondsPerDay, climate));
UpdateSystems.Add(
new AnimalPregnancySystem(
Store,
+32
View File
@@ -65,6 +65,38 @@ public static class AnimalFactory
);
}
/// <summary>Сколько мяса даёт труп на единицу размера тела (для будущей разделки/падальщиков).</summary>
public const float MeatPerBodySize = 25f;
// Тинт свежего трупа (стадии гниения/скелета задаёт CorpseSystem).
private static readonly Color CorpseFreshTint = new(176, 148, 128);
/// <summary>Создаёт сущность-труп после смерти животного: спрайт-падаль, запас мяса (∝ размер тела).</summary>
public static Entity CreateCorpse(
EntityStore store,
AnimalSet.Species sp,
Vector2 position,
float bodySize,
int cellSize
)
{
var region = sp.Corpse;
var sprite = new Sprite(region, GameLayers.Beings);
sprite.CenterOrigin();
sprite.Color = CorpseFreshTint;
var sizeCells = bodySize > 0f ? bodySize : sp.Def.SizeCells;
return store.CreateEntity(
new Transform2D(position, scale: new Vector2(cellSize * sizeCells / region.Width)),
sprite,
new Corpse
{
RotDays = 0f,
Stage = 0,
Meat = bodySize * MeatPerBodySize,
}
);
}
/// <summary>Пол из локуса GeneSex: наличие аллеля Y (значение ≈1) → самец; иначе самка (XX).</summary>
public static bool IsMaleGenome(Genome genome) =>
genome.Has("GeneSex") && (genome["GeneSex"].A > 0.5f || genome["GeneSex"].B > 0.5f);
+17
View File
@@ -171,6 +171,23 @@ public struct Health : IComponent
public HealthState State;
}
/// <summary>
/// Труп животного (фаза A5): отдельная сущность после смерти. Разлагается во времени по стадиям
/// (Fresh→Rotting→Skeleton→исчезает), скорость зависит от температуры. Несёт запас мяса (∝ размер тела)
/// для будущей разделки/падальщиков. Управляется <see cref="Sim.CorpseSystem"/>.
/// </summary>
public struct Corpse : IComponent
{
/// <summary>Накоплено игровых дней разложения.</summary>
public float RotDays;
/// <summary>Стадия: 0 свежий, 1 гниющий, 2 скелет.</summary>
public int Stage;
/// <summary>Доступное мясо (∝ размер тела) — для разделки/падальщиков (фаза-горизонт).</summary>
public float Meat;
}
/// <summary>
/// Беременность самки (фаза A4): managed-ссылка на геном отца («слепок» на момент зачатия), остаток
/// срока вынашивания и размер помёта. По истечении срока <see cref="Sim.AnimalPregnancySystem"/>
+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();
}