Implement applicant pool functionality in school simulation, allowing for the management of job seekers who are not yet part of the school roster. Update the catalog to include staffing definitions and enhance the API to support applicant data retrieval. Revise the school architecture to handle applicant refresh logic and ensure proper integration with existing roster management. Update tests to validate the new applicant functionalities and ensure robustness in handling staffing scenarios.

This commit is contained in:
Leonid Pershin
2026-08-19 00:01:56 +03:00
parent 65feda3756
commit c189578680
31 changed files with 541 additions and 120 deletions
+5
View File
@@ -308,6 +308,7 @@ public sealed class CatalogLoader
var needs = new Dictionary<string, NeedDef>(StringComparer.Ordinal);
var nameSets = new Dictionary<string, NameSetDef>(StringComparer.Ordinal);
var subjects = new Dictionary<string, SubjectDef>(StringComparer.Ordinal);
var staffing = new Dictionary<string, StaffingDef>(StringComparer.Ordinal);
foreach (var (key, json) in resolved)
{
@@ -355,6 +356,9 @@ public sealed class CatalogLoader
case DefKind.Subject:
subjects[key.Name] = Jsonc.Deserialize<SubjectDef>(json);
break;
case DefKind.Staffing:
staffing[key.Name] = Jsonc.Deserialize<StaffingDef>(json);
break;
}
}
@@ -374,6 +378,7 @@ public sealed class CatalogLoader
needs,
nameSets,
subjects,
staffing,
ru,
en);
}
+9
View File
@@ -22,6 +22,7 @@ public sealed class DefCatalog
IReadOnlyDictionary<string, NeedDef> needs,
IReadOnlyDictionary<string, NameSetDef> nameSets,
IReadOnlyDictionary<string, SubjectDef> subjects,
IReadOnlyDictionary<string, StaffingDef> staffing,
IReadOnlyDictionary<string, string> ru,
IReadOnlyDictionary<string, string> en)
{
@@ -40,6 +41,7 @@ public sealed class DefCatalog
Needs = needs;
NameSets = nameSets;
Subjects = subjects;
Staffing = staffing;
_ru = ru;
_en = en;
AnyNeedDecays = needs.Values.Any(need => !need.Abstract && need.DecayPerHour > 0f);
@@ -82,6 +84,11 @@ public sealed class DefCatalog
public IReadOnlyDictionary<string, SubjectDef> Subjects { get; }
public IReadOnlyDictionary<string, StaffingDef> Staffing { get; }
/// <summary>The one concrete staffing ruleset, or null when a pack has not defined it.</summary>
public StaffingDef? StaffingRules => Staffing.Values.FirstOrDefault(def => !def.Abstract);
private readonly IReadOnlyDictionary<string, string> _ru;
private readonly IReadOnlyDictionary<string, string> _en;
@@ -103,6 +110,7 @@ public sealed class DefCatalog
DefKind.Need => Needs.GetValueOrDefault(defName),
DefKind.NameSet => NameSets.GetValueOrDefault(defName),
DefKind.Subject => Subjects.GetValueOrDefault(defName),
DefKind.Staffing => Staffing.GetValueOrDefault(defName),
_ => null,
};
@@ -177,6 +185,7 @@ public sealed class DefCatalog
NeedDef => DefKind.Need,
NameSetDef => DefKind.NameSet,
SubjectDef => DefKind.Subject,
StaffingDef => DefKind.Staffing,
_ => throw new ArgumentOutOfRangeException(nameof(def)),
};
+1
View File
@@ -16,6 +16,7 @@ public enum DefKind
Need,
NameSet,
Subject,
Staffing,
}
/// <summary>Shared JSONC fields. Kind comes from the folder under <c>defs/</c>, not from the file.</summary>
+3
View File
@@ -104,6 +104,9 @@ internal static class PackPaths
case "subjects":
kind = DefKind.Subject;
return true;
case "staffing":
kind = DefKind.Staffing;
return true;
default:
kind = default;
return false;
+38
View File
@@ -34,6 +34,16 @@ internal static class PeopleDefValidator
ValidateSubject(subject, catalog);
}
foreach (var staffing in catalog.Staffing.Values)
{
ValidateStaffing(staffing);
}
if (catalog.Staffing.Values.Count(def => !def.Abstract) > 1)
{
throw new ContentLoadException("A catalog may only have one concrete StaffingDef.");
}
RequireBuildInputs(catalog);
}
@@ -260,6 +270,34 @@ internal static class PeopleDefValidator
}
}
private static void ValidateStaffing(StaffingDef staffing)
{
if (staffing.Abstract)
{
return;
}
if (staffing.PoolSize < 1 || staffing.PoolSize > 64)
{
throw new ContentLoadException($"StaffingDef '{staffing.DefName}' poolSize must be 164.");
}
if (staffing.StayChance <= 0f || staffing.StayChance >= 1f)
{
throw new ContentLoadException($"StaffingDef '{staffing.DefName}' stayChance must be between 0 and 1 exclusive.");
}
if (staffing.ParentChance < 0f || staffing.ParentChance > 1f)
{
throw new ContentLoadException($"StaffingDef '{staffing.DefName}' parentChance must be 01.");
}
if (staffing.HourlyWageBase < 0f || staffing.HourlyWagePerSkill < 0f)
{
throw new ContentLoadException($"StaffingDef '{staffing.DefName}' wage scale cannot be negative.");
}
}
private static void ValidateNameSet(NameSetDef names)
{
if (!NameGrammar.IsKnownPatronymic(names.PatronymicRule))
+18
View File
@@ -157,6 +157,24 @@ public sealed class TraitDef : Def
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; }
}
public enum BodyAttributeKind