Remove PixiJS dependencies and references from the project, transitioning to a plain DOM approach for UI rendering. Update documentation to reflect these changes and enhance the layout and structure of the game screen for improved user experience.
This commit is contained in:
@@ -115,8 +115,7 @@ say so explicitly in the change description.
|
||||
bare `<PackageReference Include="..." />` in the project.
|
||||
- Transitive pinning is on, so a downgrade warning means you bump the central version rather than
|
||||
adding a per-project override.
|
||||
- Keep the dependency count low. Arch, Aspire and the test runners are the whole budget; `pixi.js`
|
||||
is installed but not imported yet — it is there for the game view inside a school.
|
||||
- Keep the dependency count low. Arch, Aspire and the test runners are the whole budget.
|
||||
|
||||
## Things that will bite you
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ school screen with a running game clock. The school itself is still empty.
|
||||
| Server | .NET 10, ASP.NET Core |
|
||||
| Simulation | [Arch](https://github.com/genaray/Arch) ECS, fixed 20 Hz tick |
|
||||
| Transport | REST for the menu, raw WebSocket + binary protocol for the clock |
|
||||
| Client | TypeScript, Vite, plain DOM ([PixiJS 8](https://pixijs.com/) is installed for the game view that comes next) |
|
||||
| Client | TypeScript, Vite, plain DOM |
|
||||
| Orchestration | .NET Aspire 13 |
|
||||
| Tests | xUnit v3, Vitest, `Aspire.Hosting.Testing` |
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ QSP: не «полотно текста + список действий», а п
|
||||
Анимация пока только переходы панелей/списков и живые цифры (часы, счётчики). Графики —
|
||||
когда появятся числа, которые ими стоит показывать, не раньше.
|
||||
|
||||
Клиент остаётся plain DOM + CSS. `pixi.js` для этого не нужен и в этом горизонте его стоит
|
||||
убрать из зависимостей.
|
||||
Клиент остаётся plain DOM + CSS. WebGL-сцены нет.
|
||||
|
||||
Симуляция по-прежнему на сервере: ученики и комнаты могут существовать как данные в Arch ECS,
|
||||
даже если клиент рисует строки списка, а не человечков.
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
## Задачи
|
||||
|
||||
- [ ] Удалить `pixi.js` из `src/HSchool.Client/package.json` и обновить lockfile
|
||||
- [ ] Убрать обещание Pixi из `README.md` и `AGENTS.md` (клиент: Vite + TypeScript + DOM)
|
||||
- [x] Удалить `pixi.js` из `src/HSchool.Client/package.json` и обновить lockfile
|
||||
- [x] Убрать обещание Pixi из `README.md` и `AGENTS.md` (клиент: Vite + TypeScript + DOM)
|
||||
|
||||
## Критерий готовности
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
|
||||
## Задачи
|
||||
|
||||
- [ ] Три области: дерево карты, общие события, выбранная локация — плюс часы и скорость
|
||||
- [ ] События: заголовок и пустое состояние, без выдуманных записей
|
||||
- [ ] Локация: секции имя, предметы, персонажи, действия на месте, должности — пустые, не ломаются
|
||||
- [ ] Клик по дереву переключает панель локации на клиенте (пока узлы-заглушки или пустое дерево)
|
||||
- [ ] Переключение языка обновляет подписи панелей и пустых состояний (`t(...)`)
|
||||
- [x] Три области: дерево карты, общие события, выбранная локация — плюс часы и скорость
|
||||
- [x] События: заголовок и пустое состояние, без выдуманных записей
|
||||
- [x] Локация: секции имя, предметы, персонажи, действия на месте, должности — пустые, не ломаются
|
||||
- [x] Клик по дереву переключает панель локации на клиенте (пока узлы-заглушки или пустое дерево)
|
||||
- [x] Переключение языка обновляет подписи панелей и пустых состояний (`t(...)`)
|
||||
|
||||
## Критерий готовности
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
| Фаза | Статус | Зачем |
|
||||
| --- | --- | --- |
|
||||
| [0. Убрать PixiJS](00-drop-pixi.md) | ⬜ | Сцена не планируется |
|
||||
| [1. Оболочка менеджера](01-manager-shell.md) | ⬜ | Панели с секциями среза, пока без данных |
|
||||
| [0. Убрать PixiJS](00-drop-pixi.md) | ✅ | Сцена не планируется |
|
||||
| [1. Оболочка менеджера](01-manager-shell.md) | ✅ | Панели с секциями среза, пока без данных |
|
||||
| [2. Работник школы и диск](02-school-worker.md) | ⬜ | Поток + World + сейв — основа |
|
||||
| [3. Каталог def и карта](03-defs-map.md) | ⬜ | JSONC, core, валидация раскладки |
|
||||
| [4. Моды и редактор в create](04-create-editor.md) | ⬜ | Выбор модов, карта в POST, снимок при открытии |
|
||||
|
||||
Generated
-104
@@ -7,9 +7,6 @@
|
||||
"": {
|
||||
"name": "hschool-client",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"pixi.js": "^8.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.1",
|
||||
"typescript": "~5.9.3",
|
||||
@@ -37,12 +34,6 @@
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/colord": {
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz",
|
||||
"integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz",
|
||||
@@ -313,12 +304,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/earcut": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-3.0.0.tgz",
|
||||
"integrity": "sha512-k/9fOUGO39yd2sCjrbAJvGDEQvRwRnQIZlBz43roGwUZo5SHAmyVvSFyaVVZkicRVCaDXPKlbxrUcBuJoSWunQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
@@ -449,21 +434,6 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@webgpu/types": {
|
||||
"version": "0.1.71",
|
||||
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.71.tgz",
|
||||
"integrity": "sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.14",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.14.tgz",
|
||||
"integrity": "sha512-T4EDRUBVZYRldYApjEJiU0e1stYWaRAX7CuSnKzrpwdZKo53zGV8/pqfzV6FfwNl9YThD2OumQYvqtvjvgG7aQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
@@ -501,12 +471,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/earcut": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.3.tgz",
|
||||
"integrity": "sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
|
||||
@@ -524,12 +488,6 @@
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
|
||||
@@ -573,27 +531,6 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gifuct-js": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz",
|
||||
"integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-binary-schema-parser": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/ismobilejs": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz",
|
||||
"integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-binary-schema-parser": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz",
|
||||
"integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
|
||||
@@ -898,12 +835,6 @@
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-svg-path": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.2.0.tgz",
|
||||
"integrity": "sha512-Tf7FFIrguPKQwzD4pWnYkR2VOv3raoHeKED80Bm+BYHI3KxC8KsgsGC5+fSMzAGDA6UEk4bHvmi+RsjmL3khpg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
@@ -931,32 +862,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pixi.js": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.19.0.tgz",
|
||||
"integrity": "sha512-pq1O6emA/GFjjeF+8d3Pb5t7knD8FsnfWGqQcRjYjsqFZ7QdzG1XgjLDUu0DFJRbafjV5+g8iNLFBx0b9649lg==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"examples",
|
||||
"playground"
|
||||
],
|
||||
"dependencies": {
|
||||
"@pixi/colord": "^2.9.6",
|
||||
"@types/earcut": "^3.0.0",
|
||||
"@webgpu/types": "^0.1.69",
|
||||
"@xmldom/xmldom": "^0.8.13",
|
||||
"earcut": "^3.0.2",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"gifuct-js": "^2.1.2",
|
||||
"ismobilejs": "^1.1.1",
|
||||
"parse-svg-path": "^0.2.0",
|
||||
"tiny-lru": "^11.4.7"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/pixijs"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.26",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
||||
@@ -1050,15 +955,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-lru": {
|
||||
"version": "11.4.7",
|
||||
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.7.tgz",
|
||||
"integrity": "sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
|
||||
@@ -14,9 +14,6 @@
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"pixi.js": "^8.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.1",
|
||||
"typescript": "~5.9.3",
|
||||
|
||||
@@ -40,9 +40,25 @@ const ru = {
|
||||
errorInvalidStartDate: 'Дата начала вне допустимого диапазона.',
|
||||
|
||||
backToMenu: '← В главное меню',
|
||||
emptySchoolHint: 'Школа пока пуста — здесь появится сама игра.',
|
||||
pause: 'Пауза',
|
||||
resume: 'Продолжить',
|
||||
|
||||
mapTitle: 'Карта',
|
||||
eventsTitle: 'События',
|
||||
eventsEmpty: 'Пока ничего не происходит.',
|
||||
locationName: 'Локация',
|
||||
locationItems: 'Предметы',
|
||||
locationCharacters: 'Персонажи',
|
||||
locationActivities: 'Сейчас',
|
||||
locationPositions: 'Должности',
|
||||
itemsEmpty: 'Пусто.',
|
||||
charactersEmpty: 'Никого нет.',
|
||||
activitiesEmpty: 'Ничего не происходит.',
|
||||
positionsEmpty: 'Нет должностей.',
|
||||
stubTerritory: 'Двор',
|
||||
stubBuilding: 'Главный корпус',
|
||||
stubFloor: '1 этаж',
|
||||
stubRoom: 'Кабинет директора',
|
||||
} as const;
|
||||
|
||||
type Messages = { [K in keyof typeof ru]: string };
|
||||
@@ -87,9 +103,25 @@ const en: Messages = {
|
||||
errorInvalidStartDate: 'The start date is outside the allowed range.',
|
||||
|
||||
backToMenu: '← Main menu',
|
||||
emptySchoolHint: 'The school is empty for now — the game itself will appear here.',
|
||||
pause: 'Pause',
|
||||
resume: 'Resume',
|
||||
|
||||
mapTitle: 'Map',
|
||||
eventsTitle: 'Events',
|
||||
eventsEmpty: 'Nothing is happening yet.',
|
||||
locationName: 'Location',
|
||||
locationItems: 'Items',
|
||||
locationCharacters: 'People',
|
||||
locationActivities: 'Now',
|
||||
locationPositions: 'Positions',
|
||||
itemsEmpty: 'Empty.',
|
||||
charactersEmpty: 'Nobody here.',
|
||||
activitiesEmpty: 'Nothing is happening.',
|
||||
positionsEmpty: 'No positions.',
|
||||
stubTerritory: 'Yard',
|
||||
stubBuilding: 'Main building',
|
||||
stubFloor: 'Floor 1',
|
||||
stubRoom: "Principal's office",
|
||||
};
|
||||
|
||||
const catalogs: Record<Locale, Messages> = { ru, en };
|
||||
|
||||
@@ -24,7 +24,7 @@ body {
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 900px;
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 20px 72px;
|
||||
}
|
||||
@@ -191,6 +191,101 @@ body {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.screen.game .clock {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.screen.game .clock__time {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
/* Manager */
|
||||
|
||||
.manager {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 1fr) minmax(200px, 1fr) minmax(240px, 1.35fr);
|
||||
gap: 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
min-width: 0;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
background: var(--surface-raised);
|
||||
}
|
||||
|
||||
.panel__title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.panel__name {
|
||||
margin: 0 0 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.panel__section {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.panel__section-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.panel__empty {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tree {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.tree .tree {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.tree__button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tree__button:hover {
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.tree__button--active {
|
||||
border-color: var(--accent);
|
||||
background: rgba(76, 201, 240, 0.16);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.manager {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
|
||||
.button {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CLOCK_SPEEDS, type ClockMessage } from '../net/protocol.ts';
|
||||
import { formatGameDate, formatGameTimeOfDay, formatGameWeekday } from '../format/gameTime.ts';
|
||||
import { t } from '../i18n/strings.ts';
|
||||
import { t, type MessageKey } from '../i18n/strings.ts';
|
||||
import type { School } from '../net/api.ts';
|
||||
import { el } from './dom.ts';
|
||||
|
||||
@@ -10,11 +10,37 @@ interface GameScreenOptions {
|
||||
readonly onSetSpeed: (speedIndex: number) => void;
|
||||
}
|
||||
|
||||
interface StubNode {
|
||||
readonly id: string;
|
||||
readonly labelKey: MessageKey;
|
||||
readonly children?: readonly StubNode[];
|
||||
}
|
||||
|
||||
/** Placeholder tree until the server sends a map snapshot. Clicking only filters the location panel. */
|
||||
const STUB_MAP: StubNode = {
|
||||
id: 'territory',
|
||||
labelKey: 'stubTerritory',
|
||||
children: [
|
||||
{
|
||||
id: 'building',
|
||||
labelKey: 'stubBuilding',
|
||||
children: [
|
||||
{
|
||||
id: 'floor',
|
||||
labelKey: 'stubFloor',
|
||||
children: [{ id: 'room', labelKey: 'stubRoom' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const SPEED_LABELS = ['×½', '×1', '×2', '×3', '×4'];
|
||||
|
||||
/**
|
||||
* The inside of a school: for now the calendar and the controls that drive it. The server owns
|
||||
* the clock, so every button only sends an intent and the display follows the next clock frame.
|
||||
* The inside of a school: calendar controls plus the manager shell. Data for the tree and
|
||||
* location lists arrives in a later phase; until then the panels stay empty except for stubs
|
||||
* that prove the tree filters the location pane on the client.
|
||||
*/
|
||||
export class GameScreen {
|
||||
private readonly root = el('section', { class: 'screen game' });
|
||||
@@ -23,10 +49,26 @@ export class GameScreen {
|
||||
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' });
|
||||
private readonly emptyHint = el('p', { class: 'hint' });
|
||||
private readonly playPauseButton = el('button', { class: 'button button--icon', type: 'button', text: '▶' });
|
||||
private readonly speedButtons: HTMLButtonElement[];
|
||||
|
||||
private readonly mapTitle = el('h2', { class: 'panel__title' });
|
||||
private readonly tree = el('ul', { class: 'tree' });
|
||||
private readonly eventsTitle = el('h2', { class: 'panel__title' });
|
||||
private readonly eventsEmpty = el('p', { class: 'panel__empty' });
|
||||
private readonly locationTitle = el('h2', { class: 'panel__title' });
|
||||
private readonly locationName = el('p', { class: 'panel__name' });
|
||||
private readonly itemsHeading = el('h3', { class: 'panel__section-title' });
|
||||
private readonly itemsEmpty = el('p', { class: 'panel__empty' });
|
||||
private readonly charactersHeading = el('h3', { class: 'panel__section-title' });
|
||||
private readonly charactersEmpty = el('p', { class: 'panel__empty' });
|
||||
private readonly activitiesHeading = el('h3', { class: 'panel__section-title' });
|
||||
private readonly activitiesEmpty = el('p', { class: 'panel__empty' });
|
||||
private readonly positionsHeading = el('h3', { class: 'panel__section-title' });
|
||||
private readonly positionsEmpty = el('p', { class: 'panel__empty' });
|
||||
|
||||
private readonly treeButtons = new Map<string, HTMLButtonElement>();
|
||||
private selectedId = STUB_MAP.id;
|
||||
private running = false;
|
||||
private lastGameTime: Date | null = null;
|
||||
private lastSpeedIndex = 0;
|
||||
@@ -44,6 +86,8 @@ export class GameScreen {
|
||||
this.backButton.addEventListener('click', options.onLeave);
|
||||
this.playPauseButton.addEventListener('click', () => options.onSetRunning(!this.running));
|
||||
|
||||
this.buildTree(this.tree, [STUB_MAP], 0);
|
||||
|
||||
this.root.append(
|
||||
el('header', { class: 'screen__header' }, this.backButton, this.schoolName),
|
||||
el(
|
||||
@@ -54,7 +98,22 @@ export class GameScreen {
|
||||
this.weekday,
|
||||
el('div', { class: 'clock__controls' }, this.playPauseButton, ...this.speedButtons),
|
||||
),
|
||||
this.emptyHint,
|
||||
el(
|
||||
'div',
|
||||
{ class: 'manager' },
|
||||
el('section', { class: 'panel' }, this.mapTitle, this.tree),
|
||||
el('section', { class: 'panel' }, this.eventsTitle, this.eventsEmpty),
|
||||
el(
|
||||
'section',
|
||||
{ class: 'panel' },
|
||||
this.locationTitle,
|
||||
this.locationName,
|
||||
el('div', { class: 'panel__section' }, this.itemsHeading, this.itemsEmpty),
|
||||
el('div', { class: 'panel__section' }, this.charactersHeading, this.charactersEmpty),
|
||||
el('div', { class: 'panel__section' }, this.activitiesHeading, this.activitiesEmpty),
|
||||
el('div', { class: 'panel__section' }, this.positionsHeading, this.positionsEmpty),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
this.localize();
|
||||
@@ -66,7 +125,21 @@ export class GameScreen {
|
||||
|
||||
localize(): void {
|
||||
this.backButton.textContent = t('backToMenu');
|
||||
this.emptyHint.textContent = t('emptySchoolHint');
|
||||
this.mapTitle.textContent = t('mapTitle');
|
||||
this.eventsTitle.textContent = t('eventsTitle');
|
||||
this.eventsEmpty.textContent = t('eventsEmpty');
|
||||
this.locationTitle.textContent = t('locationName');
|
||||
this.itemsHeading.textContent = t('locationItems');
|
||||
this.itemsEmpty.textContent = t('itemsEmpty');
|
||||
this.charactersHeading.textContent = t('locationCharacters');
|
||||
this.charactersEmpty.textContent = t('charactersEmpty');
|
||||
this.activitiesHeading.textContent = t('locationActivities');
|
||||
this.activitiesEmpty.textContent = t('activitiesEmpty');
|
||||
this.positionsHeading.textContent = t('locationPositions');
|
||||
this.positionsEmpty.textContent = t('positionsEmpty');
|
||||
|
||||
this.paintTreeLabels(STUB_MAP);
|
||||
this.paintSelection();
|
||||
|
||||
if (this.lastGameTime !== null) {
|
||||
this.applyClock(this.lastGameTime, this.running, this.lastSpeedIndex);
|
||||
@@ -85,6 +158,53 @@ export class GameScreen {
|
||||
this.applyClock(clock.gameTime, clock.running, clock.speedIndex);
|
||||
}
|
||||
|
||||
private buildTree(list: HTMLUListElement, nodes: readonly StubNode[], depth: number): void {
|
||||
for (const node of nodes) {
|
||||
const item = el('li', { class: 'tree__node' });
|
||||
const button = el('button', {
|
||||
class: 'tree__button',
|
||||
type: 'button',
|
||||
onClick: () => this.select(node.id),
|
||||
});
|
||||
button.style.paddingLeft = `${8 + depth * 14}px`;
|
||||
this.treeButtons.set(node.id, button);
|
||||
item.append(button);
|
||||
|
||||
if (node.children !== undefined && node.children.length > 0) {
|
||||
const nested = el('ul', { class: 'tree' });
|
||||
this.buildTree(nested, node.children, depth + 1);
|
||||
item.append(nested);
|
||||
}
|
||||
|
||||
list.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
private paintTreeLabels(node: StubNode): void {
|
||||
const button = this.treeButtons.get(node.id);
|
||||
if (button !== undefined) {
|
||||
button.textContent = t(node.labelKey);
|
||||
}
|
||||
|
||||
for (const child of node.children ?? []) {
|
||||
this.paintTreeLabels(child);
|
||||
}
|
||||
}
|
||||
|
||||
private select(id: string): void {
|
||||
this.selectedId = id;
|
||||
this.paintSelection();
|
||||
}
|
||||
|
||||
private paintSelection(): void {
|
||||
for (const [id, button] of this.treeButtons) {
|
||||
button.classList.toggle('tree__button--active', id === this.selectedId);
|
||||
}
|
||||
|
||||
const labelKey = findStub(STUB_MAP, this.selectedId)?.labelKey;
|
||||
this.locationName.textContent = labelKey === undefined ? '' : t(labelKey);
|
||||
}
|
||||
|
||||
private applyClock(gameTime: Date, running: boolean, speedIndex: number): void {
|
||||
this.running = running;
|
||||
this.lastGameTime = gameTime;
|
||||
@@ -102,3 +222,18 @@ export class GameScreen {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function findStub(node: StubNode, id: string): StubNode | null {
|
||||
if (node.id === id) {
|
||||
return node;
|
||||
}
|
||||
|
||||
for (const child of node.children ?? []) {
|
||||
const match = findStub(child, id);
|
||||
if (match !== null) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user