Enhance school creation functionality by introducing support for name sets in the API and UI. Update the catalog to include skills, traits, body attributes, needs, and name sets, improving character generation capabilities. Revise localization strings for better user guidance and update tests to validate the new name set functionality and ensure robustness in school creation processes.
ci / server (push) Failing after 3m45s
ci / client (push) Successful in 17s

This commit is contained in:
Leonid Pershin
2026-08-18 18:57:01 +03:00
parent 38afbcad36
commit e6182e0e45
56 changed files with 3706 additions and 9 deletions
+2
View File
@@ -86,6 +86,7 @@ internal sealed record CatalogResponse(
IReadOnlyList<DefInfoResponse> Floors,
IReadOnlyList<RoomInfoResponse> Rooms,
IReadOnlyList<DefInfoResponse> Things,
IReadOnlyList<DefInfoResponse> NameSets,
MapLayout DefaultMap)
{
public static CatalogResponse From(DefCatalog catalog, MapLayout map, string locale) =>
@@ -95,6 +96,7 @@ internal sealed record CatalogResponse(
Placeable(catalog.Floors.Values, catalog, locale),
PlaceableRooms(catalog, locale),
PlaceableThings(catalog, locale),
Placeable(catalog.NameSets.Values, catalog, locale),
map);
private static IReadOnlyList<DefInfoResponse> Placeable<T>(IEnumerable<T> defs, DefCatalog catalog, string locale)
+9 -1
View File
@@ -50,6 +50,7 @@ internal static class SchoolEndpoints
DateTime.SpecifyKind(request.StartDate, DateTimeKind.Utc),
request.ModIds,
request.Map,
request.NameSetId,
NewCompletion<SchoolCreationOutcome>());
commands.Enqueue(command);
@@ -71,6 +72,8 @@ internal static class SchoolEndpoints
Problem(StatusCodes.Status400BadRequest, "unknown-mod", "A selected mod is missing."),
SchoolCreationError.InvalidCatalog =>
Problem(StatusCodes.Status400BadRequest, "invalid-catalog", "The selected packs could not be loaded."),
SchoolCreationError.UnknownNameSet =>
Problem(StatusCodes.Status400BadRequest, "unknown-name-set", "The selected name set is not in the catalog."),
_ => Results.Problem("Unknown error."),
};
})
@@ -107,7 +110,12 @@ internal static class SchoolEndpoints
}
/// <summary>Body of <c>POST /api/schools</c>. The start date is a game calendar date, not a real one.</summary>
internal sealed record CreateSchoolRequest(string? Name, DateTime StartDate, IReadOnlyList<string>? ModIds, MapLayout? Map);
internal sealed record CreateSchoolRequest(
string? Name,
DateTime StartDate,
IReadOnlyList<string>? ModIds,
MapLayout? Map,
string? NameSetId);
internal sealed record SchoolResponse(int Id, string Name, DateTime GameTime, bool Running, byte SpeedIndex)
{
+1
View File
@@ -14,6 +14,7 @@ internal abstract record GameCommand
DateTime StartDate,
IReadOnlyList<string>? ExtraModIds,
MapLayout? Map,
string? NameSetId,
TaskCompletionSource<SchoolCreationOutcome> Result) : GameCommand;
internal sealed record DeleteSchool(int SchoolId, TaskCompletionSource<bool> Result) : GameCommand;
+48 -3
View File
@@ -216,10 +216,28 @@ internal sealed class GameLoopService(
return;
}
DefCatalog catalog;
try
{
catalog = mods.LoadCatalog(packIds);
}
catch (Exception ex) when (ex is ContentLoadException or SchoolContentUnavailableException)
{
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.InvalidCatalog));
return;
}
var nameSetId = ResolveNameSetId(catalog, command.NameSetId);
if (nameSetId is null)
{
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.UnknownNameSet));
return;
}
var id = _nextId++;
store.WriteNextId(_nextId);
var worker = SpawnWorker(id, normalized, command.StartDate, running: true, ClockSpeed.DefaultIndex, isNew: true, packIds, command.Map);
var worker = SpawnWorker(id, normalized, command.StartDate, running: true, ClockSpeed.DefaultIndex, isNew: true, packIds, command.Map, nameSetId);
Track(worker);
worker.Start();
@@ -385,7 +403,8 @@ internal sealed class GameLoopService(
save.SpeedIndex,
isNew: false,
save.ModIds,
save.Map);
save.Map,
save.NameSetId);
worker.Start();
try
@@ -431,7 +450,8 @@ internal sealed class GameLoopService(
int speedIndex,
bool isNew,
IReadOnlyList<string>? modIds,
MapLayout? map) =>
MapLayout? map,
string? nameSetId) =>
new(
id,
name,
@@ -441,6 +461,7 @@ internal sealed class GameLoopService(
isNew,
modIds,
map,
nameSetId,
_options,
clients,
metrics,
@@ -449,6 +470,30 @@ internal sealed class GameLoopService(
onFailed: schoolId => commands.Enqueue(new GameCommand.WorkerFailed(schoolId)),
loggerFactory.CreateLogger($"HSchool.Server.Game.SchoolWorker.{id}"));
/// <summary>
/// Empty request uses the first placeable set (core's Slavic). A named id must exist in the
/// catalog already loaded for this pack list — unknown extras were rejected above.
/// </summary>
internal static string? ResolveNameSetId(DefCatalog catalog, string? requested)
{
var available = catalog.NameSets.Values
.Where(def => !def.Abstract)
.Select(def => def.DefName)
.OrderBy(name => name, StringComparer.Ordinal)
.ToArray();
if (available.Length == 0)
{
return null;
}
if (string.IsNullOrWhiteSpace(requested))
{
return available[0];
}
return available.Contains(requested, StringComparer.Ordinal) ? requested : null;
}
private void Track(SchoolWorker worker)
{
_workers[worker.Id] = worker;
+3
View File
@@ -23,6 +23,8 @@ internal sealed class SchoolSave
public IReadOnlyList<string>? ModIds { get; init; }
public MapLayout? Map { get; init; }
public string? NameSetId { get; init; }
}
/// <summary>Allocates school ids that survive a process restart.</summary>
@@ -152,6 +154,7 @@ internal sealed class SchoolStore
SpeedIndex = save.SpeedIndex,
ModIds = save.ModIds,
Map = save.Map,
NameSetId = save.NameSetId,
});
}
catch (Exception ex)
+4
View File
@@ -29,6 +29,7 @@ internal sealed class SchoolWorker
private readonly bool _isNew;
private readonly IReadOnlyList<string>? _modIds;
private readonly MapLayout? _savedMap;
private readonly string? _nameSetId;
private readonly Action<int> _onFailed;
private readonly int _id;
@@ -53,6 +54,7 @@ internal sealed class SchoolWorker
bool isNew,
IReadOnlyList<string>? modIds,
MapLayout? savedMap,
string? nameSetId,
SimulationOptions options,
ClientRegistry clients,
GameMetrics metrics,
@@ -69,6 +71,7 @@ internal sealed class SchoolWorker
_isNew = isNew;
_modIds = modIds;
_savedMap = savedMap;
_nameSetId = nameSetId;
_options = options;
_clients = clients;
_metrics = metrics;
@@ -411,6 +414,7 @@ internal sealed class SchoolWorker
SpeedIndex = school.Clock.SpeedIndex,
ModIds = school.Catalog?.PackIds,
Map = school.Map,
NameSetId = _nameSetId,
});
}
catch (Exception ex)
@@ -0,0 +1,11 @@
{
"defName": "EyeColor",
"kind": "choice",
"options": [
{ "value": "BrownEyes", "weight": 40 },
{ "value": "BlueEyes", "weight": 20 },
{ "value": "GrayEyes", "weight": 20 },
{ "value": "GreenEyes", "weight": 12 },
{ "value": "HazelEyes", "weight": 8 },
],
}
@@ -0,0 +1,11 @@
{
"defName": "HairColor",
"kind": "choice",
"options": [
{ "value": "BlackHair", "weight": 25 },
{ "value": "BrownHair", "weight": 40 },
{ "value": "BlondHair", "weight": 20 },
{ "value": "RedHair", "weight": 8 },
{ "value": "GrayHair", "weight": 2, "ageMin": 40 },
],
}
@@ -0,0 +1,14 @@
{
"defName": "Height",
"kind": "number",
"distributions": [
{ "sex": "male", "ageMin": 6, "ageMax": 9, "distribution": { "mean": 128, "stdDev": 6 }, "range": { "min": 110, "max": 150 } },
{ "sex": "male", "ageMin": 10, "ageMax": 13, "distribution": { "mean": 150, "stdDev": 8 }, "range": { "min": 128, "max": 175 } },
{ "sex": "male", "ageMin": 14, "ageMax": 17, "distribution": { "mean": 172, "stdDev": 8 }, "range": { "min": 150, "max": 195 } },
{ "sex": "male", "ageMin": 18, "distribution": { "mean": 176, "stdDev": 7 }, "range": { "min": 155, "max": 205 } },
{ "sex": "female", "ageMin": 6, "ageMax": 9, "distribution": { "mean": 127, "stdDev": 6 }, "range": { "min": 108, "max": 148 } },
{ "sex": "female", "ageMin": 10, "ageMax": 13, "distribution": { "mean": 148, "stdDev": 7 }, "range": { "min": 128, "max": 170 } },
{ "sex": "female", "ageMin": 14, "ageMax": 17, "distribution": { "mean": 163, "stdDev": 6 }, "range": { "min": 145, "max": 185 } },
{ "sex": "female", "ageMin": 18, "distribution": { "mean": 164, "stdDev": 6 }, "range": { "min": 145, "max": 190 } },
],
}
@@ -0,0 +1,14 @@
{
"defName": "Weight",
"kind": "number",
"distributions": [
{ "sex": "male", "ageMin": 6, "ageMax": 9, "distribution": { "mean": 27, "stdDev": 5 }, "range": { "min": 16, "max": 45 } },
{ "sex": "male", "ageMin": 10, "ageMax": 13, "distribution": { "mean": 42, "stdDev": 8 }, "range": { "min": 25, "max": 70 } },
{ "sex": "male", "ageMin": 14, "ageMax": 17, "distribution": { "mean": 62, "stdDev": 10 }, "range": { "min": 40, "max": 95 } },
{ "sex": "male", "ageMin": 18, "distribution": { "mean": 78, "stdDev": 12 }, "range": { "min": 50, "max": 130 } },
{ "sex": "female", "ageMin": 6, "ageMax": 9, "distribution": { "mean": 26, "stdDev": 5 }, "range": { "min": 15, "max": 44 } },
{ "sex": "female", "ageMin": 10, "ageMax": 13, "distribution": { "mean": 42, "stdDev": 8 }, "range": { "min": 24, "max": 70 } },
{ "sex": "female", "ageMin": 14, "ageMax": 17, "distribution": { "mean": 54, "stdDev": 8 }, "range": { "min": 38, "max": 85 } },
{ "sex": "female", "ageMin": 18, "distribution": { "mean": 62, "stdDev": 10 }, "range": { "min": 42, "max": 110 } },
],
}
@@ -0,0 +1,131 @@
{
"defName": "Slavic",
"patronymicRule": "slavic",
"defaultGivenDeclension": "hard",
"defaultSurnameDeclension": "ov",
"maleGiven": [
{ "form": "Александр" },
{ "form": "Алексей" },
{ "form": "Андрей" },
{ "form": "Антон" },
{ "form": "Артём" },
{ "form": "Борис" },
{ "form": "Вадим" },
{ "form": "Валерий" },
{ "form": "Виктор" },
{ "form": "Виталий" },
{ "form": "Владимир" },
{ "form": "Владислав" },
{ "form": "Григорий" },
{ "form": "Денис" },
{ "form": "Дмитрий" },
{ "form": "Евгений" },
{ "form": "Егор" },
{ "form": "Иван" },
{ "form": "Игорь", "declension": "soft" },
{ "form": "Илья", "declension": "ya" },
{ "form": "Кирилл" },
{ "form": "Константин" },
{ "form": "Максим" },
{ "form": "Михаил" },
{ "form": "Никита", "declension": "a" },
{ "form": "Николай" },
{ "form": "Олег" },
{ "form": "Павел" },
{ "form": "Пётр" },
{ "form": "Роман" },
{ "form": "Сергей" },
{ "form": "Станислав" },
{ "form": "Степан" },
{ "form": "Фёдор" },
{ "form": "Юрий" },
],
"femaleGiven": [
{ "form": "Александра", "declension": "a" },
{ "form": "Алина", "declension": "a" },
{ "form": "Анастасия", "declension": "iya" },
{ "form": "Анна", "declension": "a" },
{ "form": "Валентина", "declension": "a" },
{ "form": "Валерия", "declension": "iya" },
{ "form": "Вера", "declension": "a" },
{ "form": "Виктория", "declension": "iya" },
{ "form": "Дарья", "declension": "ya" },
{ "form": "Екатерина", "declension": "a" },
{ "form": "Елена", "declension": "a" },
{ "form": "Елизавета", "declension": "a" },
{ "form": "Ирина", "declension": "a" },
{ "form": "Ксения", "declension": "iya" },
{
"form": "Любовь",
"cases": {
"nom": "Любовь",
"gen": "Любови",
"dat": "Любови",
"acc": "Любовь",
"ins": "Любовью",
"pre": "Любови",
},
},
{ "form": "Людмила", "declension": "a" },
{ "form": "Маргарита", "declension": "a" },
{ "form": "Мария", "declension": "iya" },
{ "form": "Надежда", "declension": "a" },
{ "form": "Наталья", "declension": "ya" },
{ "form": "Нина", "declension": "a" },
{ "form": "Оксана", "declension": "a" },
{ "form": "Ольга", "declension": "a" },
{ "form": "Полина", "declension": "a" },
{ "form": "Светлана", "declension": "a" },
{ "form": "София", "declension": "iya" },
{ "form": "Татьяна", "declension": "a" },
{ "form": "Юлия", "declension": "iya" },
{ "form": "Яна", "declension": "a" },
{ "form": "Вероника", "declension": "a" },
{ "form": "Диана", "declension": "a" },
{ "form": "Марина", "declension": "a" },
],
"surnames": [
{ "male": "Иванов", "female": "Иванова" },
{ "male": "Петров", "female": "Петрова" },
{ "male": "Смирнов", "female": "Смирнова" },
{ "male": "Кузнецов", "female": "Кузнецова" },
{ "male": "Попов", "female": "Попова" },
{ "male": "Васильев", "female": "Васильева" },
{ "male": "Соколов", "female": "Соколова" },
{ "male": "Михайлов", "female": "Михайлова" },
{ "male": "Новиков", "female": "Новикова" },
{ "male": "Фёдоров", "female": "Фёдорова" },
{ "male": "Морозов", "female": "Морозова" },
{ "male": "Волков", "female": "Волкова" },
{ "male": "Алексеев", "female": "Алексеева" },
{ "male": "Лебедев", "female": "Лебедева" },
{ "male": "Семёнов", "female": "Семёнова" },
{ "male": "Егоров", "female": "Егорова" },
{ "male": "Павлов", "female": "Павлова" },
{ "male": "Козлов", "female": "Козлова" },
{ "male": "Степанов", "female": "Степанова" },
{ "male": "Николаев", "female": "Николаева" },
{ "male": "Орлов", "female": "Орлова" },
{ "male": "Андреев", "female": "Андреева" },
{ "male": "Макаров", "female": "Макарова" },
{ "male": "Никитин", "female": "Никитина", "declension": "in" },
{ "male": "Захаров", "female": "Захарова" },
{ "male": "Зайцев", "female": "Зайцева" },
{ "male": "Соловьёв", "female": "Соловьёва" },
{ "male": "Борисов", "female": "Борисова" },
{ "male": "Яковлев", "female": "Яковлева" },
{ "male": "Григорьев", "female": "Григорьева" },
{ "male": "Романов", "female": "Романова" },
{ "male": "Воробьёв", "female": "Воробьёва" },
{ "male": "Сергеев", "female": "Сергеева" },
{ "male": "Кузьмин", "female": "Кузьмина", "declension": "in" },
{ "male": "Фролов", "female": "Фролова" },
{ "male": "Александров", "female": "Александрова" },
{ "male": "Дмитриев", "female": "Дмитриева" },
{ "male": "Королёв", "female": "Королёва" },
{ "male": "Громов", "female": "Громова" },
{ "male": "Ильин", "female": "Ильина", "declension": "in" },
{ "male": "Козловский", "female": "Козловская", "declension": "sky" },
{ "male": "Орловский", "female": "Орловская", "declension": "sky" },
],
}
@@ -0,0 +1,6 @@
[
{ "defName": "Sleep", "initial": 1, "decayPerHour": 0, "min": 0, "max": 1 },
{ "defName": "Hunger", "initial": 1, "decayPerHour": 0, "min": 0, "max": 1 },
{ "defName": "Toilet", "initial": 1, "decayPerHour": 0, "min": 0, "max": 1 },
{ "defName": "Social", "initial": 1, "decayPerHour": 0, "min": 0, "max": 1 },
]
@@ -0,0 +1,69 @@
[
{
"defName": "Mathematics",
"range": { "min": 0, "max": 100 },
"distribution": { "mean": 50, "stdDev": 14 },
"ageMeans": [
{ "age": 7, "mean": 28 },
{ "age": 17, "mean": 55 },
],
},
{
"defName": "RussianLanguage",
"range": { "min": 0, "max": 100 },
"distribution": { "mean": 52, "stdDev": 13 },
"ageMeans": [
{ "age": 7, "mean": 32 },
{ "age": 17, "mean": 56 },
],
},
{
"defName": "Literature",
"range": { "min": 0, "max": 100 },
"distribution": { "mean": 50, "stdDev": 14 },
},
{
"defName": "Physics",
"range": { "min": 0, "max": 100 },
"distribution": { "mean": 48, "stdDev": 15 },
"ageMeans": [
{ "age": 12, "mean": 30 },
{ "age": 17, "mean": 50 },
],
},
{
"defName": "History",
"range": { "min": 0, "max": 100 },
"distribution": { "mean": 50, "stdDev": 13 },
},
{
"defName": "PhysicalEducation",
"range": { "min": 0, "max": 100 },
"distribution": { "mean": 55, "stdDev": 14 },
},
{
"defName": "Biology",
"range": { "min": 0, "max": 100 },
"distribution": { "mean": 50, "stdDev": 13 },
},
{
"defName": "Chemistry",
"range": { "min": 0, "max": 100 },
"distribution": { "mean": 48, "stdDev": 15 },
},
{
"defName": "Geography",
"range": { "min": 0, "max": 100 },
"distribution": { "mean": 50, "stdDev": 12 },
},
{
"defName": "Informatics",
"range": { "min": 0, "max": 100 },
"distribution": { "mean": 48, "stdDev": 16 },
},
{
"defName": "ForeignLanguage",
"range": { "min": 0, "max": 100 },
"distribution": { "mean": 50, "stdDev": 15 },
},
]
@@ -0,0 +1,30 @@
[
{
"defName": "Agility",
"range": { "min": 0, "max": 100 },
"distribution": { "mean": 50, "stdDev": 14 },
"bodyLimits": [
{ "attribute": "Build", "value": "Obese", "max": 25 },
{ "attribute": "Build", "value": "Heavy", "max": 45 },
{ "attribute": "Build", "value": "Athletic", "min": 40 },
],
},
{
"defName": "Strength",
"range": { "min": 0, "max": 100 },
"distribution": { "mean": 50, "stdDev": 14 },
"bodyLimits": [
{ "attribute": "Build", "value": "Skinny", "max": 45 },
{ "attribute": "Build", "value": "Athletic", "min": 40 },
],
},
{
"defName": "Endurance",
"range": { "min": 0, "max": 100 },
"distribution": { "mean": 50, "stdDev": 13 },
"bodyLimits": [
{ "attribute": "Build", "value": "Obese", "max": 35 },
{ "attribute": "Build", "value": "Athletic", "min": 40 },
],
},
]
@@ -0,0 +1,78 @@
[
{
"defName": "Diligent",
"weight": 8,
"incompatible": ["Lazy", "AbsentMinded"],
"skillModifiers": [
{ "skill": "Mathematics", "offset": 8 },
{ "skill": "RussianLanguage", "offset": 6 },
],
},
{
"defName": "AbsentMinded",
"weight": 6,
"incompatible": ["Diligent"],
"skillModifiers": [
{ "skill": "Mathematics", "offset": -6 },
{ "skill": "Informatics", "offset": -4 },
],
},
{
"defName": "Bully",
"weight": 4,
"incompatible": ["Kind", "Quiet"],
"roles": ["student"],
"age": { "min": 7, "max": 18 },
"skillModifiers": [
{ "skill": "Strength", "offset": 8 },
{ "skill": "Literature", "offset": -4 },
],
},
{
"defName": "Quiet",
"weight": 7,
"incompatible": ["Leader", "Bully"],
},
{
"defName": "Leader",
"weight": 4,
"incompatible": ["Quiet"],
"skillModifiers": [
{ "skill": "History", "offset": 4 },
],
},
{
"defName": "Lazy",
"weight": 6,
"incompatible": ["Diligent"],
"skillModifiers": [
{ "skill": "PhysicalEducation", "offset": -8 },
{ "skill": "Mathematics", "offset": -6 },
],
},
{
"defName": "Curious",
"weight": 7,
"skillModifiers": [
{ "skill": "Physics", "offset": 6 },
{ "skill": "Informatics", "offset": 6 },
],
},
{
"defName": "HotTempered",
"weight": 5,
"incompatible": ["Quiet"],
},
{
"defName": "Kind",
"weight": 8,
"incompatible": ["Bully"],
},
{
"defName": "Neat",
"weight": 6,
"skillModifiers": [
{ "skill": "Chemistry", "offset": 4 },
],
},
]
@@ -44,4 +44,52 @@
"ComputerLab": "Computer lab",
"GymHall": "Gym",
"ChangingRoom": "Changing room",
"Mathematics": "Mathematics",
"RussianLanguage": "Russian",
"Literature": "Literature",
"Physics": "Physics",
"History": "History",
"PhysicalEducation": "PE",
"Biology": "Biology",
"Chemistry": "Chemistry",
"Geography": "Geography",
"Informatics": "Informatics",
"ForeignLanguage": "Foreign language",
"Agility": "Agility",
"Strength": "Strength",
"Endurance": "Endurance",
"Diligent": "Diligent",
"AbsentMinded": "Absent-minded",
"Bully": "Bully",
"Quiet": "Quiet",
"Leader": "Leader",
"Lazy": "Lazy",
"Curious": "Curious",
"HotTempered": "Hot-tempered",
"Kind": "Kind",
"Neat": "Neat",
"Height": "Height",
"Weight": "Weight",
"HairColor": "Hair colour",
"EyeColor": "Eye colour",
"BlackHair": "Black",
"BrownHair": "Brown",
"BlondHair": "Blond",
"RedHair": "Red",
"GrayHair": "Grey",
"BrownEyes": "Brown",
"BlueEyes": "Blue",
"GrayEyes": "Grey",
"GreenEyes": "Green",
"HazelEyes": "Hazel",
"Sleep": "Sleep",
"Hunger": "Hunger",
"Toilet": "Toilet",
"Social": "Social",
"Slavic": "Slavic",
"Skinny": "Skinny",
"Average": "Average",
"Athletic": "Athletic",
"Heavy": "Heavy",
"Obese": "Obese",
}
@@ -44,4 +44,52 @@
"ComputerLab": "Компьютерный класс",
"GymHall": "Спортивный зал",
"ChangingRoom": "Раздевалка",
"Mathematics": "Математика",
"RussianLanguage": "Русский язык",
"Literature": "Литература",
"Physics": "Физика",
"History": "История",
"PhysicalEducation": "Физкультура",
"Biology": "Биология",
"Chemistry": "Химия",
"Geography": "География",
"Informatics": "Информатика",
"ForeignLanguage": "Иностранный язык",
"Agility": "Ловкость",
"Strength": "Сила",
"Endurance": "Выносливость",
"Diligent": "Усидчивый",
"AbsentMinded": "Рассеянный",
"Bully": "Задира",
"Quiet": "Тихоня",
"Leader": "Лидер",
"Lazy": "Лентяй",
"Curious": "Любопытный",
"HotTempered": "Вспыльчивый",
"Kind": "Добрый",
"Neat": "Аккуратный",
"Height": "Рост",
"Weight": "Вес",
"HairColor": "Цвет волос",
"EyeColor": "Цвет глаз",
"BlackHair": "Чёрные",
"BrownHair": "Каштановые",
"BlondHair": "Русые",
"RedHair": "Рыжие",
"GrayHair": "Седые",
"BrownEyes": "Карие",
"BlueEyes": "Голубые",
"GrayEyes": "Серые",
"GreenEyes": "Зелёные",
"HazelEyes": "Ореховые",
"Sleep": "Сон",
"Hunger": "Голод",
"Toilet": "Туалет",
"Social": "Общение",
"Slavic": "Славянский",
"Skinny": "Худощавое",
"Average": "Обычное",
"Athletic": "Атлетическое",
"Heavy": "Плотное",
"Obese": "Полное",
}