# Прототип: тактический отряд с авто-огнём (изометрия, software rendering) Ты делаешь **технический прототип боевого ядра** для 2D-изометрического sci-fi horror roguelite / incremental ARPG. Цель прототипа — **только одно**: доказать, что отряд из 5 агентов ведёт себя как «умная текучая сущность», которая сама расставляется так, чтобы стрелять и не задевать своих. Всё остальное (лут, прогрессия, враги-ИИ, уровни) — вне рамок. Не добавляй. --- ## 1. Технические ограничения - **C++17**, **raylib 5.x**, **CMake** (raylib через `FetchContent`, если не найден в системе). - Никаких других зависимостей. Никакого ECS-фреймворка, никакой физдвижка. - **Software rendering.** Вся отрисовка сцены — ручная запись пикселей в собственный фреймбуфер (`uint32_t*`, RGBA8). Ни одного вызова raylib-примитивов для игровой сцены. - Из raylib используются только: окно, ввод, таймер, `UpdateTexture` + `DrawTexturePro` для финального блита фреймбуфера на экран (фильтрация `TEXTURE_FILTER_POINT`). - Исключение: **debug-оверлей** можно рисовать через `DrawText`/`DrawLine` поверх уже отмасштабированного кадра. Это dev-only, отключается по F1. - **Внутреннее разрешение 480×270**, окно 1440×810 (целочисленный ×3 апскейл). Разрешение и множитель — константы. - **Fixed timestep симуляции 60 Гц**, накопитель времени, рендер с интерполяцией позиций. Симуляция детерминирована. - Однопоточно. Читаемость важнее производительности, но 5 агентов + 12 мишеней + 200 пуль должны идти в 60 fps без проблем. --- ## 2. Ассеты **Никаких файлов.** Все спрайты генерируются процедурно в код при старте (пишешь пиксели в буфер): - `tile_floor` — ромб 32×16, 2 варианта оттенка (шахматка), тёмно-серый металл. - `tile_wall` — блок 32×32 (ромб + вертикальные грани), заметно светлее пола. - `agent` — 10×14, капсула с обводкой, 5 цветовых вариантов (по одному на агента). - `target` — 12×16, тускло-красная капсула с обводкой. - `bullet` — 2×2 точка. Спрайты хранятся как `Sprite { int w, h; std::vector px; }` с альфа-тестом (0 = прозрачно), без блендинга. --- ## 3. Координаты и рендер **Критично: вся симуляция — в декартовом мире (top-down), изометрия существует только в рендерере.** Никакой iso-математики в логике, ИИ или коллизиях. - Мир в тайлах, `float` координаты. Тайл = 1.0 юнит. - Проекция: `sx = (wx - wy) * 16`, `sy = (wx + wy) * 8` (тайл 32×16), плюс смещение камеры. - Камера следует за якорем отряда со сглаживанием, округляется до целых пикселей (чтобы не дрожало). - Сортировка по глубине: все видимые сущности и стены в один список, сортировка по `(wx + wy)`, потом блит. - Пол рисуется первым сплошным проходом по видимой области карты. --- ## 4. Уровень Один захардкоженный тайлмап ~48×48, генерируется в коде: - открытая арена в центре; - **колонны/огрызки стен по всей карте** — они обязаны быть, без них не проверить LOS и обход; - пара узких проходов (шириной 2 тайла) — там правило «не стрелять сквозь своих» упирается в геометрию, это важный тест-кейс; - по периметру — сплошная стена. Тайл: `EMPTY` (проходим, прозрачен) / `WALL` (непроходим, блокирует LOS и пули). **Мишени:** 12 штук, статичные, разбросаны кучками (по 1, по 3, по 5). У мишени есть HP (например, 500) и она **респавнится через 3 сек** после смерти на том же месте — прототип должен работать бесконечно. Мишени не атакуют и не двигаются (на этом этапе это шум). --- ## 5. Управление (только клавиатура) | Клавиша | Действие | |---|---| | `WASD` | движение якоря отряда (в **экранных** осях, не мировых — иначе неинтуитивно) | | `Shift` (удерж.) | медленный шаг | | `Space` | переключить режим ручного прицеливания | | `←/→` | в ручном режиме — поворот вектора прицела | | `Tab` | сменить назначенную цель | | `1/2/3` | пресет строя: клин / линия / кольцо | | `F1` | debug-оверлей вкл/выкл | | `F2` | показ линий огня | | `F3` | показ кандидатов позиций и их оценок | | `F4` | метрики | | `R` | сброс уровня | | `Esc` | выход | Стрельба **всегда автоматическая**. Игрок не нажимает «огонь» никогда, ни в одном режиме. --- ## 6. ЯДРО: поведение отряда Это главная часть задания. Ниже — точная спецификация, реализуй её как **чистые, тестируемые функции**, отдельно от рендера. ### 6.1 Сущности ```cpp struct Agent { Vec2 pos, vel; float facing; // радианы, куда смотрит ствол float bodyRadius = 0.28f; int slotIndex; // место в строю AgentState state; int currentTargetId; // -1 = нет Vec2 postPos; // куда агент решил встать (огневая позиция) float commitTimer; // блокировка перерешивания float solveTimer; // стаггер решателя Weapon weapon; }; struct Weapon { float range = 9.0f; float fireInterval = 0.35f; float cooldown; float laneHalfWidth = 0.10f; // «толщина» траектории пули float damage = 12.0f; }; ``` ### 6.2 Состояния отряда - `ADVANCING` — есть ввод движения. - `HOLDING` — ввода нет дольше `0.15 сек`. ### 6.3 Проверка линии огня (сердце всей механики) ``` bool LaneClear(shooter, targetPos, allAgents, tilemap): seg = Segment(shooter.muzzle(), targetPos) // 1. свои for each ally in allAgents, ally != shooter: d = DistancePointToSegment(ally.pos, seg) if d < ally.bodyRadius + shooter.weapon.laneHalfWidth + SAFETY_MARGIN: return false // 2. геометрия if TileRaycastBlocked(seg, tilemap): return false // 3. дистанция if seg.length() > shooter.weapon.range: return false return true ``` `SAFETY_MARGIN = 0.15f` (тюнится). `muzzle()` — точка в `0.3` юнита от центра агента по `facing`. Рейкаст по тайлам — Amanatides & Woo (DDA), без выделений памяти. ### 6.4 Логика в состоянии `ADVANCING` - Каждый агент движется к своему **слоту строя** относительно якоря, строй ориентирован по направлению движения. - Стрельба **оппортунистическая**: стреляет только тот, у кого прямо сейчас `LaneClear` до какой-либо цели в радиусе. Остальные просто бегут. - Приоритет цели у бегущего: ближайшая, до которой линия чиста. - **Никакого перестроения ради огня** в этом состоянии — отряд едет туда, куда сказал игрок. - Работает уклонение от чужих линий (см. 6.6) — агент, влезший в чужой сектор обстрела, смещается. ### 6.5 Логика в состоянии `HOLDING` — решатель огневых позиций Запускается **не каждый кадр**: у каждого агента свой `solveTimer` (интервал `0.25 сек`), фазы сдвинуты так, чтобы за кадр решал максимум 1 агент. Условие запуска: у агента **нет** валидной линии огня ни к одной живой цели в радиусе. **Кандидаты позиций:** - текущая позиция (для сравнения); - 3 кольца радиусами `0.8 / 1.6 / 2.4` юнита × 12 направлений = 36 точек. Отбрасываются кандидаты: в стене, ближе `0.55` юнита к другому агенту, вне карты. **Оценка кандидата:** ``` score = 0 if LaneClearFrom(candidate, bestTarget): score += 100 score += 25 * (число целей, простреливаемых из candidate) / totalTargets score -= 35 * (число союзников, чью текущую валидную линию перекрывает candidate) score -= 9 * distance(candidate, agent.pos) score -= 14 * max(0, distance(candidate, anchor) - COHESION_RADIUS) if distance(candidate, ближайшая цель) < MIN_ENGAGE_DIST: score -= 60 ``` **Коммит с гистерезисом (обязательно, иначе будет дёрганье):** ``` if bestScore > currentPositionScore + HYSTERESIS(15.0): agent.postPos = bestCandidate agent.commitTimer = 0.6f // не перерешивать это время ``` `COHESION_RADIUS = 3.5`, `MIN_ENGAGE_DIST = 2.0`. Пока `commitTimer > 0` — агент идёт к `postPos` и не пересчитывает. ### 6.6 Резервирование линий и расступание («текучесть») Каждый кадр собирается список **активных линий огня** — сегментов от стреляющих агентов к их целям. Любой агент, чьё тело попадает в чужую активную линию (`dist < bodyRadius + laneHalfWidth + SAFETY_MARGIN`), получает **steering-импульс перпендикулярно линии**, в сторону ближайшего края. Сила импульса растёт при приближении к оси. Это работает в **обоих** состояниях отряда и даёт главное ощущение: отряд сам «расплывается», освобождая сектора обстрела, без единой явной команды. ### 6.7 Движение агента (слои, складываются в таком порядке) 1. `seek` к целевой точке (слот строя или `postPos`); 2. `separation` от союзников (радиус `0.7`); 3. `laneAvoidance` (6.6) — **вес выше, чем у separation**; 4. `wallAvoidance` + скольжение вдоль стен при коллизии; 5. клампинг по максимальной скорости, интеграция, разрешение коллизий с тайлами (circle-vs-AABB, push-out). Скорость: `3.2` юнита/сек, `1.6` на Shift. ### 6.8 Ручное прицеливание (`Space`) - Игрок вращает вектор прицела стрелками. - **Назначенная цель** — ближайшая мишень в конусе ±20° от вектора прицела. `Tab` циклит между целями в конусе. - Все агенты приоритезируют назначенную цель. - **Правила 6.3–6.6 не меняются вообще.** Агент без чистой линии до назначенной цели: - в `HOLDING` — перестраивается через решатель, целью решателя становится назначенная цель; - в `ADVANCING` — не стреляет по ней; стреляет по вторичным целям, только если включён флаг `freeFireSecondary` (по умолчанию `true`). - Ручной режим **никогда** не позволяет стрелять сквозь своих. Никаких исключений. ### 6.9 Пули Настоящие снаряды, не хитскан: скорость `22` юнита/сек, коллизия с тайлами и со **всеми** капсулами, включая союзников. **`friendlyFire = true` всегда.** Дружественный урон должен быть физически возможен — иначе прототип ничего не доказывает. Счётчик попаданий по своим — главная метрика корректности. --- ## 7. Debug-оверлей По `F1`, поверх кадра: - состояние отряда (`ADVANCING` / `HOLDING`), режим прицеливания; - над каждым агентом: индекс, состояние, id цели; - `F2`: линии огня — **зелёная** (чистая, стреляет), **жёлтая** (цель есть, линия перекрыта своим), **красная** (перекрыта стеной); - `F3`: кандидаты последнего решателя точками, цвет = оценка (синий низкая → белый высокая), выбранный кандидат обведён; - якорь отряда, радиус когезии, слоты строя; - `F4`, метрики: - **`FRIENDLY_HITS`** — накопительный счётчик попаданий по своим; - **`FIRE UPTIME`** — % времени, когда агент имел валидную линию (за последние 10 сек, по каждому агенту и среднее); - **`SETTLE TIME`** — время от перехода в `HOLDING` до момента, когда ≥4 агентов получили линию огня; - fps, время симуляции на кадр. --- ## 8. Тюнинг Все числа выше — в **одной структуре** `Tuning` в `tuning.h`, ни одного магического числа по коду. Плюс runtime-подкрутка: `[` / `]` выбирают параметр, `-` / `=` меняют значение, текущее значение видно в оверлее. Это нужно, чтобы я сам почувствовал баланс. --- ## 9. Структура проекта ``` CMakeLists.txt src/ main.cpp — окно, главный цикл, fixed timestep tuning.h — все константы core/math.h — Vec2, сегменты, DistPointSegment, углы render/framebuffer.h/.cpp — фреймбуфер, clear, blit, линия, круг, блит на экран render/sprites.h/.cpp — процедурная генерация спрайтов render/iso.h — проекция мир↔экран, сортировка глубины world/tilemap.h/.cpp — карта, DDA-рейкаст, коллизии sim/agent.h/.cpp — агент, движение, steering sim/squad.h/.cpp — якорь, строй, состояния отряда sim/target.h/.cpp sim/bullet.h/.cpp ai/firing_solver.h/.cpp — LaneClear, оценка кандидатов, коммит ai/lane_registry.h/.cpp — активные линии, расступание debug/overlay.h/.cpp debug/metrics.h/.cpp ``` `ai/` не должен знать ничего о рендере и об изометрии. Вообще. --- ## 10. Порядок работы Делай по вехам, **после каждой — собери и запусти**, убедись что работает, потом иди дальше. Не пиши всё сразу. - **M0** — окно, фреймбуфер, апскейл, изометрическая сетка на экране. - **M1** — тайлмап, стены, камера, сортировка по глубине. - **M2** — 5 агентов, движение якоря, строй, separation, коллизии со стенами. - **M3** — мишени, DDA-рейкаст, `LaneClear`, авто-огонь, пули, дружественный урон, счётчик `FRIENDLY_HITS`. - **M4** — решатель огневых позиций, гистерезис, коммит, расступание из чужих линий. **Ключевая веха.** - **M5** — ручное прицеливание, назначенная цель, `Tab`. - **M6** — полный debug-оверлей, метрики, runtime-тюнинг. --- ## 11. Критерии приёмки Прототип готов, когда **все** пункты выполняются: 1. Отряд стоит в куче, игрок останавливается → **за ≤1.5 сек** минимум 4 из 5 агентов имеют чистую линию огня. Видно, как они расходятся веером. 2. **`FRIENDLY_HITS` = 0** за 3 минуты непрерывного боя во всех режимах, включая узкие проходы. 3. При движении отряда стреляют только те, у кого чистая линия; остальные не палят в спину союзникам. Визуально это заметно — часть агентов молчит. 4. Агент, оказавшийся на чужой линии огня, **сам отходит вбок** в течение ~0.3 сек. 5. Нет дёрганья: агент не переключает `postPos` чаще чем раз в `0.6` сек, позиции не осциллируют между двумя точками. 6. В узком проходе (2 тайла) отряд **самостоятельно вытягивается в колонну**, стреляет передний, задние подтягиваются и не стреляют. 7. В ручном режиме те же правила: агент без линии до назначенной цели перестраивается (стоя) либо молчит (в движении), но **никогда** не стреляет сквозь своего. 8. `FIRE UPTIME` в стоячем бою на открытой арене ≥ 80%. 9. Стабильные 60 fps. 10. Вся сцена нарисована собственным софтверным растеризатором; в игровой отрисовке нет raylib-примитивов. --- ## 12. Чего не делать - Не добавлять врагов с ИИ, атаками, лут, UI, меню, звук, партиклы, освещение, туман войны. - Не подключать сторонние библиотеки. - Не писать ECS, не абстрагировать преждевременно. Простые структуры и вектора. - Не «улучшать» правила стрельбы по своему усмотрению — раздел 6 реализуется как написано. Если считаешь, что где-то ошибка — сначала скажи, потом делай.