Пистолет не пропадает, эвакуация откуда угодно, кнопки в HUD (M36)

БАГ С ПИСТОЛЕТОМ - на самом деле ДВА бага.

Первый: экран отряда правит ЖИВОЙ МИР, а вылазка собирает бойцов заново
из профиля (NewRun пересоздаёт ecs::World и раздаёт снаряжение через
ApplyProfileLoadout). Всё, переложенное в меню, стиралось на старте, а
вещь, вынутая из схрона, исчезала вместе с миром - схрон её уже отдал.
Теперь всякая правка снаряжения пишется в профиль сразу.

Второй, рядом: перетаскивание мышью НЕ РАБОТАЛО ВООБЩЕ. Флаг dropOk
нигде не выставлялся в true - только сбрасывался, - и сброс молча
отменялся. Единственным рабочим способом переодеться оставался ENTER.
Теперь флаг считается каждый кадр, и ячейка честно красится в "нельзя".

Заодно: вне забега сумка не принимает вещей. Она принадлежит вылазке и
очищается при сборке этажа; вещь, положенная в неё из меню, исчезла бы
так же молча.

Проверка ловит именно цепочку "поменял в меню -> начал вылазку -> вышел
с этим": сам обмен работал и раньше, пропадал он на старте.

  сборы: в профиле после обмена PISTOL, на этаже PISTOL

ТВАРИ НЕ ВОСКРЕСАЮТ. Убитая в этой вылазке убита насовсем: с возвратом
любая расчищенная комната через полминуты снова была полна, и "пробиться
и уйти" не отличалось от "молотить бесконечную очередь". Исключение одно
- аура плакальщика, это его роль.

ЭВАКУАЦИЯ С ЛЮБОГО ЭТАЖА И ИЗ ЛЮБОЙ ТОЧКИ. Раньше уйти можно было только
стоя на кольце: отряд, решивший не рисковать, всё равно шёл через этаж и
умирал ровно там, где игрок уже принял решение. Чекпоинты теперь значат
только то, чем должны быть: откуда можно НАЧАТЬ следующую вылазку.

ОТЗЫВЧИВОСТЬ СТРОЯ. Отряд ходил 1.75 при курсоре 3.3 - он всегда
отставал, и управление ощущалось как "тяну за верёвку". Стало 2.6,
поводок короче (2.5 против 3.0), доворот строя вдвое резче.

Ориентация строя теперь ПОВОРАЧИВАЕТСЯ, а не щёлкает: раньше слоты клина
перепрыгивали через весь отряд на каждом повороте, и бойцы бросали дорогу
ради перестроения. В тесноте доворот почти мгновенный - там строй и так
колонна, а колонна, смотрящая в прошлую сторону, упирается в стену.

Автопилот поймал, что первая версия правок сделала хуже: при быстром
наборе натяжения (5.2) отряд уходил в вечный спринт, а спринт растягивает
строй и выключает огонь. Порог спринта поднят до 0.95, набор до 3.4 -
приёмка 11.4 держится (0.13 с при пороге 0.30).

КНОПКИ В HUD. Слева внизу четыре: SQUAD, TREE, WORK, MENU. Экраны
открывались только клавишами, о которых игра нигде не говорила, - без
кнопки мастерская и дерево не открываются никогда.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
z.kirill 2026-08-17 05:24:10 +03:00
parent f7ce189352
commit 53ea43f2a4
22 changed files with 530 additions and 214 deletions

View File

