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>
This commit is contained in:
Leonid Pershin
2026-08-21 13:55:24 +03:00
co-authored by Cursor
parent a369308116
commit 4a52107bd2
19 changed files with 648 additions and 15 deletions
+9 -1
View File
@@ -46,11 +46,19 @@ internal static class DiseaseDefValidator
if (def.BaseChancePerDay < 0f
|| def.ColdChancePerDay < 0f
|| def.RainChancePerDay < 0f
|| def.SnowChancePerDay < 0f)
|| def.SnowChancePerDay < 0f
|| def.NodeContagionChancePerDay < 0f
|| def.ClassContagionChancePerDay < 0f)
{
throw new ContentLoadException($"DiseaseDef '{def.DefName}' chance fields cannot be negative.");
}
if (def.NodeContagionChancePerDay > 1f || def.ClassContagionChancePerDay > 1f)
{
throw new ContentLoadException(
$"DiseaseDef '{def.DefName}' contagion chances must be 01.");
}
DiseaseStage? previous = null;
foreach (var stage in def.Stages)
{
+16 -2
View File
@@ -31,8 +31,8 @@ public sealed class DiseaseStage
}
/// <summary>
/// A named disease: stages, weather vectors, lesson/attendance effects, temporary immunity.
/// Contagion between people is phase 79 — these defs may omit it.
/// A named disease: stages, weather vectors, lesson/attendance effects, temporary immunity,
/// and optional person-to-person contagion (node and/or class).
/// </summary>
public sealed class DiseaseDef : Def
{
@@ -58,4 +58,18 @@ public sealed class DiseaseDef : Def
/// <summary>Days of immunity to this def after recovery.</summary>
public float ImmunityDays { get; init; }
/// <summary>
/// Daily chance to infect each other person sharing the same map node. 0 — no node contagion.
/// </summary>
public float NodeContagionChancePerDay { get; init; }
/// <summary>
/// Daily chance to infect each classmate (same <c>ClassId</c>) while both are on campus.
/// 0 — no class contagion.
/// </summary>
public float ClassContagionChancePerDay { get; init; }
/// <summary>When true, the carrier can infect during incubation before stage effects apply.</summary>
public bool ContagiousDuringIncubation { get; init; }
}
+1
View File
@@ -16,6 +16,7 @@ internal static class EventDefValidator
EventTriggers.GenerationFailed,
EventTriggers.DirectorSummon,
EventTriggers.ParentMeeting,
EventTriggers.DiseaseOutbreak,
};
private static readonly HashSet<string> Actions = new(StringComparer.Ordinal)
+1
View File
@@ -14,6 +14,7 @@ public static class EventTriggers
public const string GenerationFailed = "generationFailed";
public const string DirectorSummon = "directorSummon";
public const string ParentMeeting = "parentMeeting";
public const string DiseaseOutbreak = "diseaseOutbreak";
}
public static class EventActions
@@ -757,6 +757,12 @@ internal static class PeopleDefValidator
throw new ContentLoadException(
$"BehaviorDef '{behavior.DefName}' diseaseVectorScale cannot be negative.");
}
if (behavior.DiseaseOutbreakMinNewCases < 0)
{
throw new ContentLoadException(
$"BehaviorDef '{behavior.DefName}' diseaseOutbreakMinNewCases cannot be negative.");
}
}
private static void ValidateTopic(TopicDef topic, DefCatalog catalog)
+6
View File
@@ -660,6 +660,12 @@ public sealed class BehaviorDef : Def
/// </summary>
public float DiseaseVectorScale { get; init; } = 1f;
/// <summary>
/// New contagion cases in one day at or above this raise a diseaseOutbreak world event.
/// 0 disables the toast.
/// </summary>
public int DiseaseOutbreakMinNewCases { get; init; } = 3;
public static IReadOnlyList<float> DefaultLessonMarkThresholds { get; } =
[
0.85f,