diff --git a/Mods/Core/Defs/animals.json b/Mods/Core/Defs/animals.json
index fd69df5..df6f5b0 100644
--- a/Mods/Core/Defs/animals.json
+++ b/Mods/Core/Defs/animals.json
@@ -11,6 +11,7 @@
"texture": "things/pawn/animal/deer/DeerFemale_east",
"maleTexture": "things/pawn/animal/deer/DeerMale_east",
"babyTexture": "things/pawn/animal/deer/DeerBaby_east",
+ "corpseTexture": "things/pawn/animal/deer/Dessicated_DeerFemale_east",
"diet": ["plant"],
"spawnPer1000Cells": 2.5,
"genome": {
diff --git a/docs/животные.md b/docs/животные.md
index a7b8bf0..e0d8236 100644
--- a/docs/животные.md
+++ b/docs/животные.md
@@ -1,7 +1,14 @@
# Животные (зоология)
-> Статус: **A1–A4 реализованы**, остальное по плану. Это живой план — правится по мере
-> реализации фаз A1–A8. Стиль и правила — как в [концепте](concept.md) и [модах](mods.md).
+> Статус: **A1–A4 + рефактор расширяемости + A5(труп) реализованы**, остальное по плану. Это живой
+> план — правится по мере реализации фаз A1–A8. Стиль и правила — как в [концепте](concept.md) и [модах](mods.md).
+>
+> **A5 часть 1 (готово): смерть → труп → разложение.** `Corpse`-компонент + `CorpseSystem`
+> (Fresh→Rotting→Skeleton→исчезает, скорость от температуры, тинт по стадии); смерть теперь оставляет
+> труп (`AnimalFactory.CreateCorpse`, мясо ∝ размер тела) вместо деспавна; `AnimalDef.CorpseTexture` +
+> `corpseTexture` оленя (`Dessicated_*`, скелет — бледный тинт, своего спрайта пока нет). Продукты
+> `ProductMeat/Bone/Leather` в `products.json` (разделка/падальщики — позже). Сборка/линт чистые.
+> Осталось в A5: части тела (`BodyDef`), кровь, capacities (часть 2).
>
> **A4 (готово):** нужда `Mating`; лёгкий hediff-каркас (`HediffDef`, `hediffs.json` с `Rut`,
> компонент `Health`/`HealthState`); гены `GeneBreedingSeason`/`GeneGestationDays`/`GeneLitterSize`;
diff --git a/src/LittleSim/Content/AnimalSet.cs b/src/LittleSim/Content/AnimalSet.cs
index 5bf007b..a5a353a 100644
--- a/src/LittleSim/Content/AnimalSet.cs
+++ b/src/LittleSim/Content/AnimalSet.cs
@@ -27,6 +27,9 @@ public sealed class AnimalSet
/// Спрайт детёныша/подростка (fallback — самка).
public required Texture2DRegion Baby { get; init; }
+ /// Спрайт трупа (fallback — самка).
+ public required Texture2DRegion Corpse { get; init; }
+
/// Стадии роста (из дефа или встроенный дефолт), по возрастанию возраста входа.
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),
};
diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs
index ffe45ff..36e7da3 100644
--- a/src/LittleSim/Content/GameDefs.cs
+++ b/src/LittleSim/Content/GameDefs.cs
@@ -249,6 +249,9 @@ public sealed class AnimalDef : PawnDef
/// Текстура детёныша/подростка (фаза A3); пусто — берётся .
public string BabyTexture { get; init; } = "";
+ /// Текстура трупа (фаза A5); пусто — берётся .
+ public string CorpseTexture { get; init; } = "";
+
///
/// Геном вида как ДАННЫЕ: ключ — id гена (), значение —
/// центр аллелей особи. Состав открыт — модер добавляет/убирает ген одной строкой JSON, без правки
diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs
index b29c1e6..f4e5b41 100644
--- a/src/LittleSim/Scenes/WorldScene.cs
+++ b/src/LittleSim/Scenes/WorldScene.cs
@@ -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,
diff --git a/src/LittleSim/Sim/AnimalFactory.cs b/src/LittleSim/Sim/AnimalFactory.cs
index 7d1ca87..9dd6258 100644
--- a/src/LittleSim/Sim/AnimalFactory.cs
+++ b/src/LittleSim/Sim/AnimalFactory.cs
@@ -65,6 +65,38 @@ public static class AnimalFactory
);
}
+ /// Сколько мяса даёт труп на единицу размера тела (для будущей разделки/падальщиков).
+ public const float MeatPerBodySize = 25f;
+
+ // Тинт свежего трупа (стадии гниения/скелета задаёт CorpseSystem).
+ private static readonly Color CorpseFreshTint = new(176, 148, 128);
+
+ /// Создаёт сущность-труп после смерти животного: спрайт-падаль, запас мяса (∝ размер тела).
+ 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,
+ }
+ );
+ }
+
/// Пол из локуса GeneSex: наличие аллеля Y (значение ≈1) → самец; иначе самка (XX).
public static bool IsMaleGenome(Genome genome) =>
genome.Has("GeneSex") && (genome["GeneSex"].A > 0.5f || genome["GeneSex"].B > 0.5f);
diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs
index 56b4a03..061709f 100644
--- a/src/LittleSim/Sim/AnimalOrganism.cs
+++ b/src/LittleSim/Sim/AnimalOrganism.cs
@@ -171,6 +171,23 @@ public struct Health : IComponent
public HealthState State;
}
+///
+/// Труп животного (фаза A5): отдельная сущность после смерти. Разлагается во времени по стадиям
+/// (Fresh→Rotting→Skeleton→исчезает), скорость зависит от температуры. Несёт запас мяса (∝ размер тела)
+/// для будущей разделки/падальщиков. Управляется .
+///
+public struct Corpse : IComponent
+{
+ /// Накоплено игровых дней разложения.
+ public float RotDays;
+
+ /// Стадия: 0 свежий, 1 гниющий, 2 скелет.
+ public int Stage;
+
+ /// Доступное мясо (∝ размер тела) — для разделки/падальщиков (фаза-горизонт).
+ public float Meat;
+}
+
///
/// Беременность самки (фаза A4): managed-ссылка на геном отца («слепок» на момент зачатия), остаток
/// срока вынашивания и размер помёта. По истечении срока
diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs
index 858c0e4..be89838 100644
--- a/src/LittleSim/Sim/AnimalSystems.cs
+++ b/src/LittleSim/Sim/AnimalSystems.cs
@@ -742,19 +742,25 @@ public sealed class AnimalGrowthSystem(
}
///
-/// Смертность (фаза A3): простая гибель с деспавном — от старости (возраст превысил продолжительность
-/// жизни) или от истощения (счётчик голода/жажды превысил порог). Замыкает контур ёмкости среды.
-/// На A5 «апгрейдится» в смерть через здоровье и оставит труп вместо деспавна.
+/// Смертность (фаза A3/A5): гибель от старости (возраст превысил продолжительность жизни) или истощения
+/// (счётчик голода/жажды превысил порог). Замыкает контур ёмкости среды. Фаза A5: вместо деспавна
+/// оставляет (через ), затем удаляет особь.
///
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 _query;
private readonly List _deaths = [];
- public AnimalMortalitySystem(EntityStore store)
+ public AnimalMortalitySystem(EntityStore store, AnimalSet animals, int cellSize)
{
+ _store = store;
+ _animals = animals;
+ _cellSize = cellSize;
_query = store.Query();
}
@@ -775,7 +781,99 @@ public sealed class AnimalMortalitySystem : BaseSystem
}
}
- foreach (var entity in _deaths)
+ foreach (var dead in _deaths)
+ {
+ if (dead.IsNull || !dead.HasComponent())
+ {
+ continue;
+ }
+
+ ref readonly var org = ref dead.GetComponent();
+ var position = dead.GetComponent().Position;
+ AnimalFactory.CreateCorpse(
+ _store,
+ _animals[org.Species],
+ position,
+ org.Traits.BodySize,
+ _cellSize
+ );
+ dead.DeleteEntity();
+ }
+ }
+}
+
+///
+/// Разложение трупа (фаза A5): копит дни гниения (ускоряется в тепле, замедляется на морозе),
+/// переключает стадии Fresh→Rotting→Skeleton по порогам и тинтит спрайт; по достижении предела труп
+/// исчезает. Скелет рисуется бледным тинтом падали (отдельного спрайта пока нет — см. docs).
+///
+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 _query;
+ private readonly List _gone = [];
+
+ public CorpseSystem(EntityStore store, GameClock clock, float secondsPerDay, Climate climate)
+ {
+ _clock = clock;
+ _secondsPerDay = secondsPerDay;
+ _climate = climate;
+ _query = store.Query();
+ }
+
+ 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();
}