@ -50,12 +50,29 @@ int PickRoom(const RoomLibrary& lib, LevelRng& rng, int depth, bool needStart)
}
// -----------------------------------------------------------------------------
// Прорубка. Коридор шириной 2: отряд из пяти человек в проходе шириной 1 идёт
// строго гуськом, и весь смысл строя пропадает. Ширина берётся ровно такой же,
// как у проходов захардкоженной карты (docs/04-world.md) — иначе поведение
// отряда в коридоре пришлось бы настраивать заново.
// Прорубка. Ширина прохода — ТРИ тайла.
//
// Два тайла хватало ровно на то, чтобы отряд протиснулся гуськом, и не хватало
// ни на что больше. Пятеро в строю занимают поперёк заметно больше одного тела:
// у каждого свой радиус, между ними интервал строя, а на повороте колонна ещё и
// раздувается наружу. В проходе шириной 2 передний упирается в угол, задние — в
// переднего, и отряд встаёт намертво (прогон `--play 4 5`).
//
// Три — это «двое плечом к плечу и место обойти третьего». Меньше не даёт
// разойтись, больше превращает коридор в комнату и съедает разницу между ними.
//
// Отсюда же требование к КОМНАТАМ: сторона не больше 12 при слоте в 16 тайлов.
// Комната шире оставляет между собой и соседкой меньше четырёх клеток камня, а
// в них полоса шириной 3 уже не ложится — поперечный отрезок начинает резать
// чужую стену. Комнату шире 12 генератор не сломает (полоса прижмётся клампом
// ниже), но зазор станет узким местом, ради устранения которого всё и делалось.
// -----------------------------------------------------------------------------
constexpr int CORRIDOR_W = 2;
constexpr int CORRIDOR_W = 3;
// Проём в стене комнаты — ровно той же ширины, что коридор. Разойдись эти два
// числа — узкое место осталось бы В САМОМ ПРОЁМЕ, то есть там, где отряд
// заведомо идёт плотно и где его меньше всего ждёшь.
constexpr int DOORWAY_W = CORRIDOR_W;
// Внешнее кольцо карты остаётся камнем всегда: вне карты считается стеной, и
// коридор, дошедший до края, дал бы отряду выйти в это «вне».
@ -90,6 +107,31 @@ void CarveRun(Tilemap& map, int x0, int y0, int x1, int y1, uint8_t theme)
}
}
// Квадрат CORRIDOR_W x CORRIDOR_W с началом в (x, y).
//
// Нужен ровно на ПОВОРОТЕ. Два отрезка, сходящиеся углом, дают на стыке не
// широкий поворот, а выемку: полоса вдоль x кончается там, где полоса вдоль y
// начинается, и внешний угол остаётся камнем. Отряд в этом месте снова
// протискивается по одному, и весь выигрыш от широкого коридора съедается на
// первом же изгибе.
void CarveCorner(Tilemap& map, int x, int y, uint8_t theme)
{
for (int j = 0; j < CORRIDOR_W; ++j)
for (int i = 0; i < CORRIDOR_W; ++i) CarveCell(map, x + i, y + j, theme);
}
// Начало полосы прохода вдоль стены длиной `extent`.
//
// Полоса растёт от точки контакта в сторону роста координаты, и без клампа она
// вылезала бы за угол комнаты: угол — единственная клетка рамки, за которой
// сразу чужой слот, и дыра в нём читается как обвал, а не как проход. Поэтому
// полоса прижимается к последней НЕугловой клетке стены.
int DoorSpanStart(int contact, int extent, int width)
{
const int last = extent - 2; // последняя неугловая клетка стороны
return std::max(1, std::min(contact, last - width + 1));
}
// Где комната встречает коридор со стороны side: дверь, если она есть, иначе
// середина стены. Второе — не поблажка: комната без двери на нужной стороне
// остаётся легальной, просто генератор пробьёт её сам, и это видно на карте.
@ -121,38 +163,64 @@ void ConnectSlots(Tilemap& map, const RoomLibrary& lib, const GeneratedLevel& lv
ContactPoint(ra, dir, ax, ay);
ContactPoint(rb, OppositeSide(dir), bx, by);
const int dx = SideDX(dir);
const int dy = SideDY(dir);
// Полоса проёма идёт ПОПЕРЁК хода коридора: у восточной двери — по y, у
// северной — по x. Точка контакта задаёт её начало, а не середину: комната
// рисует группу дверей '+++' с той же клетки, и проём совпадает с тем, что
// видно в файле комнаты, а не съезжает от него на тайл.
if (dx != 0)
{
ay = DoorSpanStart(ay, ra.h, DOORWAY_W);
by = DoorSpanStart(by, rb.h, DOORWAY_W);
}
else
{
ax = DoorSpanStart(ax, ra.w, DOORWAY_W);
bx = DoorSpanStart(bx, rb.w, DOORWAY_W);
}
// В координаты карты.
ax += A.originX; ay += A.originY;
bx += B.originX; by += B.originY;
const int dx = SideDX(dir);
const int dy = SideDY(dir);
// Сам проём прорубается всегда и на всю ширину: если его пробили по
// умолчанию, стены на этом месте больше нет, а если это нарисованная дверь —
// она и так пол.
for (int t = 0; t < DOORWAY_W; ++t)
{
CarveCell(map, ax + (dy != 0 ? t : 0), ay + (dx != 0 ? t : 0), theme);
CarveCell(map, bx + (dy != 0 ? t : 0), by + (dx != 0 ? t : 0), theme);
}
// Сама дверь прорубается всегда: если её пробили по умолчанию, стены на
// этом месте больше нет, а если это нарисованная дверь — она и так пол.
CarveCell(map, ax, ay, theme);
CarveCell(map, bx, by, theme);
// Дверь шириной в один тайл упёрлась бы в коридор шириной два: узкое место
// осталось бы ровно в проёме, там, где его меньше всего ждёшь.
CarveCell(map, ax + (dy != 0 ? 1 : 0), ay + (dx != 0 ? 1 : 0), theme);
CarveCell(map, bx + (dy != 0 ? 1 : 0), by + (dx != 0 ? 1 : 0), theme);
// Шаг наружу из обеих дверей — в зазор между комнатами.
// Шаг наружу из обоих проёмов — в зазор между комнатами.
const int aox = ax + dx, aoy = ay + dy;
const int box = bx - dx, boy = by - dy;
if (dx != 0)
{
const int midX = (aox + box) / 2;
// Поперечный отрезок ложится полосой в +x, и удержать его надо В ЗАЗОРЕ:
// вылезь он на тайл — прорезал бы стену соседней комнаты на всю свою
// длину, и вместо комнаты вышел бы сарай с распахнутым боком.
const int lo = std::min(aox, box), hi = std::max(aox, box);
const int midX = std::max(lo, std::min((aox + box) / 2, hi - (CORRIDOR_W - 1)));
CarveRun(map, aox, aoy, midX, aoy, theme);
CarveCorner(map, midX, aoy, theme);
CarveRun(map, midX, aoy, midX, boy, theme);
CarveCorner(map, midX, boy, theme);
CarveRun(map, midX, boy, box, boy, theme);
}
else
{
const int midY = (aoy + boy) / 2;
const int lo = std::min(aoy, boy), hi = std::max(aoy, boy);
const int midY = std::max(lo, std::min((aoy + boy) / 2, hi - (CORRIDOR_W - 1)));
CarveRun(map, aox, aoy, aox, midY, theme);
CarveCorner(map, aox, midY, theme);
CarveRun(map, aox, midY, box, midY, theme);
CarveCorner(map, box, midY, theme);
CarveRun(map, box, midY, box, boy, theme);
}
}

View File

@ -1,24 +1,28 @@
# Открытая арена с четырьмя опорами. Здесь работает решатель огневых позиций:
# места хватает, и отряд обязан разойтись по линиям, а не сбиться в кучу.
#
# Опоры стали ОДНОРЯДНЫМИ вместо блоков 2x2, а сама арена сузилась до 12: блок
# отнимал у центра столько пола, что пятеро вставали в него впритык и переставали
# расходиться. Укрытие нужно, чтобы за него зайти, а не чтобы об него спотыкаться.
name arena
size 14 12
size 12 12
weight 3
theme 0
start 0
depth 1 0
tiles
####++####++##
#............#
#..##....##..#
#..##....##..#
#............#
+............+
+............+
#............#
#..##....##..#
#..##....##..#
#............#
####++####++##
####+++#####
#..........#
#..........#
#..##..##..#
#..........#
+..........+
+..........+
+..........+
#..........#
#..##..##..#
#..........#
####+++#####
spawn 4 4 0.90 shambler:4 rusher:3 spitter:2
spawn 9 4 0.85 shambler:4 rusher:3
spawn 4 7 0.85 shambler:3 rusher:2 brute:1

View File

@ -1,23 +1,27 @@
# Казарма: ряды перегородок и один сквозной проход посередине. Твари выходят
# Казарма: ряды перегородок и сквозной проход посередине. Твари выходят
# из-за переборок сбоку, а не в лоб, — строй приходится разворачивать на ходу.
#
# Перегородок стало вдвое меньше, а проход посередине — в три ряда. Прежние пять
# рядов по два тайла резали комнату на щели шириной в тело: сбоку выходить было
# откуда, а разворачиваться в ответ уже негде.
name barracks
size 13 9
size 12 9
weight 2
theme 1
start 0
depth 2 0
tiles
#####+++#####
#...........#
#.##.##.##..#
#.##.##.##..#
+...........+
#.##.##.##..#
#.##.##.##..#
#...........#
#####+++#####
####+++#####
#..........#
#..##..##..#
+..........+
+..........+
+..........+
#..##..##..#
#..........#
####+++#####
spawn 4 1 0.75 shambler:3 rusher:2
spawn 8 1 0.60 spitter:2 shambler:1
spawn 11 4 0.70 rusher:3 lurker:1
spawn 10 4 0.70 rusher:3 lurker:1
spawn 4 7 0.75 shambler:3 brute:1
spawn 1 4 0.45 crate:2 barrel:2

