Files
h-school/src/HSchool.Content/PeopleDefs.cs
T
Leonid Pershin 25ce26064d
ci / server (push) Failing after 3m39s
ci / client (push) Successful in 14s
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.
2026-08-19 12:51:38 +03:00

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; } = [];
}