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
+4
View File
@@ -41,9 +41,11 @@ const ru = {
errorInvalidMap: 'Карта должна быть связным графом: двор и хотя бы одна комната.',
errorUnknownMod: 'Выбранный мод не найден.',
errorInvalidCatalog: 'Не удалось загрузить выбранные моды.',
errorUnknownNameSet: 'Выбранный набор имён не найден.',
catalogLoadFailed: 'Не удалось загрузить каталог модов.',
modsTitle: 'Моды',
nameSetTitle: 'Набор имён',
coreModLocked: '{id} (всегда включён)',
mapEditorTitle: 'Карта',
editMap: 'Редактировать карту',
@@ -126,9 +128,11 @@ const en: Messages = {
errorInvalidMap: 'The map must be a connected graph: a yard and at least one room.',
errorUnknownMod: 'A selected mod is missing.',
errorInvalidCatalog: 'The selected packs could not be loaded.',
errorUnknownNameSet: 'The selected name set is not in the catalog.',
catalogLoadFailed: 'Could not load the mod catalog.',
modsTitle: 'Mods',
nameSetTitle: 'Name set',
coreModLocked: '{id} (always on)',
mapEditorTitle: 'Map',
editMap: 'Edit map',
+3
View File
@@ -44,6 +44,7 @@ export async function fetchRandomName(lang: string): Promise<string> {
export interface CreateSchoolOptions {
readonly modIds?: readonly string[];
readonly map?: MapLayout;
readonly nameSetId?: string;
}
export async function createSchool(
@@ -59,6 +60,7 @@ export async function createSchool(
startDate: startDate.toISOString(),
modIds: extras.modIds ?? [],
map: extras.map ?? null,
nameSetId: extras.nameSetId ?? null,
}),
});
}
@@ -109,6 +111,7 @@ export interface CatalogResponse {
readonly rooms: readonly RoomInfo[];
readonly things: readonly DefInfo[];
readonly defaultMap: MapLayout;
readonly nameSets: readonly DefInfo[];
}
export async function fetchMods(): Promise<readonly ModInfo[]> {
@@ -32,12 +32,20 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
const extraModIds = new Set<string>();
let catalog: CatalogResponse | null = null;
let currentMap: MapLayout | null = null;
let nameSetId: string | null = null;
const modsField = el('div', { class: 'field' });
const modsLabel = el('span', { class: 'field__label' });
const modsList = el('div', { class: 'mod-list' });
modsField.append(modsLabel, modsList);
const nameSetLabel = el('span', { class: 'field__label' });
const nameSetSelect = el('select', { class: 'input' });
const nameSetField = el('label', { class: 'field' }, nameSetLabel, nameSetSelect);
nameSetSelect.addEventListener('change', () => {
nameSetId = nameSetSelect.value === '' ? null : nameSetSelect.value;
});
const mapLabel = el('span', { class: 'field__label' });
const editMapButton = el('button', { class: 'button', type: 'button' });
const mapHint = el('p', { class: 'hint' });
@@ -78,6 +86,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
el('label', { class: 'field' }, nameLabel, el('div', { class: 'field__row' }, nameInput, randomButton)),
el('div', { class: 'field' }, startLabel, el('div', { class: 'field__row' }, dateInput, timeInput)),
modsField,
nameSetField,
mapField,
error,
el('div', { class: 'dialog__actions' }, cancelButton, submitButton),
@@ -112,10 +121,25 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
const applyCatalog = (next: CatalogResponse): void => {
catalog = next;
currentMap = structuredClone(next.defaultMap);
const stillThere = next.nameSets.some((set) => set.defName === nameSetId);
nameSetId = stillThere ? nameSetId : (next.nameSets[0]?.defName ?? null);
paintNameSets(next);
paintMapHint();
setBusy(busy);
};
const paintNameSets = (next: CatalogResponse): void => {
nameSetSelect.replaceChildren();
for (const set of next.nameSets) {
const option = el('option', { text: set.label });
option.value = set.defName;
nameSetSelect.append(option);
}
nameSetSelect.value = nameSetId ?? '';
nameSetSelect.disabled = next.nameSets.length <= 1;
};
const reloadCatalog = async (): Promise<void> => {
try {
applyCatalog(await fetchCatalog(getLocale(), [...extraModIds]));
@@ -168,6 +192,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
const localize = (): void => {
title.textContent = t('newSchool');
modsLabel.textContent = t('modsTitle');
nameSetLabel.textContent = t('nameSetTitle');
nameLabel.textContent = t('schoolName');
startLabel.textContent = t('gameStart');
mapLabel.textContent = t('mapEditorTitle');
@@ -178,6 +203,9 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
submitButton.textContent = t('create');
cancelButton.textContent = t('cancel');
paintMapHint();
if (catalog !== null) {
paintNameSets(catalog);
}
};
localize();
@@ -227,6 +255,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
.create(nameInput.value.trim(), startDate, {
modIds: [...extraModIds],
map: currentMap,
nameSetId: nameSetId ?? undefined,
})
.then((school) => modal.close(school))
.catch((reason: unknown) => {
@@ -263,6 +292,8 @@ function describe(reason: unknown): string {
return t('errorUnknownMod');
case 'invalid-catalog':
return t('errorInvalidCatalog');
case 'unknown-name-set':
return t('errorUnknownNameSet');
default:
return reason.message;
}