Add wet condition and disease aftereffect (phase 84).
This commit is contained in:
@@ -12,20 +12,20 @@
|
||||
|
||||
## Задачи
|
||||
|
||||
- [ ] Короткое состояние «мокрый» после улицы под дождём/снегом (или эквивалент на слое 14)
|
||||
- [ ] Мокрый повышает вектор респираторных болезней (поля def)
|
||||
- [ ] Aftereffect после тяжёлой болезни — короткий ослабленный хвост (урок/нужды), данные
|
||||
- [ ] Сход сами / у медсестры не обязателен для мокрого (высох), aftereffect — по кривой
|
||||
- [ ] Без новой вкладки, если здоровье (80) уже показывает условия
|
||||
- [ ] Опционально: один info «средний след за период» без аттестации — только если не раздувает
|
||||
- [x] Короткое состояние «мокрый» после улицы под дождём/снегом (или эквивалент на слое 14)
|
||||
- [x] Мокрый повышает вектор респираторных болезней (поля def)
|
||||
- [x] Aftereffect после тяжёлой болезни — короткий ослабленный хвост (урок/нужды), данные
|
||||
- [x] Сход сами / у медсестры не обязателен для мокрого (высох), aftereffect — по кривой
|
||||
- [x] Без новой вкладки, если здоровье (80) уже показывает условия
|
||||
- [x] Опционально: один info «средний след за период» без аттестации — только если не раздувает
|
||||
фазу; иначе не делать
|
||||
|
||||
## Тесты, без которых фаза не закрыта
|
||||
|
||||
- [ ] Приход под дождём чаще даёт «мокрый», чем ясная погода
|
||||
- [ ] Мокрый повышает шанс ванильной респираторной относительно сухого контроля
|
||||
- [ ] После тяжёлой болезни aftereffect появляется и сходит за ограниченное время
|
||||
- [ ] Сейв сохраняет оба вида условий
|
||||
- [x] Приход под дождём чаще даёт «мокрый», чем ясная погода
|
||||
- [x] Мокрый повышает шанс ванильной респираторной относительно сухого контроля
|
||||
- [x] После тяжёлой болезни aftereffect появляется и сходит за ограниченное время
|
||||
- [x] Сейв сохраняет оба вида условий
|
||||
|
||||
## Критерий готовности
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -15,6 +15,8 @@ public class DiseaseDefTests
|
||||
Assert.True(catalog.Diseases.ContainsKey("Influenza"));
|
||||
Assert.True(catalog.Diseases.ContainsKey("StomachBug"));
|
||||
Assert.True(catalog.Diseases.ContainsKey("Otitis"));
|
||||
Assert.True(catalog.Diseases.ContainsKey(DiseaseFamilies.WetDefName));
|
||||
Assert.True(catalog.Diseases.ContainsKey(DiseaseFamilies.AftereffectDefName));
|
||||
|
||||
var cold = catalog.Diseases["CommonCold"];
|
||||
Assert.Equal(DiseaseFamilies.Respiratory, cold.Family);
|
||||
@@ -22,6 +24,9 @@ public class DiseaseDefTests
|
||||
Assert.True(cold.Stages.Count >= 2);
|
||||
Assert.Equal(0.5f, cold.IncubationDays);
|
||||
Assert.Equal(14f, cold.ImmunityDays);
|
||||
Assert.Equal(0.07f, cold.WetChancePerDay);
|
||||
Assert.Equal(0.55f, catalog.Diseases["Influenza"].AftereffectMinSeverity);
|
||||
Assert.Equal(DiseaseFamilies.Condition, catalog.Diseases[DiseaseFamilies.WetDefName].Family);
|
||||
Assert.Equal(0.55f, cold.NodeContagionChancePerDay);
|
||||
Assert.Equal(0.25f, cold.ClassContagionChancePerDay);
|
||||
Assert.True(cold.ContagiousDuringIncubation);
|
||||
@@ -30,7 +35,11 @@ public class DiseaseDefTests
|
||||
|
||||
Assert.Equal("ОРВИ", catalog.Label("ru", cold));
|
||||
Assert.Equal("Common cold", catalog.Label("en", cold));
|
||||
Assert.Equal("Мокрый", catalog.Label("ru", catalog.Diseases[DiseaseFamilies.WetDefName]));
|
||||
Assert.Equal("Wet", catalog.Label("en", catalog.Diseases[DiseaseFamilies.WetDefName]));
|
||||
Assert.Equal(1f, catalog.BehaviorRules!.DiseaseVectorScale);
|
||||
Assert.Equal(0.75f, catalog.BehaviorRules.WetRainChance);
|
||||
Assert.Equal(0.85f, catalog.BehaviorRules.WetSnowChance);
|
||||
Assert.Equal(3, catalog.BehaviorRules.DiseaseOutbreakMinNewCases);
|
||||
Assert.True(catalog.Events.ContainsKey("DiseaseOutbreak"));
|
||||
Assert.Equal(EventTriggers.DiseaseOutbreak, catalog.Events["DiseaseOutbreak"].Trigger);
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
using HSchool.Content;
|
||||
|
||||
namespace HSchool.People.Tests;
|
||||
|
||||
public class WetAftereffectTests
|
||||
{
|
||||
[Fact]
|
||||
public void HeavyDisease_LeavesAftereffectThatClears()
|
||||
{
|
||||
var catalog = LoadVanilla();
|
||||
var started = new DateTime(2012, 4, 3, 8, 0, 0, DateTimeKind.Utc);
|
||||
var person = Blank("p1");
|
||||
HealthConditions.Add(
|
||||
person,
|
||||
new HealthCondition
|
||||
{
|
||||
DefName = "Influenza",
|
||||
Severity = 0.85f,
|
||||
Progress = 0.99f,
|
||||
Source = "cold",
|
||||
StartedAt = started,
|
||||
});
|
||||
|
||||
Assert.True(HealthConditions.Tick(
|
||||
person,
|
||||
peopleSeed: 3,
|
||||
dayNumber: 50,
|
||||
gameMinutes: 24 * 60,
|
||||
catalog,
|
||||
started.AddDays(3)));
|
||||
|
||||
Assert.NotNull(person.Conditions);
|
||||
Assert.Contains(person.Conditions!, row => row.DefName == DiseaseFamilies.AftereffectDefName);
|
||||
Assert.DoesNotContain(person.Conditions!, row => row.DefName == "Influenza");
|
||||
Assert.True(DiseaseImmunities.IsImmune(person, "Influenza", started.AddDays(4)));
|
||||
|
||||
// Aftereffect clears on its stage curve within a few game days — not a year-long chronicle.
|
||||
for (var day = 0; day < 14 && person.Conditions is { Count: > 0 }; day++)
|
||||
{
|
||||
HealthConditions.Tick(
|
||||
person,
|
||||
peopleSeed: 3,
|
||||
dayNumber: 51 + day,
|
||||
gameMinutes: 24 * 60,
|
||||
catalog,
|
||||
started.AddDays(4 + day));
|
||||
}
|
||||
|
||||
Assert.True(
|
||||
person.Conditions is null
|
||||
|| person.Conditions.All(row => row.DefName != DiseaseFamilies.AftereffectDefName));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RosterJson_RoundTripsWetAndAftereffect()
|
||||
{
|
||||
var roster = Fixtures.Generate(Fixtures.Classrooms(4));
|
||||
var pupil = roster.People.First(person => person.IsStudent && !person.IsParent);
|
||||
var started = new DateTime(2012, 4, 3, 9, 0, 0, DateTimeKind.Utc);
|
||||
HealthConditions.Add(
|
||||
pupil,
|
||||
new HealthCondition
|
||||
{
|
||||
DefName = DiseaseFamilies.WetDefName,
|
||||
Severity = 0.6f,
|
||||
Progress = 0.1f,
|
||||
Source = "rain",
|
||||
StartedAt = started,
|
||||
});
|
||||
HealthConditions.Add(
|
||||
pupil,
|
||||
new HealthCondition
|
||||
{
|
||||
DefName = DiseaseFamilies.AftereffectDefName,
|
||||
Severity = 0.4f,
|
||||
Progress = 0.2f,
|
||||
Source = "Influenza",
|
||||
StartedAt = started.AddDays(-1),
|
||||
});
|
||||
|
||||
var json = RosterJson.Serialize(RosterDocument.From(1, roster));
|
||||
Assert.Contains($"\"defName\": \"{DiseaseFamilies.WetDefName}\"", json, StringComparison.Ordinal);
|
||||
Assert.Contains($"\"defName\": \"{DiseaseFamilies.AftereffectDefName}\"", json, StringComparison.Ordinal);
|
||||
|
||||
var loaded = RosterJson.Parse(json).ToRoster();
|
||||
var loadedPupil = loaded.People.First(person => person.Id.Equals(pupil.Id, StringComparison.Ordinal));
|
||||
Assert.NotNull(loadedPupil.Conditions);
|
||||
Assert.Equal(2, loadedPupil.Conditions!.Count);
|
||||
Assert.Contains(loadedPupil.Conditions, row => row.DefName == DiseaseFamilies.WetDefName && row.Source == "rain");
|
||||
Assert.Contains(
|
||||
loadedPupil.Conditions,
|
||||
row => row.DefName == DiseaseFamilies.AftereffectDefName && row.Source == "Influenza");
|
||||
}
|
||||
|
||||
private static DefCatalog LoadVanilla()
|
||||
{
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "vanilla");
|
||||
return new CatalogLoader().Load(
|
||||
[CatalogLoader.CorePackId],
|
||||
PackDocuments.FromDirectory(CatalogLoader.CorePackId, root));
|
||||
}
|
||||
|
||||
private static Person Blank(string id)
|
||||
{
|
||||
var cases = new CaseTable
|
||||
{
|
||||
Nom = id,
|
||||
Gen = id,
|
||||
Dat = id,
|
||||
Acc = id,
|
||||
Ins = id,
|
||||
Pre = id,
|
||||
};
|
||||
return new Person
|
||||
{
|
||||
Id = id,
|
||||
FamilyId = "f",
|
||||
Female = false,
|
||||
BirthDate = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
Name = new PersonName(id, id, id, cases, cases, cases),
|
||||
IsStudent = true,
|
||||
IsStaff = false,
|
||||
IsParent = false,
|
||||
Numbers = new Dictionary<string, int>(StringComparer.Ordinal),
|
||||
Choices = new Dictionary<string, string>(StringComparer.Ordinal),
|
||||
Skills = new Dictionary<string, int>(StringComparer.Ordinal),
|
||||
Traits = [],
|
||||
Needs = new Dictionary<string, float>(StringComparer.Ordinal),
|
||||
Opinions = new Dictionary<string, int>(StringComparer.Ordinal),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using HSchool.Ai;
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
|
||||
namespace HSchool.Simulation.Tests;
|
||||
|
||||
public class WetAftereffectTests
|
||||
{
|
||||
private static readonly DateTime TuesdayMorning = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
[Fact]
|
||||
public void RainArrival_AssignsWetMoreThanClear()
|
||||
{
|
||||
using var rainSchool = OpenStaffed(seed: 42);
|
||||
using var clearSchool = OpenStaffed(seed: 42);
|
||||
rainSchool.ForceWeather(new OutdoorWeather(12f, Precipitation.Rain));
|
||||
clearSchool.ForceWeather(new OutdoorWeather(12f, Precipitation.None));
|
||||
|
||||
var rainWet = CountWet(rainSchool);
|
||||
var clearWet = CountWet(clearSchool);
|
||||
|
||||
Assert.True(rainWet > clearWet);
|
||||
Assert.True(rainWet > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wet_RaisesRespiratoryOnsetVsDryControl()
|
||||
{
|
||||
using var wetSchool = OpenStaffed(seed: 7);
|
||||
using var drySchool = OpenStaffed(seed: 7);
|
||||
wetSchool.ForceWeather(new OutdoorWeather(14f, Precipitation.None));
|
||||
drySchool.ForceWeather(new OutdoorWeather(14f, Precipitation.None));
|
||||
|
||||
foreach (var person in wetSchool.Roster!.People.Where(row => row.IsStudent || row.IsStaff))
|
||||
{
|
||||
HealthConditions.Add(
|
||||
person,
|
||||
new HealthCondition
|
||||
{
|
||||
DefName = DiseaseFamilies.WetDefName,
|
||||
Severity = 0.7f,
|
||||
Progress = 0f,
|
||||
Source = "rain",
|
||||
StartedAt = TuesdayMorning,
|
||||
});
|
||||
}
|
||||
|
||||
var wetHits = CountRespiratoryOnset(wetSchool);
|
||||
var dryHits = CountRespiratoryOnset(drySchool);
|
||||
|
||||
Assert.True(wetHits > dryHits);
|
||||
Assert.True(wetHits > 0);
|
||||
}
|
||||
|
||||
private static int CountWet(School school)
|
||||
{
|
||||
school.LastDiseaseDay = int.MinValue;
|
||||
DiseaseSystem.Apply(school, gameMinutes: 0);
|
||||
return school.Roster!.People.Count(person =>
|
||||
person.Conditions is not null
|
||||
&& person.Conditions.Any(row => row.DefName == DiseaseFamilies.WetDefName));
|
||||
}
|
||||
|
||||
private static int CountRespiratoryOnset(School school)
|
||||
{
|
||||
school.LastDiseaseDay = int.MinValue;
|
||||
DiseaseSystem.Apply(school, gameMinutes: 0);
|
||||
return school.Roster!.People.Count(person =>
|
||||
person.Conditions is not null
|
||||
&& person.Conditions.Any(row =>
|
||||
row.DefName is "CommonCold" or "Influenza" or "Otitis"));
|
||||
}
|
||||
|
||||
private static School OpenStaffed(int seed)
|
||||
{
|
||||
var (catalog, map) = Vanilla();
|
||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: seed, "Russia", TuesdayMorning);
|
||||
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: seed, "Russia", TuesdayMorning);
|
||||
var school = School.Create(1, "Мокрый", TuesdayMorning, catalog, map);
|
||||
school.InstallPeople(roster, seed: seed, "Russia", pool);
|
||||
school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 10_000);
|
||||
return school;
|
||||
}
|
||||
|
||||
private static (DefCatalog Catalog, MapLayout Map) Vanilla()
|
||||
{
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "vanilla");
|
||||
var documents = new List<ContentDocument>();
|
||||
foreach (var path in Directory.EnumerateFiles(root, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (!path.EndsWith(".jsonc", StringComparison.OrdinalIgnoreCase)
|
||||
&& !path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var relative = Path.GetRelativePath(root, path).Replace('\\', '/');
|
||||
documents.Add(new ContentDocument(
|
||||
CatalogLoader.CorePackId,
|
||||
relative,
|
||||
File.ReadAllText(path)));
|
||||
}
|
||||
|
||||
var catalog = new CatalogLoader().Load([CatalogLoader.CorePackId], documents);
|
||||
var map = CatalogLoader.LastDefaultMap([CatalogLoader.CorePackId], documents);
|
||||
Assert.NotNull(map);
|
||||
return (catalog, map);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user