add shel
This commit is contained in:
@@ -307,6 +307,7 @@ public sealed class CatalogLoader
|
||||
var bodyAttributes = new Dictionary<string, BodyAttributeDef>(StringComparer.Ordinal);
|
||||
var needs = new Dictionary<string, NeedDef>(StringComparer.Ordinal);
|
||||
var nameSets = new Dictionary<string, NameSetDef>(StringComparer.Ordinal);
|
||||
var subjects = new Dictionary<string, SubjectDef>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var (key, json) in resolved)
|
||||
{
|
||||
@@ -351,6 +352,9 @@ public sealed class CatalogLoader
|
||||
case DefKind.NameSet:
|
||||
nameSets[key.Name] = Jsonc.Deserialize<NameSetDef>(json);
|
||||
break;
|
||||
case DefKind.Subject:
|
||||
subjects[key.Name] = Jsonc.Deserialize<SubjectDef>(json);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,6 +373,7 @@ public sealed class CatalogLoader
|
||||
bodyAttributes,
|
||||
needs,
|
||||
nameSets,
|
||||
subjects,
|
||||
ru,
|
||||
en);
|
||||
}
|
||||
@@ -421,6 +426,33 @@ public sealed class CatalogLoader
|
||||
throw new ContentLoadException($"RoomDef '{room.DefName}' references unknown WorkDef '{work}'.");
|
||||
}
|
||||
}
|
||||
|
||||
if (room.Homeroom)
|
||||
{
|
||||
if (room.Slots.Count > 0)
|
||||
{
|
||||
throw new ContentLoadException($"RoomDef '{room.DefName}' is a homeroom and cannot declare named slots.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(room.SeatThing) || !catalog.Things.TryGetValue(room.SeatThing, out var seat) || seat.Abstract)
|
||||
{
|
||||
throw new ContentLoadException($"RoomDef '{room.DefName}' seatThing is missing or unknown.");
|
||||
}
|
||||
|
||||
if (seat.PupilSlots <= 0)
|
||||
{
|
||||
throw new ContentLoadException($"RoomDef '{room.DefName}' seatThing '{room.SeatThing}' must have pupilSlots.");
|
||||
}
|
||||
|
||||
if (room.DefaultSeats < 1 || room.DefaultSeats > byte.MaxValue)
|
||||
{
|
||||
throw new ContentLoadException($"RoomDef '{room.DefName}' defaultSeats must be 1–{byte.MaxValue}.");
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(room.SeatThing) || room.DefaultSeats != 0)
|
||||
{
|
||||
throw new ContentLoadException($"RoomDef '{room.DefName}' is not a homeroom and cannot set seatThing or defaultSeats.");
|
||||
}
|
||||
}
|
||||
|
||||
PeopleDefValidator.Validate(catalog);
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed class DefCatalog
|
||||
IReadOnlyDictionary<string, BodyAttributeDef> bodyAttributes,
|
||||
IReadOnlyDictionary<string, NeedDef> needs,
|
||||
IReadOnlyDictionary<string, NameSetDef> nameSets,
|
||||
IReadOnlyDictionary<string, SubjectDef> subjects,
|
||||
IReadOnlyDictionary<string, string> ru,
|
||||
IReadOnlyDictionary<string, string> en)
|
||||
{
|
||||
@@ -38,6 +39,7 @@ public sealed class DefCatalog
|
||||
BodyAttributes = bodyAttributes;
|
||||
Needs = needs;
|
||||
NameSets = nameSets;
|
||||
Subjects = subjects;
|
||||
_ru = ru;
|
||||
_en = en;
|
||||
AnyNeedDecays = needs.Values.Any(need => !need.Abstract && need.DecayPerHour > 0f);
|
||||
@@ -78,6 +80,8 @@ public sealed class DefCatalog
|
||||
|
||||
public IReadOnlyDictionary<string, NameSetDef> NameSets { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, SubjectDef> Subjects { get; }
|
||||
|
||||
private readonly IReadOnlyDictionary<string, string> _ru;
|
||||
private readonly IReadOnlyDictionary<string, string> _en;
|
||||
|
||||
@@ -98,6 +102,7 @@ public sealed class DefCatalog
|
||||
DefKind.BodyAttribute => BodyAttributes.GetValueOrDefault(defName),
|
||||
DefKind.Need => Needs.GetValueOrDefault(defName),
|
||||
DefKind.NameSet => NameSets.GetValueOrDefault(defName),
|
||||
DefKind.Subject => Subjects.GetValueOrDefault(defName),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
@@ -143,6 +148,13 @@ public sealed class DefCatalog
|
||||
/// </summary>
|
||||
public string Text(string locale, string key) => TryText(locale, key, out var text) ? text : key;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this locale actually carries the key. <see cref="Text"/> falls back to the key
|
||||
/// itself, which reads as a label in English and as debug output in Russian — so a test that
|
||||
/// wants to prove nothing is unlabelled has to ask this instead of comparing strings.
|
||||
/// </summary>
|
||||
public bool HasText(string locale, string key) => TryText(locale, key, out _);
|
||||
|
||||
private bool TryText(string locale, string key, out string text)
|
||||
{
|
||||
var table = locale.Equals("en", StringComparison.OrdinalIgnoreCase) ? _en : _ru;
|
||||
@@ -164,6 +176,7 @@ public sealed class DefCatalog
|
||||
BodyAttributeDef => DefKind.BodyAttribute,
|
||||
NeedDef => DefKind.Need,
|
||||
NameSetDef => DefKind.NameSet,
|
||||
SubjectDef => DefKind.Subject,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(def)),
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ public enum DefKind
|
||||
BodyAttribute,
|
||||
Need,
|
||||
NameSet,
|
||||
Subject,
|
||||
}
|
||||
|
||||
/// <summary>Shared JSONC fields. Kind comes from the folder under <c>defs/</c>, not from the file.</summary>
|
||||
@@ -64,8 +65,15 @@ public sealed class RoomDef : Def
|
||||
/// <summary>
|
||||
/// When true, a map room of this def with pupil slots becomes a roster class.
|
||||
/// Labs keep <see cref="ThingDef.PupilSlots"/> for lessons without forming a homeroom.
|
||||
/// Homerooms are a seat count of <see cref="SeatThing"/>, not a table of named slots.
|
||||
/// </summary>
|
||||
public bool Homeroom { get; init; }
|
||||
|
||||
/// <summary>Thing whose <see cref="ThingDef.PupilSlots"/> × map seats is class capacity.</summary>
|
||||
public string? SeatThing { get; init; }
|
||||
|
||||
/// <summary>Editor default when placing a new homeroom. Vanilla classrooms are 16.</summary>
|
||||
public int DefaultSeats { get; init; }
|
||||
}
|
||||
|
||||
public sealed class BuildingDef : Def;
|
||||
|
||||
@@ -55,6 +55,12 @@ public sealed class RoomNode
|
||||
/// <summary>Optional designation such as a classroom number. The def label still supplies the noun.</summary>
|
||||
public string? Label { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Homeroom seat count. Capacity is <c>SeatThing.PupilSlots × Seats</c>. Named
|
||||
/// <see cref="Slots"/> stay on rooms whose furnishing actually varies.
|
||||
/// </summary>
|
||||
public int Seats { get; init; }
|
||||
|
||||
public IReadOnlyList<SlotFill> Slots { get; init; } = [];
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ public static class MapValidator
|
||||
throw new MapValidationException($"Room '{room.Id}' is on floor '{room.Floor}', which belongs to another building.");
|
||||
}
|
||||
|
||||
ValidateSlotFills(room, catalog);
|
||||
ValidateRoomContents(room, catalog);
|
||||
rooms[room.Id] = room;
|
||||
}
|
||||
|
||||
@@ -121,13 +121,33 @@ public static class MapValidator
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateSlotFills(RoomNode room, DefCatalog catalog)
|
||||
private static void ValidateRoomContents(RoomNode room, DefCatalog catalog)
|
||||
{
|
||||
if (!catalog.Rooms.TryGetValue(room.Def, out var roomDef))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (roomDef.Homeroom)
|
||||
{
|
||||
if (room.Slots.Count > 0)
|
||||
{
|
||||
throw new MapValidationException($"Homeroom '{room.Id}' cannot fill named slots.");
|
||||
}
|
||||
|
||||
if (room.Seats < 1 || room.Seats > byte.MaxValue)
|
||||
{
|
||||
throw new MapValidationException($"Homeroom '{room.Id}' seats must be 1–{byte.MaxValue}.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (room.Seats != 0)
|
||||
{
|
||||
throw new MapValidationException($"Room '{room.Id}' is not a homeroom and cannot set seats.");
|
||||
}
|
||||
|
||||
var keys = roomDef.Slots.Select(slot => slot.Key).ToHashSet(StringComparer.Ordinal);
|
||||
foreach (var fill in room.Slots)
|
||||
{
|
||||
|
||||
@@ -121,23 +121,24 @@ public static class MapView
|
||||
string locale,
|
||||
RoomNode room)
|
||||
{
|
||||
var items = new List<MapViewItem>(room.Slots.Count);
|
||||
var pupilSlots = 0L;
|
||||
foreach (var fill in room.Slots)
|
||||
if (catalog.Rooms.TryGetValue(room.Def, out var def) && def.Homeroom && !string.IsNullOrWhiteSpace(def.SeatThing))
|
||||
{
|
||||
var count = SlotQuantity(fill.Count);
|
||||
items.Add(new MapViewItem(LabelOf(catalog, locale, DefKind.Thing, fill.Thing), count));
|
||||
if (catalog.Things.TryGetValue(fill.Thing, out var thing) && thing.PupilSlots > 0)
|
||||
{
|
||||
pupilSlots += (long)thing.PupilSlots * count;
|
||||
}
|
||||
var seats = RoomOccupancy.Quantity(room.Seats > 0 ? room.Seats : def.DefaultSeats);
|
||||
return (
|
||||
[new MapViewItem(LabelOf(catalog, locale, DefKind.Thing, def.SeatThing), seats)],
|
||||
RoomOccupancy.PupilSlots(catalog, room));
|
||||
}
|
||||
|
||||
return (items, (int)Math.Clamp(pupilSlots, 0, ushort.MaxValue));
|
||||
}
|
||||
var items = new List<MapViewItem>(room.Slots.Count);
|
||||
foreach (var fill in room.Slots)
|
||||
{
|
||||
items.Add(new MapViewItem(
|
||||
LabelOf(catalog, locale, DefKind.Thing, fill.Thing),
|
||||
RoomOccupancy.Quantity(fill.Count)));
|
||||
}
|
||||
|
||||
private static int SlotQuantity(int count) =>
|
||||
count < 1 ? 1 : Math.Min(count, byte.MaxValue);
|
||||
return (items, RoomOccupancy.PupilSlots(catalog, room));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A floor's <see cref="FloorNode.Label"/> is its designation, not a replacement name — the
|
||||
|
||||
@@ -101,6 +101,9 @@ internal static class PackPaths
|
||||
case "namesets":
|
||||
kind = DefKind.NameSet;
|
||||
return true;
|
||||
case "subjects":
|
||||
kind = DefKind.Subject;
|
||||
return true;
|
||||
default:
|
||||
kind = default;
|
||||
return false;
|
||||
|
||||
@@ -29,6 +29,11 @@ internal static class PeopleDefValidator
|
||||
ValidateNameSet(names);
|
||||
}
|
||||
|
||||
foreach (var subject in catalog.Subjects.Values)
|
||||
{
|
||||
ValidateSubject(subject, catalog);
|
||||
}
|
||||
|
||||
RequireBuildInputs(catalog);
|
||||
}
|
||||
|
||||
@@ -218,6 +223,43 @@ internal static class PeopleDefValidator
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateSubject(SubjectDef subject, DefCatalog catalog)
|
||||
{
|
||||
if (subject.HoursPerWeek < 0)
|
||||
{
|
||||
throw new ContentLoadException($"SubjectDef '{subject.DefName}' hoursPerWeek cannot be negative.");
|
||||
}
|
||||
|
||||
if (subject.Grades.Min < 1 || subject.Grades.Max > 11 || subject.Grades.Min > subject.Grades.Max)
|
||||
{
|
||||
throw new ContentLoadException($"SubjectDef '{subject.DefName}' grades must be 1–11 with min ≤ max.");
|
||||
}
|
||||
|
||||
if (subject.Skills.Count == 0)
|
||||
{
|
||||
throw new ContentLoadException($"SubjectDef '{subject.DefName}' needs at least one skill.");
|
||||
}
|
||||
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var share in subject.Skills)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(share.Skill) || !seen.Add(share.Skill))
|
||||
{
|
||||
throw new ContentLoadException($"SubjectDef '{subject.DefName}' has a missing or duplicate skill.");
|
||||
}
|
||||
|
||||
if (!catalog.Skills.TryGetValue(share.Skill, out var skill) || skill.Abstract)
|
||||
{
|
||||
throw new ContentLoadException($"SubjectDef '{subject.DefName}' references unknown SkillDef '{share.Skill}'.");
|
||||
}
|
||||
|
||||
if (share.Share < 0)
|
||||
{
|
||||
throw new ContentLoadException($"SubjectDef '{subject.DefName}' skill '{share.Skill}' share cannot be negative.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateNameSet(NameSetDef names)
|
||||
{
|
||||
if (!NameGrammar.IsKnownPatronymic(names.PatronymicRule))
|
||||
|
||||
@@ -122,6 +122,22 @@ public sealed class SkillDef : Def
|
||||
public IReadOnlyList<BodySkillLimit> BodyLimits { 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<SubjectSkillShare> Skills { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class TraitSkillModifier
|
||||
{
|
||||
public required string Skill { get; init; }
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace HSchool.Content;
|
||||
|
||||
/// <summary>
|
||||
/// How many pupils a map room can host. Homerooms are a seat count of one thing; other rooms
|
||||
/// still sum <c>ThingDef.PupilSlots × fill count</c> across named slots.
|
||||
/// </summary>
|
||||
public static class RoomOccupancy
|
||||
{
|
||||
public static int Quantity(int count) =>
|
||||
count < 1 ? 1 : Math.Min(count, byte.MaxValue);
|
||||
|
||||
public static int PupilSlots(DefCatalog catalog, RoomNode room)
|
||||
{
|
||||
if (catalog.Rooms.TryGetValue(room.Def, out var def) && def.Homeroom)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(def.SeatThing)
|
||||
|| !catalog.Things.TryGetValue(def.SeatThing, out var seat)
|
||||
|| seat.PupilSlots <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var seats = room.Seats > 0 ? Quantity(room.Seats) : Quantity(def.DefaultSeats);
|
||||
return (int)Math.Clamp((long)seat.PupilSlots * seats, 0, ushort.MaxValue);
|
||||
}
|
||||
|
||||
var pupilSlots = 0L;
|
||||
foreach (var fill in room.Slots)
|
||||
{
|
||||
if (catalog.Things.TryGetValue(fill.Thing, out var thing) && thing.PupilSlots > 0)
|
||||
{
|
||||
pupilSlots += (long)thing.PupilSlots * Quantity(fill.Count);
|
||||
}
|
||||
}
|
||||
|
||||
return (int)Math.Clamp(pupilSlots, 0, ushort.MaxValue);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user