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(); }