Запуск грузит проект; автопилот --play; строй сжимается в тесноте (M31)
ГЛАВНОЕ. "Дольше пяти секунд не продержаться на первом уровне" - это был не баланс, а запуск. По умолчанию useRooms был false: обычный запуск exe собирал ЗАХАРДКОЖЕННУЮ карту прототипа со старой расстановкой тварей и высаживал туда ОДНОГО оперативника с ножом (размер отряда берётся из профиля). Игрок попадал не на первый этаж игры, а вообще не в игру. Теперь проект подключается по умолчанию, а старая карта осталась под флагом --no-rooms. --PLAY. Новый прогон: автопилот ведёт отряд к спуску и дерётся по дороге. Ни --descend, ни --wall не меряли ПРОХОЖДЕНИЕ: первый собирает этажи и ни с кем не воюет, второй ставит отряд в толпу и запрещает двигаться. Жалоба же была именно про проход этажа. Автопилот ходит по полю расстояний до спуска (заливка по проходимым клеткам), целится на несколько клеток вперёд по спуску поля и правит курсором - тем же вводом, каким играет человек. Три версии до рабочей: по прямой упирался в первую стену, по восьми соседям лез в диагональную щель между углами (тело круглое, не пролезает), по соседней клетке дёргался на каждой границе. Одиночка проходит три этажа и гибнет на четвёртом: 1 тварей 3, убил 0, 100% здоровья, света 244 дошёл (78 с) 2 тварей 6, убил 1, 100%, света 155 дошёл (44 с) 3 тварей 6, убил 0, 65%, света 8 дошёл (74 с) 4 тварей 9, убил 5, ВАЙП (37 с) Свет кончается к третьему этажу: расчёт "заряда на четыре пути до спуска" считал прямые пути, а живой отряд ходит петлями. СТРОЙ В ТЕСНОТЕ. Клин шириной в пять тел не проходит в дверь шириной в клетку: бойцы упираются друг в друга, расталкивание держит их на месте, отряд встаёт насмерть. Теперь строй сам сжимается в колонну, когда вокруг тел мало свободных клеток. ЧЕСТНО: это НЕ вылечило проход пятёрки до конца. Автопилот с полным отрядом всё ещё застревает на первом этаже там, где одиночка проходит, и причина не в ширине строя (проверено сжатием в точку и отрядом без клинков). Похоже, нужна нормальная проводка строя по проходам - это отдельная веха, а не патч в конце. Оставляю с числами, а не с обещанием. --accept PASS, все семь проверок зелёные. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
e3294cffd4
commit
9bcb5ca013
@ -32,6 +32,17 @@ inline Vec2 ScreenDirToWorld(Vec2 screenDir)
|
|||||||
return Normalized(ScreenToWorld(screenDir));
|
return Normalized(ScreenToWorld(screenDir));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обратное: направление в МИРЕ -> направление в экранных осях. Нужно тому, кто
|
||||||
|
// водит отряд не рукой, а кодом (автопилот балансового прогона): ввод игры
|
||||||
|
// экранный, и подсовывать в него мировой вектор значило бы проверять не тот
|
||||||
|
// путь, которым ходит живой игрок.
|
||||||
|
inline Vec2 WorldDirToScreen(Vec2 worldDir)
|
||||||
|
{
|
||||||
|
const Vec2 a = g_proj.WorldToScreen(Vec2{0.0f, 0.0f});
|
||||||
|
const Vec2 b = g_proj.WorldToScreen(worldDir);
|
||||||
|
return Normalized(Vec2{b.x - a.x, b.y - a.y});
|
||||||
|
}
|
||||||
|
|
||||||
// Камера следует за якорем со сглаживанием; смещение округляется до пикселя.
|
// Камера следует за якорем со сглаживанием; смещение округляется до пикселя.
|
||||||
struct Camera
|
struct Camera
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2446,3 +2446,217 @@ int RunWall(const RoomLibrary& rooms, const EntityCatalog& catalog, uint32_t see
|
|||||||
}
|
}
|
||||||
return bad == 0 ? 0 : 1;
|
return bad == 0 ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// --play N: автопилот. Отряд ИДЁТ К СПУСКУ и дерётся по дороге.
|
||||||
|
//
|
||||||
|
// Зачем ещё один прогон, когда есть `--descend` и `--wall`: они меряют разные
|
||||||
|
// куски, и ни один не меряет ИГРУ. `--descend` собирает этажи и ни с кем не
|
||||||
|
// воюет, `--wall` ставит отряд в толпу и запрещает ему двигаться. А жалоба
|
||||||
|
// игрока звучит иначе: «дольше пяти секунд не продержаться» — это про то, как
|
||||||
|
// проходится этаж целиком.
|
||||||
|
//
|
||||||
|
// Автопилот тупой намеренно: он ведёт курсор к спуску и всё. Он не отступает,
|
||||||
|
// не выбирает цели, не лечится вручную (лечение и так автоматическое). Это
|
||||||
|
// нижняя граница проходимости: игрок сыграет лучше. Если этаж не проходит даже
|
||||||
|
// автопилот — этаж непроходим.
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
int RunPlay(const RoomLibrary& rooms, const EntityCatalog& catalog, uint32_t seed, int floors,
|
||||||
|
int squadSize)
|
||||||
|
{
|
||||||
|
Profile profile;
|
||||||
|
profile.ResetLoadouts();
|
||||||
|
profile.squadSize = std::max(1, std::min(NUM_AGENTS, squadSize));
|
||||||
|
profile.maxDepth = 100000; // чекпоинты не должны резать стартовую глубину
|
||||||
|
|
||||||
|
Game game;
|
||||||
|
game.combatMode = true;
|
||||||
|
game.rooms = &rooms;
|
||||||
|
game.catalog = &catalog;
|
||||||
|
game.profile = &profile;
|
||||||
|
game.levelSeed = seed;
|
||||||
|
game.Init();
|
||||||
|
game.NewRun(1);
|
||||||
|
|
||||||
|
std::printf("автопилот: отряд из %d, ведём к спуску, без отступлений\n\n",
|
||||||
|
profile.squadSize);
|
||||||
|
std::printf("%-6s %7s %7s %8s %8s %9s %8s %s\n", "DEPTH", "ТВАРЕЙ", "УБИЛ", "ЖИВЫХ",
|
||||||
|
"ЗДОРОВЬЕ", "ПАТРОНЫ", "СВЕТ", "ИТОГ");
|
||||||
|
|
||||||
|
int deepest = 0;
|
||||||
|
int wipedAt = 0;
|
||||||
|
|
||||||
|
for (int f = 0; f < floors && game.AliveAgents() > 0; ++f)
|
||||||
|
{
|
||||||
|
const int startMonsters = [&]() {
|
||||||
|
int n = 0;
|
||||||
|
for (const Target& t : game.targets)
|
||||||
|
if (t.alive && !t.IsProp() && !t.IsDummy()) ++n;
|
||||||
|
return n;
|
||||||
|
}();
|
||||||
|
|
||||||
|
// Поле расстояний до спуска. Считается один раз на этаж обычной
|
||||||
|
// заливкой по проходимым клеткам — тем же способом, каким генератор
|
||||||
|
// ищет саму точку спуска (engine/level_gen.cpp).
|
||||||
|
std::vector<int> flow(size_t(MAP_W) * MAP_H, -1);
|
||||||
|
{
|
||||||
|
std::vector<int> queue;
|
||||||
|
queue.reserve(size_t(MAP_W) * MAP_H);
|
||||||
|
const int ex = int(game.level.exitPos.x), ey = int(game.level.exitPos.y);
|
||||||
|
if (ex >= 0 && ey >= 0 && ex < MAP_W && ey < MAP_H)
|
||||||
|
{
|
||||||
|
flow[size_t(ey * MAP_W + ex)] = 0;
|
||||||
|
queue.push_back(ey * MAP_W + ex);
|
||||||
|
}
|
||||||
|
for (size_t head = 0; head < queue.size(); ++head)
|
||||||
|
{
|
||||||
|
const int cur = queue[head];
|
||||||
|
const int x = cur % MAP_W, y = cur / MAP_W;
|
||||||
|
|
||||||
|
// Только четыре соседа, БЕЗ диагоналей. По диагонали между
|
||||||
|
// двумя углами стен путь есть на сетке, но нет в мире: тело
|
||||||
|
// круглое и в щель не пролезает. Первая версия автопилота
|
||||||
|
// ходила по восьми и намертво вставала в первом же таком
|
||||||
|
// проходе — четыре клетки от старта.
|
||||||
|
const int step4[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||||
|
for (const auto& d4 : step4)
|
||||||
|
{
|
||||||
|
const int dx = d4[0], dy = d4[1];
|
||||||
|
const int nx = x + dx, ny = y + dy;
|
||||||
|
if (nx < 0 || ny < 0 || nx >= MAP_W || ny >= MAP_H) continue;
|
||||||
|
if (game.map.IsWall(nx, ny)) continue;
|
||||||
|
int& cell = flow[size_t(ny * MAP_W + nx)];
|
||||||
|
if (cell >= 0) continue;
|
||||||
|
cell = flow[size_t(cur)] + 1;
|
||||||
|
queue.push_back(ny * MAP_W + nx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Три минуты на этаж — с запасом: путь до спуска редко длиннее полутора.
|
||||||
|
constexpr int MAX_STEPS = 60 * 180;
|
||||||
|
bool reached = false;
|
||||||
|
int steps = 0;
|
||||||
|
int lastDist = -1; // на сколько клеток отряд был близок к спуску
|
||||||
|
int stuckFor = 0; // сколько шагов это число не меняется
|
||||||
|
|
||||||
|
for (; steps < MAX_STEPS && game.AliveAgents() > 0; ++steps)
|
||||||
|
{
|
||||||
|
// Куда идти — берём из ПОЛЯ РАССТОЯНИЙ до спуска, а не по прямой.
|
||||||
|
// Прямая упирается в первую же стену: у отряда нет поиска пути, он
|
||||||
|
// умеет только «иди туда». Первая версия автопилота именно так и
|
||||||
|
// застревала, показывая «этаж непроходим» там, где непроходимым был
|
||||||
|
// сам автопилот.
|
||||||
|
// Ведём КУРСОР, а не центр отряда: игрок водит именно его, а центр
|
||||||
|
// пятерых вообще может оказаться внутри стены, пока сами бойцы
|
||||||
|
// обходят её с двух сторон. На этом автопилот с пятёркой вставал на
|
||||||
|
// первом же этаже, хотя одиночка тот же этаж проходил.
|
||||||
|
const Vec2 c = game.squad.anchor;
|
||||||
|
const int cx = int(c.x), cy = int(c.y);
|
||||||
|
const int here = (cx >= 0 && cy >= 0 && cx < MAP_W && cy < MAP_H)
|
||||||
|
? flow[size_t(cy * MAP_W + cx)]
|
||||||
|
: -1;
|
||||||
|
|
||||||
|
// Цель — ЦЕНТР соседней клетки, а не «ось». По оси курсор дёргается
|
||||||
|
// между двумя клетками на границе и стоит на месте; по центру он
|
||||||
|
// идёт ровно.
|
||||||
|
// Цель — точка на НЕСКОЛЬКО клеток вперёд по спуску поля, а не
|
||||||
|
// соседняя клетка. По соседней курсор дёргается между двумя
|
||||||
|
// вариантами на каждой границе и стоит на месте; дальняя точка
|
||||||
|
// тянет ровно, как рука игрока.
|
||||||
|
int bestDist = here;
|
||||||
|
Vec2 goal = c;
|
||||||
|
{
|
||||||
|
int wx = cx, wy = cy;
|
||||||
|
for (int hop = 0; hop < 6; ++hop)
|
||||||
|
{
|
||||||
|
int nextX = wx, nextY = wy;
|
||||||
|
int nextD = flow[size_t(wy * MAP_W + wx)];
|
||||||
|
const int look4[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||||
|
for (const auto& l4 : look4)
|
||||||
|
{
|
||||||
|
const int nx = wx + l4[0], ny = wy + l4[1];
|
||||||
|
if (nx < 0 || ny < 0 || nx >= MAP_W || ny >= MAP_H) continue;
|
||||||
|
const int d = flow[size_t(ny * MAP_W + nx)];
|
||||||
|
if (d < 0 || (nextD >= 0 && d >= nextD)) continue;
|
||||||
|
nextD = d;
|
||||||
|
nextX = nx;
|
||||||
|
nextY = ny;
|
||||||
|
}
|
||||||
|
if (nextX == wx && nextY == wy) break;
|
||||||
|
wx = nextX;
|
||||||
|
wy = nextY;
|
||||||
|
bestDist = nextD;
|
||||||
|
}
|
||||||
|
goal = Vec2{float(wx) + 0.5f, float(wy) + 0.5f};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Застряли — идём вторым по качеству соседом. Отряд упирается в
|
||||||
|
// угол или в собственную толпу, и «лучшее направление» перестаёт
|
||||||
|
// быть проходимым; живой игрок в этот момент просто обходит.
|
||||||
|
(void)bestDist;
|
||||||
|
if (here >= 0 && here == lastDist) ++stuckFor;
|
||||||
|
else stuckFor = 0;
|
||||||
|
lastDist = here;
|
||||||
|
|
||||||
|
game.in = InputState{};
|
||||||
|
const Vec2 toGoal = goal - c;
|
||||||
|
if (LengthSq(toGoal) > 1e-4f)
|
||||||
|
game.in.moveScreen = v2d::WorldDirToScreen(Normalized(toGoal));
|
||||||
|
|
||||||
|
game.Step(FIXED_DT);
|
||||||
|
|
||||||
|
if (game.atExit)
|
||||||
|
{
|
||||||
|
reached = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int aliveNow = 0;
|
||||||
|
for (const Target& t : game.targets)
|
||||||
|
if (t.alive && !t.IsProp() && !t.IsDummy()) ++aliveNow;
|
||||||
|
|
||||||
|
float hp = 0.0f, maxHp = 0.0f;
|
||||||
|
int ammo = 0;
|
||||||
|
for (int i = 0; i < NUM_AGENTS; ++i)
|
||||||
|
{
|
||||||
|
const Agent& a = game.squad.agents[i];
|
||||||
|
if (!a.present) continue;
|
||||||
|
hp += std::max(0.0f, a.hp);
|
||||||
|
maxHp += a.maxHp;
|
||||||
|
ammo += std::max(0, a.ammoReserve);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* verdict = reached ? "дошёл" : (game.AliveAgents() > 0 ? "ЗАСТРЯЛ" : "ВАЙП");
|
||||||
|
std::printf("%-6d %7d %7d %8d %7.0f%% %9d %7.0f %s (%.0f с)\n", game.depth, startMonsters,
|
||||||
|
startMonsters - aliveNow, game.AliveAgents(), double(100.0f * hp / std::max(1.0f, maxHp)),
|
||||||
|
ammo, double(game.squad.lightCharge), verdict, double(steps) * double(FIXED_DT));
|
||||||
|
|
||||||
|
if (game.AliveAgents() <= 0)
|
||||||
|
{
|
||||||
|
wipedAt = game.depth;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
deepest = game.depth;
|
||||||
|
if (!reached) break; // застрял — дальше мерить нечего
|
||||||
|
game.Descend();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("\nпройдено этажей: %d", deepest);
|
||||||
|
if (wipedAt > 0) std::printf(", вайп на D%d", wipedAt);
|
||||||
|
std::printf("\n");
|
||||||
|
|
||||||
|
int bad = 0;
|
||||||
|
if (deepest < 1)
|
||||||
|
{
|
||||||
|
std::printf(" [FAIL] автопилот не проходит ПЕРВЫЙ этаж — игра непроходима\n");
|
||||||
|
++bad;
|
||||||
|
}
|
||||||
|
if (wipedAt == 1)
|
||||||
|
{
|
||||||
|
std::printf(" [FAIL] отряд гибнет на первом этаже\n");
|
||||||
|
++bad;
|
||||||
|
}
|
||||||
|
return bad == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
|
|||||||
@ -42,3 +42,8 @@ int RunUiCheck();
|
|||||||
// который появился вместе с чекпоинтами через 50 этажей: растёт ли достижимая
|
// который появился вместе с чекпоинтами через 50 этажей: растёт ли достижимая
|
||||||
// глубина от вылазки к вылазке (docs/16-descent.md).
|
// глубина от вылазки к вылазке (docs/16-descent.md).
|
||||||
int RunWall(const RoomLibrary& rooms, const EntityCatalog& catalog, uint32_t seed, int maxDepth);
|
int RunWall(const RoomLibrary& rooms, const EntityCatalog& catalog, uint32_t seed, int maxDepth);
|
||||||
|
|
||||||
|
// --play N [размер отряда]: автопилот. Отряд идёт к спуску и дерётся по дороге —
|
||||||
|
// единственный прогон, который меряет ПРОХОЖДЕНИЕ, а не отдельные правила.
|
||||||
|
int RunPlay(const RoomLibrary& rooms, const EntityCatalog& catalog, uint32_t seed, int floors,
|
||||||
|
int squadSize);
|
||||||
|
|||||||
26
src/main.cpp
26
src/main.cpp
@ -136,9 +136,16 @@ int main(int argc, char** argv)
|
|||||||
// Нужен ровно для картинок в документации: экраны вне боя иначе не снять,
|
// Нужен ровно для картинок в документации: экраны вне боя иначе не снять,
|
||||||
// потому что --shot никого не нажимает.
|
// потому что --shot никого не нажимает.
|
||||||
const char* uiShot = nullptr;
|
const char* uiShot = nullptr;
|
||||||
// --rooms: планировка собирается из комнат вместо захардкоженной карты.
|
// Проект подключён ПО УМОЛЧАНИЮ: игра — это проект, и запускать её без него
|
||||||
// Приёмки и headless это не касается — они до сюда не доходят.
|
// бессмысленно. Раньше по умолчанию собиралась захардкоженная карта
|
||||||
bool useRooms = false;
|
// прототипа, и обычный запуск exe высаживал ОДНОГО оперативника с ножом на
|
||||||
|
// арену, набитую тварями по старой расстановке. Это выглядело как «на
|
||||||
|
// первом этаже не прожить и пяти секунд» — и было правдой, только этаж был
|
||||||
|
// не первый, а вообще не из игры.
|
||||||
|
//
|
||||||
|
// Приёмки и headless это не касается: они до сюда не доходят и по-прежнему
|
||||||
|
// меряют песочницу (--accept, --headless).
|
||||||
|
bool useRooms = true;
|
||||||
// Какой проект открывать. Своё имя игра знает, движок — нет: для него это
|
// Какой проект открывать. Своё имя игра знает, движок — нет: для него это
|
||||||
// просто строка, которую ему передали.
|
// просто строка, которую ему передали.
|
||||||
const char* projectArg = nullptr;
|
const char* projectArg = nullptr;
|
||||||
@ -146,6 +153,8 @@ int main(int argc, char** argv)
|
|||||||
uint32_t levelSeed = 1;
|
uint32_t levelSeed = 1;
|
||||||
int startDepth = 1; // --depth N: начать спуск не с первого этажа
|
int startDepth = 1; // --depth N: начать спуск не с первого этажа
|
||||||
int descendFloors = 0;
|
int descendFloors = 0;
|
||||||
|
int playFloors = 0; // --play N: автопилот на N этажей
|
||||||
|
int playSquad = 1;
|
||||||
int wallDepth = 0; // --wall N: до какой глубины доходит отряд // --descend N: прогнать N этажей без окна и выйти
|
int wallDepth = 0; // --wall N: до какой глубины доходит отряд // --descend N: прогнать N этажей без окна и выйти
|
||||||
const char* profilePath = nullptr;
|
const char* profilePath = nullptr;
|
||||||
bool useProfile = true; // --no-profile: не трогать диск (dev-прогоны)
|
bool useProfile = true; // --no-profile: не трогать диск (dev-прогоны)
|
||||||
@ -178,6 +187,7 @@ int main(int argc, char** argv)
|
|||||||
else if (std::strcmp(argv[i], "--sf-agents") == 0) sfAgents = true;
|
else if (std::strcmp(argv[i], "--sf-agents") == 0) sfAgents = true;
|
||||||
else if (std::strcmp(argv[i], "--sf") == 0) sfTiles = sfEnemies = sfAgents = true;
|
else if (std::strcmp(argv[i], "--sf") == 0) sfTiles = sfEnemies = sfAgents = true;
|
||||||
else if (std::strcmp(argv[i], "--sprites") == 0) sfReport = true;
|
else if (std::strcmp(argv[i], "--sprites") == 0) sfReport = true;
|
||||||
|
else if (std::strcmp(argv[i], "--no-rooms") == 0) useRooms = false;
|
||||||
else if (std::strcmp(argv[i], "--windowed") == 0) windowed = true;
|
else if (std::strcmp(argv[i], "--windowed") == 0) windowed = true;
|
||||||
else if (std::strcmp(argv[i], "--sandbox") == 0) sandbox = true;
|
else if (std::strcmp(argv[i], "--sandbox") == 0) sandbox = true;
|
||||||
else if (std::strcmp(argv[i], "--ui-shot") == 0 && i + 1 < argc) uiShot = argv[++i];
|
else if (std::strcmp(argv[i], "--ui-shot") == 0 && i + 1 < argc) uiShot = argv[++i];
|
||||||
@ -186,6 +196,11 @@ int main(int argc, char** argv)
|
|||||||
else if (std::strcmp(argv[i], "--profile") == 0 && i + 1 < argc)
|
else if (std::strcmp(argv[i], "--profile") == 0 && i + 1 < argc)
|
||||||
profilePath = argv[++i];
|
profilePath = argv[++i];
|
||||||
else if (std::strcmp(argv[i], "--no-profile") == 0) useProfile = false;
|
else if (std::strcmp(argv[i], "--no-profile") == 0) useProfile = false;
|
||||||
|
else if (std::strcmp(argv[i], "--play") == 0 && i + 1 < argc)
|
||||||
|
{
|
||||||
|
playFloors = TextToInteger(argv[++i]);
|
||||||
|
if (i + 1 < argc && argv[i + 1][0] != '-') playSquad = TextToInteger(argv[++i]);
|
||||||
|
}
|
||||||
else if (std::strcmp(argv[i], "--wall") == 0 && i + 1 < argc)
|
else if (std::strcmp(argv[i], "--wall") == 0 && i + 1 < argc)
|
||||||
wallDepth = TextToInteger(argv[++i]);
|
wallDepth = TextToInteger(argv[++i]);
|
||||||
else if (std::strcmp(argv[i], "--descend") == 0 && i + 1 < argc)
|
else if (std::strcmp(argv[i], "--descend") == 0 && i + 1 < argc)
|
||||||
@ -295,6 +310,11 @@ int main(int argc, char** argv)
|
|||||||
if (accept) return RunAccept();
|
if (accept) return RunAccept();
|
||||||
if (uiCheck) return RunUiCheck();
|
if (uiCheck) return RunUiCheck();
|
||||||
if (harness.seconds > 0.0f) return RunHeadless(harness);
|
if (harness.seconds > 0.0f) return RunHeadless(harness);
|
||||||
|
if (playFloors > 0)
|
||||||
|
{
|
||||||
|
if (!contentOk) { std::printf("автопилоту нужен набор комнат\n"); return 1; }
|
||||||
|
return RunPlay(rooms, catalog, levelSeed, playFloors, playSquad);
|
||||||
|
}
|
||||||
if (wallDepth > 0)
|
if (wallDepth > 0)
|
||||||
{
|
{
|
||||||
if (!contentOk) { std::printf("проверка стены невозможна без набора комнат\n"); return 1; }
|
if (!contentOk) { std::printf("проверка стены невозможна без набора комнат\n"); return 1; }
|
||||||
|
|||||||
@ -243,14 +243,53 @@ 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
|
||||||
: Lerpf(1.0f, g_tune.sprintSpacing, urgency);
|
: Lerpf(1.0f, g_tune.sprintSpacing, urgency);
|
||||||
|
// В тесноте строй И сжимается вбок, И вытягивается вдоль хода: это и есть
|
||||||
|
// колонна. Числа в tuning, чтобы крутились на живой игре.
|
||||||
const float wantStretch = Lerpf(1.0f, g_tune.sprintStretch, urgency);
|
const float wantStretch = Lerpf(1.0f, g_tune.sprintStretch, urgency);
|
||||||
|
const float tightSpacing = Lerpf(wantSpacing, g_tune.tightSpacing, tight);
|
||||||
|
const float tightStretch = Lerpf(wantStretch, g_tune.tightStretch, tight);
|
||||||
const float k = 1.0f - std::exp(-std::max(0.01f, g_tune.formMorphRate) * dt);
|
const float k = 1.0f - std::exp(-std::max(0.01f, g_tune.formMorphRate) * dt);
|
||||||
formSpacing += (wantSpacing - formSpacing) * k;
|
formSpacing += (tightSpacing - formSpacing) * k;
|
||||||
formStretch += (wantStretch - formStretch) * k;
|
formStretch += (tightStretch - formStretch) * k;
|
||||||
|
|
||||||
if (state != prevState)
|
if (state != prevState)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -256,6 +256,10 @@ struct Tuning
|
|||||||
|
|
||||||
// --- дыхание строя --------------------------------------------------------
|
// --- дыхание строя --------------------------------------------------------
|
||||||
float holdSpacing = 0.90f; // на стоянке отряд собирается
|
float holdSpacing = 0.90f; // на стоянке отряд собирается
|
||||||
|
// Строй в тесноте: во сколько раз сжимается вбок и вытягивается вдоль хода.
|
||||||
|
// 0.28 — это колонна: пять тел проходят в дверь шириной в одну клетку.
|
||||||
|
float tightSpacing = 0.28f;
|
||||||
|
float tightStretch = 1.60f;
|
||||||
float sprintSpacing = 1.30f; // в беге расходится
|
float sprintSpacing = 1.30f; // в беге расходится
|
||||||
float sprintStretch = 1.55f; // и вытягивается вдоль движения
|
float sprintStretch = 1.55f; // и вытягивается вдоль движения
|
||||||
float formMorphRate = 4.0f; // скорость перестроения, 1/сек
|
float formMorphRate = 4.0f; // скорость перестроения, 1/сек
|
||||||
@ -456,6 +460,7 @@ inline const TunableParam TUNABLES[] = {
|
|||||||
{"sTooClose", &Tuning::sTooClose, 2.0f, 0.0f, 300.0f},
|
{"sTooClose", &Tuning::sTooClose, 2.0f, 0.0f, 300.0f},
|
||||||
{"postLeash", &Tuning::postLeash, 0.1f, 0.0f, 10.0f},
|
{"postLeash", &Tuning::postLeash, 0.1f, 0.0f, 10.0f},
|
||||||
{"formSpacing", &Tuning::formationSpacing, 0.05f, 0.4f, 4.0f},
|
{"formSpacing", &Tuning::formationSpacing, 0.05f, 0.4f, 4.0f},
|
||||||
|
{"tightSpacing", &Tuning::tightSpacing, 0.02f, 0.1f, 1.0f},
|
||||||
{"anchorSpeed", &Tuning::anchorSpeed, 0.1f, 0.5f, 12.0f},
|
{"anchorSpeed", &Tuning::anchorSpeed, 0.1f, 0.5f, 12.0f},
|
||||||
{"anchorSpeedFast",&Tuning::anchorSpeedFast, 0.1f, 0.5f, 16.0f},
|
{"anchorSpeedFast",&Tuning::anchorSpeedFast, 0.1f, 0.5f, 16.0f},
|
||||||
{"anchorLeash", &Tuning::anchorLeash, 0.1f, 0.2f, 12.0f},
|
{"anchorLeash", &Tuning::anchorLeash, 0.1f, 0.2f, 12.0f},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user