View File

@ -1,25 +1,28 @@
# Перекрёсток. Рукава шириной три тайла: правило «не стрелять сквозь своих»
# упирается в геометрию, и отряд вытягивается в колонну.
# Перекрёсток. Рукава шириной три тайла — ровно как коридоры между комнатами:
# правило «не стрелять сквозь своих» упирается в геометрию, и отряд вытягивается
# в колонну, но не встаёт в ней намертво.
#
# Середина расширена до четырёх рядов: перекрёсток — единственное место, где
# колонна имеет право снова стать строем, и на трёх рядах она этого не успевала.
name corridor_x
size 13 13
size 12 12
weight 3
theme 0
start 0
depth 1 0
tiles
#####+++#####
#####...#####
#####...#####
#####...#####
#####...#####
+...........+
+...........+
+...........+
#####...#####
#####...#####
#####...#####
#####...#####
#####+++#####
####+++#####
####...#####
####...#####
####...#####
+..........+
+..........+
+..........+
#..........#
####...#####
####...#####
####...#####
####+++#####
spawn 6 2 0.60 rusher:3 lurker:1
spawn 6 10 0.60 rusher:3 lurker:1
spawn 2 6 0.50 shambler:2 spitter:1

View File

@ -1,5 +1,9 @@
# Стартовая: просторно, светло по планировке и почти пусто.
# Отряд обязан успеть собраться в строй до первой твари.
#
# Поперёк комнаты идёт сплошная полоса пола в три ряда, и опор осталось по одной
# на угол. Первое, что отряд делает после высадки, — разворачивается; делать это
# должно быть где, иначе первые секунды каждого этажа уходят на распутывание строя.
name hall_start
size 12 10
weight 2
@ -7,15 +11,15 @@ theme 0
start 1
depth 1 0
tiles
#####++#####
####+++#####
#..........#
#..........#
#..#....#..#
#..#....#..#
+..........+
+..........+
#..#....#..#
+..........+
#..#....#..#
#..........#
#####++#####
####+++#####
spawn 2 2 0.50 crate:2 barrel:1
spawn 9 7 0.40 barrel:1 locker:1

View File

@ -1,5 +1,9 @@
# Гнездо: кольцевые перегородки и ядро посередине. Линий огня почти нет,
# Гнездо: глухое ядро посередине и кольцо вокруг него. Линий огня почти нет,
# зато клинку есть где работать — сектор упирается в стену, а не в союзника.
#
# Кольцо теперь в три тайла со всех сторон, а перегородок внутри него нет вовсе.
# Прежние два вложенных кольца давали проходы шириной в одну клетку: отряд не
# обходил ядро, он в нём застревал, и «нет линий огня» превращалось в «нет игры».
name nest
size 12 12
weight 2
@ -7,18 +11,18 @@ theme 2
start 0
depth 4 0
tiles
#####++#####
####+++#####
#..........#
#.##....##.#
#.#......#.#
#.#..##..#.#
+....##....+
+....##....+
#.#..##..#.#
#.#......#.#
#.##....##.#
#..........#
#####++#####
#..........#
#...####...#
+...####...+
+...####...+
+..........+
#..........#
#..........#
#..........#
####+++#####
spawn 5 1 0.85 shambler:3 brute:1
spawn 2 5 0.80 lurker:2 rusher:2
spawn 9 5 0.80 lurker:2 rusher:2

View File

@ -1,28 +1,30 @@
# Яма: колодец посередине с единственным входом снизу. Громилу в нём
# приходится разбирать вдвоём, а круг его удара достаёт обоих.
#
# Колодец сузился до двух тайлов, зато обход вокруг него стал трёхрядным. Смысл
# комнаты в том, что В КОЛОДЦЕ тесно, а не в том, что тесно везде: раньше отряд
# вставал ещё на подходе и до ямы просто не доходил.
name pit
size 14 14
size 12 12
weight 2
theme 2
start 0
depth 6 0
tiles
######++######
#............#
#............#
#...######...#
#...#....#...#
+...#....#...+
+...#....#...+
+...#....#...+
+...#....#...+
#...#....#...#
#...##..##...#
#............#
#............#
######++######
spawn 6 6 0.95 brute:3 shambler:2
####+++#####
#..........#
#..........#
#..........#
#...####...#
+...#..#...+
+...#..#...+
+...#..#...+
#..........#
#..........#
#..........#
####+++#####
spawn 5 6 0.95 brute:3 shambler:2
spawn 2 4 0.70 spitter:2 rusher:2
spawn 11 4 0.70 spitter:2 rusher:2
spawn 6 12 0.60 shambler:3 lurker:1
spawn 2 11 0.40 barrel:2 crate:1
spawn 9 4 0.70 spitter:2 rusher:2
spawn 6 10 0.60 shambler:3 lurker:1
spawn 2 9 0.40 barrel:2 crate:1

View File

@ -1,5 +1,9 @@
# Склад: обстановки много, тварей мало. Место, где игрок узнаёт, что хлам
# ломается, — и тратит на это магазин, которого потом не хватит.
#
# Стеллаж стал одним глухим блоком посередине, а обход вокруг него — трёхрядным.
# Прежняя двойная рамка оставляла по краям щели в один тайл: отряд входил на
# склад и не мог из него выйти иначе как гуськом.
name storage
size 12 10
weight 2
@ -7,17 +11,17 @@ theme 1
start 0
depth 1 0
tiles
####++######
####+++#####
#..........#
#.####.###.#
#.#......#.#
+.#......#.+
+.#......#.+
#.#......#.#
#.###.####.#
#..........#
######++####
spawn 4 4 0.90 crate:4 barrel:3 locker:2
spawn 7 4 0.85 crate:3 barrel:2
spawn 4 6 0.70 barrel:2 locker:2
#...####...#
+...####...+
+...####...+
+..........+
#..........#
#..........#
####+++#####
spawn 2 4 0.90 crate:4 barrel:3 locker:2
spawn 9 4 0.85 crate:3 barrel:2
spawn 5 7 0.70 barrel:2 locker:2
spawn 1 1 0.45 shambler:2 lurker:1

View File

