Merge branch 'main' into phase/32-weather-warmth

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	src/HSchool.Simulation/School.cs
This commit is contained in:
Leonid Pershin
2026-08-20 04:03:42 +03:00
21 changed files with 848 additions and 29 deletions
+2
View File
@@ -303,6 +303,8 @@ export interface WornItem {
readonly color: string | null;
readonly colorLabel: string | null;
readonly layers: readonly DefLabel[];
readonly condition: number;
readonly conditionLabel: string;
}
export interface CarriedItem {
+28
View File
@@ -676,6 +676,34 @@ body {
font-variant-numeric: tabular-nums;
}
.people__garments {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 8px 16px;
}
.people__garment {
display: flex;
flex-direction: column;
gap: 2px;
}
.people__wear {
display: grid;
grid-template-columns: 1fr auto;
gap: 2px 8px;
font-size: 12px;
}
.people__wear-label {
grid-column: 1 / -1;
color: var(--text-muted);
}
.people__wear .people__need-track {
grid-column: 1 / -1;
}
.people__tags {
display: flex;
flex-wrap: wrap;
@@ -41,6 +41,8 @@ function card(overrides: Partial<PersonCard> = {}): PersonCard {
color: 'White',
colorLabel: 'Белый',
layers: [{ defName: 'Top', label: 'Верх' }],
condition: 1,
conditionLabel: 'целая',
},
],
carried: [
@@ -74,4 +76,58 @@ describe('renderPersonCard', () => {
expect(root.textContent).toContain('Учебник · Математика');
expect(root.querySelector('.people__tabs')).toBeNull();
});
it('shows the server condition caption, not a band inferred from the number', () => {
setLocale('ru');
const root = document.createElement('div');
renderPersonCard(
root,
card({
worn: [
{
defName: 'Shirt',
label: 'Рубашка',
color: 'White',
colorLabel: 'Белый',
layers: [{ defName: 'Top', label: 'Верх' }],
condition: 0.1,
conditionLabel: 'целая',
},
],
}),
() => {},
);
expect(root.textContent).toContain('целая');
expect(root.textContent).not.toContain('висит лохмотьями');
expect(root.querySelector('.people__wear-label')?.textContent).toBe('целая');
expect((root.querySelector('.people__need-fill') as HTMLElement | null)?.style.width).toBe('10%');
});
it('changes the caption when the payload label crosses a threshold', () => {
setLocale('ru');
const root = document.createElement('div');
renderPersonCard(root, card(), () => {});
expect(root.querySelector('.people__wear-label')?.textContent).toBe('целая');
root.replaceChildren();
renderPersonCard(
root,
card({
worn: [
{
defName: 'Shirt',
label: 'Рубашка',
color: 'White',
colorLabel: 'Белый',
layers: [{ defName: 'Top', label: 'Верх' }],
condition: 0.1,
conditionLabel: 'висит лохмотьями',
},
],
}),
() => {},
);
expect(root.querySelector('.people__wear-label')?.textContent).toBe('висит лохмотьями');
});
});
+20 -6
View File
@@ -119,22 +119,36 @@ function appendApparel(parent: HTMLElement, rows: PersonCard['worn']): void {
return;
}
const grid = el('dl', { class: 'people__pairs' });
const list = el('div', { class: 'people__garments' });
for (const row of rows) {
const layers = row.layers.map((layer) => layer.label).join(', ');
const color = row.colorLabel ?? row.color;
const value = color !== null && color.length > 0 ? `${row.label} · ${color}` : row.label;
grid.append(
const share = Math.max(0, Math.min(1, row.condition));
const fill = el('span', { class: 'people__need-fill' });
fill.style.width = `${Math.round(share * 100)}%`;
fill.classList.toggle('people__need-fill--low', share < 0.25);
list.append(
el(
'div',
{ class: 'people__pair' },
el('dt', { text: layers.length > 0 ? layers : row.label }),
el('dd', { text: value }),
{ class: 'people__garment' },
el(
'div',
{ class: 'people__pair' },
el('dt', { text: layers.length > 0 ? layers : row.label }),
el('dd', { text: value }),
),
el(
'div',
{ class: 'people__wear' },
el('span', { class: 'people__wear-label', text: row.conditionLabel }),
el('span', { class: 'people__need-track' }, fill),
),
),
);
}
parent.append(section(t('peopleApparel')), grid);
parent.append(section(t('peopleApparel')), list);
}
function appendCarry(parent: HTMLElement, card: PersonCard): void {