diff --git a/src/HSchool.Client/src/net/api.ts b/src/HSchool.Client/src/net/api.ts index c090540..fd193ec 100644 --- a/src/HSchool.Client/src/net/api.ts +++ b/src/HSchool.Client/src/net/api.ts @@ -94,6 +94,7 @@ export interface MapLayout { def: string; building: string; floor: string; + label?: string; slots?: { key: string; thing: string }[]; }[]; links: { a: string; b: string }[]; diff --git a/src/HSchool.Client/src/ui/mapEditor.ts b/src/HSchool.Client/src/ui/mapEditor.ts index c749f4d..18b7c61 100644 --- a/src/HSchool.Client/src/ui/mapEditor.ts +++ b/src/HSchool.Client/src/ui/mapEditor.ts @@ -65,7 +65,7 @@ export function mapEditor(options: MapEditorOptions): { for (const floor of map.floors.filter((candidate) => candidate.building === building.id)) { appendNode(tree, floor.id, floorName(catalog, floor), 2); for (const room of map.rooms.filter((candidate) => candidate.floor === floor.id)) { - appendNode(tree, room.id, defLabel(catalog.rooms, room.def) ?? room.id, 3); + appendNode(tree, room.id, roomName(catalog, room), 3); } } } @@ -325,6 +325,17 @@ function floorName(catalog: CatalogResponse, floor: MapLayout['floors'][number]) return label.length > 0 ? `${def} ${label}` : def; } +function roomName(catalog: CatalogResponse, room: MapLayout['rooms'][number]): string { + const def = defLabel(catalog.rooms, room.def); + const label = room.label ?? ''; + + if (def === undefined) { + return label.length > 0 ? label : room.id; + } + + return label.length > 0 ? `${def} ${label}` : def; +} + function defLabel(defs: readonly { defName: string; label: string }[], defName: string): string | undefined { return defs.find((def) => def.defName === defName)?.label; } @@ -346,7 +357,7 @@ function labelOf(catalog: CatalogResponse, map: MapLayout, id: string): string { const room = map.rooms.find((node) => node.id === id); if (room !== undefined) { - return defLabel(catalog.rooms, room.def) ?? id; + return roomName(catalog, room); } return id; diff --git a/src/HSchool.Content/MapLayout.cs b/src/HSchool.Content/MapLayout.cs index 62ec931..dd18dee 100644 --- a/src/HSchool.Content/MapLayout.cs +++ b/src/HSchool.Content/MapLayout.cs @@ -52,6 +52,9 @@ public sealed class RoomNode public required string Floor { get; init; } + /// Optional designation such as a classroom number. The def label still supplies the noun. + public string? Label { get; init; } + public IReadOnlyList Slots { get; init; } = []; } diff --git a/src/HSchool.Content/MapView.cs b/src/HSchool.Content/MapView.cs index 6700e93..d7fefc6 100644 --- a/src/HSchool.Content/MapView.cs +++ b/src/HSchool.Content/MapView.cs @@ -84,7 +84,7 @@ public static class MapView MapNodeKind.Room, room.Id, room.Floor, - LabelOf(catalog, locale, DefKind.Room, room.Def), + RoomName(catalog, locale, room), items, PositionsOf(catalog, locale, DefKind.Room, room.Def))); } @@ -117,12 +117,17 @@ public static class MapView private static string FloorName(DefCatalog catalog, string locale, FloorNode floor) { var defLabel = LabelOf(catalog, locale, DefKind.Floor, floor.Def); - if (string.IsNullOrWhiteSpace(floor.Label)) - { - return defLabel; - } + return string.IsNullOrWhiteSpace(floor.Label) ? defLabel : $"{defLabel} {floor.Label}"; + } - return defLabel == floor.Def ? floor.Label : $"{defLabel} {floor.Label}"; + /// + /// Same rule as floors: is "1A", not a replacement for "Classroom". + /// Do not treat "label equals defName" as "untranslated" — English classrooms are named Classroom. + /// + 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) => diff --git a/src/HSchool.Server/mods/core/defs/buildings/gym-building.jsonc b/src/HSchool.Server/mods/core/defs/buildings/gym-building.jsonc new file mode 100644 index 0000000..92a56a2 --- /dev/null +++ b/src/HSchool.Server/mods/core/defs/buildings/gym-building.jsonc @@ -0,0 +1 @@ +{ "defName": "GymBuilding" } diff --git a/src/HSchool.Server/mods/core/defs/positions/principal.jsonc b/src/HSchool.Server/mods/core/defs/positions/principal.jsonc deleted file mode 100644 index 7b907e9..0000000 --- a/src/HSchool.Server/mods/core/defs/positions/principal.jsonc +++ /dev/null @@ -1 +0,0 @@ -{ "defName": "Principal" } diff --git a/src/HSchool.Server/mods/core/defs/positions/staff.jsonc b/src/HSchool.Server/mods/core/defs/positions/staff.jsonc new file mode 100644 index 0000000..662031f --- /dev/null +++ b/src/HSchool.Server/mods/core/defs/positions/staff.jsonc @@ -0,0 +1,9 @@ +[ + { "defName": "Principal" }, + { "defName": "Secretary" }, + { "defName": "Teacher" }, + { "defName": "Librarian" }, + { "defName": "Nurse" }, + { "defName": "PETeacher" }, + { "defName": "CafeteriaCook" }, +] diff --git a/src/HSchool.Server/mods/core/defs/rooms/academic.jsonc b/src/HSchool.Server/mods/core/defs/rooms/academic.jsonc new file mode 100644 index 0000000..25cc2f8 --- /dev/null +++ b/src/HSchool.Server/mods/core/defs/rooms/academic.jsonc @@ -0,0 +1,30 @@ +[ + { + "defName": "Classroom", + "slots": [ + { "key": "board", "thing": "Blackboard" }, + { "key": "teacherDesk", "thing": "Desk" }, + { "key": "studentDesks", "thing": "StudentDesk", "count": 16 }, + ], + "positions": ["Teacher"], + "works": ["TeachLesson"], + }, + { + "defName": "Library", + "slots": [ + { "key": "desk", "thing": "Desk" }, + { "key": "shelves", "thing": "Bookshelf", "count": 4 }, + ], + "positions": ["Librarian"], + "works": ["LibraryWork"], + }, + { + "defName": "ComputerLab", + "slots": [ + { "key": "teacherDesk", "thing": "Desk" }, + { "key": "computers", "thing": "Computer", "count": 12 }, + ], + "positions": ["Teacher"], + "works": ["TeachLesson"], + }, +] diff --git a/src/HSchool.Server/mods/core/defs/rooms/circulation.jsonc b/src/HSchool.Server/mods/core/defs/rooms/circulation.jsonc new file mode 100644 index 0000000..2ba38c2 --- /dev/null +++ b/src/HSchool.Server/mods/core/defs/rooms/circulation.jsonc @@ -0,0 +1,5 @@ +[ + // Walkable rooms that exist to connect the graph: lobby, stairs. Empty on purpose. + { "defName": "EntranceHall" }, + { "defName": "Stairwell" }, +] diff --git a/src/HSchool.Server/mods/core/defs/rooms/gym-hall.jsonc b/src/HSchool.Server/mods/core/defs/rooms/gym-hall.jsonc new file mode 100644 index 0000000..08d51fe --- /dev/null +++ b/src/HSchool.Server/mods/core/defs/rooms/gym-hall.jsonc @@ -0,0 +1,8 @@ +{ + "defName": "GymHall", + "slots": [ + { "key": "benches", "thing": "Bench", "count": 4 }, + ], + "positions": ["PETeacher"], + "works": ["PELesson"], +} diff --git a/src/HSchool.Server/mods/core/defs/rooms/service.jsonc b/src/HSchool.Server/mods/core/defs/rooms/service.jsonc new file mode 100644 index 0000000..55e8feb --- /dev/null +++ b/src/HSchool.Server/mods/core/defs/rooms/service.jsonc @@ -0,0 +1,45 @@ +[ + { + "defName": "SecretaryOffice", + "slots": [ + { "key": "desk", "thing": "Desk" }, + { "key": "chair", "thing": "Chair" }, + ], + "positions": ["Secretary"], + "works": ["OfficeWork"], + }, + { + "defName": "TeachersRoom", + "slots": [ + { "key": "table", "thing": "DiningTable" }, + { "key": "chairs", "thing": "Chair", "count": 6 }, + ], + }, + { + "defName": "Cafeteria", + "slots": [ + { "key": "counter", "thing": "DiningTable" }, + { "key": "seats", "thing": "Chair", "count": 8 }, + ], + "positions": ["CafeteriaCook"], + "works": ["ServeLunch"], + }, + { + "defName": "Restroom", + }, + { + "defName": "MedicalOffice", + "slots": [ + { "key": "couch", "thing": "MedicalCouch" }, + { "key": "desk", "thing": "Desk" }, + ], + "positions": ["Nurse"], + "works": ["MedicalDuty"], + }, + { + "defName": "ChangingRoom", + "slots": [ + { "key": "lockers", "thing": "Locker", "count": 12 }, + ], + }, +] diff --git a/src/HSchool.Server/mods/core/defs/things/furniture.jsonc b/src/HSchool.Server/mods/core/defs/things/furniture.jsonc new file mode 100644 index 0000000..5876694 --- /dev/null +++ b/src/HSchool.Server/mods/core/defs/things/furniture.jsonc @@ -0,0 +1,10 @@ +[ + { "defName": "Blackboard" }, + { "defName": "StudentDesk", "parent": "Desk", "actions": ["Sit"] }, + { "defName": "Bookshelf" }, + { "defName": "DiningTable" }, + { "defName": "Bench", "actions": ["Sit"] }, + { "defName": "Computer" }, + { "defName": "MedicalCouch" }, + { "defName": "Locker" }, +] diff --git a/src/HSchool.Server/mods/core/defs/works/works.jsonc b/src/HSchool.Server/mods/core/defs/works/works.jsonc index a89785c..0edc6ec 100644 --- a/src/HSchool.Server/mods/core/defs/works/works.jsonc +++ b/src/HSchool.Server/mods/core/defs/works/works.jsonc @@ -2,4 +2,9 @@ { "defName": "PrincipalOfficeWork" }, { "defName": "TeachLesson" }, { "defName": "WalkSchool" }, + { "defName": "OfficeWork" }, + { "defName": "LibraryWork" }, + { "defName": "MedicalDuty" }, + { "defName": "PELesson" }, + { "defName": "ServeLunch" }, ] diff --git a/src/HSchool.Server/mods/core/localizations/en.jsonc b/src/HSchool.Server/mods/core/localizations/en.jsonc index 66af6e6..f0e861e 100644 --- a/src/HSchool.Server/mods/core/localizations/en.jsonc +++ b/src/HSchool.Server/mods/core/localizations/en.jsonc @@ -3,13 +3,45 @@ "Chair": "Chair", "DirectorsChair": "Principal's chair", "Desk": "Desk", + "StudentDesk": "Student desk", + "Blackboard": "Blackboard", + "Bookshelf": "Bookshelf", + "DiningTable": "Table", + "Bench": "Bench", + "Computer": "Computer", + "MedicalCouch": "Exam couch", + "Locker": "Locker", "Principal": "Principal", + "Secretary": "Secretary", + "Teacher": "Teacher", + "Librarian": "Librarian", + "Nurse": "Nurse", + "PETeacher": "PE teacher", + "CafeteriaCook": "Cook", "PrincipalOfficeWork": "Principal's work", "TeachLesson": "Teach a lesson", "WalkSchool": "Walk the school", + "OfficeWork": "Office work", + "LibraryWork": "Library work", + "MedicalDuty": "Nurse's duty", + "PELesson": "PE lesson", + "ServeLunch": "Serve lunch", "SchoolYard": "Yard", "MainBuilding": "Main building", + "GymBuilding": "Gym building", "StandardFloor": "Floor", "Corridor": "Corridor", + "EntranceHall": "Lobby", + "Stairwell": "Stairs", "PrincipalsOffice": "Principal's office", + "SecretaryOffice": "Reception", + "TeachersRoom": "Staff room", + "Classroom": "Classroom", + "Cafeteria": "Cafeteria", + "Restroom": "Restroom", + "MedicalOffice": "Nurse's office", + "Library": "Library", + "ComputerLab": "Computer lab", + "GymHall": "Gym", + "ChangingRoom": "Changing room", } diff --git a/src/HSchool.Server/mods/core/localizations/ru.jsonc b/src/HSchool.Server/mods/core/localizations/ru.jsonc index 1d009f4..da57ead 100644 --- a/src/HSchool.Server/mods/core/localizations/ru.jsonc +++ b/src/HSchool.Server/mods/core/localizations/ru.jsonc @@ -3,13 +3,45 @@ "Chair": "Стул", "DirectorsChair": "Кресло директора", "Desk": "Стол", + "StudentDesk": "Парта", + "Blackboard": "Доска", + "Bookshelf": "Стеллаж", + "DiningTable": "Обеденный стол", + "Bench": "Скамейка", + "Computer": "Компьютер", + "MedicalCouch": "Кушетка", + "Locker": "Шкафчик", "Principal": "Директор", + "Secretary": "Секретарь", + "Teacher": "Учитель", + "Librarian": "Библиотекарь", + "Nurse": "Медсестра", + "PETeacher": "Учитель физкультуры", + "CafeteriaCook": "Повар", "PrincipalOfficeWork": "Работа директора", "TeachLesson": "Урок", "WalkSchool": "Обход школы", + "OfficeWork": "Канцелярия", + "LibraryWork": "Работа в библиотеке", + "MedicalDuty": "Медпункт", + "PELesson": "Урок физкультуры", + "ServeLunch": "Раздача обеда", "SchoolYard": "Двор", "MainBuilding": "Главный корпус", + "GymBuilding": "Спортзал", "StandardFloor": "Этаж", "Corridor": "Коридор", + "EntranceHall": "Вестибюль", + "Stairwell": "Лестница", "PrincipalsOffice": "Кабинет директора", + "SecretaryOffice": "Приёмная", + "TeachersRoom": "Учительская", + "Classroom": "Класс", + "Cafeteria": "Столовая", + "Restroom": "Туалет", + "MedicalOffice": "Медкабинет", + "Library": "Библиотека", + "ComputerLab": "Компьютерный класс", + "GymHall": "Спортивный зал", + "ChangingRoom": "Раздевалка", } diff --git a/src/HSchool.Server/mods/core/maps/default.jsonc b/src/HSchool.Server/mods/core/maps/default.jsonc index fc862f2..15e18ff 100644 --- a/src/HSchool.Server/mods/core/maps/default.jsonc +++ b/src/HSchool.Server/mods/core/maps/default.jsonc @@ -1,19 +1,20 @@ { - // Walkable yard is the tree root and a graph node. Rooms reach it through the porch/corridor. + // A small two-storey school: main building on the yard, gym across the yard. + // Walkable graph is yard + rooms; floors are grouping only. Stairs link the two corridors. "territory": { "id": "yard", "def": "SchoolYard" }, "buildings": [ { "id": "main", "def": "MainBuilding" }, + { "id": "gym", "def": "GymBuilding" }, ], "floors": [ { "id": "floor-1", "def": "StandardFloor", "building": "main", "label": "1" }, + { "id": "floor-2", "def": "StandardFloor", "building": "main", "label": "2" }, + { "id": "gym-floor", "def": "StandardFloor", "building": "gym" }, ], "rooms": [ - { - "id": "corridor-1", - "def": "Corridor", - "building": "main", - "floor": "floor-1", - }, + { "id": "porch", "def": "EntranceHall", "building": "main", "floor": "floor-1" }, + { "id": "corridor-1", "def": "Corridor", "building": "main", "floor": "floor-1", "label": "1" }, + { "id": "stairs-1", "def": "Stairwell", "building": "main", "floor": "floor-1", "label": "1" }, { "id": "principals-office", "def": "PrincipalsOffice", @@ -25,9 +26,157 @@ { "key": "guestChair", "thing": "Chair" }, ], }, + { + "id": "secretary-office", + "def": "SecretaryOffice", + "building": "main", + "floor": "floor-1", + "slots": [ + { "key": "desk", "thing": "Desk" }, + { "key": "chair", "thing": "Chair" }, + ], + }, + { + "id": "teachers-room", + "def": "TeachersRoom", + "building": "main", + "floor": "floor-1", + "slots": [ + { "key": "table", "thing": "DiningTable" }, + { "key": "chairs", "thing": "Chair" }, + ], + }, + { + "id": "classroom-1a", + "def": "Classroom", + "building": "main", + "floor": "floor-1", + "label": "1A", + "slots": [ + { "key": "board", "thing": "Blackboard" }, + { "key": "teacherDesk", "thing": "Desk" }, + { "key": "studentDesks", "thing": "StudentDesk" }, + ], + }, + { + "id": "classroom-1b", + "def": "Classroom", + "building": "main", + "floor": "floor-1", + "label": "1B", + "slots": [ + { "key": "board", "thing": "Blackboard" }, + { "key": "teacherDesk", "thing": "Desk" }, + { "key": "studentDesks", "thing": "StudentDesk" }, + ], + }, + { + "id": "cafeteria", + "def": "Cafeteria", + "building": "main", + "floor": "floor-1", + "slots": [ + { "key": "counter", "thing": "DiningTable" }, + { "key": "seats", "thing": "Chair" }, + ], + }, + { "id": "restroom-1", "def": "Restroom", "building": "main", "floor": "floor-1", "label": "1" }, + { + "id": "medical-office", + "def": "MedicalOffice", + "building": "main", + "floor": "floor-1", + "slots": [ + { "key": "couch", "thing": "MedicalCouch" }, + { "key": "desk", "thing": "Desk" }, + ], + }, + { "id": "corridor-2", "def": "Corridor", "building": "main", "floor": "floor-2", "label": "2" }, + { "id": "stairs-2", "def": "Stairwell", "building": "main", "floor": "floor-2", "label": "2" }, + { + "id": "classroom-2a", + "def": "Classroom", + "building": "main", + "floor": "floor-2", + "label": "2A", + "slots": [ + { "key": "board", "thing": "Blackboard" }, + { "key": "teacherDesk", "thing": "Desk" }, + { "key": "studentDesks", "thing": "StudentDesk" }, + ], + }, + { + "id": "classroom-2b", + "def": "Classroom", + "building": "main", + "floor": "floor-2", + "label": "2B", + "slots": [ + { "key": "board", "thing": "Blackboard" }, + { "key": "teacherDesk", "thing": "Desk" }, + { "key": "studentDesks", "thing": "StudentDesk" }, + ], + }, + { + "id": "library", + "def": "Library", + "building": "main", + "floor": "floor-2", + "slots": [ + { "key": "desk", "thing": "Desk" }, + { "key": "shelves", "thing": "Bookshelf" }, + ], + }, + { + "id": "computer-lab", + "def": "ComputerLab", + "building": "main", + "floor": "floor-2", + "slots": [ + { "key": "teacherDesk", "thing": "Desk" }, + { "key": "computers", "thing": "Computer" }, + ], + }, + { "id": "restroom-2", "def": "Restroom", "building": "main", "floor": "floor-2", "label": "2" }, + { + "id": "gym-hall", + "def": "GymHall", + "building": "gym", + "floor": "gym-floor", + "slots": [ + { "key": "benches", "thing": "Bench" }, + ], + }, + { + "id": "changing-room", + "def": "ChangingRoom", + "building": "gym", + "floor": "gym-floor", + "slots": [ + { "key": "lockers", "thing": "Locker" }, + ], + }, ], "links": [ - { "a": "yard", "b": "corridor-1" }, + { "a": "yard", "b": "porch" }, + { "a": "porch", "b": "corridor-1" }, + { "a": "corridor-1", "b": "stairs-1" }, { "a": "corridor-1", "b": "principals-office" }, + { "a": "corridor-1", "b": "secretary-office" }, + { "a": "corridor-1", "b": "teachers-room" }, + { "a": "corridor-1", "b": "classroom-1a" }, + { "a": "corridor-1", "b": "classroom-1b" }, + { "a": "corridor-1", "b": "cafeteria" }, + { "a": "corridor-1", "b": "restroom-1" }, + { "a": "corridor-1", "b": "medical-office" }, + { "a": "stairs-1", "b": "stairs-2" }, + { "a": "stairs-2", "b": "corridor-2" }, + { "a": "corridor-2", "b": "classroom-2a" }, + { "a": "corridor-2", "b": "classroom-2b" }, + { "a": "corridor-2", "b": "library" }, + { "a": "corridor-2", "b": "computer-lab" }, + { "a": "corridor-2", "b": "restroom-2" }, + { "a": "yard", "b": "gym-hall" }, + { "a": "gym-hall", "b": "changing-room" }, ], } diff --git a/tests/HSchool.Content.Tests/MapViewTests.cs b/tests/HSchool.Content.Tests/MapViewTests.cs index e8f86ef..e0aef1f 100644 --- a/tests/HSchool.Content.Tests/MapViewTests.cs +++ b/tests/HSchool.Content.Tests/MapViewTests.cs @@ -14,10 +14,12 @@ public class MapViewTests var ru = MapView.Build(catalog, map, "ru"); var en = MapView.Build(catalog, map, "en"); - Assert.Equal(["yard", "main", "floor-1", "corridor-1", "principals-office"], ru.Select(node => node.Id)); + Assert.Equal("yard", ru[0].Id); Assert.Equal(string.Empty, ru[0].ParentId); Assert.Equal("yard", ru.Single(node => node.Id == "main").ParentId); + Assert.Equal("yard", ru.Single(node => node.Id == "gym").ParentId); Assert.Equal("floor-1", ru.Single(node => node.Id == "principals-office").ParentId); + Assert.Equal("gym-floor", ru.Single(node => node.Id == "gym-hall").ParentId); var officeRu = ru.Single(node => node.Id == "principals-office"); Assert.Equal("Кабинет директора", officeRu.Name); @@ -32,6 +34,13 @@ public class MapViewTests var corridor = ru.Single(node => node.Id == "corridor-1"); Assert.Empty(corridor.Items); Assert.Empty(corridor.Positions); + Assert.Equal("Коридор 1", corridor.Name); + + var classroom = ru.Single(node => node.Id == "classroom-1a"); + Assert.Equal("Класс 1A", classroom.Name); + Assert.Equal(["Доска", "Стол", "Парта"], classroom.Items); + Assert.Equal(["Учитель"], classroom.Positions); + Assert.Equal("Classroom 1A", en.Single(node => node.Id == "classroom-1a").Name); } [Fact] diff --git a/tests/HSchool.Content.Tests/VanillaCoreTests.cs b/tests/HSchool.Content.Tests/VanillaCoreTests.cs index 0667314..1a2ed05 100644 --- a/tests/HSchool.Content.Tests/VanillaCoreTests.cs +++ b/tests/HSchool.Content.Tests/VanillaCoreTests.cs @@ -21,5 +21,12 @@ public class VanillaCoreTests Assert.Equal("Кабинет директора", catalog.Label("ru", catalog.Rooms["PrincipalsOffice"])); Assert.Equal("Principal's office", catalog.Label("en", catalog.Rooms["PrincipalsOffice"])); Assert.Equal(["Sit"], catalog.Things["DirectorsChair"].Actions); + Assert.True(catalog.Buildings.ContainsKey("GymBuilding")); + Assert.True(catalog.Rooms.ContainsKey("Classroom")); + Assert.Equal("Класс", catalog.Label("ru", catalog.Rooms["Classroom"])); + Assert.Equal(["Teacher"], catalog.PositionsFor(DefKind.Room, "Classroom")); + Assert.Equal(2, map.Buildings.Count); + Assert.True(map.Rooms.Count >= 18, "Vanilla layout should look like a small school, not a stub."); + Assert.Contains(map.Rooms, room => room.Id == "classroom-1a" && room.Label == "1A"); } }