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.
|
bare `<PackageReference Include="..." />` in the project.
|
||||||
- Transitive pinning is on, so a downgrade warning means you bump the central version rather than
|
- Transitive pinning is on, so a downgrade warning means you bump the central version rather than
|
||||||
adding a per-project override.
|
adding a per-project override.
|
||||||
- Keep the dependency count low. Arch, Aspire and the test runners are the whole budget; `pixi.js`
|
- Keep the dependency count low. Arch, Aspire and the test runners are the whole budget.
|
||||||
is installed but not imported yet — it is there for the game view inside a school.
|
|
||||||
|
|
||||||
## Things that will bite you
|
## 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 |
|
| Server | .NET 10, ASP.NET Core |
|
||||||
| Simulation | [Arch](https://github.com/genaray/Arch) ECS, fixed 20 Hz tick |
|
| Simulation | [Arch](https://github.com/genaray/Arch) ECS, fixed 20 Hz tick |
|
||||||
| Transport | REST for the menu, raw WebSocket + binary protocol for the clock |
|
| 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 |
|
| Orchestration | .NET Aspire 13 |
|
||||||
| Tests | xUnit v3, Vitest, `Aspire.Hosting.Testing` |
|
| Tests | xUnit v3, Vitest, `Aspire.Hosting.Testing` |
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ QSP: не «полотно текста + список действий», а п
|
|||||||
Анимация пока только переходы панелей/списков и живые цифры (часы, счётчики). Графики —
|
Анимация пока только переходы панелей/списков и живые цифры (часы, счётчики). Графики —
|
||||||
когда появятся числа, которые ими стоит показывать, не раньше.
|
когда появятся числа, которые ими стоит показывать, не раньше.
|
||||||
|
|
||||||
Клиент остаётся plain DOM + CSS. `pixi.js` для этого не нужен и в этом горизонте его стоит
|
Клиент остаётся plain DOM + CSS. WebGL-сцены нет.
|
||||||
убрать из зависимостей.
|
|
||||||
|
|
||||||
Симуляция по-прежнему на сервере: ученики и комнаты могут существовать как данные в Arch ECS,
|
Симуляция по-прежнему на сервере: ученики и комнаты могут существовать как данные в Arch ECS,
|
||||||
даже если клиент рисует строки списка, а не человечков.
|
даже если клиент рисует строки списка, а не человечков.
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
|
|
||||||
## Задачи
|
## Задачи
|
||||||
|
|
||||||
- [ ] Удалить `pixi.js` из `src/HSchool.Client/package.json` и обновить lockfile
|
- [x] Удалить `pixi.js` из `src/HSchool.Client/package.json` и обновить lockfile
|
||||||
- [ ] Убрать обещание Pixi из `README.md` и `AGENTS.md` (клиент: Vite + TypeScript + DOM)
|
- [x] Убрать обещание Pixi из `README.md` и `AGENTS.md` (клиент: Vite + TypeScript + DOM)
|
||||||
|
|
||||||
## Критерий готовности
|
## Критерий готовности
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,11 @@
|
|||||||
|
|
||||||
## Задачи
|
## Задачи
|
||||||
|
|
||||||
- [ ] Три области: дерево карты, общие события, выбранная локация — плюс часы и скорость
|
- [x] Три области: дерево карты, общие события, выбранная локация — плюс часы и скорость
|
||||||
- [ ] События: заголовок и пустое состояние, без выдуманных записей
|
- [x] События: заголовок и пустое состояние, без выдуманных записей
|
||||||
- [ ] Локация: секции имя, предметы, персонажи, действия на месте, должности — пустые, не ломаются
|
- [x] Локация: секции имя, предметы, персонажи, действия на месте, должности — пустые, не ломаются
|
||||||
- [ ] Клик по дереву переключает панель локации на клиенте (пока узлы-заглушки или пустое дерево)
|
- [x] Клик по дереву переключает панель локации на клиенте (пока узлы-заглушки или пустое дерево)
|
||||||
- [ ] Переключение языка обновляет подписи панелей и пустых состояний (`t(...)`)
|
- [x] Переключение языка обновляет подписи панелей и пустых состояний (`t(...)`)
|
||||||
|
|
||||||
## Критерий готовности
|
## Критерий готовности
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
|
|
||||||
| Фаза | Статус | Зачем |
|
| Фаза | Статус | Зачем |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| [0. Убрать PixiJS](00-drop-pixi.md) | ⬜ | Сцена не планируется |
|
| [0. Убрать PixiJS](00-drop-pixi.md) | ✅ | Сцена не планируется |
|
||||||
| [1. Оболочка менеджера](01-manager-shell.md) | ⬜ | Панели с секциями среза, пока без данных |
|
| [1. Оболочка менеджера](01-manager-shell.md) | ✅ | Панели с секциями среза, пока без данных |
|
||||||
| [2. Работник школы и диск](02-school-worker.md) | ⬜ | Поток + World + сейв — основа |
|
| [2. Работник школы и диск](02-school-worker.md) | ⬜ | Поток + World + сейв — основа |
|
||||||
| [3. Каталог def и карта](03-defs-map.md) | ⬜ | JSONC, core, валидация раскладки |
|
| [3. Каталог def и карта](03-defs-map.md) | ⬜ | JSONC, core, валидация раскладки |
|
||||||
| [4. Моды и редактор в create](04-create-editor.md) | ⬜ | Выбор модов, карта в POST, снимок при открытии |
|
| [4. Моды и редактор в create](04-create-editor.md) | ⬜ | Выбор модов, карта в POST, снимок при открытии |
|
||||||
|
|||||||
Generated
-104
@@ -7,9 +7,6 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "hschool-client",
|
"name": "hschool-client",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
|
||||||
"pixi.js": "^8.19.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
@@ -37,12 +34,6 @@
|
|||||||
"url": "https://github.com/sponsors/Boshen"
|
"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": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz",
|
||||||
@@ -313,12 +304,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||||
@@ -449,21 +434,6 @@
|
|||||||
"url": "https://opencollective.com/vitest"
|
"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": {
|
"node_modules/assertion-error": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
@@ -501,12 +471,6 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/es-module-lexer": {
|
||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
|
||||||
@@ -524,12 +488,6 @@
|
|||||||
"@types/estree": "^1.0.0"
|
"@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": {
|
"node_modules/expect-type": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
|
"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": "^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": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.33.0",
|
"version": "1.33.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
|
||||||
@@ -898,12 +835,6 @@
|
|||||||
"node": ">=12.20.0"
|
"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": {
|
"node_modules/pathe": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
@@ -931,32 +862,6 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"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": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.26",
|
"version": "8.5.26",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
||||||
@@ -1050,15 +955,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
|||||||
@@ -14,9 +14,6 @@
|
|||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest"
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
|
||||||
"pixi.js": "^8.19.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
|
|||||||
@@ -40,9 +40,25 @@ const ru = {
|
|||||||
errorInvalidStartDate: 'Дата начала вне допустимого диапазона.',
|
errorInvalidStartDate: 'Дата начала вне допустимого диапазона.',
|
||||||
|
|
||||||
backToMenu: '← В главное меню',
|
backToMenu: '← В главное меню',
|
||||||
emptySchoolHint: 'Школа пока пуста — здесь появится сама игра.',
|
|
||||||
pause: 'Пауза',
|
pause: 'Пауза',
|
||||||
resume: 'Продолжить',
|
resume: 'Продолжить',
|
||||||
|
|
||||||
|
mapTitle: 'Карта',
|
||||||
|
eventsTitle: 'События',
|
||||||
|
eventsEmpty: 'Пока ничего не происходит.',
|
||||||
|
locationName: 'Локация',
|
||||||
|
locationItems: 'Предметы',
|
||||||
|
locationCharacters: 'Персонажи',
|
||||||
|
locationActivities: 'Сейчас',
|
||||||
|
locationPositions: 'Должности',
|
||||||
|
itemsEmpty: 'Пусто.',
|
||||||
|
charactersEmpty: 'Никого нет.',
|
||||||
|
activitiesEmpty: 'Ничего не происходит.',
|
||||||
|
positionsEmpty: 'Нет должностей.',
|
||||||
|
stubTerritory: 'Двор',
|
||||||
|
stubBuilding: 'Главный корпус',
|
||||||
|
stubFloor: '1 этаж',
|
||||||
|
stubRoom: 'Кабинет директора',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type Messages = { [K in keyof typeof ru]: string };
|
type Messages = { [K in keyof typeof ru]: string };
|
||||||
@@ -87,9 +103,25 @@ const en: Messages = {
|
|||||||
errorInvalidStartDate: 'The start date is outside the allowed range.',
|
errorInvalidStartDate: 'The start date is outside the allowed range.',
|
||||||
|
|
||||||
backToMenu: '← Main menu',
|
backToMenu: '← Main menu',
|
||||||
emptySchoolHint: 'The school is empty for now — the game itself will appear here.',
|
|
||||||
pause: 'Pause',
|
pause: 'Pause',
|
||||||
resume: 'Resume',
|
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 };
|
const catalogs: Record<Locale, Messages> = { ru, en };
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
max-width: 900px;
|
max-width: 1080px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 32px 20px 72px;
|
padding: 32px 20px 72px;
|
||||||
}
|
}
|
||||||
@@ -191,6 +191,101 @@ body {
|
|||||||
margin-top: 20px;
|
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 */
|
/* Controls */
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { CLOCK_SPEEDS, type ClockMessage } from '../net/protocol.ts';
|
import { CLOCK_SPEEDS, type ClockMessage } from '../net/protocol.ts';
|
||||||
import { formatGameDate, formatGameTimeOfDay, formatGameWeekday } from '../format/gameTime.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 type { School } from '../net/api.ts';
|
||||||
import { el } from './dom.ts';
|
import { el } from './dom.ts';
|
||||||
|
|
||||||
@@ -10,11 +10,37 @@ interface GameScreenOptions {
|
|||||||
readonly onSetSpeed: (speedIndex: number) => void;
|
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'];
|
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 inside of a school: calendar controls plus the manager shell. Data for the tree and
|
||||||
* the clock, so every button only sends an intent and the display follows the next clock frame.
|
* 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 {
|
export class GameScreen {
|
||||||
private readonly root = el('section', { class: 'screen game' });
|
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 time = el('p', { class: 'clock__time', text: '--:--' });
|
||||||
private readonly date = el('p', { class: 'clock__date' });
|
private readonly date = el('p', { class: 'clock__date' });
|
||||||
private readonly weekday = el('p', { class: 'clock__weekday' });
|
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 playPauseButton = el('button', { class: 'button button--icon', type: 'button', text: '▶' });
|
||||||
private readonly speedButtons: HTMLButtonElement[];
|
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 running = false;
|
||||||
private lastGameTime: Date | null = null;
|
private lastGameTime: Date | null = null;
|
||||||
private lastSpeedIndex = 0;
|
private lastSpeedIndex = 0;
|
||||||
@@ -44,6 +86,8 @@ export class GameScreen {
|
|||||||
this.backButton.addEventListener('click', options.onLeave);
|
this.backButton.addEventListener('click', options.onLeave);
|
||||||
this.playPauseButton.addEventListener('click', () => options.onSetRunning(!this.running));
|
this.playPauseButton.addEventListener('click', () => options.onSetRunning(!this.running));
|
||||||
|
|
||||||
|
this.buildTree(this.tree, [STUB_MAP], 0);
|
||||||
|
|
||||||
this.root.append(
|
this.root.append(
|
||||||
el('header', { class: 'screen__header' }, this.backButton, this.schoolName),
|
el('header', { class: 'screen__header' }, this.backButton, this.schoolName),
|
||||||
el(
|
el(
|
||||||
@@ -54,7 +98,22 @@ export class GameScreen {
|
|||||||
this.weekday,
|
this.weekday,
|
||||||
el('div', { class: 'clock__controls' }, this.playPauseButton, ...this.speedButtons),
|
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();
|
this.localize();
|
||||||
@@ -66,7 +125,21 @@ export class GameScreen {
|
|||||||
|
|
||||||
localize(): void {
|
localize(): void {
|
||||||
this.backButton.textContent = t('backToMenu');
|
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) {
|
if (this.lastGameTime !== null) {
|
||||||
this.applyClock(this.lastGameTime, this.running, this.lastSpeedIndex);
|
this.applyClock(this.lastGameTime, this.running, this.lastSpeedIndex);
|
||||||
@@ -85,6 +158,53 @@ export class GameScreen {
|
|||||||
this.applyClock(clock.gameTime, clock.running, clock.speedIndex);
|
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 {
|
private applyClock(gameTime: Date, running: boolean, speedIndex: number): void {
|
||||||
this.running = running;
|
this.running = running;
|
||||||
this.lastGameTime = gameTime;
|
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