Merge branch 'phase/78-diseases'
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -117,6 +117,15 @@ internal static class AttendanceSystem
|
||||
continue;
|
||||
}
|
||||
|
||||
var reason = DiseaseEffects.ShouldStayHome(
|
||||
person,
|
||||
school.Catalog!,
|
||||
school.PeopleSeed,
|
||||
school.Clock.Time)
|
||||
|| DiseaseEffects.StayHomeChance(person, school.Catalog!, school.Clock.Time) >= 0.5f
|
||||
? AbsenceReasons.Illness
|
||||
: AbsenceReasons.Truancy;
|
||||
|
||||
if (AttendanceMemory.Record(
|
||||
person,
|
||||
lesson.Subject,
|
||||
@@ -124,7 +133,7 @@ internal static class AttendanceSystem
|
||||
school.Clock.Time,
|
||||
period,
|
||||
rules,
|
||||
AbsenceReasons.Truancy))
|
||||
reason))
|
||||
{
|
||||
school.RosterTalkDirty = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
|
||||
namespace HSchool.Simulation;
|
||||
|
||||
/// <summary>
|
||||
/// DiseaseDef onset from weather vectors and ticks that follow stage curves.
|
||||
/// Person-to-person contagion is phase 79.
|
||||
/// </summary>
|
||||
internal static class DiseaseSystem
|
||||
{
|
||||
public static bool Apply(School school, double gameMinutes)
|
||||
{
|
||||
if (school.Roster is null || school.Catalog is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var catalog = school.Catalog;
|
||||
var changed = false;
|
||||
var dayNumber = DateOnly.FromDateTime(school.Clock.Time).DayNumber;
|
||||
var now = school.Clock.Time;
|
||||
|
||||
foreach (var person in school.Roster.People)
|
||||
{
|
||||
changed |= DiseaseImmunities.Expire(person, now);
|
||||
}
|
||||
|
||||
if (school.LastDiseaseDay != dayNumber)
|
||||
{
|
||||
school.LastDiseaseDay = dayNumber;
|
||||
changed |= TryOnset(school);
|
||||
}
|
||||
|
||||
if (gameMinutes > 0)
|
||||
{
|
||||
foreach (var person in school.Roster.People)
|
||||
{
|
||||
changed |= HealthConditions.Tick(
|
||||
person,
|
||||
school.PeopleSeed,
|
||||
dayNumber,
|
||||
gameMinutes,
|
||||
catalog,
|
||||
now);
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static bool TryOnset(School school)
|
||||
{
|
||||
var catalog = school.Catalog!;
|
||||
var rules = catalog.BehaviorRules;
|
||||
if (rules is null || rules.DiseaseVectorScale <= 0f || catalog.Diseases.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var weather = school.Weather;
|
||||
var dayNumber = DateOnly.FromDateTime(school.Clock.Time).DayNumber;
|
||||
var changed = false;
|
||||
|
||||
foreach (var person in school.Roster!.People)
|
||||
{
|
||||
if (!person.IsStudent && !person.IsStaff)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var disease in catalog.Diseases.Values)
|
||||
{
|
||||
if (disease.Abstract
|
||||
|| HealthConditions.Has(person, disease.DefName)
|
||||
|| DiseaseImmunities.IsImmune(person, disease.DefName, school.Clock.Time))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var chance = OnsetChance(disease, weather, rules.DiseaseVectorScale);
|
||||
if (chance <= 0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var salt = Seed.DiseaseSalt + Stable(disease.DefName);
|
||||
var roll = Seed.Mix(school.PeopleSeed, person.Id, dayNumber, salt);
|
||||
var unit = (roll & int.MaxValue) / (float)int.MaxValue;
|
||||
if (unit >= chance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
HealthConditions.Add(
|
||||
person,
|
||||
new HealthCondition
|
||||
{
|
||||
DefName = disease.DefName,
|
||||
Severity = 0.05f,
|
||||
Progress = 0f,
|
||||
Source = SourceOf(disease, weather),
|
||||
StartedAt = school.Clock.Time,
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
internal static float OnsetChance(DiseaseDef disease, OutdoorWeather weather, float vectorScale)
|
||||
{
|
||||
if (vectorScale <= 0f)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var chance = disease.BaseChancePerDay;
|
||||
if (disease.ColdBelowC is { } below && weather.TemperatureC <= below)
|
||||
{
|
||||
chance += disease.ColdChancePerDay;
|
||||
}
|
||||
|
||||
chance += weather.Precipitation switch
|
||||
{
|
||||
Precipitation.Rain => disease.RainChancePerDay,
|
||||
Precipitation.Snow => disease.SnowChancePerDay,
|
||||
_ => 0f,
|
||||
};
|
||||
|
||||
return Math.Clamp(chance * vectorScale, 0f, 1f);
|
||||
}
|
||||
|
||||
private static string SourceOf(DiseaseDef disease, OutdoorWeather weather)
|
||||
{
|
||||
if (disease.ColdBelowC is { } below && weather.TemperatureC <= below)
|
||||
{
|
||||
return "cold";
|
||||
}
|
||||
|
||||
return weather.Precipitation switch
|
||||
{
|
||||
Precipitation.Rain => "rain",
|
||||
Precipitation.Snow => "snow",
|
||||
_ => "idiopathic",
|
||||
};
|
||||
}
|
||||
|
||||
private static int Stable(string value)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var hash = 17;
|
||||
foreach (var character in value)
|
||||
{
|
||||
hash = (hash * 31) + character;
|
||||
}
|
||||
|
||||
return hash & 0xFFFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,24 +3,10 @@ using HSchool.People;
|
||||
namespace HSchool.Simulation;
|
||||
|
||||
/// <summary>
|
||||
/// Ticks medical condition severity on the roster. Contagion and DiseaseDef outbreaks are later phases.
|
||||
/// Ticks medical conditions. DiseaseDef onset and stage curves live in <see cref="DiseaseSystem"/>.
|
||||
/// Kept so older call sites and phase-77 tests still have a named entry.
|
||||
/// </summary>
|
||||
internal static class HealthConditionSystem
|
||||
{
|
||||
public static bool Apply(School school, double gameMinutes)
|
||||
{
|
||||
if (gameMinutes <= 0 || school.Roster is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var dayNumber = DateOnly.FromDateTime(school.Clock.Time).DayNumber;
|
||||
var changed = false;
|
||||
foreach (var person in school.Roster.People)
|
||||
{
|
||||
changed |= HealthConditions.Tick(person, school.PeopleSeed, dayNumber, gameMinutes);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
public static bool Apply(School school, double gameMinutes) => DiseaseSystem.Apply(school, gameMinutes);
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ internal static class LessonLearningSystem
|
||||
}
|
||||
|
||||
textbookFactor *= TalkCircles.LessonWhisperFactor(activity.ActionId, rules);
|
||||
textbookFactor *= DiseaseEffects.LessonLearningFactor(person, catalog, school.Clock.Time);
|
||||
|
||||
IReadOnlyDictionary<string, float> taught = teacherSkills.TryGetValue(lesson.TeacherId, out var found)
|
||||
? found
|
||||
|
||||
@@ -119,6 +119,9 @@ public sealed class School : IDisposable
|
||||
/// <summary>Last slot <see cref="AttendanceSystem"/> observed — detects leaving a lesson to finalize absents.</summary>
|
||||
internal DaySlot? LastAttendanceSlot { get; set; }
|
||||
|
||||
/// <summary>Calendar day of the last DiseaseDef onset pass (one roll per person per day).</summary>
|
||||
internal int LastDiseaseDay { get; set; } = int.MinValue;
|
||||
|
||||
internal Dictionary<string, DayPlan> Plans { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
internal Queue<string> DecisionQueue { get; } = new();
|
||||
@@ -227,6 +230,7 @@ public sealed class School : IDisposable
|
||||
PlanDay = null;
|
||||
LastDecisionSlot = null;
|
||||
LastAttendanceSlot = null;
|
||||
LastDiseaseDay = int.MinValue;
|
||||
Plans.Clear();
|
||||
DecisionQueue.Clear();
|
||||
LoggedActivity.Clear();
|
||||
|
||||
Reference in New Issue
Block a user