@ -1,5 +1,9 @@
# Схрон: тесная камера в камере. Хват здесь опаснее всего — вырванного из
# строя бойца в такой тесноте нечем прикрыть.
# Схрон: тесная комната с глухим сейфом посередине. Хват здесь опаснее всего —
# вырванного из строя бойца в такой тесноте нечем прикрыть.
#
# Тесно тут за счёт РАЗМЕРА комнаты, а не за счёт щелей: камера в камере давала
# обходы шириной в одну клетку, и отряд не столько рисковал, сколько стоял.
# Обход теперь трёхрядный, а схрон всё равно самая маленькая комната набора.
name vault
size 10 10
weight 1
@ -7,17 +11,17 @@ theme 3
start 0
depth 8 0
tiles
###++#####
####+++###
#........#
#.######.#
#.#....#.#
+.#....#.+
+.#....#.+
#.#....#.#
#.##..##.#
#........#
#####++###
spawn 4 4 0.90 lurker:3 brute:1
#........#
+...##...+
+...##...+
+...##...+
#........#
#........#
####+++###
spawn 7 5 0.90 lurker:3 brute:1
spawn 1 1 0.55 rusher:2 shambler:1
spawn 8 8 0.55 spitter:2
spawn 4 8 0.40 locker:2 crate:1

View File

@ -1,19 +1,22 @@
# Ряд камер. Ниши по бокам: в них удобно стоять и невозможно отступать.
#
# Ниш стало меньше, а проход между рядами — трёхрядным: ниша должна быть местом,
# КУДА зашли, а не единственным местом, где помещается тело.
name cells
size 13 9
size 12 9
weight 3
theme 0
depth 1 0
tiles
####+++++####
#...........#
#.#.#.#.#.#.#
+...........+
+...........+
+...........+
#.#.#.#.#.#.#
#...........#
####+++++####
####+++#####
#..........#
#.#.#..#.#.#
+..........+
+..........+
+..........+
#.#.#..#.#.#
#..........#
####+++#####
spawn 2 1 0.65 rusher:3 shambler:2
spawn 10 7 0.65 rusher:3 shambler:2
spawn 6 4 0.40 bandage:2 knife:1 rifle:1

View File

@ -4,24 +4,27 @@
# У ядра ДВА прохода, север и юг. Глухая коробка посередине выглядит на сетке
# так же, но точка спавна внутри неё недостижима: тварь не дойдёт до отряда и
# не будет убита, и этаж тихо станет легче. Ровно это и ловит --check.
#
# Кольцо вокруг ядра — три тайла со всех сторон: встать кругом впятером нельзя
# в обходе шириной в два тела, там можно только выстроиться в очередь.
name maw
size 13 11
size 12 11
weight 2
theme 2
depth 3 0
tiles
#####+++#####
#...........#
#...........#
#...##.##...#
+...#...#...+
+...#...#...+
+...##.##...+
#...........#
#...........#
#...........#
#####+++#####
####+++#####
#..........#
#..........#
#..........#
#...#..#...#
+...#..#...+
+...#..#...+
+...#..#...+
#...#..#...#
#..........#
####+++#####
spawn 6 5 0.85 brute:3 lurker:2
spawn 1 1 0.60 spitter:2 rusher:2
spawn 11 9 0.60 spitter:2 rusher:2
spawn 11 1 0.45 shotgun:1 plate:1 cleaver:1
spawn 10 9 0.60 spitter:2 rusher:2
spawn 10 1 0.45 shotgun:1 plate:1 cleaver:1

View File

@ -1,5 +1,9 @@
# Стартовая HOLLOW. Тесная и короткая: вид сверху показывает всю комнату
# сразу, и просторный зал первого этажа не оставил бы ничего на потом.
#
# Тесная — по размеру, а не по проходам. От четырёх опор осталась одна: отряд
# высаживается сюда всем составом и первым делом разворачивается, а разворот
# между четырьмя тумбами упирался в них, а не в стены.
name mouth
size 11 9
weight 2
@ -9,11 +13,11 @@ depth 1 0
tiles
####+++####
#.........#
#..##.##..#
#.........#
+.........+
+....#....+
+.........+
#..##.##..#
#.........#
#.........#
####+++####
spawn 2 1 0.45 barrel:1

View File

@ -1,7 +1,11 @@
# Колодец. Узкий и длинный: строй здесь не развернуть, и это единственное,
# что комната делает.
#
# «Не развернуть» — это пять тайлов в ширину против трёх рядов строя, а не щель
# в одну клетку. Перемычки теперь поджимают колодец с БОКОВ до трёх тайлов, а не
# перекрывают его посередине: отряд проходит с трудом, но проходит.
name shaft
size 7 13
size 7 12
weight 2
theme 1
depth 2 0
@ -9,16 +13,15 @@ tiles
##+++##
#.....#
#.....#
#.###.#
##...##
+.....+
+.....+
+.....+
+.....+
#.###.#
##...##
#.....#
#.....#
#.....#
##+++##
spawn 3 1 0.70 lurker:2 rusher:2
spawn 3 11 0.70 lurker:2 spitter:1
spawn 1 6 0.35 vest:1 lantern:1
spawn 3 10 0.70 lurker:2 spitter:1
spawn 1 5 0.35 vest:1 lantern:1

14
save/profile.txt Normal file
View File

@ -0,0 +1,14 @@
# профиль: всё, что переживает вылазку. Формат — sim/profile.h
version 7
maxdepth 1
squad 1
runs 1
extractions 1
wipes 0
xp 63
agent 0 knife jacket lantern
agent 1 saber jacket -
agent 2 rifle jacket -
agent 3 rifle jacket -
agent 4 pistol shroud -
stash knife knife

View File

