Files
shooting-word/ecs-refactoring.md

99 lines
7.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Архитектура (ECS на miniplex)
Проект переписан на TypeScript + PixiJS с ECS-ядром на [miniplex](https://github.com/hmans/miniplex).
Этот документ — описание того, как всё устроено сейчас.
## Слои
```txt
ввод (InputSource) → шина событий → симуляция (мир + системы) → шина событий → рендер (Pixi) и DOM-UI
```
Каждый слой знает только соседей через интерфейсы: логика не знает про Pixi и DOM,
рендер не содержит игровой логики, ввод не знает про игру — только команды.
## Сущности и компоненты (src/ecs)
Сущность — объект с опциональными компонентами; «есть компонент» = свойство не `undefined`.
| Компонент | Смысл |
| -------------------------------------------------------- | ------------------------------------------------------------------------- |
| `position`, `velocity`, `damping` | Движение (враги, снаряды, частицы — одна система интеграции) |
| `enemyKind`, `enemyBox` | Тип (simple/heavy/boss) и габариты рамки |
| `armor` | Слои слов: текущий слой отображается и уязвим только он |
| `projectile`, `player`, `target` / `targetPoint`, `fuse` | Снаряд: слово, кто выстрелил, цель (или точка промаха), задержка выстрела |
| `lifetime`, `particle` | Частицы — полноценные сущности, а не особый массив |
| `waveState` | Синглтон-сущность с состоянием волны и баннером |
Броня единая для всех типов: simple — 1 слой из 1 слова, heavy — 2 слоя по 1 слову
(порядок строгий), boss — 3 слоя по 3 слова (внутри слоя порядок свободный).
Урон снимает только текущий слой; после каждого попадания вью перерисовывается по диффу
`layerIndex:killed`.
## Системы (порядок за кадр, src/systems)
1. `fuseSystem` — снаряд стоит у стрелка, пока не истечёт задержка ввода (150 мс), потом получает скорость.
2. `homingSystem` — наведение на цель, касание → событие `projectile:hit`; цель исчезла — снаряд удаляется.
3. `missSystem` — промах долетает до точки → `projectile:miss`.
4. `waveSystem` — таймеры волны, спавн, пауза, боссы; пишет в синглтон `waveState`.
5. `integrationSystem` — единое движение для всех сущностей с position+velocity (и damping).
6. `cullSystem` — снаряды за экраном.
7. `escapeSystem` — враг дошёл до низа → `enemy:escaped` → урон.
8. `lifetimeSystem` — затухание частиц.
`Simulation` (src/game) задаёт порядок систем, принимает команды ввода (поиск цели по слову),
обрабатывает попадания/промахи/урон и держит скаляры партии (счёт, жизни, фаза, тряска)
в `GameStore` — по договорённости это не сущности.
## Ввод (src/input) — отдельный интерфейс
Игра зависит только от порта `InputSource`, а не от библиотеки:
```ts
interface InputSource {
readonly origin: "local" | "chat";
start(): void;
stop(): void;
onCommand(handler: (command: InputCommand) => void): () => void;
}
type InputCommand =
| { kind: "start" } // !play, награда за баллы канала и т.п.
| { kind: "clear" }
| { kind: "word"; word: string; player: Player };
```
Сейчас реализованы `TwitchChatInputSource` (tmi.js, парсит `!play` и первое слово сообщения)
и `LocalKeyboardInputSource` (тест без Twitch). Будущий переход на Twurple и запуск через
баллы канала = новый источник, который эмитит `{ kind: 'start' }` на событие награды;
`InputRouter` просто регистрирует его рядом с остальными.
## Рендер (src/render)
- `GridLayer` — сетка одним `TilingSprite` (вместо ~40 `stroke()` за кадр).
- `ParticleLayer` — все частицы в одном `ParticleContainer` (один draw call, пул объектов).
- Свечение — заранее запечённая радиальная текстура с аддитивным блендингом
вместо попиксельного `shadowBlur`.
- Вью врагов/снарядов создаются и уничтожаются по сигналам мира
(`world.onEntityAdded/onEntityRemoved`), каждый кадр только синхронизируют позицию/альфу.
- Тряска экрана — смещение корневого контейнера, состояние в `GameStore`.
- Цикл: внешний `GameClock` (rAF, delta в 60-кадрах), pixi-ticker остановлен; в простое
(интро закончилось, партия не идёт) рендер вообще не тикает — canvas хранит последний кадр.
Сбой одного кадра не убивает цикл.
## События (src/game/events.ts)
Типизированная шина `EventBus`: ввод, бой (`projectile:hit/miss`, `enemy:*`) и состояние
партии (`game:started/over`, `score/wave/lives`, `effect:glitch`). Рендер и DOM-UI — только
подписчики.
## DOM-UI (src/ui)
HUD, экраны старта/поражения, таблица зрителей (имена вставляются через `textContent`
ник из чата не должен стать разметкой), фидбек ввода, масштабирование оверлея под окно.
## Отладка
В консоли браузера доступен хук `window.__sw` — мир, стор, шина, метрики и рендерер
(`__sw.world.with('armor').entities`, `__sw.store.score` и т.д.).