Add wet condition and disease aftereffect (phase 84).
This commit is contained in:
@@ -7,6 +7,7 @@ internal static class DiseaseDefValidator
|
||||
DiseaseFamilies.Respiratory,
|
||||
DiseaseFamilies.Gastrointestinal,
|
||||
DiseaseFamilies.Ent,
|
||||
DiseaseFamilies.Condition,
|
||||
};
|
||||
|
||||
public static void Validate(DefCatalog catalog)
|
||||
@@ -47,10 +48,14 @@ internal static class DiseaseDefValidator
|
||||
|| def.ColdChancePerDay < 0f
|
||||
|| def.RainChancePerDay < 0f
|
||||
|| def.SnowChancePerDay < 0f
|
||||
|| def.WetChancePerDay < 0f
|
||||
|| def.AftereffectMinSeverity < 0f
|
||||
|| def.AftereffectMinSeverity > 1f
|
||||
|| def.NodeContagionChancePerDay < 0f
|
||||
|| def.ClassContagionChancePerDay < 0f)
|
||||
{
|
||||
throw new ContentLoadException($"DiseaseDef '{def.DefName}' chance fields cannot be negative.");
|
||||
throw new ContentLoadException(
|
||||
$"DiseaseDef '{def.DefName}' chance/aftereffect fields must be non-negative (aftereffect 0–1).");
|
||||
}
|
||||
|
||||
if (def.NodeContagionChancePerDay > 1f || def.ClassContagionChancePerDay > 1f)
|
||||
|
||||
@@ -8,6 +8,16 @@ public static class DiseaseFamilies
|
||||
public const string Gastrointestinal = "gastrointestinal";
|
||||
|
||||
public const string Ent = "ent";
|
||||
|
||||
/// <summary>
|
||||
/// Non-infectious short conditions (wet, post-disease aftereffect). Same HealthCondition list,
|
||||
/// but onset/contagion/immunity skips this family.
|
||||
/// </summary>
|
||||
public const string Condition = "condition";
|
||||
|
||||
public const string WetDefName = "Wet";
|
||||
|
||||
public const string AftereffectDefName = "DiseaseAftereffect";
|
||||
}
|
||||
|
||||
/// <summary>One severity band on a <see cref="DiseaseDef"/> curve.</summary>
|
||||
@@ -56,6 +66,18 @@ public sealed class DiseaseDef : Def
|
||||
|
||||
public float SnowChancePerDay { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Extra daily onset chance while the person has the Wet condition. Packs without the field
|
||||
/// add nothing.
|
||||
/// </summary>
|
||||
public float WetChancePerDay { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// On recovery at or above this severity, grant <see cref="DiseaseFamilies.AftereffectDefName"/>.
|
||||
/// 0 — no aftereffect.
|
||||
/// </summary>
|
||||
public float AftereffectMinSeverity { get; init; }
|
||||
|
||||
/// <summary>Days of immunity to this def after recovery.</summary>
|
||||
public float ImmunityDays { get; init; }
|
||||
|
||||
|
||||
@@ -788,6 +788,13 @@ internal static class PeopleDefValidator
|
||||
$"BehaviorDef '{behavior.DefName}' diseaseVectorScale cannot be negative.");
|
||||
}
|
||||
|
||||
if (behavior.WetRainChance < 0f || behavior.WetRainChance > 1f
|
||||
|| behavior.WetSnowChance < 0f || behavior.WetSnowChance > 1f)
|
||||
{
|
||||
throw new ContentLoadException(
|
||||
$"BehaviorDef '{behavior.DefName}' wetRainChance and wetSnowChance must be 0–1.");
|
||||
}
|
||||
|
||||
if (behavior.DiseaseOutbreakMinNewCases < 0)
|
||||
{
|
||||
throw new ContentLoadException(
|
||||
|
||||
@@ -688,6 +688,14 @@ public sealed class BehaviorDef : Def
|
||||
/// </summary>
|
||||
public float DiseaseVectorScale { get; init; } = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Per-person chance to pick up Wet when coming to school under rain. 0 disables.
|
||||
/// </summary>
|
||||
public float WetRainChance { get; init; }
|
||||
|
||||
/// <summary>Same as <see cref="WetRainChance"/> under snow.</summary>
|
||||
public float WetSnowChance { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// New contagion cases in one day at or above this raise a diseaseOutbreak world event.
|
||||
/// 0 disables the toast.
|
||||
|
||||
@@ -153,16 +153,42 @@ public static class HealthConditions
|
||||
foreach (var done in recovered)
|
||||
{
|
||||
list.Remove(done);
|
||||
if (catalog is not null
|
||||
&& catalog.Diseases.TryGetValue(done.DefName, out var disease)
|
||||
&& !disease.Abstract
|
||||
&& disease.ImmunityDays > 0f)
|
||||
if (catalog is null
|
||||
|| !catalog.Diseases.TryGetValue(done.DefName, out var disease)
|
||||
|| disease.Abstract)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Condition-family rows (wet / aftereffect) dry off without immunity or a second tail.
|
||||
if (disease.Family.Equals(DiseaseFamilies.Condition, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (disease.ImmunityDays > 0f)
|
||||
{
|
||||
DiseaseImmunities.Grant(
|
||||
person,
|
||||
done.DefName,
|
||||
clock.AddDays(disease.ImmunityDays));
|
||||
}
|
||||
|
||||
if (disease.AftereffectMinSeverity > 0f
|
||||
&& done.Severity + 1e-6f >= disease.AftereffectMinSeverity
|
||||
&& !Has(person, DiseaseFamilies.AftereffectDefName)
|
||||
&& catalog.Diseases.ContainsKey(DiseaseFamilies.AftereffectDefName))
|
||||
{
|
||||
list.Add(new HealthCondition
|
||||
{
|
||||
DefName = DiseaseFamilies.AftereffectDefName,
|
||||
Severity = 0.45f,
|
||||
Progress = 0f,
|
||||
Source = done.DefName,
|
||||
StartedAt = clock,
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (list.Count == 0)
|
||||
|
||||
@@ -28,6 +28,7 @@ public static class Seed
|
||||
public const int DiseaseSalt = 20;
|
||||
public const int ContagionSalt = 21;
|
||||
public const int GradeTrailSalt = 22;
|
||||
public const int WetSalt = 23;
|
||||
|
||||
/// <summary>A stream that belongs to the school rather than to one family.</summary>
|
||||
public static int ForSchool(int schoolSeed, int salt) => Mix(schoolSeed, familyIndex: -1, salt);
|
||||
|
||||
@@ -130,6 +130,9 @@
|
||||
"gradeTrailSummonChance": 0.35,
|
||||
// Scales DiseaseDef onset from weather/base (slice 14 phase 78). 0 disables natural onset.
|
||||
"diseaseVectorScale": 1,
|
||||
// Commute under rain/snow → short Wet condition (slice 15 phase 84). Clear street stays dry.
|
||||
"wetRainChance": 0.75,
|
||||
"wetSnowChance": 0.85,
|
||||
// Contagion cases in one day at or above this raise diseaseOutbreak (slice 14 phase 79).
|
||||
"diseaseOutbreakMinNewCases": 3,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,24 @@
|
||||
[
|
||||
{
|
||||
"defName": "Wet",
|
||||
"family": "condition",
|
||||
"incubationDays": 0,
|
||||
"immunityDays": 0,
|
||||
// Dries in under a day on its own — nurse is not required.
|
||||
"stages": [
|
||||
{ "minSeverity": 0, "severityPerDay": -0.4, "progressPerDay": 2.5, "lessonLearningFactor": 1, "stayHomeChance": 0, "warmthDecayFactor": 1.05 },
|
||||
],
|
||||
},
|
||||
{
|
||||
"defName": "DiseaseAftereffect",
|
||||
"family": "condition",
|
||||
"incubationDays": 0,
|
||||
"immunityDays": 0,
|
||||
// Short post-illness weakness: lighter lessons, slightly hungrier warmth.
|
||||
"stages": [
|
||||
{ "minSeverity": 0, "severityPerDay": -0.15, "progressPerDay": 0.55, "lessonLearningFactor": 0.8, "stayHomeChance": 0, "warmthDecayFactor": 1.12 },
|
||||
],
|
||||
},
|
||||
{
|
||||
"defName": "CommonCold",
|
||||
"family": "respiratory",
|
||||
@@ -9,6 +29,8 @@
|
||||
"coldChancePerDay": 0.08,
|
||||
"rainChancePerDay": 0.03,
|
||||
"snowChancePerDay": 0.04,
|
||||
"wetChancePerDay": 0.07,
|
||||
"aftereffectMinSeverity": 0.9,
|
||||
"nodeContagionChancePerDay": 0.55,
|
||||
"classContagionChancePerDay": 0.25,
|
||||
"contagiousDuringIncubation": true,
|
||||
@@ -28,6 +50,8 @@
|
||||
"coldChancePerDay": 0.06,
|
||||
"rainChancePerDay": 0.02,
|
||||
"snowChancePerDay": 0.05,
|
||||
"wetChancePerDay": 0.05,
|
||||
"aftereffectMinSeverity": 0.55,
|
||||
"nodeContagionChancePerDay": 0.65,
|
||||
"classContagionChancePerDay": 0.35,
|
||||
"contagiousDuringIncubation": true,
|
||||
@@ -44,6 +68,7 @@
|
||||
"immunityDays": 10,
|
||||
"baseChancePerDay": 0.012,
|
||||
"rainChancePerDay": 0.01,
|
||||
"aftereffectMinSeverity": 0.75,
|
||||
"nodeContagionChancePerDay": 0.3,
|
||||
"classContagionChancePerDay": 0,
|
||||
"contagiousDuringIncubation": false,
|
||||
@@ -61,6 +86,8 @@
|
||||
"coldBelowC": 8,
|
||||
"coldChancePerDay": 0.05,
|
||||
"snowChancePerDay": 0.03,
|
||||
"wetChancePerDay": 0.04,
|
||||
"aftereffectMinSeverity": 0.85,
|
||||
"stages": [
|
||||
{ "minSeverity": 0, "severityPerDay": 0.2, "progressPerDay": 0.04, "lessonLearningFactor": 0.8, "stayHomeChance": 0.2, "warmthDecayFactor": 1.05 },
|
||||
{ "minSeverity": 0.45, "severityPerDay": 0.05, "progressPerDay": 0.1, "lessonLearningFactor": 0.55, "stayHomeChance": 0.55, "warmthDecayFactor": 1.1 },
|
||||
|
||||
@@ -249,5 +249,7 @@
|
||||
"Influenza": "Influenza",
|
||||
"StomachBug": "Stomach bug",
|
||||
"Otitis": "Ear infection",
|
||||
"Wet": "Wet",
|
||||
"DiseaseAftereffect": "Recovering",
|
||||
"core": "Core",
|
||||
}
|
||||
|
||||
@@ -249,5 +249,7 @@
|
||||
"Influenza": "Грипп",
|
||||
"StomachBug": "Кишечная инфекция",
|
||||
"Otitis": "Отит",
|
||||
"Wet": "Мокрый",
|
||||
"DiseaseAftereffect": "Ослабление после болезни",
|
||||
"core": "Базовая игра",
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ internal static class DiseaseSystem
|
||||
if (school.LastDiseaseDay != dayNumber)
|
||||
{
|
||||
school.LastDiseaseDay = dayNumber;
|
||||
changed |= TryWet(school);
|
||||
changed |= TryOnset(school);
|
||||
changed |= TryContagion(school);
|
||||
}
|
||||
@@ -58,6 +59,70 @@ internal static class DiseaseSystem
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static bool TryWet(School school)
|
||||
{
|
||||
var catalog = school.Catalog!;
|
||||
var rules = catalog.BehaviorRules;
|
||||
if (rules is null || !catalog.Diseases.ContainsKey(DiseaseFamilies.WetDefName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var chance = school.Weather.Precipitation switch
|
||||
{
|
||||
Precipitation.Rain => rules.WetRainChance,
|
||||
Precipitation.Snow => rules.WetSnowChance,
|
||||
_ => 0f,
|
||||
};
|
||||
if (chance <= 0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var dayNumber = DateOnly.FromDateTime(school.Clock.Time).DayNumber;
|
||||
var source = school.Weather.Precipitation == Precipitation.Snow ? "snow" : "rain";
|
||||
var changed = false;
|
||||
|
||||
foreach (var person in school.Roster!.People)
|
||||
{
|
||||
if (!person.IsStudent && !person.IsStaff)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (school.Plans.TryGetValue(person.Id, out var plan) && !plan.Comes)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (HealthConditions.Has(person, DiseaseFamilies.WetDefName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var roll = Seed.Mix(school.PeopleSeed, person.Id, dayNumber, Seed.WetSalt);
|
||||
var unit = (roll & int.MaxValue) / (float)int.MaxValue;
|
||||
if (unit >= chance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
HealthConditions.Add(
|
||||
person,
|
||||
new HealthCondition
|
||||
{
|
||||
DefName = DiseaseFamilies.WetDefName,
|
||||
Severity = 0.65f,
|
||||
Progress = 0f,
|
||||
Source = source,
|
||||
StartedAt = school.Clock.Time,
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static bool TryOnset(School school)
|
||||
{
|
||||
var catalog = school.Catalog!;
|
||||
@@ -78,16 +143,19 @@ internal static class DiseaseSystem
|
||||
continue;
|
||||
}
|
||||
|
||||
var wet = HealthConditions.Has(person, DiseaseFamilies.WetDefName);
|
||||
|
||||
foreach (var disease in catalog.Diseases.Values)
|
||||
{
|
||||
if (disease.Abstract
|
||||
|| disease.Family.Equals(DiseaseFamilies.Condition, StringComparison.Ordinal)
|
||||
|| HealthConditions.Has(person, disease.DefName)
|
||||
|| DiseaseImmunities.IsImmune(person, disease.DefName, school.Clock.Time))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var chance = OnsetChance(disease, weather, rules.DiseaseVectorScale);
|
||||
var chance = OnsetChance(disease, weather, rules.DiseaseVectorScale, wet);
|
||||
if (chance <= 0f)
|
||||
{
|
||||
continue;
|
||||
@@ -108,7 +176,7 @@ internal static class DiseaseSystem
|
||||
DefName = disease.DefName,
|
||||
Severity = 0.05f,
|
||||
Progress = 0f,
|
||||
Source = SourceOf(disease, weather),
|
||||
Source = SourceOf(disease, weather, wet),
|
||||
StartedAt = school.Clock.Time,
|
||||
});
|
||||
changed = true;
|
||||
@@ -145,6 +213,7 @@ internal static class DiseaseSystem
|
||||
{
|
||||
if (!catalog.Diseases.TryGetValue(condition.DefName, out var disease)
|
||||
|| disease.Abstract
|
||||
|| disease.Family.Equals(DiseaseFamilies.Condition, StringComparison.Ordinal)
|
||||
|| !CanTransmit(condition, disease, now))
|
||||
{
|
||||
continue;
|
||||
@@ -348,7 +417,11 @@ internal static class DiseaseSystem
|
||||
return map;
|
||||
}
|
||||
|
||||
internal static float OnsetChance(DiseaseDef disease, OutdoorWeather weather, float vectorScale)
|
||||
internal static float OnsetChance(
|
||||
DiseaseDef disease,
|
||||
OutdoorWeather weather,
|
||||
float vectorScale,
|
||||
bool wet = false)
|
||||
{
|
||||
if (vectorScale <= 0f)
|
||||
{
|
||||
@@ -368,11 +441,21 @@ internal static class DiseaseSystem
|
||||
_ => 0f,
|
||||
};
|
||||
|
||||
if (wet)
|
||||
{
|
||||
chance += disease.WetChancePerDay;
|
||||
}
|
||||
|
||||
return Math.Clamp(chance * vectorScale, 0f, 1f);
|
||||
}
|
||||
|
||||
private static string SourceOf(DiseaseDef disease, OutdoorWeather weather)
|
||||
private static string SourceOf(DiseaseDef disease, OutdoorWeather weather, bool wet)
|
||||
{
|
||||
if (wet && disease.WetChancePerDay > 0f)
|
||||
{
|
||||
return "wet";
|
||||
}
|
||||
|
||||
if (disease.ColdBelowC is { } below && weather.TemperatureC <= below)
|
||||
{
|
||||
return "cold";
|
||||
|
||||
Reference in New Issue
Block a user