Add EventDef notices, morning and lesson bells, and info toasts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-20 20:46:33 +03:00
co-authored by Cursor
parent ea3f5c7bf1
commit a554738084
38 changed files with 952 additions and 22 deletions
+8 -1
View File
@@ -318,6 +318,7 @@ public sealed class CatalogLoader
var topics = new Dictionary<string, TopicDef>(StringComparer.Ordinal);
var orientations = new Dictionary<string, OrientationDef>(StringComparer.Ordinal);
var affinity = new Dictionary<string, AffinityRulesDef>(StringComparer.Ordinal);
var events = new Dictionary<string, EventDef>(StringComparer.Ordinal);
foreach (var (key, json) in resolved)
{
@@ -392,6 +393,9 @@ public sealed class CatalogLoader
case DefKind.AffinityRules:
affinity[key.Name] = Jsonc.Deserialize<AffinityRulesDef>(json);
break;
case DefKind.Event:
events[key.Name] = Jsonc.Deserialize<EventDef>(json);
break;
}
}
@@ -420,6 +424,7 @@ public sealed class CatalogLoader
topics,
orientations,
affinity,
events,
ru,
en);
}
@@ -534,6 +539,7 @@ public sealed class CatalogLoader
}
PeopleDefValidator.Validate(catalog, log);
EventDefValidator.Validate(catalog);
}
private static void WarnMissingLabels(DefCatalog catalog, IContentLog log)
@@ -571,7 +577,8 @@ public sealed class CatalogLoader
.Concat(Enumerate(catalog.Colors.Values))
.Concat(Enumerate(catalog.Topics.Values))
.Concat(Enumerate(catalog.Orientations.Values))
.Concat(Enumerate(catalog.Affinity.Values));
.Concat(Enumerate(catalog.Affinity.Values))
.Concat(Enumerate(catalog.Events.Values));
static IEnumerable<Def> Enumerate(IEnumerable<Def> defs) => defs.Where(def => !def.Abstract);
}
+6
View File
@@ -31,6 +31,7 @@ public sealed class DefCatalog
IReadOnlyDictionary<string, TopicDef> topics,
IReadOnlyDictionary<string, OrientationDef> orientations,
IReadOnlyDictionary<string, AffinityRulesDef> affinity,
IReadOnlyDictionary<string, EventDef> events,
IReadOnlyDictionary<string, string> ru,
IReadOnlyDictionary<string, string> en)
{
@@ -58,6 +59,7 @@ public sealed class DefCatalog
Topics = topics;
Orientations = orientations;
Affinity = affinity;
Events = events;
_ru = ru;
_en = en;
AnyNeedDecays = needs.Values.Any(need => !need.Abstract && need.DecayPerHour > 0f);
@@ -121,6 +123,8 @@ public sealed class DefCatalog
public IReadOnlyDictionary<string, AffinityRulesDef> Affinity { get; }
public IReadOnlyDictionary<string, EventDef> Events { 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);
@@ -163,6 +167,7 @@ public sealed class DefCatalog
DefKind.Topic => Topics.GetValueOrDefault(defName),
DefKind.Orientation => Orientations.GetValueOrDefault(defName),
DefKind.AffinityRules => Affinity.GetValueOrDefault(defName),
DefKind.Event => Events.GetValueOrDefault(defName),
_ => null,
};
@@ -246,6 +251,7 @@ public sealed class DefCatalog
TopicDef => DefKind.Topic,
OrientationDef => DefKind.Orientation,
AffinityRulesDef => DefKind.AffinityRules,
EventDef => DefKind.Event,
_ => throw new ArgumentOutOfRangeException(nameof(def)),
};
+1
View File
@@ -25,6 +25,7 @@ public enum DefKind
Topic,
Orientation,
AffinityRules,
Event,
}
/// <summary>Shared JSONC fields. Kind comes from the folder under <c>defs/</c>, not from the file.</summary>
+50
View File
@@ -0,0 +1,50 @@
namespace HSchool.Content;
internal static class EventDefValidator
{
private static readonly HashSet<string> Severities = new(StringComparer.Ordinal)
{
EventSeverities.Info,
EventSeverities.Warning,
EventSeverities.Error,
};
private static readonly HashSet<string> Triggers = new(StringComparer.Ordinal)
{
EventTriggers.DayStart,
EventTriggers.LessonStart,
EventTriggers.GenerationFailed,
};
private static readonly HashSet<string> Actions = new(StringComparer.Ordinal)
{
EventActions.None,
EventActions.GenerateImage,
};
public static void Validate(DefCatalog catalog)
{
foreach (var def in catalog.Events.Values)
{
if (!Severities.Contains(def.Severity))
{
throw new ContentLoadException($"EventDef '{def.DefName}' has unknown severity '{def.Severity}'.");
}
if (!Triggers.Contains(def.Trigger))
{
throw new ContentLoadException($"EventDef '{def.DefName}' has unknown trigger '{def.Trigger}'.");
}
if (!Actions.Contains(def.Action))
{
throw new ContentLoadException($"EventDef '{def.DefName}' has unknown action '{def.Action}'.");
}
if (def.TtlMs < 0)
{
throw new ContentLoadException($"EventDef '{def.DefName}' ttlMs cannot be negative.");
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
namespace HSchool.Content;
public static class EventSeverities
{
public const string Info = "info";
public const string Warning = "warning";
public const string Error = "error";
}
public static class EventTriggers
{
public const string DayStart = "dayStart";
public const string LessonStart = "lessonStart";
public const string GenerationFailed = "generationFailed";
}
public static class EventActions
{
public const string None = "none";
public const string GenerateImage = "generateImage";
}
/// <summary>
/// A fact the world can raise. Systems emit a trigger; the worker matches this def and builds a notice.
/// </summary>
public sealed class EventDef : Def
{
public string Severity { get; init; } = EventSeverities.Info;
public bool Pause { get; init; }
/// <summary>Client toast lifetime in milliseconds. Zero means it stays until dismiss.</summary>
public int TtlMs { get; init; }
public string Trigger { get; init; } = "";
public string Action { get; init; } = EventActions.None;
}
+3
View File
@@ -138,6 +138,9 @@ internal static class PackPaths
case "affinity":
kind = DefKind.AffinityRules;
return true;
case "events":
kind = DefKind.Event;
return true;
default:
kind = default;
return false;