Enhance school creation and staffing management with native language support
- Updated protocol documentation to include `nativeLanguages` in `nameSets` and added `nativeLanguage` to school creation options. - Enhanced the UI for school creation to allow selection of native languages, improving user experience. - Revised API interfaces to accommodate new native language features, ensuring proper data handling. - Improved localization strings to support new native language functionalities in both English and Russian. - Updated tests to validate the new native language features and ensure robust functionality in staffing scenarios.
This commit is contained in:
@@ -86,7 +86,7 @@ internal sealed record CatalogResponse(
|
||||
IReadOnlyList<DefInfoResponse> Floors,
|
||||
IReadOnlyList<RoomInfoResponse> Rooms,
|
||||
IReadOnlyList<DefInfoResponse> Things,
|
||||
IReadOnlyList<DefInfoResponse> NameSets,
|
||||
IReadOnlyList<NameSetInfoResponse> NameSets,
|
||||
IReadOnlyList<SubjectInfoResponse> Subjects,
|
||||
DayFrameResponse? DayFrame,
|
||||
IReadOnlyList<HolidayInfoResponse> Holidays,
|
||||
@@ -99,7 +99,7 @@ internal sealed record CatalogResponse(
|
||||
Placeable(catalog.Floors.Values, catalog, locale),
|
||||
PlaceableRooms(catalog, locale),
|
||||
PlaceableThings(catalog, locale),
|
||||
Placeable(catalog.NameSets.Values, catalog, locale),
|
||||
PlaceableNameSets(catalog, locale),
|
||||
PlaceableSubjects(catalog, locale),
|
||||
MapDayFrame(catalog, locale),
|
||||
PlaceableHolidays(catalog, locale),
|
||||
@@ -113,6 +113,22 @@ internal sealed record CatalogResponse(
|
||||
.Select(def => new DefInfoResponse(def.DefName, catalog.Label(locale, def)))
|
||||
.ToArray();
|
||||
|
||||
private static IReadOnlyList<NameSetInfoResponse> PlaceableNameSets(DefCatalog catalog, string locale) =>
|
||||
catalog.NameSets.Values
|
||||
.Where(def => !def.Abstract)
|
||||
.OrderBy(def => def.DefName, StringComparer.Ordinal)
|
||||
.Select(def => new NameSetInfoResponse(
|
||||
def.DefName,
|
||||
catalog.Label(locale, def),
|
||||
def.Spoken
|
||||
.Select(skill => new DefInfoResponse(
|
||||
skill,
|
||||
catalog.Skills.TryGetValue(skill, out var language)
|
||||
? catalog.Label(locale, language)
|
||||
: skill))
|
||||
.ToArray()))
|
||||
.ToArray();
|
||||
|
||||
private static IReadOnlyList<DefInfoResponse> PlaceableThings(DefCatalog catalog, string locale) =>
|
||||
catalog.Things.Values
|
||||
.Where(def => !def.Abstract)
|
||||
@@ -178,6 +194,11 @@ internal sealed record CatalogResponse(
|
||||
|
||||
internal sealed record DefInfoResponse(string DefName, string Label, int PupilSlots = 0);
|
||||
|
||||
internal sealed record NameSetInfoResponse(
|
||||
string DefName,
|
||||
string Label,
|
||||
IReadOnlyList<DefInfoResponse> NativeLanguages);
|
||||
|
||||
internal sealed record RoomInfoResponse(
|
||||
string DefName,
|
||||
string Label,
|
||||
|
||||
@@ -206,7 +206,8 @@ internal sealed record ApplicantResponse(
|
||||
bool IsParent,
|
||||
float HourlyWageAsk,
|
||||
float MonthlyBase,
|
||||
IReadOnlyList<LabeledStatResponse> Skills);
|
||||
IReadOnlyList<LabeledStatResponse> Skills,
|
||||
IReadOnlyList<DefLabelResponse> Traits);
|
||||
|
||||
internal sealed record StaffMemberResponse(
|
||||
string Id,
|
||||
@@ -257,7 +258,8 @@ internal static class StaffingMapper
|
||||
applicant.Person.IsParent,
|
||||
applicant.HourlyWageAsk,
|
||||
rules is null ? 0f : Staffing.MonthlyBase(rules, applicant.HourlyWageAsk),
|
||||
StrongSkills(applicant.Person, catalog, locale)))
|
||||
SkillsOf(applicant.Person, catalog, locale),
|
||||
TraitsOf(applicant.Person, catalog, locale)))
|
||||
.ToArray();
|
||||
|
||||
var staff = roster.People
|
||||
@@ -290,12 +292,11 @@ internal static class StaffingMapper
|
||||
return new StaffingResponse(allocated, payroll, remaining, uncovered, applicants, staff, positions, subjects);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<LabeledStatResponse> StrongSkills(Person person, DefCatalog? catalog, string locale)
|
||||
private static IReadOnlyList<LabeledStatResponse> SkillsOf(Person person, DefCatalog? catalog, string locale)
|
||||
{
|
||||
return person.Skills
|
||||
.OrderByDescending(pair => pair.Value)
|
||||
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
|
||||
.Take(3)
|
||||
.Select(pair => new LabeledStatResponse(
|
||||
pair.Key,
|
||||
catalog is not null && catalog.Skills.TryGetValue(pair.Key, out var def)
|
||||
@@ -305,6 +306,17 @@ internal static class StaffingMapper
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<DefLabelResponse> TraitsOf(Person person, DefCatalog? catalog, string locale)
|
||||
{
|
||||
return person.Traits
|
||||
.Select(name => new DefLabelResponse(
|
||||
name,
|
||||
catalog is not null && catalog.Traits.TryGetValue(name, out var def)
|
||||
? catalog.Label(locale, def)
|
||||
: name))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static StaffMemberResponse Member(
|
||||
Person person,
|
||||
Roster roster,
|
||||
|
||||
@@ -54,6 +54,7 @@ internal static class SchoolEndpoints
|
||||
request.ModIds,
|
||||
request.Map,
|
||||
request.NameSetId,
|
||||
request.NativeLanguage,
|
||||
NewCompletion<SchoolCreationOutcome>());
|
||||
commands.Enqueue(command);
|
||||
|
||||
@@ -77,6 +78,8 @@ internal static class SchoolEndpoints
|
||||
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."),
|
||||
SchoolCreationError.UnknownNativeLanguage =>
|
||||
Problem(StatusCodes.Status400BadRequest, "unknown-native-language", "The selected native language is not in that name set."),
|
||||
_ => Results.Problem("Unknown error."),
|
||||
};
|
||||
})
|
||||
@@ -486,7 +489,8 @@ internal sealed record CreateSchoolRequest(
|
||||
DateTime StartDate,
|
||||
IReadOnlyList<string>? ModIds,
|
||||
MapLayout? Map,
|
||||
string? NameSetId);
|
||||
string? NameSetId,
|
||||
string? NativeLanguage);
|
||||
|
||||
internal sealed record SchoolResponse(int Id, string Name, DateTime GameTime, bool Running, byte SpeedIndex)
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@ internal abstract record GameCommand
|
||||
IReadOnlyList<string>? ExtraModIds,
|
||||
MapLayout? Map,
|
||||
string? NameSetId,
|
||||
string? NativeLanguage,
|
||||
TaskCompletionSource<SchoolCreationOutcome> Result) : GameCommand;
|
||||
|
||||
internal sealed record DeleteSchool(int SchoolId, TaskCompletionSource<bool> Result) : GameCommand;
|
||||
|
||||
@@ -328,10 +328,23 @@ internal sealed class GameLoopService(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!catalog.NameSets.TryGetValue(nameSetId, out var names))
|
||||
{
|
||||
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.UnknownNameSet));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(command.NativeLanguage) && !NativeLanguages.Allows(names, command.NativeLanguage))
|
||||
{
|
||||
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.UnknownNativeLanguage));
|
||||
return;
|
||||
}
|
||||
|
||||
var id = _nextId++;
|
||||
store.WriteNextId(_nextId);
|
||||
var nativeLanguage = NativeLanguages.Pick(names, id, command.NativeLanguage, rollIfOmitted: true);
|
||||
|
||||
var worker = SpawnWorker(id, normalized, command.StartDate, running: true, ClockSpeed.DefaultIndex, isNew: true, packIds, command.Map, nameSetId);
|
||||
var worker = SpawnWorker(id, normalized, command.StartDate, running: true, ClockSpeed.DefaultIndex, isNew: true, packIds, command.Map, nameSetId, nativeLanguage);
|
||||
Track(worker);
|
||||
worker.Start();
|
||||
|
||||
@@ -499,6 +512,7 @@ internal sealed class GameLoopService(
|
||||
save.ModIds,
|
||||
save.Map,
|
||||
save.NameSetId,
|
||||
save.NativeLanguage,
|
||||
save.Presence);
|
||||
worker.Start();
|
||||
|
||||
@@ -547,6 +561,7 @@ internal sealed class GameLoopService(
|
||||
IReadOnlyList<string>? modIds,
|
||||
MapLayout? map,
|
||||
string? nameSetId,
|
||||
string? nativeLanguage,
|
||||
IReadOnlyList<PresenceSnapshot>? presence = null) =>
|
||||
new(
|
||||
id,
|
||||
@@ -558,6 +573,7 @@ internal sealed class GameLoopService(
|
||||
modIds,
|
||||
map,
|
||||
nameSetId,
|
||||
nativeLanguage,
|
||||
presence,
|
||||
_options,
|
||||
clients,
|
||||
|
||||
@@ -28,6 +28,8 @@ internal sealed class SchoolSave
|
||||
|
||||
public string? NameSetId { get; init; }
|
||||
|
||||
public string? NativeLanguage { get; init; }
|
||||
|
||||
public IReadOnlyList<PresenceSnapshot>? Presence { get; init; }
|
||||
}
|
||||
|
||||
@@ -162,6 +164,7 @@ internal sealed class SchoolStore
|
||||
ModIds = save.ModIds,
|
||||
Map = save.Map,
|
||||
NameSetId = save.NameSetId,
|
||||
NativeLanguage = save.NativeLanguage,
|
||||
Presence = save.Presence,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ internal sealed class SchoolWorker
|
||||
private readonly IReadOnlyList<string>? _modIds;
|
||||
private readonly MapLayout? _savedMap;
|
||||
private readonly string? _nameSetId;
|
||||
private string? _nativeLanguage;
|
||||
private readonly IReadOnlyList<PresenceSnapshot>? _savedPresence;
|
||||
private readonly Action<int> _onFailed;
|
||||
|
||||
@@ -65,6 +66,7 @@ internal sealed class SchoolWorker
|
||||
IReadOnlyList<string>? modIds,
|
||||
MapLayout? savedMap,
|
||||
string? nameSetId,
|
||||
string? nativeLanguage,
|
||||
IReadOnlyList<PresenceSnapshot>? savedPresence,
|
||||
SimulationOptions options,
|
||||
ClientRegistry clients,
|
||||
@@ -83,6 +85,7 @@ internal sealed class SchoolWorker
|
||||
_modIds = modIds;
|
||||
_savedMap = savedMap;
|
||||
_nameSetId = nameSetId;
|
||||
_nativeLanguage = nativeLanguage;
|
||||
_savedPresence = savedPresence;
|
||||
_options = options;
|
||||
_clients = clients;
|
||||
@@ -849,12 +852,15 @@ internal sealed class SchoolWorker
|
||||
ApplicantPool applicants;
|
||||
int seed;
|
||||
var generated = false;
|
||||
string? native;
|
||||
|
||||
if (_isNew)
|
||||
{
|
||||
seed = school.Id;
|
||||
roster = RosterGenerator.Generate(catalog, map, seed, nameSetId, school.Clock.Time);
|
||||
applicants = ApplicantPool.Create(catalog, roster, seed, nameSetId, school.Clock.Time);
|
||||
native = ResolveNative(catalog, nameSetId, seed, _nativeLanguage, generating: true);
|
||||
_nativeLanguage = native;
|
||||
roster = RosterGenerator.Generate(catalog, map, seed, nameSetId, school.Clock.Time, native);
|
||||
applicants = ApplicantPool.Create(catalog, roster, seed, nameSetId, school.Clock.Time, native);
|
||||
generated = true;
|
||||
}
|
||||
else
|
||||
@@ -863,13 +869,17 @@ internal sealed class SchoolWorker
|
||||
if (loaded is null)
|
||||
{
|
||||
seed = school.Id;
|
||||
roster = RosterGenerator.Generate(catalog, map, seed, nameSetId, school.Clock.Time);
|
||||
applicants = ApplicantPool.Create(catalog, roster, seed, nameSetId, school.Clock.Time);
|
||||
native = ResolveNative(catalog, nameSetId, seed, _nativeLanguage, generating: true);
|
||||
_nativeLanguage = native;
|
||||
roster = RosterGenerator.Generate(catalog, map, seed, nameSetId, school.Clock.Time, native);
|
||||
applicants = ApplicantPool.Create(catalog, roster, seed, nameSetId, school.Clock.Time, native);
|
||||
generated = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
seed = loaded.Seed;
|
||||
native = ResolveNative(catalog, nameSetId, seed, _nativeLanguage, generating: false);
|
||||
_nativeLanguage = native;
|
||||
roster = loaded.ToRoster();
|
||||
if (loaded.Applicants is { Applicants.Count: > 0 })
|
||||
{
|
||||
@@ -877,7 +887,7 @@ internal sealed class SchoolWorker
|
||||
}
|
||||
else
|
||||
{
|
||||
applicants = ApplicantPool.Create(catalog, roster, seed, nameSetId, school.Clock.Time);
|
||||
applicants = ApplicantPool.Create(catalog, roster, seed, nameSetId, school.Clock.Time, native);
|
||||
generated = true;
|
||||
}
|
||||
}
|
||||
@@ -889,7 +899,7 @@ internal sealed class SchoolWorker
|
||||
$"School {_id} roster does not match its map; the people file was left untouched.");
|
||||
}
|
||||
|
||||
school.InstallPeople(roster, seed, nameSetId, applicants);
|
||||
school.InstallPeople(roster, seed, nameSetId, applicants, _nativeLanguage);
|
||||
InstallTimetable(school);
|
||||
school.ConfigurePresence(_options.SchoolWeekDays, _options.MaxDecisionsPerTick);
|
||||
school.RestorePresence(_savedPresence);
|
||||
@@ -916,6 +926,21 @@ internal sealed class SchoolWorker
|
||||
return available.Contains(requested, StringComparer.Ordinal) ? requested : null;
|
||||
}
|
||||
|
||||
private static string? ResolveNative(
|
||||
DefCatalog catalog,
|
||||
string nameSetId,
|
||||
int schoolSeed,
|
||||
string? requested,
|
||||
bool generating)
|
||||
{
|
||||
if (!catalog.NameSets.TryGetValue(nameSetId, out var names))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return NativeLanguages.Pick(names, schoolSeed, requested, rollIfOmitted: generating && string.IsNullOrWhiteSpace(requested));
|
||||
}
|
||||
|
||||
private void Persist()
|
||||
{
|
||||
var school = _school;
|
||||
@@ -938,6 +963,7 @@ internal sealed class SchoolWorker
|
||||
ModIds = school.Catalog?.PackIds,
|
||||
Map = school.Map,
|
||||
NameSetId = _nameSetId,
|
||||
NativeLanguage = _nativeLanguage,
|
||||
Presence = school.CapturePresence(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
"patronymicRule": "slavic",
|
||||
"defaultGivenDeclension": "hard",
|
||||
"defaultSurnameDeclension": "ov",
|
||||
"nativeLanguages": ["RussianLanguage", "BelarusianLanguage", "UkrainianLanguage"],
|
||||
"relatedLanguageChance": 0.6,
|
||||
"relatedLanguageMean": 22,
|
||||
"relatedLanguageStdDev": 8,
|
||||
"relatedLanguageMax": 40,
|
||||
"maleGiven": [
|
||||
{ "form": "Александр" },
|
||||
{ "form": "Алексей" },
|
||||
|
||||
@@ -61,9 +61,4 @@
|
||||
"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,40 @@
|
||||
[
|
||||
{
|
||||
"defName": "English",
|
||||
"range": { "min": 0, "max": 100 },
|
||||
"distribution": { "mean": 48, "stdDev": 15 },
|
||||
"adultChance": 0.4,
|
||||
"ageMeans": [
|
||||
{ "age": 11, "mean": 28 },
|
||||
{ "age": 17, "mean": 50 },
|
||||
],
|
||||
},
|
||||
{
|
||||
"defName": "German",
|
||||
"range": { "min": 0, "max": 100 },
|
||||
"distribution": { "mean": 42, "stdDev": 14 },
|
||||
"adultChance": 0.08,
|
||||
"ageMeans": [
|
||||
{ "age": 14, "mean": 24 },
|
||||
{ "age": 17, "mean": 42 },
|
||||
],
|
||||
},
|
||||
{
|
||||
"defName": "BelarusianLanguage",
|
||||
"range": { "min": 0, "max": 100 },
|
||||
"distribution": { "mean": 52, "stdDev": 13 },
|
||||
"ageMeans": [
|
||||
{ "age": 7, "mean": 32 },
|
||||
{ "age": 17, "mean": 56 },
|
||||
],
|
||||
},
|
||||
{
|
||||
"defName": "UkrainianLanguage",
|
||||
"range": { "min": 0, "max": 100 },
|
||||
"distribution": { "mean": 52, "stdDev": 13 },
|
||||
"ageMeans": [
|
||||
{ "age": 7, "mean": 32 },
|
||||
{ "age": 17, "mean": 56 },
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"defName": "Agility",
|
||||
"always": true,
|
||||
"range": { "min": 0, "max": 100 },
|
||||
"distribution": { "mean": 50, "stdDev": 14 },
|
||||
"bodyLimits": [
|
||||
@@ -11,6 +12,7 @@
|
||||
},
|
||||
{
|
||||
"defName": "Strength",
|
||||
"always": true,
|
||||
"range": { "min": 0, "max": 100 },
|
||||
"distribution": { "mean": 50, "stdDev": 14 },
|
||||
"bodyLimits": [
|
||||
@@ -20,6 +22,7 @@
|
||||
},
|
||||
{
|
||||
"defName": "Endurance",
|
||||
"always": true,
|
||||
"range": { "min": 0, "max": 100 },
|
||||
"distribution": { "mean": 50, "stdDev": 13 },
|
||||
"bodyLimits": [
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{
|
||||
"defName": "Communication",
|
||||
"always": true,
|
||||
"range": { "min": 0, "max": 100 },
|
||||
"distribution": { "mean": 52, "stdDev": 12 },
|
||||
"ageMeans": [
|
||||
{ "age": 7, "mean": 32 },
|
||||
{ "age": 17, "mean": 52 },
|
||||
{ "age": 40, "mean": 58 },
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,52 @@
|
||||
[
|
||||
{
|
||||
"defName": "Pedagogy",
|
||||
"work": true,
|
||||
"range": { "min": 0, "max": 100 },
|
||||
"distribution": { "mean": 48, "stdDev": 14 },
|
||||
"ageMeans": [
|
||||
{ "age": 22, "mean": 40 },
|
||||
{ "age": 45, "mean": 58 },
|
||||
],
|
||||
},
|
||||
{
|
||||
"defName": "Medicine",
|
||||
"work": true,
|
||||
"range": { "min": 0, "max": 100 },
|
||||
"distribution": { "mean": 46, "stdDev": 15 },
|
||||
"ageMeans": [
|
||||
{ "age": 22, "mean": 38 },
|
||||
{ "age": 45, "mean": 56 },
|
||||
],
|
||||
},
|
||||
{
|
||||
"defName": "Cooking",
|
||||
"work": true,
|
||||
"range": { "min": 0, "max": 100 },
|
||||
"distribution": { "mean": 50, "stdDev": 14 },
|
||||
"ageMeans": [
|
||||
{ "age": 20, "mean": 42 },
|
||||
{ "age": 45, "mean": 58 },
|
||||
],
|
||||
},
|
||||
{
|
||||
"defName": "Administration",
|
||||
"work": true,
|
||||
"range": { "min": 0, "max": 100 },
|
||||
"distribution": { "mean": 48, "stdDev": 13 },
|
||||
"ageMeans": [
|
||||
{ "age": 22, "mean": 40 },
|
||||
{ "age": 45, "mean": 57 },
|
||||
],
|
||||
},
|
||||
{
|
||||
"defName": "Librarianship",
|
||||
"work": true,
|
||||
"range": { "min": 0, "max": 100 },
|
||||
"distribution": { "mean": 46, "stdDev": 13 },
|
||||
"ageMeans": [
|
||||
{ "age": 22, "mean": 38 },
|
||||
{ "age": 45, "mean": 55 },
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"defName": "Staffing",
|
||||
"poolSize": 12,
|
||||
"poolSize": 32,
|
||||
"stayChance": 0.65,
|
||||
"parentChance": 0.35,
|
||||
"hourlyWageBase": 30,
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"defName": "ForeignLanguage",
|
||||
"grades": { "min": 5, "max": 11 },
|
||||
"hoursPerWeek": 3,
|
||||
"skills": [{ "skill": "ForeignLanguage", "share": 1 }],
|
||||
"skills": [{ "skill": "English", "share": 1 }],
|
||||
},
|
||||
{
|
||||
"defName": "History",
|
||||
|
||||
@@ -62,6 +62,16 @@
|
||||
"Geography": "Geography",
|
||||
"Informatics": "Informatics",
|
||||
"ForeignLanguage": "Foreign language",
|
||||
"English": "English",
|
||||
"German": "German",
|
||||
"BelarusianLanguage": "Belarusian",
|
||||
"UkrainianLanguage": "Ukrainian",
|
||||
"Communication": "Communication",
|
||||
"Pedagogy": "Pedagogy",
|
||||
"Medicine": "Medicine",
|
||||
"Cooking": "Cooking",
|
||||
"Administration": "Administration",
|
||||
"Librarianship": "Librarianship",
|
||||
"Agility": "Agility",
|
||||
"Strength": "Strength",
|
||||
"Endurance": "Endurance",
|
||||
|
||||
@@ -62,6 +62,16 @@
|
||||
"Geography": "География",
|
||||
"Informatics": "Информатика",
|
||||
"ForeignLanguage": "Иностранный язык",
|
||||
"English": "Английский",
|
||||
"German": "Немецкий",
|
||||
"BelarusianLanguage": "Белорусский",
|
||||
"UkrainianLanguage": "Украинский",
|
||||
"Communication": "Общение",
|
||||
"Pedagogy": "Педагогика",
|
||||
"Medicine": "Медицина",
|
||||
"Cooking": "Готовка",
|
||||
"Administration": "Делопроизводство",
|
||||
"Librarianship": "Библиотечное дело",
|
||||
"Agility": "Ловкость",
|
||||
"Strength": "Сила",
|
||||
"Endurance": "Выносливость",
|
||||
|
||||
Reference in New Issue
Block a user