Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
#include "sim/squad.h"
|
|
|
|
|
|
|
|
|
|
|
|
#include "ai/lane_registry.h"
|
|
|
|
|
|
|
|
|
|
|
|
namespace
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
// Локальные слоты строя: x = вперёд, y = вправо. Множатся на formationSpacing.
|
|
|
|
|
|
const float kWedge[NUM_AGENTS][2] = {
|
|
|
|
|
|
{ 1.0f, 0.0f},
|
|
|
|
|
|
{ 0.0f, -0.9f},
|
|
|
|
|
|
{ 0.0f, 0.9f},
|
|
|
|
|
|
{-1.0f, -1.8f},
|
|
|
|
|
|
{-1.0f, 1.8f},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const float kLine[NUM_AGENTS][2] = {
|
|
|
|
|
|
{0.0f, -2.0f},
|
|
|
|
|
|
{0.0f, -1.0f},
|
|
|
|
|
|
{0.0f, 0.0f},
|
|
|
|
|
|
{0.0f, 1.0f},
|
|
|
|
|
|
{0.0f, 2.0f},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
Поле путей: отставший возвращается в строй (M37)
Отряд из пяти не проходил первый этаж ни на одном сиде, а одиночка проходил.
Дело было не в ширине коридоров: у бойцов не было поиска пути вообще. Боец,
оказавшийся за стеной от своего слота, seek'ом тянулся В СТЕНУ, обход стены
толкал его обратно, и он замирал в углу навсегда.
Теперь у каждого бойца своё поле путей (BFS от клетки его цели) с натягиванием
верёвки: он берёт самую дальнюю точку пути, которую видит. Включается обход по
двум условиям сразу — боец упёрся (хочет идти и не едет) И потерял строй. Счётчик
упора сбрасывается по факту «цель видно», а не «поехал»: сброс по скорости давал
дребезг, и боец полз шаг за две секунды.
По дороге вскрылись две неверные МЕРКИ:
* критерий 11.6 считал колонной «разброс вдоль больше, чем поперёк» — и потому
награждал отряд, у которого двое навсегда остались по ту сторону стены. Теперь
колонна меряется самим проёмом: разом в горловине не больше двух, и прошли ВСЕ.
* зонд --wall читал «стены не нашлось» (ноль) как «стена на нулевом этаже» и
объявлял пятёрку, не сломавшуюся нигде, худшей из трёх.
Обе переписаны и проверены на сломанной сборке.
Замеры: --play 6 5 на восьми сидах 47/48 (было 0/48), --accept PASS, шаг 6.1 мкс,
--wall 30 одиночка D20 / трое D20 / пятеро D31 (не найдена), --descend 8/30,
hollow --descend 6, --ui-check, оба проекта --check — без ошибок.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 22:31:34 +07:00
|
|
|
|
// «Недостижимо» в поле путей и потолок длины натягиваемой верёвки.
|
|
|
|
|
|
constexpr uint16_t NAV_FAR = 0xFFFF;
|
|
|
|
|
|
constexpr int NAV_PATH_MAX = 64;
|
|
|
|
|
|
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
|
|
const char* FormationName(Formation f)
|
|
|
|
|
|
{
|
|
|
|
|
|
switch (f)
|
|
|
|
|
|
{
|
|
|
|
|
|
case Formation::WEDGE: return "WEDGE";
|
|
|
|
|
|
case Formation::LINE: return "LINE";
|
|
|
|
|
|
case Formation::RING: return "RING";
|
|
|
|
|
|
default: return "?";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void Squad::Init(ecs::World& world, Vec2 at)
|
|
|
|
|
|
{
|
|
|
|
|
|
anchor = at;
|
|
|
|
|
|
anchorPrev = at;
|
|
|
|
|
|
anchorVel = {};
|
|
|
|
|
|
forward = {1.0f, 0.0f};
|
|
|
|
|
|
state = AgentState::HOLDING;
|
|
|
|
|
|
idleTimer = g_tune.holdDelay;
|
|
|
|
|
|
holdElapsed = 0.0f;
|
|
|
|
|
|
urgency = 0.0f;
|
|
|
|
|
|
sprinting = false;
|
|
|
|
|
|
formSpacing = 1.0f;
|
|
|
|
|
|
formStretch = 1.0f;
|
|
|
|
|
|
|
|
|
|
|
|
meleeCount = std::min(std::max(meleeCount, 0), NUM_AGENTS);
|
|
|
|
|
|
pistolCount = std::min(std::max(pistolCount, 0), NUM_AGENTS - meleeCount);
|
|
|
|
|
|
|
|
|
|
|
|
// Сначала создаём все сущности бойцов, и только потом берём указатель на
|
|
|
|
|
|
// плотный массив пула: до конца заполнения он может переехать.
|
|
|
|
|
|
ecs::Entity ids[NUM_AGENTS];
|
|
|
|
|
|
for (int i = 0; i < NUM_AGENTS; ++i)
|
|
|
|
|
|
{
|
|
|
|
|
|
ids[i] = world.Create();
|
|
|
|
|
|
world.Add<Agent>(ids[i], Agent{});
|
|
|
|
|
|
}
|
|
|
|
|
|
agents.p = world.All<Agent>().data();
|
|
|
|
|
|
agents.n = NUM_AGENTS;
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < NUM_AGENTS; ++i)
|
|
|
|
|
|
{
|
|
|
|
|
|
Agent& a = agents[i];
|
|
|
|
|
|
a = Agent{};
|
|
|
|
|
|
a.self = ids[i];
|
|
|
|
|
|
a.slotIndex = i;
|
|
|
|
|
|
|
|
|
|
|
|
// Клинки занимают первые слоты: в клине это остриё и левое крыло,
|
|
|
|
|
|
// то есть ровно те, кто и так упирается в противника первым.
|
|
|
|
|
|
// Пистолеты — последние слоты: короткий ствол уезжает в хвост клина.
|
|
|
|
|
|
const bool isMelee = (i < meleeCount);
|
|
|
|
|
|
const bool isPistol = !isMelee && (i >= NUM_AGENTS - pistolCount);
|
|
|
|
|
|
|
Tile2D: движок отдельным продуктом, игра — проектом в нём (M11-M20)
Десять вех одним состоянием: разделять их пофайлово нечем, потому что
src/game.cpp менялся почти в каждой. Собирается начисто без предупреждений,
все проверки проходят.
ДВИЖОК (engine/, цель tile2d)
Каталог свой, и решение «это движок» принимается тем, куда положен файл.
Компилятор нашёл три зависимости движка от игры, и все сняты: размеры кадра
и карты уехали в engine/config.h, потолок населения этажа стал своим
(MAX_LEVEL_SPAWNS против игровой MAX_TARGETS), запасная точка старта стала
центром карты вместо позиции отряда.
engine/projection.* единственное место, где движок знает про изометрию;
видов сразу два — абстракция с одной реализацией ничем
не проверена и обычно оказывается неправильной
engine/project.* манифест проекта: пути, вид, кривая глубины
engine/app.* окно, кадр, fixed timestep — вместо двух копий цикла
engine/tilemap_draw движок научился показывать свою карту
РЕДАКТОР (editor/, цель tile2d_editor)
Открывает любой проект и ни одной игры по имени не знает.
--tool одиннадцать безоконных команд: комнаты, каталог, кривая, сборка
этажа, ASCII-карта, проверка набора. Правку делают файлами,
последствия читают командой (docs/19-agent.md)
терминал снизу: те же инструменты, git, сборка и запросы к claude/codex,
открытый проект уходит потомкам через TILE2D_PROJECT
ПРИМЕР (samples/walk, цель tile2d_walk)
Второй потребитель движка. Игра и редактор самостоятельность движка
проверить не могут: редактор мира не рисует, а игра — то место, куда общий
код и уезжает незаметно.
ИГРА (src/)
Экстракшен: вес и два порога переноски, конечный боезапас, расходники
применяются сами, общая сумка отряда, трата опыта на улучшения.
Каждое из правил проверяется прогоном, а не глазами, и проверки поймали:
вес дважды не связывал ничего (мерил по бинтам прогона, а не по добыче);
боец с клинком не тратил найденные патроны при запасном стволе в сумке;
массив виджетов экрана переполнился и молча съел хвост схрона; проверка
улучшений сама ломала забег, вызывая NewRun посреди спуска.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 01:07:19 +07:00
|
|
|
|
// Стартовое оружие — то же, что и было: клинок, карабин, пистолет.
|
|
|
|
|
|
// Снаряжения на бойце НЕТ: броня меняет скорость, а на скорости меряются
|
|
|
|
|
|
// критерии приёмки. Комплекты раздаёт игровой режим (Game::EquipKits).
|
|
|
|
|
|
const ItemId weapon = isMelee ? ItemId::SABER
|
|
|
|
|
|
: isPistol ? ItemId::PISTOL
|
|
|
|
|
|
: ItemId::RIFLE;
|
|
|
|
|
|
|
|
|
|
|
|
a.weaponEntity = SpawnWeapon(world, weapon);
|
|
|
|
|
|
world.Add<Inventory>(ids[i], Inventory{weapon, ItemId::NONE, ItemId::NONE});
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
|
|
|
|
|
|
a.melee.sweepSign = (i & 1) ? -1.0f : 1.0f;
|
Триггеры: четыре условных эффекта на аффиксах (M28)
ON_HIT поджигает цель, ON_KILL даёт рывок скорости стаками, ON_HURT
обжигает того, кто ударил, ON_DOWN даёт отряду ярость за павшего.
Отдельным тегом аффикса, то есть по тем же правилам, что и всё
остальное: платит по тиру, влезает в бюджет, катается и прививается.
СДЕЛАНО НЕ ТАК, КАК ПРЕДЛАГАЛ ДИЗАЙН. В docs/21-damage.md §4 стояло:
журнал боевых событий уже есть, вешайте триггеры на него. Звучит
правильно, и сделать так нельзя: sim/events.h это канал «симуляция ->
эффекты», односторонний по устройству, и в headless-прогонах его нет
вовсе. Начни правила боя его читать — игра станет вести себя по-разному
с окном и без, то есть --accept начнёт мерить не то, что меряет.
Триггеры срабатывают в месте урона. Опасение «два канала разойдутся»
снято иначе: второго канала никто и не завёл, есть четыре точки рядом с
тем уроном, который триггер обслуживает.
Четыре из девяти, и это не «пока»: каждый эффект обязан класть своё
число в уже существующий канал влияния. Крита в игре нет, площадного
урона нет, выдачи предметов на входе нет — заводить их ради строк
таблицы значит завести пять мёртвых веток. Причины выписаны в документе.
Три ограничителя проверяются числами в --descend:
триггеры: аффикс доехал да, ON_HIT поджёг стаков 1, откат 0.9 с
(за него наложено 0), ON_KILL стаков 1, скорость 1.06
рекурсия: за один шаг стаков прибавлялось максимум 1
Первая строка про проводку: аффикс с триггером легко сделать так, что он
лежит в предмете, показывается в подсказке и не делает ничего.
Проверка сперва брала targets[0] и проходила на descent по везению: на
hollow первым в пуле стоял ящик, а обстановка не горит и убийством не
считается. Теперь цель ищется по виду и ставится в свободную клетку с
прямой видимостью — иначе замеряется планировка этажа, а не триггеры.
Шаг симуляции 3.7 мкс из 16666. --accept PASS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 07:32:12 +07:00
|
|
|
|
a.trig.rng = 1u + uint32_t(i) * 2654435761u; // свой поток на слот
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
a.pos = SlotWorld(i);
|
|
|
|
|
|
a.prevPos = a.pos;
|
|
|
|
|
|
a.postPos = a.pos;
|
|
|
|
|
|
a.facing = 0.0f;
|
|
|
|
|
|
a.state = state;
|
|
|
|
|
|
a.currentTargetId = -1;
|
|
|
|
|
|
a.commitTimer = 0.0f;
|
|
|
|
|
|
// Фазы решателя разведены: за кадр решает максимум один агент (6.5).
|
|
|
|
|
|
a.solveTimer = g_tune.solveInterval * (float(i) / float(NUM_AGENTS));
|
|
|
|
|
|
|
|
|
|
|
|
SyncLoadout(world, a);
|
|
|
|
|
|
a.gun.ammo = a.gun.def.magazine; // выходим с полным магазином
|
Отряд растёт с одного оперативника до пяти (M24)
Игра начинается ОДНИМ человеком: тактический нож, слабый фонарь, куртка.
Остальные четверо нанимаются за опыт по кривой 600/2400/9600/38400 —
самая дорогая покупка в игре и намеренно. Одиночка с ножом это первое
впечатление, и продать второго бойца за одну вылазку значило бы выбросить
весь хоррор начала: вдвоём уже не страшно.
NUM_AGENTS осталась величиной времени сборки и означает теперь ВМЕСТИМОСТЬ,
а не размер отряда: делать её динамической это сотня мест ради числа,
которое всё равно упирается в пять. Занятость слота это Agent::present, и
он отличается от alive. Правила боя читают только alive — для них пустой
слот и павший одно, и это верно. Разница нужна там, где показывают
человека: у павшего есть тело на полу и перечёркнутая карточка, у пустого
слота нет ни того, ни другого.
Проверка в --descend меряет цепочку «в профиле -> на этаже» и потолок, а
главное — переживает ли одиночка первую схватку:
отряд: в профиле 1 -> на этаже 1, после найма 2, цена 600, сверх потолка отказ
одиночка с ножом на первом этаже: выжил, 100 -> 41, подошло 2, убил 2
Первая версия этой проверки не измеряла ничего: боец стоял минуту, к нему
не подошёл никто, и «выжил» было пустым словом. Теперь отряд ставится
рядом с тварью.
Попутно найдено и исправлено: Profile::Save три вехи не писала ни версию
формата, ни улучшения. Купленный запас патронов действовал до конца
запуска и молча пропадал при чтении, а профиль без версии читался как
формат 1. Круговой прогон этого не видел, потому что сверял три поля из
десяти и делал это на пустом профиле, где всё сходится само. Теперь он
сверяет поля списком и на набитом профиле.
Формат профиля 3: добавлена строка squad. Старый профиль играл впятером и
получает пятерых при миграции — отобрать их значило бы съесть отряд,
которого игрок не терял.
--accept PASS без изменений: без профиля отряд по-прежнему впятером.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:29:19 +07:00
|
|
|
|
|
|
|
|
|
|
// Незанятый слот: сущность есть (по ней ходят все циклы), человека нет.
|
|
|
|
|
|
// Не alive — значит, ни решатель, ни твари, ни правила огня его не
|
|
|
|
|
|
// видят; не present — значит, его не видит и игрок.
|
|
|
|
|
|
a.present = (i < roster);
|
|
|
|
|
|
a.alive = a.present;
|
|
|
|
|
|
a.hp = a.alive ? a.maxHp : 0.0f;
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Vec2 Squad::SlotLocal(int i) const
|
|
|
|
|
|
{
|
|
|
|
|
|
// Интервал и вытяжение — это ТЕКУЩЕЕ состояние строя, а не константа:
|
|
|
|
|
|
// на стоянке отряд собирается, в беге растягивается вдоль движения.
|
|
|
|
|
|
const float s = g_tune.formationSpacing * formSpacing;
|
|
|
|
|
|
const float fwd = s * formStretch;
|
|
|
|
|
|
|
|
|
|
|
|
switch (formation)
|
|
|
|
|
|
{
|
|
|
|
|
|
case Formation::LINE:
|
|
|
|
|
|
return {kLine[i][0] * fwd, kLine[i][1] * s};
|
|
|
|
|
|
case Formation::RING:
|
|
|
|
|
|
{
|
|
|
|
|
|
const float ang = float(i) * (TWO_PI_F / float(NUM_AGENTS));
|
|
|
|
|
|
const Vec2 d = FromAngle(ang) * 1.5f;
|
|
|
|
|
|
return {d.x * fwd, d.y * s};
|
|
|
|
|
|
}
|
|
|
|
|
|
case Formation::WEDGE:
|
|
|
|
|
|
default:
|
|
|
|
|
|
return {kWedge[i][0] * fwd, kWedge[i][1] * s};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Vec2 Squad::SlotWorld(int i) const
|
|
|
|
|
|
{
|
|
|
|
|
|
const Vec2 local = SlotLocal(i);
|
|
|
|
|
|
const Vec2 right = Perp(forward);
|
|
|
|
|
|
return anchor + forward * local.x + right * local.y;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Vec2 Squad::GoalFor(int i) const
|
|
|
|
|
|
{
|
Поле путей: отставший возвращается в строй (M37)
Отряд из пяти не проходил первый этаж ни на одном сиде, а одиночка проходил.
Дело было не в ширине коридоров: у бойцов не было поиска пути вообще. Боец,
оказавшийся за стеной от своего слота, seek'ом тянулся В СТЕНУ, обход стены
толкал его обратно, и он замирал в углу навсегда.
Теперь у каждого бойца своё поле путей (BFS от клетки его цели) с натягиванием
верёвки: он берёт самую дальнюю точку пути, которую видит. Включается обход по
двум условиям сразу — боец упёрся (хочет идти и не едет) И потерял строй. Счётчик
упора сбрасывается по факту «цель видно», а не «поехал»: сброс по скорости давал
дребезг, и боец полз шаг за две секунды.
По дороге вскрылись две неверные МЕРКИ:
* критерий 11.6 считал колонной «разброс вдоль больше, чем поперёк» — и потому
награждал отряд, у которого двое навсегда остались по ту сторону стены. Теперь
колонна меряется самим проёмом: разом в горловине не больше двух, и прошли ВСЕ.
* зонд --wall читал «стены не нашлось» (ноль) как «стена на нулевом этаже» и
объявлял пятёрку, не сломавшуюся нигде, худшей из трёх.
Обе переписаны и проверены на сломанной сборке.
Замеры: --play 6 5 на восьми сидах 47/48 (было 0/48), --accept PASS, шаг 6.1 мкс,
--wall 30 одиночка D20 / трое D20 / пятеро D31 (не найдена), --descend 8/30,
hollow --descend 6, --ui-check, оба проекта --check — без ошибок.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 22:31:34 +07:00
|
|
|
|
// ОТСТАВШИЙ ВОЗВРАЩАЕТСЯ В СТРОЙ, что бы ни говорил решатель.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Без этого правила отряд намертво вставал, и вот почему. Якорь упирается в
|
|
|
|
|
|
// поводок до центра отряда -> отряд переходит в HOLDING -> отставшие в
|
|
|
|
|
|
// HOLDING держат ОГНЕВУЮ ПОЗИЦИЮ вместо возврата в слот -> центр не
|
|
|
|
|
|
// догоняет -> якорь не может идти дальше. Круг замкнулся, и снаружи это
|
|
|
|
|
|
// выглядело как «пятеро не проходят там, где одиночка проходит».
|
|
|
|
|
|
//
|
|
|
|
|
|
// Проверено отдельно: ширина коридоров тут ни при чём — отряд вставал и на
|
|
|
|
|
|
// проходах в три, четыре и пять клеток, и даже в пустых комнатах.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Правило заодно честное по игре: отряд, растянувшийся на полэтажа, — это
|
|
|
|
|
|
// не строй, а цепочка одиночек, и собраться он обязан сам.
|
|
|
|
|
|
const float lag = Distance(agents[i].pos, SlotWorld(i));
|
|
|
|
|
|
if (lag > g_tune.straggleDist) return SlotWorld(i);
|
|
|
|
|
|
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
// Боец ближнего боя ведёт себя одинаково в обоих состояниях: пока цель
|
|
|
|
|
|
// внутри его круга, он идёт к ней, а не в слот. Иначе клинок оказывался бы
|
|
|
|
|
|
// полезен только на стоянке — а он нужен как раз в движении.
|
|
|
|
|
|
// Но в спринте цель отменяется: бежим, а не деремся.
|
|
|
|
|
|
if (agents[i].IsMelee() && !sprinting) return agents[i].postPos;
|
|
|
|
|
|
if (agents[i].IsMelee()) return SlotWorld(i);
|
|
|
|
|
|
|
|
|
|
|
|
// В движении — слот строя, на месте — выбранная решателем огневая позиция.
|
|
|
|
|
|
return (state == AgentState::ADVANCING) ? SlotWorld(i) : agents[i].postPos;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Поле путей: отставший возвращается в строй (M37)
Отряд из пяти не проходил первый этаж ни на одном сиде, а одиночка проходил.
Дело было не в ширине коридоров: у бойцов не было поиска пути вообще. Боец,
оказавшийся за стеной от своего слота, seek'ом тянулся В СТЕНУ, обход стены
толкал его обратно, и он замирал в углу навсегда.
Теперь у каждого бойца своё поле путей (BFS от клетки его цели) с натягиванием
верёвки: он берёт самую дальнюю точку пути, которую видит. Включается обход по
двум условиям сразу — боец упёрся (хочет идти и не едет) И потерял строй. Счётчик
упора сбрасывается по факту «цель видно», а не «поехал»: сброс по скорости давал
дребезг, и боец полз шаг за две секунды.
По дороге вскрылись две неверные МЕРКИ:
* критерий 11.6 считал колонной «разброс вдоль больше, чем поперёк» — и потому
награждал отряд, у которого двое навсегда остались по ту сторону стены. Теперь
колонна меряется самим проёмом: разом в горловине не больше двух, и прошли ВСЕ.
* зонд --wall читал «стены не нашлось» (ноль) как «стена на нулевом этаже» и
объявлял пятёрку, не сломавшуюся нигде, худшей из трёх.
Обе переписаны и проверены на сломанной сборке.
Замеры: --play 6 5 на восьми сидах 47/48 (было 0/48), --accept PASS, шаг 6.1 мкс,
--wall 30 одиночка D20 / трое D20 / пятеро D31 (не найдена), --descend 8/30,
hollow --descend 6, --ui-check, оба проекта --check — без ошибок.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 22:31:34 +07:00
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
// Поле путей: BFS от клетки цели по проходимым клеткам, четыре соседа.
|
|
|
|
|
|
// Пересчёт только когда цель сменила клетку — это несколько тысяч клеток и
|
|
|
|
|
|
// стоит меньше одного кадра рендера.
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
void Squad::RebuildNav(NavField& field, const Tilemap& map, Vec2 goal) const
|
|
|
|
|
|
{
|
|
|
|
|
|
int gx = int(std::floor(goal.x));
|
|
|
|
|
|
int gy = int(std::floor(goal.y));
|
|
|
|
|
|
|
|
|
|
|
|
// Якорь игрок может завести в стену — поле строим от ближайшей свободной
|
|
|
|
|
|
// клетки, иначе отряд остался бы вообще без путей.
|
|
|
|
|
|
if (!Tilemap::InBounds(gx, gy) || map.IsWall(gx, gy))
|
|
|
|
|
|
{
|
|
|
|
|
|
bool found = false;
|
|
|
|
|
|
for (int r = 1; r <= 5 && !found; ++r)
|
|
|
|
|
|
for (int dy = -r; dy <= r && !found; ++dy)
|
|
|
|
|
|
for (int dx = -r; dx <= r && !found; ++dx)
|
|
|
|
|
|
{
|
|
|
|
|
|
const int x = gx + dx, y = gy + dy;
|
|
|
|
|
|
if (!Tilemap::InBounds(x, y) || map.IsWall(x, y)) continue;
|
|
|
|
|
|
gx = x; gy = y; found = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!found) return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (gx == field.goalX && gy == field.goalY && !field.cost.empty()) return;
|
|
|
|
|
|
field.goalX = gx;
|
|
|
|
|
|
field.goalY = gy;
|
|
|
|
|
|
|
|
|
|
|
|
field.cost.assign(size_t(MAP_W) * MAP_H, NAV_FAR);
|
|
|
|
|
|
std::vector<int> queue;
|
|
|
|
|
|
queue.reserve(size_t(MAP_W) * MAP_H);
|
|
|
|
|
|
|
|
|
|
|
|
field.cost[size_t(gy) * MAP_W + gx] = 0;
|
|
|
|
|
|
queue.push_back(gy * MAP_W + gx);
|
|
|
|
|
|
|
|
|
|
|
|
static const int DX[4] = {1, -1, 0, 0};
|
|
|
|
|
|
static const int DY[4] = {0, 0, 1, -1};
|
|
|
|
|
|
for (size_t head = 0; head < queue.size(); ++head)
|
|
|
|
|
|
{
|
|
|
|
|
|
const int c = queue[head];
|
|
|
|
|
|
const int cx = c % MAP_W;
|
|
|
|
|
|
const int cy = c / MAP_W;
|
|
|
|
|
|
const uint16_t nd = uint16_t(field.cost[size_t(c)] + 1);
|
|
|
|
|
|
for (int k = 0; k < 4; ++k)
|
|
|
|
|
|
{
|
|
|
|
|
|
const int x = cx + DX[k], y = cy + DY[k];
|
|
|
|
|
|
if (!Tilemap::InBounds(x, y) || map.IsWall(x, y)) continue;
|
|
|
|
|
|
const size_t n = size_t(y) * MAP_W + x;
|
|
|
|
|
|
if (field.cost[n] <= nd) continue;
|
|
|
|
|
|
field.cost[n] = nd;
|
|
|
|
|
|
queue.push_back(int(n));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Vec2 Squad::NavWaypoint(const NavField& field, const Tilemap& map, Vec2 from, Vec2 goal) const
|
|
|
|
|
|
{
|
|
|
|
|
|
// Видно цель — идём в цель. Поле нужно ровно там, где прямой не хватает.
|
|
|
|
|
|
if (field.cost.empty() || !map.RaycastBlocked(from, goal)) return goal;
|
|
|
|
|
|
|
|
|
|
|
|
int cx = int(std::floor(from.x));
|
|
|
|
|
|
int cy = int(std::floor(from.y));
|
|
|
|
|
|
if (!Tilemap::InBounds(cx, cy)) return goal;
|
|
|
|
|
|
|
|
|
|
|
|
uint16_t here = field.cost[size_t(cy) * MAP_W + cx];
|
|
|
|
|
|
if (here == NAV_FAR || here == 0) return goal; // вне поля или уже пришли
|
|
|
|
|
|
|
|
|
|
|
|
// Спуск по градиенту. Длина ограничена: дальше горизонта верёвка всё равно
|
|
|
|
|
|
// не натягивается, а лишние клетки — лишняя работа на каждого бойца.
|
|
|
|
|
|
Vec2 path[NAV_PATH_MAX];
|
|
|
|
|
|
int n = 0;
|
|
|
|
|
|
static const int DX[4] = {1, -1, 0, 0};
|
|
|
|
|
|
static const int DY[4] = {0, 0, 1, -1};
|
|
|
|
|
|
while (n < NAV_PATH_MAX && here > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
int bx = -1, by = -1;
|
|
|
|
|
|
uint16_t best = here;
|
|
|
|
|
|
for (int k = 0; k < 4; ++k)
|
|
|
|
|
|
{
|
|
|
|
|
|
const int x = cx + DX[k], y = cy + DY[k];
|
|
|
|
|
|
if (!Tilemap::InBounds(x, y)) continue;
|
|
|
|
|
|
const uint16_t d = field.cost[size_t(y) * MAP_W + x];
|
|
|
|
|
|
if (d >= best) continue;
|
|
|
|
|
|
best = d; bx = x; by = y;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (bx < 0) break;
|
|
|
|
|
|
cx = bx; cy = by; here = best;
|
|
|
|
|
|
path[n++] = Vec2{float(cx) + 0.5f, float(cy) + 0.5f};
|
|
|
|
|
|
}
|
|
|
|
|
|
if (n == 0) return goal;
|
|
|
|
|
|
|
|
|
|
|
|
// Натягиваем верёвку: самая дальняя точка пути, которую боец ещё видит.
|
|
|
|
|
|
for (int i = n - 1; i >= 1; --i)
|
|
|
|
|
|
if (!map.RaycastBlocked(from, path[i])) return path[i];
|
|
|
|
|
|
return path[0];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
Vec2 Squad::Centroid() const
|
|
|
|
|
|
{
|
Tile2D: движок отдельным продуктом, игра — проектом в нём (M11-M20)
Десять вех одним состоянием: разделять их пофайлово нечем, потому что
src/game.cpp менялся почти в каждой. Собирается начисто без предупреждений,
все проверки проходят.
ДВИЖОК (engine/, цель tile2d)
Каталог свой, и решение «это движок» принимается тем, куда положен файл.
Компилятор нашёл три зависимости движка от игры, и все сняты: размеры кадра
и карты уехали в engine/config.h, потолок населения этажа стал своим
(MAX_LEVEL_SPAWNS против игровой MAX_TARGETS), запасная точка старта стала
центром карты вместо позиции отряда.
engine/projection.* единственное место, где движок знает про изометрию;
видов сразу два — абстракция с одной реализацией ничем
не проверена и обычно оказывается неправильной
engine/project.* манифест проекта: пути, вид, кривая глубины
engine/app.* окно, кадр, fixed timestep — вместо двух копий цикла
engine/tilemap_draw движок научился показывать свою карту
РЕДАКТОР (editor/, цель tile2d_editor)
Открывает любой проект и ни одной игры по имени не знает.
--tool одиннадцать безоконных команд: комнаты, каталог, кривая, сборка
этажа, ASCII-карта, проверка набора. Правку делают файлами,
последствия читают командой (docs/19-agent.md)
терминал снизу: те же инструменты, git, сборка и запросы к claude/codex,
открытый проект уходит потомкам через TILE2D_PROJECT
ПРИМЕР (samples/walk, цель tile2d_walk)
Второй потребитель движка. Игра и редактор самостоятельность движка
проверить не могут: редактор мира не рисует, а игра — то место, куда общий
код и уезжает незаметно.
ИГРА (src/)
Экстракшен: вес и два порога переноски, конечный боезапас, расходники
применяются сами, общая сумка отряда, трата опыта на улучшения.
Каждое из правил проверяется прогоном, а не глазами, и проверки поймали:
вес дважды не связывал ничего (мерил по бинтам прогона, а не по добыче);
боец с клинком не тратил найденные патроны при запасном стволе в сумке;
массив виджетов экрана переполнился и молча съел хвост схрона; проверка
улучшений сама ломала забег, вызывая NewRun посреди спуска.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 01:07:19 +07:00
|
|
|
|
// Центр масс считается по ЖИВЫМ: поводок якоря не должен тянуть отряд
|
|
|
|
|
|
// к телу павшего.
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
Vec2 c{};
|
Tile2D: движок отдельным продуктом, игра — проектом в нём (M11-M20)
Десять вех одним состоянием: разделять их пофайлово нечем, потому что
src/game.cpp менялся почти в каждой. Собирается начисто без предупреждений,
все проверки проходят.
ДВИЖОК (engine/, цель tile2d)
Каталог свой, и решение «это движок» принимается тем, куда положен файл.
Компилятор нашёл три зависимости движка от игры, и все сняты: размеры кадра
и карты уехали в engine/config.h, потолок населения этажа стал своим
(MAX_LEVEL_SPAWNS против игровой MAX_TARGETS), запасная точка старта стала
центром карты вместо позиции отряда.
engine/projection.* единственное место, где движок знает про изометрию;
видов сразу два — абстракция с одной реализацией ничем
не проверена и обычно оказывается неправильной
engine/project.* манифест проекта: пути, вид, кривая глубины
engine/app.* окно, кадр, fixed timestep — вместо двух копий цикла
engine/tilemap_draw движок научился показывать свою карту
РЕДАКТОР (editor/, цель tile2d_editor)
Открывает любой проект и ни одной игры по имени не знает.
--tool одиннадцать безоконных команд: комнаты, каталог, кривая, сборка
этажа, ASCII-карта, проверка набора. Правку делают файлами,
последствия читают командой (docs/19-agent.md)
терминал снизу: те же инструменты, git, сборка и запросы к claude/codex,
открытый проект уходит потомкам через TILE2D_PROJECT
ПРИМЕР (samples/walk, цель tile2d_walk)
Второй потребитель движка. Игра и редактор самостоятельность движка
проверить не могут: редактор мира не рисует, а игра — то место, куда общий
код и уезжает незаметно.
ИГРА (src/)
Экстракшен: вес и два порога переноски, конечный боезапас, расходники
применяются сами, общая сумка отряда, трата опыта на улучшения.
Каждое из правил проверяется прогоном, а не глазами, и проверки поймали:
вес дважды не связывал ничего (мерил по бинтам прогона, а не по добыче);
боец с клинком не тратил найденные патроны при запасном стволе в сумке;
массив виджетов экрана переполнился и молча съел хвост схрона; проверка
улучшений сама ломала забег, вызывая NewRun посреди спуска.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 01:07:19 +07:00
|
|
|
|
int n = 0;
|
|
|
|
|
|
for (int i = 0; i < NUM_AGENTS; ++i)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!agents[i].alive) continue;
|
|
|
|
|
|
c += agents[i].pos;
|
|
|
|
|
|
++n;
|
|
|
|
|
|
}
|
|
|
|
|
|
return (n > 0) ? c / float(n) : anchor;
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void Squad::UpdateAnchorAndState(Vec2 moveWorldDir, bool sprint, const Tilemap& map, float dt)
|
|
|
|
|
|
{
|
|
|
|
|
|
anchorPrev = anchor;
|
|
|
|
|
|
|
Пистолет не пропадает, эвакуация откуда угодно, кнопки в 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>
2026-08-17 09:24:10 +07:00
|
|
|
|
// --- теснота ---------------------------------------------------------------
|
|
|
|
|
|
// Сколько свободного места вокруг ТЕЛ. Считается грубо: доля проходимых
|
|
|
|
|
|
// клеток в квадрате рядом. Этого хватает, чтобы отличить зал от коридора и
|
|
|
|
|
|
// от дверного проёма, а честный замер ширины прохода стоил бы поиска пути
|
|
|
|
|
|
// там, где его нет.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Считается ЗДЕСЬ, до всего остального: от тесноты зависят и доворот строя,
|
|
|
|
|
|
// и его ширина, и обе вещи нужны в этом же шаге.
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
const bool moving = LengthSq(moveWorldDir) > 1e-6f;
|
|
|
|
|
|
const AgentState prevState = state;
|
|
|
|
|
|
const Vec2 c = Centroid();
|
|
|
|
|
|
|
|
|
|
|
|
// --- натяжение поводка ----------------------------------------------------
|
|
|
|
|
|
// Поводок УПРУГИЙ, а не жёсткий. За мягкой границей курсор продолжает
|
|
|
|
|
|
// уходить, но идёт всё тяжелее, и до жёсткого предела не доходит никогда.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Жёсткий поводок здесь не годился принципиально: он мгновенно упирается,
|
|
|
|
|
|
// дистанция намертво встаёт в одно значение, и по ней уже нельзя отличить
|
|
|
|
|
|
// «подправил направление» от «тяну отряд вперёд изо всех сил». Упругий
|
|
|
|
|
|
// поводок превращает дистанцию в непрерывную величину: чем сильнее тянешь,
|
|
|
|
|
|
// тем дальше уезжает курсор и тем быстрее бегут за ним.
|
|
|
|
|
|
const float soft = std::max(0.2f, g_tune.anchorLeash);
|
|
|
|
|
|
const float hard = std::max(soft + 0.2f, g_tune.anchorLeashMax);
|
|
|
|
|
|
const float start = soft * Clampf(g_tune.urgeStartFrac, 0.0f, 0.95f);
|
|
|
|
|
|
const float dist = Distance(anchor, c);
|
|
|
|
|
|
|
|
|
|
|
|
float pull = 0.0f;
|
|
|
|
|
|
if (moving) pull = Clampf((dist - start) / (hard - start), 0.0f, 1.0f);
|
|
|
|
|
|
if (moving && sprint) pull = 1.0f; // Shift — приказ бежать, без уговоров
|
|
|
|
|
|
|
|
|
|
|
|
const float rate = (pull > urgency) ? g_tune.urgeAttack : g_tune.urgeRelease;
|
|
|
|
|
|
urgency += (pull - urgency) * (1.0f - std::exp(-std::max(0.01f, rate) * dt));
|
|
|
|
|
|
urgency = Clampf(urgency, 0.0f, 1.0f);
|
|
|
|
|
|
// Бег без стрельбы — либо по прямому приказу, либо когда курсор реально
|
|
|
|
|
|
// уехал: отряд догоняет и ему не до целей.
|
|
|
|
|
|
sprinting = (moving && sprint) || (urgency >= g_tune.sprintAt);
|
|
|
|
|
|
|
|
|
|
|
|
// --- сам курсор -----------------------------------------------------------
|
|
|
|
|
|
// У него есть разгон и торможение: мгновенная смена скорости — это и есть
|
|
|
|
|
|
// «топорно». Отпустил клавиши — курсор доезжает, а не втыкается в грунт.
|
|
|
|
|
|
const float over = Clampf((dist - soft) / (hard - soft), 0.0f, 1.0f);
|
|
|
|
|
|
const float dragK = std::max(0.08f, 1.0f - Clampf(g_tune.leashDrag, 0.0f, 1.0f) * over);
|
|
|
|
|
|
const float speed = (sprint ? g_tune.anchorSpeedFast : g_tune.anchorSpeed) * dragK;
|
|
|
|
|
|
|
|
|
|
|
|
const Vec2 want = moving ? Normalized(moveWorldDir, forward) * speed : Vec2{};
|
|
|
|
|
|
const Vec2 dv = want - anchorVel;
|
|
|
|
|
|
anchorVel += ClampLength(dv, std::max(0.1f, g_tune.anchorAccel) * dt);
|
|
|
|
|
|
anchor += anchorVel * dt;
|
|
|
|
|
|
|
Пистолет не пропадает, эвакуация откуда угодно, кнопки в 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>
2026-08-17 09:24:10 +07:00
|
|
|
|
// Ориентация строя ПОВОРАЧИВАЕТСЯ, а не щёлкает. Раньше она мгновенно
|
|
|
|
|
|
// равнялась направлению ввода, и на каждом повороте слоты клина
|
|
|
|
|
|
// перепрыгивали через весь отряд: бойцы бросали дорогу и бежали вбок,
|
|
|
|
|
|
// занимать новые места. Снаружи это и читалось как «строй тормозит» —
|
|
|
|
|
|
// отряд был занят перестроением, а не движением.
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
|
|
|
|
|
|
// Жёсткий предел. Сопротивление обычно не даёт до него дойти, но отряд может
|
|
|
|
|
|
// и застрять в стене — тогда курсор без предела уехал бы за полкарты.
|
|
|
|
|
|
const Vec2 d = anchor - c;
|
|
|
|
|
|
const float len = Length(d);
|
|
|
|
|
|
if (len > hard && len > 1e-5f)
|
|
|
|
|
|
{
|
|
|
|
|
|
const Vec2 n = d / len;
|
|
|
|
|
|
anchor = c + n * hard;
|
|
|
|
|
|
const float vn = Dot(anchorVel, n);
|
|
|
|
|
|
if (vn > 0.0f) anchorVel -= n * vn;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
map.ResolveCircle(anchor, g_tune.anchorRadius);
|
|
|
|
|
|
|
|
|
|
|
|
// --- состояние отряда ------------------------------------------------------
|
|
|
|
|
|
if (moving)
|
|
|
|
|
|
{
|
|
|
|
|
|
idleTimer = 0.0f;
|
|
|
|
|
|
state = AgentState::ADVANCING;
|
|
|
|
|
|
holdElapsed = 0.0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
idleTimer += dt;
|
|
|
|
|
|
if (idleTimer >= g_tune.holdDelay) state = AgentState::HOLDING;
|
|
|
|
|
|
if (state == AgentState::HOLDING) holdElapsed += dt;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- дыхание строя ---------------------------------------------------------
|
|
|
|
|
|
const float wantSpacing = (state == AgentState::HOLDING)
|
|
|
|
|
|
? g_tune.holdSpacing
|
|
|
|
|
|
: Lerpf(1.0f, g_tune.sprintSpacing, urgency);
|
Запуск грузит проект; автопилот --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>
2026-08-16 17:20:21 +07:00
|
|
|
|
// В тесноте строй И сжимается вбок, И вытягивается вдоль хода: это и есть
|
|
|
|
|
|
// колонна. Числа в tuning, чтобы крутились на живой игре.
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
const float wantStretch = Lerpf(1.0f, g_tune.sprintStretch, urgency);
|
Запуск грузит проект; автопилот --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>
2026-08-16 17:20:21 +07:00
|
|
|
|
const float tightSpacing = Lerpf(wantSpacing, g_tune.tightSpacing, tight);
|
|
|
|
|
|
const float tightStretch = Lerpf(wantStretch, g_tune.tightStretch, tight);
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
const float k = 1.0f - std::exp(-std::max(0.01f, g_tune.formMorphRate) * dt);
|
Запуск грузит проект; автопилот --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>
2026-08-16 17:20:21 +07:00
|
|
|
|
formSpacing += (tightSpacing - formSpacing) * k;
|
|
|
|
|
|
formStretch += (tightStretch - formStretch) * k;
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
|
|
|
|
|
|
if (state != prevState)
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int i = 0; i < NUM_AGENTS; ++i)
|
|
|
|
|
|
{
|
|
|
|
|
|
Agent& a = agents[i];
|
|
|
|
|
|
a.commitTimer = 0.0f;
|
|
|
|
|
|
// Встали — остаёмся на месте, пока решатель не найдёт лучше (6.5).
|
|
|
|
|
|
if (state == AgentState::HOLDING) a.postPos = a.pos;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (state == AgentState::HOLDING) holdElapsed = 0.0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < NUM_AGENTS; ++i) agents[i].state = state;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void Squad::UpdateMovement(const Tilemap& map, const LaneRegistry* lanes,
|
|
|
|
|
|
const std::vector<Target>& targets, float dt)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Скорость — не тумблер «шаг/рывок», а непрерывная величина от натяжения.
|
|
|
|
|
|
const float baseSpeed = MoveSpeed();
|
|
|
|
|
|
|
Поле путей: отставший возвращается в строй (M37)
Отряд из пяти не проходил первый этаж ни на одном сиде, а одиночка проходил.
Дело было не в ширине коридоров: у бойцов не было поиска пути вообще. Боец,
оказавшийся за стеной от своего слота, seek'ом тянулся В СТЕНУ, обход стены
толкал его обратно, и он замирал в углу навсегда.
Теперь у каждого бойца своё поле путей (BFS от клетки его цели) с натягиванием
верёвки: он берёт самую дальнюю точку пути, которую видит. Включается обход по
двум условиям сразу — боец упёрся (хочет идти и не едет) И потерял строй. Счётчик
упора сбрасывается по факту «цель видно», а не «поехал»: сброс по скорости давал
дребезг, и боец полз шаг за две секунды.
По дороге вскрылись две неверные МЕРКИ:
* критерий 11.6 считал колонной «разброс вдоль больше, чем поперёк» — и потому
награждал отряд, у которого двое навсегда остались по ту сторону стены. Теперь
колонна меряется самим проёмом: разом в горловине не больше двух, и прошли ВСЕ.
* зонд --wall читал «стены не нашлось» (ноль) как «стена на нулевом этаже» и
объявлял пятёрку, не сломавшуюся нигде, худшей из трёх.
Обе переписаны и проверены на сломанной сборке.
Замеры: --play 6 5 на восьми сидах 47/48 (было 0/48), --accept PASS, шаг 6.1 мкс,
--wall 30 одиночка D20 / трое D20 / пятеро D31 (не найдена), --descend 8/30,
hollow --descend 6, --ui-check, оба проекта --check — без ошибок.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 22:31:34 +07:00
|
|
|
|
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
for (int i = 0; i < NUM_AGENTS; ++i)
|
|
|
|
|
|
{
|
|
|
|
|
|
Agent& a = agents[i];
|
|
|
|
|
|
a.prevPos = a.pos;
|
Tile2D: движок отдельным продуктом, игра — проектом в нём (M11-M20)
Десять вех одним состоянием: разделять их пофайлово нечем, потому что
src/game.cpp менялся почти в каждой. Собирается начисто без предупреждений,
все проверки проходят.
ДВИЖОК (engine/, цель tile2d)
Каталог свой, и решение «это движок» принимается тем, куда положен файл.
Компилятор нашёл три зависимости движка от игры, и все сняты: размеры кадра
и карты уехали в engine/config.h, потолок населения этажа стал своим
(MAX_LEVEL_SPAWNS против игровой MAX_TARGETS), запасная точка старта стала
центром карты вместо позиции отряда.
engine/projection.* единственное место, где движок знает про изометрию;
видов сразу два — абстракция с одной реализацией ничем
не проверена и обычно оказывается неправильной
engine/project.* манифест проекта: пути, вид, кривая глубины
engine/app.* окно, кадр, fixed timestep — вместо двух копий цикла
engine/tilemap_draw движок научился показывать свою карту
РЕДАКТОР (editor/, цель tile2d_editor)
Открывает любой проект и ни одной игры по имени не знает.
--tool одиннадцать безоконных команд: комнаты, каталог, кривая, сборка
этажа, ASCII-карта, проверка набора. Правку делают файлами,
последствия читают командой (docs/19-agent.md)
терминал снизу: те же инструменты, git, сборка и запросы к claude/codex,
открытый проект уходит потомкам через TILE2D_PROJECT
ПРИМЕР (samples/walk, цель tile2d_walk)
Второй потребитель движка. Игра и редактор самостоятельность движка
проверить не могут: редактор мира не рисует, а игра — то место, куда общий
код и уезжает незаметно.
ИГРА (src/)
Экстракшен: вес и два порога переноски, конечный боезапас, расходники
применяются сами, общая сумка отряда, трата опыта на улучшения.
Каждое из правил проверяется прогоном, а не глазами, и проверки поймали:
вес дважды не связывал ничего (мерил по бинтам прогона, а не по добыче);
боец с клинком не тратил найденные патроны при запасном стволе в сумке;
массив виджетов экрана переполнился и молча съел хвост схрона; проверка
улучшений сама ломала забег, вызывая NewRun посреди спуска.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 01:07:19 +07:00
|
|
|
|
if (!a.alive) { a.vel = {}; continue; } // упавший лежит там, где упал
|
|
|
|
|
|
|
|
|
|
|
|
// Захваченного не двигает НИЧТО: ни строй, ни решатель, ни расступание
|
|
|
|
|
|
// из чужой линии. В этом вся цена захвата — боец выпал из геометрии
|
|
|
|
|
|
// отряда и стоит там, где его взяли, пока тварь не отпустит или не умрёт.
|
|
|
|
|
|
// Оружие при этом работает: захват держит ноги, а не руки.
|
Поле путей: отставший возвращается в строй (M37)
Отряд из пяти не проходил первый этаж ни на одном сиде, а одиночка проходил.
Дело было не в ширине коридоров: у бойцов не было поиска пути вообще. Боец,
оказавшийся за стеной от своего слота, seek'ом тянулся В СТЕНУ, обход стены
толкал его обратно, и он замирал в углу навсегда.
Теперь у каждого бойца своё поле путей (BFS от клетки его цели) с натягиванием
верёвки: он берёт самую дальнюю точку пути, которую видит. Включается обход по
двум условиям сразу — боец упёрся (хочет идти и не едет) И потерял строй. Счётчик
упора сбрасывается по факту «цель видно», а не «поехал»: сброс по скорости давал
дребезг, и боец полз шаг за две секунды.
По дороге вскрылись две неверные МЕРКИ:
* критерий 11.6 считал колонной «разброс вдоль больше, чем поперёк» — и потому
награждал отряд, у которого двое навсегда остались по ту сторону стены. Теперь
колонна меряется самим проёмом: разом в горловине не больше двух, и прошли ВСЕ.
* зонд --wall читал «стены не нашлось» (ноль) как «стена на нулевом этаже» и
объявлял пятёрку, не сломавшуюся нигде, худшей из трёх.
Обе переписаны и проверены на сломанной сборке.
Замеры: --play 6 5 на восьми сидах 47/48 (было 0/48), --accept PASS, шаг 6.1 мкс,
--wall 30 одиночка D20 / трое D20 / пятеро D31 (не найдена), --descend 8/30,
hollow --descend 6, --ui-check, оба проекта --check — без ошибок.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 22:31:34 +07:00
|
|
|
|
if (a.Grabbed()) { a.vel = {}; a.stuckFor = 0.0f; continue; }
|
|
|
|
|
|
|
|
|
|
|
|
const Vec2 rawGoal = GoalFor(i);
|
|
|
|
|
|
|
|
|
|
|
|
// Упёрся — веди по геометрии. Боец хочет идти (цель не под ногами), но
|
|
|
|
|
|
// скорость почти нулевая: значит прямая упирается в стену, и обход
|
|
|
|
|
|
// стены толкает обратно. Именно так отставший за углом замирал навсегда.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Счётчик сбрасывается НЕ по факту «поехал», а по факту «цель видно».
|
|
|
|
|
|
// Разница принципиальная: сброс по скорости давал дребезг — обход
|
|
|
|
|
|
// трогал бойца с места, поле тут же выключалось, боец упирался в ту же
|
|
|
|
|
|
// стену, и так по кругу со средней скоростью в шаг за две секунды.
|
|
|
|
|
|
// Пока цель за стеной, режим обхода ДЕРЖИТСЯ, и боец доходит по дуге.
|
|
|
|
|
|
const bool wantsMove = DistanceSq(a.pos, rawGoal) > g_tune.navGoalReached *
|
|
|
|
|
|
g_tune.navGoalReached;
|
|
|
|
|
|
if (!wantsMove || !map.RaycastBlocked(a.pos, rawGoal))
|
|
|
|
|
|
a.stuckFor = 0.0f;
|
|
|
|
|
|
else if (Length(a.vel) < g_tune.navStuckSpeed * baseSpeed)
|
|
|
|
|
|
a.stuckFor += dt;
|
|
|
|
|
|
|
|
|
|
|
|
// ЧТО именно держит бойца — вот вся развилка. Стена держит навсегда, и
|
|
|
|
|
|
// из-под неё надо выводить по геометрии. Спина соседа держит на секунду,
|
|
|
|
|
|
// и обходить её нельзя: в проходе очередь стоит именно так, и отряд,
|
|
|
|
|
|
// отправленный в обход своей же колонны, входит в горловину шеренгой
|
|
|
|
|
|
// (критерий 11.6 ловит это сразу).
|
|
|
|
|
|
//
|
|
|
|
|
|
// Пробовал развилку по расстоянию до цели — это угадайка: у очереди в
|
|
|
|
|
|
// проходе и у отставшего одиночки дистанции пересекаются, и любой порог
|
|
|
|
|
|
// ломал либо строй, либо проходимость.
|
|
|
|
|
|
// Кому обход положен: тому, кто УПЁРСЯ и при этом ПОТЕРЯЛ СТРОЙ.
|
|
|
|
|
|
// Второе условие важно не меньше первого — боец, стоящий на своём месте,
|
|
|
|
|
|
// обходить ничего не должен: он не потерялся, его держит бой.
|
|
|
|
|
|
Vec2 goal = rawGoal;
|
|
|
|
|
|
if (a.stuckFor > g_tune.navStuckTime &&
|
|
|
|
|
|
Distance(a.pos, SlotWorld(i)) > g_tune.straggleDist)
|
|
|
|
|
|
{
|
|
|
|
|
|
++navUses;
|
|
|
|
|
|
RebuildNav(nav[i], map, rawGoal);
|
|
|
|
|
|
goal = NavWaypoint(nav[i], map, a.pos, rawGoal);
|
|
|
|
|
|
}
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
|
|
|
|
|
|
const bool lunging = a.IsMelee() && a.melee.state == MeleeState::STRIKE;
|
|
|
|
|
|
|
|
|
|
|
|
// --- рывок клинка --------------------------------------------------
|
|
|
|
|
|
// В активном окне удара ось уже зафиксирована, и боец едет по ней.
|
|
|
|
|
|
// Seek/separation/расступание тут молчат: удар — это обязательство,
|
|
|
|
|
|
// а не предложение. Стены остаются: сквозь них не рвётся никто.
|
|
|
|
|
|
if (lunging)
|
|
|
|
|
|
{
|
|
|
|
|
|
const float speed = g_tune.meleeLungeSpeed;
|
|
|
|
|
|
const Vec2 dir = FromAngle(a.melee.dir);
|
|
|
|
|
|
Vec2 desired = dir * speed;
|
|
|
|
|
|
desired += WallAvoidForce(a, map, dir, speed) * g_tune.wWallAvoid;
|
|
|
|
|
|
IntegrateAgent(a, desired, speed, dt, map, g_tune.meleeLungeAccel);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- обычные слои, порядок 6.7 --------------------------------------
|
Tile2D: движок отдельным продуктом, игра — проектом в нём (M11-M20)
Десять вех одним состоянием: разделять их пофайлово нечем, потому что
src/game.cpp менялся почти в каждой. Собирается начисто без предупреждений,
все проверки проходят.
ДВИЖОК (engine/, цель tile2d)
Каталог свой, и решение «это движок» принимается тем, куда положен файл.
Компилятор нашёл три зависимости движка от игры, и все сняты: размеры кадра
и карты уехали в engine/config.h, потолок населения этажа стал своим
(MAX_LEVEL_SPAWNS против игровой MAX_TARGETS), запасная точка старта стала
центром карты вместо позиции отряда.
engine/projection.* единственное место, где движок знает про изометрию;
видов сразу два — абстракция с одной реализацией ничем
не проверена и обычно оказывается неправильной
engine/project.* манифест проекта: пути, вид, кривая глубины
engine/app.* окно, кадр, fixed timestep — вместо двух копий цикла
engine/tilemap_draw движок научился показывать свою карту
РЕДАКТОР (editor/, цель tile2d_editor)
Открывает любой проект и ни одной игры по имени не знает.
--tool одиннадцать безоконных команд: комнаты, каталог, кривая, сборка
этажа, ASCII-карта, проверка набора. Правку делают файлами,
последствия читают командой (docs/19-agent.md)
терминал снизу: те же инструменты, git, сборка и запросы к claude/codex,
открытый проект уходит потомкам через TILE2D_PROJECT
ПРИМЕР (samples/walk, цель tile2d_walk)
Второй потребитель движка. Игра и редактор самостоятельность движка
проверить не могут: редактор мира не рисует, а игра — то место, куда общий
код и уезжает незаметно.
ИГРА (src/)
Экстракшен: вес и два порога переноски, конечный боезапас, расходники
применяются сами, общая сумка отряда, трата опыта на улучшения.
Каждое из правил проверяется прогоном, а не глазами, и проверки поймали:
вес дважды не связывал ничего (мерил по бинтам прогона, а не по добыче);
боец с клинком не тратил найденные патроны при запасном стволе в сумке;
массив виджетов экрана переполнился и молча съел хвост схрона; проверка
улучшений сама ломала забег, вызывая NewRun посреди спуска.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 01:07:19 +07:00
|
|
|
|
// speedMul — это снаряжение: плита тормозит, шпоры разгоняют. Без
|
|
|
|
|
|
// снаряжения он равен 1, и движение остаётся прежним до последнего бита.
|
|
|
|
|
|
const float maxSpeed = baseSpeed * a.SpeedScale() * a.speedMul;
|
Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).
Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.
Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.
Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».
Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.
Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.
Документация — CLAUDE.md как оглавление, содержание в docs/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
|
|
|
|
|
|
|
|
|
|
Vec2 desired = SeekForce(a, goal, maxSpeed) * g_tune.wSeek;
|
|
|
|
|
|
desired += SeparationForce(a, agents.data(), NUM_AGENTS, maxSpeed) * g_tune.wSeparation;
|
|
|
|
|
|
// Замах — это обязательство. Выдёргивать бойца из чужой линии огня
|
|
|
|
|
|
// посреди него бессмысленно: стрелок всё равно не выстрелит (правило
|
|
|
|
|
|
// 6.3 запрещает), а удар сорвётся. Расступаться боец будет до и после.
|
|
|
|
|
|
if (lanes && !(a.IsMelee() && a.melee.Committed()))
|
|
|
|
|
|
desired += lanes->AvoidanceForce(i, a.pos, a.bodyRadius, maxSpeed) * g_tune.wLaneAvoid;
|
|
|
|
|
|
desired += TargetPushForce(a, targets, maxSpeed) * g_tune.wTargetPush;
|
|
|
|
|
|
desired += WallAvoidForce(a, map, Normalized(goal - a.pos), maxSpeed) * g_tune.wWallAvoid;
|
|
|
|
|
|
|
|
|
|
|
|
IntegrateAgent(a, desired, maxSpeed, dt, map);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|