Животные A6: болезни и иммунитет

HediffDef расширен (initialSeverity/severityPerDay/immunityPerDay/lethalSeverity/pain/ambientPerDay/capMods); хедифы стали инстансами (Hediff: тяжесть+иммунитет). AnimalHealthSystem (медленный тик ~час): фоновое заражение, гонка тяжесть<->иммунитет (выздоровление при иммунитете 1 / смерть при летальной тяжести), пересчёт способностей с болью и capMods; смерть по здоровью оставляет труп. Первый реальный источник урона: Moving подключён к скорости (больные медленнее). Болезнь Fever; команда infect для теста.

Сборка чистая, --check-content (10 типов дефов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-14 05:48:05 +03:00
co-authored by Claude Opus 4.8
parent 9db12273c9
commit 9f8ff93591
6 changed files with 291 additions and 23 deletions
+27 -4
View File
@@ -357,8 +357,31 @@ public sealed class BodyDef : Def
}
/// <summary>
/// Состояние/хедиф организма (Defs/hediffs.json): гон, позже раны, болезни, возрастные эффекты.
/// Фаза A4 — лёгкий каркас: пока идентичность + локализуемый <see cref="Def.Label"/>; стадии,
/// модификаторы способностей и иммунитет придут в A5/A6.
/// Состояние/хедиф организма (Defs/hediffs.json): гон, болезни, позже раны/возрастные эффекты. Фаза A6:
/// болезнь прогрессирует (<see cref="SeverityPerDay"/>), параллельно растёт иммунитет
/// (<see cref="ImmunityPerDay"/>) — гонка: иммунитет→1 раньше = выздоровление, severity→
/// <see cref="LethalSeverity"/> раньше = смерть. Снижает способности (<see cref="CapMods"/>) и даёт боль.
/// </summary>
public sealed class HediffDef : Def { }
public sealed class HediffDef : Def
{
/// <summary>Начальная тяжесть при навешивании (гон — 1; болезнь — малая, напр. 0.05).</summary>
public float InitialSeverity { get; init; } = 1f;
/// <summary>Прирост тяжести за игровой день (0 — статичный хедиф вроде гона).</summary>
public float SeverityPerDay { get; init; }
/// <summary>Прирост иммунитета за день (0 — иммунитета нет, не выздоравливает сам).</summary>
public float ImmunityPerDay { get; init; }
/// <summary>Тяжесть, при которой наступает смерть (0 — не летально).</summary>
public float LethalSeverity { get; init; }
/// <summary>Боль при тяжести 1 (масштабируется тяжестью) — вход в сознание/настроение.</summary>
public float Pain { get; init; }
/// <summary>Шанс подхватить фоном за день (0 — не фоновая болезнь).</summary>
public float AmbientPerDay { get; init; }
/// <summary>Модификаторы способностей: capacity id → множитель при тяжести 1 (лерп 1→factor по severity).</summary>
public Dictionary<string, float> CapMods { get; init; } = new();
}
+39
View File
@@ -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<HediffDef>("Rut")));
UpdateSystems.Add(
new AnimalHealthSystem(
Store,
_animals,
CellSize,
Context.Clock,
SecondsPerDay,
content.Defs.All<HediffDef>().Where(h => h.AmbientPerDay > 0f).ToArray(),
_config.Seed + 0x3EAD
)
);
UpdateSystems.Add(
new AnimalDecisionSystem(
Store,
@@ -714,6 +725,11 @@ public sealed class WorldScene : Scene
"body <species> — 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<HediffDef>(name, out var def))
{
console.WriteLine($"no hediff '{name}'");
return;
}
var count = 0;
Store
.Query<Health>()
.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<ProductDef>(productDefName, out var product)
? content.Languages.Get(product.Label)
+89 -14
View File
@@ -129,18 +129,31 @@ public struct PartInstance
public readonly float Fraction => MaxHp > 0f ? Math.Clamp(Hp / MaxHp, 0f, 1f) : 0f;
}
/// <summary>Инстанс хедифа на особи: деф + текущая тяжесть и набранный иммунитет (гонка, фаза A6).</summary>
public struct Hediff
{
/// <summary>Деф хедифа (гон/болезнь/…).</summary>
public HediffDef Def;
/// <summary>Текущая тяжесть.</summary>
public float Severity;
/// <summary>Набранный иммунитет [0,1]; при 1 — выздоровление.</summary>
public float Immunity;
}
/// <summary>
/// Состояние здоровья особи (фаза A4A5): список активных хедифов + части тела, уровень крови и
/// вычисленные способности (capacities). Managed-объект, как <see cref="Genome"/> (Friflo допускает
/// ссылку в компоненте). Источников урона пока нет — части на полном HP, способности = 1; каркас под
/// болезни (A6) и хищников (горизонт). Растёт по фазам — контейнер один.
/// Состояние здоровья особи (фазы A4A6): активные хедифы (инстансы) + части тела, кровь и вычисленные
/// способности (capacities). Managed-объект, как <see cref="Genome"/> (Friflo допускает ссылку в
/// компоненте). Болезни (A6) — первый источник урона: снижают способности и могут убить. Растёт по
/// фазам — контейнер один.
/// </summary>
public sealed class HealthState
{
private readonly List<HediffDef> _hediffs = [];
private readonly List<Hediff> _hediffs = [];
/// <summary>Активные хедифы (гон, позже — раны/болезни).</summary>
public IReadOnlyList<HediffDef> Hediffs => _hediffs;
/// <summary>Активные хедифы (гон, болезни).</summary>
public IReadOnlyList<Hediff> Hediffs => _hediffs;
/// <summary>Части тела особи (из анатомии вида); пусто — у вида нет тела.</summary>
public PartInstance[] Parts { get; private set; } = [];
@@ -160,9 +173,71 @@ public sealed class HealthState
/// <summary>Способность по id (1, если не вычислена — напр. у вида без тела), для множителей действий.</summary>
public float Capacity(string id) => Capacities.TryGetValue(id, out var v) ? v : 1f;
/// <summary>Пересчитывает способности из текущих частей/крови/боли.</summary>
public void RecomputeCapacities() =>
Capacities = CapacityCalc.Compute(Parts, BloodLevel, Pain);
/// <summary>
/// Пересчитывает боль (из хедифов) и способности: базовые из частей/крови/боли, затем модификаторы
/// способностей от хедифов (лерп 1→factor по тяжести). Вызывать после изменения частей/крови/хедифов.
/// </summary>
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;
}
/// <summary>
/// Прогрессирует болезни (тяжесть и иммунитет растут со временем); снимает выздоровевшие
/// (иммунитет ≥ 1); возвращает true, если какая-то болезнь достигла летальной тяжести.
/// </summary>
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;
}
/// <summary>Строит здоровье особи: части из анатомии вида (HP × размер тела) + начальные способности.</summary>
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;
}
/// <summary>Навешивает хедиф (если ещё не навешан).</summary>
/// <summary>Навешивает хедиф (если ещё не навешан) с начальной тяжестью из дефа.</summary>
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;
+119 -4
View File
@@ -433,7 +433,13 @@ public sealed class AnimalActionSystem : BaseSystem
private readonly GameClock _clock;
private readonly float _secondsPerDay;
private readonly RectF _bounds;
private readonly ArchetypeQuery<AnimalBrain, AnimalNeeds, AnimalOrganism, Transform2D> _query;
private readonly ArchetypeQuery<
AnimalBrain,
AnimalNeeds,
AnimalOrganism,
Transform2D,
Health
> _query;
private readonly List<Entity> _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<AnimalBrain, AnimalNeeds, AnimalOrganism, Transform2D>();
_query = store.Query<AnimalBrain, AnimalNeeds, AnimalOrganism, Transform2D, Health>();
}
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;
}
}
/// <summary>
/// Здоровье и болезни (фаза A6): медленный тик (~игровой час). Фоновое заражение, прогресс болезней
/// (гонка тяжесть↔иммунитет — выздоровление или смерть), пересчёт способностей с учётом хедифов. Смерть
/// по здоровью (летальная болезнь / кровь на нуле / потеря сознания) оставляет труп. Первый реальный
/// источник урона — оживляет каркас A5 (больные звери теряют Moving → медленнее).
/// </summary>
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<Health, AnimalOrganism> _query;
private readonly List<Entity> _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<Health, AnimalOrganism>();
}
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<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();
}
}
}