diff --git a/README.md b/README.md index 365199f..c6df158 100644 --- a/README.md +++ b/README.md @@ -14,18 +14,18 @@ Shooting Word — это browser-source виджет для стримов, пр ## Типы врагов -| Тип | Описание | Очки | -| ----------- | --------------------------------------------- | ---- | -| **Обычный** | Одно слово (hack, code, data, git...) | 10 | -| **Тяжёлый** | Два слова подряд (fire wall, null pointer...) | 50 | -| **Босс** | 3 слоя по 3 слова, появляется каждые 10 волн | 200 | +| Тип | Описание | Очки | +| ----------- | ------------------------------------------------------ | ---- | +| **Обычный** | Одно слово (hack, code, data, git...) | 10 | +| **Тяжёлый** | Два слова-слоя подряд (fire → wall, null → pointer...) | 50 | +| **Босс** | 3 слоя по 3 слова, появляется каждые 10 волн | 200 | -Слова — IT/хакерская тематика: названия языков, протоколов, уязвимостей, команд терминала. +Броня отображается и пробивается только по текущему (верхнему) слою. Слова — IT/хакерская тематика: названия языков, протоколов, уязвимостей, команд терминала. ## Возможности - **Волны** — количество врагов и их скорость растут с каждой волной -- **Боссы** — multi-phase враги со слоями защиты +- **Боссы** — multi-layer враги со слоями защиты - **Статистика** — таблица лидеров по уничтоженным врагам и «Мазила» за много промахов - **Визуальные эффекты** — частицы, трейлы, screen shake, glitch при уроне - **Режим single play** — скрытие виджета после одной партии (для автоматизации через Streamer.bot) @@ -33,10 +33,20 @@ Shooting Word — это browser-source виджет для стримов, пр ## Установка +### Сборка из исходников + +```bash +pnpm install +pnpm build # typecheck + сборка в dist/ +pnpm serve # локальный сервер на dist/ +``` + +Для разработки: `pnpm dev` — esbuild watch, пересобирает dist при изменениях в src/. + ### Быстрый старт -1. Открой [страницу настройки](http://ku6epxboctuk.is-a.dev/shooting-word-html/) -2. Введи имя.twitch канала +1. Открой [страницу настройки](https://xboctuk.duckdns.org/shooting-word/) +2. Введи имя twitch канала 3. Нажми **СКОПИРОВАТЬ ССЫЛКУ** 4. В OBS: Sources → Browser → вставь ссылку 5. Рекомендуемая высота — не менее 800px @@ -44,7 +54,7 @@ Shooting Word — это browser-source виджет для стримов, пр ### Прямая ссылка ```txt -http://ku6epxboctuk.is-a.dev/shooting-word-html/game.html?channel=ИМЯ_КАНАЛА +https://xboctuk.duckdns.org/shooting-word/game.html?channel=ИМЯ_КАНАЛА ``` ### Параметры URL @@ -61,41 +71,45 @@ http://ku6epxboctuk.is-a.dev/shooting-word-html/game.html?channel=ИМЯ_КАН ## Технологии -- Vanilla JavaScript (ES6+), без фреймворков и сборщиков -- Canvas 2D для рендеринга +- TypeScript (strict), сборка esbuild +- [PixiJS v8](https://pixijs.com/) для рендера (WebGL/WebGPU) +- [miniplex](https://github.com/hmans/miniplex) — ECS-ядро - [TMI.js](https://github.com/tmijs/tmi.js) для подключения к Twitch IRC -- Один HTML-файл + CSS + JS модули - Хостится на GitHub Pages +Архитектура описана в [ecs-refactoring.md](ecs-refactoring.md): ввод отделён интерфейсом +`InputSource` (переход на Twurple и запуск через баллы канала = новый источник без правок +игры), логика отделена от рендера, сущности и системы — на miniplex. + ## Структура проекта ```txt -├── index.html # Страница настройки (выбор канала) -├── game.html # Игровой экран -├── style.css # Стили ( cyberpunk-тема: чёрный фон, зелёный неон) -├── js/ -│ ├── config.js # Конфигурация и словарь слов -│ ├── game.js # Основной игровой цикл -│ ├── enemy.js # Класс врага (обычный / тяжёлый / босс) -│ ├── projectile.js # Снаряды -│ ├── particle.js # Система частиц -│ ├── input.js # Обработка ввода (чат + локальный) -│ ├── intro.js # Заставка -│ ├── utils.js # Утилиты -│ ├── main.js # Точка входа -│ └── tmi.min.js # Twitch Messaging Interface +├── index.html # Страница настройки (выбор канала) +├── game.html # Игровой экран +├── style.css # Стили (cyberpunk-тема: чёрный фон, зелёный неон) +├── build.js # esbuild-сборка двух бандлов + копирование статики в dist/ +├── src/ +│ ├── entries/ # Точки входа: game.ts (игра), index.ts (настройки) +│ ├── app/ # GameApp — сборка приложения, фазы, такт +│ ├── config/ # gameConfig.ts (константы), words.ts (словарь слов) +│ ├── core/ # Шина событий, игровой цикл, метрики, измеритель текста +│ ├── ecs/ # Компоненты, мир miniplex, спавн-функции +│ ├── systems/ # Системы: волны, fuse, наведение, интеграция, побег, lifetime +│ ├── game/ # Simulation (урон/оркестрация), GameStore, события, статистика +│ ├── input/ # InputSource-интерфейс, роутер, источники: tmi.js + клавиатура +│ ├── render/ # Pixi: сетка, частицы, враги, снаряды, баннер, интро +│ └── ui/ # DOM: HUD, экраны, статистика, фидбек ввода, скейлинг ├── img/ -│ └── sprite.svg # Иконки (GitHub, Twitch) -└── LICENSE # MIT +│ └── sprite.svg # Иконки (GitHub, Twitch) +└── LICENSE # MIT ``` ## Добавление своих слов -Отредактируй `js/config.js`: +Отредактируй `src/config/words.ts`: -WordGenerator.simpleWords - простые враги из одного слова - -WordGenerator.heavyWords - бронированные враги - два слова +- `SIMPLE_WORDS` — простые враги из одного слова +- `HEAVY_WORDS` — бронированные враги: пары слов, каждое слово — отдельный слой брони ## Лицензия @@ -103,10 +117,9 @@ MIT ## Связь -- [GitHub Issues](https://github.com/Ku6epXBOCTuK/shooting-word-html/issues) — баги и идеи -- [Twitch](https://www.twitch.tv/ku6ep_xboctuk) — смотри игру в действии +- [Обратная связь - telegram chat](https://t.me/Ku6epXBOCTuK_chat) — баги и идеи +- [Twitch](https://www.twitch.tv/ku6epxboctuk) — смотри игру в действии ## Похожие проекты -Этот репозиторий не будет активно развиваться. В дальнейшем функционал станет частью проекта [multi-widget](https://github.com/Ku6epXBOCTuK/multi-widget). -Пока что ожидается перенос на sveltekit + miniplex ecs и возможно pixi.js\konva +Этот репозиторий не будет активно развиваться. В дальнейшем функционал станет частью проекта более крупного проекта multi-widget diff --git a/build.js b/build.js index c0cc715..a1f91e4 100644 --- a/build.js +++ b/build.js @@ -1,12 +1,13 @@ +import { build, context } from 'esbuild' import { cpSync, copyFileSync, mkdirSync, readdirSync, rmSync } from 'node:fs' import { dirname, join, relative } from 'node:path' import { fileURLToPath } from 'node:url' const root = dirname(fileURLToPath(import.meta.url)) const dist = join(root, 'dist') +const watch = process.argv.includes('--watch') const rootFiles = ['index.html', 'game.html', 'style.css', 'serve.json'] -const jsDir = 'js' const imgFiles = ['sprite.svg'] function listFiles(dir) { @@ -22,14 +23,33 @@ for (const file of rootFiles) { copyFileSync(join(root, file), join(dist, file)) } -cpSync(join(root, jsDir), join(dist, jsDir), { recursive: true }) - mkdirSync(join(dist, 'img')) for (const file of imgFiles) { copyFileSync(join(root, 'img', file), join(dist, 'img', file)) } -console.log('Собрано в dist:') -for (const file of listFiles(dist)) { - console.log(' ' + relative(dist, file)) +const options = { + entryPoints: { + game: join(root, 'src/entries/game.ts'), + index: join(root, 'src/entries/index.ts'), + }, + outdir: dist, + bundle: true, + minify: true, + sourcemap: false, + format: 'iife', + target: 'es2020', + logLevel: 'info', +} + +if (watch) { + const contexts = await Promise.all([context(options)]) + await Promise.all(contexts.map((c) => c.watch())) + console.log('Слежу за изменениями src/...') +} else { + await build(options) + console.log('Собрано в dist:') + for (const file of listFiles(dist)) { + console.log(' ' + relative(dist, file)) + } } diff --git a/ecs-refactoring.md b/ecs-refactoring.md index f66fb65..e40e2a1 100644 --- a/ecs-refactoring.md +++ b/ecs-refactoring.md @@ -1,33 +1,98 @@ -# ECS рефакторинг — план +# Архитектура (ECS на miniplex) -## Суть +Проект переписан на TypeScript + PixiJS с ECS-ядром на [miniplex](https://github.com/hmans/miniplex). +Этот документ — описание того, как всё устроено сейчас. -Вместо классов врагов/снарядов с методами update() и draw() — единый контейнер сущностей, где каждая сущность это набор компонентов (позиция, скорость, тип, слово и т.д.). Логика вынесена в отдельные функции-системы, которые каждую итерацию проходят по нужным подмножествам сущностей и обновляют их. +## Слои -## Компоненты +```txt +ввод (InputSource) → шина событий → симуляция (мир + системы) → шина событий → рендер (Pixi) и DOM-UI +``` -Позиция и скорость — общие для всех движущихся сущностей. Тип врага и данные слова — только для врагов. Ссылка на цель — только для снарядов. Броня и босс-данные — для усиленных и босс-врагов. Пустой маркер — чтобы система знала, что сущность является снарядом. +Каждый слой знает только соседей через интерфейсы: логика не знает про Pixi и DOM, +рендер не содержит игровой логики, ввод не знает про игру — только команды. -## Системы (по порядку за кадр) +## Сущности и компоненты (src/ecs) -1. Система волн — считает таймеры, решает когда спавнить нового врага и какого типа, создаёт сущность в контейнере. -2. Система ввода — получает слово, ищет лучшего врага (точное совпадение > более позднее слово > частичное), создаёт снаряд с ссылкой на цель. -3. Система движения — для всех сущностей с позицией и скоростью: позиция += скорость. -4. Система наведения — для снарядов с целью: вычисляет расстояние, если рядом — помечает попадание, если цель исчезла — удаляет снаряд, иначе двигает к цели. -5. Система урона — для снарядов с пометкой попадания: проверяет броню (если есть и не ноль — снимает), проверяет босс-данные (считает убитые слова в слое, если слой пройден — переключает), если враг умер — удаляет и вызывает callback. -6. Система рендера — берёт все враги, сортирует по высоте, рисует форму/слово/планку брони. Отдельно рисует снаряды. Отрисовка HUD отдельно от ECS. -7. Система очистки — удаляет из контейнера все сущности с пометкой "мёртвый". +Сущность — объект с опциональными компонентами; «есть компонент» = свойство не `undefined`. -## Что остаётся за пределами ECS +| Компонент | Смысл | +| -------------------------------------------------------- | ------------------------------------------------------------------------- | +| `position`, `velocity`, `damping` | Движение (враги, снаряды, частицы — одна система интеграции) | +| `enemyKind`, `enemyBox` | Тип (simple/heavy/boss) и габариты рамки | +| `armor` | Слои слов: текущий слой отображается и уязвим только он | +| `projectile`, `player`, `target` / `targetPoint`, `fuse` | Снаряд: слово, кто выстрелил, цель (или точка промаха), задержка выстрела | +| `lifetime`, `particle` | Частицы — полноценные сущности, а не особый массив | +| `waveState` | Синглтон-сущность с состоянием волны и баннером | -Волны, ввод, рендер HUD, очистка — это системы, но не entity. Жизни/счёт/номер волны — простые переменные, не сущности. Частицы — не сущности, рисуются как визуальный эффект по времени от взрыва. +Броня единая для всех типов: simple — 1 слой из 1 слова, heavy — 2 слоя по 1 слову +(порядок строгий), boss — 3 слоя по 3 слова (внутри слоя порядок свободный). +Урон снимает только текущий слой; после каждого попадания вью перерисовывается по диффу +`layerIndex:killed`. -## Порядок миграции +## Системы (порядок за кадр, src/systems) -1. Контейнер сущностей — замена глобальных массивов. -2. Система движения — самая простая, проверка что всё работает. -3. Система наведения и урона — core gameplay. -4. Система волн — спавн. -5. Система ввода — поиск цели + создание снаряда. -6. Система рендера — вся отрисовка. -7. Остальное — очистка, game loop, удаление старых классов. +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` и т.д.). diff --git a/game.html b/game.html index 69947e3..4759363 100644 --- a/game.html +++ b/game.html @@ -5,7 +5,6 @@