@ -2823,6 +2823,30 @@ int RunPlay(const RoomLibrary& rooms, const EntityCatalog& catalog, uint32_t see
game.in.moveScreen = v2d::WorldDirToScreen(Normalized(toGoal));
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)
{

View File

@ -933,15 +933,26 @@ void Game::UpdateExit()
const float r = g_tune.descendRadius;
const bool nearExit = Distance(squad.anchor, level.exitPos) <= r;
const bool nearOut = Distance(squad.anchor, level.extractPos) <= r;
// События пишутся на ФРОНТЕ: иначе журнал забивался бы одним и тем же
// сообщением каждый кадр, пока отряд стоит на месте.
if (nearExit && !atExit) events.Push(CombatEventKind::DESCEND, -1, level.exitPos, Vec2{});
if (nearOut && !atExtract) events.Push(CombatEventKind::EXTRACT, -1, level.extractPos, Vec2{});
// ЭВАКУАЦИЯ ВОЗМОЖНА С ЛЮБОГО ЭТАЖА И ИЗ ЛЮБОЙ ТОЧКИ.
//
// Раньше уйти можно было, только стоя на кольце аномалии. Это выглядело как
// правило, а работало как ловушка: отряд, который решил уйти на середине
// этажа, всё равно должен был дойти до конкретной клетки — и умирал по
// дороге ровно там, где игрок уже принял решение не рисковать.
//
// Ставка при этом не ослабла ни на грамм: уносишь то, что на выживших,
// теряешь половину при вайпе. Решение «глубже или домой» осталось решением,
// просто перестало требовать пешей прогулки обратно.
//
// Чекпоинты после этого значат ровно то, чем и должны быть: не «где можно
// выйти», а «откуда можно начать следующую вылазку».
atExit = nearExit;
atExtract = nearOut;
atExtract = true;
// Событие пишется на ФРОНТЕ входа в кольцо: журнал не должен забиваться
// одним и тем же сообщением, пока отряд стоит на месте.
if (nearExit && !atExit) events.Push(CombatEventKind::DESCEND, -1, level.exitPos, Vec2{});
}
void Game::UpdateAim(float dt)

View File

@ -153,13 +153,15 @@ void DrawRunBar(Framebuffer& fb, const Game& game)
//
// Разные цвета — те же, что у колец на полу: подсказка обязана читаться
// как подпись к тому, на чём стоишь, а не как отдельное сообщение.
const char* hint = nullptr;
uint32_t hintCol = COL_READY;
if (game.atExtract) { hint = "EXTRACT: E"; hintCol = COL_OUT; }
else if (game.atExit) { hint = "DESCEND: F"; hintCol = COL_READY; }
if (!hint) return;
// Уйти можно откуда угодно, поэтому «EXTRACT: E» висит всегда — но тускло и
// мелко, как справка. Спуск наоборот: он привязан к месту, и его подсказка
// появляется только на кольце, зато в полную силу.
if (game.atExit)
MicroTextShadow(fb, (fb.w - MicroTextWidth("DESCEND: F")) / 2, 14, "DESCEND: F",
COL_READY, COL_SHADOW);
MicroTextShadow(fb, (fb.w - MicroTextWidth(hint)) / 2, 14, hint, hintCol, COL_SHADOW);
MicroTextShadow(fb, fb.w - MicroTextWidth("EXTRACT: E") - 5, 5, "EXTRACT: E", COL_OUT,
COL_SHADOW);
}
} // namespace

View File

