Merge phase 26 school seed.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -57,6 +57,10 @@ const ru = {
|
||||
editMap: 'Редактировать карту',
|
||||
mapDefaultHint: 'Будет использована карта по умолчанию.',
|
||||
mapEditedHint: 'Карта изменена.',
|
||||
schoolSeed: 'Сид {seed}',
|
||||
schoolSeedLabel: 'Сид',
|
||||
schoolSeedHint: 'Необязательно. Чужой сид даёт ту же школу; пустое поле бросает свой.',
|
||||
schoolSeedInvalid: 'Сид должен быть целым числом.',
|
||||
resetMap: 'Сбросить к умолчанию',
|
||||
done: 'Готово',
|
||||
editorStructure: 'Структура',
|
||||
@@ -270,6 +274,10 @@ const en: Messages = {
|
||||
editMap: 'Edit map',
|
||||
mapDefaultHint: 'The default map will be used.',
|
||||
mapEditedHint: 'The map has been edited.',
|
||||
schoolSeed: 'Seed {seed}',
|
||||
schoolSeedLabel: 'Seed',
|
||||
schoolSeedHint: 'Optional. A shared seed recreates the same people; leave blank to roll one.',
|
||||
schoolSeedInvalid: 'The seed must be a whole number.',
|
||||
resetMap: 'Reset to default',
|
||||
done: 'Done',
|
||||
editorStructure: 'Structure',
|
||||
|
||||
@@ -10,7 +10,10 @@ export interface School {
|
||||
readonly gameTime: string;
|
||||
readonly running: boolean;
|
||||
readonly speedIndex: number;
|
||||
readonly speedIndex: number;
|
||||
readonly modIds?: readonly string[];
|
||||
/** Roster generator seed. Independent of `id`; share it to recreate the same people. */
|
||||
readonly seed: number;
|
||||
}
|
||||
|
||||
export interface SchoolsResponse {
|
||||
@@ -53,6 +56,7 @@ export interface CreateSchoolOptions {
|
||||
readonly map?: MapLayout;
|
||||
readonly nameSetId?: string;
|
||||
readonly nativeLanguage?: string;
|
||||
readonly seed?: number;
|
||||
}
|
||||
|
||||
export async function createSchool(
|
||||
@@ -70,6 +74,7 @@ export async function createSchool(
|
||||
map: extras.map ?? null,
|
||||
nameSetId: extras.nameSetId ?? null,
|
||||
nativeLanguage: extras.nativeLanguage ?? null,
|
||||
seed: extras.seed ?? null,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -125,6 +125,21 @@ body {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.screen__heading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.screen__seed {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.screen__actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,12 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
||||
nativeLanguageId = nativeSelect.value === '' ? null : nativeSelect.value;
|
||||
});
|
||||
|
||||
const seedLabel = el('span', { class: 'field__label' });
|
||||
const seedInput = el('input', { class: 'input', type: 'text' });
|
||||
seedInput.inputMode = 'numeric';
|
||||
const seedHint = el('p', { class: 'hint' });
|
||||
const seedField = el('label', { class: 'field' }, seedLabel, seedInput, seedHint);
|
||||
|
||||
const mapLabel = el('span', { class: 'field__label' });
|
||||
const editMapButton = el('button', { class: 'button', type: 'button' });
|
||||
const mapHint = el('p', { class: 'hint' });
|
||||
@@ -107,6 +113,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
||||
nameSetField,
|
||||
nativeField,
|
||||
mapField,
|
||||
seedField,
|
||||
error,
|
||||
el('div', { class: 'dialog__actions' }, cancelButton, submitButton),
|
||||
);
|
||||
@@ -120,6 +127,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
||||
randomButton.toggleAttribute('disabled', value);
|
||||
nativeRandomButton.toggleAttribute('disabled', value || languagesOf(catalog, nameSetId).length <= 1);
|
||||
editMapButton.toggleAttribute('disabled', waiting);
|
||||
seedInput.toggleAttribute('disabled', value);
|
||||
};
|
||||
|
||||
const showError = (message: string): void => {
|
||||
@@ -238,6 +246,9 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
||||
nativeLabel.textContent = t('nativeLanguageTitle');
|
||||
nativeRandomButton.textContent = t('randomName');
|
||||
nativeRandomButton.title = t('randomNativeTitle');
|
||||
seedLabel.textContent = t('schoolSeedLabel');
|
||||
seedInput.placeholder = t('schoolSeedLabel');
|
||||
seedHint.textContent = t('schoolSeedHint');
|
||||
nameLabel.textContent = t('schoolName');
|
||||
startLabel.textContent = t('gameStart');
|
||||
mapLabel.textContent = t('mapEditorTitle');
|
||||
@@ -310,6 +321,12 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
||||
return;
|
||||
}
|
||||
|
||||
const seed = parseSeed(seedInput.value);
|
||||
if (seedInput.value.trim() !== '' && seed === null) {
|
||||
showError(t('schoolSeedInvalid'));
|
||||
return;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
options
|
||||
.create(nameInput.value.trim(), startDate, {
|
||||
@@ -317,6 +334,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
|
||||
map: currentMap,
|
||||
nameSetId: nameSetId ?? undefined,
|
||||
nativeLanguage: nativeLanguageId ?? undefined,
|
||||
seed: seed ?? undefined,
|
||||
})
|
||||
.then((school) => modal.close(school))
|
||||
.catch((reason: unknown) => {
|
||||
@@ -343,6 +361,24 @@ function mapsEqual(left: MapLayout, right: MapLayout): boolean {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
|
||||
function parseSeed(value: string): number | null {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!/^-?\d+$/.test(trimmed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number(trimmed);
|
||||
if (!Number.isSafeInteger(parsed) || parsed < -2147483648 || parsed > 2147483647) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function describe(reason: unknown): string {
|
||||
if (!(reason instanceof ApiError)) {
|
||||
return t('serverUnavailable');
|
||||
|
||||
@@ -34,6 +34,7 @@ export class GameScreen {
|
||||
private readonly root = el('section', { class: 'screen game' });
|
||||
private readonly backButton = el('button', { class: 'button', type: 'button' });
|
||||
private readonly schoolName = el('h1', { class: 'screen__title' });
|
||||
private readonly schoolSeed = el('p', { class: 'screen__seed' });
|
||||
private readonly time = el('p', { class: 'clock__time', text: '--:--' });
|
||||
private readonly date = el('p', { class: 'clock__date' });
|
||||
private readonly weekday = el('p', { class: 'clock__weekday' });
|
||||
@@ -79,6 +80,7 @@ export class GameScreen {
|
||||
private directoryToken = 0;
|
||||
private selectedId: string | null = null;
|
||||
private schoolId: number | null = null;
|
||||
private peopleSeed: number | null = null;
|
||||
private running = false;
|
||||
private lastGameTime: Date | null = null;
|
||||
private lastSpeedIndex = 0;
|
||||
@@ -102,7 +104,7 @@ export class GameScreen {
|
||||
this.skipButton.hidden = true;
|
||||
|
||||
this.root.append(
|
||||
el('header', { class: 'screen__header' }, this.backButton, this.schoolName),
|
||||
el('header', { class: 'screen__header' }, this.backButton, el('div', { class: 'screen__heading' }, this.schoolName, this.schoolSeed)),
|
||||
el(
|
||||
'div',
|
||||
{ class: 'clockbar' },
|
||||
@@ -188,6 +190,7 @@ export class GameScreen {
|
||||
}
|
||||
|
||||
this.paintSkip();
|
||||
this.paintSeed();
|
||||
this.people.setLocate((id) => this.placeOf(id));
|
||||
this.management.setLocate((id) => this.placeOf(id));
|
||||
}
|
||||
@@ -195,7 +198,9 @@ export class GameScreen {
|
||||
/** Called when the screen opens, before the first clock frame and snapshot arrive. */
|
||||
show(school: School): void {
|
||||
this.schoolId = school.id;
|
||||
this.peopleSeed = school.seed;
|
||||
this.schoolName.textContent = school.name;
|
||||
this.paintSeed();
|
||||
this.nodes = [];
|
||||
this.presence = null;
|
||||
this.directory = new Map();
|
||||
@@ -381,6 +386,10 @@ export class GameScreen {
|
||||
: '';
|
||||
}
|
||||
|
||||
private paintSeed(): void {
|
||||
this.schoolSeed.textContent = this.peopleSeed === null ? '' : t('schoolSeed', { seed: this.peopleSeed });
|
||||
}
|
||||
|
||||
private paintTreeLabels(): void {
|
||||
for (const [id, button] of this.treeButtons) {
|
||||
const node = this.nodes.find((candidate) => candidate.id === id);
|
||||
|
||||
Reference in New Issue
Block a user