Merge branch 'phase/78-diseases'
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -319,6 +319,7 @@ public sealed class CatalogLoader
|
||||
var orientations = new Dictionary<string, OrientationDef>(StringComparer.Ordinal);
|
||||
var affinity = new Dictionary<string, AffinityRulesDef>(StringComparer.Ordinal);
|
||||
var events = new Dictionary<string, EventDef>(StringComparer.Ordinal);
|
||||
var diseases = new Dictionary<string, DiseaseDef>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var (key, json) in resolved)
|
||||
{
|
||||
@@ -396,6 +397,9 @@ public sealed class CatalogLoader
|
||||
case DefKind.Event:
|
||||
events[key.Name] = Jsonc.Deserialize<EventDef>(json);
|
||||
break;
|
||||
case DefKind.Disease:
|
||||
diseases[key.Name] = Jsonc.Deserialize<DiseaseDef>(json);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,6 +429,7 @@ public sealed class CatalogLoader
|
||||
orientations,
|
||||
affinity,
|
||||
events,
|
||||
diseases,
|
||||
ru,
|
||||
en);
|
||||
}
|
||||
@@ -547,6 +552,7 @@ public sealed class CatalogLoader
|
||||
|
||||
PeopleDefValidator.Validate(catalog, log);
|
||||
EventDefValidator.Validate(catalog);
|
||||
DiseaseDefValidator.Validate(catalog);
|
||||
}
|
||||
|
||||
private static void WarnMissingLabels(DefCatalog catalog, IContentLog log)
|
||||
@@ -585,7 +591,8 @@ public sealed class CatalogLoader
|
||||
.Concat(Enumerate(catalog.Topics.Values))
|
||||
.Concat(Enumerate(catalog.Orientations.Values))
|
||||
.Concat(Enumerate(catalog.Affinity.Values))
|
||||
.Concat(Enumerate(catalog.Events.Values));
|
||||
.Concat(Enumerate(catalog.Events.Values))
|
||||
.Concat(Enumerate(catalog.Diseases.Values));
|
||||
|
||||
static IEnumerable<Def> Enumerate(IEnumerable<Def> defs) => defs.Where(def => !def.Abstract);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ public sealed class DefCatalog
|
||||
IReadOnlyDictionary<string, OrientationDef> orientations,
|
||||
IReadOnlyDictionary<string, AffinityRulesDef> affinity,
|
||||
IReadOnlyDictionary<string, EventDef> events,
|
||||
IReadOnlyDictionary<string, DiseaseDef> diseases,
|
||||
IReadOnlyDictionary<string, string> ru,
|
||||
IReadOnlyDictionary<string, string> en)
|
||||
{
|
||||
@@ -60,6 +61,7 @@ public sealed class DefCatalog
|
||||
Orientations = orientations;
|
||||
Affinity = affinity;
|
||||
Events = events;
|
||||
Diseases = diseases;
|
||||
_ru = ru;
|
||||
_en = en;
|
||||
AnyNeedDecays = needs.Values.Any(need => !need.Abstract && need.DecayPerHour > 0f);
|
||||
@@ -125,6 +127,8 @@ public sealed class DefCatalog
|
||||
|
||||
public IReadOnlyDictionary<string, EventDef> Events { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, DiseaseDef> Diseases { 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);
|
||||
|
||||
@@ -168,6 +172,7 @@ public sealed class DefCatalog
|
||||
DefKind.Orientation => Orientations.GetValueOrDefault(defName),
|
||||
DefKind.AffinityRules => Affinity.GetValueOrDefault(defName),
|
||||
DefKind.Event => Events.GetValueOrDefault(defName),
|
||||
DefKind.Disease => Diseases.GetValueOrDefault(defName),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
@@ -252,6 +257,7 @@ public sealed class DefCatalog
|
||||
OrientationDef => DefKind.Orientation,
|
||||
AffinityRulesDef => DefKind.AffinityRules,
|
||||
EventDef => DefKind.Event,
|
||||
DiseaseDef => DefKind.Disease,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(def)),
|
||||
};
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ public enum DefKind
|
||||
Orientation,
|
||||
AffinityRules,
|
||||
Event,
|
||||
Disease,
|
||||
}
|
||||
|
||||
/// <summary>Shared JSONC fields. Kind comes from the folder under <c>defs/</c>, not from the file.</summary>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
namespace HSchool.Content;
|
||||
|
||||
internal static class DiseaseDefValidator
|
||||
{
|
||||
private static readonly HashSet<string> Families = new(StringComparer.Ordinal)
|
||||
{
|
||||
DiseaseFamilies.Respiratory,
|
||||
DiseaseFamilies.Gastrointestinal,
|
||||
DiseaseFamilies.Ent,
|
||||
};
|
||||
|
||||
public static void Validate(DefCatalog catalog)
|
||||
{
|
||||
foreach (var def in catalog.Diseases.Values)
|
||||
{
|
||||
if (def.Abstract)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(def.Family))
|
||||
{
|
||||
throw new ContentLoadException($"DiseaseDef '{def.DefName}' family is required.");
|
||||
}
|
||||
|
||||
if (!Families.Contains(def.Family))
|
||||
{
|
||||
throw new ContentLoadException($"DiseaseDef '{def.DefName}' has unknown family '{def.Family}'.");
|
||||
}
|
||||
|
||||
if (def.Stages.Count == 0)
|
||||
{
|
||||
throw new ContentLoadException($"DiseaseDef '{def.DefName}' stages are required.");
|
||||
}
|
||||
|
||||
if (def.IncubationDays < 0f)
|
||||
{
|
||||
throw new ContentLoadException($"DiseaseDef '{def.DefName}' incubationDays cannot be negative.");
|
||||
}
|
||||
|
||||
if (def.ImmunityDays < 0f)
|
||||
{
|
||||
throw new ContentLoadException($"DiseaseDef '{def.DefName}' immunityDays cannot be negative.");
|
||||
}
|
||||
|
||||
if (def.BaseChancePerDay < 0f
|
||||
|| def.ColdChancePerDay < 0f
|
||||
|| def.RainChancePerDay < 0f
|
||||
|| def.SnowChancePerDay < 0f)
|
||||
{
|
||||
throw new ContentLoadException($"DiseaseDef '{def.DefName}' chance fields cannot be negative.");
|
||||
}
|
||||
|
||||
DiseaseStage? previous = null;
|
||||
foreach (var stage in def.Stages)
|
||||
{
|
||||
if (stage.MinSeverity < 0f || stage.MinSeverity > 1f)
|
||||
{
|
||||
throw new ContentLoadException(
|
||||
$"DiseaseDef '{def.DefName}' stage minSeverity must be 0–1.");
|
||||
}
|
||||
|
||||
if (previous is not null && stage.MinSeverity < previous.MinSeverity)
|
||||
{
|
||||
throw new ContentLoadException(
|
||||
$"DiseaseDef '{def.DefName}' stages must be ascending by minSeverity.");
|
||||
}
|
||||
|
||||
if (stage.LessonLearningFactor < 0f || stage.LessonLearningFactor > 1f)
|
||||
{
|
||||
throw new ContentLoadException(
|
||||
$"DiseaseDef '{def.DefName}' lessonLearningFactor must be 0–1.");
|
||||
}
|
||||
|
||||
if (stage.StayHomeChance < 0f || stage.StayHomeChance > 1f)
|
||||
{
|
||||
throw new ContentLoadException(
|
||||
$"DiseaseDef '{def.DefName}' stayHomeChance must be 0–1.");
|
||||
}
|
||||
|
||||
if (stage.WarmthDecayFactor < 0f)
|
||||
{
|
||||
throw new ContentLoadException(
|
||||
$"DiseaseDef '{def.DefName}' warmthDecayFactor cannot be negative.");
|
||||
}
|
||||
|
||||
previous = stage;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
namespace HSchool.Content;
|
||||
|
||||
/// <summary>Stable family ids for immunity grouping and realism labels.</summary>
|
||||
public static class DiseaseFamilies
|
||||
{
|
||||
public const string Respiratory = "respiratory";
|
||||
|
||||
public const string Gastrointestinal = "gastrointestinal";
|
||||
|
||||
public const string Ent = "ent";
|
||||
}
|
||||
|
||||
/// <summary>One severity band on a <see cref="DiseaseDef"/> curve.</summary>
|
||||
public sealed class DiseaseStage
|
||||
{
|
||||
/// <summary>Inclusive lower bound of severity for this stage.</summary>
|
||||
public float MinSeverity { get; init; }
|
||||
|
||||
public float SeverityPerDay { get; init; }
|
||||
|
||||
public float ProgressPerDay { get; init; }
|
||||
|
||||
/// <summary>Multiplies lesson skill gain while this stage is active (after incubation).</summary>
|
||||
public float LessonLearningFactor { get; init; } = 1f;
|
||||
|
||||
/// <summary>Per-day chance the person stays off campus while in this stage (after incubation).</summary>
|
||||
public float StayHomeChance { get; init; }
|
||||
|
||||
/// <summary>Multiplies Warmth decay. 1 = unchanged.</summary>
|
||||
public float WarmthDecayFactor { get; init; } = 1f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A named disease: stages, weather vectors, lesson/attendance effects, temporary immunity.
|
||||
/// Contagion between people is phase 79 — these defs may omit it.
|
||||
/// </summary>
|
||||
public sealed class DiseaseDef : Def
|
||||
{
|
||||
/// <summary><see cref="DiseaseFamilies"/> id. Required on concrete defs.</summary>
|
||||
public string Family { get; init; } = "";
|
||||
|
||||
/// <summary>Game days after onset before stage effects (lesson / stay-home) apply.</summary>
|
||||
public float IncubationDays { get; init; }
|
||||
|
||||
public IReadOnlyList<DiseaseStage> Stages { get; init; } = [];
|
||||
|
||||
/// <summary>Base daily onset chance before weather vectors and <see cref="BehaviorDef.DiseaseVectorScale"/>.</summary>
|
||||
public float BaseChancePerDay { get; init; }
|
||||
|
||||
/// <summary>Street °C at or below this adds <see cref="ColdChancePerDay"/>. Null — no cold vector.</summary>
|
||||
public float? ColdBelowC { get; init; }
|
||||
|
||||
public float ColdChancePerDay { get; init; }
|
||||
|
||||
public float RainChancePerDay { get; init; }
|
||||
|
||||
public float SnowChancePerDay { get; init; }
|
||||
|
||||
/// <summary>Days of immunity to this def after recovery.</summary>
|
||||
public float ImmunityDays { get; init; }
|
||||
}
|
||||
@@ -141,6 +141,9 @@ internal static class PackPaths
|
||||
case "events":
|
||||
kind = DefKind.Event;
|
||||
return true;
|
||||
case "diseases":
|
||||
kind = DefKind.Disease;
|
||||
return true;
|
||||
default:
|
||||
kind = default;
|
||||
return false;
|
||||
|
||||
@@ -751,6 +751,12 @@ internal static class PeopleDefValidator
|
||||
throw new ContentLoadException(
|
||||
$"BehaviorDef '{behavior.DefName}' lessonLateMinutes cannot be negative.");
|
||||
}
|
||||
|
||||
if (behavior.DiseaseVectorScale < 0f)
|
||||
{
|
||||
throw new ContentLoadException(
|
||||
$"BehaviorDef '{behavior.DefName}' diseaseVectorScale cannot be negative.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateTopic(TopicDef topic, DefCatalog catalog)
|
||||
|
||||
@@ -655,6 +655,11 @@ public sealed class BehaviorDef : Def
|
||||
/// </summary>
|
||||
public bool LessonMarkWhenLate { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Scales DiseaseDef weather/base onset chances. 0 disables natural onset; missing keeps 1.
|
||||
/// </summary>
|
||||
public float DiseaseVectorScale { get; init; } = 1f;
|
||||
|
||||
public static IReadOnlyList<float> DefaultLessonMarkThresholds { get; } =
|
||||
[
|
||||
0.85f,
|
||||
|
||||
Reference in New Issue
Block a user