squad-proto/src/ai/firing_solver.h
z.kirill 7d6266aaf3 Tile2D: движок отдельным продуктом, игра — проектом в нём (M11-M20)
Десять вех одним состоянием: разделять их пофайлово нечем, потому что
src/game.cpp менялся почти в каждой. Собирается начисто без предупреждений,
все проверки проходят.

ДВИЖОК (engine/, цель tile2d)

  Каталог свой, и решение «это движок» принимается тем, куда положен файл.
  Компилятор нашёл три зависимости движка от игры, и все сняты: размеры кадра
  и карты уехали в engine/config.h, потолок населения этажа стал своим
  (MAX_LEVEL_SPAWNS против игровой MAX_TARGETS), запасная точка старта стала
  центром карты вместо позиции отряда.

  engine/projection.*  единственное место, где движок знает про изометрию;
                       видов сразу два — абстракция с одной реализацией ничем
                       не проверена и обычно оказывается неправильной
  engine/project.*     манифест проекта: пути, вид, кривая глубины
  engine/app.*         окно, кадр, fixed timestep — вместо двух копий цикла
  engine/tilemap_draw  движок научился показывать свою карту

РЕДАКТОР (editor/, цель tile2d_editor)

  Открывает любой проект и ни одной игры по имени не знает.
  --tool  одиннадцать безоконных команд: комнаты, каталог, кривая, сборка
          этажа, ASCII-карта, проверка набора. Правку делают файлами,
          последствия читают командой (docs/19-agent.md)
  терминал снизу: те же инструменты, git, сборка и запросы к claude/codex,
          открытый проект уходит потомкам через TILE2D_PROJECT

ПРИМЕР (samples/walk, цель tile2d_walk)

  Второй потребитель движка. Игра и редактор самостоятельность движка
  проверить не могут: редактор мира не рисует, а игра — то место, куда общий
  код и уезжает незаметно.

ИГРА (src/)

  Экстракшен: вес и два порога переноски, конечный боезапас, расходники
  применяются сами, общая сумка отряда, трата опыта на улучшения.

  Каждое из правил проверяется прогоном, а не глазами, и проверки поймали:
  вес дважды не связывал ничего (мерил по бинтам прогона, а не по добыче);
  боец с клинком не тратил найденные патроны при запасном стволе в сумке;
  массив виджетов экрана переполнился и молча съел хвост схрона; проверка
  улучшений сама ломала забег, вызывая NewRun посреди спуска.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 21:07:19 +03:00

83 lines
4.7 KiB
C++
Raw 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.

#pragma once
// -----------------------------------------------------------------------------
// Ядро прототипа (спек 6.36.5): проверка линии огня, выбор цели,
// решатель огневых позиций с гистерезисом.
//
// Всё здесь — чистые функции над состоянием мира. Модуль ai/ НЕ знает
// ни про рендер, ни про изометрию, ни про ввод.
// -----------------------------------------------------------------------------
#include <vector>
#include "ai/lane_registry.h"
#include "sim/agent.h"
#include "sim/target.h"
#include "engine/tilemap.h"
struct SolverContext
{
const Tilemap* map = nullptr;
const Agent* agents = nullptr;
int agentCount = 0;
const std::vector<Target>* targets = nullptr;
const LaneRegistry* lanes = nullptr;
Vec2 anchor{};
int assignedTargetId = -1; // ручной режим (6.8); -1 = авто
};
// --- приоритет цели ----------------------------------------------------------
// Есть ли живая ТВАРЬ (не обстановка) в радиусе propYieldRadius от точки.
//
// Правило приоритета разрушаемой обстановки, одно на оба класса оружия: пока
// такая тварь есть, ящик, бочка и шкафчик целью не становятся ни у стрелка
// (firing_solver.cpp), ни у клинка (melee.cpp). Один общий предикат тут не
// украшение — разъехавшись, стрелок и клинок начали бы спорить, что важнее.
//
// Радиус СВОЙ, а не дальность ствола: с дальностью правило читалось бы как
// «не достаю — стреляю по ящику», и подошедшую тварь боец встречал бы на
// перезарядке. Стены не спасают намеренно: тварь за углом в пяти шагах — всё
// ещё повод не заниматься мебелью.
bool EnemyNearby(Vec2 from, const SolverContext& ctx);
// --- 6.3 ---------------------------------------------------------------------
// Линия от дула стрелка (в точке from, ствол смотрит на цель) до targetPos.
// Порядок проверок как в спеке: свои -> геометрия -> дистанция.
bool LaneClearFrom(Vec2 from, int shooterIndex, Vec2 targetPos, const SolverContext& ctx);
// Успеет ли союзник влезть под пулю за время её полёта.
//
// 6.3 проверяет линию в МОМЕНТ выстрела, а 6.9 делает пулю настоящим снарядом:
// до 0.4 с полёта, за которые союзник проходит больше метра. Реактивного
// расступания (6.6) тут не хватает — союзник может быть чист в момент нажатия
// и войти в коридор уже потом. Поэтому спуск дополнительно проверяет
// сближение пули и союзника как двух равномерно движущихся точек.
// Само правило 6.3 не ослабляется: см. docs/08-decisions.md.
bool AllyInterceptsShot(Vec2 muzzle, Vec2 targetPos, int shooterIndex, const SolverContext& ctx);
// --- выбор цели (6.4 / 6.8) --------------------------------------------------
// Заполняет currentTargetId, hasLane, priorityLane и диагностику для оверлея.
void EvaluateAgentLane(Agent& a, int index, const SolverContext& ctx);
// Ближайшая живая мишень в радиусе searchRadius (или -1).
int NearestAliveTarget(Vec2 from, const SolverContext& ctx, float searchRadius);
// --- 6.5 ---------------------------------------------------------------------
struct SolveResult
{
Vec2 candidates[SOLVER_CANDIDATES]{};
float scores[SOLVER_CANDIDATES]{};
bool valid[SOLVER_CANDIDATES]{};
int count = 0;
int bestIndex = -1;
float bestScore = 0.0f;
float currentScore = 0.0f;
bool committed = false;
int bestTargetId = -1;
};
float ScoreCandidate(Vec2 cand, const Agent& a, int index, const SolverContext& ctx,
int bestTargetId);
// true, если решатель отработал (нашёл bestTarget и оценил кандидатов).
bool SolveFiringPosition(Agent& a, int index, const SolverContext& ctx, SolveResult& out);