diff --git a/Mods/Core/Defs/hediffs.json b/Mods/Core/Defs/hediffs.json index 7f786db..5505d67 100644 --- a/Mods/Core/Defs/hediffs.json +++ b/Mods/Core/Defs/hediffs.json @@ -3,6 +3,15 @@ // Хедифы — состояния организма (фаза A4: лёгкий каркас). Гон (Rut) навешивается сезонно на взрослых // и поднимает половое влечение. Раны/болезни/возрастные эффекты со стадиями придут в A5/A6. "defs": [ - { "defName": "Rut", "label": "hediff.rut" } + { "defName": "Rut", "label": "hediff.rut" }, + + // Болезнь (фаза A6): прогрессирует, параллельно растёт иммунитет (гонка). Обычно иммунитет + // успевает победить (~3 дня), но больной зверь медленнее и слабее; слабые/невезучие гибнут. + { + "defName": "Fever", "label": "hediff.fever", + "initialSeverity": 0.05, "severityPerDay": 0.25, "immunityPerDay": 0.32, + "lethalSeverity": 1.0, "pain": 0.3, "ambientPerDay": 0.03, + "capMods": { "Moving": 0.6, "Consciousness": 0.85, "Digestion": 0.8 } + } ] } diff --git a/docs/животные.md b/docs/животные.md index 3c5ded9..eb65ca0 100644 --- a/docs/животные.md +++ b/docs/животные.md @@ -16,6 +16,13 @@ > 1 (каркас под болезни A6/хищников). Команда `body `. Сборка/линт чистые (10 типов дефов). > Осталось в A5: смерть через здоровье (vital-часть/кровь) + истощение как hediff — когда появятся раны. > +> **A6 (готово): болезни и иммунитет.** `HediffDef` расширён (severity/immunity/lethal/pain/capMods/ +> ambient); хедифы теперь инстансы (`Hediff` с тяжестью/иммунитетом). `AnimalHealthSystem` (медленный +> тик ~час): фоновое заражение, гонка тяжесть↔иммунитет (выздоровление или смерть), пересчёт способностей +> с учётом боли и capMods; смерть по здоровью (летальная болезнь/кровь/потеря сознания) → труп. **Первый +> реальный урон** — `Moving` подключён к скорости (больные звери медленнее). Болезнь `Fever` в +> `hediffs.json`; команда `infect [hediff]`. Сборка/линт чистые. +> > **A4 (готово):** нужда `Mating`; лёгкий hediff-каркас (`HediffDef`, `hediffs.json` с `Rut`, > компонент `Health`/`HealthState`); гены `GeneBreedingSeason`/`GeneGestationDays`/`GeneLitterSize`; > `AnimalRutSystem` (сезонный гон-хедиф поднимает влечение — вне сезона зачатия нет); действие diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs index 7f359eb..951a922 100644 --- a/src/LittleSim/Content/GameDefs.cs +++ b/src/LittleSim/Content/GameDefs.cs @@ -357,8 +357,31 @@ public sealed class BodyDef : Def } /// -/// Состояние/хедиф организма (Defs/hediffs.json): гон, позже — раны, болезни, возрастные эффекты. -/// Фаза A4 — лёгкий каркас: пока идентичность + локализуемый ; стадии, -/// модификаторы способностей и иммунитет придут в A5/A6. +/// Состояние/хедиф организма (Defs/hediffs.json): гон, болезни, позже раны/возрастные эффекты. Фаза A6: +/// болезнь прогрессирует (), параллельно растёт иммунитет +/// () — гонка: иммунитет→1 раньше = выздоровление, severity→ +/// раньше = смерть. Снижает способности () и даёт боль. /// -public sealed class HediffDef : Def { } +public sealed class HediffDef : Def +{ + /// Начальная тяжесть при навешивании (гон — 1; болезнь — малая, напр. 0.05). + public float InitialSeverity { get; init; } = 1f; + + /// Прирост тяжести за игровой день (0 — статичный хедиф вроде гона). + public float SeverityPerDay { get; init; } + + /// Прирост иммунитета за день (0 — иммунитета нет, не выздоравливает сам). + public float ImmunityPerDay { get; init; } + + /// Тяжесть, при которой наступает смерть (0 — не летально). + public float LethalSeverity { get; init; } + + /// Боль при тяжести 1 (масштабируется тяжестью) — вход в сознание/настроение. + public float Pain { get; init; } + + /// Шанс подхватить фоном за день (0 — не фоновая болезнь). + public float AmbientPerDay { get; init; } + + /// Модификаторы способностей: capacity id → множитель при тяжести 1 (лерп 1→factor по severity). + public Dictionary CapMods { get; init; } = new(); +} diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 9a25dfa..b7850e9 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -216,6 +216,17 @@ public sealed class WorldScene : Scene var shore = ComputeShore(); UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs)); UpdateSystems.Add(new AnimalRutSystem(_animals, climate, content.Defs.Get("Rut"))); + UpdateSystems.Add( + new AnimalHealthSystem( + Store, + _animals, + CellSize, + Context.Clock, + SecondsPerDay, + content.Defs.All().Where(h => h.AmbientPerDay > 0f).ToArray(), + _config.Seed + 0x3EAD + ) + ); UpdateSystems.Add( new AnimalDecisionSystem( Store, @@ -714,6 +725,11 @@ public sealed class WorldScene : Scene "body — dump a species body parts and a healthy individual's capacities", (c, args) => RunBodyDemo(c, content, args) ); + console.Register( + "infect", + "infect [hediff] — give every animal a disease (default Fever) to test the health system", + (c, args) => RunInfect(c, content, args) + ); console.Register( "menu", "menu — return to the main menu", @@ -960,6 +976,29 @@ public sealed class WorldScene : Scene ); } + // Тест системы здоровья (фаза A6): навешивает болезнь всем живым животным. + private void RunInfect(DevConsole console, GameContent content, string[] args) + { + var name = args.Length > 0 ? args[0] : "Fever"; + if (!content.Defs.TryGet(name, out var def)) + { + console.WriteLine($"no hediff '{name}'"); + return; + } + + var count = 0; + Store + .Query() + .ForEachEntity( + (ref Health h, Entity _) => + { + h.State?.Add(def); + count++; + } + ); + console.WriteLine($"infected {count} animals with {def.DefName}"); + } + private static string ProductLabel(GameContent content, string productDefName) => content.Defs.TryGet(productDefName, out var product) ? content.Languages.Get(product.Label) diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs index 244da8d..53523d2 100644 --- a/src/LittleSim/Sim/AnimalOrganism.cs +++ b/src/LittleSim/Sim/AnimalOrganism.cs @@ -129,18 +129,31 @@ public struct PartInstance public readonly float Fraction => MaxHp > 0f ? Math.Clamp(Hp / MaxHp, 0f, 1f) : 0f; } +/// Инстанс хедифа на особи: деф + текущая тяжесть и набранный иммунитет (гонка, фаза A6). +public struct Hediff +{ + /// Деф хедифа (гон/болезнь/…). + public HediffDef Def; + + /// Текущая тяжесть. + public float Severity; + + /// Набранный иммунитет [0,1]; при 1 — выздоровление. + public float Immunity; +} + /// -/// Состояние здоровья особи (фаза A4–A5): список активных хедифов + части тела, уровень крови и -/// вычисленные способности (capacities). Managed-объект, как (Friflo допускает -/// ссылку в компоненте). Источников урона пока нет — части на полном HP, способности = 1; каркас под -/// болезни (A6) и хищников (горизонт). Растёт по фазам — контейнер один. +/// Состояние здоровья особи (фазы A4–A6): активные хедифы (инстансы) + части тела, кровь и вычисленные +/// способности (capacities). Managed-объект, как (Friflo допускает ссылку в +/// компоненте). Болезни (A6) — первый источник урона: снижают способности и могут убить. Растёт по +/// фазам — контейнер один. /// public sealed class HealthState { - private readonly List _hediffs = []; + private readonly List _hediffs = []; - /// Активные хедифы (гон, позже — раны/болезни). - public IReadOnlyList Hediffs => _hediffs; + /// Активные хедифы (гон, болезни). + public IReadOnlyList Hediffs => _hediffs; /// Части тела особи (из анатомии вида); пусто — у вида нет тела. public PartInstance[] Parts { get; private set; } = []; @@ -160,9 +173,71 @@ public sealed class HealthState /// Способность по id (1, если не вычислена — напр. у вида без тела), для множителей действий. public float Capacity(string id) => Capacities.TryGetValue(id, out var v) ? v : 1f; - /// Пересчитывает способности из текущих частей/крови/боли. - public void RecomputeCapacities() => - Capacities = CapacityCalc.Compute(Parts, BloodLevel, Pain); + /// + /// Пересчитывает боль (из хедифов) и способности: базовые из частей/крови/боли, затем модификаторы + /// способностей от хедифов (лерп 1→factor по тяжести). Вызывать после изменения частей/крови/хедифов. + /// + public void RecomputeCapacities() + { + var pain = 0f; + foreach (var h in _hediffs) + { + pain += h.Def.Pain * h.Severity; + } + + Pain = Math.Clamp(pain, 0f, 1f); + + var caps = CapacityCalc.Compute(Parts, BloodLevel, Pain); + foreach (var h in _hediffs) + { + if (h.Def.CapMods.Count == 0) + { + continue; + } + + var t = Math.Clamp(h.Severity, 0f, 1f); + foreach (var (capacity, factor) in h.Def.CapMods) + { + caps[capacity] = caps.GetValueOrDefault(capacity, 1f) * (1f + (factor - 1f) * t); + } + } + + Capacities = caps; + } + + /// + /// Прогрессирует болезни (тяжесть и иммунитет растут со временем); снимает выздоровевшие + /// (иммунитет ≥ 1); возвращает true, если какая-то болезнь достигла летальной тяжести. + /// + public bool AdvanceHediffs(float days) + { + var lethal = false; + for (var i = _hediffs.Count - 1; i >= 0; i--) + { + var h = _hediffs[i]; + if (h.Def.SeverityPerDay <= 0f && h.Def.ImmunityPerDay <= 0f) + { + continue; // статичный хедиф (гон) не прогрессирует + } + + h.Severity += h.Def.SeverityPerDay * days; + h.Immunity += h.Def.ImmunityPerDay * days; + if (h.Immunity >= 1f) + { + _hediffs.RemoveAt(i); // иммунитет победил — выздоровление + continue; + } + + if (h.Def.LethalSeverity > 0f && h.Severity >= h.Def.LethalSeverity) + { + lethal = true; + } + + _hediffs[i] = h; + } + + return lethal; + } /// Строит здоровье особи: части из анатомии вида (HP × размер тела) + начальные способности. public static HealthState Create(Content.BodyDef? body, float bodySize) @@ -193,7 +268,7 @@ public sealed class HealthState { foreach (var h in _hediffs) { - if (h.DefName == defName) + if (h.Def.DefName == defName) { return true; } @@ -202,12 +277,12 @@ public sealed class HealthState return false; } - /// Навешивает хедиф (если ещё не навешан). + /// Навешивает хедиф (если ещё не навешан) с начальной тяжестью из дефа. public void Add(HediffDef def) { if (!Has(def.DefName)) { - _hediffs.Add(def); + _hediffs.Add(new Hediff { Def = def, Severity = def.InitialSeverity }); } } @@ -216,7 +291,7 @@ public sealed class HealthState { for (var i = 0; i < _hediffs.Count; i++) { - if (_hediffs[i].DefName == defName) + if (_hediffs[i].Def.DefName == defName) { _hediffs.RemoveAt(i); return true; diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs index be89838..df7d48e 100644 --- a/src/LittleSim/Sim/AnimalSystems.cs +++ b/src/LittleSim/Sim/AnimalSystems.cs @@ -433,7 +433,13 @@ public sealed class AnimalActionSystem : BaseSystem private readonly GameClock _clock; private readonly float _secondsPerDay; private readonly RectF _bounds; - private readonly ArchetypeQuery _query; + private readonly ArchetypeQuery< + AnimalBrain, + AnimalNeeds, + AnimalOrganism, + Transform2D, + Health + > _query; private readonly List _eaten = []; private readonly List<(Entity Self, Entity Partner)> _matings = []; @@ -454,7 +460,7 @@ public sealed class AnimalActionSystem : BaseSystem _clock = clock; _secondsPerDay = secondsPerDay; _bounds = bounds; - _query = store.Query(); + _query = store.Query(); } protected override void OnUpdateGroup() @@ -469,19 +475,22 @@ public sealed class AnimalActionSystem : BaseSystem _eaten.Clear(); _matings.Clear(); - foreach (var (brains, needsChunk, organisms, transforms, entities) in _query.Chunks) + foreach (var (brains, needsChunk, organisms, transforms, healths, entities) in _query.Chunks) { var b = brains.Span; var n = needsChunk.Span; var o = organisms.Span; var t = transforms.Span; + var hh = healths.Span; for (var i = 0; i < b.Length; i++) { ref var brain = ref b[i]; var values = n[i].Values; ref var pos = ref t[i].Position; var def = _animalSet[o[i].Species].Def; - var speed = def.BaseSpeed * MathF.Max(0.2f, o[i].Traits.MoveSpeed); + // Способность Moving (падает от ран/болезней) — множитель к скорости: больные медленнее. + var moving = hh[i].State.Capacity(AnimalCapacities.Moving); + var speed = def.BaseSpeed * MathF.Max(0.2f, o[i].Traits.MoveSpeed) * moving; switch (brain.Action) { @@ -1006,3 +1015,109 @@ public sealed class AnimalPregnancySystem : BaseSystem public Vector2 Position; } } + +/// +/// Здоровье и болезни (фаза A6): медленный тик (~игровой час). Фоновое заражение, прогресс болезней +/// (гонка тяжесть↔иммунитет — выздоровление или смерть), пересчёт способностей с учётом хедифов. Смерть +/// по здоровью (летальная болезнь / кровь на нуле / потеря сознания) оставляет труп. Первый реальный +/// источник урона — оживляет каркас A5 (больные звери теряют Moving → медленнее). +/// +public sealed class AnimalHealthSystem : BaseSystem +{ + private const float TickDays = 1f / 24f; // ~игровой час + + private readonly EntityStore _store; + private readonly AnimalSet _animals; + private readonly int _cellSize; + private readonly GameClock _clock; + private readonly float _secondsPerDay; + private readonly HediffDef[] _ambient; + private readonly Random _rng; + private readonly ArchetypeQuery _query; + private readonly List _deaths = []; + private float _accum; + + public AnimalHealthSystem( + EntityStore store, + AnimalSet animals, + int cellSize, + GameClock clock, + float secondsPerDay, + HediffDef[] ambient, + int seed + ) + { + _store = store; + _animals = animals; + _cellSize = cellSize; + _clock = clock; + _secondsPerDay = secondsPerDay; + _ambient = ambient; + _rng = new Random(seed); + _query = store.Query(); + } + + protected override void OnUpdateGroup() + { + _accum += _clock.DeltaTime / _secondsPerDay; + if (_accum < TickDays) + { + return; + } + + var days = _accum; + _accum = 0f; + _deaths.Clear(); + + foreach (var (healths, organisms, entities) in _query.Chunks) + { + var h = healths.Span; + for (var i = 0; i < h.Length; i++) + { + var state = h[i].State; + if (state is null) + { + continue; + } + + foreach (var disease in _ambient) + { + if (!state.Has(disease.DefName) && _rng.NextSingle() < disease.AmbientPerDay * days) + { + state.Add(disease); + } + } + + var lethal = state.AdvanceHediffs(days); + state.RecomputeCapacities(); + if ( + lethal + || state.BloodLevel <= 0f + || state.Capacity(AnimalCapacities.Consciousness) <= 0.01f + ) + { + _deaths.Add(entities.EntityAt(i)); + } + } + } + + 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(); + } + } +}