Животные 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:
co-authored by
Claude Opus 4.8
parent
cbc74c0794
commit
c90bf32460
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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, без правки
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user