- 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.
314 lines
7.9 KiB
C#
314 lines
7.9 KiB
C#
namespace HSchool.Content;
|
|
|
|
/// <summary>Roles the generator and traits talk about. Code, not a def — new roles are a rebuild.</summary>
|
|
public static class PersonRoles
|
|
{
|
|
public const string Student = "student";
|
|
public const string Staff = "staff";
|
|
public const string Parent = "parent";
|
|
|
|
public static bool IsKnown(string role) =>
|
|
role.Equals(Student, StringComparison.OrdinalIgnoreCase)
|
|
|| role.Equals(Staff, StringComparison.OrdinalIgnoreCase)
|
|
|| role.Equals(Parent, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Derived body type, not a <see cref="BodyAttributeDef"/>. Skill limits may name it the same
|
|
/// way they name hair colour; the generator computes it from height and weight.
|
|
/// </summary>
|
|
public static class BodyBuilds
|
|
{
|
|
public const string Attribute = "Build";
|
|
|
|
/// <summary>
|
|
/// The two numeric attributes the build is computed from. They are named in code, so a pack
|
|
/// that renames or drops them is rejected at load rather than quietly producing an average
|
|
/// build for everybody.
|
|
/// </summary>
|
|
public const string HeightAttribute = "Height";
|
|
|
|
public const string WeightAttribute = "Weight";
|
|
|
|
public const string Skinny = "Skinny";
|
|
public const string Average = "Average";
|
|
public const string Athletic = "Athletic";
|
|
public const string Heavy = "Heavy";
|
|
public const string Obese = "Obese";
|
|
|
|
public static readonly IReadOnlyList<string> Values =
|
|
[Skinny, Average, Athletic, Heavy, Obese];
|
|
|
|
public static bool IsKnown(string value) =>
|
|
Values.Any(candidate => candidate.Equals(value, StringComparison.Ordinal));
|
|
|
|
/// <summary>
|
|
/// WHO-ish bands with a fit slice in the healthy range so <c>Athletic</c> is derived, not rolled.
|
|
/// Height in centimetres, weight in kilograms.
|
|
/// </summary>
|
|
public static string FromHeightAndWeight(int heightCm, int weightKg)
|
|
{
|
|
if (heightCm <= 0)
|
|
{
|
|
return Average;
|
|
}
|
|
|
|
var metres = heightCm / 100d;
|
|
var bmi = weightKg / (metres * metres);
|
|
if (bmi < 18.5)
|
|
{
|
|
return Skinny;
|
|
}
|
|
|
|
if (bmi < 22.0)
|
|
{
|
|
return Average;
|
|
}
|
|
|
|
if (bmi < 25.0)
|
|
{
|
|
return Athletic;
|
|
}
|
|
|
|
if (bmi < 30.0)
|
|
{
|
|
return Heavy;
|
|
}
|
|
|
|
return Obese;
|
|
}
|
|
}
|
|
|
|
public sealed class IntRange
|
|
{
|
|
public int Min { get; init; }
|
|
|
|
public int Max { get; init; } = 100;
|
|
}
|
|
|
|
public sealed class StatDistribution
|
|
{
|
|
public float Mean { get; init; }
|
|
|
|
public float StdDev { get; init; } = 1;
|
|
}
|
|
|
|
public sealed class AgeMeanPoint
|
|
{
|
|
public int Age { get; init; }
|
|
|
|
public float Mean { get; init; }
|
|
}
|
|
|
|
public sealed class BodySkillLimit
|
|
{
|
|
public required string Attribute { get; init; }
|
|
|
|
public string? Value { get; init; }
|
|
|
|
public int? Min { get; init; }
|
|
|
|
public int? Max { get; init; }
|
|
}
|
|
|
|
public sealed class SkillDef : Def
|
|
{
|
|
public IntRange Range { get; init; } = new();
|
|
|
|
public StatDistribution? Distribution { get; init; }
|
|
|
|
public IReadOnlyList<AgeMeanPoint> AgeMeans { get; init; } = [];
|
|
|
|
public IReadOnlyList<BodySkillLimit> BodyLimits { get; init; } = [];
|
|
}
|
|
|
|
public sealed class SubjectSkillShare
|
|
{
|
|
public required string Skill { get; init; }
|
|
|
|
public float Share { get; init; }
|
|
}
|
|
|
|
public sealed class SubjectDef : Def
|
|
{
|
|
public IntRange Grades { get; init; } = new() { Min = 1, Max = 11 };
|
|
|
|
public int HoursPerWeek { get; init; }
|
|
|
|
public IReadOnlyList<SubjectSkillShare> Skills { get; init; } = [];
|
|
|
|
/// <summary>
|
|
/// RoomDef the lesson needs. Null means the class's homeroom. PE uses a gym, informatics a lab.
|
|
/// </summary>
|
|
public string? Room { get; init; }
|
|
}
|
|
|
|
public sealed class TraitSkillModifier
|
|
{
|
|
public required string Skill { get; init; }
|
|
|
|
public int Offset { get; init; }
|
|
}
|
|
|
|
public sealed class TraitDef : Def
|
|
{
|
|
public int Weight { get; init; } = 1;
|
|
|
|
public IReadOnlyList<string> Incompatible { get; init; } = [];
|
|
|
|
/// <summary>Empty means every role. Values are <see cref="PersonRoles"/> ids.</summary>
|
|
public IReadOnlyList<string> Roles { get; init; } = [];
|
|
|
|
public IntRange? Age { get; init; }
|
|
|
|
public IReadOnlyList<TraitSkillModifier> SkillModifiers { get; init; } = [];
|
|
|
|
/// <summary>
|
|
/// Added to the hourly wage ask. Positive means the person wants more at the same skills.
|
|
/// </summary>
|
|
public float WageAsk { get; init; }
|
|
}
|
|
|
|
public sealed class StaffingDef : Def
|
|
{
|
|
public int PoolSize { get; init; }
|
|
|
|
public float StayChance { get; init; }
|
|
|
|
public float ParentChance { get; init; }
|
|
|
|
public float HourlyWageBase { get; init; }
|
|
|
|
public float HourlyWagePerSkill { get; init; }
|
|
|
|
/// <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; }
|
|
|
|
/// <summary>
|
|
/// 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 MaxWeeklyHours { get; init; }
|
|
|
|
public float WeeksPerMonth { get; init; }
|
|
}
|
|
|
|
public enum BodyAttributeKind
|
|
{
|
|
Number,
|
|
Choice,
|
|
}
|
|
|
|
public sealed class SexAgeDistribution
|
|
{
|
|
/// <summary><c>male</c>, <c>female</c>, or omit for both.</summary>
|
|
public string? Sex { get; init; }
|
|
|
|
public int? AgeMin { get; init; }
|
|
|
|
public int? AgeMax { get; init; }
|
|
|
|
public required StatDistribution Distribution { get; init; }
|
|
|
|
public IntRange? Range { get; init; }
|
|
}
|
|
|
|
public sealed class WeightedOption
|
|
{
|
|
public required string Value { get; init; }
|
|
|
|
public int Weight { get; init; } = 1;
|
|
|
|
public string? Sex { get; init; }
|
|
|
|
public int? AgeMin { get; init; }
|
|
|
|
public int? AgeMax { get; init; }
|
|
}
|
|
|
|
public sealed class BodyAttributeDef : Def
|
|
{
|
|
public BodyAttributeKind Kind { get; init; }
|
|
|
|
public IReadOnlyList<SexAgeDistribution> Distributions { get; init; } = [];
|
|
|
|
public IReadOnlyList<WeightedOption> Options { get; init; } = [];
|
|
}
|
|
|
|
public sealed class NeedDef : Def
|
|
{
|
|
public float Initial { get; init; } = 1;
|
|
|
|
public float DecayPerHour { get; init; }
|
|
|
|
public float Min { get; init; }
|
|
|
|
public float Max { get; init; } = 1;
|
|
}
|
|
|
|
public sealed class CaseTable
|
|
{
|
|
public required string Nom { get; init; }
|
|
|
|
public required string Gen { get; init; }
|
|
|
|
public required string Dat { get; init; }
|
|
|
|
public required string Acc { get; init; }
|
|
|
|
public required string Ins { get; init; }
|
|
|
|
public required string Pre { get; init; }
|
|
|
|
public string this[GrammaticalCase grammaticalCase] => grammaticalCase switch
|
|
{
|
|
GrammaticalCase.Nominative => Nom,
|
|
GrammaticalCase.Genitive => Gen,
|
|
GrammaticalCase.Dative => Dat,
|
|
GrammaticalCase.Accusative => Acc,
|
|
GrammaticalCase.Instrumental => Ins,
|
|
GrammaticalCase.Prepositional => Pre,
|
|
_ => Nom,
|
|
};
|
|
}
|
|
|
|
public sealed class GivenNameEntry
|
|
{
|
|
public required string Form { get; init; }
|
|
|
|
public string? Declension { get; init; }
|
|
|
|
public CaseTable? Cases { get; init; }
|
|
}
|
|
|
|
public sealed class SurnameEntry
|
|
{
|
|
public required string Male { get; init; }
|
|
|
|
public required string Female { get; init; }
|
|
|
|
public string? Declension { get; init; }
|
|
|
|
public CaseTable? MaleCases { get; init; }
|
|
|
|
public CaseTable? FemaleCases { get; init; }
|
|
}
|
|
|
|
public sealed class NameSetDef : Def
|
|
{
|
|
public string PatronymicRule { get; init; } = NameGrammar.SlavicPatronymic;
|
|
|
|
public string DefaultGivenDeclension { get; init; } = NameGrammar.Hard;
|
|
|
|
public string DefaultSurnameDeclension { get; init; } = NameGrammar.Ov;
|
|
|
|
public IReadOnlyList<GivenNameEntry> MaleGiven { get; init; } = [];
|
|
|
|
public IReadOnlyList<GivenNameEntry> FemaleGiven { get; init; } = [];
|
|
|
|
public IReadOnlyList<SurnameEntry> Surnames { get; init; } = [];
|
|
}
|