Merge branch 'review/slice-11-bd'
This commit is contained in:
@@ -33,3 +33,42 @@ HTTP: `GET /api/settings/swarmui` и create копируют два Krea-id (т
|
||||
- Пример JSON у `GET .../portrait/prompt` в `protocol.md` всё ещё с `cinematic photo`, `age 12` и `(low quality, worst quality:1.4)` — это HTTP промпта/сцены (этапы C–D), не каталог. Здесь не правился: соседний проход трогает `protocol.md`.
|
||||
- Хостовый GET/create на Krea-id не перегонялся в этом worktree.
|
||||
- Этапы B–D (тосты, пауза, сборщик сцены, `generateImage`) — не этот проход.
|
||||
|
||||
## Срез 11. Этапы B–D — лента, сцена и кнопка
|
||||
|
||||
- **Фазы:** 64–67
|
||||
- **Проверен на:** `a544d34`, 2026-08-21
|
||||
- **Пути:** `src/HSchool.Content/EventDefs.cs`, `src/HSchool.Content/EventDefValidator.cs`, `src/HSchool.Content/PromptContribution.cs`, `src/HSchool.Content/PromptContributionValidator.cs`, `src/HSchool.Simulation/EventSystem.cs`, `src/HSchool.Simulation/WorldEvent.cs`, `src/HSchool.Protocol/ProtocolCodec.cs`, `src/HSchool.Client/src/net/protocol.ts`, `docs/protocol.md`, `src/HSchool.Server/Game/NoticeBoard.cs`, `src/HSchool.Server/Game/PortraitSceneCollector.cs`, `src/HSchool.Server/Game/PortraitPromptBuilder.cs`, `src/HSchool.Server/Game/GenerationFailedPoster.cs`, `src/HSchool.Client/src/ui/noticeToasts.ts`, `src/HSchool.Client/src/ui/noticeModals.ts`, `src/HSchool.Client/src/ui/gameScreen.ts`
|
||||
- **Итог:** обещания 64–67 совпадают с кодом и тестами. Сокет v10 (`0x87` / `0x08`), info не в сейве, sticky паузит и переживает Open, сцена после одежды с бюджетом 6/2/2, кнопка `generateImage` — хозяин, успех не снимает паузу. Дописаны тесты на Play lock, неизвестный `action` и сборку «доска после одежды». Пример JSON у `GET .../portrait/prompt` приведён к прозе и сцене (замечание этапа A). Каталог Krea / состав моделей не ревьюился. Хостовые `GameSocketTests` / `NoticeImageApiTests` в этом проходе не гонялись (общий AppHost / живой сервер).
|
||||
|
||||
### 64. События и info-тост
|
||||
|
||||
`EventDef`: severity / pause / ttlMs / trigger / action, локаль как у других def. Ваниль `DayStarted` / `LessonStarted` — info, TTL 8000, `none`. `EventSystem` на крае рабочего утра и на смене периода урока — один факт на школу, не по классам; повторный тик в ту же минуту пуст. Работник сопоставляет def и шлёт `0x87`. Клиент — тост, `t(defName)`, клик → `0x08`, TTL гасит локально, колонки нет. Info не в сейве и не на Open. Круглый трип и байтовая раскладка на C# и TS, `PROTOCOL_VERSION` = 10.
|
||||
|
||||
`dotnet test tests/HSchool.Content.Tests --filter FullyQualifiedName~EventDefTests`: 3 зелёных (включая неизвестный action). `HSchool.Simulation.Tests` `WorldEventTests` и `HSchool.Protocol.Tests` `Notice_*` на месте, в этом проходе не перегонялись.
|
||||
|
||||
### 65. Варнинг и пауза
|
||||
|
||||
`Notice` с `pause` ставит `running=false`; `SetRunning(true)` игнор, пока sticky не пуст. Dismiss последнего не Play. Потолок 8, девятый не эмитится. Sticky в сейве, info нет. Dismiss pausing — хозяин (сокет `CanManage`, HTTP `403` `not-owner`). Модал без TTL, очередь из одного. Play серый, пока модал открыт.
|
||||
|
||||
### 66. Сцена в промпте
|
||||
|
||||
Блок `prompt` на Thing / Room / Territory / осадках; 121 символ и вес < 0 — ошибка загрузки. Ваниль: доска и снег непустые, `Chair` молчит. Сборщик на работнике: узел + fill + worn/held + погода только на улице, после одежды, вес ↓ затем `defName`. Бюджет в `SimulationOptions` (6 / 2 / 2). LoRA в `loras`, embed как `<embed:…>`. Сокет не бампили. `GET .../portrait/prompt` и Generate делят `PortraitPromptBuilder.Build` + `LookupBuildInput`.
|
||||
|
||||
### 67. Картинка с уведомления
|
||||
|
||||
`GenerationFailed` — error, pause, `generateImage`. Эмит из `PortraitService` при Unavailable/TimedOut, не при успехе и не при пустом personId. HTTP `POST .../notices/{id}/generate` — хозяин, тот же сборщик, kind с доски или `full`. Без personId — `400` `notice-cannot-generate`, Swarm не зовётся. Успех паузу не снимает. Кнопка только при action и personId ≠ 0; тост дня без кнопки.
|
||||
|
||||
Дописано:
|
||||
|
||||
- `UnknownAction_FailsTheCatalog` — `action: teleport` валит загрузку
|
||||
- `CollectThenBuild_PutsBlackboardAfterClothing_ChairRoomDoesNot` — фрагмент доски после одежды; стул не попадает
|
||||
- `disables Play while a pausing notice is open` — Play `disabled`, title `noticePauseLocked`
|
||||
|
||||
Код фаз не чинился, кроме примера JSON в `protocol.md` (устаревшие `age 12` / cinematic / `(word:1.4)`). Абзац каталога Krea не трогался.
|
||||
|
||||
### Открытое
|
||||
|
||||
- `generateImage` на info-тосте (не sticky): кнопка на клиенте есть, HTTP ищет только sticky → `404` `unknown-notice`. Ваниль так не делает (кнопка на `GenerationFailed`). Валидатор не требует `pause` у `generateImage`.
|
||||
- Хостовые `GameSocketTests` (утро, пауза, гость, F5) и `NoticeImageApiTests` в этом worktree не гонялись: живой сервер пользователя, общий AppHost.
|
||||
- Локаль тоста при смене языка: `NoticeToasts.localize` обновляет текст, не подпись кнопки «создать картинку» (тост живёт 8 с).
|
||||
|
||||
@@ -38,3 +38,4 @@
|
||||
| Портреты (вне очереди) 62 | `d093ea3` | [off-queue](off-queue/reviewed.md) |
|
||||
| Вне очереди. DX 49/54/55 | `ee452b2` | [off-queue](off-queue/reviewed.md) |
|
||||
| Срез 11. Этап A — только Krea 2 | `1002aeb` | [11-events](11-events/reviewed.md) |
|
||||
| Срез 11. Этапы B–D — лента, сцена и кнопка | `a544d34` | [11-events](11-events/reviewed.md) |
|
||||
|
||||
+2
-2
@@ -521,8 +521,8 @@ Empty `prompt` blocks are silent. Unknown school is `404` `unknown-school`; unkn
|
||||
```json
|
||||
{
|
||||
"kind": "avatar",
|
||||
"positive": "cinematic photo, …, close up, head and shoulders portrait, …, age 12, …",
|
||||
"negative": "(low quality, worst quality:1.4), …",
|
||||
"positive": "…, Head and shoulders., A teenage girl, 12 years old, with black hair, wearing a white shirt, a classroom blackboard with chalk notes",
|
||||
"negative": "…",
|
||||
"promptExtra": null,
|
||||
"presetId": "default",
|
||||
"presetLabel": "Default"
|
||||
|
||||
@@ -292,6 +292,35 @@ describe('GameScreen view restore', () => {
|
||||
expect(location.search).toContain('tab=people');
|
||||
});
|
||||
|
||||
it('disables Play while a pausing notice is open', () => {
|
||||
const screen = new GameScreen({
|
||||
onLeave: () => {},
|
||||
onSetRunning: () => {},
|
||||
onSetSpeed: () => {},
|
||||
onSkip: () => {},
|
||||
});
|
||||
document.body.append(screen.element);
|
||||
screen.show(school({ running: false }));
|
||||
|
||||
const play = screen.element.querySelector<HTMLButtonElement>('.clock__controls .button--icon');
|
||||
expect(play?.disabled).toBe(false);
|
||||
|
||||
screen.applyNotice({
|
||||
type: 'notice',
|
||||
id: 4,
|
||||
defName: 'GenerationFailed',
|
||||
severity: 2,
|
||||
pause: true,
|
||||
ttlMs: 0,
|
||||
personId: 0,
|
||||
action: 'none',
|
||||
});
|
||||
|
||||
expect(play?.disabled).toBe(true);
|
||||
expect(play?.title).toBe(t('noticePauseLocked'));
|
||||
expect(screen.element.querySelector('.notice-modal')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('keeps a guest on overview even when the URL asks for manage', () => {
|
||||
history.replaceState(null, '', '/?school=3&mode=manage');
|
||||
const screen = new GameScreen({
|
||||
|
||||
@@ -53,6 +53,24 @@ public class EventDefTests
|
||||
Assert.Contains("fullMoon", ex.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownAction_FailsTheCatalog()
|
||||
{
|
||||
var documents = PackDocuments.FromDirectory(
|
||||
CatalogLoader.CorePackId,
|
||||
Path.Combine(AppContext.BaseDirectory, "vanilla"))
|
||||
.Append(PackDocuments.Def(
|
||||
CatalogLoader.CorePackId,
|
||||
"events",
|
||||
"bad-action",
|
||||
"""{ "defName": "BadAct", "severity": "info", "ttlMs": 1, "trigger": "dayStart", "action": "teleport" }"""))
|
||||
.ToList();
|
||||
|
||||
var ex = Assert.Throws<ContentLoadException>(() => _loader.Load([CatalogLoader.CorePackId], documents));
|
||||
Assert.Contains("action", ex.Message, StringComparison.Ordinal);
|
||||
Assert.Contains("teleport", ex.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private DefCatalog LoadVanilla()
|
||||
{
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "vanilla");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using HSchool.Content;
|
||||
using HSchool.Server.Api;
|
||||
using HSchool.Server.Game;
|
||||
using HSchool.Simulation;
|
||||
|
||||
@@ -101,6 +102,56 @@ public class PortraitSceneCollectorTests
|
||||
Assert.DoesNotContain(scene.PositiveFragments, fragment => fragment.Contains("<embed:", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CollectThenBuild_PutsBlackboardAfterClothing_ChairRoomDoesNot()
|
||||
{
|
||||
var catalog = LoadCatalog();
|
||||
var map = SampleMap();
|
||||
var profile = SwarmUiPresetDefinition.CreateDefault().ToProfile(PortraitKind.Avatar);
|
||||
var card = SampleWornCard();
|
||||
|
||||
var withBoard = PortraitSceneCollector.Collect(
|
||||
catalog, map, "class-1", Precipitation.None, "TestClimate", [], [], Budget);
|
||||
var (positive, _) = PortraitPromptBuilder.Build(card, profile, PortraitKind.Avatar, scene: withBoard);
|
||||
var clothesAt = positive.IndexOf("wearing a white shirt", StringComparison.OrdinalIgnoreCase);
|
||||
var sceneAt = positive.IndexOf("blackboard", StringComparison.OrdinalIgnoreCase);
|
||||
Assert.True(clothesAt >= 0 && sceneAt > clothesAt);
|
||||
|
||||
var chairs = PortraitSceneCollector.Collect(
|
||||
catalog, map, "chairs", Precipitation.None, "TestClimate", [], [], Budget);
|
||||
var (without, _) = PortraitPromptBuilder.Build(card, profile, PortraitKind.Avatar, scene: chairs);
|
||||
Assert.DoesNotContain("blackboard", without, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("chair", without, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static PersonCardResponse SampleWornCard() =>
|
||||
new(
|
||||
"f0.c0",
|
||||
"Maria Ivanova",
|
||||
"Ivanova",
|
||||
"Maria",
|
||||
"",
|
||||
true,
|
||||
12,
|
||||
new DateTime(2000, 3, 14, 0, 0, 0, DateTimeKind.Utc),
|
||||
["student"],
|
||||
5,
|
||||
"A",
|
||||
"class-1",
|
||||
null,
|
||||
null,
|
||||
[new LabeledStatResponse("HairColor", "Hair colour", "Black")],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
new PersonFamilyResponse([], [], [], []),
|
||||
[new WornItemResponse("Shirt", "Shirt", "White", "White", [new DefLabelResponse(ApparelLayers.Top, "Top")], 1f, "new", [ApparelLayers.Underwear])],
|
||||
[],
|
||||
0f,
|
||||
0f);
|
||||
|
||||
private DefCatalog LoadCatalog()
|
||||
{
|
||||
return _loader.Load(
|
||||
|
||||
Reference in New Issue
Block a user