Merge branch 'phase/84-wet-aftereffect'
This commit is contained in:
@@ -12,20 +12,20 @@
|
|||||||
|
|
||||||
## Задачи
|
## Задачи
|
||||||
|
|
||||||
- [ ] Короткое состояние «мокрый» после улицы под дождём/снегом (или эквивалент на слое 14)
|
- [x] Короткое состояние «мокрый» после улицы под дождём/снегом (или эквивалент на слое 14)
|
||||||
- [ ] Мокрый повышает вектор респираторных болезней (поля def)
|
- [x] Мокрый повышает вектор респираторных болезней (поля def)
|
||||||
- [ ] Aftereffect после тяжёлой болезни — короткий ослабленный хвост (урок/нужды), данные
|
- [x] Aftereffect после тяжёлой болезни — короткий ослабленный хвост (урок/нужды), данные
|
||||||
- [ ] Сход сами / у медсестры не обязателен для мокрого (высох), aftereffect — по кривой
|
- [x] Сход сами / у медсестры не обязателен для мокрого (высох), aftereffect — по кривой
|
||||||
- [ ] Без новой вкладки, если здоровье (80) уже показывает условия
|
- [x] Без новой вкладки, если здоровье (80) уже показывает условия
|
||||||
- [ ] Опционально: один info «средний след за период» без аттестации — только если не раздувает
|
- [x] Опционально: один info «средний след за период» без аттестации — только если не раздувает
|
||||||
фазу; иначе не делать
|
фазу; иначе не делать
|
||||||
|
|
||||||
## Тесты, без которых фаза не закрыта
|
## Тесты, без которых фаза не закрыта
|
||||||
|
|
||||||
- [ ] Приход под дождём чаще даёт «мокрый», чем ясная погода
|
- [x] Приход под дождём чаще даёт «мокрый», чем ясная погода
|
||||||
- [ ] Мокрый повышает шанс ванильной респираторной относительно сухого контроля
|
- [x] Мокрый повышает шанс ванильной респираторной относительно сухого контроля
|
||||||
- [ ] После тяжёлой болезни aftereffect появляется и сходит за ограниченное время
|
- [x] После тяжёлой болезни aftereffect появляется и сходит за ограниченное время
|
||||||
- [ ] Сейв сохраняет оба вида условий
|
- [x] Сейв сохраняет оба вида условий
|
||||||
|
|
||||||
## Критерий готовности
|
## Критерий готовности
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,6 @@
|
|||||||
|
|
||||||
| Фаза | Статус | Зачем |
|
| Фаза | Статус | Зачем |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| [84. Мокрый и aftereffect](84-wet-aftereffect.md) | 🔄 | Дождь → мокрый; хвост болезни |
|
| [84. Мокрый и aftereffect](84-wet-aftereffect.md) | ✅ | Дождь → мокрый; хвост болезни |
|
||||||
|
|
||||||
81 стоит на 44 и 77; 82 — на 78 и 73/48; 83 — на 68/70/76; 84 — на 78 и 32.
|
81 стоит на 44 и 77; 82 — на 78 и 73/48; 83 — на 68/70/76; 84 — на 78 и 32.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ internal static class DiseaseDefValidator
|
|||||||
DiseaseFamilies.Respiratory,
|
DiseaseFamilies.Respiratory,
|
||||||
DiseaseFamilies.Gastrointestinal,
|
DiseaseFamilies.Gastrointestinal,
|
||||||
DiseaseFamilies.Ent,
|
DiseaseFamilies.Ent,
|
||||||
|
DiseaseFamilies.Condition,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static void Validate(DefCatalog catalog)
|
public static void Validate(DefCatalog catalog)
|
||||||
@@ -47,10 +48,14 @@ internal static class DiseaseDefValidator
|
|||||||
|| def.ColdChancePerDay < 0f
|
|| def.ColdChancePerDay < 0f
|
||||||
|| def.RainChancePerDay < 0f
|
|| def.RainChancePerDay < 0f
|
||||||
|| def.SnowChancePerDay < 0f
|
|| def.SnowChancePerDay < 0f
|
||||||
|
|| def.WetChancePerDay < 0f
|
||||||
|
|| def.AftereffectMinSeverity < 0f
|
||||||
|
|| def.AftereffectMinSeverity > 1f
|
||||||
|| def.NodeContagionChancePerDay < 0f
|
|| def.NodeContagionChancePerDay < 0f
|
||||||
|| def.ClassContagionChancePerDay < 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)
|
if (def.NodeContagionChancePerDay > 1f || def.ClassContagionChancePerDay > 1f)
|
||||||
|
|||||||
@@ -8,6 +8,16 @@ public static class DiseaseFamilies
|
|||||||
public const string Gastrointestinal = "gastrointestinal";
|
public const string Gastrointestinal = "gastrointestinal";
|
||||||
|
|
||||||
public const string Ent = "ent";
|
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>
|
/// <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; }
|
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>
|
/// <summary>Days of immunity to this def after recovery.</summary>
|
||||||
public float ImmunityDays { get; init; }
|
public float ImmunityDays { get; init; }
|
||||||
|
|
||||||
|
|||||||
@@ -788,6 +788,13 @@ internal static class PeopleDefValidator
|
|||||||
$"BehaviorDef '{behavior.DefName}' diseaseVectorScale cannot be negative.");
|
$"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)
|
if (behavior.DiseaseOutbreakMinNewCases < 0)
|
||||||
{
|
{
|
||||||
throw new ContentLoadException(
|
throw new ContentLoadException(
|
||||||
|
|||||||
@@ -688,6 +688,14 @@ public sealed class BehaviorDef : Def
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public float DiseaseVectorScale { get; init; } = 1f;
|
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>
|
/// <summary>
|
||||||
/// New contagion cases in one day at or above this raise a diseaseOutbreak world event.
|
/// New contagion cases in one day at or above this raise a diseaseOutbreak world event.
|
||||||
/// 0 disables the toast.
|
/// 0 disables the toast.
|
||||||
|
|||||||
@@ -153,16 +153,42 @@ public static class HealthConditions
|
|||||||
foreach (var done in recovered)
|
foreach (var done in recovered)
|
||||||
{
|
{
|
||||||
list.Remove(done);
|
list.Remove(done);
|
||||||
if (catalog is not null
|
if (catalog is null
|
||||||
&& catalog.Diseases.TryGetValue(done.DefName, out var disease)
|
|| !catalog.Diseases.TryGetValue(done.DefName, out var disease)
|
||||||
&& !disease.Abstract
|
|| disease.Abstract)
|
||||||
&& disease.ImmunityDays > 0f)
|
{
|
||||||
|
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(
|
DiseaseImmunities.Grant(
|
||||||
person,
|
person,
|
||||||
done.DefName,
|
done.DefName,
|
||||||
clock.AddDays(disease.ImmunityDays));
|
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)
|
if (list.Count == 0)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public static class Seed
|
|||||||
public const int ContagionSalt = 21;
|
public const int ContagionSalt = 21;
|
||||||
public const int GradeTrailSalt = 22;
|
public const int GradeTrailSalt = 22;
|
||||||
public const int NurseSalt = 23;
|
public const int NurseSalt = 23;
|
||||||
|
public const int WetSalt = 24;
|
||||||
|
|
||||||
/// <summary>A stream that belongs to the school rather than to one family.</summary>
|
/// <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);
|
public static int ForSchool(int schoolSeed, int salt) => Mix(schoolSeed, familyIndex: -1, salt);
|
||||||
|
|||||||
@@ -130,6 +130,9 @@
|
|||||||
"gradeTrailSummonChance": 0.35,
|
"gradeTrailSummonChance": 0.35,
|
||||||
// Scales DiseaseDef onset from weather/base (slice 14 phase 78). 0 disables natural onset.
|
// Scales DiseaseDef onset from weather/base (slice 14 phase 78). 0 disables natural onset.
|
||||||
"diseaseVectorScale": 1,
|
"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).
|
// Contagion cases in one day at or above this raise diseaseOutbreak (slice 14 phase 79).
|
||||||
"diseaseOutbreakMinNewCases": 3,
|
"diseaseOutbreakMinNewCases": 3,
|
||||||
// Nurse tend scales with Medicine skill (slice 14 phase 80).
|
// 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",
|
"defName": "CommonCold",
|
||||||
"family": "respiratory",
|
"family": "respiratory",
|
||||||
@@ -9,6 +29,8 @@
|
|||||||
"coldChancePerDay": 0.08,
|
"coldChancePerDay": 0.08,
|
||||||
"rainChancePerDay": 0.03,
|
"rainChancePerDay": 0.03,
|
||||||
"snowChancePerDay": 0.04,
|
"snowChancePerDay": 0.04,
|
||||||
|
"wetChancePerDay": 0.07,
|
||||||
|
"aftereffectMinSeverity": 0.9,
|
||||||
"nodeContagionChancePerDay": 0.55,
|
"nodeContagionChancePerDay": 0.55,
|
||||||
"classContagionChancePerDay": 0.25,
|
"classContagionChancePerDay": 0.25,
|
||||||
"contagiousDuringIncubation": true,
|
"contagiousDuringIncubation": true,
|
||||||
@@ -31,6 +53,8 @@
|
|||||||
"coldChancePerDay": 0.06,
|
"coldChancePerDay": 0.06,
|
||||||
"rainChancePerDay": 0.02,
|
"rainChancePerDay": 0.02,
|
||||||
"snowChancePerDay": 0.05,
|
"snowChancePerDay": 0.05,
|
||||||
|
"wetChancePerDay": 0.05,
|
||||||
|
"aftereffectMinSeverity": 0.55,
|
||||||
"nodeContagionChancePerDay": 0.65,
|
"nodeContagionChancePerDay": 0.65,
|
||||||
"classContagionChancePerDay": 0.35,
|
"classContagionChancePerDay": 0.35,
|
||||||
"contagiousDuringIncubation": true,
|
"contagiousDuringIncubation": true,
|
||||||
@@ -50,6 +74,7 @@
|
|||||||
"immunityDays": 10,
|
"immunityDays": 10,
|
||||||
"baseChancePerDay": 0.012,
|
"baseChancePerDay": 0.012,
|
||||||
"rainChancePerDay": 0.01,
|
"rainChancePerDay": 0.01,
|
||||||
|
"aftereffectMinSeverity": 0.75,
|
||||||
"nodeContagionChancePerDay": 0.3,
|
"nodeContagionChancePerDay": 0.3,
|
||||||
"classContagionChancePerDay": 0,
|
"classContagionChancePerDay": 0,
|
||||||
"contagiousDuringIncubation": false,
|
"contagiousDuringIncubation": false,
|
||||||
@@ -73,6 +98,8 @@
|
|||||||
"nurseSeekSeverity": 0.4,
|
"nurseSeekSeverity": 0.4,
|
||||||
"tendSeverityPerDay": -0.25,
|
"tendSeverityPerDay": -0.25,
|
||||||
"tendProgressPerDay": 0.35,
|
"tendProgressPerDay": 0.35,
|
||||||
|
"wetChancePerDay": 0.04,
|
||||||
|
"aftereffectMinSeverity": 0.85,
|
||||||
"stages": [
|
"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, "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" },
|
{ "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",
|
"OtitisEffectMild": "Earache, hears worse",
|
||||||
"OtitisEffectModerate": "Pain hurts lessons and talk",
|
"OtitisEffectModerate": "Pain hurts lessons and talk",
|
||||||
"OtitisEffectHeavy": "Severe pain, barely talks",
|
"OtitisEffectHeavy": "Severe pain, barely talks",
|
||||||
|
"Wet": "Wet",
|
||||||
|
"DiseaseAftereffect": "Recovering",
|
||||||
"core": "Core",
|
"core": "Core",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,5 +263,7 @@
|
|||||||
"OtitisEffectMild": "Боль в ухе, хуже слышит",
|
"OtitisEffectMild": "Боль в ухе, хуже слышит",
|
||||||
"OtitisEffectModerate": "Боль мешает уроку и разговору",
|
"OtitisEffectModerate": "Боль мешает уроку и разговору",
|
||||||
"OtitisEffectHeavy": "Сильная боль, почти не общается",
|
"OtitisEffectHeavy": "Сильная боль, почти не общается",
|
||||||
|
"Wet": "Мокрый",
|
||||||
|
"DiseaseAftereffect": "Ослабление после болезни",
|
||||||
"core": "Базовая игра",
|
"core": "Базовая игра",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ internal static class DiseaseSystem
|
|||||||
if (school.LastDiseaseDay != dayNumber)
|
if (school.LastDiseaseDay != dayNumber)
|
||||||
{
|
{
|
||||||
school.LastDiseaseDay = dayNumber;
|
school.LastDiseaseDay = dayNumber;
|
||||||
|
changed |= TryWet(school);
|
||||||
changed |= TryOnset(school);
|
changed |= TryOnset(school);
|
||||||
changed |= TryContagion(school);
|
changed |= TryContagion(school);
|
||||||
}
|
}
|
||||||
@@ -58,6 +59,70 @@ internal static class DiseaseSystem
|
|||||||
return changed;
|
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)
|
private static bool TryOnset(School school)
|
||||||
{
|
{
|
||||||
var catalog = school.Catalog!;
|
var catalog = school.Catalog!;
|
||||||
@@ -78,16 +143,19 @@ internal static class DiseaseSystem
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var wet = HealthConditions.Has(person, DiseaseFamilies.WetDefName);
|
||||||
|
|
||||||
foreach (var disease in catalog.Diseases.Values)
|
foreach (var disease in catalog.Diseases.Values)
|
||||||
{
|
{
|
||||||
if (disease.Abstract
|
if (disease.Abstract
|
||||||
|
|| disease.Family.Equals(DiseaseFamilies.Condition, StringComparison.Ordinal)
|
||||||
|| HealthConditions.Has(person, disease.DefName)
|
|| HealthConditions.Has(person, disease.DefName)
|
||||||
|| DiseaseImmunities.IsImmune(person, disease.DefName, school.Clock.Time))
|
|| DiseaseImmunities.IsImmune(person, disease.DefName, school.Clock.Time))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var chance = OnsetChance(disease, weather, rules.DiseaseVectorScale);
|
var chance = OnsetChance(disease, weather, rules.DiseaseVectorScale, wet);
|
||||||
if (chance <= 0f)
|
if (chance <= 0f)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -108,7 +176,7 @@ internal static class DiseaseSystem
|
|||||||
DefName = disease.DefName,
|
DefName = disease.DefName,
|
||||||
Severity = 0.05f,
|
Severity = 0.05f,
|
||||||
Progress = 0f,
|
Progress = 0f,
|
||||||
Source = SourceOf(disease, weather),
|
Source = SourceOf(disease, weather, wet),
|
||||||
StartedAt = school.Clock.Time,
|
StartedAt = school.Clock.Time,
|
||||||
});
|
});
|
||||||
changed = true;
|
changed = true;
|
||||||
@@ -145,6 +213,7 @@ internal static class DiseaseSystem
|
|||||||
{
|
{
|
||||||
if (!catalog.Diseases.TryGetValue(condition.DefName, out var disease)
|
if (!catalog.Diseases.TryGetValue(condition.DefName, out var disease)
|
||||||
|| disease.Abstract
|
|| disease.Abstract
|
||||||
|
|| disease.Family.Equals(DiseaseFamilies.Condition, StringComparison.Ordinal)
|
||||||
|| !CanTransmit(condition, disease, now))
|
|| !CanTransmit(condition, disease, now))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -348,7 +417,11 @@ internal static class DiseaseSystem
|
|||||||
return map;
|
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)
|
if (vectorScale <= 0f)
|
||||||
{
|
{
|
||||||
@@ -368,11 +441,21 @@ internal static class DiseaseSystem
|
|||||||
_ => 0f,
|
_ => 0f,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (wet)
|
||||||
|
{
|
||||||
|
chance += disease.WetChancePerDay;
|
||||||
|
}
|
||||||
|
|
||||||
return Math.Clamp(chance * vectorScale, 0f, 1f);
|
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)
|
if (disease.ColdBelowC is { } below && weather.TemperatureC <= below)
|
||||||
{
|
{
|
||||||
return "cold";
|
return "cold";
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ public class DiseaseDefTests
|
|||||||
Assert.True(catalog.Diseases.ContainsKey("Influenza"));
|
Assert.True(catalog.Diseases.ContainsKey("Influenza"));
|
||||||
Assert.True(catalog.Diseases.ContainsKey("StomachBug"));
|
Assert.True(catalog.Diseases.ContainsKey("StomachBug"));
|
||||||
Assert.True(catalog.Diseases.ContainsKey("Otitis"));
|
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"];
|
var cold = catalog.Diseases["CommonCold"];
|
||||||
Assert.Equal(DiseaseFamilies.Respiratory, cold.Family);
|
Assert.Equal(DiseaseFamilies.Respiratory, cold.Family);
|
||||||
@@ -22,6 +24,9 @@ public class DiseaseDefTests
|
|||||||
Assert.True(cold.Stages.Count >= 2);
|
Assert.True(cold.Stages.Count >= 2);
|
||||||
Assert.Equal(0.5f, cold.IncubationDays);
|
Assert.Equal(0.5f, cold.IncubationDays);
|
||||||
Assert.Equal(14f, cold.ImmunityDays);
|
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.55f, cold.NodeContagionChancePerDay);
|
||||||
Assert.Equal(0.25f, cold.ClassContagionChancePerDay);
|
Assert.Equal(0.25f, cold.ClassContagionChancePerDay);
|
||||||
Assert.True(cold.ContagiousDuringIncubation);
|
Assert.True(cold.ContagiousDuringIncubation);
|
||||||
@@ -30,7 +35,11 @@ public class DiseaseDefTests
|
|||||||
|
|
||||||
Assert.Equal("ОРВИ", catalog.Label("ru", cold));
|
Assert.Equal("ОРВИ", catalog.Label("ru", cold));
|
||||||
Assert.Equal("Common cold", catalog.Label("en", 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(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.Equal(3, catalog.BehaviorRules.DiseaseOutbreakMinNewCases);
|
||||||
Assert.True(catalog.Events.ContainsKey("DiseaseOutbreak"));
|
Assert.True(catalog.Events.ContainsKey("DiseaseOutbreak"));
|
||||||
Assert.Equal(EventTriggers.DiseaseOutbreak, catalog.Events["DiseaseOutbreak"].Trigger);
|
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