Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
#include "ai/firing_solver.h"
|
|
|
|
|
|
|
|
|
|
|
|
namespace
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
constexpr float BIG_F = 1e18f;
|
|
|
|
|
|
|
|
|
|
|
|
// Дуло: muzzleOffset юнита от точки from в сторону цели.
|
|
|
|
|
|
inline Vec2 MuzzleAt(const Agent& shooter, Vec2 from, Vec2 targetPos)
|
|
|
|
|
|
{
|
|
|
|
|
|
return from + Normalized(targetPos - from) * shooter.gun.def.muzzleOffset;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Полураствор конуса выстрела, выраженный как тангенс: боковое отклонение
|
|
|
|
|
|
// пули на расстоянии L от дула равно tan * L.
|
|
|
|
|
|
inline float SpreadTan(const Agent& shooter)
|
|
|
|
|
|
{
|
|
|
|
|
|
return std::tan(std::min(shooter.SpreadRad(), 1.2f));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Свободен ли конус «дуло -> цель» от своих.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Раньше проверялся отрезок: пуля летела ровно по нему, и этого хватало.
|
|
|
|
|
|
// Теперь у выстрела есть разброс, и проверять надо ровно ту область, куда
|
|
|
|
|
|
// пуля может уйти, — конус. Ослабления правила 6.3 тут нет, наоборот: зона
|
|
|
|
|
|
// проверки стала шире отрезка ровно настолько, насколько шире стал выстрел.
|
|
|
|
|
|
// Расширение растёт вдоль линии (у дула конус нулевой), поэтому союзник,
|
|
|
|
|
|
// стоящий вплотную к стрелку сбоку, лишнего запрета не создаёт.
|
|
|
|
|
|
bool ConeClearOfAllies(const Agent& shooter, Vec2 muzzle, int shooterIndex, Vec2 targetPos,
|
|
|
|
|
|
const SolverContext& ctx)
|
|
|
|
|
|
{
|
|
|
|
|
|
const float half = shooter.gun.def.laneHalfWidth + g_tune.safetyMargin;
|
|
|
|
|
|
const float segLen = Distance(muzzle, targetPos);
|
|
|
|
|
|
const float k = SpreadTan(shooter) * segLen;
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < ctx.agentCount; ++i)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (i == shooterIndex) continue;
|
|
|
|
|
|
const Agent& ally = ctx.agents[i];
|
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-14 01:07:19 +07:00
|
|
|
|
if (!ally.alive) continue; // упавший больше не перекрывает линию
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
float t = 0.0f;
|
|
|
|
|
|
const float d = DistancePointToSegment(ally.pos, muzzle, targetPos, &t);
|
|
|
|
|
|
if (d < ally.bodyRadius + half + k * t) return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Почему линии нет: свой или стена (для F2-оверлея).
|
|
|
|
|
|
void ClassifyBlock(Vec2 from, int shooterIndex, Vec2 targetPos, const SolverContext& ctx,
|
|
|
|
|
|
bool& byAlly, bool& byWall)
|
|
|
|
|
|
{
|
|
|
|
|
|
const Agent& shooter = ctx.agents[shooterIndex];
|
|
|
|
|
|
const Vec2 muzzle = MuzzleAt(shooter, from, targetPos);
|
|
|
|
|
|
|
|
|
|
|
|
byAlly = !ConeClearOfAllies(shooter, muzzle, shooterIndex, targetPos, ctx);
|
|
|
|
|
|
byWall = ctx.map->RaycastBlocked(muzzle, targetPos);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Куда союзник МОЖЕТ уехать за время t. Обычно это линейная экстраполяция
|
|
|
|
|
|
// скорости, но боец ближнего боя на замахе стоит почти неподвижно и через
|
|
|
|
|
|
// мгновение выстреливает рывком по УЖЕ ИЗВЕСТНОЙ оси удара — скорость об этом
|
|
|
|
|
|
// ещё ничего не говорит. Поэтому для него проверяются обе точки: где он есть
|
|
|
|
|
|
// и куда он прыгнет. Иначе выстрел «мимо стоящего» находит его в полёте.
|
|
|
|
|
|
int PredictAllyPositions(const Agent& ally, float t, Vec2 out[2])
|
|
|
|
|
|
{
|
|
|
|
|
|
out[0] = ally.pos + ally.vel * t;
|
|
|
|
|
|
if (!ally.IsMelee() || !ally.melee.Committed()) return 1;
|
|
|
|
|
|
|
|
|
|
|
|
out[1] = ally.pos + FromAngle(ally.melee.dir) * (g_tune.meleeLungeSpeed * t);
|
|
|
|
|
|
return 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
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-14 01:07:19 +07:00
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
// Приоритет цели: обстановка идёт последней
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
bool EnemyNearby(Vec2 from, const SolverContext& ctx)
|
|
|
|
|
|
{
|
|
|
|
|
|
const float r = g_tune.propYieldRadius;
|
|
|
|
|
|
for (const Target& t : *ctx.targets)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!t.alive || t.IsProp()) continue;
|
|
|
|
|
|
if (Distance(from, t.pos) <= r) return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
// 6.3 LaneClear
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
bool LaneClearFrom(Vec2 from, int shooterIndex, Vec2 targetPos, const SolverContext& ctx)
|
|
|
|
|
|
{
|
|
|
|
|
|
const Agent& shooter = ctx.agents[shooterIndex];
|
|
|
|
|
|
const Vec2 muzzle = MuzzleAt(shooter, from, targetPos);
|
|
|
|
|
|
|
|
|
|
|
|
// 1. свои — по конусу разброса, а не по осевому отрезку
|
|
|
|
|
|
if (!ConeClearOfAllies(shooter, muzzle, shooterIndex, targetPos, ctx)) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// 2. геометрия
|
|
|
|
|
|
if (ctx.map->RaycastBlocked(muzzle, targetPos)) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// 3. дистанция — своя у каждого ствола
|
|
|
|
|
|
if (Distance(muzzle, targetPos) > shooter.gun.def.range) return false;
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool AllyInterceptsShot(Vec2 muzzle, Vec2 targetPos, int shooterIndex, const SolverContext& ctx)
|
|
|
|
|
|
{
|
|
|
|
|
|
const Agent& shooter = ctx.agents[shooterIndex];
|
|
|
|
|
|
const Vec2 dir = Normalized(targetPos - muzzle);
|
|
|
|
|
|
const float speed = std::max(0.001f, shooter.gun.def.bulletSpeed);
|
|
|
|
|
|
const float tImpact = Distance(muzzle, targetPos) / speed;
|
|
|
|
|
|
// Пуля летит внутри конуса, поэтому её «толщина» растёт с пройденным путём.
|
|
|
|
|
|
const float spreadK = SpreadTan(shooter);
|
|
|
|
|
|
|
|
|
|
|
|
// Идём по времени полёта: где будет пуля и где будет союзник.
|
|
|
|
|
|
// Радиус безопасности растёт со временем — союзник может и повернуть,
|
|
|
|
|
|
// а линейная экстраполяция скорости это не ловит.
|
|
|
|
|
|
constexpr int SAMPLES = 6;
|
|
|
|
|
|
for (int k = 0; k <= SAMPLES; ++k)
|
|
|
|
|
|
{
|
|
|
|
|
|
const float t = tImpact * (float(k) / float(SAMPLES));
|
|
|
|
|
|
const Vec2 bulletPos = muzzle + dir * (speed * t);
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < ctx.agentCount; ++i)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (i == shooterIndex) continue;
|
|
|
|
|
|
const Agent& ally = ctx.agents[i];
|
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-14 01:07:19 +07:00
|
|
|
|
if (!ally.alive) continue;
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
const float r = ally.bodyRadius + shooter.gun.def.bulletRadius + g_tune.safetyMargin +
|
|
|
|
|
|
g_tune.shotPredictSlack * MaxAgentSpeed() * t +
|
|
|
|
|
|
spreadK * speed * t;
|
|
|
|
|
|
|
|
|
|
|
|
Vec2 predicted[2];
|
|
|
|
|
|
const int n = PredictAllyPositions(ally, t, predicted);
|
|
|
|
|
|
for (int q = 0; q < n; ++q)
|
|
|
|
|
|
if (DistanceSq(bulletPos, predicted[q]) < r * r) return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int NearestAliveTarget(Vec2 from, const SolverContext& ctx, float searchRadius)
|
|
|
|
|
|
{
|
|
|
|
|
|
const std::vector<Target>& targets = *ctx.targets;
|
|
|
|
|
|
int best = -1;
|
|
|
|
|
|
float bestD = searchRadius;
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < int(targets.size()); ++i)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!targets[i].alive) continue;
|
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-14 01:07:19 +07:00
|
|
|
|
if (targets[i].IsProp()) continue; // хлам ближайшей целью не бывает
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
const float d = Distance(from, targets[i].pos);
|
|
|
|
|
|
if (d < bestD)
|
|
|
|
|
|
{
|
|
|
|
|
|
bestD = d;
|
|
|
|
|
|
best = i;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return best;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
// Выбор цели: 6.4 (ближайшая с чистой линией) + 6.8 (приоритет назначенной).
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
void EvaluateAgentLane(Agent& a, int index, const SolverContext& ctx)
|
|
|
|
|
|
{
|
|
|
|
|
|
const std::vector<Target>& targets = *ctx.targets;
|
|
|
|
|
|
|
|
|
|
|
|
a.hasLane = false;
|
|
|
|
|
|
a.priorityLane = false;
|
|
|
|
|
|
a.laneIntent = false;
|
|
|
|
|
|
a.currentTargetId = -1;
|
|
|
|
|
|
a.targetBlockedByAlly = false;
|
|
|
|
|
|
a.targetBlockedByWall = false;
|
|
|
|
|
|
|
|
|
|
|
|
// --- ручной режим: сначала назначенная цель ---
|
|
|
|
|
|
if (ctx.assignedTargetId >= 0 && ctx.assignedTargetId < int(targets.size()) &&
|
|
|
|
|
|
targets[ctx.assignedTargetId].alive)
|
|
|
|
|
|
{
|
|
|
|
|
|
const Vec2 tp = targets[ctx.assignedTargetId].pos;
|
|
|
|
|
|
a.laneEnd = tp;
|
|
|
|
|
|
|
|
|
|
|
|
if (LaneClearFrom(a.pos, index, tp, ctx))
|
|
|
|
|
|
{
|
|
|
|
|
|
a.currentTargetId = ctx.assignedTargetId;
|
|
|
|
|
|
a.hasLane = true;
|
|
|
|
|
|
a.priorityLane = true;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
ClassifyBlock(a.pos, index, tp, ctx, a.targetBlockedByAlly, a.targetBlockedByWall);
|
|
|
|
|
|
a.laneIntent = a.targetBlockedByAlly && !a.targetBlockedByWall &&
|
|
|
|
|
|
Distance(a.pos, tp) <= a.gun.def.range + a.gun.def.muzzleOffset;
|
|
|
|
|
|
|
|
|
|
|
|
// Вторичные цели — только в движении и только при включённом флаге (6.8).
|
|
|
|
|
|
if (!(a.state == AgentState::ADVANCING && g_tune.freeFireSecondary)) return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- авто: ближайшая цель, до которой линия чиста ---
|
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-14 01:07:19 +07:00
|
|
|
|
// Два прохода, и порядок между ними — это и есть приоритет. Сначала твари;
|
|
|
|
|
|
// обстановка рассматривается только когда противника в поле зрения нет
|
|
|
|
|
|
// вообще, иначе боец бросал бы врага ради ящика за его спиной.
|
|
|
|
|
|
auto nearestClear = [&](bool wantProps) {
|
|
|
|
|
|
int best = -1;
|
|
|
|
|
|
float bestD = BIG_F;
|
|
|
|
|
|
for (int i = 0; i < int(targets.size()); ++i)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!targets[i].alive) continue;
|
|
|
|
|
|
if (targets[i].IsProp() != wantProps) continue;
|
|
|
|
|
|
const float d = Distance(a.pos, targets[i].pos);
|
|
|
|
|
|
if (d >= bestD || d > a.gun.def.range + a.gun.def.muzzleOffset) continue;
|
|
|
|
|
|
if (!LaneClearFrom(a.pos, index, targets[i].pos, ctx)) continue;
|
|
|
|
|
|
best = i;
|
|
|
|
|
|
bestD = d;
|
|
|
|
|
|
}
|
|
|
|
|
|
return best;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
int best = nearestClear(false);
|
|
|
|
|
|
const bool onProp = (best < 0 && !EnemyNearby(a.pos, ctx));
|
|
|
|
|
|
if (onProp) best = nearestClear(true);
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
|
|
|
|
|
|
if (best >= 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
a.currentTargetId = best;
|
|
|
|
|
|
a.hasLane = true;
|
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-14 01:07:19 +07:00
|
|
|
|
// Линия к хламу приоритетной не бывает: приоритет запрещает решателю
|
|
|
|
|
|
// искать позицию получше, а ради ящика боец переступать обязан.
|
|
|
|
|
|
a.priorityLane = (ctx.assignedTargetId < 0) && !onProp;
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
a.laneEnd = targets[best].pos;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Линии нет — для оверлея показываем ближайшую цель и причину.
|
|
|
|
|
|
if (ctx.assignedTargetId < 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
const int nearest = NearestAliveTarget(a.pos, ctx, a.gun.def.range);
|
|
|
|
|
|
if (nearest >= 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
a.laneEnd = targets[nearest].pos;
|
|
|
|
|
|
ClassifyBlock(a.pos, index, targets[nearest].pos, ctx,
|
|
|
|
|
|
a.targetBlockedByAlly, a.targetBlockedByWall);
|
|
|
|
|
|
// Линию перекрыл только свой -> намерение стрелять остаётся, и линия
|
|
|
|
|
|
// всё равно резервируется (6.6), иначе перекрывшего ничто не сдвинет.
|
|
|
|
|
|
a.laneIntent = a.targetBlockedByAlly && !a.targetBlockedByWall;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
// 6.5 Оценка кандидата
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
float ScoreCandidate(Vec2 cand, const Agent& a, int index, const SolverContext& ctx,
|
|
|
|
|
|
int bestTargetId)
|
|
|
|
|
|
{
|
|
|
|
|
|
const std::vector<Target>& targets = *ctx.targets;
|
|
|
|
|
|
float score = 0.0f;
|
|
|
|
|
|
|
|
|
|
|
|
if (bestTargetId >= 0 && targets[bestTargetId].alive &&
|
|
|
|
|
|
LaneClearFrom(cand, index, targets[bestTargetId].pos, ctx))
|
|
|
|
|
|
score += g_tune.sLaneClear;
|
|
|
|
|
|
|
|
|
|
|
|
int shootable = 0;
|
|
|
|
|
|
int total = 0;
|
|
|
|
|
|
float nearestDist = BIG_F;
|
|
|
|
|
|
for (int i = 0; i < int(targets.size()); ++i)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!targets[i].alive) continue;
|
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-14 01:07:19 +07:00
|
|
|
|
// Покрытие считается по ПРОТИВНИКАМ. Позиция, с которой видно три
|
|
|
|
|
|
// ящика, огневой не является, а «слишком близко» к бочке — не штраф.
|
|
|
|
|
|
if (targets[i].IsProp()) continue;
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
++total;
|
|
|
|
|
|
nearestDist = std::min(nearestDist, Distance(cand, targets[i].pos));
|
|
|
|
|
|
if (LaneClearFrom(cand, index, targets[i].pos, ctx)) ++shootable;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (total > 0) score += g_tune.sCoverage * float(shootable) / float(total);
|
|
|
|
|
|
|
|
|
|
|
|
if (ctx.lanes)
|
|
|
|
|
|
score -= g_tune.sBlockAlly * float(ctx.lanes->CountBlocked(index, cand, a.bodyRadius));
|
|
|
|
|
|
|
|
|
|
|
|
score -= g_tune.sDistCost * Distance(cand, a.pos);
|
|
|
|
|
|
score -= g_tune.sCohesionCost *
|
|
|
|
|
|
std::max(0.0f, Distance(cand, ctx.anchor) - g_tune.cohesionRadius);
|
|
|
|
|
|
|
|
|
|
|
|
if (nearestDist < g_tune.minEngageDist) score -= g_tune.sTooClose;
|
|
|
|
|
|
|
|
|
|
|
|
return score;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
// 6.5 Решатель: 3 кольца x 12 направлений + текущая позиция, коммит с гистерезисом
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
bool SolveFiringPosition(Agent& a, int index, const SolverContext& ctx, SolveResult& out)
|
|
|
|
|
|
{
|
|
|
|
|
|
const std::vector<Target>& targets = *ctx.targets;
|
|
|
|
|
|
|
|
|
|
|
|
out.count = 0;
|
|
|
|
|
|
out.bestIndex = -1;
|
|
|
|
|
|
out.committed = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Цель решателя: назначенная (ручной режим) либо ближайшая достижимая.
|
|
|
|
|
|
int bestTargetId = -1;
|
|
|
|
|
|
if (ctx.assignedTargetId >= 0 && ctx.assignedTargetId < int(targets.size()) &&
|
|
|
|
|
|
targets[ctx.assignedTargetId].alive)
|
|
|
|
|
|
bestTargetId = ctx.assignedTargetId;
|
|
|
|
|
|
else
|
|
|
|
|
|
bestTargetId = NearestAliveTarget(a.pos, ctx, a.gun.def.range + g_tune.ring3);
|
|
|
|
|
|
|
|
|
|
|
|
out.bestTargetId = bestTargetId;
|
|
|
|
|
|
if (bestTargetId < 0) return false; // стрелять не по чему — стоим
|
|
|
|
|
|
|
|
|
|
|
|
const float rings[SOLVER_RINGS] = {g_tune.ring1, g_tune.ring2, g_tune.ring3};
|
|
|
|
|
|
|
|
|
|
|
|
auto push = [&](Vec2 p, bool forceValid) {
|
|
|
|
|
|
if (out.count >= SOLVER_CANDIDATES) return;
|
|
|
|
|
|
const int i = out.count++;
|
|
|
|
|
|
out.candidates[i] = p;
|
|
|
|
|
|
out.valid[i] = true;
|
|
|
|
|
|
out.scores[i] = 0.0f;
|
|
|
|
|
|
|
|
|
|
|
|
if (!forceValid)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Отбраковка: вне карты / в стене / за поводком от диска /
|
|
|
|
|
|
// слишком близко к другому агенту.
|
|
|
|
|
|
if (p.x < 1.0f || p.y < 1.0f || p.x > float(MAP_W) - 1.0f || p.y > float(MAP_H) - 1.0f)
|
|
|
|
|
|
out.valid[i] = false;
|
|
|
|
|
|
else if (Distance(p, ctx.anchor) > g_tune.cohesionRadius + g_tune.postLeash)
|
|
|
|
|
|
out.valid[i] = false;
|
|
|
|
|
|
else if (ctx.map->CircleHitsWall(p, a.bodyRadius))
|
|
|
|
|
|
out.valid[i] = false;
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int k = 0; k < ctx.agentCount; ++k)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (k == index) continue;
|
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-14 01:07:19 +07:00
|
|
|
|
if (!ctx.agents[k].alive) continue;
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
if (Distance(p, ctx.agents[k].pos) < g_tune.candMinAlly)
|
|
|
|
|
|
{
|
|
|
|
|
|
out.valid[i] = false;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (out.valid[i]) out.scores[i] = ScoreCandidate(p, a, index, ctx, bestTargetId);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
push(a.pos, true); // индекс 0 — текущая позиция, для сравнения
|
|
|
|
|
|
out.currentScore = out.scores[0];
|
|
|
|
|
|
|
|
|
|
|
|
for (int r = 0; r < SOLVER_RINGS; ++r)
|
|
|
|
|
|
for (int k = 0; k < SOLVER_DIRS; ++k)
|
|
|
|
|
|
push(a.pos + FromAngle(float(k) * (TWO_PI_F / float(SOLVER_DIRS))) * rings[r], false);
|
|
|
|
|
|
|
|
|
|
|
|
out.bestScore = out.currentScore;
|
|
|
|
|
|
for (int i = 1; i < out.count; ++i)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!out.valid[i]) continue;
|
|
|
|
|
|
if (out.scores[i] > out.bestScore)
|
|
|
|
|
|
{
|
|
|
|
|
|
out.bestScore = out.scores[i];
|
|
|
|
|
|
out.bestIndex = i;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Коммит с гистерезисом — иначе агент дёргается между двумя точками.
|
|
|
|
|
|
if (out.bestIndex > 0 && out.bestScore > out.currentScore + g_tune.hysteresis)
|
|
|
|
|
|
{
|
|
|
|
|
|
a.postPos = out.candidates[out.bestIndex];
|
|
|
|
|
|
a.commitTimer = g_tune.commitTime;
|
|
|
|
|
|
out.committed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|