Enhance school simulation management by introducing a new SchoolWeekDays option, allowing configuration of the number of working days in a week. Update the API to include school week days in responses and enhance documentation to reflect these changes. Revise UI components to support the new management features, ensuring a seamless user experience when hiring and assigning staff. Update localization strings for improved clarity and consistency across the application.
ci / server (push) Failing after 3m37s
ci / client (push) Successful in 13s

This commit is contained in:
Leonid Pershin
2026-08-19 00:47:09 +03:00
parent 94842ab192
commit 2ebc783585
38 changed files with 1837 additions and 208 deletions
+47 -2
View File
@@ -88,6 +88,8 @@ internal sealed record CatalogResponse(
IReadOnlyList<DefInfoResponse> Things,
IReadOnlyList<DefInfoResponse> NameSets,
IReadOnlyList<SubjectInfoResponse> Subjects,
DayFrameResponse? DayFrame,
IReadOnlyList<HolidayInfoResponse> Holidays,
MapLayout DefaultMap)
{
public static CatalogResponse From(DefCatalog catalog, MapLayout map, string locale) =>
@@ -99,6 +101,8 @@ internal sealed record CatalogResponse(
PlaceableThings(catalog, locale),
Placeable(catalog.NameSets.Values, catalog, locale),
PlaceableSubjects(catalog, locale),
MapDayFrame(catalog, locale),
PlaceableHolidays(catalog, locale),
map);
private static IReadOnlyList<DefInfoResponse> Placeable<T>(IEnumerable<T> defs, DefCatalog catalog, string locale)
@@ -140,7 +144,35 @@ internal sealed record CatalogResponse(
def.Grades.Min,
def.Grades.Max,
def.HoursPerWeek,
def.Skills.Select(share => new SubjectSkillInfo(share.Skill, share.Share)).ToArray()))
def.Skills.Select(share => new SubjectSkillInfo(share.Skill, share.Share)).ToArray(),
def.Room))
.ToArray();
private static DayFrameResponse? MapDayFrame(DefCatalog catalog, string locale)
{
var frame = catalog.DayFrame;
return frame is null
? null
: new DayFrameResponse(
frame.DefName,
catalog.Label(locale, frame),
frame.FirstLesson,
frame.LessonCount,
frame.LessonMinutes,
frame.BreakMinutes,
frame.LongBreakAfter,
frame.LongBreakMinutes);
}
private static IReadOnlyList<HolidayInfoResponse> PlaceableHolidays(DefCatalog catalog, string locale) =>
catalog.Holidays.Values
.Where(def => !def.Abstract)
.OrderBy(def => def.DefName, StringComparer.Ordinal)
.Select(def => new HolidayInfoResponse(
def.DefName,
catalog.Label(locale, def),
def.Start,
def.End))
.ToArray();
}
@@ -165,4 +197,17 @@ internal sealed record SubjectInfoResponse(
int GradeMin,
int GradeMax,
int HoursPerWeek,
IReadOnlyList<SubjectSkillInfo> Skills);
IReadOnlyList<SubjectSkillInfo> Skills,
string? Room);
internal sealed record DayFrameResponse(
string DefName,
string Label,
string FirstLesson,
int LessonCount,
int LessonMinutes,
int BreakMinutes,
int LongBreakAfter,
int LongBreakMinutes);
internal sealed record HolidayInfoResponse(string DefName, string Label, MonthDay Start, MonthDay End);
+43 -4
View File
@@ -180,7 +180,9 @@ internal sealed record StaffingResponse(
float Remaining,
IReadOnlyList<UncoveredSubjectResponse> Uncovered,
IReadOnlyList<ApplicantResponse> Applicants,
IReadOnlyList<StaffMemberResponse> Staff);
IReadOnlyList<StaffMemberResponse> Staff,
IReadOnlyList<DefLabelResponse> Positions,
IReadOnlyList<UncoveredSubjectResponse> Subjects);
internal sealed record UncoveredSubjectResponse(
string DefName,
@@ -196,7 +198,8 @@ internal sealed record ApplicantResponse(
int Age,
bool IsParent,
float HourlyWageAsk,
float MonthlyBase);
float MonthlyBase,
IReadOnlyList<LabeledStatResponse> Skills);
internal sealed record StaffMemberResponse(
string Id,
@@ -245,7 +248,8 @@ internal static class StaffingMapper
applicant.Person.AgeOn(asOf),
applicant.Person.IsParent,
applicant.HourlyWageAsk,
rules is null ? 0f : Staffing.MonthlyBase(rules, applicant.HourlyWageAsk)))
rules is null ? 0f : Staffing.MonthlyBase(rules, applicant.HourlyWageAsk),
StrongSkills(applicant.Person, catalog, locale)))
.ToArray();
var staff = roster.People
@@ -255,7 +259,42 @@ internal static class StaffingMapper
.Select(person => Member(person, catalog, rules, locale, asOf))
.ToArray();
return new StaffingResponse(allocated, payroll, remaining, uncovered, applicants, staff);
var positions = catalog is null
? Array.Empty<DefLabelResponse>()
: catalog.Positions.Values
.Where(def => !def.Abstract)
.Select(def => new DefLabelResponse(def.DefName, catalog.Label(locale, def)))
.OrderBy(row => row.Label, StringComparer.Ordinal)
.ToArray();
var subjects = catalog is null
? Array.Empty<UncoveredSubjectResponse>()
: catalog.Subjects.Values
.Where(def => !def.Abstract)
.Select(def => new UncoveredSubjectResponse(
def.DefName,
catalog.Label(locale, def),
def.Grades.Min,
def.Grades.Max,
def.HoursPerWeek))
.ToArray();
return new StaffingResponse(allocated, payroll, remaining, uncovered, applicants, staff, positions, subjects);
}
private static IReadOnlyList<LabeledStatResponse> StrongSkills(Person person, DefCatalog? catalog, string locale)
{
return person.Skills
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.Take(3)
.Select(pair => new LabeledStatResponse(
pair.Key,
catalog is not null && catalog.Skills.TryGetValue(pair.Key, out var def)
? catalog.Label(locale, def)
: pair.Key,
pair.Value.ToString()))
.ToArray();
}
private static StaffMemberResponse Member(
@@ -27,6 +27,7 @@ internal static class SchoolEndpoints
state.MaxSchools,
options.DefaultStartDate,
options.GameMinutesPerRealSecond,
options.SchoolWeekDays,
[.. state.Schools.Select(SchoolResponse.From)]);
})
.WithName("GetSchools");
@@ -474,6 +475,7 @@ internal sealed record SchoolsResponse(
int MaxSchools,
DateTime DefaultStartDate,
double GameMinutesPerRealSecond,
int SchoolWeekDays,
IReadOnlyList<SchoolResponse> Schools);
internal sealed record RandomNameResponse(string Name);
@@ -23,6 +23,9 @@ internal static class PersonCardReader
}
var person = roster.People.FirstOrDefault(candidate => candidate.Id.Equals(personId, StringComparison.Ordinal));
person ??= school.Applicants?.Applicants
.FirstOrDefault(applicant => applicant.Person.Id.Equals(personId, StringComparison.Ordinal))
?.Person;
if (person is null)
{
return null;
+1
View File
@@ -22,6 +22,7 @@ builder.Services
.Validate(options => !string.IsNullOrWhiteSpace(options.ModsDirectory), "Simulation:ModsDirectory must be set.")
.Validate(options => options.SaveIntervalSeconds is > 0 and <= 3600, "Simulation:SaveIntervalSeconds must be between 1 and 3600.")
.Validate(options => options.MonthlyPayrollCap > 0, "Simulation:MonthlyPayrollCap must be positive.")
.Validate(options => options.SchoolWeekDays is >= 5 and <= 7, "Simulation:SchoolWeekDays must be between 5 and 7.")
.ValidateOnStart();
builder.Services.AddSingleton<GameCommandQueue>();
+2 -1
View File
@@ -14,6 +14,7 @@
"SavesDirectory": "saves",
"ModsDirectory": "mods",
"SaveIntervalSeconds": 30,
"MonthlyPayrollCap": 10000
"MonthlyPayrollCap": 10000,
"SchoolWeekDays": 5
}
}
@@ -0,0 +1,9 @@
{
"defName": "StandardDay",
"firstLesson": "08:30",
"lessonCount": 7,
"lessonMinutes": 45,
"breakMinutes": 10,
"longBreakAfter": 3,
"longBreakMinutes": 20,
}
@@ -0,0 +1,22 @@
[
{
"defName": "AutumnBreak",
"start": { "month": 10, "day": 29 },
"end": { "month": 11, "day": 4 },
},
{
"defName": "WinterBreak",
"start": { "month": 12, "day": 31 },
"end": { "month": 1, "day": 13 },
},
{
"defName": "SpringBreak",
"start": { "month": 3, "day": 24 },
"end": { "month": 3, "day": 31 },
},
{
"defName": "SummerBreak",
"start": { "month": 6, "day": 1 },
"end": { "month": 8, "day": 31 },
},
]
@@ -56,6 +56,7 @@
"defName": "Informatics",
"grades": { "min": 5, "max": 11 },
"hoursPerWeek": 1,
"room": "ComputerLab",
"skills": [{ "skill": "Informatics", "share": 1 }],
},
{
@@ -74,6 +75,7 @@
"defName": "PhysicalEducation",
"grades": { "min": 1, "max": 11 },
"hoursPerWeek": 3,
"room": "GymHall",
"skills": [{ "skill": "PhysicalEducation", "share": 1 }],
},
]
@@ -94,4 +94,9 @@
"Athletic": "Athletic",
"Heavy": "Heavy",
"Obese": "Obese",
"StandardDay": "School day",
"AutumnBreak": "Autumn break",
"WinterBreak": "Winter break",
"SpringBreak": "Spring break",
"SummerBreak": "Summer break",
}
@@ -94,4 +94,9 @@
"Athletic": "Атлетическое",
"Heavy": "Плотное",
"Obese": "Полное",
"StandardDay": "Учебный день",
"AutumnBreak": "Осенние каникулы",
"WinterBreak": "Зимние каникулы",
"SpringBreak": "Весенние каникулы",
"SummerBreak": "Летние каникулы",
}