Merge branch 'phase/84-wet-aftereffect'

This commit is contained in:
Leonid Pershin
2026-08-21 20:35:51 +03:00
16 changed files with 456 additions and 20 deletions
+6 -1
View File
@@ -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 01).");
}
if (def.NodeContagionChancePerDay > 1f || def.ClassContagionChancePerDay > 1f)
+22
View File
@@ -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>
@@ -65,6 +75,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 01.");
}
if (behavior.DiseaseOutbreakMinNewCases < 0)
{
throw new ContentLoadException(
+8
View File
@@ -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.
+30 -4
View File
@@ -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)
+1
View File
@@ -29,6 +29,7 @@ public static class Seed
public const int ContagionSalt = 21;
public const int GradeTrailSalt = 22;
public const int NurseSalt = 23;
public const int WetSalt = 24;
/// <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,
// Nurse tend scales with Medicine skill (slice 14 phase 80).
@@ -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,
@@ -31,6 +53,8 @@
"coldChancePerDay": 0.06,
"rainChancePerDay": 0.02,
"snowChancePerDay": 0.05,
"wetChancePerDay": 0.05,
"aftereffectMinSeverity": 0.55,
"nodeContagionChancePerDay": 0.65,
"classContagionChancePerDay": 0.35,
"contagiousDuringIncubation": true,
@@ -50,6 +74,7 @@
"immunityDays": 10,
"baseChancePerDay": 0.012,
"rainChancePerDay": 0.01,
"aftereffectMinSeverity": 0.75,
"nodeContagionChancePerDay": 0.3,
"classContagionChancePerDay": 0,
"contagiousDuringIncubation": false,
@@ -73,6 +98,8 @@
"nurseSeekSeverity": 0.4,
"tendSeverityPerDay": -0.25,
"tendProgressPerDay": 0.35,
"wetChancePerDay": 0.04,
"aftereffectMinSeverity": 0.85,
"stages": [
{ "minSeverity": 0, "severityPerDay": 0.2, "progressPerDay": 0.04, "lessonLearningFactor": 0.8, "stayHomeChance": 0.2, "warmthDecayFactor": 1.05, "talkWeightFactor": 0.75, "effect": "OtitisEffectMild" },
{ "minSeverity": 0.45, "severityPerDay": 0.05, "progressPerDay": 0.1, "lessonLearningFactor": 0.55, "stayHomeChance": 0.55, "warmthDecayFactor": 1.1, "talkWeightFactor": 0.45, "effect": "OtitisEffectModerate" },
@@ -263,5 +263,7 @@
"OtitisEffectMild": "Earache, hears worse",
"OtitisEffectModerate": "Pain hurts lessons and talk",
"OtitisEffectHeavy": "Severe pain, barely talks",
"Wet": "Wet",
"DiseaseAftereffect": "Recovering",
"core": "Core",
}
@@ -263,5 +263,7 @@
"OtitisEffectMild": "Боль в ухе, хуже слышит",
"OtitisEffectModerate": "Боль мешает уроку и разговору",
"OtitisEffectHeavy": "Сильная боль, почти не общается",
"Wet": "Мокрый",
"DiseaseAftereffect": "Ослабление после болезни",
"core": "Базовая игра",
}
+87 -4
View File
@@ -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";