Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (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/melee.h"
|
|
|
|
|
|
|
|
|
|
|
|
namespace
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
constexpr float BIG_F = 1e18f;
|
|
|
|
|
|
|
|
|
|
|
|
// Угловой полуразмер тела радиуса r, видимого с расстояния dist.
|
|
|
|
|
|
// Вплотную союзник перекрывает почти весь сектор — так и должно быть.
|
|
|
|
|
|
float AngularHalfSize(float dist, float r)
|
|
|
|
|
|
{
|
|
|
|
|
|
return std::asin(Clampf(r / std::max(dist, r), 0.0f, 1.0f));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Годится ли мишень как цель ближнего боя: жива, внутри СВОЕГО круга бойца,
|
|
|
|
|
|
// не дальше meleeSeekRadius от него самого и видна (за стену не бегаем —
|
|
|
|
|
|
// путей в прототипе нет, а обход угла даёт слой wallAvoidance).
|
|
|
|
|
|
bool MeleeCandidate(const Agent& a, const SolverContext& ctx, int id)
|
|
|
|
|
|
{
|
|
|
|
|
|
const Target& t = (*ctx.targets)[id];
|
|
|
|
|
|
if (!t.alive) return false;
|
|
|
|
|
|
if (Distance(t.pos, ctx.anchor) > g_tune.meleeCohesionRadius) return false;
|
|
|
|
|
|
if (Distance(t.pos, a.pos) > g_tune.meleeSeekRadius) return false;
|
|
|
|
|
|
if (ctx.map->RaycastBlocked(a.pos, t.pos)) return false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
|
|
void EvaluateMeleeTarget(Agent& a, int index, const SolverContext& ctx)
|
|
|
|
|
|
{
|
|
|
|
|
|
const std::vector<Target>& targets = *ctx.targets;
|
|
|
|
|
|
|
|
|
|
|
|
// Линии огня у бойца нет и быть не может — обнуляем всю стрелковую часть,
|
|
|
|
|
|
// иначе он попадёт и в реестр линий, и в FIRE UPTIME.
|
|
|
|
|
|
a.hasLane = false;
|
|
|
|
|
|
a.priorityLane = false;
|
|
|
|
|
|
a.laneIntent = false;
|
|
|
|
|
|
a.targetBlockedByAlly = false;
|
|
|
|
|
|
a.targetBlockedByWall = false;
|
|
|
|
|
|
|
|
|
|
|
|
MeleeWeapon& m = a.melee;
|
|
|
|
|
|
|
|
|
|
|
|
// Пока идёт замах или удар, цель не переназначается: ось уже зафиксирована.
|
|
|
|
|
|
if (m.Committed() && m.targetId >= 0 && targets[m.targetId].alive)
|
|
|
|
|
|
{
|
|
|
|
|
|
a.currentTargetId = m.targetId;
|
|
|
|
|
|
a.laneEnd = targets[m.targetId].pos;
|
|
|
|
|
|
m.inReach = Distance(a.pos, targets[m.targetId].pos) <=
|
|
|
|
|
|
m.reach + targets[m.targetId].radius;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
m.targetId = -1;
|
|
|
|
|
|
m.inReach = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Назначенная игроком цель приоритетна и здесь (6.8), но только если она
|
|
|
|
|
|
// проходит те же условия — иначе боец убежал бы через всю карту.
|
|
|
|
|
|
int best = -1;
|
|
|
|
|
|
if (ctx.assignedTargetId >= 0 && ctx.assignedTargetId < int(targets.size()) &&
|
|
|
|
|
|
MeleeCandidate(a, ctx, ctx.assignedTargetId))
|
|
|
|
|
|
{
|
|
|
|
|
|
best = ctx.assignedTargetId;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
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
|
|
|
|
// Обстановка — только когда вокруг чисто. Предикат общий со стрелком
|
|
|
|
|
|
// (EnemyNearby): ящик за спиной не уводит с боя ни клинок, ни ствол.
|
|
|
|
|
|
const bool wantProps = !EnemyNearby(a.pos, ctx);
|
|
|
|
|
|
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (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 bestCost = BIG_F;
|
|
|
|
|
|
for (int i = 0; i < int(targets.size()); ++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 (targets[i].IsProp() != wantProps) 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 (!MeleeCandidate(a, ctx, i)) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Цена = дистанция + штраф за каждого своего, уже вцепившегося
|
|
|
|
|
|
// в эту мишень. Ближняя цель всё ещё выигрывает, но не любой ценой:
|
|
|
|
|
|
// вдвоём на одну — это оборванные взмахи друг об друга и ноль урона.
|
|
|
|
|
|
float cost = Distance(a.pos, targets[i].pos);
|
|
|
|
|
|
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 (ctx.agents[k].IsMelee() && ctx.agents[k].melee.targetId == i)
|
|
|
|
|
|
cost += g_tune.meleeClaimPenalty;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (cost >= bestCost) continue;
|
|
|
|
|
|
best = i;
|
|
|
|
|
|
bestCost = cost;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
m.targetId = best;
|
|
|
|
|
|
a.currentTargetId = best;
|
|
|
|
|
|
if (best < 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
a.laneEnd = targets[best].pos;
|
|
|
|
|
|
m.inReach = Distance(a.pos, targets[best].pos) <= m.reach + targets[best].radius;
|
|
|
|
|
|
m.arcBlockedByAlly = m.inReach && !MeleeArcClear(a, index, AngleOf(targets[best].pos - a.pos), ctx);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Vec2 MeleeGoal(const Agent& a, const SolverContext& ctx, Vec2 fallbackSlot)
|
|
|
|
|
|
{
|
|
|
|
|
|
const MeleeWeapon& m = a.melee;
|
|
|
|
|
|
if (m.targetId < 0) return fallbackSlot;
|
|
|
|
|
|
|
|
|
|
|
|
const Target& t = (*ctx.targets)[m.targetId];
|
|
|
|
|
|
|
|
|
|
|
|
// Встаём вплотную, но не внутрь: reach считается от центра тела, поэтому
|
|
|
|
|
|
// с такой дистанции удар достаёт с запасом.
|
|
|
|
|
|
const float stand = t.radius + a.bodyRadius + g_tune.meleeGap;
|
|
|
|
|
|
const Vec2 away = Normalized(a.pos - t.pos, FromAngle(a.facing + PI_F));
|
|
|
|
|
|
return t.pos + away * stand;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool MeleeArcClear(const Agent& a, int index, float axisAngle, const SolverContext& ctx)
|
|
|
|
|
|
{
|
|
|
|
|
|
const MeleeWeapon& m = a.melee;
|
|
|
|
|
|
const float half = m.HalfArc();
|
|
|
|
|
|
const float reach = m.reach + g_tune.meleeAllyMargin;
|
|
|
|
|
|
|
|
|
|
|
|
// Взмах живёт windup + strike секунд, и всё это время союзник продолжает
|
|
|
|
|
|
// идти. Проверять только текущее положение — значит начинать удары, которые
|
|
|
|
|
|
// всё равно придётся обрывать. Это ровно та же поправка, что и
|
|
|
|
|
|
// AllyInterceptsShot для пули: правило не ослабляется, оно смотрит вперёд.
|
|
|
|
|
|
const float horizon = (m.windup + m.strike) * g_tune.meleeStartPredict;
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < ctx.agentCount; ++i)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (i == index) 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 Vec2 probe[2] = {ally.pos, ally.pos + ally.vel * horizon};
|
|
|
|
|
|
for (const Vec2& q : probe)
|
|
|
|
|
|
{
|
|
|
|
|
|
const Vec2 d = q - a.pos;
|
|
|
|
|
|
const float dist = Length(d);
|
|
|
|
|
|
if (dist > reach + ally.bodyRadius) continue;
|
|
|
|
|
|
if (dist < 1e-4f) return false; // стоим в одной точке — бить нельзя
|
|
|
|
|
|
|
|
|
|
|
|
const float pad = AngularHalfSize(dist, ally.bodyRadius + g_tune.meleeAllyMargin);
|
|
|
|
|
|
if (std::fabs(AngleDiff(axisAngle, AngleOf(d))) <= half + pad) return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool BladeSweepHits(Vec2 from, float angPrev, float angCur, float bladeHalf, float reach,
|
|
|
|
|
|
Vec2 p, float pRadius)
|
|
|
|
|
|
{
|
|
|
|
|
|
const Vec2 d = p - from;
|
|
|
|
|
|
const float dist = Length(d);
|
|
|
|
|
|
if (dist > reach + pRadius) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Тела перекрываются — попадает любой взмах, и это честно: цель обнимает
|
|
|
|
|
|
// рукоять. Случай редкий, отсечён отдельно, чтобы ниже не делить на ноль.
|
|
|
|
|
|
if (dist <= pRadius) return true;
|
|
|
|
|
|
|
|
|
|
|
|
// Дальше — ТОЧНАЯ геометрия: лезвие это ОТРЕЗОК от бойца длиной reach,
|
|
|
|
|
|
// повернувшийся за шаг с angPrev на angCur.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Раньше здесь стоял угловой допуск asin(r/dist), и он вырождался: на
|
|
|
|
|
|
// контактной дистанции (dist -> pRadius) asin даёт 90 градусов, «сектор»
|
|
|
|
|
|
// разрастался в полуплоскость, и боец рубил за спину. В фазе рывка он как
|
|
|
|
|
|
// раз проезжает вплотную — отсюда и брались удары «на 360 градусов».
|
|
|
|
|
|
// Отрезок такого не допускает: для цели позади ближайшая точка лезвия —
|
|
|
|
|
|
// рукоять, а до неё ровно dist, что больше pRadius.
|
|
|
|
|
|
const float ang = AngleOf(d);
|
|
|
|
|
|
const float sweep = AngleDiff(angPrev, angCur); // короткая дельта за шаг
|
|
|
|
|
|
const float rel = AngleDiff(angPrev, ang); // где цель относительно старта
|
|
|
|
|
|
|
|
|
|
|
|
// Направление на цель попало в заметённую за шаг полосу — значит лезвие
|
|
|
|
|
|
// прошло через неё (дистанцию уже проверили сверху).
|
|
|
|
|
|
const float lo = std::min(0.0f, sweep) - bladeHalf;
|
|
|
|
|
|
const float hi = std::max(0.0f, sweep) + bladeHalf;
|
|
|
|
|
|
if (rel >= lo && rel <= hi) return true;
|
|
|
|
|
|
|
|
|
|
|
|
// Иначе цель могла быть задета краем: проверяем оба крайних положения.
|
|
|
|
|
|
const float ends[2] = {angPrev, angCur};
|
|
|
|
|
|
for (float a : ends)
|
|
|
|
|
|
if (DistancePointToSegment(p, from, from + FromAngle(a) * reach) <= pRadius) return true;
|
|
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Vec2 MeleeStrikeEnd(const Agent& a, const SolverContext& ctx)
|
|
|
|
|
|
{
|
|
|
|
|
|
const MeleeWeapon& m = a.melee;
|
|
|
|
|
|
// На замахе и ударе ось уже зафиксирована, до него — направление на цель.
|
|
|
|
|
|
float axis = m.dir;
|
|
|
|
|
|
if (!m.Committed())
|
|
|
|
|
|
{
|
|
|
|
|
|
if (m.targetId < 0) return a.pos;
|
|
|
|
|
|
axis = AngleOf((*ctx.targets)[m.targetId].pos - a.pos);
|
|
|
|
|
|
}
|
|
|
|
|
|
return a.pos + FromAngle(axis) * m.reach;
|
|
|
|
|
|
}
|