Пистолет не пропадает, эвакуация откуда угодно, кнопки в 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:
parent
f7ce189352
commit
53ea43f2a4
@ -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: дверь, если она есть, иначе
|
// Где комната встречает коридор со стороны side: дверь, если она есть, иначе
|
||||||
// середина стены. Второе — не поблажка: комната без двери на нужной стороне
|
// середина стены. Второе — не поблажка: комната без двери на нужной стороне
|
||||||
// остаётся легальной, просто генератор пробьёт её сам, и это видно на карте.
|
// остаётся легальной, просто генератор пробьёт её сам, и это видно на карте.
|
||||||
@ -121,38 +163,64 @@ void ConnectSlots(Tilemap& map, const RoomLibrary& lib, const GeneratedLevel& lv
|
|||||||
ContactPoint(ra, dir, ax, ay);
|
ContactPoint(ra, dir, ax, ay);
|
||||||
ContactPoint(rb, OppositeSide(dir), bx, by);
|
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;
|
ax += A.originX; ay += A.originY;
|
||||||
bx += B.originX; by += B.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 aox = ax + dx, aoy = ay + dy;
|
||||||
const int box = bx - dx, boy = by - dy;
|
const int box = bx - dx, boy = by - dy;
|
||||||
|
|
||||||
if (dx != 0)
|
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);
|
CarveRun(map, aox, aoy, midX, aoy, theme);
|
||||||
|
CarveCorner(map, midX, aoy, theme);
|
||||||
CarveRun(map, midX, aoy, midX, boy, theme);
|
CarveRun(map, midX, aoy, midX, boy, theme);
|
||||||
|
CarveCorner(map, midX, boy, theme);
|
||||||
CarveRun(map, midX, boy, box, boy, theme);
|
CarveRun(map, midX, boy, box, boy, theme);
|
||||||
}
|
}
|
||||||
else
|
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);
|
CarveRun(map, aox, aoy, aox, midY, theme);
|
||||||
|
CarveCorner(map, aox, midY, theme);
|
||||||
CarveRun(map, aox, midY, box, midY, theme);
|
CarveRun(map, aox, midY, box, midY, theme);
|
||||||
|
CarveCorner(map, box, midY, theme);
|
||||||
CarveRun(map, box, midY, box, boy, theme);
|
CarveRun(map, box, midY, box, boy, theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,28 @@
|
|||||||
# Открытая арена с четырьмя опорами. Здесь работает решатель огневых позиций:
|
# Открытая арена с четырьмя опорами. Здесь работает решатель огневых позиций:
|
||||||
# места хватает, и отряд обязан разойтись по линиям, а не сбиться в кучу.
|
# места хватает, и отряд обязан разойтись по линиям, а не сбиться в кучу.
|
||||||
|
#
|
||||||
|
# Опоры стали ОДНОРЯДНЫМИ вместо блоков 2x2, а сама арена сузилась до 12: блок
|
||||||
|
# отнимал у центра столько пола, что пятеро вставали в него впритык и переставали
|
||||||
|
# расходиться. Укрытие нужно, чтобы за него зайти, а не чтобы об него спотыкаться.
|
||||||
name arena
|
name arena
|
||||||
size 14 12
|
size 12 12
|
||||||
weight 3
|
weight 3
|
||||||
theme 0
|
theme 0
|
||||||
start 0
|
start 0
|
||||||
depth 1 0
|
depth 1 0
|
||||||
tiles
|
tiles
|
||||||
####++####++##
|
####+++#####
|
||||||
#............#
|
#..........#
|
||||||
#..##....##..#
|
#..........#
|
||||||
#..##....##..#
|
#..##..##..#
|
||||||
#............#
|
#..........#
|
||||||
+............+
|
+..........+
|
||||||
+............+
|
+..........+
|
||||||
#............#
|
+..........+
|
||||||
#..##....##..#
|
#..........#
|
||||||
#..##....##..#
|
#..##..##..#
|
||||||
#............#
|
#..........#
|
||||||
####++####++##
|
####+++#####
|
||||||
spawn 4 4 0.90 shambler:4 rusher:3 spitter:2
|
spawn 4 4 0.90 shambler:4 rusher:3 spitter:2
|
||||||
spawn 9 4 0.85 shambler:4 rusher:3
|
spawn 9 4 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
|
||||||
|
|||||||
@ -1,23 +1,27 @@
|
|||||||
# Казарма: ряды перегородок и один сквозной проход посередине. Твари выходят
|
# Казарма: ряды перегородок и сквозной проход посередине. Твари выходят
|
||||||
# из-за переборок сбоку, а не в лоб, — строй приходится разворачивать на ходу.
|
# из-за переборок сбоку, а не в лоб, — строй приходится разворачивать на ходу.
|
||||||
|
#
|
||||||
|
# Перегородок стало вдвое меньше, а проход посередине — в три ряда. Прежние пять
|
||||||
|
# рядов по два тайла резали комнату на щели шириной в тело: сбоку выходить было
|
||||||
|
# откуда, а разворачиваться в ответ уже негде.
|
||||||
name barracks
|
name barracks
|
||||||
size 13 9
|
size 12 9
|
||||||
weight 2
|
weight 2
|
||||||
theme 1
|
theme 1
|
||||||
start 0
|
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
|
||||||
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 4 7 0.75 shambler:3 brute:1
|
||||||
spawn 1 4 0.45 crate:2 barrel:2
|
spawn 1 4 0.45 crate:2 barrel:2
|
||||||
|
|||||||
@ -1,25 +1,28 @@
|
|||||||
# Перекрёсток. Рукава шириной три тайла: правило «не стрелять сквозь своих»
|
# Перекрёсток. Рукава шириной три тайла — ровно как коридоры между комнатами:
|
||||||
# упирается в геометрию, и отряд вытягивается в колонну.
|
# правило «не стрелять сквозь своих» упирается в геометрию, и отряд вытягивается
|
||||||
|
# в колонну, но не встаёт в ней намертво.
|
||||||
|
#
|
||||||
|
# Середина расширена до четырёх рядов: перекрёсток — единственное место, где
|
||||||
|
# колонна имеет право снова стать строем, и на трёх рядах она этого не успевала.
|
||||||
name corridor_x
|
name corridor_x
|
||||||
size 13 13
|
size 12 12
|
||||||
weight 3
|
weight 3
|
||||||
theme 0
|
theme 0
|
||||||
start 0
|
start 0
|
||||||
depth 1 0
|
depth 1 0
|
||||||
tiles
|
tiles
|
||||||
#####+++#####
|
####+++#####
|
||||||
#####...#####
|
####...#####
|
||||||
#####...#####
|
####...#####
|
||||||
#####...#####
|
####...#####
|
||||||
#####...#####
|
+..........+
|
||||||
+...........+
|
+..........+
|
||||||
+...........+
|
+..........+
|
||||||
+...........+
|
#..........#
|
||||||
#####...#####
|
####...#####
|
||||||
#####...#####
|
####...#####
|
||||||
#####...#####
|
####...#####
|
||||||
#####...#####
|
####+++#####
|
||||||
#####+++#####
|
|
||||||
spawn 6 2 0.60 rusher:3 lurker:1
|
spawn 6 2 0.60 rusher:3 lurker:1
|
||||||
spawn 6 10 0.60 rusher:3 lurker:1
|
spawn 6 10 0.60 rusher:3 lurker:1
|
||||||
spawn 2 6 0.50 shambler:2 spitter:1
|
spawn 2 6 0.50 shambler:2 spitter:1
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
# Стартовая: просторно, светло по планировке и почти пусто.
|
# Стартовая: просторно, светло по планировке и почти пусто.
|
||||||
# Отряд обязан успеть собраться в строй до первой твари.
|
# Отряд обязан успеть собраться в строй до первой твари.
|
||||||
|
#
|
||||||
|
# Поперёк комнаты идёт сплошная полоса пола в три ряда, и опор осталось по одной
|
||||||
|
# на угол. Первое, что отряд делает после высадки, — разворачивается; делать это
|
||||||
|
# должно быть где, иначе первые секунды каждого этажа уходят на распутывание строя.
|
||||||
name hall_start
|
name hall_start
|
||||||
size 12 10
|
size 12 10
|
||||||
weight 2
|
weight 2
|
||||||
@ -7,15 +11,15 @@ theme 0
|
|||||||
start 1
|
start 1
|
||||||
depth 1 0
|
depth 1 0
|
||||||
tiles
|
tiles
|
||||||
#####++#####
|
####+++#####
|
||||||
|
#..........#
|
||||||
#..........#
|
#..........#
|
||||||
#..#....#..#
|
|
||||||
#..#....#..#
|
#..#....#..#
|
||||||
+..........+
|
+..........+
|
||||||
+..........+
|
+..........+
|
||||||
#..#....#..#
|
+..........+
|
||||||
#..#....#..#
|
#..#....#..#
|
||||||
#..........#
|
#..........#
|
||||||
#####++#####
|
####+++#####
|
||||||
spawn 2 2 0.50 crate:2 barrel:1
|
spawn 2 2 0.50 crate:2 barrel:1
|
||||||
spawn 9 7 0.40 barrel:1 locker:1
|
spawn 9 7 0.40 barrel:1 locker:1
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
# Гнездо: кольцевые перегородки и ядро посередине. Линий огня почти нет,
|
# Гнездо: глухое ядро посередине и кольцо вокруг него. Линий огня почти нет,
|
||||||
# зато клинку есть где работать — сектор упирается в стену, а не в союзника.
|
# зато клинку есть где работать — сектор упирается в стену, а не в союзника.
|
||||||
|
#
|
||||||
|
# Кольцо теперь в три тайла со всех сторон, а перегородок внутри него нет вовсе.
|
||||||
|
# Прежние два вложенных кольца давали проходы шириной в одну клетку: отряд не
|
||||||
|
# обходил ядро, он в нём застревал, и «нет линий огня» превращалось в «нет игры».
|
||||||
name nest
|
name nest
|
||||||
size 12 12
|
size 12 12
|
||||||
weight 2
|
weight 2
|
||||||
@ -7,18 +11,18 @@ theme 2
|
|||||||
start 0
|
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 2 5 0.80 lurker:2 rusher:2
|
||||||
spawn 9 5 0.80 lurker:2 rusher:2
|
spawn 9 5 0.80 lurker:2 rusher:2
|
||||||
|
|||||||
@ -1,28 +1,30 @@
|
|||||||
# Яма: колодец посередине с единственным входом снизу. Громилу в нём
|
# Яма: колодец посередине с единственным входом снизу. Громилу в нём
|
||||||
# приходится разбирать вдвоём, а круг его удара достаёт обоих.
|
# приходится разбирать вдвоём, а круг его удара достаёт обоих.
|
||||||
|
#
|
||||||
|
# Колодец сузился до двух тайлов, зато обход вокруг него стал трёхрядным. Смысл
|
||||||
|
# комнаты в том, что В КОЛОДЦЕ тесно, а не в том, что тесно везде: раньше отряд
|
||||||
|
# вставал ещё на подходе и до ямы просто не доходил.
|
||||||
name pit
|
name pit
|
||||||
size 14 14
|
size 12 12
|
||||||
weight 2
|
weight 2
|
||||||
theme 2
|
theme 2
|
||||||
start 0
|
start 0
|
||||||
depth 6 0
|
depth 6 0
|
||||||
tiles
|
tiles
|
||||||
######++######
|
####+++#####
|
||||||
#............#
|
#..........#
|
||||||
#............#
|
#..........#
|
||||||
#...######...#
|
#..........#
|
||||||
#...#....#...#
|
#...####...#
|
||||||
+...#....#...+
|
+...#..#...+
|
||||||
+...#....#...+
|
+...#..#...+
|
||||||
+...#....#...+
|
+...#..#...+
|
||||||
+...#....#...+
|
#..........#
|
||||||
#...#....#...#
|
#..........#
|
||||||
#...##..##...#
|
#..........#
|
||||||
#............#
|
####+++#####
|
||||||
#............#
|
spawn 5 6 0.95 brute:3 shambler:2
|
||||||
######++######
|
|
||||||
spawn 6 6 0.95 brute:3 shambler:2
|
|
||||||
spawn 2 4 0.70 spitter:2 rusher:2
|
spawn 2 4 0.70 spitter:2 rusher:2
|
||||||
spawn 11 4 0.70 spitter:2 rusher:2
|
spawn 9 4 0.70 spitter:2 rusher:2
|
||||||
spawn 6 12 0.60 shambler:3 lurker:1
|
spawn 6 10 0.60 shambler:3 lurker:1
|
||||||
spawn 2 11 0.40 barrel:2 crate:1
|
spawn 2 9 0.40 barrel:2 crate:1
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
# Склад: обстановки много, тварей мало. Место, где игрок узнаёт, что хлам
|
# Склад: обстановки много, тварей мало. Место, где игрок узнаёт, что хлам
|
||||||
# ломается, — и тратит на это магазин, которого потом не хватит.
|
# ломается, — и тратит на это магазин, которого потом не хватит.
|
||||||
|
#
|
||||||
|
# Стеллаж стал одним глухим блоком посередине, а обход вокруг него — трёхрядным.
|
||||||
|
# Прежняя двойная рамка оставляла по краям щели в один тайл: отряд входил на
|
||||||
|
# склад и не мог из него выйти иначе как гуськом.
|
||||||
name storage
|
name storage
|
||||||
size 12 10
|
size 12 10
|
||||||
weight 2
|
weight 2
|
||||||
@ -7,17 +11,17 @@ theme 1
|
|||||||
start 0
|
start 0
|
||||||
depth 1 0
|
depth 1 0
|
||||||
tiles
|
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
|
spawn 1 1 0.45 shambler:2 lurker:1
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
# Схрон: тесная камера в камере. Хват здесь опаснее всего — вырванного из
|
# Схрон: тесная комната с глухим сейфом посередине. Хват здесь опаснее всего —
|
||||||
# строя бойца в такой тесноте нечем прикрыть.
|
# вырванного из строя бойца в такой тесноте нечем прикрыть.
|
||||||
|
#
|
||||||
|
# Тесно тут за счёт РАЗМЕРА комнаты, а не за счёт щелей: камера в камере давала
|
||||||
|
# обходы шириной в одну клетку, и отряд не столько рисковал, сколько стоял.
|
||||||
|
# Обход теперь трёхрядный, а схрон всё равно самая маленькая комната набора.
|
||||||
name vault
|
name vault
|
||||||
size 10 10
|
size 10 10
|
||||||
weight 1
|
weight 1
|
||||||
@ -7,17 +11,17 @@ theme 3
|
|||||||
start 0
|
start 0
|
||||||
depth 8 0
|
depth 8 0
|
||||||
tiles
|
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 1 1 0.55 rusher:2 shambler:1
|
||||||
spawn 8 8 0.55 spitter:2
|
spawn 8 8 0.55 spitter:2
|
||||||
spawn 4 8 0.40 locker:2 crate:1
|
spawn 4 8 0.40 locker:2 crate:1
|
||||||
|
|||||||
@ -1,19 +1,22 @@
|
|||||||
# Ряд камер. Ниши по бокам: в них удобно стоять и невозможно отступать.
|
# Ряд камер. Ниши по бокам: в них удобно стоять и невозможно отступать.
|
||||||
|
#
|
||||||
|
# Ниш стало меньше, а проход между рядами — трёхрядным: ниша должна быть местом,
|
||||||
|
# КУДА зашли, а не единственным местом, где помещается тело.
|
||||||
name cells
|
name cells
|
||||||
size 13 9
|
size 12 9
|
||||||
weight 3
|
weight 3
|
||||||
theme 0
|
theme 0
|
||||||
depth 1 0
|
depth 1 0
|
||||||
tiles
|
tiles
|
||||||
####+++++####
|
####+++#####
|
||||||
#...........#
|
#..........#
|
||||||
#.#.#.#.#.#.#
|
#.#.#..#.#.#
|
||||||
+...........+
|
+..........+
|
||||||
+...........+
|
+..........+
|
||||||
+...........+
|
+..........+
|
||||||
#.#.#.#.#.#.#
|
#.#.#..#.#.#
|
||||||
#...........#
|
#..........#
|
||||||
####+++++####
|
####+++#####
|
||||||
spawn 2 1 0.65 rusher:3 shambler:2
|
spawn 2 1 0.65 rusher:3 shambler:2
|
||||||
spawn 10 7 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
|
spawn 6 4 0.40 bandage:2 knife:1 rifle:1
|
||||||
|
|||||||
@ -4,24 +4,27 @@
|
|||||||
# У ядра ДВА прохода, север и юг. Глухая коробка посередине выглядит на сетке
|
# У ядра ДВА прохода, север и юг. Глухая коробка посередине выглядит на сетке
|
||||||
# так же, но точка спавна внутри неё недостижима: тварь не дойдёт до отряда и
|
# так же, но точка спавна внутри неё недостижима: тварь не дойдёт до отряда и
|
||||||
# не будет убита, и этаж тихо станет легче. Ровно это и ловит --check.
|
# не будет убита, и этаж тихо станет легче. Ровно это и ловит --check.
|
||||||
|
#
|
||||||
|
# Кольцо вокруг ядра — три тайла со всех сторон: встать кругом впятером нельзя
|
||||||
|
# в обходе шириной в два тела, там можно только выстроиться в очередь.
|
||||||
name maw
|
name maw
|
||||||
size 13 11
|
size 12 11
|
||||||
weight 2
|
weight 2
|
||||||
theme 2
|
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 1 1 0.60 spitter:2 rusher:2
|
||||||
spawn 11 9 0.60 spitter:2 rusher:2
|
spawn 10 9 0.60 spitter:2 rusher:2
|
||||||
spawn 11 1 0.45 shotgun:1 plate:1 cleaver:1
|
spawn 10 1 0.45 shotgun:1 plate:1 cleaver:1
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
# Стартовая HOLLOW. Тесная и короткая: вид сверху показывает всю комнату
|
# Стартовая HOLLOW. Тесная и короткая: вид сверху показывает всю комнату
|
||||||
# сразу, и просторный зал первого этажа не оставил бы ничего на потом.
|
# сразу, и просторный зал первого этажа не оставил бы ничего на потом.
|
||||||
|
#
|
||||||
|
# Тесная — по размеру, а не по проходам. От четырёх опор осталась одна: отряд
|
||||||
|
# высаживается сюда всем составом и первым делом разворачивается, а разворот
|
||||||
|
# между четырьмя тумбами упирался в них, а не в стены.
|
||||||
name mouth
|
name mouth
|
||||||
size 11 9
|
size 11 9
|
||||||
weight 2
|
weight 2
|
||||||
@ -9,11 +13,11 @@ depth 1 0
|
|||||||
tiles
|
tiles
|
||||||
####+++####
|
####+++####
|
||||||
#.........#
|
#.........#
|
||||||
#..##.##..#
|
#.........#
|
||||||
+.........+
|
+.........+
|
||||||
+....#....+
|
+....#....+
|
||||||
+.........+
|
+.........+
|
||||||
#..##.##..#
|
#.........#
|
||||||
#.........#
|
#.........#
|
||||||
####+++####
|
####+++####
|
||||||
spawn 2 1 0.45 barrel:1
|
spawn 2 1 0.45 barrel:1
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
# Колодец. Узкий и длинный: строй здесь не развернуть, и это единственное,
|
# Колодец. Узкий и длинный: строй здесь не развернуть, и это единственное,
|
||||||
# что комната делает.
|
# что комната делает.
|
||||||
|
#
|
||||||
|
# «Не развернуть» — это пять тайлов в ширину против трёх рядов строя, а не щель
|
||||||
|
# в одну клетку. Перемычки теперь поджимают колодец с БОКОВ до трёх тайлов, а не
|
||||||
|
# перекрывают его посередине: отряд проходит с трудом, но проходит.
|
||||||
name shaft
|
name shaft
|
||||||
size 7 13
|
size 7 12
|
||||||
weight 2
|
weight 2
|
||||||
theme 1
|
theme 1
|
||||||
depth 2 0
|
depth 2 0
|
||||||
@ -9,16 +13,15 @@ tiles
|
|||||||
##+++##
|
##+++##
|
||||||
#.....#
|
#.....#
|
||||||
#.....#
|
#.....#
|
||||||
#.###.#
|
##...##
|
||||||
+.....+
|
+.....+
|
||||||
+.....+
|
+.....+
|
||||||
+.....+
|
+.....+
|
||||||
+.....+
|
##...##
|
||||||
#.###.#
|
|
||||||
#.....#
|
#.....#
|
||||||
#.....#
|
#.....#
|
||||||
#.....#
|
#.....#
|
||||||
##+++##
|
##+++##
|
||||||
spawn 3 1 0.70 lurker:2 rusher:2
|
spawn 3 1 0.70 lurker:2 rusher:2
|
||||||
spawn 3 11 0.70 lurker:2 spitter:1
|
spawn 3 10 0.70 lurker:2 spitter:1
|
||||||
spawn 1 6 0.35 vest:1 lantern:1
|
spawn 1 5 0.35 vest:1 lantern:1
|
||||||
|
|||||||
14
save/profile.txt
Normal file
14
save/profile.txt
Normal 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
|
||||||
@ -2823,6 +2823,30 @@ 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)
|
||||||
{
|
{
|
||||||
|
|||||||
25
src/game.cpp
25
src/game.cpp
@ -933,15 +933,26 @@ void Game::UpdateExit()
|
|||||||
|
|
||||||
const float r = g_tune.descendRadius;
|
const float r = g_tune.descendRadius;
|
||||||
const bool nearExit = Distance(squad.anchor, level.exitPos) <= r;
|
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;
|
atExit = nearExit;
|
||||||
atExtract = nearOut;
|
atExtract = true;
|
||||||
|
|
||||||
|
// Событие пишется на ФРОНТЕ входа в кольцо: журнал не должен забиваться
|
||||||
|
// одним и тем же сообщением, пока отряд стоит на месте.
|
||||||
|
if (nearExit && !atExit) events.Push(CombatEventKind::DESCEND, -1, level.exitPos, Vec2{});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::UpdateAim(float dt)
|
void Game::UpdateAim(float dt)
|
||||||
|
|||||||
@ -153,13 +153,15 @@ void DrawRunBar(Framebuffer& fb, const Game& game)
|
|||||||
//
|
//
|
||||||
// Разные цвета — те же, что у колец на полу: подсказка обязана читаться
|
// Разные цвета — те же, что у колец на полу: подсказка обязана читаться
|
||||||
// как подпись к тому, на чём стоишь, а не как отдельное сообщение.
|
// как подпись к тому, на чём стоишь, а не как отдельное сообщение.
|
||||||
const char* hint = nullptr;
|
// Уйти можно откуда угодно, поэтому «EXTRACT: E» висит всегда — но тускло и
|
||||||
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 (game.atExit)
|
||||||
if (!hint) return;
|
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
|
} // namespace
|
||||||
|
|||||||
@ -172,6 +172,30 @@ void Squad::UpdateAnchorAndState(Vec2 moveWorldDir, bool sprint, const Tilemap&
|
|||||||
{
|
{
|
||||||
anchorPrev = anchor;
|
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 bool moving = LengthSq(moveWorldDir) > 1e-6f;
|
||||||
const AgentState prevState = state;
|
const AgentState prevState = state;
|
||||||
const Vec2 c = Centroid();
|
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);
|
anchorVel += ClampLength(dv, std::max(0.1f, g_tune.anchorAccel) * dt);
|
||||||
anchor += anchorVel * 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;
|
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)
|
const float wantSpacing = (state == AgentState::HOLDING)
|
||||||
? g_tune.holdSpacing
|
? g_tune.holdSpacing
|
||||||
|
|||||||
@ -141,6 +141,12 @@ struct EnemyDef
|
|||||||
EnemyDef EnemyDefOf(EnemyKind kind);
|
EnemyDef EnemyDefOf(EnemyKind kind);
|
||||||
const char* EnemyName(EnemyKind kind);
|
const char* EnemyName(EnemyKind kind);
|
||||||
|
|
||||||
|
// Сколько «живёт» труп твари. Практически вечность: убитая в вылазке тварь
|
||||||
|
// не возвращается. Не бесконечность только потому, что плакальщик умеет
|
||||||
|
// ускорять этот таймер своей аурой, а деление вечности ни на что не даёт
|
||||||
|
// ничего.
|
||||||
|
constexpr float ENEMY_STAYS_DEAD = 100000.0f;
|
||||||
|
|
||||||
struct Target
|
struct Target
|
||||||
{
|
{
|
||||||
ecs::Entity self = ecs::NO_ENTITY;
|
ecs::Entity self = ecs::NO_ENTITY;
|
||||||
@ -228,14 +234,23 @@ struct Target
|
|||||||
// напряжения, глаза в темноте, знаменатель покрытия в решателе.
|
// напряжения, глаза в темноте, знаменатель покрытия в решателе.
|
||||||
bool IsProp() const { return kind >= FIRST_PROP_KIND; }
|
bool IsProp() const { return kind >= FIRST_PROP_KIND; }
|
||||||
|
|
||||||
// Мишень приёмки встаёт быстро — на ней меряют. Тварь не возвращается,
|
// Мишень приёмки встаёт быстро — на ней меряют, и без возврата мерить было
|
||||||
// пока отряд не успеет уйти: иначе зачистка не имеет смысла.
|
// бы нечего: сценарии длятся дольше, чем живут мишени.
|
||||||
// Разбитый ящик не встаёт НИКОГДА: разрушаемая обстановка, которая
|
//
|
||||||
// отрастает обратно, — это не обстановка, а мишень.
|
// ТВАРЬ НЕ ВСТАЁТ ВООБЩЕ. Убитая в этой вылазке — убита насовсем: зачистка
|
||||||
|
// обязана что-то значить. С возвратом любая расчищенная комната через
|
||||||
|
// полминуты снова была полна, и «пробиться и уйти» переставало отличаться
|
||||||
|
// от «стоять и молотить бесконечную очередь».
|
||||||
|
//
|
||||||
|
// Единственное исключение — аура плакальщика (sim/target.cpp): он поднимает
|
||||||
|
// ПАВШИХ рядом с собой, и это его роль, а не общее правило. Поэтому не
|
||||||
|
// «мгновенно навсегда», а очень долгий срок, который аура умеет ускорить.
|
||||||
|
// Разбитый ящик не встаёт никогда: обстановка, которая отрастает, — это
|
||||||
|
// не обстановка, а мишень.
|
||||||
float RespawnDelay() const
|
float RespawnDelay() const
|
||||||
{
|
{
|
||||||
if (IsProp()) return -1.0f;
|
if (IsProp()) return -1.0f;
|
||||||
return IsDummy() ? g_tune.targetRespawn : g_tune.enemyRespawn;
|
return IsDummy() ? g_tune.targetRespawn : ENEMY_STAYS_DEAD;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Замах виден: единственный признак, по которому игрок успевает среагировать.
|
// Замах виден: единственный признак, по которому игрок успевает среагировать.
|
||||||
|
|||||||
27
src/tuning.h
27
src/tuning.h
@ -71,12 +71,19 @@ constexpr int SOLVER_CANDIDATES = SOLVER_RINGS * SOLVER_DIRS + 1;
|
|||||||
struct Tuning
|
struct Tuning
|
||||||
{
|
{
|
||||||
// движение агента
|
// движение агента
|
||||||
float moveSpeed = 1.75f; // юнитов/сек: хоррор, а не шутер-беготня
|
// Скорость отряда. Была 1.75 при курсоре 3.3: отряд ВСЕГДА отставал, и
|
||||||
float moveSpeedFast = 3.6f; // догоняют курсор быстро — иначе отряд отстаёт
|
// управление ощущалось как «тяну за верёвку», а не «веду людей». Поднято до
|
||||||
float steerAccel = 18.0f; // тяжелее; НИЖЕ 16 нельзя — ломается критерий 11.4
|
// 2.6 — это по-прежнему медленнее курсора (иначе поводок теряет смысл), но
|
||||||
|
// разрыв закрывается за доли секунды, а не за метры.
|
||||||
|
float moveSpeed = 2.60f; // юнитов/сек: хоррор, а не шутер-беготня
|
||||||
|
float moveSpeedFast = 4.4f; // догоняют курсор быстро — иначе отряд отстаёт
|
||||||
|
float steerAccel = 26.0f; // тяжелее; НИЖЕ 16 нельзя — ломается критерий 11.4
|
||||||
float bodyRadius = 0.28f;
|
float bodyRadius = 0.28f;
|
||||||
float separationRadius = 0.7f;
|
float separationRadius = 0.7f;
|
||||||
float wSeek = 1.0f;
|
float wSeek = 1.0f;
|
||||||
|
// Как быстро строй доворачивается к направлению хода. Мгновенный доворот
|
||||||
|
// заставлял бойцов бегать вбок на каждом повороте (sim/squad.cpp).
|
||||||
|
float formTurnRate = 7.0f;
|
||||||
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;
|
||||||
@ -237,8 +244,8 @@ struct Tuning
|
|||||||
// anchorLeash — мягкая граница: до неё курсор идёт свободно;
|
// anchorLeash — мягкая граница: до неё курсор идёт свободно;
|
||||||
// anchorLeashMax — жёсткий предел: страховка, до неё обычно не доходит;
|
// anchorLeashMax — жёсткий предел: страховка, до неё обычно не доходит;
|
||||||
// leashDrag — насколько тяжелеет курсор между границами (0..1).
|
// leashDrag — насколько тяжелеет курсор между границами (0..1).
|
||||||
float anchorLeash = 3.0f;
|
float anchorLeash = 2.5f;
|
||||||
float anchorLeashMax = 5.0f;
|
float anchorLeashMax = 4.2f;
|
||||||
float leashDrag = 0.75f; // на жёсткой границе остаётся 25% скорости
|
float leashDrag = 0.75f; // на жёсткой границе остаётся 25% скорости
|
||||||
|
|
||||||
// --- натяжение поводка (см. docs/05-squad-ai.md) --------------------------
|
// --- натяжение поводка (см. docs/05-squad-ai.md) --------------------------
|
||||||
@ -246,9 +253,14 @@ struct Tuning
|
|||||||
// быстрее, чем отряд шагает. Это и есть команда «бегом»: отряд ускоряется,
|
// быстрее, чем отряд шагает. Это и есть команда «бегом»: отряд ускоряется,
|
||||||
// строй растягивается, а на верхней трети шкалы огонь прекращается вовсе.
|
// строй растягивается, а на верхней трети шкалы огонь прекращается вовсе.
|
||||||
float urgeStartFrac = 0.50f; // с какой доли поводка начинается натяжение
|
float urgeStartFrac = 0.50f; // с какой доли поводка начинается натяжение
|
||||||
float urgeAttack = 2.6f; // как быстро натяжение набирается
|
// Натяжение набирается быстро, но НЕ мгновенно, и порог спринта поднят.
|
||||||
|
// При urgeAttack 5.2 отряд уходил в спринт от любого движения курсора, а
|
||||||
|
// спринт растягивает строй и выключает огонь: в коридорах это выглядело как
|
||||||
|
// «отряд застрял». Поймано автопилотом (--play): первый этаж переставал
|
||||||
|
// проходиться вообще.
|
||||||
|
float urgeAttack = 3.4f; // как быстро натяжение набирается
|
||||||
float urgeRelease = 3.4f; // и как быстро отпускает
|
float urgeRelease = 3.4f; // и как быстро отпускает
|
||||||
float sprintAt = 0.85f; // выше — спринт: бежим, не атакуем.
|
float sprintAt = 0.95f; // выше — спринт: бежим, не атакуем.
|
||||||
// Порог высокий намеренно: при удержании направления натяжение садится
|
// Порог высокий намеренно: при удержании направления натяжение садится
|
||||||
// примерно на 0.54, то есть отряд идёт быстрым шагом И ПРОДОЛЖАЕТ работать
|
// примерно на 0.54, то есть отряд идёт быстрым шагом И ПРОДОЛЖАЕТ работать
|
||||||
// по целям. Спринт остаётся тем, чем должен быть, — отдельным решением
|
// по целям. Спринт остаётся тем, чем должен быть, — отдельным решением
|
||||||
@ -442,6 +454,7 @@ inline const TunableParam TUNABLES[] = {
|
|||||||
{"predictSlack", &Tuning::shotPredictSlack, 0.05f, 0.0f, 2.0f},
|
{"predictSlack", &Tuning::shotPredictSlack, 0.05f, 0.0f, 2.0f},
|
||||||
{"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},
|
||||||
{"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},
|
||||||
|
|||||||
125
src/ui/menu.cpp
125
src/ui/menu.cpp
@ -165,6 +165,28 @@ constexpr int WS_SLOT_CELLS = WS_SLOT_COLS * WS_SLOT_ROWS;
|
|||||||
// внизу экрана. Кадр 480x270, и место здесь считается по-настоящему.
|
// внизу экрана. Кадр 480x270, и место здесь считается по-настоящему.
|
||||||
constexpr int WS_OP_X = 252, WS_OP_Y = 84, WS_OP_W = 212, WS_OP_H = 20, WS_OP_STEP = 22;
|
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».
|
// Выбор чекпоинта: столбец кнопок «начать с этажа N».
|
||||||
constexpr int DEPTH_Y0 = 92, DEPTH_STEP = 18, DEPTH_SIZE = 10;
|
constexpr int DEPTH_Y0 = 92, DEPTH_STEP = 18, DEPTH_SIZE = 10;
|
||||||
constexpr int DEPTH_MAX_SHOWN = 7; // больше на экран не влезает
|
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)
|
switch (ui.screen)
|
||||||
{
|
{
|
||||||
case Screen::GAME:
|
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;
|
return;
|
||||||
|
|
||||||
case Screen::MENU:
|
case Screen::MENU:
|
||||||
@ -595,8 +625,13 @@ bool IsSlot(UiPart p)
|
|||||||
p == UiPart::BAG || p == UiPart::STASH;
|
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)
|
switch (part)
|
||||||
{
|
{
|
||||||
// Пустых рук не бывает: отряд не носит воздух. ItemId::NONE — не оружие,
|
// Пустых рук не бывает: отряд не носит воздух. 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);
|
SpawnFloorItem(game.world, *slot, a.pos, 2.0f);
|
||||||
*slot = ItemInstance{};
|
*slot = ItemInstance{};
|
||||||
|
SaveLoadout(game, ui.agentIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApplyDrop(Ui& ui, Game& game, UiPart to, int toIndex)
|
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)
|
if (ui.dragFrom == UiPart::HANDS || to == UiPart::HANDS)
|
||||||
EquipWeapon(game.world, a, inv->weapon);
|
EquipWeapon(game.world, a, inv->weapon);
|
||||||
|
SaveLoadout(game, ui.agentIndex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -743,6 +802,7 @@ void ApplyDrop(Ui& ui, Game& game, UiPart to, int toIndex)
|
|||||||
// а статы, класс и правила стрельбы остались от прежнего ствола.
|
// а статы, класс и правила стрельбы остались от прежнего ствола.
|
||||||
if (ui.dragFrom == UiPart::HANDS || to == UiPart::HANDS)
|
if (ui.dragFrom == UiPart::HANDS || to == UiPart::HANDS)
|
||||||
EquipWeapon(game.world, a, inv->weapon);
|
EquipWeapon(game.world, a, inv->weapon);
|
||||||
|
SaveLoadout(game, ui.agentIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@ -804,6 +864,16 @@ void UiActivate(Ui& ui, Game& game, UiPart part, int index)
|
|||||||
ui.screen = Screen::GAME;
|
ui.screen = Screen::GAME;
|
||||||
return;
|
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:
|
case UiPart::CRAFT_SLOT:
|
||||||
// Клик по ячейке схрона: обычный — положить вещь на верстак,
|
// Клик по ячейке схрона: обычный — положить вещь на верстак,
|
||||||
// повторный по той же — назначить её ДОНОРОМ для отпечатка.
|
// повторный по той же — назначить её ДОНОРОМ для отпечатка.
|
||||||
@ -909,6 +979,11 @@ int Step()
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
void UiApplyDropForTest(Ui& ui, Game& game, UiPart to, int toIndex)
|
||||||
|
{
|
||||||
|
ApplyDrop(ui, game, to, toIndex);
|
||||||
|
}
|
||||||
|
|
||||||
int UiMenuItemCount() { return MENU_COUNT; }
|
int UiMenuItemCount() { return MENU_COUNT; }
|
||||||
|
|
||||||
const char* UiMenuItemName(int index)
|
const char* UiMenuItemName(int index)
|
||||||
@ -961,7 +1036,15 @@ bool Ui::Update(Game& game, const ScreenRect& fit, float dt)
|
|||||||
switch (screen)
|
switch (screen)
|
||||||
{
|
{
|
||||||
case Screen::GAME:
|
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_ESCAPE)) { screen = Screen::PAUSE; return true; }
|
||||||
|
if (Pressed(KEY_U)) { screen = Screen::UPGRADE; return true; }
|
||||||
// Только I: TAB в бою переключает цель (core/input.h), и отбирать
|
// Только I: TAB в бою переключает цель (core/input.h), и отбирать
|
||||||
// его под инвентарь нельзя.
|
// его под инвентарь нельзя.
|
||||||
if (Pressed(KEY_I)) { screen = Screen::SQUAD; return true; }
|
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 &&
|
else if (!dragging && IsMouseButtonPressed(MOUSE_BUTTON_RIGHT) && hot &&
|
||||||
IsSlot(hot->part) && hot->item != ItemId::NONE)
|
IsSlot(hot->part) && hot->item != ItemId::NONE)
|
||||||
DropToFloor(*this, game, hot->part, hot->index);
|
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 (dragging && lmbUp)
|
||||||
{
|
{
|
||||||
if (dropOk && hot) ApplyDrop(*this, game, hot->part, hot->index);
|
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);
|
std::swap(cell, inv->weapon);
|
||||||
EquipWeapon(game.world, a, inv->weapon);
|
EquipWeapon(game.world, a, inv->weapon);
|
||||||
|
SaveLoadout(game, agentIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -1332,7 +1432,8 @@ void DrawPointer(const Layout& l, const Ui& ui)
|
|||||||
|
|
||||||
void Ui::Draw(const Game& game, const ScreenRect& fit) const
|
void Ui::Draw(const Game& game, const ScreenRect& fit) const
|
||||||
{
|
{
|
||||||
if (screen == Screen::GAME) return;
|
// В бою меню раньше не рисовалось вовсе. Теперь рисуется — но только нижняя
|
||||||
|
// панель кнопок: она часть боевого экрана, а не «меню поверх игры».
|
||||||
|
|
||||||
const Layout l = Of(fit);
|
const Layout l = Of(fit);
|
||||||
const bool pulse = (int(blink * 2.0f) & 1) == 0;
|
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)
|
switch (screen)
|
||||||
{
|
{
|
||||||
case Screen::GAME:
|
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:
|
case Screen::MENU:
|
||||||
Shade(l, COL_DIM);
|
Shade(l, COL_DIM);
|
||||||
|
|||||||
@ -54,6 +54,7 @@ enum class UiPart : uint8_t
|
|||||||
CRAFT_ITEM, // строка операции мастерской
|
CRAFT_ITEM, // строка операции мастерской
|
||||||
CRAFT_SLOT, // ячейка схрона в мастерской: что дорабатываем
|
CRAFT_SLOT, // ячейка схрона в мастерской: что дорабатываем
|
||||||
CRAFT_AFFIX, // строка аффикса на верстаке: цель направленной операции
|
CRAFT_AFFIX, // строка аффикса на верстаке: цель направленной операции
|
||||||
|
HUD_BUTTON, // кнопка нижней панели в бою: открывает экран
|
||||||
};
|
};
|
||||||
|
|
||||||
// Прямоугольник в координатах КАДРА (480x270), а не окна: кадр лежит в окне
|
// Прямоугольник в координатах КАДРА (480x270), а не окна: кадр лежит в окне
|
||||||
@ -142,5 +143,10 @@ void UiActivate(Ui& ui, Game& game, UiPart part, int index);
|
|||||||
|
|
||||||
// Сколько пунктов в главном меню и как называется i-й. Нужны той же проверке:
|
// Сколько пунктов в главном меню и как называется i-й. Нужны той же проверке:
|
||||||
// она обязана пройти по ВСЕМ пунктам, а не по тем, о которых знает.
|
// она обязана пройти по ВСЕМ пунктам, а не по тем, о которых знает.
|
||||||
|
// Применить перетаскивание как будто мышью. Наружу — ради проверки «сборы
|
||||||
|
// переживают старт вылазки» (--ui-check): собрать её через настоящую мышь
|
||||||
|
// нельзя, окна в прогоне нет.
|
||||||
|
void UiApplyDropForTest(Ui& ui, Game& game, UiPart to, int toIndex);
|
||||||
|
|
||||||
int UiMenuItemCount();
|
int UiMenuItemCount();
|
||||||
const char* UiMenuItemName(int index);
|
const char* UiMenuItemName(int index);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user