Update staffing management to enhance payroll and hiring functionalities
- Increased the `MonthlyPayrollCap` from 10,000 to 40,000, allowing for greater flexibility in hiring and subject assignments. - Revised payroll calculation logic to ensure that staff members are compensated based on their actual weekly hours, with a minimum payment reflecting one full rate. - Updated documentation to clarify the new payroll structure and its implications for hiring and subject assignments. - Enhanced tests to validate the new payroll cap and ensure proper functionality in staffing scenarios, including the handling of uncovered subjects. - Improved localization strings to reflect changes in staffing and payroll terminology.
This commit is contained in:
@@ -142,6 +142,8 @@ const ru = {
|
||||
staffColSkills: 'Навыки',
|
||||
staffColPosition: 'Должность',
|
||||
staffColSubjects: 'Предметы',
|
||||
staffColHours: 'Часов',
|
||||
staffHours: '{n} ч/нед',
|
||||
staffColPay: 'В месяц',
|
||||
staffAsk: '{hourly}/ч · {monthly}/мес',
|
||||
staffSubjectRange: '{label} ({min}–{max})',
|
||||
@@ -331,6 +333,8 @@ const en: Messages = {
|
||||
staffColSkills: 'Skills',
|
||||
staffColPosition: 'Position',
|
||||
staffColSubjects: 'Subjects',
|
||||
staffColHours: 'Hours',
|
||||
staffHours: '{n} h/wk',
|
||||
staffColPay: 'Monthly',
|
||||
staffAsk: '{hourly}/h · {monthly}/mo',
|
||||
staffSubjectRange: '{label} ({min}–{max})',
|
||||
|
||||
@@ -320,6 +320,7 @@ export interface StaffMember {
|
||||
readonly position: string;
|
||||
readonly positionLabel: string;
|
||||
readonly hourlyWageAsk: number;
|
||||
readonly weeklyHours: number;
|
||||
readonly monthlyPay: number;
|
||||
readonly subjects: readonly DefLabel[];
|
||||
}
|
||||
|
||||
@@ -338,6 +338,7 @@ export class ManagementPanel {
|
||||
el('th', { text: t('staffColName') }),
|
||||
el('th', { text: t('staffColPosition') }),
|
||||
el('th', { text: t('staffColSubjects') }),
|
||||
el('th', { text: t('staffColHours') }),
|
||||
el('th', { text: t('staffColPay') }),
|
||||
),
|
||||
),
|
||||
@@ -360,6 +361,7 @@ export class ManagementPanel {
|
||||
name,
|
||||
el('td', { text: member.positionLabel }),
|
||||
el('td', { text: member.subjects.map((subject) => subject.label).join(', ') || '—' }),
|
||||
el('td', { text: t('staffHours', { n: formatHours(member.weeklyHours) }) }),
|
||||
el('td', { text: formatMoney(member.monthlyPay) }),
|
||||
);
|
||||
body.append(row);
|
||||
@@ -668,6 +670,10 @@ function fillSelect(
|
||||
}
|
||||
}
|
||||
|
||||
function formatHours(value: number): string {
|
||||
return new Intl.NumberFormat(intlTag(), { maximumFractionDigits: 1 }).format(value);
|
||||
}
|
||||
|
||||
function formatMoney(value: number): string {
|
||||
return new Intl.NumberFormat(intlTag(), { maximumFractionDigits: 2 }).format(value);
|
||||
}
|
||||
|
||||
@@ -327,9 +327,11 @@ internal static class PeopleDefValidator
|
||||
throw new ContentLoadException($"StaffingDef '{staffing.DefName}' monthly hours must be positive.");
|
||||
}
|
||||
|
||||
if (staffing.ExtraSubjectSurcharge < 0f)
|
||||
if (staffing.MaxWeeklyHours < staffing.BaseWeeklyHours)
|
||||
{
|
||||
throw new ContentLoadException($"StaffingDef '{staffing.DefName}' extraSubjectSurcharge cannot be negative.");
|
||||
throw new ContentLoadException(
|
||||
$"StaffingDef '{staffing.DefName}' maxWeeklyHours must be at least baseWeeklyHours: "
|
||||
+ "nobody can be paid for a full rate they are not allowed to work.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -181,16 +181,19 @@ public sealed class StaffingDef : Def
|
||||
|
||||
public float HourlyWagePerSkill { get; init; }
|
||||
|
||||
/// <summary>Hours assumed in the monthly base rate. Extra subjects add a surcharge on top.</summary>
|
||||
/// <summary>
|
||||
/// One full rate. A hired person is paid for at least this many hours even with nothing
|
||||
/// assigned; hours beyond it are paid on top.
|
||||
/// </summary>
|
||||
public float BaseWeeklyHours { get; init; }
|
||||
|
||||
public float WeeksPerMonth { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Added to the monthly base for each subject after the first. <c>0.25</c> means a second
|
||||
/// subject costs 25% of the base rate.
|
||||
/// Most hours one person can carry. A subject whose curriculum needs more than this needs a
|
||||
/// second teacher — the hours are then shared between them.
|
||||
/// </summary>
|
||||
public float ExtraSubjectSurcharge { get; init; }
|
||||
public float MaxWeeklyHours { get; init; }
|
||||
|
||||
public float WeeksPerMonth { get; init; }
|
||||
}
|
||||
|
||||
public enum BodyAttributeKind
|
||||
|
||||
+103
-29
@@ -38,31 +38,103 @@ public static class Staffing
|
||||
public static StaffingOutcome UnknownSchool() =>
|
||||
Fail(StaffingError.UnknownSchool, new Roster([], [], []), ApplicantPool.Empty, allocated: 0, payroll: 0, attempted: 0);
|
||||
|
||||
/// <summary>What one full rate costs at this price per hour — the floor under any hire.</summary>
|
||||
public static float MonthlyBase(StaffingDef rules, float hourlyAsk) =>
|
||||
Round(hourlyAsk * rules.BaseWeeklyHours * rules.WeeksPerMonth);
|
||||
|
||||
public static float MonthlyPay(StaffingDef rules, float hourlyAsk, int subjectCount)
|
||||
/// <summary>
|
||||
/// Price of an hour times the hours actually carried, never below one full rate. A hired
|
||||
/// person costs something even with nothing assigned; loading them past the rate costs more.
|
||||
/// </summary>
|
||||
public static float MonthlyPay(StaffingDef rules, float hourlyAsk, float weeklyHours) =>
|
||||
Round(hourlyAsk * Math.Max(weeklyHours, rules.BaseWeeklyHours) * rules.WeeksPerMonth);
|
||||
|
||||
/// <summary>
|
||||
/// Weekly hours this person carries: for every subject assigned to them, the curriculum hours
|
||||
/// of that subject across the classes that study it, split between everyone teaching it.
|
||||
///
|
||||
/// Deliberately not capped at <see cref="StaffingDef.MaxWeeklyHours"/>. Refusing the
|
||||
/// assignment would deadlock — the first teacher of a subject always inherits all of its
|
||||
/// hours, so nobody could ever be the first. Capping what is paid would be worse: piling a
|
||||
/// seventh subject onto someone already at the cap would cost nothing, which is the loophole
|
||||
/// the hourly model exists to close. So the hours count in full, the money says so, and
|
||||
/// <see cref="Uncovered"/> is what reports that one person cannot really teach all of it.
|
||||
/// </summary>
|
||||
public static float WeeklyHours(DefCatalog catalog, Roster roster, Person person)
|
||||
{
|
||||
var extras = Math.Max(0, subjectCount - 1);
|
||||
return Round(MonthlyBase(rules, hourlyAsk) * (1f + (rules.ExtraSubjectSurcharge * extras)));
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
ArgumentNullException.ThrowIfNull(roster);
|
||||
ArgumentNullException.ThrowIfNull(person);
|
||||
|
||||
if (person.Subjects.Count == 0)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var hours = 0f;
|
||||
foreach (var subject in person.Subjects)
|
||||
{
|
||||
hours += SubjectHours(catalog, roster, subject) / Math.Max(1, TeachersOf(roster, subject));
|
||||
}
|
||||
|
||||
return hours;
|
||||
}
|
||||
|
||||
public static float Payroll(StaffingDef rules, IEnumerable<Person> people)
|
||||
/// <summary>Curriculum hours a subject needs each week across every class that studies it.</summary>
|
||||
public static float SubjectHours(DefCatalog catalog, Roster roster, string subject)
|
||||
{
|
||||
if (!catalog.Subjects.TryGetValue(subject, out var def) || def.Abstract)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var classes = 0;
|
||||
foreach (var schoolClass in roster.Classes)
|
||||
{
|
||||
if (schoolClass.Year >= def.Grades.Min && schoolClass.Year <= def.Grades.Max)
|
||||
{
|
||||
classes++;
|
||||
}
|
||||
}
|
||||
|
||||
return def.HoursPerWeek * classes;
|
||||
}
|
||||
|
||||
private static int TeachersOf(Roster roster, string subject)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var person in roster.People)
|
||||
{
|
||||
if (person.IsStaff && person.Subjects.Contains(subject, StringComparer.Ordinal))
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public static float Payroll(DefCatalog catalog, StaffingDef rules, Roster roster)
|
||||
{
|
||||
var total = 0f;
|
||||
foreach (var person in people)
|
||||
foreach (var person in roster.People)
|
||||
{
|
||||
if (!person.IsStaff || person.HourlyWageAsk is not { } ask)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
total += MonthlyPay(rules, ask, person.Subjects.Count);
|
||||
total += MonthlyPay(rules, ask, WeeklyHours(catalog, roster, person));
|
||||
}
|
||||
|
||||
return Round(total);
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<SubjectDef> Uncovered(DefCatalog catalog, Roster roster)
|
||||
{
|
||||
var years = new HashSet<int>();
|
||||
@@ -71,7 +143,7 @@ public static class Staffing
|
||||
years.Add(schoolClass.Year);
|
||||
}
|
||||
|
||||
var taught = new HashSet<string>(StringComparer.Ordinal);
|
||||
var teachers = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
foreach (var person in roster.People)
|
||||
{
|
||||
if (!person.IsStaff)
|
||||
@@ -81,19 +153,24 @@ public static class Staffing
|
||||
|
||||
foreach (var subject in person.Subjects)
|
||||
{
|
||||
taught.Add(subject);
|
||||
teachers[subject] = teachers.TryGetValue(subject, out var count) ? count + 1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
var cap = catalog.StaffingRules?.MaxWeeklyHours ?? 0f;
|
||||
var uncovered = new List<SubjectDef>();
|
||||
foreach (var subject in catalog.Subjects.Values)
|
||||
{
|
||||
if (subject.Abstract || !TouchesYears(subject, years) || taught.Contains(subject.DefName))
|
||||
if (subject.Abstract || !TouchesYears(subject, years))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
uncovered.Add(subject);
|
||||
var assigned = teachers.GetValueOrDefault(subject.DefName);
|
||||
if (assigned == 0 || (cap > 0f && SubjectHours(catalog, roster, subject.DefName) > assigned * cap))
|
||||
{
|
||||
uncovered.Add(subject);
|
||||
}
|
||||
}
|
||||
|
||||
return uncovered;
|
||||
@@ -109,7 +186,7 @@ public static class Staffing
|
||||
float allocated)
|
||||
{
|
||||
var rules = RequireRules(catalog);
|
||||
var payroll = Payroll(rules, roster.People);
|
||||
var payroll = Payroll(catalog, rules, roster);
|
||||
|
||||
if (roster.People.Any(person => person.Id.Equals(personId, StringComparison.Ordinal) && person.IsStaff))
|
||||
{
|
||||
@@ -132,13 +209,6 @@ public static class Staffing
|
||||
return Fail(StaffingError.NoOpening, roster, pool, allocated, payroll, payroll);
|
||||
}
|
||||
|
||||
var pay = MonthlyPay(rules, applicant.HourlyWageAsk, subjectCount: 0);
|
||||
var attempted = Round(payroll + pay);
|
||||
if (attempted > allocated)
|
||||
{
|
||||
return Fail(StaffingError.PayrollExceeded, roster, pool, allocated, payroll, attempted);
|
||||
}
|
||||
|
||||
var hired = applicant.Person with
|
||||
{
|
||||
IsStaff = true,
|
||||
@@ -148,9 +218,17 @@ public static class Staffing
|
||||
Subjects = [],
|
||||
};
|
||||
|
||||
// Priced on the resulting roster, not by adding one salary: hours are shared between the
|
||||
// teachers of a subject, so one person joining can change what the others cost.
|
||||
var nextRoster = UpsertStaff(roster, hired);
|
||||
var attempted = Payroll(catalog, rules, nextRoster);
|
||||
if (attempted > allocated)
|
||||
{
|
||||
return Fail(StaffingError.PayrollExceeded, roster, pool, allocated, payroll, attempted);
|
||||
}
|
||||
|
||||
var nextPool = pool.Without(personId);
|
||||
return Ok(nextRoster, nextPool, allocated, Payroll(rules, nextRoster.People));
|
||||
return Ok(nextRoster, nextPool, allocated, attempted);
|
||||
}
|
||||
|
||||
public static StaffingOutcome AssignSubject(
|
||||
@@ -162,7 +240,7 @@ public static class Staffing
|
||||
float allocated)
|
||||
{
|
||||
var rules = RequireRules(catalog);
|
||||
var payroll = Payroll(rules, roster.People);
|
||||
var payroll = Payroll(catalog, rules, roster);
|
||||
var index = IndexOf(roster, personId);
|
||||
if (index < 0)
|
||||
{
|
||||
@@ -192,18 +270,14 @@ public static class Staffing
|
||||
|
||||
var ask = person.HourlyWageAsk ?? ApplicantPool.HourlyAsk(catalog, person);
|
||||
var nextSubjects = person.Subjects.Append(subject).ToArray();
|
||||
var currentPay = person.HourlyWageAsk is float currentAsk
|
||||
? MonthlyPay(rules, currentAsk, person.Subjects.Count)
|
||||
: MonthlyPay(rules, ask, person.Subjects.Count);
|
||||
var nextPay = MonthlyPay(rules, ask, nextSubjects.Length);
|
||||
var attempted = Round(payroll - currentPay + nextPay);
|
||||
var nextRoster = Replace(roster, index, person with { HourlyWageAsk = ask, Subjects = nextSubjects });
|
||||
var attempted = Payroll(catalog, rules, nextRoster);
|
||||
if (attempted > allocated)
|
||||
{
|
||||
return Fail(StaffingError.PayrollExceeded, roster, pool, allocated, payroll, attempted);
|
||||
}
|
||||
|
||||
var nextRoster = Replace(roster, index, person with { HourlyWageAsk = ask, Subjects = nextSubjects });
|
||||
return Ok(nextRoster, pool, allocated, Payroll(rules, nextRoster.People));
|
||||
return Ok(nextRoster, pool, allocated, attempted);
|
||||
}
|
||||
|
||||
public static StaffingOutcome UnassignSubject(
|
||||
@@ -215,7 +289,7 @@ public static class Staffing
|
||||
float allocated)
|
||||
{
|
||||
var rules = RequireRules(catalog);
|
||||
var payroll = Payroll(rules, roster.People);
|
||||
var payroll = Payroll(catalog, rules, roster);
|
||||
var index = IndexOf(roster, personId);
|
||||
if (index < 0)
|
||||
{
|
||||
@@ -235,7 +309,7 @@ public static class Staffing
|
||||
|
||||
var nextSubjects = person.Subjects.Where(name => !name.Equals(subject, StringComparison.Ordinal)).ToArray();
|
||||
var nextRoster = Replace(roster, index, person with { Subjects = nextSubjects });
|
||||
return Ok(nextRoster, pool, allocated, Payroll(rules, nextRoster.People));
|
||||
return Ok(nextRoster, pool, allocated, Payroll(catalog, rules, nextRoster));
|
||||
}
|
||||
|
||||
private static StaffingOutcome Ok(Roster roster, ApplicantPool pool, float allocated, float payroll) =>
|
||||
|
||||
@@ -56,14 +56,7 @@ public static class TimetablePlanner
|
||||
demand.Hours,
|
||||
placed);
|
||||
|
||||
if (leftover > 0)
|
||||
{
|
||||
demand.Hours = leftover;
|
||||
}
|
||||
else
|
||||
{
|
||||
demand.Hours = 0;
|
||||
}
|
||||
demand.Hours = leftover;
|
||||
}
|
||||
|
||||
var uncovered = remaining
|
||||
@@ -162,6 +155,14 @@ public static class TimetablePlanner
|
||||
return leftover;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Picks the slot, and the order days are tried in is the whole difference between a timetable
|
||||
/// and a pile of lessons. Scanning day 0 upwards and taking the first free slot produced blocks
|
||||
/// of the same subject and a week that ran out by Thursday: Monday held 77 lessons, Friday 16.
|
||||
///
|
||||
/// So days are ranked instead: first those that do not have this subject yet, then the emptiest
|
||||
/// one for this class. Periods stay ascending inside the chosen day, so a day has no holes.
|
||||
/// </summary>
|
||||
private static bool TryPlaceOne(
|
||||
Occupancy occupancy,
|
||||
string classId,
|
||||
@@ -172,7 +173,26 @@ public static class TimetablePlanner
|
||||
int lessonCount,
|
||||
List<LessonPlacement> placed)
|
||||
{
|
||||
var days = new int[weekDays];
|
||||
for (var day = 0; day < weekDays; day++)
|
||||
{
|
||||
days[day] = day;
|
||||
}
|
||||
|
||||
Array.Sort(days, (left, right) =>
|
||||
{
|
||||
var bySubject = occupancy.SubjectLoad(classId, subject, left)
|
||||
.CompareTo(occupancy.SubjectLoad(classId, subject, right));
|
||||
if (bySubject != 0)
|
||||
{
|
||||
return bySubject;
|
||||
}
|
||||
|
||||
var byDay = occupancy.DayLoad(classId, left).CompareTo(occupancy.DayLoad(classId, right));
|
||||
return byDay != 0 ? byDay : left.CompareTo(right);
|
||||
});
|
||||
|
||||
foreach (var day in days)
|
||||
{
|
||||
for (var period = 1; period <= lessonCount; period++)
|
||||
{
|
||||
@@ -313,17 +333,33 @@ public static class TimetablePlanner
|
||||
private readonly HashSet<(string Id, int Day, int Period)> _classes = [];
|
||||
private readonly HashSet<(string Id, int Day, int Period)> _teachers = [];
|
||||
private readonly HashSet<(string Id, int Day, int Period)> _rooms = [];
|
||||
private readonly Dictionary<(string ClassId, int Day), int> _dayLoad = [];
|
||||
private readonly Dictionary<(string ClassId, string Subject, int Day), int> _subjectLoad = [];
|
||||
|
||||
public bool IsFree(string classId, string teacherId, string roomId, int day, int period) =>
|
||||
!_classes.Contains((classId, day, period))
|
||||
&& !_teachers.Contains((teacherId, day, period))
|
||||
&& !_rooms.Contains((roomId, day, period));
|
||||
|
||||
/// <summary>How many lessons this class already has that day — counted, not recomputed.</summary>
|
||||
public int DayLoad(string classId, int day) =>
|
||||
_dayLoad.TryGetValue((classId, day), out var count) ? count : 0;
|
||||
|
||||
/// <summary>How many lessons of this subject this class already has that day.</summary>
|
||||
public int SubjectLoad(string classId, string subject, int day) =>
|
||||
_subjectLoad.TryGetValue((classId, subject, day), out var count) ? count : 0;
|
||||
|
||||
public void Add(LessonPlacement lesson)
|
||||
{
|
||||
_classes.Add((lesson.ClassId, lesson.Day, lesson.Period));
|
||||
_teachers.Add((lesson.TeacherId, lesson.Day, lesson.Period));
|
||||
_rooms.Add((lesson.RoomId, lesson.Day, lesson.Period));
|
||||
|
||||
var day = (lesson.ClassId, lesson.Day);
|
||||
_dayLoad[day] = (_dayLoad.TryGetValue(day, out var lessons) ? lessons : 0) + 1;
|
||||
|
||||
var subject = (lesson.ClassId, lesson.Subject, lesson.Day);
|
||||
_subjectLoad[subject] = (_subjectLoad.TryGetValue(subject, out var same) ? same : 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +211,7 @@ internal sealed record StaffMemberResponse(
|
||||
string Position,
|
||||
string PositionLabel,
|
||||
float HourlyWageAsk,
|
||||
float WeeklyHours,
|
||||
float MonthlyPay,
|
||||
IReadOnlyList<DefLabelResponse> Subjects);
|
||||
|
||||
@@ -227,7 +228,7 @@ internal static class StaffingMapper
|
||||
roster ??= new Roster([], [], []);
|
||||
pool ??= ApplicantPool.Empty;
|
||||
var rules = catalog?.StaffingRules;
|
||||
var payroll = rules is null ? 0f : Staffing.Payroll(rules, roster.People);
|
||||
var payroll = rules is null || catalog is null ? 0f : Staffing.Payroll(catalog, rules, roster);
|
||||
var remaining = MathF.Round(MathF.Max(0f, allocated - payroll), 2);
|
||||
|
||||
var uncovered = catalog is null
|
||||
@@ -257,7 +258,7 @@ internal static class StaffingMapper
|
||||
.Where(person => person.IsStaff)
|
||||
.OrderBy(person => person.Name.Surname, StringComparer.Ordinal)
|
||||
.ThenBy(person => person.Id, StringComparer.Ordinal)
|
||||
.Select(person => Member(person, catalog, rules, locale, asOf))
|
||||
.Select(person => Member(person, roster, catalog, rules, locale, asOf))
|
||||
.ToArray();
|
||||
|
||||
var positions = catalog is null
|
||||
@@ -300,13 +301,15 @@ internal static class StaffingMapper
|
||||
|
||||
private static StaffMemberResponse Member(
|
||||
Person person,
|
||||
Roster roster,
|
||||
DefCatalog? catalog,
|
||||
StaffingDef? rules,
|
||||
string locale,
|
||||
DateTime asOf)
|
||||
{
|
||||
var ask = person.HourlyWageAsk ?? 0f;
|
||||
var pay = rules is null ? 0f : Staffing.MonthlyPay(rules, ask, person.Subjects.Count);
|
||||
var hours = catalog is null ? 0f : Staffing.WeeklyHours(catalog, roster, person);
|
||||
var pay = rules is null ? 0f : Staffing.MonthlyPay(rules, ask, hours);
|
||||
var subjects = person.Subjects
|
||||
.Select(name => new DefLabelResponse(
|
||||
name,
|
||||
@@ -324,6 +327,7 @@ internal static class StaffingMapper
|
||||
person.Position ?? string.Empty,
|
||||
PeopleListMapper.PositionLabel(catalog, locale, person.Position) ?? person.Position ?? string.Empty,
|
||||
ask,
|
||||
MathF.Round(hours, 1),
|
||||
pay,
|
||||
subjects);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Simulation": {
|
||||
"TickRate": 20,
|
||||
"MaxSchools": 6,
|
||||
"GameMinutesPerRealSecond": 5,
|
||||
"DefaultStartDate": "2012-03-31T06:00:00",
|
||||
"SavesDirectory": "saves",
|
||||
"ModsDirectory": "mods",
|
||||
"SaveIntervalSeconds": 30,
|
||||
"MonthlyPayrollCap": 10000,
|
||||
"SchoolWeekDays": 5
|
||||
}
|
||||
}
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Simulation": {
|
||||
"TickRate": 20,
|
||||
"MaxSchools": 6,
|
||||
"GameMinutesPerRealSecond": 5,
|
||||
"DefaultStartDate": "2012-03-31T06:00:00",
|
||||
"SavesDirectory": "saves",
|
||||
"ModsDirectory": "mods",
|
||||
"SaveIntervalSeconds": 30,
|
||||
"MonthlyPayrollCap": 40000,
|
||||
"SchoolWeekDays": 5
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"parentChance": 0.35,
|
||||
"hourlyWageBase": 30,
|
||||
"hourlyWagePerSkill": 0.6,
|
||||
// One ставка. A hired person costs this much even with no subjects, and hours beyond it are
|
||||
// paid on top — that is what makes loading one teacher instead of hiring two cost money.
|
||||
"baseWeeklyHours": 20,
|
||||
// Nobody can carry more than this. Past it the subject needs a second teacher.
|
||||
"maxWeeklyHours": 36,
|
||||
"weeksPerMonth": 4,
|
||||
"extraSubjectSurcharge": 0.25,
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public sealed class SimulationOptions
|
||||
/// Monthly payroll the player may commit. Hire and subject assignment that would exceed it
|
||||
/// are rejected immediately; money itself does not move.
|
||||
/// </summary>
|
||||
public float MonthlyPayrollCap { get; set; } = 10_000f;
|
||||
public float MonthlyPayrollCap { get; set; } = 40_000f;
|
||||
|
||||
/// <summary>
|
||||
/// Working days from Monday. Five is Mon–Fri; six adds Saturday; seven is every day.
|
||||
|
||||
Reference in New Issue
Block a user