Files
h-school/src/HSchool.People/DiseaseEffects.cs
T
Leonid PershinandCursor 4a52107bd2 Add person-to-person disease contagion with outbreak notices.
Node/class contact rolls use pair+day seeds; vanilla respiratory diseases spread, and a threshold raises an info toast instead of per-sneeze noise.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 13:55:24 +03:00

164 lines
4.8 KiB
C#

using HSchool.Content;
namespace HSchool.People;
/// <summary>
/// Reads DiseaseDef stages for lesson gain, stay-home rolls and warmth decay.
/// Contagion rolls live in DiseaseSystem; this type only answers stage questions.
/// </summary>
public static class DiseaseEffects
{
public static bool TryStage(DiseaseDef disease, float severity, out DiseaseStage stage)
{
ArgumentNullException.ThrowIfNull(disease);
DiseaseStage? best = null;
foreach (var candidate in disease.Stages)
{
if (severity + 1e-6f < candidate.MinSeverity)
{
continue;
}
if (best is null || candidate.MinSeverity >= best.MinSeverity)
{
best = candidate;
}
}
if (best is null)
{
stage = null!;
return false;
}
stage = best;
return true;
}
public static bool IsIncubated(HealthCondition condition, DiseaseDef disease, DateTime now)
{
ArgumentNullException.ThrowIfNull(condition);
ArgumentNullException.ThrowIfNull(disease);
var elapsed = (DateTime.SpecifyKind(now, DateTimeKind.Utc) - condition.StartedAt).TotalDays;
return elapsed >= disease.IncubationDays;
}
/// <summary>Product of active post-incubation lesson factors (1 when healthy).</summary>
public static float LessonLearningFactor(Person person, DefCatalog catalog, DateTime now)
{
ArgumentNullException.ThrowIfNull(person);
ArgumentNullException.ThrowIfNull(catalog);
var list = person.Conditions;
if (list is null || list.Count == 0)
{
return 1f;
}
var factor = 1f;
foreach (var condition in list)
{
if (!catalog.Diseases.TryGetValue(condition.DefName, out var disease) || disease.Abstract)
{
continue;
}
if (!IsIncubated(condition, disease, now) || !TryStage(disease, condition.Severity, out var stage))
{
continue;
}
factor *= stage.LessonLearningFactor;
}
return factor;
}
/// <summary>Highest stay-home chance among incubated conditions.</summary>
public static float StayHomeChance(Person person, DefCatalog catalog, DateTime now)
{
ArgumentNullException.ThrowIfNull(person);
ArgumentNullException.ThrowIfNull(catalog);
var list = person.Conditions;
if (list is null || list.Count == 0)
{
return 0f;
}
var chance = 0f;
foreach (var condition in list)
{
if (!catalog.Diseases.TryGetValue(condition.DefName, out var disease) || disease.Abstract)
{
continue;
}
if (!IsIncubated(condition, disease, now) || !TryStage(disease, condition.Severity, out var stage))
{
continue;
}
if (stage.StayHomeChance > chance)
{
chance = stage.StayHomeChance;
}
}
return chance;
}
/// <summary>
/// Deterministic per person+day: whether illness keeps them off campus today.
/// </summary>
public static bool ShouldStayHome(Person person, DefCatalog catalog, int peopleSeed, DateTime now)
{
var chance = StayHomeChance(person, catalog, now);
if (chance <= 0f)
{
return false;
}
if (chance >= 1f)
{
return true;
}
var dayNumber = DateOnly.FromDateTime(DateTime.SpecifyKind(now, DateTimeKind.Utc)).DayNumber;
var roll = Seed.Mix(peopleSeed, person.Id, dayNumber, Seed.DiseaseSalt);
var unit = (roll & int.MaxValue) / (float)int.MaxValue;
return unit < chance;
}
/// <summary>Max Warmth decay multiplier from incubated stages (1 when healthy).</summary>
public static float WarmthDecayFactor(Person person, DefCatalog catalog, DateTime now)
{
ArgumentNullException.ThrowIfNull(person);
ArgumentNullException.ThrowIfNull(catalog);
var list = person.Conditions;
if (list is null || list.Count == 0)
{
return 1f;
}
var factor = 1f;
foreach (var condition in list)
{
if (!catalog.Diseases.TryGetValue(condition.DefName, out var disease) || disease.Abstract)
{
continue;
}
if (!IsIncubated(condition, disease, now) || !TryStage(disease, condition.Severity, out var stage))
{
continue;
}
if (stage.WarmthDecayFactor > factor)
{
factor = stage.WarmthDecayFactor;
}
}
return factor;
}
}