184 lines
6.1 KiB
C#
184 lines
6.1 KiB
C#
namespace HSchool.Content;
|
||
|
||
/// <summary>Kind of a map-tree node, matching the snapshot wire values.</summary>
|
||
public enum MapNodeKind : byte
|
||
{
|
||
Territory = 0,
|
||
Building = 1,
|
||
Floor = 2,
|
||
Room = 3,
|
||
}
|
||
|
||
/// <summary>One labelled node of a school's map, ready to put on the wire or in a test.</summary>
|
||
public sealed class MapViewNode
|
||
{
|
||
public required MapNodeKind Kind { get; init; }
|
||
|
||
public required string Id { get; init; }
|
||
|
||
/// <summary>Empty for the yard.</summary>
|
||
public required string ParentId { get; init; }
|
||
|
||
public required string Name { get; init; }
|
||
|
||
public IReadOnlyList<MapViewItem> Items { get; init; } = [];
|
||
|
||
/// <summary>Sum of <c>ThingDef.PupilSlots × fill count</c> for this node. Zero off rooms.</summary>
|
||
public int PupilSlots { get; init; }
|
||
|
||
public IReadOnlyList<string> Positions { get; init; } = [];
|
||
}
|
||
|
||
/// <summary>One stacked thing in a room. The client formats <c>Парта ×16</c>; this is the data.</summary>
|
||
public sealed record MapViewItem(string Name, int Count);
|
||
|
||
/// <summary>
|
||
/// Builds the location tree the client draws. Labels come from the frozen catalog in the
|
||
/// requested locale; people and in-place activities are not part of this view.
|
||
/// </summary>
|
||
public static class MapView
|
||
{
|
||
public static IReadOnlyList<MapViewNode> Build(DefCatalog catalog, MapLayout map, string locale)
|
||
{
|
||
if (map.Territory is not { } territory)
|
||
{
|
||
return [];
|
||
}
|
||
|
||
var nodes = new List<MapViewNode>
|
||
{
|
||
Node(
|
||
MapNodeKind.Territory,
|
||
territory.Id,
|
||
parentId: string.Empty,
|
||
LabelOf(catalog, locale, DefKind.Territory, territory.Def),
|
||
items: [],
|
||
pupilSlots: 0,
|
||
PositionsOf(catalog, locale, DefKind.Territory, territory.Def)),
|
||
};
|
||
|
||
foreach (var building in map.Buildings)
|
||
{
|
||
nodes.Add(Node(
|
||
MapNodeKind.Building,
|
||
building.Id,
|
||
territory.Id,
|
||
LabelOf(catalog, locale, DefKind.Building, building.Def),
|
||
[],
|
||
pupilSlots: 0,
|
||
PositionsOf(catalog, locale, DefKind.Building, building.Def)));
|
||
}
|
||
|
||
foreach (var floor in map.Floors)
|
||
{
|
||
nodes.Add(Node(
|
||
MapNodeKind.Floor,
|
||
floor.Id,
|
||
floor.Building,
|
||
FloorName(catalog, locale, floor),
|
||
[],
|
||
pupilSlots: 0,
|
||
PositionsOf(catalog, locale, DefKind.Floor, floor.Def)));
|
||
}
|
||
|
||
foreach (var room in map.Rooms)
|
||
{
|
||
var (items, pupilSlots) = RoomContents(catalog, locale, room);
|
||
nodes.Add(Node(
|
||
MapNodeKind.Room,
|
||
room.Id,
|
||
room.Floor,
|
||
RoomName(catalog, locale, room),
|
||
items,
|
||
pupilSlots,
|
||
PositionsOf(catalog, locale, DefKind.Room, room.Def)));
|
||
}
|
||
|
||
return nodes;
|
||
}
|
||
|
||
private static MapViewNode Node(
|
||
MapNodeKind kind,
|
||
string id,
|
||
string parentId,
|
||
string name,
|
||
IReadOnlyList<MapViewItem> items,
|
||
int pupilSlots,
|
||
IReadOnlyList<string> positions) =>
|
||
new()
|
||
{
|
||
Kind = kind,
|
||
Id = id,
|
||
ParentId = parentId,
|
||
Name = name,
|
||
Items = items,
|
||
PupilSlots = pupilSlots,
|
||
Positions = positions,
|
||
};
|
||
|
||
private static (IReadOnlyList<MapViewItem> Items, int PupilSlots) RoomContents(
|
||
DefCatalog catalog,
|
||
string locale,
|
||
RoomNode room)
|
||
{
|
||
if (catalog.Rooms.TryGetValue(room.Def, out var def) && def.Homeroom && !string.IsNullOrWhiteSpace(def.SeatThing))
|
||
{
|
||
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));
|
||
}
|
||
|
||
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)));
|
||
}
|
||
|
||
return (items, RoomOccupancy.PupilSlots(catalog, room));
|
||
}
|
||
|
||
/// <summary>
|
||
/// A floor's <see cref="FloorNode.Label"/> is its designation, not a replacement name — the
|
||
/// vanilla map says "1". On its own that is a stray digit in the client's tree, so it follows
|
||
/// the localized def label: "Этаж 1", "Floor 1". Mirrored by <c>floorName</c> in the editor.
|
||
/// </summary>
|
||
private static string FloorName(DefCatalog catalog, string locale, FloorNode floor)
|
||
{
|
||
var defLabel = LabelOf(catalog, locale, DefKind.Floor, floor.Def);
|
||
return string.IsNullOrWhiteSpace(floor.Label) ? defLabel : $"{defLabel} {floor.Label}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// Same rule as floors: <see cref="RoomNode.Label"/> is "101", not a replacement for "Classroom".
|
||
/// Do not treat "label equals defName" as "untranslated" — English classrooms are named Classroom.
|
||
/// </summary>
|
||
private static string RoomName(DefCatalog catalog, string locale, RoomNode room)
|
||
{
|
||
var defLabel = LabelOf(catalog, locale, DefKind.Room, room.Def);
|
||
return string.IsNullOrWhiteSpace(room.Label) ? defLabel : $"{defLabel} {room.Label}";
|
||
}
|
||
|
||
private static string LabelOf(DefCatalog catalog, string locale, DefKind kind, string defName) =>
|
||
catalog.TryGet(kind, defName, out var def) ? catalog.Label(locale, def) : defName;
|
||
|
||
private static IReadOnlyList<string> PositionsOf(DefCatalog catalog, string locale, DefKind kind, string defName)
|
||
{
|
||
var names = catalog.PositionsFor(kind, defName);
|
||
if (names.Count == 0)
|
||
{
|
||
return [];
|
||
}
|
||
|
||
var labels = new string[names.Count];
|
||
for (var i = 0; i < names.Count; i++)
|
||
{
|
||
labels[i] = LabelOf(catalog, locale, DefKind.Position, names[i]);
|
||
}
|
||
|
||
return labels;
|
||
}
|
||
}
|