Животные 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:
co-authored by
Claude Opus 4.8
parent
9db12273c9
commit
9f8ff93591
@@ -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 }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,6 +16,13 @@
|
||||
> 1 (каркас под болезни A6/хищников). Команда `body <species>`. Сборка/линт чистые (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` (сезонный гон-хедиф поднимает влечение — вне сезона зачатия нет); действие
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
/// Состояние здоровья особи (фаза A4–A5): список активных хедифов + части тела, уровень крови и
|
||||
/// вычисленные способности (capacities). Managed-объект, как <see cref="Genome"/> (Friflo допускает
|
||||
/// ссылку в компоненте). Источников урона пока нет — части на полном HP, способности = 1; каркас под
|
||||
/// болезни (A6) и хищников (горизонт). Растёт по фазам — контейнер один.
|
||||
/// Состояние здоровья особи (фазы A4–A6): активные хедифы (инстансы) + части тела, кровь и вычисленные
|
||||
/// способности (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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user