Enhance staffing management with uncovered teacher tracking
ci / server (push) Failing after 3m43s
ci / client (push) Successful in 14s

- Updated `protocol.md` to clarify the concept of uncovered subjects and the number of additional teachers required.
- Introduced `teachersShort` property in the `StaffingSubject` interface to indicate how many more teachers are needed for a subject.
- Enhanced localization strings to reflect the new uncovered teacher information in both English and Russian.
- Updated the management panel UI to display the number of teachers short for each uncovered subject.
- Revised the `Uncovered` method in the staffing logic to return detailed information about uncovered subjects, including the shortfall of teachers.
- Added tests to validate the new uncovered teacher tracking functionality and ensure accurate reporting in the staffing API.
- Marked related tasks as complete in the documentation for the golden fixtures phase.
This commit is contained in:
Leonid Pershin
2026-08-19 23:53:37 +03:00
parent c27d43f66a
commit e5a0ae5b9d
27 changed files with 16055 additions and 38 deletions
+36 -5
View File
@@ -18,6 +18,14 @@ public enum StaffingError
NoOpening,
}
/// <summary>
/// A subject the school cannot actually teach, and how many more teachers would cover it.
/// </summary>
public sealed record UncoveredSubject(SubjectDef Subject, int Assigned, int TeachersShort)
{
public string DefName => Subject.DefName;
}
public sealed record StaffingOutcome(
StaffingError Error,
Roster Roster,
@@ -130,12 +138,32 @@ public static class Staffing
return Round(total);
}
/// <summary>
/// How many people a subject needs at the weekly cap. Primary school on a vanilla map is
/// eighty hours: one person cannot carry it, and the uncovered row has to say so.
/// </summary>
public static int TeachersToCover(float hours, float maxWeeklyHours)
{
if (hours <= 0f)
{
return 0;
}
if (maxWeeklyHours <= 0f)
{
return 1;
}
return (int)Math.Ceiling(hours / maxWeeklyHours);
}
/// <summary>
/// Subjects the school cannot actually teach: nobody is assigned them, or the people who are
/// cannot between them carry the curriculum hours. Both read the same way to a player — the
/// lessons will not happen — so both belong in one list.
/// lessons will not happen — so both belong in one list. <see cref="UncoveredSubject.TeachersShort"/>
/// is how many more people it still needs.
/// </summary>
public static IReadOnlyList<SubjectDef> Uncovered(DefCatalog catalog, Roster roster)
public static IReadOnlyList<UncoveredSubject> Uncovered(DefCatalog catalog, Roster roster)
{
var years = new HashSet<int>();
foreach (var schoolClass in roster.Classes)
@@ -158,7 +186,7 @@ public static class Staffing
}
var cap = catalog.StaffingRules?.MaxWeeklyHours ?? 0f;
var uncovered = new List<SubjectDef>();
var uncovered = new List<UncoveredSubject>();
foreach (var subject in catalog.Subjects.Values)
{
if (subject.Abstract || !TouchesYears(subject, years))
@@ -167,9 +195,12 @@ public static class Staffing
}
var assigned = teachers.GetValueOrDefault(subject.DefName);
if (assigned == 0 || (cap > 0f && SubjectHours(catalog, roster, subject.DefName) > assigned * cap))
var hours = SubjectHours(catalog, roster, subject.DefName);
var needed = TeachersToCover(hours, cap);
var shortfall = Math.Max(0, needed - assigned);
if (shortfall > 0)
{
uncovered.Add(subject);
uncovered.Add(new UncoveredSubject(subject, assigned, shortfall));
}
}