Merge review/slice-7bc-climate-wear (phases 32-37).
This commit is contained in:
+47
-3
@@ -256,7 +256,6 @@
|
|||||||
|
|
||||||
- `Dump_ReturnsPeopleNodesAndTimetable` принимает `NodeId is not null || Needs.Count > 0`.
|
- `Dump_ReturnsPeopleNodesAndTimetable` принимает `NodeId is not null || Needs.Count > 0`.
|
||||||
Нужды есть у всех, так что узлы на кампусе не проверяются. Усиливать до явки — срез 5.
|
Нужды есть у всех, так что узлы на кампусе не проверяются. Усиливать до явки — срез 5.
|
||||||
- Срез 7 (одежда) в работе: не писать в журнал как закрытый.
|
|
||||||
|
|
||||||
## Срез 7. Этап A — данные и гардероб
|
## Срез 7. Этап A — данные и гардероб
|
||||||
|
|
||||||
@@ -301,11 +300,56 @@
|
|||||||
`npm run build` падал; ключ перенесён, `peopleTabPortrait` рядом с остальными вкладками.
|
`npm run build` падал; ключ перенесён, `peopleTabPortrait` рядом с остальными вкладками.
|
||||||
- Повтор `using HSchool.Content` в `SchoolStore.cs` (CS0105).
|
- Повтор `using HSchool.Content` в `SchoolStore.cs` (CS0105).
|
||||||
|
|
||||||
Замечено рядом (фазы 32–37, не правилось):
|
Замечено рядом:
|
||||||
|
|
||||||
- Карточка уже с вкладками (`personCard.ts`, фаза 31 обещала один свиток — позже по
|
- Карточка уже с вкладками (`personCard.ts`, фаза 31 обещала один свиток — позже по
|
||||||
`design/inventory.md`); портрет/SwarmUI-строки в `strings.ts` — не этап A.
|
`design/inventory.md`); портрет/SwarmUI-строки в `strings.ts` — не этап A.
|
||||||
- `ResolveClimatePreset` в `SchoolWorker` при reload без сохранённого пресета берёт первый
|
- `ResolveClimatePreset` в `SchoolWorker` при reload без сохранённого пресета берёт первый
|
||||||
пресет страны (`schoolSeed: 0`, `rollIfOmitted: false`) — по дизайну для legacy; create
|
пресет страны (`schoolSeed: 0`, `rollIfOmitted: false`) — по дизайну для legacy; create
|
||||||
кидает пресет из `GameLoopService` до spawn.
|
кидает пресет из `GameLoopService` до spawn.
|
||||||
- Погода, тепло, уместность, журнал дня — срез 7B–C у соседнего ревьюера.
|
|
||||||
|
## Срез 7. Этап B–C — климат, жизнь одежды и правила
|
||||||
|
|
||||||
|
- **Фазы:** 32–37
|
||||||
|
- **Проверен на:** `<commit>`, 2026-08-20
|
||||||
|
- **Пути:** `src/HSchool.Simulation/{WeatherSampler,WarmthDecay,PlaceClimate,ApparelDresser,ApparelWear,MorningDress,PersonLogBrowser}.cs`,
|
||||||
|
`src/HSchool.People/LockerAssigner.cs`, `src/HSchool.Content/{Appropriateness,ClimatePreset,DressRules}.cs`,
|
||||||
|
`src/HSchool.Ai/ApparelGoals.cs`, `src/HSchool.Protocol`, `src/HSchool.Server/Api/{SchoolEndpoints,DressRulesModels}.cs`,
|
||||||
|
`src/HSchool.Server/Game/{PersonCardReader,SchoolWorker}.cs`, `src/HSchool.Client/src/{ui/dressRulesPanel.ts,ui/managementPanel.ts,format/weather.ts,net/api.ts}`,
|
||||||
|
`docs/protocol.md`, `tests/HSchool.Simulation.Tests`, `tests/HSchool.People.Tests/LockerAssignerTests.cs`,
|
||||||
|
`tests/HSchool.Content.Tests/{ClimatePresetTests,AppropriatenessTests,ChangingRoomMapTests}.cs`,
|
||||||
|
`tests/HSchool.AppHost.Tests/{PersonLogApiTests,DressRulesApiTests}.cs`, `tests/HSchool.Protocol.Tests`,
|
||||||
|
`src/HSchool.Client/src/ui/{personCard,dressRulesPanel,managementPanel}.test.ts`
|
||||||
|
- **Итог:** фазы 32–37 подтверждены; дописано 5 тестов симуляции; исправлены 2 бага переодевания и 2
|
||||||
|
регрессии тестов (не протокол)
|
||||||
|
|
||||||
|
Что подтверждено:
|
||||||
|
|
||||||
|
- **32–36:** погода детерминирована; skip выставляет утро; кадр часов v8 (27 байт) в трёх местах;
|
||||||
|
`Warmth` и техник; шкафчики; износ и утренняя замена; уместность и PE; вкладки карточки и HTTP-лог.
|
||||||
|
- **37:** вкладка «Правила» в «Управлении»; четыре комбобокса; GET/POST `dress-rules` с pending;
|
||||||
|
`unknown-form`/`unknown-color` → `400`; правило с завтра; `ColorAllowed` для `whiteTopBlackBottom`;
|
||||||
|
`dressRulesPanel.test.ts` — combobox без API до Apply, ошибка текстом; `ManagementPanel` не ломает штат
|
||||||
|
и сетку.
|
||||||
|
|
||||||
|
Дописано (32–36, прошлый проход):
|
||||||
|
|
||||||
|
- `AppropriatenessSimulationTests.MorningDress_StagesPeKitForTodaysLesson`
|
||||||
|
- `AppropriatenessSimulationTests.PeBreak_CompletesChangeBeforeLesson`
|
||||||
|
- `AppropriatenessSimulationTests.PeLesson_WearsPeThenReturnsToEveryday`
|
||||||
|
- `AppropriatenessSimulationTests.AfterPeBreak_ModeExpectsEveryday`
|
||||||
|
- `PresenceTests.PhysicalEducationBeforeLesson_GoesToChangingRoomOnTheBreak`
|
||||||
|
|
||||||
|
Исправлено:
|
||||||
|
|
||||||
|
- `PresenceSystem`: ходьба по duty не обрывает активный `ChangeClothes`.
|
||||||
|
- `ApparelPresence.ModeAt`: после перемены following PE — режим everyday.
|
||||||
|
- `managementPanel.test.ts`: панель штата скрывается через `hidden`, не удаляется из DOM.
|
||||||
|
- `PortraitApiTests.GetSwarmUiSettings_ReturnsPresets`: URL `/api/settings/swarmui` (CS1503).
|
||||||
|
- `dressRulesPanel.ts`: неиспользуемый импорт `clear` — TS6133 на `npm run build`.
|
||||||
|
|
||||||
|
Замечено рядом:
|
||||||
|
|
||||||
|
- `PhysicalEducationBeforeLesson_…` не гоняет полный цикл «зал с формой» — это в
|
||||||
|
`AppropriatenessSimulationTests.PeLesson_…`.
|
||||||
|
- Портреты/SwarmUI — вне среза 7, но `PortraitApiTests` чинился, чтобы AppHost-сборка не падала.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { fetchDressRules, setDressRules, type DressRulePair, type DressRules } from '../net/api.ts';
|
import { fetchDressRules, setDressRules, type DressRulePair, type DressRules } from '../net/api.ts';
|
||||||
import { t } from '../i18n/strings.ts';
|
import { t } from '../i18n/strings.ts';
|
||||||
import { clear, el } from './dom.ts';
|
import { el } from './dom.ts';
|
||||||
import {
|
import {
|
||||||
dressColorLabel,
|
dressColorLabel,
|
||||||
dressColorOptions,
|
dressColorOptions,
|
||||||
|
|||||||
@@ -293,7 +293,8 @@ describe('ManagementPanel rules tab', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rulesTab.click();
|
rulesTab.click();
|
||||||
expect(panel.listElement.querySelector('.staffing')).toBeNull();
|
expect(panel.listElement.querySelector('.staffing')?.hasAttribute('hidden')).toBe(true);
|
||||||
|
expect(panel.listElement.querySelector('.rules')?.hasAttribute('hidden')).toBe(false);
|
||||||
|
|
||||||
const staffTab = [...panel.listElement.querySelectorAll('.panel__tab')].find(
|
const staffTab = [...panel.listElement.querySelectorAll('.panel__tab')].find(
|
||||||
(button) => button.textContent === t('manageTabStaff'),
|
(button) => button.textContent === t('manageTabStaff'),
|
||||||
|
|||||||
@@ -166,9 +166,10 @@ internal static class ActivitySystem
|
|||||||
ApparelDresser.RedressPerson(school, person, mode, includeHome: false, todaySubjects: subjects);
|
ApparelDresser.RedressPerson(school, person, mode, includeHome: false, todaySubjects: subjects);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsChangeClothes(string actionId) =>
|
internal static bool IsChangeClothes(string? actionId) =>
|
||||||
actionId.Equals(ApparelActions.ChangeMale, StringComparison.Ordinal)
|
actionId is not null
|
||||||
|| actionId.Equals(ApparelActions.ChangeFemale, StringComparison.Ordinal);
|
&& (actionId.Equals(ApparelActions.ChangeMale, StringComparison.Ordinal)
|
||||||
|
|| actionId.Equals(ApparelActions.ChangeFemale, StringComparison.Ordinal));
|
||||||
|
|
||||||
private static int Occupied(School school, string nodeId, string thing)
|
private static int Occupied(School school, string nodeId, string thing)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -56,12 +56,29 @@ internal static class ApparelPresence
|
|||||||
return ApparelDresser.ModeForLesson(current?.Subject);
|
return ApparelDresser.ModeForLesson(current?.Subject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (slot.Kind == DaySlotKind.Break)
|
||||||
|
{
|
||||||
|
var justEnded = lessons.FirstOrDefault(lesson => lesson.Period == slot.Index);
|
||||||
|
if (justEnded is not null && ApparelDresser.ModeForLesson(justEnded.Subject) == ApparelMode.Pe)
|
||||||
|
{
|
||||||
|
return ApparelMode.Everyday;
|
||||||
|
}
|
||||||
|
|
||||||
var next = lessons.Where(lesson => lesson.Period > slot.Index).OrderBy(lesson => lesson.Period).FirstOrDefault();
|
var next = lessons.Where(lesson => lesson.Period > slot.Index).OrderBy(lesson => lesson.Period).FirstOrDefault();
|
||||||
if (next is not null)
|
if (next is not null)
|
||||||
{
|
{
|
||||||
return ApparelDresser.ModeForLesson(next.Subject);
|
return ApparelDresser.ModeForLesson(next.Subject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ApparelMode.Everyday;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextLesson = lessons.Where(lesson => lesson.Period > slot.Index).OrderBy(lesson => lesson.Period).FirstOrDefault();
|
||||||
|
if (nextLesson is not null)
|
||||||
|
{
|
||||||
|
return ApparelDresser.ModeForLesson(nextLesson.Subject);
|
||||||
|
}
|
||||||
|
|
||||||
var previous = lessons.Where(lesson => lesson.Period < slot.Index).OrderByDescending(lesson => lesson.Period).FirstOrDefault();
|
var previous = lessons.Where(lesson => lesson.Period < slot.Index).OrderByDescending(lesson => lesson.Period).FirstOrDefault();
|
||||||
if (previous is not null && ApparelDresser.ModeForLesson(previous.Subject) == ApparelMode.Pe)
|
if (previous is not null && ApparelDresser.ModeForLesson(previous.Subject) == ApparelMode.Pe)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -400,15 +400,18 @@ internal static class PresenceSystem
|
|||||||
state,
|
state,
|
||||||
(node, thing) => occupied.GetValueOrDefault((node, thing)));
|
(node, thing) => occupied.GetValueOrDefault((node, thing)));
|
||||||
|
|
||||||
|
var changing = activity.IsActive && ActivitySystem.IsChangeClothes(activity.ActionId);
|
||||||
|
|
||||||
if (decision.WalkTo is not null
|
if (decision.WalkTo is not null
|
||||||
&& !decision.WalkTo.Equals(presence.NodeId, StringComparison.Ordinal)
|
&& !decision.WalkTo.Equals(presence.NodeId, StringComparison.Ordinal)
|
||||||
&& activity.IsActive)
|
&& activity.IsActive
|
||||||
|
&& !changing)
|
||||||
{
|
{
|
||||||
activity = PersonActivity.Idle;
|
activity = PersonActivity.Idle;
|
||||||
}
|
}
|
||||||
|
|
||||||
intent = decision.Intent;
|
intent = decision.Intent;
|
||||||
if (decision.WalkTo is not null)
|
if (decision.WalkTo is not null && !changing)
|
||||||
{
|
{
|
||||||
presence = PresenceStepper.StartWalk(presence, walks, decision.WalkTo, headingHome: false);
|
presence = PresenceStepper.StartWalk(presence, walks, decision.WalkTo, headingHome: false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,9 @@ public class PortraitApiTests(AppHostFixture fixture)
|
|||||||
{
|
{
|
||||||
using var client = fixture.App.CreateHttpClient("server");
|
using var client = fixture.App.CreateHttpClient("server");
|
||||||
|
|
||||||
var settings = await client.GetFromJsonAsync<SwarmUiSettingsPayload>(TestContext.Current.CancellationToken);
|
var settings = await client.GetFromJsonAsync<SwarmUiSettingsPayload>(
|
||||||
|
"/api/settings/swarmui",
|
||||||
|
TestContext.Current.CancellationToken);
|
||||||
Assert.NotNull(settings);
|
Assert.NotNull(settings);
|
||||||
Assert.NotEmpty(settings!.Presets);
|
Assert.NotEmpty(settings!.Presets);
|
||||||
Assert.False(string.IsNullOrWhiteSpace(settings.ActivePresetId));
|
Assert.False(string.IsNullOrWhiteSpace(settings.ActivePresetId));
|
||||||
|
|||||||
@@ -55,6 +55,51 @@ public class AppropriatenessSimulationTests
|
|||||||
item => item.Location == ItemLocations.Worn && item.Def == "FurCoat");
|
item => item.Location == ItemLocations.Worn && item.Def == "FurCoat");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MorningDress_StagesPeKitForTodaysLesson()
|
||||||
|
{
|
||||||
|
var (school, pupilId) = StaffedPeClass();
|
||||||
|
using (school)
|
||||||
|
{
|
||||||
|
MorningDress.Apply(school);
|
||||||
|
var person = school.Roster!.People.Single(item => item.Id == pupilId);
|
||||||
|
Assert.Contains(
|
||||||
|
person.Items,
|
||||||
|
item => item.Def is "PeShirt" or "PeShorts"
|
||||||
|
&& item.Location is ItemLocations.Locker or ItemLocations.Bag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PeBreak_CompletesChangeBeforeLesson()
|
||||||
|
{
|
||||||
|
var (school, pupilId) = StaffedPeClass();
|
||||||
|
using (school)
|
||||||
|
{
|
||||||
|
MorningDress.Apply(school);
|
||||||
|
AdvanceTo(school, new DateTime(2012, 4, 3, 9, 30, 0, DateTimeKind.Utc));
|
||||||
|
var person = school.Roster!.People.Single(item => item.Id == pupilId);
|
||||||
|
Assert.Contains(
|
||||||
|
person.Items,
|
||||||
|
item => item.Location == ItemLocations.Worn && item.Def == "PeShirt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AfterPeBreak_ModeExpectsEveryday()
|
||||||
|
{
|
||||||
|
var (school, pupilId) = StaffedPeClass();
|
||||||
|
using (school)
|
||||||
|
{
|
||||||
|
MorningDress.Apply(school);
|
||||||
|
AdvanceTo(school, new DateTime(2012, 4, 3, 10, 12, 0, DateTimeKind.Utc));
|
||||||
|
var person = school.Roster!.People.Single(item => item.Id == pupilId);
|
||||||
|
var schoolClass = school.Roster.Classes.First(row => row.PupilIds.Contains(pupilId));
|
||||||
|
Assert.Equal(ApparelMode.Everyday, ApparelPresence.ModeAt(school, person, schoolClass));
|
||||||
|
Assert.True(ApparelDresser.CurrentIssues(school, person, ApparelMode.Everyday).HasFlag(ApparelIssue.PeForbidden));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PeBreak_PupilReachesOwnChangingRoom()
|
public void PeBreak_PupilReachesOwnChangingRoom()
|
||||||
{
|
{
|
||||||
@@ -71,6 +116,38 @@ public class AppropriatenessSimulationTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PeLesson_WearsPeThenReturnsToEveryday()
|
||||||
|
{
|
||||||
|
var (school, pupilId) = StaffedPeClass();
|
||||||
|
using (school)
|
||||||
|
{
|
||||||
|
MorningDress.Apply(school);
|
||||||
|
AdvanceTo(school, new DateTime(2012, 4, 3, 9, 35, 0, DateTimeKind.Utc));
|
||||||
|
var person = school.Roster!.People.Single(item => item.Id == pupilId);
|
||||||
|
Assert.Contains(
|
||||||
|
person.Items,
|
||||||
|
item => item.Location == ItemLocations.Worn && item.Def == "PeShirt");
|
||||||
|
Assert.Contains(
|
||||||
|
person.Items,
|
||||||
|
item => item.Location == ItemLocations.Worn && item.Def == "PeShorts");
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
person.Items,
|
||||||
|
item => item.Location == ItemLocations.Worn
|
||||||
|
&& item.Def is "Shirt" or "TShirt" or "Dress");
|
||||||
|
|
||||||
|
AdvanceTo(school, new DateTime(2012, 4, 3, 10, 25, 0, DateTimeKind.Utc));
|
||||||
|
person = school.Roster.People.Single(item => item.Id == pupilId);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
person.Items,
|
||||||
|
item => item.Location == ItemLocations.Worn && item.Def == "PeShirt");
|
||||||
|
Assert.Contains(
|
||||||
|
person.Items,
|
||||||
|
item => item.Location == ItemLocations.Worn
|
||||||
|
&& item.Def is "Shirt" or "TShirt" or "Dress" or "Skirt" or "Pants" or "Jeans" or "Trousers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Boy_DoesNotEnterGirlsChangingRoom()
|
public void Boy_DoesNotEnterGirlsChangingRoom()
|
||||||
{
|
{
|
||||||
@@ -258,6 +335,7 @@ public class AppropriatenessSimulationTests
|
|||||||
[
|
[
|
||||||
new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 1),
|
new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 1),
|
||||||
new LessonPlacement(schoolClass.Id, "PhysicalEducation", "t2", "gym-hall", Day: 1, Period: 2),
|
new LessonPlacement(schoolClass.Id, "PhysicalEducation", "t2", "gym-hall", Day: 1, Period: 2),
|
||||||
|
new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 3),
|
||||||
],
|
],
|
||||||
[]));
|
[]));
|
||||||
school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 10_000);
|
school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 10_000);
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ public class PresenceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PhysicalEducationAfterAClassroomLesson_ReachesTheGymOnTheBreak()
|
public void PhysicalEducationBeforeLesson_GoesToChangingRoomOnTheBreak()
|
||||||
{
|
{
|
||||||
var (catalog, map) = Vanilla();
|
var (catalog, map) = Vanilla();
|
||||||
var walks = WalkGraph.Build(catalog, map);
|
var walks = WalkGraph.Build(catalog, map);
|
||||||
@@ -78,9 +78,11 @@ public class PresenceTests
|
|||||||
var (school, _, pupilId) = StaffedFirstFloorClass();
|
var (school, _, pupilId) = StaffedFirstFloorClass();
|
||||||
using (school)
|
using (school)
|
||||||
{
|
{
|
||||||
AdvanceTo(school, new DateTime(2012, 4, 3, 9, 25, 0, DateTimeKind.Utc));
|
AdvanceTo(school, new DateTime(2012, 4, 3, 9, 24, 0, DateTimeKind.Utc));
|
||||||
var row = school.CapturePresence().Single(item => item.PersonId == pupilId);
|
var row = school.CapturePresence().Single(item => item.PersonId == pupilId);
|
||||||
Assert.Equal("gym-hall", row.NodeId);
|
var person = school.Roster!.People.Single(item => item.Id == pupilId);
|
||||||
|
var room = person.Female ? "girls-changing-room" : "boys-changing-room";
|
||||||
|
Assert.Equal(room, row.NodeId);
|
||||||
Assert.Empty(row.Path);
|
Assert.Empty(row.Path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user