namespace HSchool.Content; /// Roles the generator and traits talk about. Code, not a def — new roles are a rebuild. 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); } /// /// Derived body type, not a . Skill limits may name it the same /// way they name hair colour; the generator computes it from height and weight. /// public static class BodyBuilds { public const string Attribute = "Build"; /// /// 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. /// 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 Values = [Skinny, Average, Athletic, Heavy, Obese]; public static bool IsKnown(string value) => Values.Any(candidate => candidate.Equals(value, StringComparison.Ordinal)); /// /// WHO-ish bands with a fit slice in the healthy range so Athletic is derived, not rolled. /// Height in centimetres, weight in kilograms. /// 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 AgeMeans { get; init; } = []; public IReadOnlyList BodyLimits { get; init; } = []; /// Everyone has this: body, speech, the floor under a profession. public bool Always { get; init; } /// Adult extras: cook, nurse, secretary. Not rolled for pupils. public bool Work { get; init; } /// /// Probability that an adult also has this, on top of the native language. 0 means never /// by chance — pupils still get it when a subject of their year points here. /// public float AdultChance { 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 Skills { get; init; } = []; /// /// RoomDef the lesson needs. Null means the class's homeroom. PE uses a gym, informatics a lab. /// 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 Incompatible { get; init; } = []; /// Empty means every role. Values are ids. public IReadOnlyList Roles { get; init; } = []; public IntRange? Age { get; init; } public IReadOnlyList SkillModifiers { get; init; } = []; /// /// Added to the hourly wage ask. Positive means the person wants more at the same skills. /// public float WageAsk { get; init; } /// /// Extra minutes of commute slack. Positive arrives earlier; negative cuts it closer. /// public int CommuteMinutes { get; init; } /// /// Shifts the warmth comfort band, in °C. Heat-loving is positive (suffers cold earlier); /// cold-loving is negative. /// public float ComfortTemperatureOffset { 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; } /// /// 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. /// public float BaseWeeklyHours { get; init; } /// /// 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. /// public float MaxWeeklyHours { get; init; } public float WeeksPerMonth { get; init; } } /// /// One school's behaviour numbers: when a need is urgent, how fast lessons teach, commute slack, /// how much a new goal must beat the current one before a person switches, and the four goal /// weights the decision planner compares. A catalog may have only one concrete ruleset. Missing /// weights keep today's numbers so a pack without them does not empty the classrooms. /// public sealed class BehaviorDef : Def { /// A need at or below this value is urgent. The planner turns that into a goal weight. public float NeedThreshold { get; init; } /// Skill points a lesson adds per game hour, before traits and need state. public float LessonSkillPerHour { get; init; } /// Inclusive range of extra commute minutes rolled per person per day. public int CommuteSlackMin { get; init; } public int CommuteSlackMax { get; init; } /// A new goal must beat the current one by this much before the person switches. public float SwitchMargin { get; init; } /// Lesson or posted work. Beats leisure and a need that only just crossed the threshold. public float DutyLessonWeight { get; init; } = 10f; /// Walk to the next room on a break. Beats chatting in the corridor you are standing in. public float DutyTravelWeight { get; init; } = 5f; /// Need at zero. Beats a lesson so a desperate toilet trip leaves class. public float NeedWeightAtZero { get; init; } = 20f; /// /// A sitting during this parallel's own lunch break. Above so /// lunch beats walking on to the next room, below so it never /// pulls anybody out of a lesson. /// public float LunchWeight { get; init; } = 6f; /// Kilograms a person can carry at strength/endurance/hauling 0, before the per-skill terms. public float CarryMassBase { get; init; } = 5f; public float CarryMassPerStrength { get; init; } = 0.08f; public float CarryMassPerEndurance { get; init; } = 0.04f; public float CarryMassPerHauling { get; init; } = 0.08f; /// Chance each optional layer (sweater, coat, hat, accessory) is worn at generation. public float OptionalApparelChance { get; init; } = 0.4f; } public enum BodyAttributeKind { Number, Choice, } public sealed class SexAgeDistribution { /// male, female, or omit for both. 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 Distributions { get; init; } = []; public IReadOnlyList 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; /// /// When true, this need snaps to off campus instead of draining. Sleep /// restores overnight; hunger does not keep falling at home. /// public bool RestoredOffCampus { get; init; } /// /// When true, campus drain is not per hour. Warmth uses it as the /// drop per °C of mismatch against the place temperature. /// public bool Environmental { get; init; } } 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; } } /// /// Nested name grammar of a . Not a selectable catalog kind of its own. /// public sealed class NameSetDef { public string PatronymicRule { get; init; } = NameGrammar.SlavicPatronymic; public string DefaultGivenDeclension { get; init; } = NameGrammar.Hard; public string DefaultSurnameDeclension { get; init; } = NameGrammar.Ov; /// /// Languages this country can speak natively. Slavic names cover Russian, Belarusian and /// Ukrainian; the school picks one at create. Singular is still /// accepted in JSONC for a one-language pack. /// public IReadOnlyList NativeLanguages { get; init; } = []; /// One-language form. Folded into when the list is empty. public string? NativeLanguage { get; init; } public IReadOnlyList Spoken => NativeLanguages.Count > 0 ? NativeLanguages : string.IsNullOrWhiteSpace(NativeLanguage) ? [] : [NativeLanguage]; /// /// Chance each other language in is present at a low level — a Russian /// speaker who understands Belarusian. 0 leaves relatives off the card. /// public float RelatedLanguageChance { get; init; } public float RelatedLanguageMean { get; init; } = 22f; public float RelatedLanguageStdDev { get; init; } = 8f; /// Cap so a related roll cannot look like a native speaker. public int RelatedLanguageMax { get; init; } = 40; public IReadOnlyList MaleGiven { get; init; } = []; public IReadOnlyList FemaleGiven { get; init; } = []; public IReadOnlyList Surnames { get; init; } = []; } /// /// What the player picks at create: nested names plus climate-preset ids. Weather numbers live /// on . /// public sealed class CountryDef : Def { public IReadOnlyList ClimatePresets { get; init; } = []; public NameSetDef Names { get; init; } = new(); } /// /// Outdoor climate a country may roll. Monthly norms, day/hour spread and precipitation chance /// are the numbers the school uses to sample the street; indoor offset is walls without a technician. /// public sealed class ClimatePresetDef : Def { /// Mean outdoor °C for months 1–12. Concrete presets must list all twelve. public IReadOnlyList MonthlyNorms { get; init; } = []; /// How far a day's mean may wander from the monthly norm, °C. public float DaySpread { get; init; } /// How far the hour wanders from that day's mean, °C. Coldest around 03:00, warmest 15:00. public float HourSpread { get; init; } /// Chance of precipitation this hour, 0–1. Below 0 °C the same roll is snow. public float PrecipitationChance { get; init; } /// Added to indoor temperature vs the street. Walls hold heat; this is not comfort. public float IndoorOffset { get; init; } = 8f; /// Centre of the clothing comfort band, °C, before trait offsets. public float ComfortC { get; init; } = 21f; /// Half-width of the comfort band, °C. Inside it warmth barely drops. public float ComfortHalfWidthC { get; init; } = 3f; /// How many °C of protection one insulation point is worth. public float InsulationPerC { get; init; } = 1f; }