@ -172,6 +172,30 @@ void Squad::UpdateAnchorAndState(Vec2 moveWorldDir, bool sprint, const Tilemap&
{
anchorPrev = anchor;
// --- теснота ---------------------------------------------------------------
// Сколько свободного места вокруг ТЕЛ. Считается грубо: доля проходимых
// клеток в квадрате рядом. Этого хватает, чтобы отличить зал от коридора и
// от дверного проёма, а честный замер ширины прохода стоил бы поиска пути
// там, где его нет.
//
// Считается ЗДЕСЬ, до всего остального: от тесноты зависят и доворот строя,
// и его ширина, и обе вещи нужны в этом же шаге.
int freeTiles = 0, seenTiles = 0;
{
const Vec2 body = Centroid();
const int ax = int(body.x), ay = int(body.y);
for (int dy = -2; dy <= 2; ++dy)
for (int dx = -2; dx <= 2; ++dx)
{
const int nx = ax + dx, ny = ay + dy;
if (nx < 0 || ny < 0 || nx >= MAP_W || ny >= MAP_H) continue;
++seenTiles;
if (!map.IsWall(nx, ny)) ++freeTiles;
}
}
const float openness = (seenTiles > 0) ? float(freeTiles) / float(seenTiles) : 1.0f;
const float tight = Clampf((0.72f - openness) / 0.32f, 0.0f, 1.0f);
const bool moving = LengthSq(moveWorldDir) > 1e-6f;
const AgentState prevState = state;
const Vec2 c = Centroid();
@ -213,7 +237,24 @@ void Squad::UpdateAnchorAndState(Vec2 moveWorldDir, bool sprint, const Tilemap&
anchorVel += ClampLength(dv, std::max(0.1f, g_tune.anchorAccel) * dt);
anchor += anchorVel * dt;
if (moving) forward = Normalized(moveWorldDir, forward);
// Ориентация строя ПОВОРАЧИВАЕТСЯ, а не щёлкает. Раньше она мгновенно
// равнялась направлению ввода, и на каждом повороте слоты клина
// перепрыгивали через весь отряд: бойцы бросали дорогу и бежали вбок,
// занимать новые места. Снаружи это и читалось как «строй тормозит» —
// отряд был занят перестроением, а не движением.
if (moving)
{
const Vec2 want = Normalized(moveWorldDir, forward);
// В ЗАЛЕ доворот плавный: мгновенный заставлял бойцов бросать дорогу и
// бежать вбок за перепрыгнувшими слотами. В ТЕСНОТЕ наоборот — почти
// мгновенный: там строй и так колонна, и колонна, смотрящая в прошлую
// сторону, упирается в стену. Замер поймал это сразу: со сглаживанием
// на всех случаях автопилот перестал проходить даже первый этаж.
const float rate = Lerpf(g_tune.formTurnRate, 40.0f, tight);
const float k = 1.0f - std::exp(-rate * dt);
forward = Normalized(forward + (want - forward) * k, want);
}
// Жёсткий предел. Сопротивление обычно не даёт до него дойти, но отряд может
// и застрять в стене — тогда курсор без предела уехал бы за полкарты.
@ -243,41 +284,6 @@ void Squad::UpdateAnchorAndState(Vec2 moveWorldDir, bool sprint, const Tilemap&
if (state == AgentState::HOLDING) holdElapsed += dt;
}
// --- теснота ---------------------------------------------------------------
// Сколько свободного места вокруг курсора. Считается грубо: доля
// проходимых клеток в квадрате рядом. Этого хватает, чтобы отличить зал от
// коридора и от дверного проёма, а честный замер ширины прохода стоил бы
// поиска пути там, где его нет.
//
// Зачем: клин шириной в пять тел не пролезает в дверь шириной в одну
// клетку. Бойцы упираются друг в друга, расталкивание держит их на месте, и
// ОТРЯД ВСТАЁТ НАСМЕРТЬ — курсор уходит вперёд, поводок натягивается,
// и всё. Автопилот (`--play`) ловил это как «пятеро не проходят первый
// этаж там, где одиночка проходит».
//
// Поэтому в тесноте строй сжимается сам, до колонны. Это не «удобство»:
// без этого половина комнат набора непроходима для полного отряда.
int freeTiles = 0, seenTiles = 0;
{
// Меряем вокруг ТЕЛ, а не вокруг курсора: курсор к этому моменту уже
// ушёл вперёд в открытый коридор, а упираются в проём именно бойцы.
// С замером у курсора сжатие включалось там, где оно уже не нужно.
const Vec2 body = Centroid();
const int ax = int(body.x), ay = int(body.y);
for (int dy = -2; dy <= 2; ++dy)
for (int dx = -2; dx <= 2; ++dx)
{
const int nx = ax + dx, ny = ay + dy;
if (nx < 0 || ny < 0 || nx >= MAP_W || ny >= MAP_H) continue;
++seenTiles;
if (!map.IsWall(nx, ny)) ++freeTiles;
}
}
// 1.0 — открытый зал, 0.0 — щель. Полностью свободный квадрат 5x5 это 25
// клеток; порог 0.6 отделяет коридор от комнаты.
const float openness = (seenTiles > 0) ? float(freeTiles) / float(seenTiles) : 1.0f;
const float tight = Clampf((0.72f - openness) / 0.32f, 0.0f, 1.0f);
// --- дыхание строя ---------------------------------------------------------
const float wantSpacing = (state == AgentState::HOLDING)
? g_tune.holdSpacing

View File

@ -141,6 +141,12 @@ struct EnemyDef
EnemyDef EnemyDefOf(EnemyKind kind);
const char* EnemyName(EnemyKind kind);
// Сколько «живёт» труп твари. Практически вечность: убитая в вылазке тварь
// не возвращается. Не бесконечность только потому, что плакальщик умеет
// ускорять этот таймер своей аурой, а деление вечности ни на что не даёт
// ничего.
constexpr float ENEMY_STAYS_DEAD = 100000.0f;
struct Target
{
ecs::Entity self = ecs::NO_ENTITY;
@ -228,14 +234,23 @@ struct Target
// напряжения, глаза в темноте, знаменатель покрытия в решателе.
bool IsProp() const { return kind >= FIRST_PROP_KIND; }
// Мишень приёмки встаёт быстро — на ней меряют. Тварь не возвращается,
// пока отряд не успеет уйти: иначе зачистка не имеет смысла.
// Разбитый ящик не встаёт НИКОГДА: разрушаемая обстановка, которая
// отрастает обратно, — это не обстановка, а мишень.
// Мишень приёмки встаёт быстро — на ней меряют, и без возврата мерить было
// бы нечего: сценарии длятся дольше, чем живут мишени.
//
// ТВАРЬ НЕ ВСТАЁТ ВООБЩЕ. Убитая в этой вылазке — убита насовсем: зачистка
// обязана что-то значить. С возвратом любая расчищенная комната через
// полминуты снова была полна, и «пробиться и уйти» переставало отличаться
// от «стоять и молотить бесконечную очередь».
//
// Единственное исключение — аура плакальщика (sim/target.cpp): он поднимает
// ПАВШИХ рядом с собой, и это его роль, а не общее правило. Поэтому не
// «мгновенно навсегда», а очень долгий срок, который аура умеет ускорить.
// Разбитый ящик не встаёт никогда: обстановка, которая отрастает, — это
// не обстановка, а мишень.
float RespawnDelay() const
{
if (IsProp()) return -1.0f;
return IsDummy() ? g_tune.targetRespawn : g_tune.enemyRespawn;
return IsDummy() ? g_tune.targetRespawn : ENEMY_STAYS_DEAD;
}
// Замах виден: единственный признак, по которому игрок успевает среагировать.

View File

@ -71,12 +71,19 @@ constexpr int SOLVER_CANDIDATES = SOLVER_RINGS * SOLVER_DIRS + 1;
struct Tuning
{
// движение агента
float moveSpeed = 1.75f; // юнитов/сек: хоррор, а не шутер-беготня
float moveSpeedFast = 3.6f; // догоняют курсор быстро — иначе отряд отстаёт
float steerAccel = 18.0f; // тяжелее; НИЖЕ 16 нельзя — ломается критерий 11.4
// Скорость отряда. Была 1.75 при курсоре 3.3: отряд ВСЕГДА отставал, и
// управление ощущалось как «тяну за верёвку», а не «веду людей». Поднято до
// 2.6 — это по-прежнему медленнее курсора (иначе поводок теряет смысл), но
// разрыв закрывается за доли секунды, а не за метры.
float moveSpeed = 2.60f; // юнитов/сек: хоррор, а не шутер-беготня
float moveSpeedFast = 4.4f; // догоняют курсор быстро — иначе отряд отстаёт
float steerAccel = 26.0f; // тяжелее; НИЖЕ 16 нельзя — ломается критерий 11.4
float bodyRadius = 0.28f;
float separationRadius = 0.7f;
float wSeek = 1.0f;
// Как быстро строй доворачивается к направлению хода. Мгновенный доворот
// заставлял бойцов бегать вбок на каждом повороте (sim/squad.cpp).
float formTurnRate = 7.0f;
float wSeparation = 1.0f;
float wLaneAvoid = 2.4f; // строго выше wSeparation (спек 6.7)
float wWallAvoid = 1.2f;
@ -237,8 +244,8 @@ struct Tuning
// anchorLeash — мягкая граница: до неё курсор идёт свободно;
// anchorLeashMax — жёсткий предел: страховка, до неё обычно не доходит;
// leashDrag — насколько тяжелеет курсор между границами (0..1).
float anchorLeash = 3.0f;
float anchorLeashMax = 5.0f;
float anchorLeash = 2.5f;
float anchorLeashMax = 4.2f;
float leashDrag = 0.75f; // на жёсткой границе остаётся 25% скорости
// --- натяжение поводка (см. docs/05-squad-ai.md) --------------------------
@ -246,9 +253,14 @@ struct Tuning
// быстрее, чем отряд шагает. Это и есть команда «бегом»: отряд ускоряется,
// строй растягивается, а на верхней трети шкалы огонь прекращается вовсе.
float urgeStartFrac = 0.50f; // с какой доли поводка начинается натяжение
float urgeAttack = 2.6f; // как быстро натяжение набирается
// Натяжение набирается быстро, но НЕ мгновенно, и порог спринта поднят.
// При urgeAttack 5.2 отряд уходил в спринт от любого движения курсора, а
// спринт растягивает строй и выключает огонь: в коридорах это выглядело как
// «отряд застрял». Поймано автопилотом (--play): первый этаж переставал
// проходиться вообще.
float urgeAttack = 3.4f; // как быстро натяжение набирается
float urgeRelease = 3.4f; // и как быстро отпускает
float sprintAt = 0.85f; // выше — спринт: бежим, не атакуем.
float sprintAt = 0.95f; // выше — спринт: бежим, не атакуем.
// Порог высокий намеренно: при удержании направления натяжение садится
// примерно на 0.54, то есть отряд идёт быстрым шагом И ПРОДОЛЖАЕТ работать
// по целям. Спринт остаётся тем, чем должен быть, — отдельным решением
@ -442,6 +454,7 @@ inline const TunableParam TUNABLES[] = {
{"predictSlack", &Tuning::shotPredictSlack, 0.05f, 0.0f, 2.0f},
{"wSeparation", &Tuning::wSeparation, 0.1f, 0.0f, 10.0f},
{"steerAccel", &Tuning::steerAccel, 1.0f, 2.0f, 60.0f},
{"formTurnRate", &Tuning::formTurnRate, 0.5f, 0.5f, 30.0f},
{"moveSpeed", &Tuning::moveSpeed, 0.1f, 0.5f, 10.0f},
{"moveSpeedFast", &Tuning::moveSpeedFast, 0.1f, 0.5f, 12.0f},
{"weaponRange", &Tuning::weaponRange, 0.25f, 2.0f, 20.0f},

View File

@ -165,6 +165,28 @@ constexpr int WS_SLOT_CELLS = WS_SLOT_COLS * WS_SLOT_ROWS;
// внизу экрана. Кадр 480x270, и место здесь считается по-настоящему.
constexpr int WS_OP_X = 252, WS_OP_Y = 84, WS_OP_W = 212, WS_OP_H = 20, WS_OP_STEP = 22;
// -----------------------------------------------------------------------------
// Нижняя панель боя: четыре кнопки, открывающие экраны.
//
// До неё все экраны открывались только клавишами (I, U, ESC), и знать их было
// неоткуда: игра нигде о них не говорит. Кнопка — это ещё и НАПОМИНАНИЕ, что
// экраны существуют, а мастерская и дерево без такого напоминания просто не
// открываются никогда.
//
// Панель слева, карточки бойцов по центру: они и так занимают середину, и
// класть кнопки под них значило бы драться за то же место.
constexpr int HB_X = 4, HB_W = 40, HB_H = 11, HB_GAP = 2;
constexpr int HB_COUNT = 4;
// SQUAD, TREE, WORKSHOP, MENU. Порядок — порядок частоты нажатий.
const char* const HUD_BUTTONS[HB_COUNT] = {"SQUAD", "TREE", "WORK", "MENU"};
inline UiRect HudButtonRect(int i, int frameH)
{
const int y = frameH - (HB_COUNT - i) * (HB_H + HB_GAP) - 2;
return UiRect{HB_X, y, HB_W, HB_H};
}
// Выбор чекпоинта: столбец кнопок «начать с этажа N».
constexpr int DEPTH_Y0 = 92, DEPTH_STEP = 18, DEPTH_SIZE = 10;
constexpr int DEPTH_MAX_SHOWN = 7; // больше на экран не влезает
@ -429,6 +451,14 @@ void BuildWidgets(const Ui& ui, const Game& game, const Layout& l, Widgets& out)
switch (ui.screen)
{
case Screen::GAME:
// Кнопки живут и в бою: экраны вне боя должны открываться мышью, а
// не только клавишами, о которых игра нигде не говорит.
for (int i = 0; i < HB_COUNT; ++i)
{
Widget& b = out.Add(UiPart::HUD_BUTTON, i, HudButtonRect(i, l.fh));
b.text = HUD_BUTTONS[i];
b.size = 7;
}
return;
case Screen::MENU:
@ -595,8 +625,13 @@ bool IsSlot(UiPart p)
p == UiPart::BAG || p == UiPart::STASH;
}
bool SlotAccepts(UiPart part, ItemId id)
// started — вылазка уже идёт. Вне забега сумки НЕТ: она принадлежит вылазке и
// очищается при сборке этажа. Вещь, положенная в неё из меню, исчезла бы вместе
// с миром — молча, как и пропадал пистолет до этой правки.
bool SlotAccepts(UiPart part, ItemId id, bool started = true)
{
if (part == UiPart::BAG && !started) return false;
switch (part)
{
// Пустых рук не бывает: отряд не носит воздух. ItemId::NONE — не оружие,
@ -610,9 +645,31 @@ bool SlotAccepts(UiPart part, ItemId id)
}
}
bool CanDrop(UiPart from, ItemId moved, UiPart to, ItemId there)
bool CanDrop(UiPart from, ItemId moved, UiPart to, ItemId there, bool started)
{
return SlotAccepts(to, moved) && SlotAccepts(from, there);
return SlotAccepts(to, moved, started) && SlotAccepts(from, there, started);
}
// -----------------------------------------------------------------------------
// Сохранить снаряжение бойца В ПРОФИЛЬ.
//
// Без этого сборы перед вылазкой не значили НИЧЕГО. Экран отряда правит живой
// мир (компонент Inventory), а вылазка собирает бойцов заново: NewRun пересоздаёт
// ecs::World и раздаёт снаряжение из профиля (Game::ApplyProfileLoadout). Всё,
// что игрок переложил в меню, стиралось при старте, а вещь, вынутая из схрона,
// исчезала вместе с миром — схрон-то её уже отдал.
//
// Снаружи это выглядело ровно так, как в жалобе: «переодел пистолет первому —
// он выходит с ножом, а пистолет вообще пропал».
//
// Пишем ВСЕГДА, а не только вне забега: посреди вылазки профиль всё равно
// перезапишется при эвакуации, а лишняя запись стоит ничего.
void SaveLoadout(Game& game, int slot)
{
if (!game.profile) return;
if (slot < 0 || slot >= NUM_AGENTS) return;
if (const Inventory* inv = game.world.Get<Inventory>(game.squad.agents[slot].self))
game.profile->loadout[slot] = *inv;
}
// Надетое живёт в инвентаре бойца, сумка — в отряде, поэтому указатель на
@ -676,6 +733,7 @@ void DropToFloor(Ui& ui, Game& game, UiPart part, int index)
// без неё тот же шаг поднял бы её обратно.
SpawnFloorItem(game.world, *slot, a.pos, 2.0f);
*slot = ItemInstance{};
SaveLoadout(game, ui.agentIndex);
}
void ApplyDrop(Ui& ui, Game& game, UiPart to, int toIndex)
@ -728,6 +786,7 @@ void ApplyDrop(Ui& ui, Game& game, UiPart to, int toIndex)
// поменяться вместе с ними.
if (ui.dragFrom == UiPart::HANDS || to == UiPart::HANDS)
EquipWeapon(game.world, a, inv->weapon);
SaveLoadout(game, ui.agentIndex);
return;
}
@ -743,6 +802,7 @@ void ApplyDrop(Ui& ui, Game& game, UiPart to, int toIndex)
// а статы, класс и правила стрельбы остались от прежнего ствола.
if (ui.dragFrom == UiPart::HANDS || to == UiPart::HANDS)
EquipWeapon(game.world, a, inv->weapon);
SaveLoadout(game, ui.agentIndex);
}
} // namespace
@ -804,6 +864,16 @@ void UiActivate(Ui& ui, Game& game, UiPart part, int index)
ui.screen = Screen::GAME;
return;
case UiPart::HUD_BUTTON:
switch (index)
{
case 0: ui.screen = Screen::SQUAD; break;
case 1: ui.screen = Screen::UPGRADE; break;
case 2: ui.screen = Screen::WORKSHOP; break;
default: ui.screen = Screen::PAUSE; break;
}
return;
case UiPart::CRAFT_SLOT:
// Клик по ячейке схрона: обычный — положить вещь на верстак,
// повторный по той же — назначить её ДОНОРОМ для отпечатка.
@ -909,6 +979,11 @@ int Step()
} // namespace
void UiApplyDropForTest(Ui& ui, Game& game, UiPart to, int toIndex)
{
ApplyDrop(ui, game, to, toIndex);
}
int UiMenuItemCount() { return MENU_COUNT; }
const char* UiMenuItemName(int index)
@ -961,7 +1036,15 @@ bool Ui::Update(Game& game, const ScreenRect& fit, float dt)
switch (screen)
{
case Screen::GAME:
// Клик по кнопке панели забирает ввод себе: иначе он ушёл бы в игру
// и отряд дёрнулся бы к курсору в момент открытия экрана.
if (lmbDown && hot && hot->part == UiPart::HUD_BUTTON)
{
UiActivate(*this, game, hot->part, hot->index);
return true;
}
if (Pressed(KEY_ESCAPE)) { screen = Screen::PAUSE; return true; }
if (Pressed(KEY_U)) { screen = Screen::UPGRADE; return true; }
// Только I: TAB в бою переключает цель (core/input.h), и отбирать
// его под инвентарь нельзя.
if (Pressed(KEY_I)) { screen = Screen::SQUAD; return true; }
@ -1094,6 +1177,22 @@ bool Ui::Update(Game& game, const ScreenRect& fit, float dt)
else if (!dragging && IsMouseButtonPressed(MOUSE_BUTTON_RIGHT) && hot &&
IsSlot(hot->part) && hot->item != ItemId::NONE)
DropToFloor(*this, game, hot->part, hot->index);
// Примет ли слот под курсором то, что тащим. Считается КАЖДЫЙ кадр,
// пока предмет на курсоре: по этому же флагу ячейка красится в
// «нельзя» (DrawCell).
//
// Раньше флаг не выставлялся НИГДЕ — только сбрасывался. То есть
// перетаскивание мышью не работало вообще: сброс молча отменялся, и
// единственным рабочим способом переодеться оставался ENTER. Это и
// была вторая половина жалобы «пистолет пропадает»: игрок тащил его
// в руки, отпускал, и не происходило ничего.
if (dragging)
{
const ItemInstance there = hot ? hot->item : ItemInstance{};
dropOk = hot && IsSlot(hot->part) &&
CanDrop(dragFrom, dragItem, hot->part, there, started);
}
if (dragging && lmbUp)
{
if (dropOk && hot) ApplyDrop(*this, game, hot->part, hot->index);
@ -1148,6 +1247,7 @@ bool Ui::Update(Game& game, const ScreenRect& fit, float dt)
{
std::swap(cell, inv->weapon);
EquipWeapon(game.world, a, inv->weapon);
SaveLoadout(game, agentIndex);
}
}
return true;
@ -1332,7 +1432,8 @@ void DrawPointer(const Layout& l, const Ui& ui)
void Ui::Draw(const Game& game, const ScreenRect& fit) const
{
if (screen == Screen::GAME) return;
// В бою меню раньше не рисовалось вовсе. Теперь рисуется — но только нижняя
// панель кнопок: она часть боевого экрана, а не «меню поверх игры».
const Layout l = Of(fit);
const bool pulse = (int(blink * 2.0f) & 1) == 0;
@ -1343,7 +1444,21 @@ void Ui::Draw(const Game& game, const ScreenRect& fit) const
switch (screen)
{
case Screen::GAME:
return;
{
// Панель рисуется поверх готового кадра, как и всё остальное меню:
// сам мир в софтверном фреймбуфере про неё не знает.
for (int i = 0; i < w.count; ++i)
{
const Widget& b = w.item[i];
if (b.part != UiPart::HUD_BUTTON) continue;
const bool hot = (hoverPart == b.part && hoverIndex == b.index);
Box(l, b.rect, hot ? Color{40, 34, 30, 220} : Color{12, 12, 14, 190});
BoxLines(l, b.rect, hot ? COL_PICK : COL_EDGE);
Text(l, b.text, b.rect.x + 4, b.rect.y + 3, 7, hot ? COL_TEXT : COL_FAINT);
}
break;
}
case Screen::MENU:
Shade(l, COL_DIM);

View File

@ -54,6 +54,7 @@ enum class UiPart : uint8_t
CRAFT_ITEM, // строка операции мастерской
CRAFT_SLOT, // ячейка схрона в мастерской: что дорабатываем
CRAFT_AFFIX, // строка аффикса на верстаке: цель направленной операции
HUD_BUTTON, // кнопка нижней панели в бою: открывает экран
};
// Прямоугольник в координатах КАДРА (480x270), а не окна: кадр лежит в окне
@ -142,5 +143,10 @@ void UiActivate(Ui& ui, Game& game, UiPart part, int index);
// Сколько пунктов в главном меню и как называется i-й. Нужны той же проверке:
// она обязана пройти по ВСЕМ пунктам, а не по тем, о которых знает.
// Применить перетаскивание как будто мышью. Наружу — ради проверки «сборы
// переживают старт вылазки» (--ui-check): собрать её через настоящую мышь
// нельзя, окна в прогоне нет.
void UiApplyDropForTest(Ui& ui, Game& game, UiPart to, int toIndex);
int UiMenuItemCount();
const char* UiMenuItemName(int index);