Compare commits
3 Commits
9d715a9c3a
...
c1f79a1fb8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1f79a1fb8 | ||
|
|
aa9a175751 | ||
|
|
da670af6dc |
@ -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), «поле путей»). Боец в строю по-прежнему
|
||||||
|
ходит локальными слоями, и это осознанно: строй должен вести себя как строй,
|
||||||
|
а не как пять независимых искателей пути.
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
# Открытая арена с четырьмя опорами. Здесь работает решатель огневых позиций:
|
# Открытая арена с четырьмя опорами. Здесь работает решатель огневых позиций:
|
||||||
# места хватает, и отряд обязан разойтись по линиям, а не сбиться в кучу.
|
# места хватает, и отряд обязан разойтись по линиям, а не сбиться в кучу.
|
||||||
#
|
#
|
||||||
# Опоры стали ОДНОРЯДНЫМИ вместо блоков 2x2, а сама арена сузилась до 12: блок
|
# Опоры отодвинуты В УГЛЫ и стали однорядными. Причина не в красоте: строй из
|
||||||
# отнимал у центра столько пола, что пятеро вставали в него впритык и переставали
|
# пяти занимает поперёк почти четыре тайла, и опора, стоявшая у самого прохода,
|
||||||
# расходиться. Укрытие нужно, чтобы за него зайти, а не чтобы об него спотыкаться.
|
# ловила крайнего бойца намертво — упереться в неё он мог, а обойти нет.
|
||||||
|
# Полоса пола между дверями противоположных стен обязана быть шире строя.
|
||||||
name arena
|
name arena
|
||||||
size 12 12
|
size 12 12
|
||||||
weight 3
|
weight 3
|
||||||
@ -12,20 +13,20 @@ start 0
|
|||||||
depth 1 0
|
depth 1 0
|
||||||
tiles
|
tiles
|
||||||
####+++#####
|
####+++#####
|
||||||
#..........#
|
#.#......#.#
|
||||||
#..........#
|
#.#......#.#
|
||||||
#..##..##..#
|
|
||||||
#..........#
|
#..........#
|
||||||
+..........+
|
+..........+
|
||||||
+..........+
|
+..........+
|
||||||
+..........+
|
+..........+
|
||||||
#..........#
|
#..........#
|
||||||
#..##..##..#
|
|
||||||
#..........#
|
#..........#
|
||||||
|
#.#......#.#
|
||||||
|
#.#......#.#
|
||||||
####+++#####
|
####+++#####
|
||||||
spawn 4 4 0.90 shambler:4 rusher:3 spitter:2
|
spawn 4 5 0.90 shambler:4 rusher:3 spitter:2
|
||||||
spawn 9 4 0.85 shambler:4 rusher:3
|
spawn 8 5 0.85 shambler:4 rusher:3
|
||||||
spawn 4 7 0.85 shambler:3 rusher:2 brute:1
|
spawn 4 7 0.85 shambler:3 rusher:2 brute:1
|
||||||
spawn 9 7 0.90 shambler:4 spitter:2
|
spawn 8 7 0.90 shambler:4 spitter:2
|
||||||
spawn 6 1 0.40 crate:2 barrel:1
|
spawn 6 1 0.40 crate:2 barrel:1
|
||||||
spawn 7 10 0.40 crate:1 locker:1
|
spawn 6 10 0.40 crate:1 locker:1
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
# Казарма: ряды перегородок и сквозной проход посередине. Твари выходят
|
# Казарма: койки по нишам вдоль стен, середина сквозная. Твари выходят
|
||||||
# из-за переборок сбоку, а не в лоб, — строй приходится разворачивать на ходу.
|
# из-за переборок сбоку, а не в лоб, — строй приходится разворачивать на ходу.
|
||||||
#
|
#
|
||||||
# Перегородок стало вдвое меньше, а проход посередине — в три ряда. Прежние пять
|
# Прежние пять рядов переборок резали казарму на щели шириной в тело: сбоку
|
||||||
# рядов по два тайла резали комнату на щели шириной в тело: сбоку выходить было
|
# выходить было откуда, а разворачиваться в ответ уже негде. Ниши остались
|
||||||
# откуда, а разворачиваться в ответ уже негде.
|
# ровно там, где не мешают проходу от двери до двери.
|
||||||
name barracks
|
name barracks
|
||||||
size 12 9
|
size 12 9
|
||||||
weight 2
|
weight 2
|
||||||
@ -12,13 +12,13 @@ start 0
|
|||||||
depth 2 0
|
depth 2 0
|
||||||
tiles
|
tiles
|
||||||
####+++#####
|
####+++#####
|
||||||
|
#.#......#.#
|
||||||
#..........#
|
#..........#
|
||||||
#..##..##..#
|
|
||||||
+..........+
|
+..........+
|
||||||
+..........+
|
+..........+
|
||||||
+..........+
|
+..........+
|
||||||
#..##..##..#
|
|
||||||
#..........#
|
#..........#
|
||||||
|
#.#......#.#
|
||||||
####+++#####
|
####+++#####
|
||||||
spawn 4 1 0.75 shambler:3 rusher:2
|
spawn 4 1 0.75 shambler:3 rusher:2
|
||||||
spawn 8 1 0.60 spitter:2 shambler:1
|
spawn 8 1 0.60 spitter:2 shambler:1
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
# Перекрёсток. Рукава шириной три тайла — ровно как коридоры между комнатами:
|
# Перекрёсток. Рукава шириной пять тайлов: правило «не стрелять сквозь своих»
|
||||||
# правило «не стрелять сквозь своих» упирается в геометрию, и отряд вытягивается
|
# упирается в геометрию, и отряд вытягивается в колонну.
|
||||||
# в колонну, но не встаёт в ней намертво.
|
|
||||||
#
|
#
|
||||||
# Середина расширена до четырёх рядов: перекрёсток — единственное место, где
|
# Пять, а не три: строй из пяти занимает поперёк почти четыре тайла, и в рукаве
|
||||||
# колонна имеет право снова стать строем, и на трёх рядах она этого не успевала.
|
# шириной три крайние бойцы упирались в его же стены на входе. Комната обязана
|
||||||
|
# СЖИМАТЬ строй, а не запирать его.
|
||||||
name corridor_x
|
name corridor_x
|
||||||
size 12 12
|
size 12 12
|
||||||
weight 3
|
weight 3
|
||||||
@ -12,18 +12,18 @@ start 0
|
|||||||
depth 1 0
|
depth 1 0
|
||||||
tiles
|
tiles
|
||||||
####+++#####
|
####+++#####
|
||||||
####...#####
|
###.....####
|
||||||
####...#####
|
###.....####
|
||||||
####...#####
|
#..........#
|
||||||
+..........+
|
+..........+
|
||||||
+..........+
|
+..........+
|
||||||
+..........+
|
+..........+
|
||||||
#..........#
|
#..........#
|
||||||
####...#####
|
#..........#
|
||||||
####...#####
|
###.....####
|
||||||
####...#####
|
###.....####
|
||||||
####+++#####
|
####+++#####
|
||||||
spawn 6 2 0.60 rusher:3 lurker:1
|
spawn 5 1 0.60 rusher:3 lurker:1
|
||||||
spawn 6 10 0.60 rusher:3 lurker:1
|
spawn 5 10 0.60 rusher:3 lurker:1
|
||||||
spawn 2 6 0.50 shambler:2 spitter:1
|
spawn 2 5 0.50 shambler:2 spitter:1
|
||||||
spawn 10 6 0.50 shambler:2 spitter:1
|
spawn 9 5 0.50 shambler:2 spitter:1
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
# Стартовая: просторно, светло по планировке и почти пусто.
|
# Стартовая: просторно, светло по планировке и почти пусто.
|
||||||
# Отряд обязан успеть собраться в строй до первой твари.
|
# Отряд обязан успеть собраться в строй до первой твари.
|
||||||
#
|
#
|
||||||
# Поперёк комнаты идёт сплошная полоса пола в три ряда, и опор осталось по одной
|
# Пусто здесь не от лени. Отряд высаживается в центре и первым делом
|
||||||
# на угол. Первое, что отряд делает после высадки, — разворачивается; делать это
|
# разворачивается; разворот из пяти тел не помещается между двумя тумбами,
|
||||||
# должно быть где, иначе первые секунды каждого этажа уходят на распутывание строя.
|
# и первые секунды каждого этажа уходили на распутывание строя.
|
||||||
name hall_start
|
name hall_start
|
||||||
size 12 10
|
size 12 10
|
||||||
weight 2
|
weight 2
|
||||||
@ -12,14 +12,14 @@ start 1
|
|||||||
depth 1 0
|
depth 1 0
|
||||||
tiles
|
tiles
|
||||||
####+++#####
|
####+++#####
|
||||||
|
#.#......#.#
|
||||||
#..........#
|
#..........#
|
||||||
#..........#
|
#..........#
|
||||||
#..#....#..#
|
|
||||||
+..........+
|
+..........+
|
||||||
+..........+
|
+..........+
|
||||||
+..........+
|
+..........+
|
||||||
#..#....#..#
|
|
||||||
#..........#
|
#..........#
|
||||||
|
#.#......#.#
|
||||||
####+++#####
|
####+++#####
|
||||||
spawn 2 2 0.50 crate:2 barrel:1
|
spawn 4 1 0.50 crate:2 barrel:1
|
||||||
spawn 9 7 0.40 barrel:1 locker:1
|
spawn 7 8 0.40 barrel:1 locker:1
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
# Гнездо: глухое ядро посередине и кольцо вокруг него. Линий огня почти нет,
|
# Гнездо: выводковые камеры по углам, середина открыта. Линий огня почти нет —
|
||||||
# зато клинку есть где работать — сектор упирается в стену, а не в союзника.
|
# они упираются в углы, — зато клинку есть где работать.
|
||||||
#
|
#
|
||||||
# Кольцо теперь в три тайла со всех сторон, а перегородок внутри него нет вовсе.
|
# Кольцевые перегородки посередине пришлось убрать: между ними оставались
|
||||||
# Прежние два вложенных кольца давали проходы шириной в одну клетку: отряд не
|
# проходы в одну клетку, и отряд не обходил гнездо, а вставал в нём. Тесноту
|
||||||
# обходил ядро, он в нём застревал, и «нет линий огня» превращалось в «нет игры».
|
# теперь делает не ширина прохода, а то, что тварей выпускают со всех четырёх
|
||||||
|
# сторон сразу.
|
||||||
name nest
|
name nest
|
||||||
size 12 12
|
size 12 12
|
||||||
weight 2
|
weight 2
|
||||||
@ -12,18 +13,18 @@ start 0
|
|||||||
depth 4 0
|
depth 4 0
|
||||||
tiles
|
tiles
|
||||||
####+++#####
|
####+++#####
|
||||||
|
##.......###
|
||||||
|
##.......###
|
||||||
#..........#
|
#..........#
|
||||||
#..........#
|
+..........+
|
||||||
#..........#
|
+..........+
|
||||||
#...####...#
|
|
||||||
+...####...+
|
|
||||||
+...####...+
|
|
||||||
+..........+
|
+..........+
|
||||||
#..........#
|
#..........#
|
||||||
#..........#
|
#..........#
|
||||||
#..........#
|
###.......##
|
||||||
|
###.......##
|
||||||
####+++#####
|
####+++#####
|
||||||
spawn 5 1 0.85 shambler:3 brute:1
|
spawn 5 1 0.85 shambler:3 brute:1
|
||||||
spawn 2 5 0.80 lurker:2 rusher:2
|
spawn 3 5 0.80 lurker:2 rusher:2
|
||||||
spawn 9 5 0.80 lurker:2 rusher:2
|
spawn 8 5 0.80 lurker:2 rusher:2
|
||||||
spawn 6 10 0.85 shambler:3 spitter:2
|
spawn 6 10 0.85 shambler:3 spitter:2
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
# Яма: колодец посередине с единственным входом снизу. Громилу в нём
|
# Яма: колодец в дальнем углу, а не посередине. Громилу в нём приходится
|
||||||
# приходится разбирать вдвоём, а круг его удара достаёт обоих.
|
# разбирать вдвоём, а круг его удара достаёт обоих.
|
||||||
#
|
#
|
||||||
# Колодец сузился до двух тайлов, зато обход вокруг него стал трёхрядным. Смысл
|
# Посередине колодец стоять не может: он перегораживал бы проход от двери к
|
||||||
# комнаты в том, что В КОЛОДЦЕ тесно, а не в том, что тесно везде: раньше отряд
|
# двери, и строй упирался бы в его стенку всей шириной. В углу он делает ровно
|
||||||
# вставал ещё на подходе и до ямы просто не доходил.
|
# то же самое — заставляет лезть в тесноту, — но лезть туда решает игрок.
|
||||||
name pit
|
name pit
|
||||||
size 12 12
|
size 12 12
|
||||||
weight 2
|
weight 2
|
||||||
@ -12,19 +12,19 @@ start 0
|
|||||||
depth 6 0
|
depth 6 0
|
||||||
tiles
|
tiles
|
||||||
####+++#####
|
####+++#####
|
||||||
|
#.#......#.#
|
||||||
|
#.#......#.#
|
||||||
|
#..........#
|
||||||
|
+..........+
|
||||||
|
+..........+
|
||||||
|
+..........+
|
||||||
#..........#
|
#..........#
|
||||||
#..........#
|
#..........#
|
||||||
#..........#
|
#.#.....#..#
|
||||||
#...####...#
|
#.#.....#..#
|
||||||
+...#..#...+
|
|
||||||
+...#..#...+
|
|
||||||
+...#..#...+
|
|
||||||
#..........#
|
|
||||||
#..........#
|
|
||||||
#..........#
|
|
||||||
####+++#####
|
####+++#####
|
||||||
spawn 5 6 0.95 brute:3 shambler:2
|
spawn 9 10 0.95 brute:3 shambler:2
|
||||||
spawn 2 4 0.70 spitter:2 rusher:2
|
spawn 2 5 0.70 spitter:2 rusher:2
|
||||||
spawn 9 4 0.70 spitter:2 rusher:2
|
spawn 9 5 0.70 spitter:2 rusher:2
|
||||||
spawn 6 10 0.60 shambler:3 lurker:1
|
spawn 6 8 0.60 shambler:3 lurker:1
|
||||||
spawn 2 9 0.40 barrel:2 crate:1
|
spawn 5 1 0.40 barrel:2 crate:1
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
# Склад: обстановки много, тварей мало. Место, где игрок узнаёт, что хлам
|
# Склад: обстановки много, тварей мало. Место, где игрок узнаёт, что хлам
|
||||||
# ломается, — и тратит на это магазин, которого потом не хватит.
|
# ломается, — и тратит на это магазин, которого потом не хватит.
|
||||||
#
|
#
|
||||||
# Стеллаж стал одним глухим блоком посередине, а обход вокруг него — трёхрядным.
|
# Стеллажи стоят вдоль стен, а не двойной рамкой посередине: рамка оставляла по
|
||||||
# Прежняя двойная рамка оставляла по краям щели в один тайл: отряд входил на
|
# краям щели в один тайл, и отряд входил на склад, а выходил из него гуськом.
|
||||||
# склад и не мог из него выйти иначе как гуськом.
|
|
||||||
name storage
|
name storage
|
||||||
size 12 10
|
size 12 10
|
||||||
weight 2
|
weight 2
|
||||||
@ -12,16 +11,16 @@ start 0
|
|||||||
depth 1 0
|
depth 1 0
|
||||||
tiles
|
tiles
|
||||||
####+++#####
|
####+++#####
|
||||||
|
#.#......#.#
|
||||||
|
#.#......#.#
|
||||||
#..........#
|
#..........#
|
||||||
#..........#
|
+..........+
|
||||||
#...####...#
|
+..........+
|
||||||
+...####...+
|
|
||||||
+...####...+
|
|
||||||
+..........+
|
+..........+
|
||||||
#..........#
|
#..........#
|
||||||
#..........#
|
#.#......#.#
|
||||||
####+++#####
|
####+++#####
|
||||||
spawn 2 4 0.90 crate:4 barrel:3 locker:2
|
spawn 4 1 0.90 crate:4 barrel:3 locker:2
|
||||||
spawn 9 4 0.85 crate:3 barrel:2
|
spawn 8 1 0.85 crate:3 barrel:2
|
||||||
spawn 5 7 0.70 barrel:2 locker:2
|
spawn 4 8 0.70 barrel:2 locker:2
|
||||||
spawn 1 1 0.45 shambler:2 lurker:1
|
spawn 1 4 0.45 shambler:2 lurker:1
|
||||||
|
|||||||
@ -1,27 +1,27 @@
|
|||||||
# Схрон: тесная комната с глухим сейфом посередине. Хват здесь опаснее всего —
|
# Схрон: самая маленькая комната набора. Хват здесь опаснее всего — вырванного
|
||||||
# вырванного из строя бойца в такой тесноте нечем прикрыть.
|
# из строя бойца в такой тесноте нечем прикрыть.
|
||||||
#
|
#
|
||||||
# Тесно тут за счёт РАЗМЕРА комнаты, а не за счёт щелей: камера в камере давала
|
# Тесно тут за счёт РАЗМЕРА, а не за счёт щелей: камера в камере давала обходы
|
||||||
# обходы шириной в одну клетку, и отряд не столько рисковал, сколько стоял.
|
# шириной в одну клетку, и отряд в схроне не рисковал, а стоял.
|
||||||
# Обход теперь трёхрядный, а схрон всё равно самая маленькая комната набора.
|
|
||||||
name vault
|
name vault
|
||||||
size 10 10
|
size 11 11
|
||||||
weight 1
|
weight 1
|
||||||
theme 3
|
theme 3
|
||||||
start 0
|
start 0
|
||||||
depth 8 0
|
depth 8 0
|
||||||
tiles
|
tiles
|
||||||
####+++###
|
####+++####
|
||||||
#........#
|
#.#.....#.#
|
||||||
#........#
|
#.#.....#.#
|
||||||
#........#
|
#.........#
|
||||||
+...##...+
|
+.........+
|
||||||
+...##...+
|
+.........+
|
||||||
+...##...+
|
+.........+
|
||||||
#........#
|
#.........#
|
||||||
#........#
|
#.#.....#.#
|
||||||
####+++###
|
#.#.....#.#
|
||||||
spawn 7 5 0.90 lurker:3 brute:1
|
####+++####
|
||||||
spawn 1 1 0.55 rusher:2 shambler:1
|
spawn 5 5 0.90 lurker:3 brute:1
|
||||||
spawn 8 8 0.55 spitter:2
|
spawn 4 1 0.55 rusher:2 shambler:1
|
||||||
spawn 4 8 0.40 locker:2 crate:1
|
spawn 6 9 0.55 spitter:2
|
||||||
|
spawn 4 9 0.40 locker:2 crate:1
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
# Ряд камер. Ниши по бокам: в них удобно стоять и невозможно отступать.
|
# Ряд камер. Ниши по бокам: в них удобно стоять и невозможно отступать.
|
||||||
#
|
#
|
||||||
# Ниш стало меньше, а проход между рядами — трёхрядным: ниша должна быть местом,
|
# Ниш стало вдвое меньше, и стоят они у самых стен. Прежний частокол оставлял
|
||||||
# КУДА зашли, а не единственным местом, где помещается тело.
|
# между камерами проходы в одну клетку: ниша должна быть местом, КУДА зашли,
|
||||||
|
# а не единственным местом, где помещается тело.
|
||||||
name cells
|
name cells
|
||||||
size 12 9
|
size 12 9
|
||||||
weight 3
|
weight 3
|
||||||
@ -9,14 +10,14 @@ theme 0
|
|||||||
depth 1 0
|
depth 1 0
|
||||||
tiles
|
tiles
|
||||||
####+++#####
|
####+++#####
|
||||||
|
#.#......#.#
|
||||||
#..........#
|
#..........#
|
||||||
#.#.#..#.#.#
|
|
||||||
+..........+
|
+..........+
|
||||||
+..........+
|
+..........+
|
||||||
+..........+
|
+..........+
|
||||||
#.#.#..#.#.#
|
|
||||||
#..........#
|
#..........#
|
||||||
|
#.#......#.#
|
||||||
####+++#####
|
####+++#####
|
||||||
spawn 2 1 0.65 rusher:3 shambler:2
|
spawn 4 1 0.65 rusher:3 shambler:2
|
||||||
spawn 10 7 0.65 rusher:3 shambler:2
|
spawn 8 7 0.65 rusher:3 shambler:2
|
||||||
spawn 6 4 0.40 bandage:2 knife:1 rifle:1
|
spawn 6 4 0.40 bandage:2 knife:1 rifle:1
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
# Зев. Открытая арена с ядром посередине: единственное место в наборе, где
|
# Зев. Открытая арена: единственное место в наборе, где отряд может встать
|
||||||
# отряд может встать кругом.
|
# кругом, — а встать кругом впятером нельзя нигде, где проход уже четырёх тайлов.
|
||||||
#
|
#
|
||||||
# У ядра ДВА прохода, север и юг. Глухая коробка посередине выглядит на сетке
|
# Ядро посередине пришлось убрать не ради простора, а ради проходимости: оно
|
||||||
# так же, но точка спавна внутри неё недостижима: тварь не дойдёт до отряда и
|
# стояло ровно на линии между дверями и ловило строй всей шириной. Урок, ради
|
||||||
# не будет убита, и этаж тихо станет легче. Ровно это и ловит --check.
|
# которого его когда-то рисовали, остался в силе: точка спавна в глухой коробке
|
||||||
#
|
# недостижима, тварь до отряда не дойдёт, и этаж тихо станет легче. Ровно это
|
||||||
# Кольцо вокруг ядра — три тайла со всех сторон: встать кругом впятером нельзя
|
# и ловит --check.
|
||||||
# в обходе шириной в два тела, там можно только выстроиться в очередь.
|
|
||||||
name maw
|
name maw
|
||||||
size 12 11
|
size 12 11
|
||||||
weight 2
|
weight 2
|
||||||
@ -14,17 +13,17 @@ theme 2
|
|||||||
depth 3 0
|
depth 3 0
|
||||||
tiles
|
tiles
|
||||||
####+++#####
|
####+++#####
|
||||||
|
#.#......#.#
|
||||||
|
#.#......#.#
|
||||||
#..........#
|
#..........#
|
||||||
|
+..........+
|
||||||
|
+..........+
|
||||||
|
+..........+
|
||||||
#..........#
|
#..........#
|
||||||
#..........#
|
#.#......#.#
|
||||||
#...#..#...#
|
#.#......#.#
|
||||||
+...#..#...+
|
|
||||||
+...#..#...+
|
|
||||||
+...#..#...+
|
|
||||||
#...#..#...#
|
|
||||||
#..........#
|
|
||||||
####+++#####
|
####+++#####
|
||||||
spawn 6 5 0.85 brute:3 lurker:2
|
spawn 6 5 0.85 brute:3 lurker:2
|
||||||
spawn 1 1 0.60 spitter:2 rusher:2
|
spawn 4 1 0.60 spitter:2 rusher:2
|
||||||
spawn 10 9 0.60 spitter:2 rusher:2
|
spawn 8 9 0.60 spitter:2 rusher:2
|
||||||
spawn 10 1 0.45 shotgun:1 plate:1 cleaver:1
|
spawn 8 1 0.45 shotgun:1 plate:1 cleaver:1
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
# Стартовая HOLLOW. Тесная и короткая: вид сверху показывает всю комнату
|
# Стартовая HOLLOW. Тесная и короткая: вид сверху показывает всю комнату
|
||||||
# сразу, и просторный зал первого этажа не оставил бы ничего на потом.
|
# сразу, и просторный зал первого этажа не оставил бы ничего на потом.
|
||||||
#
|
#
|
||||||
# Тесная — по размеру, а не по проходам. От четырёх опор осталась одна: отряд
|
# Тесная — по РАЗМЕРУ, а не по проходам. Четыре тумбы посередине стояли ровно
|
||||||
# высаживается сюда всем составом и первым делом разворачивается, а разворот
|
# там, где отряд разворачивается после высадки, и разворот в них упирался.
|
||||||
# между четырьмя тумбами упирался в них, а не в стены.
|
|
||||||
name mouth
|
name mouth
|
||||||
size 11 9
|
size 11 9
|
||||||
weight 2
|
weight 2
|
||||||
@ -12,13 +11,13 @@ start 1
|
|||||||
depth 1 0
|
depth 1 0
|
||||||
tiles
|
tiles
|
||||||
####+++####
|
####+++####
|
||||||
#.........#
|
#.#.....#.#
|
||||||
#.........#
|
#.........#
|
||||||
+.........+
|
+.........+
|
||||||
+....#....+
|
+.........+
|
||||||
+.........+
|
+.........+
|
||||||
#.........#
|
#.........#
|
||||||
#.........#
|
#.#.....#.#
|
||||||
####+++####
|
####+++####
|
||||||
spawn 2 1 0.45 barrel:1
|
spawn 4 1 0.45 barrel:1
|
||||||
spawn 8 7 0.45 barrel:1
|
spawn 6 7 0.45 barrel:1
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
# Колодец. Узкий и длинный: строй здесь не развернуть, и это единственное,
|
# Колодец. Узкий и длинный: строй здесь не развернуть, и это единственное,
|
||||||
# что комната делает.
|
# что комната делает.
|
||||||
#
|
#
|
||||||
# «Не развернуть» — это пять тайлов в ширину против трёх рядов строя, а не щель
|
# «Не развернуть» — это пять тайлов в ширину против четырёх, которые занимает
|
||||||
# в одну клетку. Перемычки теперь поджимают колодец с БОКОВ до трёх тайлов, а не
|
# строй, а не щель в одну клетку. Перемычки поперёк колодца убраны: они не
|
||||||
# перекрывают его посередине: отряд проходит с трудом, но проходит.
|
# сжимали отряд, они его останавливали.
|
||||||
name shaft
|
name shaft
|
||||||
size 7 12
|
size 7 12
|
||||||
weight 2
|
weight 2
|
||||||
@ -13,11 +13,11 @@ tiles
|
|||||||
##+++##
|
##+++##
|
||||||
#.....#
|
#.....#
|
||||||
#.....#
|
#.....#
|
||||||
##...##
|
#.....#
|
||||||
+.....+
|
+.....+
|
||||||
+.....+
|
+.....+
|
||||||
+.....+
|
+.....+
|
||||||
##...##
|
#.....#
|
||||||
#.....#
|
#.....#
|
||||||
#.....#
|
#.....#
|
||||||
#.....#
|
#.....#
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
@ -2036,7 +2076,24 @@ int RunDescend(const RoomLibrary& rooms, const EntityCatalog& catalog, uint32_t
|
|||||||
return n;
|
return n;
|
||||||
}();
|
}();
|
||||||
|
|
||||||
game.PlaceSquad(game.targets[size_t(victim)].pos + Vec2{3.0f, 0.0f}, true);
|
// Ставим отряд в СВОБОДНУЮ клетку рядом с тварью и с прямой
|
||||||
|
// видимостью. Слепое «+3 по X» попадало в стену, стоило
|
||||||
|
// комнатам измениться: боец не видел цель, клинок её не брал, и
|
||||||
|
// проверка объявляла непроходимым первый этаж — хотя мерила
|
||||||
|
// собственную раскладку.
|
||||||
|
const Vec2 foePos = game.targets[size_t(victim)].pos;
|
||||||
|
Vec2 spot = foePos + Vec2{3.0f, 0.0f};
|
||||||
|
const Vec2 around[8] = {{3, 0}, {-3, 0}, {0, 3}, {0, -3},
|
||||||
|
{2, 2}, {-2, 2}, {2, -2}, {-2, -2}};
|
||||||
|
for (const Vec2& d : around)
|
||||||
|
{
|
||||||
|
const Vec2 test = foePos + d;
|
||||||
|
if (game.map.IsWallAt(test)) continue;
|
||||||
|
if (game.map.RaycastBlocked(test, foePos)) continue;
|
||||||
|
spot = test;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
game.PlaceSquad(spot, true);
|
||||||
game.Step(FIXED_DT); // SyncLoadout: здоровье по надетому
|
game.Step(FIXED_DT); // SyncLoadout: здоровье по надетому
|
||||||
game.squad.agents[0].hp = game.squad.agents[0].maxHp;
|
game.squad.agents[0].hp = game.squad.agents[0].maxHp;
|
||||||
hpStart = game.squad.agents[0].hp;
|
hpStart = game.squad.agents[0].hp;
|
||||||
@ -2645,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]);
|
||||||
|
|
||||||
@ -2782,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)];
|
||||||
@ -2805,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};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Застряли — идём вторым по качеству соседом. Отряд упирается в
|
// Застряли — идём вторым по качеству соседом. Отряд упирается в
|
||||||
@ -2823,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)
|
||||||
{
|
{
|
||||||
@ -2871,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},
|
||||||
|
|||||||
@ -151,8 +151,11 @@ constexpr int SQ_HINT_Y = 250, SQ_HINT_STEP = 10;
|
|||||||
// Схрон: то, что лежит на базе между вылазками. Двумя рядами во всю ширину
|
// Схрон: то, что лежит на базе между вылазками. Двумя рядами во всю ширину
|
||||||
// внизу, а не колонкой сбоку: в схроне десятки предметов, и подпись каждого
|
// внизу, а не колонкой сбоку: в схроне десятки предметов, и подпись каждого
|
||||||
// должна читаться — иначе выбор снаряжения превращается в перебор иконок.
|
// должна читаться — иначе выбор снаряжения превращается в перебор иконок.
|
||||||
constexpr int SQ_STASH_X = 22, SQ_STASH_Y = 208, SQ_STASH_W = 52, SQ_STASH_H = 18;
|
// Ячейка схрона шире, а колонок меньше: в 52 пикселя не влезало даже слово
|
||||||
constexpr int SQ_STASH_GX = 4, SQ_STASH_GY = 3, SQ_STASH_COLS = 8, SQ_STASH_ROWS = 2;
|
// BATTERY, и половина имён обрезалась на середине. Читаемость важнее, чем ещё
|
||||||
|
// две ячейки в ряду: прокрутка есть, а угадывать вещь по обрубку — нет.
|
||||||
|
constexpr int SQ_STASH_X = 16, SQ_STASH_Y = 206, SQ_STASH_W = 74, SQ_STASH_H = 18;
|
||||||
|
constexpr int SQ_STASH_GX = 4, SQ_STASH_GY = 3, SQ_STASH_COLS = 6, SQ_STASH_ROWS = 2;
|
||||||
constexpr int SQ_STASH_CELLS = SQ_STASH_COLS * SQ_STASH_ROWS;
|
constexpr int SQ_STASH_CELLS = SQ_STASH_COLS * SQ_STASH_ROWS;
|
||||||
|
|
||||||
// Мастерская. Слева схрон той же сеткой, что и на экране отряда (привычка
|
// Мастерская. Слева схрон той же сеткой, что и на экране отряда (привычка
|
||||||
@ -385,9 +388,16 @@ void DrawTip(const Layout& l, const Tip& t, int mx, int my)
|
|||||||
int wide = 0;
|
int wide = 0;
|
||||||
for (int i = 0; i < t.count; ++i) wide = std::max(wide, TextW(l, t.lines[i], 8));
|
for (int i = 0; i < t.count; ++i) wide = std::max(wide, TextW(l, t.lines[i], 8));
|
||||||
|
|
||||||
UiRect box{mx + 8, my + 8, wide + pad * 2, t.count * step + pad * 2 - 2};
|
// Подсказка встаёт ВЫШЕ курсора, а не ниже.
|
||||||
// Подсказка не должна вылезать за кадр: у края экрана она переезжает на
|
//
|
||||||
// другую сторону курсора, а не обрезается.
|
// Ниже она ложилась ровно на строки характеристик — те самые, которые
|
||||||
|
// объясняет: игрок наводил на броню, чтобы понять, откуда 28%, и подсказка
|
||||||
|
// закрывала строку ARMOR. Сверху под ней только пустое место.
|
||||||
|
UiRect box{mx + 8, my - 6 - (t.count * step + pad * 2 - 2), wide + pad * 2,
|
||||||
|
t.count * step + pad * 2 - 2};
|
||||||
|
|
||||||
|
// И не должна вылезать за кадр: у края экрана она переезжает на другую
|
||||||
|
// сторону курсора, а не обрезается.
|
||||||
if (box.x + box.w > INTERNAL_W - 2) box.x = mx - 8 - box.w;
|
if (box.x + box.w > INTERNAL_W - 2) box.x = mx - 8 - box.w;
|
||||||
if (box.y + box.h > INTERNAL_H - 2) box.y = INTERNAL_H - 2 - box.h;
|
if (box.y + box.h > INTERNAL_H - 2) box.y = INTERNAL_H - 2 - box.h;
|
||||||
if (box.x < 2) box.x = 2;
|
if (box.x < 2) box.x = 2;
|
||||||
@ -545,11 +555,13 @@ void BuildWidgets(const Ui& ui, const Game& game, const Layout& l, Widgets& out)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Сумка ОБЩАЯ: она не зависит от выбранного бойца, и переложить
|
// Сумка ОБЩАЯ: она не зависит от выбранного бойца, и переложить
|
||||||
// из неё может любой (sim/items.h).
|
// из неё может любой (sim/items.h). Вне вылазки она недоступна:
|
||||||
|
// сумка принадлежит забегу и очищается при сборке этажа.
|
||||||
for (int s = 0; s < SQUAD_BAG_SLOTS; ++s)
|
for (int s = 0; s < SQUAD_BAG_SLOTS; ++s)
|
||||||
{
|
{
|
||||||
Widget& w = out.Add(UiPart::BAG, s, BagRect(s));
|
Widget& w = out.Add(UiPart::BAG, s, BagRect(s));
|
||||||
w.item = game.squad.bag.slots[s];
|
w.item = game.squad.bag.slots[s];
|
||||||
|
w.enabled = ui.started;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Строки характеристик тоже наводимы. Читать «SPEED 95%» и гадать,
|
// Строки характеристик тоже наводимы. Читать «SPEED 95%» и гадать,
|
||||||
@ -1364,7 +1376,10 @@ void DrawSquadScreen(const Layout& l, const Game& game, const Ui& ui, const Widg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(l, "BAG", SQ_BAG_X, SQ_BAG_Y - 14, 10, COL_TEXT);
|
// Подпись честно говорит, почему сумка не берёт вещи вне забега. Молчащий
|
||||||
|
// отказ читается как поломка — а это правило: сумка принадлежит вылазке.
|
||||||
|
if (ui.started) Text(l, "BAG", SQ_BAG_X, SQ_BAG_Y - 14, 10, COL_TEXT);
|
||||||
|
else Text(l, "BAG - RAID ONLY", SQ_BAG_X, SQ_BAG_Y - 14, 8, COL_DEAD);
|
||||||
|
|
||||||
if (game.profile)
|
if (game.profile)
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user