Enhance school creation and staffing management with native language support
- Updated protocol documentation to include `nativeLanguages` in `nameSets` and added `nativeLanguage` to school creation options. - Enhanced the UI for school creation to allow selection of native languages, improving user experience. - Revised API interfaces to accommodate new native language features, ensuring proper data handling. - Improved localization strings to support new native language functionalities in both English and Russian. - Updated tests to validate the new native language features and ensure robust functionality in staffing scenarios.
This commit is contained in:
@@ -35,28 +35,275 @@ internal static class PersonSampler
|
||||
|
||||
public static Dictionary<string, int> Skills(
|
||||
DefCatalog catalog,
|
||||
NameSetDef? names,
|
||||
Random rng,
|
||||
int age,
|
||||
IReadOnlyDictionary<string, string> choices,
|
||||
IReadOnlyList<string> traits)
|
||||
IReadOnlyList<string> traits,
|
||||
int? pupilYear,
|
||||
bool adult,
|
||||
string? nativeLanguage = null)
|
||||
{
|
||||
var values = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
foreach (var skill in catalog.Skills.Values.OrderBy(candidate => candidate.DefName, StringComparer.Ordinal))
|
||||
{
|
||||
if (skill.Abstract)
|
||||
if (!skill.Abstract && skill.Always)
|
||||
{
|
||||
RollInto(values, rng, age, choices, skill);
|
||||
}
|
||||
}
|
||||
|
||||
GrantNative(values, catalog, NativeOf(names, nativeLanguage), rng, age, choices);
|
||||
if (pupilYear is { } year)
|
||||
{
|
||||
GrantYearSubjects(values, catalog, rng, age, choices, year);
|
||||
}
|
||||
|
||||
if (adult)
|
||||
{
|
||||
GrantAdultExtras(values, catalog, rng, age, choices);
|
||||
}
|
||||
|
||||
ApplyTraitsAndBody(values, catalog, choices, traits, values.Keys.ToArray());
|
||||
// After traits so Diligent cannot push a related tongue past the "knows a bit" cap.
|
||||
GrantRelated(values, catalog, names, NativeOf(names, nativeLanguage), rng, choices);
|
||||
return values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds subject skills a pupil just became old enough for. Existing values stay put — yearly
|
||||
/// intake must not re-roll mathematics every September.
|
||||
/// </summary>
|
||||
public static Dictionary<string, int> EnsurePupilYear(
|
||||
DefCatalog catalog,
|
||||
NameSetDef? names,
|
||||
Random rng,
|
||||
int age,
|
||||
IReadOnlyDictionary<string, string> choices,
|
||||
IReadOnlyList<string> traits,
|
||||
IReadOnlyDictionary<string, int> existing,
|
||||
int pupilYear,
|
||||
string? nativeLanguage = null)
|
||||
{
|
||||
var values = new Dictionary<string, int>(existing, StringComparer.Ordinal);
|
||||
foreach (var skill in catalog.Skills.Values.OrderBy(candidate => candidate.DefName, StringComparer.Ordinal))
|
||||
{
|
||||
if (skill.Abstract || !skill.Always || values.ContainsKey(skill.DefName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var mean = MeanForAge(skill, age);
|
||||
var stdDev = skill.Distribution?.StdDev ?? 10f;
|
||||
var rolled = (int)Math.Round(mean + (stdDev * NextGaussian(rng)));
|
||||
rolled = Clamp(rolled, skill.Range);
|
||||
rolled = ApplyBodyLimits(rolled, skill, choices);
|
||||
rolled = Clamp(rolled, skill.Range);
|
||||
values[skill.DefName] = rolled;
|
||||
RollInto(values, rng, age, choices, skill);
|
||||
}
|
||||
|
||||
GrantNative(values, catalog, NativeOf(names, nativeLanguage), rng, age, choices);
|
||||
GrantYearSubjects(values, catalog, rng, age, choices, pupilYear);
|
||||
|
||||
var added = values.Keys.Where(name => !existing.ContainsKey(name)).ToArray();
|
||||
ApplyTraitsAndBody(values, catalog, choices, traits, added);
|
||||
return values;
|
||||
}
|
||||
|
||||
private static string? NativeOf(NameSetDef? names, string? nativeLanguage)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(nativeLanguage))
|
||||
{
|
||||
return nativeLanguage;
|
||||
}
|
||||
|
||||
var spoken = names?.Spoken;
|
||||
return spoken is { Count: > 0 } ? spoken[0] : null;
|
||||
}
|
||||
|
||||
private static void GrantNative(
|
||||
Dictionary<string, int> values,
|
||||
DefCatalog catalog,
|
||||
string? native,
|
||||
Random rng,
|
||||
int age,
|
||||
IReadOnlyDictionary<string, string> choices)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(native)
|
||||
|| !catalog.Skills.TryGetValue(native, out var skill)
|
||||
|| skill.Abstract)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RollInto(values, rng, age, choices, skill);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Other tongues of the same name set, at a low level. Skips keys already granted as native
|
||||
/// or by a subject — a Belarusian pupil who studies Russian keeps the school value.
|
||||
/// </summary>
|
||||
private static void GrantRelated(
|
||||
Dictionary<string, int> values,
|
||||
DefCatalog catalog,
|
||||
NameSetDef? names,
|
||||
string? native,
|
||||
Random rng,
|
||||
IReadOnlyDictionary<string, string> choices)
|
||||
{
|
||||
if (names is null || names.RelatedLanguageChance <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var name in names.Spoken.OrderBy(candidate => candidate, StringComparer.Ordinal))
|
||||
{
|
||||
if (name.Equals(native, StringComparison.Ordinal)
|
||||
|| values.ContainsKey(name)
|
||||
|| !catalog.Skills.TryGetValue(name, out var skill)
|
||||
|| skill.Abstract)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rng.NextDouble() >= names.RelatedLanguageChance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var rolled = (int)Math.Round(names.RelatedLanguageMean + (names.RelatedLanguageStdDev * NextGaussian(rng)));
|
||||
rolled = Clamp(rolled, skill.Range);
|
||||
rolled = Math.Min(rolled, names.RelatedLanguageMax);
|
||||
rolled = ApplyBodyLimits(rolled, skill, choices);
|
||||
values[skill.DefName] = Clamp(rolled, skill.Range);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GrantYearSubjects(
|
||||
Dictionary<string, int> values,
|
||||
DefCatalog catalog,
|
||||
Random rng,
|
||||
int age,
|
||||
IReadOnlyDictionary<string, string> choices,
|
||||
int pupilYear)
|
||||
{
|
||||
foreach (var subject in catalog.Subjects.Values.OrderBy(def => def.DefName, StringComparer.Ordinal))
|
||||
{
|
||||
if (subject.Abstract || pupilYear < subject.Grades.Min || pupilYear > subject.Grades.Max)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var share in subject.Skills.OrderBy(row => row.Skill, StringComparer.Ordinal))
|
||||
{
|
||||
if (!catalog.Skills.TryGetValue(share.Skill, out var skill) || skill.Abstract)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RollInto(values, rng, age, choices, skill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void GrantAdultExtras(
|
||||
Dictionary<string, int> values,
|
||||
DefCatalog catalog,
|
||||
Random rng,
|
||||
int age,
|
||||
IReadOnlyDictionary<string, string> choices)
|
||||
{
|
||||
var academic = catalog.Skills.Values
|
||||
.Where(skill =>
|
||||
!skill.Abstract
|
||||
&& !skill.Always
|
||||
&& !skill.Work
|
||||
&& skill.AdultChance <= 0
|
||||
&& SubjectUses(catalog, skill.DefName))
|
||||
.OrderBy(skill => skill.DefName, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
PickInto(values, rng, age, choices, academic, rng.Next(3));
|
||||
|
||||
var work = catalog.Skills.Values
|
||||
.Where(skill => !skill.Abstract && skill.Work)
|
||||
.OrderBy(skill => skill.DefName, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
PickInto(values, rng, age, choices, work, rng.Next(3));
|
||||
|
||||
foreach (var skill in catalog.Skills.Values.OrderBy(candidate => candidate.DefName, StringComparer.Ordinal))
|
||||
{
|
||||
if (skill.Abstract || skill.AdultChance <= 0 || values.ContainsKey(skill.DefName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rng.NextDouble() < skill.AdultChance)
|
||||
{
|
||||
RollInto(values, rng, age, choices, skill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool SubjectUses(DefCatalog catalog, string skillName)
|
||||
{
|
||||
foreach (var subject in catalog.Subjects.Values)
|
||||
{
|
||||
if (subject.Abstract)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var share in subject.Skills)
|
||||
{
|
||||
if (share.Skill.Equals(skillName, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void PickInto(
|
||||
Dictionary<string, int> values,
|
||||
Random rng,
|
||||
int age,
|
||||
IReadOnlyDictionary<string, string> choices,
|
||||
List<SkillDef> pool,
|
||||
int count)
|
||||
{
|
||||
var remaining = pool.Where(skill => !values.ContainsKey(skill.DefName)).ToList();
|
||||
for (var n = 0; n < count && remaining.Count > 0; n++)
|
||||
{
|
||||
var index = rng.Next(remaining.Count);
|
||||
RollInto(values, rng, age, choices, remaining[index]);
|
||||
remaining.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RollInto(
|
||||
Dictionary<string, int> values,
|
||||
Random rng,
|
||||
int age,
|
||||
IReadOnlyDictionary<string, string> choices,
|
||||
SkillDef skill)
|
||||
{
|
||||
if (values.ContainsKey(skill.DefName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var mean = MeanForAge(skill, age);
|
||||
var stdDev = skill.Distribution?.StdDev ?? 10f;
|
||||
var rolled = (int)Math.Round(mean + (stdDev * NextGaussian(rng)));
|
||||
rolled = Clamp(rolled, skill.Range);
|
||||
rolled = ApplyBodyLimits(rolled, skill, choices);
|
||||
values[skill.DefName] = Clamp(rolled, skill.Range);
|
||||
}
|
||||
|
||||
private static void ApplyTraitsAndBody(
|
||||
Dictionary<string, int> values,
|
||||
DefCatalog catalog,
|
||||
IReadOnlyDictionary<string, string> choices,
|
||||
IReadOnlyList<string> traits,
|
||||
IEnumerable<string> names)
|
||||
{
|
||||
var allowed = names as ISet<string> ?? new HashSet<string>(names, StringComparer.Ordinal);
|
||||
var touched = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var traitName in traits)
|
||||
{
|
||||
@@ -67,7 +314,8 @@ internal static class PersonSampler
|
||||
|
||||
foreach (var modifier in trait.SkillModifiers)
|
||||
{
|
||||
if (!values.TryGetValue(modifier.Skill, out var current)
|
||||
if (!allowed.Contains(modifier.Skill)
|
||||
|| !values.TryGetValue(modifier.Skill, out var current)
|
||||
|| !catalog.Skills.TryGetValue(modifier.Skill, out var skill))
|
||||
{
|
||||
continue;
|
||||
@@ -86,8 +334,6 @@ internal static class PersonSampler
|
||||
var skill = catalog.Skills[skillName];
|
||||
values[skillName] = Clamp(ApplyBodyLimits(values[skillName], skill, choices), skill.Range);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
public static List<string> Traits(DefCatalog catalog, Random rng, IReadOnlyList<string> roles, int age)
|
||||
|
||||
Reference in New Issue
Block a user