Поле путей: отставший возвращается в строй (M37)
Отряд из пяти не проходил первый этаж ни на одном сиде, а одиночка проходил. Дело было не в ширине коридоров: у бойцов не было поиска пути вообще. Боец, оказавшийся за стеной от своего слота, seek'ом тянулся В СТЕНУ, обход стены толкал его обратно, и он замирал в углу навсегда. Теперь у каждого бойца своё поле путей (BFS от клетки его цели) с натягиванием верёвки: он берёт самую дальнюю точку пути, которую видит. Включается обход по двум условиям сразу — боец упёрся (хочет идти и не едет) И потерял строй. Счётчик упора сбрасывается по факту «цель видно», а не «поехал»: сброс по скорости давал дребезг, и боец полз шаг за две секунды. По дороге вскрылись две неверные МЕРКИ: * критерий 11.6 считал колонной «разброс вдоль больше, чем поперёк» — и потому награждал отряд, у которого двое навсегда остались по ту сторону стены. Теперь колонна меряется самим проёмом: разом в горловине не больше двух, и прошли ВСЕ. * зонд --wall читал «стены не нашлось» (ноль) как «стена на нулевом этаже» и объявлял пятёрку, не сломавшуюся нигде, худшей из трёх. Обе переписаны и проверены на сломанной сборке. Замеры: --play 6 5 на восьми сидах 47/48 (было 0/48), --accept PASS, шаг 6.1 мкс, --wall 30 одиночка D20 / трое D20 / пятеро D31 (не найдена), --descend 8/30, hollow --descend 6, --ui-check, оба проекта --check — без ошибок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
aa9a175751
commit
c1f79a1fb8
@ -148,8 +148,50 @@ muzzle = from + normalize(target - from) * muzzleOffset // 0.3 юнита
|
|||||||
отталкивание уравновешиваются, и агент залипает в «тени» препятствия — это
|
отталкивание уравновешиваются, и агент залипает в «тени» препятствия — это
|
||||||
реально ловилось в прогонах (агент оставался в комнате, когда отряд ушёл).
|
реально ловилось в прогонах (агент оставался в комнате, когда отряд ушёл).
|
||||||
|
|
||||||
Скорость 1.75 юнита/с, 2.7 на `Shift`. Скорость подтягивается к желаемой с
|
Скорость 2.6 юнита/с, 4.4 на пределе натяжения. Скорость подтягивается к
|
||||||
ограничением `steerAccel`, чтобы резкие развороты не телепортировали агента.
|
желаемой с ограничением `steerAccel`, чтобы резкие развороты не телепортировали
|
||||||
|
агента.
|
||||||
|
|
||||||
|
## 5b. Поле путей: как отставший возвращается в строй
|
||||||
|
|
||||||
|
Слои выше — локальные, и этого хватает ровно до первого угла. Боец, оказавшийся
|
||||||
|
за стеной от своего слота, seek'ом тянулся В СТЕНУ, слой 4 толкал его обратно, и
|
||||||
|
он замирал в углу **навсегда**. Снаружи это выглядело как «пятеро не проходят
|
||||||
|
там, где одиночка проходит»: `--play 6 5` вставал на первом же этаже на всех
|
||||||
|
восьми сидах. Ширина коридоров тут ни при чём — отряд вставал и на проходах в
|
||||||
|
три, четыре и пять клеток.
|
||||||
|
|
||||||
|
Устройство (`Squad::RebuildNav`, `Squad::NavWaypoint`, `sim/squad.cpp`):
|
||||||
|
|
||||||
|
- BFS по проходимым клеткам **от клетки цели**, четыре соседа (по диагонали
|
||||||
|
круглое тело в щель между углами не пролезает);
|
||||||
|
- боец спускается по градиенту и берёт **самую дальнюю точку пути, которую ещё
|
||||||
|
видит** — натянутая верёвка; идёт он по ней как по прямой;
|
||||||
|
- поле **у каждого своё**. Одно общее поле до якоря сводило всех обходящих в
|
||||||
|
одну точку, и отряд входил в проход лужей вместо колонны.
|
||||||
|
|
||||||
|
Включается обход по ДВУМ условиям сразу:
|
||||||
|
|
||||||
|
| Условие | Зачем |
|
||||||
|
|---|---|
|
||||||
|
| `stuckFor > navStuckTime` (0.35 с) — хочет идти и не едет | обход нужен тому, кто реально упёрся |
|
||||||
|
| отстал от слота дальше `straggleDist` (2.8) | боец на своём месте не потерялся, его держит бой |
|
||||||
|
|
||||||
|
Счётчик `stuckFor` сбрасывается **не по факту «поехал», а по факту «цель
|
||||||
|
видно»**. Разница принципиальная: сброс по скорости давал дребезг — обход трогал
|
||||||
|
бойца с места, поле выключалось, боец упирался в ту же стену, и так по кругу со
|
||||||
|
средней скоростью шаг за две секунды.
|
||||||
|
|
||||||
|
Платим только когда обход реально включён: поле пересчитывается на смену клетки
|
||||||
|
цели и только у того, кто сейчас идёт в обход. Средний шаг симуляции — 6.1 мкс
|
||||||
|
при бюджете кадра 16666 мкс.
|
||||||
|
|
||||||
|
**Замеры** (`--play 6 5`, восемь сидов):
|
||||||
|
|
||||||
|
| Сборка | Этажей пройдено |
|
||||||
|
|---|---|
|
||||||
|
| до поля путей | 0 из 48 — отряд вставал на первом этаже |
|
||||||
|
| поле путей | 47 из 48 (единственная потеря — вайп в бою, не затык) |
|
||||||
|
|
||||||
## 5a. Диск и поводок
|
## 5a. Диск и поводок
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,14 @@
|
|||||||
|
|
||||||
| **M33** | крафт стал выбором: закалка (двигает рулон выбранного аффикса) и отпечаток (переносит аффикс с другой вещи, сжигая её), отсечение и возвышение работают по ВЫБРАННОМУ аффиксу, направленные операции вдвое дороже слепых | прежние пять операций были слепыми — игрок жал кнопку и смотрел, что выпало; проверка меряет направленность, а не срабатывание: отсечение пробуется на СРЕДНЕМ аффиксе из трёх, иначе слепая версия прошла бы её тоже; `--accept` PASS ([22-loot.md](22-loot.md)) |
|
| **M33** | крафт стал выбором: закалка (двигает рулон выбранного аффикса) и отпечаток (переносит аффикс с другой вещи, сжигая её), отсечение и возвышение работают по ВЫБРАННОМУ аффиксу, направленные операции вдвое дороже слепых | прежние пять операций были слепыми — игрок жал кнопку и смотрел, что выпало; проверка меряет направленность, а не срабатывание: отсечение пробуется на СРЕДНЕМ аффиксе из трёх, иначе слепая версия прошла бы её тоже; `--accept` PASS ([22-loot.md](22-loot.md)) |
|
||||||
|
|
||||||
|
| **M34** | семь новых тварей на глубины 9–73: акты II–VI перестали быть пустыми | `--tool enemies` и `--tool ttk`: каждая тварь попадает в свою полосу глубины, время убийства держится в коридоре ([16-descent.md](16-descent.md)) |
|
||||||
|
|
||||||
|
| **M35** | 19 новых вещей (тиры 1–6, глубины 3–31), исправлена кривая редкости и выбор тиров аффиксов | `--tool drops` и `--tool affix`: добыча перестала быть мусором, ядро тега гарантируется ([22-loot.md](22-loot.md)) |
|
||||||
|
|
||||||
|
| **M36** | пистолет не пропадает при переодевании, эвакуация с ЛЮБОГО этажа, кнопки игровых меню снизу HUD, твари не воскресают в пределах забега | пропажа была потерей экземпляра: снаряжение бойца не сохранялось в профиль после перекладывания; `--ui-check` и `--descend 8` PASS ([13-combat.md](13-combat.md)) |
|
||||||
|
|
||||||
|
| **M37** | **поле путей**: отставший боец возвращается в строй по геометрии уровня, а не по прямой; обход включается только тому, кто упёрся и потерял строй | это был тот самый «отряд не проходит там, где одиночка проходит»: `--play 6 5` вставал на первом этаже на всех восьми сидах, стало 47 из 48 (единственная потеря — вайп в бою); по дороге вскрылись ДВЕ неверные мерки: критерий 11.6 награждал брошенных за стеной бойцов (растянутый отряд = «колонна»), а `--wall` читал «стены не нашлось» как «стена на нулевом этаже» и объявлял лучший результат провалом; обе переписаны и проверены на сломанной сборке; шаг симуляции 6.1 мкс; `--accept` PASS ([05-squad-ai.md](05-squad-ai.md), [08-decisions.md](08-decisions.md)) |
|
||||||
|
|
||||||
## Что находилось по ходу и чинилось
|
## Что находилось по ходу и чинилось
|
||||||
|
|
||||||
Прогоны без окна оказались полезнее, чем разглядывание кадра: три дефекта,
|
Прогоны без окна оказались полезнее, чем разглядывание кадра: три дефекта,
|
||||||
|
|||||||
@ -280,6 +280,45 @@ seek тянет вперёд, отталкивание держит — аген
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### A22. Критерий 11.6: «колонна» меряется проёмом, а не разбросом
|
||||||
|
|
||||||
|
**Было**: колонной считалось «разброс вдоль движения больше, чем поперёк,
|
||||||
|
в полтора раза» — по бойцам в створе прохода вместе с подходами.
|
||||||
|
|
||||||
|
**Стало**: колонна — это про сам проём. Одновременно в горловине не больше
|
||||||
|
двух (проём шириной два тайла), и через неё прошли **все** живые бойцы.
|
||||||
|
|
||||||
|
**Почему**. Старая мерка награждала ровно ту поломку, ради которой писалась.
|
||||||
|
Отряд, у которого двое навсегда остались по ту сторону стены (у них не было
|
||||||
|
поиска пути и вернуться они не могли), растянут вдоль на шесть тайлов — и
|
||||||
|
проверку ПРОХОДИЛ. Тот же отряд, когда отставшие научились обходить стену и
|
||||||
|
собираться, стал компактным — и проверку ронял. Замер трасс показал это прямо:
|
||||||
|
без обхода бойцы 3 и 4 все 26 секунд стоят на y≈16.3 и y≈16.9, южнее стены.
|
||||||
|
|
||||||
|
**Замер**: на исправленной сборке — прошли 5 из 5, в горловине разом до 2,
|
||||||
|
FRIENDLY_HITS=0, сквозь своего 0. На сборке без обхода (`--set navStuckTime=99`)
|
||||||
|
критерий падает: прошли 3 из 5. То есть мерка стала строже: раньше она
|
||||||
|
пропускала брошенных за стеной, теперь нет.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### A23. Зонд `--wall`: «стены не нашлось» — это не «стена на нулевом этаже»
|
||||||
|
|
||||||
|
**Было**: глубина первой стены хранилась нулём как «ещё не найдена», и итог
|
||||||
|
сравнивал `wall[пятеро] <= wall[одиночка]` буквально.
|
||||||
|
|
||||||
|
**Стало**: не найденная стена читается как «глубже последней проверенной»
|
||||||
|
(`maxDepth + 1`) и печатается как `D31 (не найдена)`.
|
||||||
|
|
||||||
|
**Почему**. Пятёрка перестала ломаться на всех проверяемых глубинах — то есть
|
||||||
|
показала лучший возможный результат, — и зонд объявил это провалом: ноль меньше
|
||||||
|
двадцати. Ошибка мерки, а не игры, того же рода, что и A22.
|
||||||
|
|
||||||
|
**Замер**: `--wall 30` — одиночка D20, трое D20, пятеро D31 (не найдена),
|
||||||
|
код возврата 0.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## B. Структурные (поведение не меняют)
|
## B. Структурные (поведение не меняют)
|
||||||
|
|
||||||
### B1. Дополнительные файлы
|
### B1. Дополнительные файлы
|
||||||
@ -323,5 +362,7 @@ seek тянет вперёд, отталкивание держит — аген
|
|||||||
- `SETTLE TIME` по той же причине растёт, если отряд остановился там, где стрелять
|
- `SETTLE TIME` по той же причине растёт, если отряд остановился там, где стрелять
|
||||||
не по чему; в общем прогоне попадаются значения до 5.7 с. В обстановке, к
|
не по чему; в общем прогоне попадаются значения до 5.7 с. В обстановке, к
|
||||||
которой относится критерий 11.1 (мишени в радиусе), — 0.02 с.
|
которой относится критерий 11.1 (мишени в радиусе), — 0.02 с.
|
||||||
- Пути агентов чисто локальные, без поиска пути. Отряд проходит узкие коридоры,
|
- Поиск пути включается ТОЛЬКО для отставшего, который упёрся (см.
|
||||||
но лабиринт с тупиками не осилит — это вне рамок прототипа.
|
[05-squad-ai.md](05-squad-ai.md), «поле путей»). Боец в строю по-прежнему
|
||||||
|
ходит локальными слоями, и это осознанно: строй должен вести себя как строй,
|
||||||
|
а не как пять независимых искателей пути.
|
||||||
|
|||||||
@ -374,6 +374,10 @@ int RunAccept()
|
|||||||
int throughAllyShots = 0; // выстрелов, прошедших через тело союзника
|
int throughAllyShots = 0; // выстрелов, прошедших через тело союзника
|
||||||
int samples = 0;
|
int samples = 0;
|
||||||
|
|
||||||
|
// --- проход гуськом --------------------------------------------------
|
||||||
|
int maxInThroat = 0; // сколько разом влезло в сам проём
|
||||||
|
bool crossed[NUM_AGENTS] = {}; // кто оказался по ту сторону стены
|
||||||
|
|
||||||
// Две фазы: сначала входим в проход, потом ВСТАЁМ и меряем.
|
// Две фазы: сначала входим в проход, потом ВСТАЁМ и меряем.
|
||||||
//
|
//
|
||||||
// Мерить на ходу нельзя: пока игрок тянет курсор, тот всегда обгоняет
|
// Мерить на ходу нельзя: пока игрок тянет курсор, тот всегда обгоняет
|
||||||
@ -401,6 +405,22 @@ int RunAccept()
|
|||||||
watch.Observe(game, FIXED_DT);
|
watch.Observe(game, FIXED_DT);
|
||||||
corridorWatch.Observe(game, FIXED_DT);
|
corridorWatch.Observe(game, FIXED_DT);
|
||||||
|
|
||||||
|
// Горловина — это САМ ПРОЁМ: стена y=13..15, проём x=30..31.
|
||||||
|
// Пятеро в проём шириной два тайла лезть разом не должны, а пройти
|
||||||
|
// его обязаны все до одного.
|
||||||
|
{
|
||||||
|
int inThroat = 0;
|
||||||
|
for (int k = 0; k < NUM_AGENTS; ++k)
|
||||||
|
{
|
||||||
|
const Agent& a = game.squad.agents[k];
|
||||||
|
if (!a.alive) continue;
|
||||||
|
if (a.pos.y > 12.8f && a.pos.y < 15.2f &&
|
||||||
|
a.pos.x > 29.6f && a.pos.x < 32.4f) ++inThroat;
|
||||||
|
if (a.pos.y < 12.8f) crossed[k] = true; // севернее стены
|
||||||
|
}
|
||||||
|
maxInThroat = std::max(maxInThroat, inThroat);
|
||||||
|
}
|
||||||
|
|
||||||
// Кто сейчас в створе прохода (горловина + подходы).
|
// Кто сейчас в створе прохода (горловина + подходы).
|
||||||
int inside[NUM_AGENTS];
|
int inside[NUM_AGENTS];
|
||||||
int insideCount = 0;
|
int insideCount = 0;
|
||||||
@ -476,15 +496,35 @@ int RunAccept()
|
|||||||
// Плюс форма: отряд обязан вытянуться в колонну, а не идти шеренгой.
|
// Плюс форма: отряд обязан вытянуться в колонну, а не идти шеренгой.
|
||||||
// Пороги с запасом: строгое неравенство на маленьких числах — монетка,
|
// Пороги с запасом: строгое неравенство на маленьких числах — монетка,
|
||||||
// а не тест.
|
// а не тест.
|
||||||
const bool column = longAvg > lateralAvg * 1.5;
|
// Что здесь ПРАВИЛЬНО считать колонной.
|
||||||
|
//
|
||||||
|
// Первая мерка сравнивала разброс вдоль движения с разбросом поперёк — и
|
||||||
|
// была неверной насквозь. Отряд, у которого двое навсегда застряли по ту
|
||||||
|
// сторону стены, а трое стоят севернее, растянут вдоль на шесть тайлов и
|
||||||
|
// проверку ПРОХОДИЛ. Тот же отряд, когда отставшие научились обходить
|
||||||
|
// стену и собираться, стал компактным — и проверку ронял. Мерка
|
||||||
|
// награждала ровно ту поломку, ради которой писалась (docs/08-decisions.md).
|
||||||
|
//
|
||||||
|
// Честное «гуськом» — это про сам проём: одновременно в нём не больше
|
||||||
|
// двух (проём шириной два тайла), и прошли через него ВСЕ.
|
||||||
|
int crossedCount = 0, aliveCount = 0;
|
||||||
|
for (int k = 0; k < NUM_AGENTS; ++k)
|
||||||
|
{
|
||||||
|
if (!game.squad.agents[k].alive) continue;
|
||||||
|
++aliveCount;
|
||||||
|
if (crossed[k]) ++crossedCount;
|
||||||
|
}
|
||||||
|
const bool column = (maxInThroat <= 2) && (crossedCount == aliveCount);
|
||||||
const bool clean = (throughAllyShots == 0) && (game.metrics.friendlyHits == 0);
|
const bool clean = (throughAllyShots == 0) && (game.metrics.friendlyHits == 0);
|
||||||
const bool enough = (frontShotsInside + rearShotsInside) >= 4;
|
const bool enough = (frontShotsInside + rearShotsInside) >= 4;
|
||||||
const bool p6 = (samples > 0) && (maxInside >= 2) && column && clean && enough;
|
const bool p6 = (samples > 0) && (maxInside >= 2) && column && clean && enough;
|
||||||
allPass = allPass && p6;
|
allPass = allPass && p6;
|
||||||
PrintVerdict("11.6 в проходе колонна, огонь не идёт сквозь своих", p6,
|
PrintVerdict("11.6 в проходе колонна, огонь не идёт сквозь своих", p6,
|
||||||
Fmt("до %d агентов в проходе, поперёк %.2f / вдоль %.2f тайла, "
|
Fmt("прошли проход %d из %d, в горловине разом до %d, обходов %d, "
|
||||||
|
"в створе до %d, поперёк %.2f / вдоль %.2f тайла, "
|
||||||
"выстрелов вперёд передний/хвост %d/%d (назад %d), "
|
"выстрелов вперёд передний/хвост %d/%d (назад %d), "
|
||||||
"сквозь своего %d, FRIENDLY_HITS=%d",
|
"сквозь своего %d, FRIENDLY_HITS=%d",
|
||||||
|
crossedCount, aliveCount, maxInThroat, game.squad.navUses,
|
||||||
maxInside, lateralAvg, longAvg, frontShotsInside, rearShotsInside,
|
maxInside, lateralAvg, longAvg, frontShotsInside, rearShotsInside,
|
||||||
backwardShots, throughAllyShots, game.metrics.friendlyHits));
|
backwardShots, throughAllyShots, game.metrics.friendlyHits));
|
||||||
}
|
}
|
||||||
@ -2662,7 +2702,19 @@ int RunWall(const RoomLibrary& rooms, const EntityCatalog& catalog, uint32_t see
|
|||||||
// этажах твари разбросаны, и отряд иногда доживает полторы минуты просто
|
// этажах твари разбросаны, и отряд иногда доживает полторы минуты просто
|
||||||
// потому, что до него дошли не все. Последний выживший — шум, первая
|
// потому, что до него дошли не все. Последний выживший — шум, первая
|
||||||
// стена — сигнал.
|
// стена — сигнал.
|
||||||
std::printf("первая стена: одиночка D%d, трое D%d, пятеро D%d\n", wall[0], wall[1], wall[2]);
|
// Ноль в wall[] означает «стены не нашлось ВООБЩЕ»: отряд пережил все
|
||||||
|
// проверенные глубины. Читать этот ноль как «стена на нулевом этаже» —
|
||||||
|
// ошибка мерки, а не игры: пятёрка, не сломавшаяся нигде, объявлялась худшей
|
||||||
|
// из трёх и роняла проверку. Не нашлось — значит глубже последней.
|
||||||
|
const int noWall = maxDepth + 1;
|
||||||
|
for (int si = 0; si < 3; ++si)
|
||||||
|
if (wall[si] == 0) wall[si] = noWall;
|
||||||
|
|
||||||
|
std::printf("первая стена:");
|
||||||
|
const char* names[3] = {"одиночка", "трое", "пятеро"};
|
||||||
|
for (int si = 0; si < 3; ++si)
|
||||||
|
std::printf(" %s D%d%s%s", names[si], wall[si],
|
||||||
|
wall[si] == noWall ? " (не найдена)" : "", si < 2 ? "," : "\n");
|
||||||
std::printf("глубже всего кто-то дожил (шумно): D%d / D%d / D%d\n", lastAlive[0],
|
std::printf("глубже всего кто-то дожил (шумно): D%d / D%d / D%d\n", lastAlive[0],
|
||||||
lastAlive[1], lastAlive[2]);
|
lastAlive[1], lastAlive[2]);
|
||||||
|
|
||||||
@ -2799,11 +2851,19 @@ int RunPlay(const RoomLibrary& rooms, const EntityCatalog& catalog, uint32_t see
|
|||||||
// соседняя клетка. По соседней курсор дёргается между двумя
|
// соседняя клетка. По соседней курсор дёргается между двумя
|
||||||
// вариантами на каждой границе и стоит на месте; дальняя точка
|
// вариантами на каждой границе и стоит на месте; дальняя точка
|
||||||
// тянет ровно, как рука игрока.
|
// тянет ровно, как рука игрока.
|
||||||
|
// ...но НЕ ДАЛЬШЕ, ЧЕМ ВИДНО. Дальняя точка по полю может лежать за
|
||||||
|
// углом, и тогда прямая до неё идёт сквозь стену: курсор упирается
|
||||||
|
// в эту стену и стоит вплотную к спуску, до которого «две клетки по
|
||||||
|
// воздуху и шесть по полу». Берём самую дальнюю точку пути, которую
|
||||||
|
// видно, — ровно так же натягивает верёвку и сам отряд
|
||||||
|
// (Squad::NavWaypoint).
|
||||||
int bestDist = here;
|
int bestDist = here;
|
||||||
Vec2 goal = c;
|
Vec2 goal = c;
|
||||||
{
|
{
|
||||||
|
Vec2 hops[12];
|
||||||
|
int nHops = 0;
|
||||||
int wx = cx, wy = cy;
|
int wx = cx, wy = cy;
|
||||||
for (int hop = 0; hop < 6; ++hop)
|
for (int hop = 0; hop < 12; ++hop)
|
||||||
{
|
{
|
||||||
int nextX = wx, nextY = wy;
|
int nextX = wx, nextY = wy;
|
||||||
int nextD = flow[size_t(wy * MAP_W + wx)];
|
int nextD = flow[size_t(wy * MAP_W + wx)];
|
||||||
@ -2822,8 +2882,14 @@ int RunPlay(const RoomLibrary& rooms, const EntityCatalog& catalog, uint32_t see
|
|||||||
wx = nextX;
|
wx = nextX;
|
||||||
wy = nextY;
|
wy = nextY;
|
||||||
bestDist = nextD;
|
bestDist = nextD;
|
||||||
|
hops[nHops++] = Vec2{float(wx) + 0.5f, float(wy) + 0.5f};
|
||||||
|
}
|
||||||
|
if (nHops > 0)
|
||||||
|
{
|
||||||
|
goal = hops[0];
|
||||||
|
for (int h = nHops - 1; h >= 1; --h)
|
||||||
|
if (!game.map.RaycastBlocked(c, hops[h])) { goal = hops[h]; break; }
|
||||||
}
|
}
|
||||||
goal = Vec2{float(wx) + 0.5f, float(wy) + 0.5f};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Застряли — идём вторым по качеству соседом. Отряд упирается в
|
// Застряли — идём вторым по качеству соседом. Отряд упирается в
|
||||||
@ -2840,30 +2906,6 @@ int RunPlay(const RoomLibrary& rooms, const EntityCatalog& catalog, uint32_t see
|
|||||||
game.in.moveScreen = v2d::WorldDirToScreen(Normalized(toGoal));
|
game.in.moveScreen = v2d::WorldDirToScreen(Normalized(toGoal));
|
||||||
|
|
||||||
game.Step(FIXED_DT);
|
game.Step(FIXED_DT);
|
||||||
if (getenv("PLAYDBG") && (steps % 300) == 0)
|
|
||||||
{
|
|
||||||
std::printf(" DBG t=%5.1f anchor(%.2f,%.2f) here=%d goal(%.1f,%.1f) in(%.2f,%.2f)"
|
|
||||||
" state=%d\n",
|
|
||||||
double(steps) * double(FIXED_DT), double(c.x), double(c.y), here,
|
|
||||||
double(goal.x), double(goal.y), double(game.in.moveScreen.x),
|
|
||||||
double(game.in.moveScreen.y), int(game.squad.state));
|
|
||||||
for (int k = 0; k < NUM_AGENTS; ++k)
|
|
||||||
{
|
|
||||||
const Agent& a = game.squad.agents[k];
|
|
||||||
if (!a.present) continue;
|
|
||||||
std::printf(" #%d pos(%.2f,%.2f) post(%.2f,%.2f) lane=%d tgt=%d melee=%d\n",
|
|
||||||
k, double(a.pos.x), double(a.pos.y), double(a.postPos.x),
|
|
||||||
double(a.postPos.y), int(a.hasLane), a.currentTargetId,
|
|
||||||
int(a.IsMelee()));
|
|
||||||
}
|
|
||||||
for (int yy = int(c.y) - 2; yy <= int(c.y) + 2; ++yy)
|
|
||||||
{
|
|
||||||
std::printf(" MAP y=%2d ", yy);
|
|
||||||
for (int xx = int(c.x) - 6; xx <= int(c.x) + 6; ++xx)
|
|
||||||
std::printf("%c", game.map.IsWall(xx, yy) ? '#' : '.');
|
|
||||||
std::printf("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (game.atExit)
|
if (game.atExit)
|
||||||
{
|
{
|
||||||
@ -2888,6 +2930,32 @@ int RunPlay(const RoomLibrary& rooms, const EntityCatalog& catalog, uint32_t see
|
|||||||
}
|
}
|
||||||
|
|
||||||
const char* verdict = reached ? "дошёл" : (game.AliveAgents() > 0 ? "ЗАСТРЯЛ" : "ВАЙП");
|
const char* verdict = reached ? "дошёл" : (game.AliveAgents() > 0 ? "ЗАСТРЯЛ" : "ВАЙП");
|
||||||
|
|
||||||
|
// Затык — самая дорогая поломка в игре и самая трудная в чтении: снаружи
|
||||||
|
// видно только «отряд не дошёл». Поэтому зонд обязан сказать, КТО встал
|
||||||
|
// и где, иначе следующий раз опять начнётся с ручной диагностики.
|
||||||
|
if (!reached && game.AliveAgents() > 0)
|
||||||
|
{
|
||||||
|
const Vec2 c = game.squad.anchor;
|
||||||
|
const int cx = int(c.x), cy = int(c.y);
|
||||||
|
const int fld = (cx >= 0 && cy >= 0 && cx < MAP_W && cy < MAP_H)
|
||||||
|
? flow[size_t(cy * MAP_W + cx)] : -1;
|
||||||
|
std::printf(" затык: курсор %.1f,%.1f (до спуска по полу %d кл.), "
|
||||||
|
"спуск %.1f,%.1f, состояние %d\n",
|
||||||
|
double(c.x), double(c.y), fld,
|
||||||
|
double(game.level.exitPos.x), double(game.level.exitPos.y),
|
||||||
|
int(game.squad.state));
|
||||||
|
for (int i = 0; i < NUM_AGENTS; ++i)
|
||||||
|
{
|
||||||
|
const Agent& a = game.squad.agents[i];
|
||||||
|
if (!a.present || !a.alive) continue;
|
||||||
|
std::printf(" боец %d: %.1f,%.1f скорость %.2f отстал на %.1f%s\n", i,
|
||||||
|
double(a.pos.x), double(a.pos.y), double(Length(a.vel)),
|
||||||
|
double(Distance(a.pos, game.squad.SlotWorld(i))),
|
||||||
|
a.Grabbed() ? " (захвачен)" : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::printf("%-6d %7d %7d %8d %7.0f%% %9d %7.0f %s (%.0f с)\n", game.depth, startMonsters,
|
std::printf("%-6d %7d %7d %8d %7.0f%% %9d %7.0f %s (%.0f с)\n", game.depth, startMonsters,
|
||||||
startMonsters - aliveNow, game.AliveAgents(), double(100.0f * hp / std::max(1.0f, maxHp)),
|
startMonsters - aliveNow, game.AliveAgents(), double(100.0f * hp / std::max(1.0f, maxHp)),
|
||||||
ammo, double(game.squad.lightCharge), verdict, double(steps) * double(FIXED_DT));
|
ammo, double(game.squad.lightCharge), verdict, double(steps) * double(FIXED_DT));
|
||||||
|
|||||||
@ -42,6 +42,13 @@ struct Agent
|
|||||||
float commitTimer = 0.0f; // блокировка перерешивания
|
float commitTimer = 0.0f; // блокировка перерешивания
|
||||||
float solveTimer = 0.0f; // стаггер решателя
|
float solveTimer = 0.0f; // стаггер решателя
|
||||||
|
|
||||||
|
// Сколько боец УЖЕ УПИРАЕТСЯ: хочет идти, а не едет. По этому счётчику
|
||||||
|
// включается поле путей (Squad::NavWaypoint) — только для того, кто реально
|
||||||
|
// застрял, и ни для кого больше. Разбирать «видно цель или нет» тут нельзя:
|
||||||
|
// на подходе к проходу цель не видна вообще никому, и отряд входил бы в
|
||||||
|
// горловину шеренгой.
|
||||||
|
float stuckFor = 0.0f;
|
||||||
|
|
||||||
// Сущности ECS: сам боец и его оружие. Статы оружия копируются отсюда
|
// Сущности ECS: сам боец и его оружие. Статы оружия копируются отсюда
|
||||||
// в gun/melee каждый шаг (SyncLoadout), поэтому ai/ про ECS не знает вообще.
|
// в gun/melee каждый шаг (SyncLoadout), поэтому ai/ про ECS не знает вообще.
|
||||||
ecs::Entity self = ecs::NO_ENTITY;
|
ecs::Entity self = ecs::NO_ENTITY;
|
||||||
|
|||||||
@ -22,6 +22,10 @@ const float kLine[NUM_AGENTS][2] = {
|
|||||||
{0.0f, 2.0f},
|
{0.0f, 2.0f},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// «Недостижимо» в поле путей и потолок длины натягиваемой верёвки.
|
||||||
|
constexpr uint16_t NAV_FAR = 0xFFFF;
|
||||||
|
constexpr int NAV_PATH_MAX = 64;
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
const char* FormationName(Formation f)
|
const char* FormationName(Formation f)
|
||||||
@ -142,6 +146,22 @@ Vec2 Squad::SlotWorld(int i) const
|
|||||||
|
|
||||||
Vec2 Squad::GoalFor(int i) const
|
Vec2 Squad::GoalFor(int i) const
|
||||||
{
|
{
|
||||||
|
// ОТСТАВШИЙ ВОЗВРАЩАЕТСЯ В СТРОЙ, что бы ни говорил решатель.
|
||||||
|
//
|
||||||
|
// Без этого правила отряд намертво вставал, и вот почему. Якорь упирается в
|
||||||
|
// поводок до центра отряда -> отряд переходит в HOLDING -> отставшие в
|
||||||
|
// HOLDING держат ОГНЕВУЮ ПОЗИЦИЮ вместо возврата в слот -> центр не
|
||||||
|
// догоняет -> якорь не может идти дальше. Круг замкнулся, и снаружи это
|
||||||
|
// выглядело как «пятеро не проходят там, где одиночка проходит».
|
||||||
|
//
|
||||||
|
// Проверено отдельно: ширина коридоров тут ни при чём — отряд вставал и на
|
||||||
|
// проходах в три, четыре и пять клеток, и даже в пустых комнатах.
|
||||||
|
//
|
||||||
|
// Правило заодно честное по игре: отряд, растянувшийся на полэтажа, — это
|
||||||
|
// не строй, а цепочка одиночек, и собраться он обязан сам.
|
||||||
|
const float lag = Distance(agents[i].pos, SlotWorld(i));
|
||||||
|
if (lag > g_tune.straggleDist) return SlotWorld(i);
|
||||||
|
|
||||||
// Боец ближнего боя ведёт себя одинаково в обоих состояниях: пока цель
|
// Боец ближнего боя ведёт себя одинаково в обоих состояниях: пока цель
|
||||||
// внутри его круга, он идёт к ней, а не в слот. Иначе клинок оказывался бы
|
// внутри его круга, он идёт к ней, а не в слот. Иначе клинок оказывался бы
|
||||||
// полезен только на стоянке — а он нужен как раз в движении.
|
// полезен только на стоянке — а он нужен как раз в движении.
|
||||||
@ -153,6 +173,105 @@ Vec2 Squad::GoalFor(int i) const
|
|||||||
return (state == AgentState::ADVANCING) ? SlotWorld(i) : agents[i].postPos;
|
return (state == AgentState::ADVANCING) ? SlotWorld(i) : agents[i].postPos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Поле путей: BFS от клетки цели по проходимым клеткам, четыре соседа.
|
||||||
|
// Пересчёт только когда цель сменила клетку — это несколько тысяч клеток и
|
||||||
|
// стоит меньше одного кадра рендера.
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
void Squad::RebuildNav(NavField& field, const Tilemap& map, Vec2 goal) const
|
||||||
|
{
|
||||||
|
int gx = int(std::floor(goal.x));
|
||||||
|
int gy = int(std::floor(goal.y));
|
||||||
|
|
||||||
|
// Якорь игрок может завести в стену — поле строим от ближайшей свободной
|
||||||
|
// клетки, иначе отряд остался бы вообще без путей.
|
||||||
|
if (!Tilemap::InBounds(gx, gy) || map.IsWall(gx, gy))
|
||||||
|
{
|
||||||
|
bool found = false;
|
||||||
|
for (int r = 1; r <= 5 && !found; ++r)
|
||||||
|
for (int dy = -r; dy <= r && !found; ++dy)
|
||||||
|
for (int dx = -r; dx <= r && !found; ++dx)
|
||||||
|
{
|
||||||
|
const int x = gx + dx, y = gy + dy;
|
||||||
|
if (!Tilemap::InBounds(x, y) || map.IsWall(x, y)) continue;
|
||||||
|
gx = x; gy = y; found = true;
|
||||||
|
}
|
||||||
|
if (!found) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gx == field.goalX && gy == field.goalY && !field.cost.empty()) return;
|
||||||
|
field.goalX = gx;
|
||||||
|
field.goalY = gy;
|
||||||
|
|
||||||
|
field.cost.assign(size_t(MAP_W) * MAP_H, NAV_FAR);
|
||||||
|
std::vector<int> queue;
|
||||||
|
queue.reserve(size_t(MAP_W) * MAP_H);
|
||||||
|
|
||||||
|
field.cost[size_t(gy) * MAP_W + gx] = 0;
|
||||||
|
queue.push_back(gy * MAP_W + gx);
|
||||||
|
|
||||||
|
static const int DX[4] = {1, -1, 0, 0};
|
||||||
|
static const int DY[4] = {0, 0, 1, -1};
|
||||||
|
for (size_t head = 0; head < queue.size(); ++head)
|
||||||
|
{
|
||||||
|
const int c = queue[head];
|
||||||
|
const int cx = c % MAP_W;
|
||||||
|
const int cy = c / MAP_W;
|
||||||
|
const uint16_t nd = uint16_t(field.cost[size_t(c)] + 1);
|
||||||
|
for (int k = 0; k < 4; ++k)
|
||||||
|
{
|
||||||
|
const int x = cx + DX[k], y = cy + DY[k];
|
||||||
|
if (!Tilemap::InBounds(x, y) || map.IsWall(x, y)) continue;
|
||||||
|
const size_t n = size_t(y) * MAP_W + x;
|
||||||
|
if (field.cost[n] <= nd) continue;
|
||||||
|
field.cost[n] = nd;
|
||||||
|
queue.push_back(int(n));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec2 Squad::NavWaypoint(const NavField& field, const Tilemap& map, Vec2 from, Vec2 goal) const
|
||||||
|
{
|
||||||
|
// Видно цель — идём в цель. Поле нужно ровно там, где прямой не хватает.
|
||||||
|
if (field.cost.empty() || !map.RaycastBlocked(from, goal)) return goal;
|
||||||
|
|
||||||
|
int cx = int(std::floor(from.x));
|
||||||
|
int cy = int(std::floor(from.y));
|
||||||
|
if (!Tilemap::InBounds(cx, cy)) return goal;
|
||||||
|
|
||||||
|
uint16_t here = field.cost[size_t(cy) * MAP_W + cx];
|
||||||
|
if (here == NAV_FAR || here == 0) return goal; // вне поля или уже пришли
|
||||||
|
|
||||||
|
// Спуск по градиенту. Длина ограничена: дальше горизонта верёвка всё равно
|
||||||
|
// не натягивается, а лишние клетки — лишняя работа на каждого бойца.
|
||||||
|
Vec2 path[NAV_PATH_MAX];
|
||||||
|
int n = 0;
|
||||||
|
static const int DX[4] = {1, -1, 0, 0};
|
||||||
|
static const int DY[4] = {0, 0, 1, -1};
|
||||||
|
while (n < NAV_PATH_MAX && here > 0)
|
||||||
|
{
|
||||||
|
int bx = -1, by = -1;
|
||||||
|
uint16_t best = here;
|
||||||
|
for (int k = 0; k < 4; ++k)
|
||||||
|
{
|
||||||
|
const int x = cx + DX[k], y = cy + DY[k];
|
||||||
|
if (!Tilemap::InBounds(x, y)) continue;
|
||||||
|
const uint16_t d = field.cost[size_t(y) * MAP_W + x];
|
||||||
|
if (d >= best) continue;
|
||||||
|
best = d; bx = x; by = y;
|
||||||
|
}
|
||||||
|
if (bx < 0) break;
|
||||||
|
cx = bx; cy = by; here = best;
|
||||||
|
path[n++] = Vec2{float(cx) + 0.5f, float(cy) + 0.5f};
|
||||||
|
}
|
||||||
|
if (n == 0) return goal;
|
||||||
|
|
||||||
|
// Натягиваем верёвку: самая дальняя точка пути, которую боец ещё видит.
|
||||||
|
for (int i = n - 1; i >= 1; --i)
|
||||||
|
if (!map.RaycastBlocked(from, path[i])) return path[i];
|
||||||
|
return path[0];
|
||||||
|
}
|
||||||
|
|
||||||
Vec2 Squad::Centroid() const
|
Vec2 Squad::Centroid() const
|
||||||
{
|
{
|
||||||
// Центр масс считается по ЖИВЫМ: поводок якоря не должен тянуть отряд
|
// Центр масс считается по ЖИВЫМ: поводок якоря не должен тянуть отряд
|
||||||
@ -318,6 +437,7 @@ void Squad::UpdateMovement(const Tilemap& map, const LaneRegistry* lanes,
|
|||||||
// Скорость — не тумблер «шаг/рывок», а непрерывная величина от натяжения.
|
// Скорость — не тумблер «шаг/рывок», а непрерывная величина от натяжения.
|
||||||
const float baseSpeed = MoveSpeed();
|
const float baseSpeed = MoveSpeed();
|
||||||
|
|
||||||
|
|
||||||
for (int i = 0; i < NUM_AGENTS; ++i)
|
for (int i = 0; i < NUM_AGENTS; ++i)
|
||||||
{
|
{
|
||||||
Agent& a = agents[i];
|
Agent& a = agents[i];
|
||||||
@ -328,9 +448,47 @@ void Squad::UpdateMovement(const Tilemap& map, const LaneRegistry* lanes,
|
|||||||
// из чужой линии. В этом вся цена захвата — боец выпал из геометрии
|
// из чужой линии. В этом вся цена захвата — боец выпал из геометрии
|
||||||
// отряда и стоит там, где его взяли, пока тварь не отпустит или не умрёт.
|
// отряда и стоит там, где его взяли, пока тварь не отпустит или не умрёт.
|
||||||
// Оружие при этом работает: захват держит ноги, а не руки.
|
// Оружие при этом работает: захват держит ноги, а не руки.
|
||||||
if (a.Grabbed()) { a.vel = {}; continue; }
|
if (a.Grabbed()) { a.vel = {}; a.stuckFor = 0.0f; continue; }
|
||||||
|
|
||||||
|
const Vec2 rawGoal = GoalFor(i);
|
||||||
|
|
||||||
|
// Упёрся — веди по геометрии. Боец хочет идти (цель не под ногами), но
|
||||||
|
// скорость почти нулевая: значит прямая упирается в стену, и обход
|
||||||
|
// стены толкает обратно. Именно так отставший за углом замирал навсегда.
|
||||||
|
//
|
||||||
|
// Счётчик сбрасывается НЕ по факту «поехал», а по факту «цель видно».
|
||||||
|
// Разница принципиальная: сброс по скорости давал дребезг — обход
|
||||||
|
// трогал бойца с места, поле тут же выключалось, боец упирался в ту же
|
||||||
|
// стену, и так по кругу со средней скоростью в шаг за две секунды.
|
||||||
|
// Пока цель за стеной, режим обхода ДЕРЖИТСЯ, и боец доходит по дуге.
|
||||||
|
const bool wantsMove = DistanceSq(a.pos, rawGoal) > g_tune.navGoalReached *
|
||||||
|
g_tune.navGoalReached;
|
||||||
|
if (!wantsMove || !map.RaycastBlocked(a.pos, rawGoal))
|
||||||
|
a.stuckFor = 0.0f;
|
||||||
|
else if (Length(a.vel) < g_tune.navStuckSpeed * baseSpeed)
|
||||||
|
a.stuckFor += dt;
|
||||||
|
|
||||||
|
// ЧТО именно держит бойца — вот вся развилка. Стена держит навсегда, и
|
||||||
|
// из-под неё надо выводить по геометрии. Спина соседа держит на секунду,
|
||||||
|
// и обходить её нельзя: в проходе очередь стоит именно так, и отряд,
|
||||||
|
// отправленный в обход своей же колонны, входит в горловину шеренгой
|
||||||
|
// (критерий 11.6 ловит это сразу).
|
||||||
|
//
|
||||||
|
// Пробовал развилку по расстоянию до цели — это угадайка: у очереди в
|
||||||
|
// проходе и у отставшего одиночки дистанции пересекаются, и любой порог
|
||||||
|
// ломал либо строй, либо проходимость.
|
||||||
|
// Кому обход положен: тому, кто УПЁРСЯ и при этом ПОТЕРЯЛ СТРОЙ.
|
||||||
|
// Второе условие важно не меньше первого — боец, стоящий на своём месте,
|
||||||
|
// обходить ничего не должен: он не потерялся, его держит бой.
|
||||||
|
Vec2 goal = rawGoal;
|
||||||
|
if (a.stuckFor > g_tune.navStuckTime &&
|
||||||
|
Distance(a.pos, SlotWorld(i)) > g_tune.straggleDist)
|
||||||
|
{
|
||||||
|
++navUses;
|
||||||
|
RebuildNav(nav[i], map, rawGoal);
|
||||||
|
goal = NavWaypoint(nav[i], map, a.pos, rawGoal);
|
||||||
|
}
|
||||||
|
|
||||||
const Vec2 goal = GoalFor(i);
|
|
||||||
const bool lunging = a.IsMelee() && a.melee.state == MeleeState::STRIKE;
|
const bool lunging = a.IsMelee() && a.melee.state == MeleeState::STRIKE;
|
||||||
|
|
||||||
// --- рывок клинка --------------------------------------------------
|
// --- рывок клинка --------------------------------------------------
|
||||||
|
|||||||
@ -123,8 +123,45 @@ struct Squad
|
|||||||
// Сколько ПОСЛЕДНИХ слотов носят пистолет вместо карабина. Ставится ДО Init.
|
// Сколько ПОСЛЕДНИХ слотов носят пистолет вместо карабина. Ставится ДО Init.
|
||||||
int pistolCount = DEFAULT_PISTOL_COUNT;
|
int pistolCount = DEFAULT_PISTOL_COUNT;
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// ПОЛЕ ПУТЕЙ. Отряд ходит по геометрии уровня, а не по прямой линии.
|
||||||
|
//
|
||||||
|
// Без него отставший за углом не возвращался в строй НИКОГДА: seek тянул
|
||||||
|
// его в стену, обход стены толкал обратно, и боец замирал в углу навсегда.
|
||||||
|
// Снаружи это выглядело как «пятеро не проходят там, где одиночка проходит»
|
||||||
|
// — и никакая ширина коридоров этого не лечила, потому что дело было не в
|
||||||
|
// ширине.
|
||||||
|
//
|
||||||
|
// Устройство простое: BFS по проходимым клеткам от клетки цели. Боец,
|
||||||
|
// которому цель не видна, спускается по градиенту и берёт самую дальнюю
|
||||||
|
// точку пути, которую ещё видит, — это натянутая верёвка, и идёт он по ней
|
||||||
|
// как по прямой.
|
||||||
|
//
|
||||||
|
// Поле У КАЖДОГО СВОЁ, и это не расточительство. Одно общее поле до якоря
|
||||||
|
// сводило всех обходящих в одну точку: в горловине прохода отряд входил
|
||||||
|
// лужей вместо колонны, и критерий 11.6 падал. Боец идёт к СВОЕЙ цели —
|
||||||
|
// тогда строй сохраняется и в обходе.
|
||||||
|
//
|
||||||
|
// Платим за это только когда обход реально включён: поле пересчитывается
|
||||||
|
// на смену клетки цели и только у того, кто сейчас идёт в обход, а таких
|
||||||
|
// обычно ноль.
|
||||||
|
struct NavField
|
||||||
|
{
|
||||||
|
std::vector<uint16_t> cost;
|
||||||
|
int goalX = -1;
|
||||||
|
int goalY = -1;
|
||||||
|
};
|
||||||
|
NavField nav[NUM_AGENTS];
|
||||||
|
|
||||||
|
// Сколько раз за прогон включался обход. Чистая диагностика: по ней видно,
|
||||||
|
// работает поле путей или молчит, — без неё пришлось бы гадать.
|
||||||
|
int navUses = 0;
|
||||||
|
|
||||||
void Init(ecs::World& world, Vec2 at);
|
void Init(ecs::World& world, Vec2 at);
|
||||||
|
|
||||||
|
void RebuildNav(NavField& field, const Tilemap& map, Vec2 goal) const;
|
||||||
|
Vec2 NavWaypoint(const NavField& field, const Tilemap& map, Vec2 from, Vec2 goal) const;
|
||||||
|
|
||||||
Vec2 SlotLocal(int i) const; // локальные координаты слота (x = вперёд)
|
Vec2 SlotLocal(int i) const; // локальные координаты слота (x = вперёд)
|
||||||
Vec2 SlotWorld(int i) const; // слот в мире
|
Vec2 SlotWorld(int i) const; // слот в мире
|
||||||
Vec2 GoalFor(int i) const; // куда агент реально идёт
|
Vec2 GoalFor(int i) const; // куда агент реально идёт
|
||||||
|
|||||||
15
src/tuning.h
15
src/tuning.h
@ -84,6 +84,17 @@ struct Tuning
|
|||||||
// Как быстро строй доворачивается к направлению хода. Мгновенный доворот
|
// Как быстро строй доворачивается к направлению хода. Мгновенный доворот
|
||||||
// заставлял бойцов бегать вбок на каждом повороте (sim/squad.cpp).
|
// заставлял бойцов бегать вбок на каждом повороте (sim/squad.cpp).
|
||||||
float formTurnRate = 7.0f;
|
float formTurnRate = 7.0f;
|
||||||
|
// Насколько боец может отстать от своего слота, прежде чем бросит всё и
|
||||||
|
// вернётся в строй. Ниже 2 отряд перестаёт разбредаться по огневым
|
||||||
|
// позициям вообще, выше 4 — снова застревает цепочкой (sim/squad.cpp).
|
||||||
|
float straggleDist = 2.8f;
|
||||||
|
// Когда включается поле путей. Не по расстоянию до цели, а по факту упора:
|
||||||
|
// боец хочет идти и не едет. Расстоянием этот порог не задать — на подходе
|
||||||
|
// к проходу цель не видна никому, и по «не видно» все пятеро получили бы
|
||||||
|
// одну путевую точку и вошли бы в горловину шеренгой (критерий 11.6).
|
||||||
|
float navStuckTime = 0.35f; // секунд упора до обхода
|
||||||
|
float navStuckSpeed = 0.25f; // «не едет» — это доля от скорости хода
|
||||||
|
float navGoalReached = 0.35f; // ближе этого цель считается достигнутой
|
||||||
float wSeparation = 1.0f;
|
float wSeparation = 1.0f;
|
||||||
float wLaneAvoid = 2.4f; // строго выше wSeparation (спек 6.7)
|
float wLaneAvoid = 2.4f; // строго выше wSeparation (спек 6.7)
|
||||||
float wWallAvoid = 1.2f;
|
float wWallAvoid = 1.2f;
|
||||||
@ -455,6 +466,10 @@ inline const TunableParam TUNABLES[] = {
|
|||||||
{"wSeparation", &Tuning::wSeparation, 0.1f, 0.0f, 10.0f},
|
{"wSeparation", &Tuning::wSeparation, 0.1f, 0.0f, 10.0f},
|
||||||
{"steerAccel", &Tuning::steerAccel, 1.0f, 2.0f, 60.0f},
|
{"steerAccel", &Tuning::steerAccel, 1.0f, 2.0f, 60.0f},
|
||||||
{"formTurnRate", &Tuning::formTurnRate, 0.5f, 0.5f, 30.0f},
|
{"formTurnRate", &Tuning::formTurnRate, 0.5f, 0.5f, 30.0f},
|
||||||
|
{"straggleDist", &Tuning::straggleDist, 0.2f, 1.0f, 12.0f},
|
||||||
|
{"navStuckTime", &Tuning::navStuckTime, 0.05f, 0.05f, 3.0f},
|
||||||
|
{"navStuckSpeed", &Tuning::navStuckSpeed, 0.05f, 0.02f, 1.0f},
|
||||||
|
{"navGoalReached", &Tuning::navGoalReached, 0.05f, 0.05f, 2.0f},
|
||||||
{"moveSpeed", &Tuning::moveSpeed, 0.1f, 0.5f, 10.0f},
|
{"moveSpeed", &Tuning::moveSpeed, 0.1f, 0.5f, 10.0f},
|
||||||
{"moveSpeedFast", &Tuning::moveSpeedFast, 0.1f, 0.5f, 12.0f},
|
{"moveSpeedFast", &Tuning::moveSpeedFast, 0.1f, 0.5f, 12.0f},
|
||||||
{"weaponRange", &Tuning::weaponRange, 0.25f, 2.0f, 20.0f},
|
{"weaponRange", &Tuning::weaponRange, 0.25f, 2.0f, 20.0f},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user