squad-proto/src/game.cpp

1384 lines
66 KiB
C++
Raw Normal View History

Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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 "game.h"
Материалы и мастерская: вещь можно доработать (M27) Аффиксы сделали находку решением, но решением одноразовым: выпало как выпало. Материалы дают второй способ получить нужное — не «повезло», а «доработал». Пять материалов, пять операций, экран WORKSHOP. Главное правило экономики — материал добывается ТОЛЬКО В СВОЕЙ ПОЛОСЕ: био-ядра с плотских этажей, крио с мёрзлых. Это единственная причина возвращаться на средние глубины, когда рекорд ушёл вниз; без полос весь контент, кроме последнего акта, умирает в день прохождения. Материалы — валюта, а не предметы: не занимают ячеек, не весят, в схроне не лежат. Но теряются как добыча: зачисляются только при эвакуации. Три уточнения операций, каждое против конкретного обхода: перековка меняет только значения (иначе это новый бросок предмета в обход редкости), прививка не ставит уже стоящий тег (иначе «+урон» шесть раз), возвышается случайный аффикс (иначе все предметы сойдутся к одному). --tool craft поймал, что материалы бесплатны. С твари падало по её danger, и вылазка на восьмидесятом этаже приносила тысячу ядер при цене доводки вещи в шестьсот: 0.6 вылазки на предмет вместо восьми. Правило заменено на «одно ядро с мелочи, два с крупной» — стало 6.2. Сам инструмент при этом тоже считал неверно: брал доход из бюджета опасности этажа, хотя на этаж влезает не больше MAX_LEVEL_SPAWNS тварей. Бюджет уходит не в количество тварей, а в их силу, и доход упирается в потолок расстановки с сорокового этажа. Глубина даёт не количество материалов, а качество предметов — и вместе с ним цену доводки. Залом замерен на тысяче попыток: рассыпалось 11.6% при заявленных 12%, плата снимается всегда, слот теряется всегда. «У меня трижды не рассыпалось» не доказательство ни в какую сторону. Пункт меню WORKSHOP проверен --ui-check — той самой проверкой, которая появилась вчера из-за пункта UPGRADES, молча закрывавшего игру. Формат профиля 5: строка mat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 07:07:46 +07:00
#include "sim/craft.h"
Триггеры: четыре условных эффекта на аффиксах (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
#include "sim/trigger.h"
Статусы стихий и реакции (M22) Стихии сами по себе были только множителем урона: «верный ствол бьёт втрое больнее». Этого мало, чтобы они стали ЗНАНИЕМ, ради которого заводились — множитель одинаково работает у того, кто понимает систему, и у того, кто взял ствол посильнее. Статусы и реакции добавляют слой, который работает только у первого: наложить ДВЕ разные стихии на одну цель — это решение о составе отряда, а не о характеристиках оружия. sim/status.h правила, потолки стаков, реакции, числа sim/reactions.* наложение и запуск, единственное место UpdateStatuses тик горения и сгорание стаков, из UpdateTargets СИЛА РЕАКЦИИ СЧИТАЕТСЯ ОТ ГЛУБИНЫ, А НЕ ОТ УРОНА ОРУЖИЯ. Считай она от оружия — на сотом этаже стала бы округлением, и весь слой стихий превратился бы в украшение. От глубины она растёт сама, но только у того, кто умеет её запускать. ТРИ ОГРАНИЧИТЕЛЯ, И ВСЕ ПРОВЕРЯЮТСЯ потолок стаков один стак не значит ничего, полный набор меняет бой откат на цели вторая пара внутри 1.2 c даёт 0.0 урона запрет рекурсии урон ОТ реакции не кладёт статусов Третий не теоретический: без него цепочка «реакция бьёт по площади -> урон кладёт стихию -> новая пара -> реакция» уходит в самоповтор на первом же скоплении. Именно так ломаются все игры с триггерами. Четыре реакции из десяти. Остальные ждут систем, которых нет (крит, воскрешение, притяжение), и дописывать их «пока не работает» значило бы завести шесть мёртвых веток. Два отступления от документа, оба сознательные: кислота и дуга увеличивают получаемый урон вместо снятия брони и метки крита (у тварей нет ни брони как числа, ни крита), SUPERCOND кладёт стаки пустоты вместо отдельного среза сопротивлений — пустота в этой игре и означает «защита не работает». Шаг симуляции 2.9 мкс из бюджета 16666: статусы на 64 целях ничего не стоят. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:45:17 +07:00
#include "sim/reactions.h"
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
// Перевод экранных осей ввода в мировые — единственное место,
// где симуляционный слой касается изометрии (см. docs/02-architecture.md).
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
#include "engine/view2d.h"
#include "sim/enemy.h"
#include "sim/spawn_catalog.h"
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
{
// Снять с мира все пули разом. Пуля — сущность, поэтому просто очистить вектор
// нельзя: остались бы живые идентификаторы без компонентов.
void ClearBullets(ecs::World& world)
{
std::vector<Bullet>& bullets = world.All<Bullet>();
while (!bullets.empty()) world.Destroy(bullets.back().self);
}
} // namespace
Game::Game()
: targets(world.Store<Target>().data),
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
bullets(world.Store<Bullet>().data),
floorItems(world.Store<FloorItem>().data)
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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 Game::Init()
{
sprites.Generate();
Reset();
}
void Game::Reset()
{
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
BuildFloor(nullptr);
xp = 0.0f;
floorsCleared = 0;
lootTaken = 0;
Материалы и мастерская: вещь можно доработать (M27) Аффиксы сделали находку решением, но решением одноразовым: выпало как выпало. Материалы дают второй способ получить нужное — не «повезло», а «доработал». Пять материалов, пять операций, экран WORKSHOP. Главное правило экономики — материал добывается ТОЛЬКО В СВОЕЙ ПОЛОСЕ: био-ядра с плотских этажей, крио с мёрзлых. Это единственная причина возвращаться на средние глубины, когда рекорд ушёл вниз; без полос весь контент, кроме последнего акта, умирает в день прохождения. Материалы — валюта, а не предметы: не занимают ячеек, не весят, в схроне не лежат. Но теряются как добыча: зачисляются только при эвакуации. Три уточнения операций, каждое против конкретного обхода: перековка меняет только значения (иначе это новый бросок предмета в обход редкости), прививка не ставит уже стоящий тег (иначе «+урон» шесть раз), возвышается случайный аффикс (иначе все предметы сойдутся к одному). --tool craft поймал, что материалы бесплатны. С твари падало по её danger, и вылазка на восьмидесятом этаже приносила тысячу ядер при цене доводки вещи в шестьсот: 0.6 вылазки на предмет вместо восьми. Правило заменено на «одно ядро с мелочи, два с крупной» — стало 6.2. Сам инструмент при этом тоже считал неверно: брал доход из бюджета опасности этажа, хотя на этаж влезает не больше MAX_LEVEL_SPAWNS тварей. Бюджет уходит не в количество тварей, а в их силу, и доход упирается в потолок расстановки с сорокового этажа. Глубина даёт не количество материалов, а качество предметов — и вместе с ним цену доводки. Залом замерен на тысяче попыток: рассыпалось 11.6% при заявленных 12%, плата снимается всегда, слот теряется всегда. «У меня трижды не рассыпалось» не доказательство ни в какую сторону. Пункт меню WORKSHOP проверен --ui-check — той самой проверкой, которая появилась вчера из-за пункта UPGRADES, молча закрывавшего игру. Формат профиля 5: строка mat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 07:07:46 +07:00
for (int m = 0; m < MATERIAL_COUNT; ++m) runMaterials[m] = 0;
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
}
void Game::NewRun(int startDepth)
{
// Новый забег — новый сид. Сид крутится тем же линейным конгруэнтным шагом,
// что и кнопка SEED в редакторе: «следующий» обязано быть одним и тем же
// понятием в обоих приложениях, иначе сид, подобранный в редакторе, в игре
// не повторить.
levelSeed = levelSeed * 1664525u + 1013904223u;
// Начать глубже открытого чекпоинта нельзя: иначе достигнутая глубина
// перестаёт что-либо значить, а вместе с ней и весь риск.
depth = std::max(1, startDepth);
if (profile) depth = std::min(depth, profile->DeepestCheckpoint());
report = RunReport{};
if (profile) ++profile->runs;
Reset();
ApplyProfileLoadout();
}
void Game::Extract()
{
// Унести можно только то, что на ВЫЖИВШИХ. Павший забирает своё с собой —
// в этом вся ставка: не «доживи», а «доживи, имея что вынести».
HarvestToProfile(true);
FinishRun(RunOutcome::EXTRACTED);
}
void Game::WipeRun()
{
Правки по игре: начало, ставка, наёмники (M29) Семь изменений по итогам игры вживую. НАЧАЛО. Первый этаж встречал одинокого оперативника с ножом тремя тварями, из которых одна быстрая: бюджет 3.0 и бегуны с первого этажа. Стало 2.2 (ровно два бредущих), бегуны с четвёртого. Света прибавлено: обзор начинается с 1.30 вместо 1.0, у этажа появилась своя яркость пройденного (0.42 наверху против 0.13 на дне). Наверху ещё работает освещение, и хоррор первого этажа в том, что игрок один, а не в том, что не видно стен. Темнота это инструмент глубины, и тратить её на первый этаж значит не иметь запаса дальше. СТАВКА. Опыт платится за убийства и сразу: выносить надо ВЕЩИ. Вайп больше не отнимает всё - доезжает около половины добычи, и чем вещь реже, тем охотнее теряется. Прежнее "вайп не возвращает ничего" делало единственным разумным поведением уход наверх при первой царапине. ВЫХОД. Аномалия возврата переехала из точки входа к спуску: развилка "вниз или наверх" теперь там же, где этаж кончился. Обратный путь через растревоженный этаж ничего не решал - это был ещё раз тот же этаж. ЧЕКПОИНТЫ через 50 этажей вместо пяти: забег это долгий спуск, а не перебежка между сохранениями. НАЁМНИКИ. Кнопка "нанять за опыт" убрана, оперативники находятся в капсулах на этажах: в среднем раз в сотню этажей и не чаще раза в двадцать. У каждого свои три прибавки из одного бюджета - разница в характере, а не в ранге. Покупка была скучной ровно в том смысле, в каком скучна любая кнопка "стать сильнее": ничего не рассказывает и никуда не зовёт. ЗАМЕРЫ. Вайп на сотне повторов: доезжает 52% (обычное 69%, реликты 25%). Первая версия проверки считала одну сумку и выдала "обычного доехало 112%" - спасение забирает ещё и надетое. Опыт: банк 558 -> 578 за убийство без эвакуации. Капсула поднимает отряд до двух и даёт наёмнику его числа. Формат профиля 6: строки merc и rescued. Не сделано и обсуждается: ростер сверх пятерых, задания для свободных, пассивные бонусы от сидящих на базе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 08:09:26 +07:00
// Отряд полёг — но выносится ПОЛОВИНА добычи, а не ноль.
//
// Ноль был честным правилом и плохой игрой: вылазка, кончившаяся вайпом на
// тридцатом этаже, не оставляла ни следа, и единственным разумным поведением
// становилось уходить наверх при первой царапине. Половина сохраняет ставку
// (терять по-прежнему больно) и оставляет смысл идти глубже.
//
// ЧТО ИМЕННО теряется — решает редкость: чем вещь лучше, тем охотнее она
// роняется. Иначе половина всегда состояла бы из бинтов, а реликты доезжали
// бы домой сами.
if (profile)
{
SalvageAfterWipe();
profile->ResetLoadouts();
}
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
FinishRun(RunOutcome::WIPED);
}
Правки по игре: начало, ставка, наёмники (M29) Семь изменений по итогам игры вживую. НАЧАЛО. Первый этаж встречал одинокого оперативника с ножом тремя тварями, из которых одна быстрая: бюджет 3.0 и бегуны с первого этажа. Стало 2.2 (ровно два бредущих), бегуны с четвёртого. Света прибавлено: обзор начинается с 1.30 вместо 1.0, у этажа появилась своя яркость пройденного (0.42 наверху против 0.13 на дне). Наверху ещё работает освещение, и хоррор первого этажа в том, что игрок один, а не в том, что не видно стен. Темнота это инструмент глубины, и тратить её на первый этаж значит не иметь запаса дальше. СТАВКА. Опыт платится за убийства и сразу: выносить надо ВЕЩИ. Вайп больше не отнимает всё - доезжает около половины добычи, и чем вещь реже, тем охотнее теряется. Прежнее "вайп не возвращает ничего" делало единственным разумным поведением уход наверх при первой царапине. ВЫХОД. Аномалия возврата переехала из точки входа к спуску: развилка "вниз или наверх" теперь там же, где этаж кончился. Обратный путь через растревоженный этаж ничего не решал - это был ещё раз тот же этаж. ЧЕКПОИНТЫ через 50 этажей вместо пяти: забег это долгий спуск, а не перебежка между сохранениями. НАЁМНИКИ. Кнопка "нанять за опыт" убрана, оперативники находятся в капсулах на этажах: в среднем раз в сотню этажей и не чаще раза в двадцать. У каждого свои три прибавки из одного бюджета - разница в характере, а не в ранге. Покупка была скучной ровно в том смысле, в каком скучна любая кнопка "стать сильнее": ничего не рассказывает и никуда не зовёт. ЗАМЕРЫ. Вайп на сотне повторов: доезжает 52% (обычное 69%, реликты 25%). Первая версия проверки считала одну сумку и выдала "обычного доехало 112%" - спасение забирает ещё и надетое. Опыт: банк 558 -> 578 за убийство без эвакуации. Капсула поднимает отряд до двух и даёт наёмнику его числа. Формат профиля 6: строки merc и rescued. Не сделано и обсуждается: ростер сверх пятерых, задания для свободных, пассивные бонусы от сидящих на базе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 08:09:26 +07:00
// Шанс УЦЕЛЕТЬ по редкости. В среднем половина, но обычное доезжает часто,
// а реликт — редко: 0.68 / 0.52 / 0.38 / 0.24.
static float SalvageChance(Rarity r)
{
switch (r)
{
case Rarity::MARKED: return 0.52f;
case Rarity::PROTOTYPE: return 0.38f;
case Rarity::RELIC: return 0.24f;
default: return 0.68f;
}
}
void Game::SalvageAfterWipe()
{
if (!profile) return;
// Сид от состояния забега, а не от времени: тот же прогон обязан давать тот
// же итог, иначе ни воспроизвести жалобу, ни проверить правило.
uint32_t rng = uint32_t(levelSeed) * 2654435761u + uint32_t(depth) * 9176u + 7u;
auto roll = [&rng]() {
rng = rng * 1664525u + 1013904223u;
return float((rng >> 8) & 0xFFFFu) / 65536.0f;
};
report.itemsBrought = 0;
report.itemsLost = 0;
auto trySave = [&](const ItemInstance& it) {
if (it.Empty()) return;
if (roll() < SalvageChance(it.rarity) && profile->StashPush(it)) ++report.itemsBrought;
else ++report.itemsLost;
};
// Считается ВСЁ, что отряд нёс: и надетое павшими, и общая сумка. Разделять
// их тут незачем — выживших нет ни одного, и «на ком именно» была вещь,
// роли уже не играет.
for (int i = 0; i < NUM_AGENTS; ++i)
{
const Agent& a = squad.agents[i];
if (!a.present) continue;
if (const Inventory* inv = world.Get<Inventory>(a.self))
{
trySave(inv->weapon);
trySave(inv->armor);
trySave(inv->trinket);
}
}
for (int s = 0; s < SQUAD_BAG_SLOTS; ++s) trySave(squad.bag.slots[s]);
squad.bag.Clear();
// Материалы уходят целиком: они валюта, и делить их «по редкости» не по
// чему. Вайп обязан стоить чего-то безусловно.
}
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
void Game::FinishRun(RunOutcome outcome)
{
report.outcome = outcome;
report.depth = depth;
report.deepest = std::max(report.deepest, depth);
report.survivors = AliveAgents();
report.xp = xp;
if (!profile) { report.checkpoint = 1; return; }
if (outcome == RunOutcome::EXTRACTED)
{
++profile->extractions;
Правки по игре: начало, ставка, наёмники (M29) Семь изменений по итогам игры вживую. НАЧАЛО. Первый этаж встречал одинокого оперативника с ножом тремя тварями, из которых одна быстрая: бюджет 3.0 и бегуны с первого этажа. Стало 2.2 (ровно два бредущих), бегуны с четвёртого. Света прибавлено: обзор начинается с 1.30 вместо 1.0, у этажа появилась своя яркость пройденного (0.42 наверху против 0.13 на дне). Наверху ещё работает освещение, и хоррор первого этажа в том, что игрок один, а не в том, что не видно стен. Темнота это инструмент глубины, и тратить её на первый этаж значит не иметь запаса дальше. СТАВКА. Опыт платится за убийства и сразу: выносить надо ВЕЩИ. Вайп больше не отнимает всё - доезжает около половины добычи, и чем вещь реже, тем охотнее теряется. Прежнее "вайп не возвращает ничего" делало единственным разумным поведением уход наверх при первой царапине. ВЫХОД. Аномалия возврата переехала из точки входа к спуску: развилка "вниз или наверх" теперь там же, где этаж кончился. Обратный путь через растревоженный этаж ничего не решал - это был ещё раз тот же этаж. ЧЕКПОИНТЫ через 50 этажей вместо пяти: забег это долгий спуск, а не перебежка между сохранениями. НАЁМНИКИ. Кнопка "нанять за опыт" убрана, оперативники находятся в капсулах на этажах: в среднем раз в сотню этажей и не чаще раза в двадцать. У каждого свои три прибавки из одного бюджета - разница в характере, а не в ранге. Покупка была скучной ровно в том смысле, в каком скучна любая кнопка "стать сильнее": ничего не рассказывает и никуда не зовёт. ЗАМЕРЫ. Вайп на сотне повторов: доезжает 52% (обычное 69%, реликты 25%). Первая версия проверки считала одну сумку и выдала "обычного доехало 112%" - спасение забирает ещё и надетое. Опыт: банк 558 -> 578 за убийство без эвакуации. Капсула поднимает отряд до двух и даёт наёмнику его числа. Формат профиля 6: строки merc и rescued. Не сделано и обсуждается: ростер сверх пятерых, задания для свободных, пассивные бонусы от сидящих на базе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 08:09:26 +07:00
// Опыт здесь НЕ начисляется: он уже зачислен по убийствам (AwardKillXp).
// Материалы — по-прежнему ставка: донёс или потерял.
Материалы и мастерская: вещь можно доработать (M27) Аффиксы сделали находку решением, но решением одноразовым: выпало как выпало. Материалы дают второй способ получить нужное — не «повезло», а «доработал». Пять материалов, пять операций, экран WORKSHOP. Главное правило экономики — материал добывается ТОЛЬКО В СВОЕЙ ПОЛОСЕ: био-ядра с плотских этажей, крио с мёрзлых. Это единственная причина возвращаться на средние глубины, когда рекорд ушёл вниз; без полос весь контент, кроме последнего акта, умирает в день прохождения. Материалы — валюта, а не предметы: не занимают ячеек, не весят, в схроне не лежат. Но теряются как добыча: зачисляются только при эвакуации. Три уточнения операций, каждое против конкретного обхода: перековка меняет только значения (иначе это новый бросок предмета в обход редкости), прививка не ставит уже стоящий тег (иначе «+урон» шесть раз), возвышается случайный аффикс (иначе все предметы сойдутся к одному). --tool craft поймал, что материалы бесплатны. С твари падало по её danger, и вылазка на восьмидесятом этаже приносила тысячу ядер при цене доводки вещи в шестьсот: 0.6 вылазки на предмет вместо восьми. Правило заменено на «одно ядро с мелочи, два с крупной» — стало 6.2. Сам инструмент при этом тоже считал неверно: брал доход из бюджета опасности этажа, хотя на этаж влезает не больше MAX_LEVEL_SPAWNS тварей. Бюджет уходит не в количество тварей, а в их силу, и доход упирается в потолок расстановки с сорокового этажа. Глубина даёт не количество материалов, а качество предметов — и вместе с ним цену доводки. Залом замерен на тысяче попыток: рассыпалось 11.6% при заявленных 12%, плата снимается всегда, слот теряется всегда. «У меня трижды не рассыпалось» не доказательство ни в какую сторону. Пункт меню WORKSHOP проверен --ui-check — той самой проверкой, которая появилась вчера из-за пункта UPGRADES, молча закрывавшего игру. Формат профиля 5: строка mat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 07:07:46 +07:00
for (int m = 0; m < MATERIAL_COUNT; ++m) profile->material[m] += runMaterials[m];
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
}
else
{
++profile->wipes;
}
// Рекорд глубины засчитывается ЛЮБЫМ исходом. Дошёл — значит дошёл;
// требовать за чекпоинт ещё и удачную эвакуацию значило бы наказывать
// дважды за одну смерть.
report.newRecord = report.deepest > profile->maxDepth;
profile->maxDepth = std::max(profile->maxDepth, report.deepest);
report.checkpoint = profile->DeepestCheckpoint();
}
void Game::HarvestToProfile(bool onlySurvivors)
{
if (!profile) return;
report.itemsBrought = 0;
report.itemsLost = 0;
for (int i = 0; i < NUM_AGENTS; ++i)
{
const Agent& a = squad.agents[i];
const Inventory* inv = world.Get<Inventory>(a.self);
if (!inv) continue;
Отряд растёт с одного оперативника до пяти (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
// Пустой слот ничего не выносит и ничего не теряет: там некому.
if (!a.present) continue;
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
auto countCarried = [&](const Inventory& v) {
int n = 0;
if (v.weapon != ItemId::NONE) ++n;
if (v.armor != ItemId::NONE) ++n;
if (v.trinket != ItemId::NONE) ++n;
return n;
};
if (onlySurvivors && !a.alive)
{
report.itemsLost += countCarried(*inv);
// Слот павшего получает стартовый набор: следующая вылазка идёт
// впятером, и пустые руки одного из них — это не сложность,
// а неиграбельность.
profile->loadout[i] = Inventory{};
continue;
}
profile->loadout[i] = *inv;
report.itemsBrought += countCarried(*inv);
}
// Общая сумка уходит в схрон целиком, если из забега вообще кто-то вышел.
//
// Правило «унести можно только то, что на выживших» этим не ослаблено, оно
// стало точнее: НАДЕТОЕ павшего теряется по-прежнему, а общая ноша на нём
// не висела — её тащил отряд. Вайп по-прежнему не выносит ничего: тогда
// выживших нет вовсе, и сумку некому донести.
const bool anySurvivor = AliveAgents() > 0;
for (int s = 0; s < SQUAD_BAG_SLOTS; ++s)
{
Редкости и аффиксы: находка стала решением (M25) Предмет перестал быть строкой каталога. ItemInstance несёт редкость и до шести аффиксов, и «карабин» с первого этажа больше не равен «карабину» с сорокового. Четыре редкости, десять тегов, тир и бюджет от глубины. Аффиксы не заводят новых каналов влияния: каждый тег ложится в то, что уже есть — урон оружия, смесь стихий, пробитие, четыре числа бойца, ресурсы вылазки. У предмета по-прежнему нет ручки к правилу «не бить своих», и появиться ей неоткуда. ItemInstance неявно приводится к ItemId, поэтому полторы сотни мест, которым нужен ВИД вещи, остались как были. Цена решения — присваивание в ItemId молча теряет рулоны; таких мест три (подбор, схрон, запись профиля), и все они работают с экземпляром целиком. Замеры поймали три расхождения дизайн-документа с самим собой: * формула редкости противоречила таблице рядом с ней: 2.7% находок с аффиксами на первом этаже против 22%. Выбрана формула, наклон поднят до схождения с таблицей на дне (12% обычных на сотом); * бюджет пускал восемь высших аффиксов вместо обещанных шести; * предельная вещь собиралась к семидесятому этажу вместо сотого. Числа пересчитаны, документ исправлен: 6·cost(9) = 294 <= B(100) = 295 < 7·cost(9) = 343. Предельная вещь существует ровно на дне и ровно одна. Проверка в --descend меряет не бросок, а ПРОВОДКУ до бойца — там, где дважды умирали вес и стихии: аффиксы: урон 12.0 -> 18.0, здоровье 100 -> 140, броня 0.10 -> 0.25, пробитие 0.20, KIN 1.00 -> 0.60, CRY 0.40, сумма долей 1.000 Последнее число не украшение: конверсия обязана переводить урон, а не добавлять седьмую долю сверх единицы. Попутно, на глубине 30 и только там: * «зачистка поля» в проверках не держалась — у твари есть откат воскрешения, и с нулевым таймером она оживала тем же шагом. Лечение вне схватки и расход света на стоянке молча мерили бой. На восьмом этаже тварей мало, и проверка проходила по удаче; * порог «стоянка бесплатна» стоял в единицах заряда, а тот же шаг стоит дороже с глубиной. Переведён в тайлы — и сразу показал, что отряд в строю проходит 0.8 тайла за две секунды «стоянки». Свет теперь списывается только в ADVANCING: топтание и драка бесплатны, тратит РАЗВЕДКА. Без этого таймер возвращался через чёрный ход. Формат профиля 4: токен предмета вида rifle@relic+dmg7,0:0.63. Миграции не требует — у старых токенов нет хвоста после '@'. --accept PASS без изменений: в песочнице находок нет вовсе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:35:00 +07:00
// ЭКЗЕМПЛЯР, а не вид: через ItemId вещь доехала бы до схрона без
// аффиксов, то есть игрок вынес бы реликт, а на базе нашёл обычную
// куртку. Неявная конверсия молчит, поэтому такие места пишутся явно.
const ItemInstance& id = squad.bag.slots[s];
if (id.Empty()) continue;
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 (anySurvivor && profile->StashPush(id)) ++report.itemsBrought;
else ++report.itemsLost;
}
squad.bag.Clear();
// Пустой слот павшего добивается стартовым комплектом. Делается через
// ResetLoadouts только для тех, у кого ничего нет: иначе выжившие
Отряд растёт с одного оперативника до пяти (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
// потеряли бы найденное. Ненанятые слоты набивать нечем и незачем — но и
// мешать они не мешают: их снаряжение никто не наденет.
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
Profile fresh;
fresh.ResetLoadouts();
for (int i = 0; i < NUM_AGENTS; ++i)
if (profile->loadout[i].weapon == ItemId::NONE) profile->loadout[i] = fresh.loadout[i];
}
void Game::ApplyProfileLoadout()
{
if (!profile) return;
for (int i = 0; i < NUM_AGENTS; ++i)
{
Agent& a = squad.agents[i];
Inventory* inv = world.Get<Inventory>(a.self);
if (!inv) continue;
*inv = profile->loadout[i];
Правки по игре: начало, ставка, наёмники (M29) Семь изменений по итогам игры вживую. НАЧАЛО. Первый этаж встречал одинокого оперативника с ножом тремя тварями, из которых одна быстрая: бюджет 3.0 и бегуны с первого этажа. Стало 2.2 (ровно два бредущих), бегуны с четвёртого. Света прибавлено: обзор начинается с 1.30 вместо 1.0, у этажа появилась своя яркость пройденного (0.42 наверху против 0.13 на дне). Наверху ещё работает освещение, и хоррор первого этажа в том, что игрок один, а не в том, что не видно стен. Темнота это инструмент глубины, и тратить её на первый этаж значит не иметь запаса дальше. СТАВКА. Опыт платится за убийства и сразу: выносить надо ВЕЩИ. Вайп больше не отнимает всё - доезжает около половины добычи, и чем вещь реже, тем охотнее теряется. Прежнее "вайп не возвращает ничего" делало единственным разумным поведением уход наверх при первой царапине. ВЫХОД. Аномалия возврата переехала из точки входа к спуску: развилка "вниз или наверх" теперь там же, где этаж кончился. Обратный путь через растревоженный этаж ничего не решал - это был ещё раз тот же этаж. ЧЕКПОИНТЫ через 50 этажей вместо пяти: забег это долгий спуск, а не перебежка между сохранениями. НАЁМНИКИ. Кнопка "нанять за опыт" убрана, оперативники находятся в капсулах на этажах: в среднем раз в сотню этажей и не чаще раза в двадцать. У каждого свои три прибавки из одного бюджета - разница в характере, а не в ранге. Покупка была скучной ровно в том смысле, в каком скучна любая кнопка "стать сильнее": ничего не рассказывает и никуда не зовёт. ЗАМЕРЫ. Вайп на сотне повторов: доезжает 52% (обычное 69%, реликты 25%). Первая версия проверки считала одну сумку и выдала "обычного доехало 112%" - спасение забирает ещё и надетое. Опыт: банк 558 -> 578 за убийство без эвакуации. Капсула поднимает отряд до двух и даёт наёмнику его числа. Формат профиля 6: строки merc и rescued. Не сделано и обсуждается: ростер сверх пятерых, задания для свободных, пассивные бонусы от сидящих на базе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 08:09:26 +07:00
a.merc = profile->merc[i];
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
// Ствол в руках живёт ещё и отдельной сущностью-оружием: без этого
// боец выходит с карабином по умолчанию, что бы ни лежало в профиле.
EquipWeapon(world, a, inv->weapon);
// Запас здоровья зависит от надетого (броня, оберег), и посчитан он
// будет только на первом шаге. Без явной синхронизации боец выходит
// на этаж с прежними ста единицами при полутора сотнях максимума —
// то есть уже раненым, ничего не сделав.
SyncLoadout(world, a);
a.hp = a.maxHp;
}
}
void Game::Descend()
{
// Отряд переезжает КАК ЕСТЬ: здоровье, оружие и сумки. В этом весь забег —
// спуск обязан ощущаться как продолжение, а не как новый заход.
SquadCarry carry;
SnapshotSquad(carry);
++depth;
++floorsCleared;
report.deepest = std::max(report.deepest, depth);
BuildFloor(&carry);
}
// -----------------------------------------------------------------------------
// Что переезжает между этажами. Отдельная структура, а не «не трогать мир»:
// Reset пересоздаёт весь ecs::World, и по-другому сохранить бойцов нельзя без
// того, чтобы половина систем начала различать «новый мир» и «тот же мир».
// -----------------------------------------------------------------------------
void Game::SnapshotSquad(SquadCarry& out) const
{
for (int i = 0; i < NUM_AGENTS; ++i)
{
const Agent& a = squad.agents[i];
out.alive[i] = a.alive;
out.hp[i] = a.hp;
out.ammo[i] = a.ammoReserve;
if (const Inventory* inv = world.Get<Inventory>(a.self)) out.inv[i] = *inv;
}
out.valid = true;
}
void Game::RestoreSquad(const SquadCarry& carry)
{
if (!carry.valid) return;
for (int i = 0; i < NUM_AGENTS; ++i)
{
Agent& a = squad.agents[i];
if (Inventory* inv = world.Get<Inventory>(a.self))
{
*inv = carry.inv[i];
// Ствол в руках — часть инвентаря, но живёт ещё и отдельной
// сущностью-оружием. Без этого боец приезжает на новый этаж с
// карабином по умолчанию, что бы он ни нашёл этажом выше.
EquipWeapon(world, a, inv->weapon);
}
// Павший остаётся павшим: воскрешение на лестнице обесценило бы
// единственную настоящую потерю в забеге.
a.alive = carry.alive[i];
a.hp = a.alive ? std::max(1.0f, carry.hp[i]) : 0.0f;
a.ammoReserve = carry.ammo[i];
}
}
void Game::BuildFloor(const SquadCarry* carry)
{
// Этаж из комнат — только если набор подключён снаружи. Не собрался
// (пустой набор, битые комнаты) — откатываемся на захардкоженную карту:
// прототип обязан запускаться на голом репозитории.
levelGenerated = false;
levelUnknownSpawns = 0;
atExit = false;
Vec2 start{SQUAD_START_X, SQUAD_START_Y};
const FloorSpec spec = DescribeFloor(depth);
if (rooms && catalog && rooms->Count() > 0 &&
GenerateLevel(*rooms, *catalog, spec, levelSeed, map, level))
{
levelGenerated = true;
start = level.squadStart;
}
else
{
map.Generate();
level = GeneratedLevel{};
level.floor = spec;
}
Отряд растёт с одного оперативника до пяти (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
// Сколько слотов занято. Из профиля, потому что размер отряда — это
// накопленный результат, а не состояние вылазки. Нет профиля (приёмка,
// dev-прогоны) — все пять, как было всегда: критерии меряются на пятерых.
squad.roster = profile ? std::max(1, std::min(NUM_AGENTS, profile->squadSize)) : NUM_AGENTS;
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
world.Clear(); // бойцы, оружие, мишени, пули — всё разом
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
squad.Init(world, start);
if (carry) RestoreSquad(*carry);
// Собранный этаж населяет СЕБЯ САМ: захардкоженная расстановка мишеней
// и тварей относится к другой карте, и мешать их — верный способ получить
// тварь в стене на пустом месте.
if (levelGenerated)
{
int unknownLoot = 0;
SpawnFromLevel(world, level.spawns, map, &levelUnknownSpawns);
Редкости и аффиксы: находка стала решением (M25) Предмет перестал быть строкой каталога. ItemInstance несёт редкость и до шести аффиксов, и «карабин» с первого этажа больше не равен «карабину» с сорокового. Четыре редкости, десять тегов, тир и бюджет от глубины. Аффиксы не заводят новых каналов влияния: каждый тег ложится в то, что уже есть — урон оружия, смесь стихий, пробитие, четыре числа бойца, ресурсы вылазки. У предмета по-прежнему нет ручки к правилу «не бить своих», и появиться ей неоткуда. ItemInstance неявно приводится к ItemId, поэтому полторы сотни мест, которым нужен ВИД вещи, остались как были. Цена решения — присваивание в ItemId молча теряет рулоны; таких мест три (подбор, схрон, запись профиля), и все они работают с экземпляром целиком. Замеры поймали три расхождения дизайн-документа с самим собой: * формула редкости противоречила таблице рядом с ней: 2.7% находок с аффиксами на первом этаже против 22%. Выбрана формула, наклон поднят до схождения с таблицей на дне (12% обычных на сотом); * бюджет пускал восемь высших аффиксов вместо обещанных шести; * предельная вещь собиралась к семидесятому этажу вместо сотого. Числа пересчитаны, документ исправлен: 6·cost(9) = 294 <= B(100) = 295 < 7·cost(9) = 343. Предельная вещь существует ровно на дне и ровно одна. Проверка в --descend меряет не бросок, а ПРОВОДКУ до бойца — там, где дважды умирали вес и стихии: аффиксы: урон 12.0 -> 18.0, здоровье 100 -> 140, броня 0.10 -> 0.25, пробитие 0.20, KIN 1.00 -> 0.60, CRY 0.40, сумма долей 1.000 Последнее число не украшение: конверсия обязана переводить урон, а не добавлять седьмую долю сверх единицы. Попутно, на глубине 30 и только там: * «зачистка поля» в проверках не держалась — у твари есть откат воскрешения, и с нулевым таймером она оживала тем же шагом. Лечение вне схватки и расход света на стоянке молча мерили бой. На восьмом этаже тварей мало, и проверка проходила по удаче; * порог «стоянка бесплатна» стоял в единицах заряда, а тот же шаг стоит дороже с глубиной. Переведён в тайлы — и сразу показал, что отряд в строю проходит 0.8 тайла за две секунды «стоянки». Свет теперь списывается только в ADVANCING: топтание и драка бесплатны, тратит РАЗВЕДКА. Без этого таймер возвращался через чёрный ход. Формат профиля 4: токен предмета вида rifle@relic+dmg7,0:0.63. Миграции не требует — у старых токенов нет хвоста после '@'. --accept PASS без изменений: в песочнице находок нет вовсе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:35:00 +07:00
SpawnLootFromLevel(world, level.loot, depth, levelSeed, &unknownLoot);
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
levelUnknownSpawns += unknownLoot;
Правки по игре: начало, ставка, наёмники (M29) Семь изменений по итогам игры вживую. НАЧАЛО. Первый этаж встречал одинокого оперативника с ножом тремя тварями, из которых одна быстрая: бюджет 3.0 и бегуны с первого этажа. Стало 2.2 (ровно два бредущих), бегуны с четвёртого. Света прибавлено: обзор начинается с 1.30 вместо 1.0, у этажа появилась своя яркость пройденного (0.42 наверху против 0.13 на дне). Наверху ещё работает освещение, и хоррор первого этажа в том, что игрок один, а не в том, что не видно стен. Темнота это инструмент глубины, и тратить её на первый этаж значит не иметь запаса дальше. СТАВКА. Опыт платится за убийства и сразу: выносить надо ВЕЩИ. Вайп больше не отнимает всё - доезжает около половины добычи, и чем вещь реже, тем охотнее теряется. Прежнее "вайп не возвращает ничего" делало единственным разумным поведением уход наверх при первой царапине. ВЫХОД. Аномалия возврата переехала из точки входа к спуску: развилка "вниз или наверх" теперь там же, где этаж кончился. Обратный путь через растревоженный этаж ничего не решал - это был ещё раз тот же этаж. ЧЕКПОИНТЫ через 50 этажей вместо пяти: забег это долгий спуск, а не перебежка между сохранениями. НАЁМНИКИ. Кнопка "нанять за опыт" убрана, оперативники находятся в капсулах на этажах: в среднем раз в сотню этажей и не чаще раза в двадцать. У каждого свои три прибавки из одного бюджета - разница в характере, а не в ранге. Покупка была скучной ровно в том смысле, в каком скучна любая кнопка "стать сильнее": ничего не рассказывает и никуда не зовёт. ЗАМЕРЫ. Вайп на сотне повторов: доезжает 52% (обычное 69%, реликты 25%). Первая версия проверки считала одну сумку и выдала "обычного доехало 112%" - спасение забирает ещё и надетое. Опыт: банк 558 -> 578 за убийство без эвакуации. Капсула поднимает отряд до двух и даёт наёмнику его числа. Формат профиля 6: строки merc и rescued. Не сделано и обсуждается: ростер сверх пятерых, задания для свободных, пассивные бонусы от сидящих на базе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 08:09:26 +07:00
MaybeSpawnCapsule();
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
}
else
{
SpawnTargets(world, combatMode, &map);
}
// Снаряжение раздаётся только тем, кто пришёл ни с чем: переезжающий отряд
// уже одет тем, что нашёл, и выдавать ему стартовый комплект заново
// означало бы стирать весь смысл находок.
//
// Есть профиль — одеваемся из него: это и есть «в чём вернулись с прошлой
// вылазки». Нет профиля (приёмка, dev-прогоны) — прежний фиксированный
// комплект, чтобы замеры не поехали.
if (!carry)
{
if (profile) ApplyProfileLoadout();
else if (combatMode) EquipKits();
// Улучшения профиля — сюда же: они часть того, с чем отряд выходит на
// вылазку. Нет профиля (приёмка, dev-прогоны) — нет и прибавок.
squad.carryBonus = 0.0f;
squad.sightBonus = 0.0f;
squad.hpBonus = 0.0f;
float ammoBonus = 0.0f;
if (profile)
{
squad.carryBonus = UPGRADE_CARRY_STEP * float(profile->UpgradeLevel(Upgrade::CARRY));
squad.sightBonus = UPGRADE_SIGHT_STEP * float(profile->UpgradeLevel(Upgrade::SIGHT));
squad.hpBonus = UPGRADE_VIGOR_STEP * float(profile->UpgradeLevel(Upgrade::VIGOR));
ammoBonus = UPGRADE_AMMO_STEP * float(profile->UpgradeLevel(Upgrade::AMMO));
}
// Запас патронов выдаётся вместе со снаряжением и только в БОЮ.
// В песочнице приёмки его нет вовсе (-1 = бесконечно): критерии меряют
// правила стрельбы, а не то, на сколько минут хватило патронов.
Отряд растёт с одного оперативника до пяти (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
// Патроны выдаются ЛЮДЯМ. Пустому слоту запас не нужен, и выдай его
// туда — отряд из одного оперативника выходил бы с пятикратным
// боезапасом, разложенным по четырём никому.
Редкости и аффиксы: находка стала решением (M25) Предмет перестал быть строкой каталога. ItemInstance несёт редкость и до шести аффиксов, и «карабин» с первого этажа больше не равен «карабину» с сорокового. Четыре редкости, десять тегов, тир и бюджет от глубины. Аффиксы не заводят новых каналов влияния: каждый тег ложится в то, что уже есть — урон оружия, смесь стихий, пробитие, четыре числа бойца, ресурсы вылазки. У предмета по-прежнему нет ручки к правилу «не бить своих», и появиться ей неоткуда. ItemInstance неявно приводится к ItemId, поэтому полторы сотни мест, которым нужен ВИД вещи, остались как были. Цена решения — присваивание в ItemId молча теряет рулоны; таких мест три (подбор, схрон, запись профиля), и все они работают с экземпляром целиком. Замеры поймали три расхождения дизайн-документа с самим собой: * формула редкости противоречила таблице рядом с ней: 2.7% находок с аффиксами на первом этаже против 22%. Выбрана формула, наклон поднят до схождения с таблицей на дне (12% обычных на сотом); * бюджет пускал восемь высших аффиксов вместо обещанных шести; * предельная вещь собиралась к семидесятому этажу вместо сотого. Числа пересчитаны, документ исправлен: 6·cost(9) = 294 <= B(100) = 295 < 7·cost(9) = 343. Предельная вещь существует ровно на дне и ровно одна. Проверка в --descend меряет не бросок, а ПРОВОДКУ до бойца — там, где дважды умирали вес и стихии: аффиксы: урон 12.0 -> 18.0, здоровье 100 -> 140, броня 0.10 -> 0.25, пробитие 0.20, KIN 1.00 -> 0.60, CRY 0.40, сумма долей 1.000 Последнее число не украшение: конверсия обязана переводить урон, а не добавлять седьмую долю сверх единицы. Попутно, на глубине 30 и только там: * «зачистка поля» в проверках не держалась — у твари есть откат воскрешения, и с нулевым таймером она оживала тем же шагом. Лечение вне схватки и расход света на стоянке молча мерили бой. На восьмом этаже тварей мало, и проверка проходила по удаче; * порог «стоянка бесплатна» стоял в единицах заряда, а тот же шаг стоит дороже с глубиной. Переведён в тайлы — и сразу показал, что отряд в строю проходит 0.8 тайла за две секунды «стоянки». Свет теперь списывается только в ADVANCING: топтание и драка бесплатны, тратит РАЗВЕДКА. Без этого таймер возвращался через чёрный ход. Формат профиля 4: токен предмета вида rifle@relic+dmg7,0:0.63. Миграции не требует — у старых токенов нет хвоста после '@'. --accept PASS без изменений: в песочнице находок нет вовсе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:35:00 +07:00
//
// Аффикс ROUNDS прибавляется ЛИЧНО тому, у кого он выкатан: запас
// патронов личный, в отличие от света. Складывать его в общий котёл
// значило бы, что найденная кем-то одним разгрузка кормит весь отряд.
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
for (int i = 0; i < NUM_AGENTS; ++i)
Отряд растёт с одного оперативника до пяти (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
{
Agent& a = squad.agents[i];
Редкости и аффиксы: находка стала решением (M25) Предмет перестал быть строкой каталога. ItemInstance несёт редкость и до шести аффиксов, и «карабин» с первого этажа больше не равен «карабину» с сорокового. Четыре редкости, десять тегов, тир и бюджет от глубины. Аффиксы не заводят новых каналов влияния: каждый тег ложится в то, что уже есть — урон оружия, смесь стихий, пробитие, четыре числа бойца, ресурсы вылазки. У предмета по-прежнему нет ручки к правилу «не бить своих», и появиться ей неоткуда. ItemInstance неявно приводится к ItemId, поэтому полторы сотни мест, которым нужен ВИД вещи, остались как были. Цена решения — присваивание в ItemId молча теряет рулоны; таких мест три (подбор, схрон, запись профиля), и все они работают с экземпляром целиком. Замеры поймали три расхождения дизайн-документа с самим собой: * формула редкости противоречила таблице рядом с ней: 2.7% находок с аффиксами на первом этаже против 22%. Выбрана формула, наклон поднят до схождения с таблицей на дне (12% обычных на сотом); * бюджет пускал восемь высших аффиксов вместо обещанных шести; * предельная вещь собиралась к семидесятому этажу вместо сотого. Числа пересчитаны, документ исправлен: 6·cost(9) = 294 <= B(100) = 295 < 7·cost(9) = 343. Предельная вещь существует ровно на дне и ровно одна. Проверка в --descend меряет не бросок, а ПРОВОДКУ до бойца — там, где дважды умирали вес и стихии: аффиксы: урон 12.0 -> 18.0, здоровье 100 -> 140, броня 0.10 -> 0.25, пробитие 0.20, KIN 1.00 -> 0.60, CRY 0.40, сумма долей 1.000 Последнее число не украшение: конверсия обязана переводить урон, а не добавлять седьмую долю сверх единицы. Попутно, на глубине 30 и только там: * «зачистка поля» в проверках не держалась — у твари есть откат воскрешения, и с нулевым таймером она оживала тем же шагом. Лечение вне схватки и расход света на стоянке молча мерили бой. На восьмом этаже тварей мало, и проверка проходила по удаче; * порог «стоянка бесплатна» стоял в единицах заряда, а тот же шаг стоит дороже с глубиной. Переведён в тайлы — и сразу показал, что отряд в строю проходит 0.8 тайла за две секунды «стоянки». Свет теперь списывается только в ADVANCING: топтание и драка бесплатны, тратит РАЗВЕДКА. Без этого таймер возвращался через чёрный ход. Формат профиля 4: токен предмета вида rifle@relic+dmg7,0:0.63. Миграции не требует — у старых токенов нет хвоста после '@'. --accept PASS без изменений: в песочнице находок нет вовсе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:35:00 +07:00
a.ammoReserve =
combatMode ? (a.present ? int(g_tune.startAmmo + ammoBonus + a.mods.ammo) : 0) : -1;
Отряд растёт с одного оперативника до пяти (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
}
Свет как расходник: заряд тратится за путь, а не за время (M23) Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он меняется с тварями местами. Батарея встаёт сама, как бинт. Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия считала по секундам, то есть была таймером, которого в игре нет по прямому требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как секундомер, только без циферблата, и думать над этажом стало бы дорого. За расстояние получается тот же размен, что и с весом, но с другой стороны: вес делает дорогим «унести всё», свет — «обшарить всё». Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды. Проверка в --descend меряет правило целиком, и первая её строка не про баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже был неправ — делил потраченное на смещение отряда, а не на пройденный путь, и показывал 1.55 за тайл при 1.10 в каталоге. свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска обзор 1.00 -> 0.35 в темноте, батарея встала сама Хватает ли заряда на этаж считается в этажах и по средней длине забега: путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы удачу, а не баланс. --accept PASS без изменений: в песочнице света бесконечно, как и патронов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
Редкости и аффиксы: находка стала решением (M25) Предмет перестал быть строкой каталога. ItemInstance несёт редкость и до шести аффиксов, и «карабин» с первого этажа больше не равен «карабину» с сорокового. Четыре редкости, десять тегов, тир и бюджет от глубины. Аффиксы не заводят новых каналов влияния: каждый тег ложится в то, что уже есть — урон оружия, смесь стихий, пробитие, четыре числа бойца, ресурсы вылазки. У предмета по-прежнему нет ручки к правилу «не бить своих», и появиться ей неоткуда. ItemInstance неявно приводится к ItemId, поэтому полторы сотни мест, которым нужен ВИД вещи, остались как были. Цена решения — присваивание в ItemId молча теряет рулоны; таких мест три (подбор, схрон, запись профиля), и все они работают с экземпляром целиком. Замеры поймали три расхождения дизайн-документа с самим собой: * формула редкости противоречила таблице рядом с ней: 2.7% находок с аффиксами на первом этаже против 22%. Выбрана формула, наклон поднят до схождения с таблицей на дне (12% обычных на сотом); * бюджет пускал восемь высших аффиксов вместо обещанных шести; * предельная вещь собиралась к семидесятому этажу вместо сотого. Числа пересчитаны, документ исправлен: 6·cost(9) = 294 <= B(100) = 295 < 7·cost(9) = 343. Предельная вещь существует ровно на дне и ровно одна. Проверка в --descend меряет не бросок, а ПРОВОДКУ до бойца — там, где дважды умирали вес и стихии: аффиксы: урон 12.0 -> 18.0, здоровье 100 -> 140, броня 0.10 -> 0.25, пробитие 0.20, KIN 1.00 -> 0.60, CRY 0.40, сумма долей 1.000 Последнее число не украшение: конверсия обязана переводить урон, а не добавлять седьмую долю сверх единицы. Попутно, на глубине 30 и только там: * «зачистка поля» в проверках не держалась — у твари есть откат воскрешения, и с нулевым таймером она оживала тем же шагом. Лечение вне схватки и расход света на стоянке молча мерили бой. На восьмом этаже тварей мало, и проверка проходила по удаче; * порог «стоянка бесплатна» стоял в единицах заряда, а тот же шаг стоит дороже с глубиной. Переведён в тайлы — и сразу показал, что отряд в строю проходит 0.8 тайла за две секунды «стоянки». Свет теперь списывается только в ADVANCING: топтание и драка бесплатны, тратит РАЗВЕДКА. Без этого таймер возвращался через чёрный ход. Формат профиля 4: токен предмета вида rifle@relic+dmg7,0:0.63. Миграции не требует — у старых токенов нет хвоста после '@'. --accept PASS без изменений: в песочнице находок нет вовсе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:35:00 +07:00
// Аффикс LIGHT — наоборот, ОБЩИЙ: фонарь у отряда один (sim/items.h),
// и «мой фонарь держит дольше» тут не бывает.
float lightBonus = 0.0f;
for (int i = 0; i < NUM_AGENTS; ++i)
if (squad.agents[i].present) lightBonus += squad.agents[i].mods.light;
Свет как расходник: заряд тратится за путь, а не за время (M23) Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он меняется с тварями местами. Батарея встаёт сама, как бинт. Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия считала по секундам, то есть была таймером, которого в игре нет по прямому требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как секундомер, только без циферблата, и думать над этажом стало бы дорого. За расстояние получается тот же размен, что и с весом, но с другой стороны: вес делает дорогим «унести всё», свет — «обшарить всё». Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды. Проверка в --descend меряет правило целиком, и первая её строка не про баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже был неправ — делил потраченное на смещение отряда, а не на пройденный путь, и показывал 1.55 за тайл при 1.10 в каталоге. свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска обзор 1.00 -> 0.35 в темноте, батарея встала сама Хватает ли заряда на этаж считается в этажах и по средней длине забега: путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы удачу, а не баланс. --accept PASS без изменений: в песочнице света бесконечно, как и патронов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
// Свежий фонарь выдаётся на ВЫЛАЗКУ, а не на этаж: заряд переезжает
// вниз вместе с отрядом (Squad живёт дольше уровня), и в этом весь
// смысл — на четвёртом этаже светить уже почти нечем.
// В песочнице приёмки света бесконечно (-1), как и патронов.
Редкости и аффиксы: находка стала решением (M25) Предмет перестал быть строкой каталога. ItemInstance несёт редкость и до шести аффиксов, и «карабин» с первого этажа больше не равен «карабину» с сорокового. Четыре редкости, десять тегов, тир и бюджет от глубины. Аффиксы не заводят новых каналов влияния: каждый тег ложится в то, что уже есть — урон оружия, смесь стихий, пробитие, четыре числа бойца, ресурсы вылазки. У предмета по-прежнему нет ручки к правилу «не бить своих», и появиться ей неоткуда. ItemInstance неявно приводится к ItemId, поэтому полторы сотни мест, которым нужен ВИД вещи, остались как были. Цена решения — присваивание в ItemId молча теряет рулоны; таких мест три (подбор, схрон, запись профиля), и все они работают с экземпляром целиком. Замеры поймали три расхождения дизайн-документа с самим собой: * формула редкости противоречила таблице рядом с ней: 2.7% находок с аффиксами на первом этаже против 22%. Выбрана формула, наклон поднят до схождения с таблицей на дне (12% обычных на сотом); * бюджет пускал восемь высших аффиксов вместо обещанных шести; * предельная вещь собиралась к семидесятому этажу вместо сотого. Числа пересчитаны, документ исправлен: 6·cost(9) = 294 <= B(100) = 295 < 7·cost(9) = 343. Предельная вещь существует ровно на дне и ровно одна. Проверка в --descend меряет не бросок, а ПРОВОДКУ до бойца — там, где дважды умирали вес и стихии: аффиксы: урон 12.0 -> 18.0, здоровье 100 -> 140, броня 0.10 -> 0.25, пробитие 0.20, KIN 1.00 -> 0.60, CRY 0.40, сумма долей 1.000 Последнее число не украшение: конверсия обязана переводить урон, а не добавлять седьмую долю сверх единицы. Попутно, на глубине 30 и только там: * «зачистка поля» в проверках не держалась — у твари есть откат воскрешения, и с нулевым таймером она оживала тем же шагом. Лечение вне схватки и расход света на стоянке молча мерили бой. На восьмом этаже тварей мало, и проверка проходила по удаче; * порог «стоянка бесплатна» стоял в единицах заряда, а тот же шаг стоит дороже с глубиной. Переведён в тайлы — и сразу показал, что отряд в строю проходит 0.8 тайла за две секунды «стоянки». Свет теперь списывается только в ADVANCING: топтание и драка бесплатны, тратит РАЗВЕДКА. Без этого таймер возвращался через чёрный ход. Формат профиля 4: токен предмета вида rifle@relic+dmg7,0:0.63. Миграции не требует — у старых токенов нет хвоста после '@'. --accept PASS без изменений: в песочнице находок нет вовсе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:35:00 +07:00
squad.lightCharge = combatMode ? (LIGHT_FULL + lightBonus) : -1.0f;
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
lanes.Clear();
metrics.Reset();
events.Clear();
aim = AimState{};
for (bool& v : lastSolveValid) v = false;
simTime = 0.0f;
}
void Game::PlaceSquad(Vec2 at, bool clumped)
{
squad.anchor = at;
squad.anchorPrev = at;
squad.anchorVel = {};
squad.state = AgentState::HOLDING;
squad.idleTimer = g_tune.holdDelay;
squad.holdElapsed = 0.0f;
squad.urgency = 0.0f;
squad.sprinting = false;
squad.formSpacing = 1.0f;
squad.formStretch = 1.0f;
for (int i = 0; i < NUM_AGENTS; ++i)
{
Agent& a = squad.agents[i];
a.pos = clumped ? at : squad.SlotWorld(i);
map.ResolveCircle(a.pos, a.bodyRadius);
a.prevPos = a.pos;
a.postPos = a.pos;
a.vel = {};
a.commitTimer = 0.0f;
a.state = AgentState::HOLDING;
a.currentTargetId = -1;
a.hasLane = false;
// Недорубленный взмах пережил бы расстановку и уехал в следующий
// сценарий приёмки. Параметры при этом не трогаем — они из tuning.
a.melee.state = MeleeState::IDLE;
a.melee.phase = 0.0f;
a.melee.cooldown = 0.0f;
a.melee.targetId = -1;
a.melee.inReach = false;
a.melee.arcBlockedByAlly = false;
a.melee.hitMask = 0;
a.melee.comboStep = 0; // связка тоже не должна переезжать в новый сценарий
a.melee.comboTimer = 0.0f;
// Магазин тоже относится к расстановке: недозаряженный ствол переехал бы
// в следующий сценарий приёмки вместе с бойцом.
a.gun.ammo = a.gun.def.magazine;
a.gun.reloadTimer = 0.0f;
a.gun.cooldown = 0.0f;
a.gun.recoilDeg = 0.0f;
a.gun.shotSeq = 0;
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
// Расстановка — это НАЧАЛО сценария. Павший боец, переехавший в него
// трупом, испортил бы и замеры, и игру.
a.alive = true;
a.hp = a.maxHp;
a.hurtTimer = 0.0f;
// Захват — тоже состояние сценария: боец, переехавший в новый сценарий
// в лапах хвата, просто не сдвинулся бы с места.
a.grabbedTimer = 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
}
ClearBullets(world);
lanes.Clear();
metrics.Reset();
events.Clear();
simTime = 0.0f;
}
SolverContext Game::MakeContext() const
{
SolverContext ctx;
ctx.map = &map;
ctx.agents = squad.agents.data();
ctx.agentCount = NUM_AGENTS;
ctx.targets = &targets;
ctx.lanes = &lanes;
ctx.anchor = squad.anchor;
ctx.assignedTargetId = aim.manual ? aim.assignedTargetId : -1;
return ctx;
}
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
// -----------------------------------------------------------------------------
// Расходники. Одно место на все: бинт, стим и пачка патронов — это «предмет,
// который тратится, когда стало нужно», и разводить их по разным системам
// значило бы трижды написать одно правило.
//
// Лечиться можно только ВНЕ схватки: перевязка посреди боя обесценила бы урон,
// который тварь только что нанесла. Патроны — наоборот, набиваются когда угодно:
// боец с пустым стволом и пачкой в сумке, стоящий столбом, выглядел бы поломкой.
Свет как расходник: заряд тратится за путь, а не за время (M23) Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он меняется с тварями местами. Батарея встаёт сама, как бинт. Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия считала по секундам, то есть была таймером, которого в игре нет по прямому требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как секундомер, только без циферблата, и думать над этажом стало бы дорого. За расстояние получается тот же размен, что и с весом, но с другой стороны: вес делает дорогим «унести всё», свет — «обшарить всё». Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды. Проверка в --descend меряет правило целиком, и первая её строка не про баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже был неправ — делил потраченное на смещение отряда, а не на пройденный путь, и показывал 1.55 за тайл при 1.10 в каталоге. свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска обзор 1.00 -> 0.35 в темноте, батарея встала сама Хватает ли заряда на этаж считается в этажах и по средней длине забега: путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы удачу, а не баланс. --accept PASS без изменений: в песочнице света бесконечно, как и патронов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
// Батарея ставится ровно в тот шаг, когда фонарь сел: держать её в сумке при
// погасшем свете отряду незачем, а темнота ради темноты — не решение игрока.
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
// -----------------------------------------------------------------------------
void Game::UseConsumables(float dt)
{
(void)dt;
for (int i = 0; i < NUM_AGENTS; ++i)
{
Agent& a = squad.agents[i];
if (!a.alive) continue;
const Inventory* inv = world.Get<Inventory>(a.self);
if (!inv) continue;
// Насколько близко ближайшая живая тварь: лечиться при ней нельзя.
float nearest = 1e9f;
for (const Target& t : targets)
{
if (!t.alive || t.IsProp()) continue;
nearest = std::min(nearest, Distance(t.pos, a.pos));
}
const bool hurt = a.hp < a.maxHp * g_tune.healAtFrac;
const bool safe = nearest > g_tune.healSafeDist;
Свет как расходник: заряд тратится за путь, а не за время (M23) Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он меняется с тварями местами. Батарея встаёт сама, как бинт. Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия считала по секундам, то есть была таймером, которого в игре нет по прямому требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как секундомер, только без циферблата, и думать над этажом стало бы дорого. За расстояние получается тот же размен, что и с весом, но с другой стороны: вес делает дорогим «унести всё», свет — «обшарить всё». Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды. Проверка в --descend меряет правило целиком, и первая её строка не про баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже был неправ — делил потраченное на смещение отряда, а не на пройденный путь, и показывал 1.55 за тайл при 1.10 в каталоге. свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска обзор 1.00 -> 0.35 в темноте, батарея встала сама Хватает ли заряда на этаж считается в этажах и по средней длине забега: путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы удачу, а не баланс. --accept PASS без изменений: в песочнице света бесконечно, как и патронов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
// Свет — ресурс ОТРЯДА, поэтому условие пересчитывается на каждом бойце:
// поставил батарею первый — второй уже видит заряженный фонарь и свою
// не тратит.
const bool dark = squad.LightOut();
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
// Патроны нужны тому, у кого ВООБЩЕ есть ствол, — не только тому, кто
// держит его прямо сейчас. Боец с клинком в руках и запасным карабином
// в сумке иначе таскал бы найденную пачку до конца забега мёртвым
// грузом, а это ровно то, от чего расходники и лечились.
bool ownsGun = !a.IsMelee();
for (int s = 0; s < SQUAD_BAG_SLOTS && !ownsGun; ++s)
if (ItemDefOf(squad.bag.slots[s]).kind == ItemKind::FIREARM) ownsGun = true;
const bool dry = (a.ammoReserve == 0) && ownsGun;
for (int s = 0; s < SQUAD_BAG_SLOTS; ++s)
{
const ItemDef d = ItemDefOf(squad.bag.slots[s]);
if (d.kind != ItemKind::CONSUMABLE) continue;
const bool healsNow = (d.heal > 0.0f) && hurt && safe;
const bool loadsNow = (d.rounds > 0) && dry;
Свет как расходник: заряд тратится за путь, а не за время (M23) Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он меняется с тварями местами. Батарея встаёт сама, как бинт. Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия считала по секундам, то есть была таймером, которого в игре нет по прямому требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как секундомер, только без циферблата, и думать над этажом стало бы дорого. За расстояние получается тот же размен, что и с весом, но с другой стороны: вес делает дорогим «унести всё», свет — «обшарить всё». Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды. Проверка в --descend меряет правило целиком, и первая её строка не про баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже был неправ — делил потраченное на смещение отряда, а не на пройденный путь, и показывал 1.55 за тайл при 1.10 в каталоге. свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска обзор 1.00 -> 0.35 в темноте, батарея встала сама Хватает ли заряда на этаж считается в этажах и по средней длине забега: путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы удачу, а не баланс. --accept PASS без изменений: в песочнице света бесконечно, как и патронов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
const bool lightsNow = (d.charge > 0.0f) && dark;
if (!healsNow && !loadsNow && !lightsNow) continue;
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 (healsNow) a.hp = std::min(a.maxHp, a.hp + d.heal);
if (loadsNow) a.ammoReserve += d.rounds;
Свет как расходник: заряд тратится за путь, а не за время (M23) Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он меняется с тварями местами. Батарея встаёт сама, как бинт. Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия считала по секундам, то есть была таймером, которого в игре нет по прямому требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как секундомер, только без циферблата, и думать над этажом стало бы дорого. За расстояние получается тот же размен, что и с весом, но с другой стороны: вес делает дорогим «унести всё», свет — «обшарить всё». Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды. Проверка в --descend меряет правило целиком, и первая её строка не про баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже был неправ — делил потраченное на смещение отряда, а не на пройденный путь, и показывал 1.55 за тайл при 1.10 в каталоге. свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска обзор 1.00 -> 0.35 в темноте, батарея встала сама Хватает ли заряда на этаж считается в этажах и по средней длине забега: путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы удачу, а не баланс. --accept PASS без изменений: в песочнице света бесконечно, как и патронов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
if (lightsNow) squad.lightCharge = d.charge;
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
squad.bag.slots[s] = ItemId::NONE;
events.Push(CombatEventKind::ITEM_USED, i, a.pos, Vec2{});
++metrics.itemsUsed;
break; // не больше одного расходника за шаг: тратить пачку разом незачем
}
}
}
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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 Game::Step(float dt)
{
if (in.setFormation >= 0 && in.setFormation < int(Formation::COUNT))
squad.formation = Formation(in.setFormation);
// --- 0. статы снаряжения: с сущностей-оружия на бойцов ---------------------
// Одна система в начале шага. Всё остальное дальше читает уже готовые числа
// и не лазает в ECS: ai/ про мир сущностей не знает вообще.
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
// Общая ноша делится поровну на живых. Потерять бойца — значит взвалить
// его долю на остальных: обратно наверх отряд пойдёт медленнее, чем шёл
// сюда, и это не побочный эффект, а то, ради чего сумка общая.
const float bagShare = squad.bag.Load() / float(squad.Carriers());
Свет как расходник: заряд тратится за путь, а не за время (M23) Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он меняется с тварями местами. Батарея встаёт сама, как бинт. Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия считала по секундам, то есть была таймером, которого в игре нет по прямому требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как секундомер, только без циферблата, и думать над этажом стало бы дорого. За расстояние получается тот же размен, что и с весом, но с другой стороны: вес делает дорогим «унести всё», свет — «обшарить всё». Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды. Проверка в --descend меряет правило целиком, и первая её строка не про баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже был неправ — делил потраченное на смещение отряда, а не на пройденный путь, и показывал 1.55 за тайл при 1.10 в каталоге. свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска обзор 1.00 -> 0.35 в темноте, батарея встала сама Хватает ли заряда на этаж считается в этажах и по средней длине забега: путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы удачу, а не баланс. --accept PASS без изменений: в песочнице света бесконечно, как и патронов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
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
SquadBuffs buffs;
buffs.sight = squad.sightBonus;
buffs.hp = squad.hpBonus;
buffs.carry = squad.carryBonus;
Свет как расходник: заряд тратится за путь, а не за время (M23) Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он меняется с тварями местами. Батарея встаёт сама, как бинт. Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия считала по секундам, то есть была таймером, которого в игре нет по прямому требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как секундомер, только без циферблата, и думать над этажом стало бы дорого. За расстояние получается тот же размен, что и с весом, но с другой стороны: вес делает дорогим «унести всё», свет — «обшарить всё». Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды. Проверка в --descend меряет правило целиком, и первая её строка не про баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже был неправ — делил потраченное на смещение отряда, а не на пройденный путь, и показывал 1.55 за тайл при 1.10 в каталоге. свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска обзор 1.00 -> 0.35 в темноте, батарея встала сама Хватает ли заряда на этаж считается в этажах и по средней длине забега: путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы удачу, а не баланс. --accept PASS без изменений: в песочнице света бесконечно, как и патронов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
buffs.lightOut = squad.LightOut();
Триггеры: четыре условных эффекта на аффиксах (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
// Ярость живёт таймером и гаснет сама. Множитель урона от неё уезжает в
// SyncLoadout вместе с аффиксами: своего канала у триггера нет.
squad.rageTimer = std::max(0.0f, squad.rageTimer - dt);
buffs.rage = (squad.rageTimer > 0.0f) ? SQUAD_RAGE_DAMAGE : 0.0f;
// Откаты триггеров и стаки рывка тикают у КАЖДОГО бойца, включая павших:
// иначе набранное на прошлом этаже держалось бы вечно.
for (int i = 0; i < NUM_AGENTS; ++i) squad.agents[i].trig.Tick(dt);
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
for (int i = 0; i < NUM_AGENTS; ++i) SyncLoadout(world, squad.agents[i], bagShare, buffs);
for (int i = 0; i < NUM_AGENTS; ++i)
squad.agents[i].hurtTimer = std::max(0.0f, squad.agents[i].hurtTimer - dt);
// --- 0.5 расходники ---------------------------------------------------------
// Ручных действий в игре нет (спек 5), поэтому расходники боец применяет
// САМ. Иначе бинт остаётся числом в каталоге и мёртвым грузом в сумке —
// ровно тем, чем он и был до этой вехи.
UseConsumables(dt);
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
UpdateTargets(targets, dt);
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
// Твари ходят и бьют ДО того, как отряд принимает решения: боец реагирует
// на то, что видит сейчас, а не на прошлый кадр.
Свет как расходник: заряд тратится за путь, а не за время (M23) Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он меняется с тварями местами. Батарея встаёт сама, как бинт. Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия считала по секундам, то есть была таймером, которого в игре нет по прямому требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как секундомер, только без циферблата, и думать над этажом стало бы дорого. За расстояние получается тот же размен, что и с весом, но с другой стороны: вес делает дорогим «унести всё», свет — «обшарить всё». Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды. Проверка в --descend меряет правило целиком, и первая её строка не про баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже был неправ — делил потраченное на смещение отряда, а не на пройденный путь, и показывал 1.55 за тайл при 1.10 в каталоге. свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска обзор 1.00 -> 0.35 в темноте, батарея встала сама Хватает ли заряда на этаж считается в этажах и по средней длине забега: путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы удачу, а не баланс. --accept PASS без изменений: в песочнице света бесконечно, как и патронов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
UpdateEnemies(world, map, squad.agents.data(), NUM_AGENTS, targets, dt, &events,
Триггеры: четыре условных эффекта на аффиксах (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
buffs.lightOut ? DARK_AGGRO_MUL : 1.0f, &squad.rageTimer);
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
// --- 1. якорь и состояние отряда -----------------------------------------
// Экранные оси ввода -> мировое направление. Дальше изометрии нет нигде.
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
const Vec2 moveWorld = v2d::ScreenDirToWorld(in.moveScreen);
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
squad.UpdateAnchorAndState(moveWorld, in.sprint, map, dt);
// --- 2. движение (реестр линий — с предыдущего шага, задержка 1/60 с) -----
Свет как расходник: заряд тратится за путь, а не за время (M23) Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он меняется с тварями местами. Батарея встаёт сама, как бинт. Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия считала по секундам, то есть была таймером, которого в игре нет по прямому требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как секундомер, только без циферблата, и думать над этажом стало бы дорого. За расстояние получается тот же размен, что и с весом, но с другой стороны: вес делает дорогим «унести всё», свет — «обшарить всё». Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды. Проверка в --descend меряет правило целиком, и первая её строка не про баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже был неправ — делил потраченное на смещение отряда, а не на пройденный путь, и показывал 1.55 за тайл при 1.10 в каталоге. свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска обзор 1.00 -> 0.35 в темноте, батарея встала сама Хватает ли заряда на этаж считается в этажах и по средней длине забега: путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы удачу, а не баланс. --accept PASS без изменений: в песочнице света бесконечно, как и патронов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
const Vec2 lightFrom = squad.Centroid();
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
squad.UpdateMovement(map, enableLaneAvoid ? &lanes : nullptr, targets, dt);
Свет как расходник: заряд тратится за путь, а не за время (M23) Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он меняется с тварями местами. Батарея встаёт сама, как бинт. Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия считала по секундам, то есть была таймером, которого в игре нет по прямому требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как секундомер, только без циферблата, и думать над этажом стало бы дорого. За расстояние получается тот же размен, что и с весом, но с другой стороны: вес делает дорогим «унести всё», свет — «обшарить всё». Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды. Проверка в --descend меряет правило целиком, и первая её строка не про баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже был неправ — делил потраченное на смещение отряда, а не на пройденный путь, и показывал 1.55 за тайл при 1.10 в каталоге. свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска обзор 1.00 -> 0.35 в темноте, батарея встала сама Хватает ли заряда на этаж считается в этажах и по средней длине забега: путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы удачу, а не баланс. --accept PASS без изменений: в песочнице света бесконечно, как и патронов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
// --- 2.5 свет -------------------------------------------------------------
Редкости и аффиксы: находка стала решением (M25) Предмет перестал быть строкой каталога. ItemInstance несёт редкость и до шести аффиксов, и «карабин» с первого этажа больше не равен «карабину» с сорокового. Четыре редкости, десять тегов, тир и бюджет от глубины. Аффиксы не заводят новых каналов влияния: каждый тег ложится в то, что уже есть — урон оружия, смесь стихий, пробитие, четыре числа бойца, ресурсы вылазки. У предмета по-прежнему нет ручки к правилу «не бить своих», и появиться ей неоткуда. ItemInstance неявно приводится к ItemId, поэтому полторы сотни мест, которым нужен ВИД вещи, остались как были. Цена решения — присваивание в ItemId молча теряет рулоны; таких мест три (подбор, схрон, запись профиля), и все они работают с экземпляром целиком. Замеры поймали три расхождения дизайн-документа с самим собой: * формула редкости противоречила таблице рядом с ней: 2.7% находок с аффиксами на первом этаже против 22%. Выбрана формула, наклон поднят до схождения с таблицей на дне (12% обычных на сотом); * бюджет пускал восемь высших аффиксов вместо обещанных шести; * предельная вещь собиралась к семидесятому этажу вместо сотого. Числа пересчитаны, документ исправлен: 6·cost(9) = 294 <= B(100) = 295 < 7·cost(9) = 343. Предельная вещь существует ровно на дне и ровно одна. Проверка в --descend меряет не бросок, а ПРОВОДКУ до бойца — там, где дважды умирали вес и стихии: аффиксы: урон 12.0 -> 18.0, здоровье 100 -> 140, броня 0.10 -> 0.25, пробитие 0.20, KIN 1.00 -> 0.60, CRY 0.40, сумма долей 1.000 Последнее число не украшение: конверсия обязана переводить урон, а не добавлять седьмую долю сверх единицы. Попутно, на глубине 30 и только там: * «зачистка поля» в проверках не держалась — у твари есть откат воскрешения, и с нулевым таймером она оживала тем же шагом. Лечение вне схватки и расход света на стоянке молча мерили бой. На восьмом этаже тварей мало, и проверка проходила по удаче; * порог «стоянка бесплатна» стоял в единицах заряда, а тот же шаг стоит дороже с глубиной. Переведён в тайлы — и сразу показал, что отряд в строю проходит 0.8 тайла за две секунды «стоянки». Свет теперь списывается только в ADVANCING: топтание и драка бесплатны, тратит РАЗВЕДКА. Без этого таймер возвращался через чёрный ход. Формат профиля 4: токен предмета вида rifle@relic+dmg7,0:0.63. Миграции не требует — у старых токенов нет хвоста после '@'. --accept PASS без изменений: в песочнице находок нет вовсе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:35:00 +07:00
// Фонарь садится за ПРОЙДЕННЫЙ ПУТЬ, и путь меряется по центру отряда, а не
Свет как расходник: заряд тратится за путь, а не за время (M23) Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он меняется с тварями местами. Батарея встаёт сама, как бинт. Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия считала по секундам, то есть была таймером, которого в игре нет по прямому требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как секундомер, только без циферблата, и думать над этажом стало бы дорого. За расстояние получается тот же размен, что и с весом, но с другой стороны: вес делает дорогим «унести всё», свет — «обшарить всё». Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды. Проверка в --descend меряет правило целиком, и первая её строка не про баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже был неправ — делил потраченное на смещение отряда, а не на пройденный путь, и показывал 1.55 за тайл при 1.10 в каталоге. свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска обзор 1.00 -> 0.35 в темноте, батарея встала сама Хватает ли заряда на этаж считается в этажах и по средней длине забега: путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы удачу, а не баланс. --accept PASS без изменений: в песочнице света бесконечно, как и патронов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
// по якорю: якорь — это курсор, он ходит и тогда, когда отряд упёрся в
// стену, и светил бы фонарь на месте.
//
Редкости и аффиксы: находка стала решением (M25) Предмет перестал быть строкой каталога. ItemInstance несёт редкость и до шести аффиксов, и «карабин» с первого этажа больше не равен «карабину» с сорокового. Четыре редкости, десять тегов, тир и бюджет от глубины. Аффиксы не заводят новых каналов влияния: каждый тег ложится в то, что уже есть — урон оружия, смесь стихий, пробитие, четыре числа бойца, ресурсы вылазки. У предмета по-прежнему нет ручки к правилу «не бить своих», и появиться ей неоткуда. ItemInstance неявно приводится к ItemId, поэтому полторы сотни мест, которым нужен ВИД вещи, остались как были. Цена решения — присваивание в ItemId молча теряет рулоны; таких мест три (подбор, схрон, запись профиля), и все они работают с экземпляром целиком. Замеры поймали три расхождения дизайн-документа с самим собой: * формула редкости противоречила таблице рядом с ней: 2.7% находок с аффиксами на первом этаже против 22%. Выбрана формула, наклон поднят до схождения с таблицей на дне (12% обычных на сотом); * бюджет пускал восемь высших аффиксов вместо обещанных шести; * предельная вещь собиралась к семидесятому этажу вместо сотого. Числа пересчитаны, документ исправлен: 6·cost(9) = 294 <= B(100) = 295 < 7·cost(9) = 343. Предельная вещь существует ровно на дне и ровно одна. Проверка в --descend меряет не бросок, а ПРОВОДКУ до бойца — там, где дважды умирали вес и стихии: аффиксы: урон 12.0 -> 18.0, здоровье 100 -> 140, броня 0.10 -> 0.25, пробитие 0.20, KIN 1.00 -> 0.60, CRY 0.40, сумма долей 1.000 Последнее число не украшение: конверсия обязана переводить урон, а не добавлять седьмую долю сверх единицы. Попутно, на глубине 30 и только там: * «зачистка поля» в проверках не держалась — у твари есть откат воскрешения, и с нулевым таймером она оживала тем же шагом. Лечение вне схватки и расход света на стоянке молча мерили бой. На восьмом этаже тварей мало, и проверка проходила по удаче; * порог «стоянка бесплатна» стоял в единицах заряда, а тот же шаг стоит дороже с глубиной. Переведён в тайлы — и сразу показал, что отряд в строю проходит 0.8 тайла за две секунды «стоянки». Свет теперь списывается только в ADVANCING: топтание и драка бесплатны, тратит РАЗВЕДКА. Без этого таймер возвращался через чёрный ход. Формат профиля 4: токен предмета вида rifle@relic+dmg7,0:0.63. Миграции не требует — у старых токенов нет хвоста после '@'. --accept PASS без изменений: в песочнице находок нет вовсе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:35:00 +07:00
// Считается ТОЛЬКО в ADVANCING, то есть когда игрок ведёт отряд. И это не
// мелочь: в строю бойцы постоянно подтягиваются к слотам и расталкиваются,
// и центр отряда за две секунды «стоянки» проходит около 0.8 тайла. Списывай
// мы и это — заряд таял бы, пока игрок думает над картой, то есть вернулся
// бы ровно тот таймер, ради отсутствия которого расход и считается за путь
// (docs/17-extraction.md). Замер поймал это на трёх глубинах сразу.
//
// Перестановки в бою при этом бесплатны — и правильно: свет тратит РАЗВЕДКА,
// а не драка.
if (squad.lightCharge > 0.0f && squad.state == AgentState::ADVANCING)
Свет как расходник: заряд тратится за путь, а не за время (M23) Второй ресурс вылазки после патронов. Фонарь садится, погас — обзор падает до 35%, а твари замечают отряд в 1.35 раза дальше: отряд не слепнет, он меняется с тварями местами. Батарея встаёт сама, как бинт. Главное решение — расход за ПРОЙДЕННЫЙ ПУТЬ, а не за время. Первая версия считала по секундам, то есть была таймером, которого в игре нет по прямому требованию к жанру: садящийся сам собой фонарь торопил бы игрока ровно как секундомер, только без циферблата, и думать над этажом стало бы дорого. За расстояние получается тот же размен, что и с весом, но с другой стороны: вес делает дорогим «унести всё», свет — «обшарить всё». Заряд общий на отряд и едет вниз вместе с ним: обновляйся он на этаже, это был бы ресурс этажа, а не вылазки, и никуда бы не гнал. Чем глубже, тем дороже шаг, а этажи внизу длиннее — глубина жмёт на свет дважды. Проверка в --descend меряет правило целиком, и первая её строка не про баланс, а про жанр: заряд на стоянке обязан стоять. Сам измеритель тоже был неправ — делил потраченное на смещение отряда, а не на пройденный путь, и показывал 1.55 за тайл при 1.10 в каталоге. свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге глубина 8: заряда на 383 тайла, это 4.0 прямых пути до спуска обзор 1.00 -> 0.35 в темноте, батарея встала сама Хватает ли заряда на этаж считается в этажах и по средней длине забега: путь до спуска скачет вдвое от сида, и порог по одному этажу ловил бы удачу, а не баланс. --accept PASS без изменений: в песочнице света бесконечно, как и патронов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:12:05 +07:00
{
const float walked = Distance(squad.Centroid(), lightFrom);
squad.lightCharge = std::max(0.0f, squad.lightCharge - walked * LightDrain(depth));
}
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
// --- 3. ручное прицеливание (6.8) ----------------------------------------
UpdateAim(dt);
// --- 4. цели по НОВЫМ позициям: линии огня у стрелков, сектор у клинков ---
SolverContext ctx = MakeContext();
for (int i = 0; i < NUM_AGENTS; ++i)
{
Agent& a = squad.agents[i];
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) continue;
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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 (a.IsMelee()) EvaluateMeleeTarget(a, i, ctx);
else EvaluateAgentLane(a, i, ctx);
}
// --- 5. реестр активных линий (6.6) --------------------------------------
lanes.Clear();
for (int i = 0; i < NUM_AGENTS; ++i)
{
const Agent& a = squad.agents[i];
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) continue; // мёртвый ничего не резервирует
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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 (a.IsMelee())
{
// Траектория клинка резервируется тем же механизмом, что и линия
// огня: свои расступаются с дороги замаха, а не выясняют это телом.
// Резерв задаётся явно и шире отрезка — по боковому размаху сектора,
// иначе союзника отжимает ровно во фланг дуги.
if (a.melee.targetId >= 0 && (a.melee.inReach || a.melee.Committed()))
{
const float flank = a.melee.reach * std::sin(a.melee.HalfArc());
lanes.Add(i, a.pos, MeleeStrikeEnd(a, ctx), flank * g_tune.meleeReserveFrac);
}
continue;
}
if (a.hasLane && a.currentTargetId >= 0)
{
lanes.Add(i, a.MuzzleTowards(targets[a.currentTargetId].pos),
targets[a.currentTargetId].pos);
}
else if (a.laneIntent)
{
// Линия перекрыта своим: резервируем её, чтобы перекрывшего отжало
// в сторону. Без этого сектор обстрела никогда не освободится.
lanes.Add(i, a.MuzzleTowards(a.laneEnd), a.laneEnd);
}
}
// --- 6. решатель огневых позиций (6.5) / подход клинка --------------------
for (int i = 0; i < NUM_AGENTS; ++i)
{
Agent& a = squad.agents[i];
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) { lastSolveValid[i] = false; continue; }
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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.commitTimer = std::max(0.0f, a.commitTimer - dt);
if (a.IsMelee())
{
// Решателю здесь делать нечего: у клинка одна огневая позиция —
// вплотную к цели. Нет цели в своём круге — назад в строй.
a.postPos = MeleeGoal(a, ctx, squad.SlotWorld(i));
lastSolveValid[i] = false;
continue;
}
a.solveTimer -= dt;
if (a.solveTimer > 0.0f) continue;
a.solveTimer += g_tune.solveInterval;
if (!enableSolver) continue;
if (a.state != AgentState::HOLDING) continue;
if (a.commitTimer > 0.0f) continue;
if (a.priorityLane) continue; // линия к приоритетной цели уже есть
lastSolveValid[i] = SolveFiringPosition(a, i, ctx, lastSolve[i]);
// Решателю не по чему работать (ни одной цели в досягаемости) — случай,
// который спек не описывает. Возвращаемся в свой слот строя, иначе агент,
// отставший от отряда, остаётся стоять на месте навсегда.
if (!lastSolveValid[i]) a.postPos = squad.SlotWorld(i);
}
// --- 7. доворот ствола / клинка -------------------------------------------
for (int i = 0; i < NUM_AGENTS; ++i)
{
Agent& a = squad.agents[i];
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) continue;
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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 (a.IsMelee() && a.melee.Committed())
{
a.facing = a.melee.dir;
continue;
}
if (a.currentTargetId >= 0) AimAt(a, targets[a.currentTargetId].pos, dt);
else AimAlongMotion(a, dt);
}
// --- 8. атака: огонь и клинок ---------------------------------------------
FireWeapons(ctx, dt);
UpdateMelee(ctx, dt);
// --- 9. пули ---------------------------------------------------------------
Статусы стихий и реакции (M22) Стихии сами по себе были только множителем урона: «верный ствол бьёт втрое больнее». Этого мало, чтобы они стали ЗНАНИЕМ, ради которого заводились — множитель одинаково работает у того, кто понимает систему, и у того, кто взял ствол посильнее. Статусы и реакции добавляют слой, который работает только у первого: наложить ДВЕ разные стихии на одну цель — это решение о составе отряда, а не о характеристиках оружия. sim/status.h правила, потолки стаков, реакции, числа sim/reactions.* наложение и запуск, единственное место UpdateStatuses тик горения и сгорание стаков, из UpdateTargets СИЛА РЕАКЦИИ СЧИТАЕТСЯ ОТ ГЛУБИНЫ, А НЕ ОТ УРОНА ОРУЖИЯ. Считай она от оружия — на сотом этаже стала бы округлением, и весь слой стихий превратился бы в украшение. От глубины она растёт сама, но только у того, кто умеет её запускать. ТРИ ОГРАНИЧИТЕЛЯ, И ВСЕ ПРОВЕРЯЮТСЯ потолок стаков один стак не значит ничего, полный набор меняет бой откат на цели вторая пара внутри 1.2 c даёт 0.0 урона запрет рекурсии урон ОТ реакции не кладёт статусов Третий не теоретический: без него цепочка «реакция бьёт по площади -> урон кладёт стихию -> новая пара -> реакция» уходит в самоповтор на первом же скоплении. Именно так ломаются все игры с триггерами. Четыре реакции из десяти. Остальные ждут систем, которых нет (крит, воскрешение, притяжение), и дописывать их «пока не работает» значило бы завести шесть мёртвых веток. Два отступления от документа, оба сознательные: кислота и дуга увеличивают получаемый урон вместо снятия брони и метки крита (у тварей нет ни брони как числа, ни крита), SUPERCOND кладёт стаки пустоты вместо отдельного среза сопротивлений — пустота в этой игре и означает «защита не работает». Шаг симуляции 2.9 мкс из бюджета 16666: статусы на 64 целях ничего не стоят. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:45:17 +07:00
UpdateBullets(world, map, squad.agents.data(), NUM_AGENTS, targets, dt, metrics, &events,
depth);
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
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
// --- 10. находки -----------------------------------------------------------
// После пуль и клинка: предмет, выпавший из разбитого ящика на этом же шаге,
// обязан быть подбираемым сразу, а не через кадр.
Правки по игре: начало, ставка, наёмники (M29) Семь изменений по итогам игры вживую. НАЧАЛО. Первый этаж встречал одинокого оперативника с ножом тремя тварями, из которых одна быстрая: бюджет 3.0 и бегуны с первого этажа. Стало 2.2 (ровно два бредущих), бегуны с четвёртого. Света прибавлено: обзор начинается с 1.30 вместо 1.0, у этажа появилась своя яркость пройденного (0.42 наверху против 0.13 на дне). Наверху ещё работает освещение, и хоррор первого этажа в том, что игрок один, а не в том, что не видно стен. Темнота это инструмент глубины, и тратить её на первый этаж значит не иметь запаса дальше. СТАВКА. Опыт платится за убийства и сразу: выносить надо ВЕЩИ. Вайп больше не отнимает всё - доезжает около половины добычи, и чем вещь реже, тем охотнее теряется. Прежнее "вайп не возвращает ничего" делало единственным разумным поведением уход наверх при первой царапине. ВЫХОД. Аномалия возврата переехала из точки входа к спуску: развилка "вниз или наверх" теперь там же, где этаж кончился. Обратный путь через растревоженный этаж ничего не решал - это был ещё раз тот же этаж. ЧЕКПОИНТЫ через 50 этажей вместо пяти: забег это долгий спуск, а не перебежка между сохранениями. НАЁМНИКИ. Кнопка "нанять за опыт" убрана, оперативники находятся в капсулах на этажах: в среднем раз в сотню этажей и не чаще раза в двадцать. У каждого свои три прибавки из одного бюджета - разница в характере, а не в ранге. Покупка была скучной ровно в том смысле, в каком скучна любая кнопка "стать сильнее": ничего не рассказывает и никуда не зовёт. ЗАМЕРЫ. Вайп на сотне повторов: доезжает 52% (обычное 69%, реликты 25%). Первая версия проверки считала одну сумку и выдала "обычного доехало 112%" - спасение забирает ещё и надетое. Опыт: банк 558 -> 578 за убийство без эвакуации. Капсула поднимает отряд до двух и даёт наёмнику его числа. Формат профиля 6: строки merc и rescued. Не сделано и обсуждается: ростер сверх пятерых, задания для свободных, пассивные бонусы от сидящих на базе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 08:09:26 +07:00
int rescuedBy = -1;
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
lootTaken += UpdateLootPickup(world, squad.agents.data(), NUM_AGENTS, squad.bag,
Правки по игре: начало, ставка, наёмники (M29) Семь изменений по итогам игры вживую. НАЧАЛО. Первый этаж встречал одинокого оперативника с ножом тремя тварями, из которых одна быстрая: бюджет 3.0 и бегуны с первого этажа. Стало 2.2 (ровно два бредущих), бегуны с четвёртого. Света прибавлено: обзор начинается с 1.30 вместо 1.0, у этажа появилась своя яркость пройденного (0.42 наверху против 0.13 на дне). Наверху ещё работает освещение, и хоррор первого этажа в том, что игрок один, а не в том, что не видно стен. Темнота это инструмент глубины, и тратить её на первый этаж значит не иметь запаса дальше. СТАВКА. Опыт платится за убийства и сразу: выносить надо ВЕЩИ. Вайп больше не отнимает всё - доезжает около половины добычи, и чем вещь реже, тем охотнее теряется. Прежнее "вайп не возвращает ничего" делало единственным разумным поведением уход наверх при первой царапине. ВЫХОД. Аномалия возврата переехала из точки входа к спуску: развилка "вниз или наверх" теперь там же, где этаж кончился. Обратный путь через растревоженный этаж ничего не решал - это был ещё раз тот же этаж. ЧЕКПОИНТЫ через 50 этажей вместо пяти: забег это долгий спуск, а не перебежка между сохранениями. НАЁМНИКИ. Кнопка "нанять за опыт" убрана, оперативники находятся в капсулах на этажах: в среднем раз в сотню этажей и не чаще раза в двадцать. У каждого свои три прибавки из одного бюджета - разница в характере, а не в ранге. Покупка была скучной ровно в том смысле, в каком скучна любая кнопка "стать сильнее": ничего не рассказывает и никуда не зовёт. ЗАМЕРЫ. Вайп на сотне повторов: доезжает 52% (обычное 69%, реликты 25%). Первая версия проверки считала одну сумку и выдала "обычного доехало 112%" - спасение забирает ещё и надетое. Опыт: банк 558 -> 578 за убийство без эвакуации. Капсула поднимает отряд до двух и даёт наёмнику его числа. Формат профиля 6: строки merc и rescued. Не сделано и обсуждается: ростер сверх пятерых, задания для свободных, пассивные бонусы от сидящих на базе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 08:09:26 +07:00
squad.Carriers(), squad.carryBonus, &events, dt, &rescuedBy);
// Найденный оперативник встаёт в строй ТУТ ЖЕ, посреди этажа: капсула —
// это место, до которого дошли, а не покупка (sim/profile.h).
if (rescuedBy >= 0) RescueOperative(rescuedBy);
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
// --- 11. опыт за убитых ----------------------------------------------------
AwardKillXp();
// --- 12. спуск -------------------------------------------------------------
// Симуляция только ОТМЕЧАЕТ, что отряд дошёл. Сам этаж она не меняет: смена
// мира посреди шага, между движением и стрельбой, оставила бы половину
// систем со ссылками на снесённые сущности.
UpdateExit();
// --- 13. метрики -----------------------------------------------------------
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
metrics.Sample(squad, dt);
simTime += dt;
}
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
// -----------------------------------------------------------------------------
// Опыт. Цена убитого — его danger из каталога движка, помноженная на множитель
// этажа: и то и другое живёт в данных, а не здесь.
//
// Обстановка опыта не даёт по определению: ящик — это обстановка, и платить за
// него значило бы сделать разбивание хлама самым выгодным занятием в игре.
// -----------------------------------------------------------------------------
void Game::AwardKillXp()
{
const float scale = g_tune.xpPerDanger * level.floor.xpScale;
for (Target& t : targets)
{
if (t.alive) { t.countedDead = false; continue; }
if (t.countedDead) continue;
t.countedDead = true;
Материалы и мастерская: вещь можно доработать (M27) Аффиксы сделали находку решением, но решением одноразовым: выпало как выпало. Материалы дают второй способ получить нужное — не «повезло», а «доработал». Пять материалов, пять операций, экран WORKSHOP. Главное правило экономики — материал добывается ТОЛЬКО В СВОЕЙ ПОЛОСЕ: био-ядра с плотских этажей, крио с мёрзлых. Это единственная причина возвращаться на средние глубины, когда рекорд ушёл вниз; без полос весь контент, кроме последнего акта, умирает в день прохождения. Материалы — валюта, а не предметы: не занимают ячеек, не весят, в схроне не лежат. Но теряются как добыча: зачисляются только при эвакуации. Три уточнения операций, каждое против конкретного обхода: перековка меняет только значения (иначе это новый бросок предмета в обход редкости), прививка не ставит уже стоящий тег (иначе «+урон» шесть раз), возвышается случайный аффикс (иначе все предметы сойдутся к одному). --tool craft поймал, что материалы бесплатны. С твари падало по её danger, и вылазка на восьмидесятом этаже приносила тысячу ядер при цене доводки вещи в шестьсот: 0.6 вылазки на предмет вместо восьми. Правило заменено на «одно ядро с мелочи, два с крупной» — стало 6.2. Сам инструмент при этом тоже считал неверно: брал доход из бюджета опасности этажа, хотя на этаж влезает не больше MAX_LEVEL_SPAWNS тварей. Бюджет уходит не в количество тварей, а в их силу, и доход упирается в потолок расстановки с сорокового этажа. Глубина даёт не количество материалов, а качество предметов — и вместе с ним цену доводки. Залом замерен на тысяче попыток: рассыпалось 11.6% при заявленных 12%, плата снимается всегда, слот теряется всегда. «У меня трижды не рассыпалось» не доказательство ни в какую сторону. Пункт меню WORKSHOP проверен --ui-check — той самой проверкой, которая появилась вчера из-за пункта UPGRADES, молча закрывавшего игру. Формат профиля 5: строка mat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 07:07:46 +07:00
// Материалы падают и с ОБСТАНОВКИ: лом — это разбитый ящик, и других
// источников у него нет. Опыт за неё по-прежнему не начисляется,
// иначе разбивание хлама стало бы самым выгодным занятием в игре.
if (t.IsProp())
{
const MaterialDrop d = PropDrop(depth);
for (int m = 0; m < MATERIAL_COUNT; ++m) runMaterials[m] += d.amount[m];
continue;
}
if (t.IsDummy()) continue;
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
float danger = 1.0f;
if (catalog)
if (const EntityDesc* d = catalog->Find(IdOfEnemyKind(t.kind))) danger = d->danger;
xp += danger * scale;
Материалы и мастерская: вещь можно доработать (M27) Аффиксы сделали находку решением, но решением одноразовым: выпало как выпало. Материалы дают второй способ получить нужное — не «повезло», а «доработал». Пять материалов, пять операций, экран WORKSHOP. Главное правило экономики — материал добывается ТОЛЬКО В СВОЕЙ ПОЛОСЕ: био-ядра с плотских этажей, крио с мёрзлых. Это единственная причина возвращаться на средние глубины, когда рекорд ушёл вниз; без полос весь контент, кроме последнего акта, умирает в день прохождения. Материалы — валюта, а не предметы: не занимают ячеек, не весят, в схроне не лежат. Но теряются как добыча: зачисляются только при эвакуации. Три уточнения операций, каждое против конкретного обхода: перековка меняет только значения (иначе это новый бросок предмета в обход редкости), прививка не ставит уже стоящий тег (иначе «+урон» шесть раз), возвышается случайный аффикс (иначе все предметы сойдутся к одному). --tool craft поймал, что материалы бесплатны. С твари падало по её danger, и вылазка на восьмидесятом этаже приносила тысячу ядер при цене доводки вещи в шестьсот: 0.6 вылазки на предмет вместо восьми. Правило заменено на «одно ядро с мелочи, два с крупной» — стало 6.2. Сам инструмент при этом тоже считал неверно: брал доход из бюджета опасности этажа, хотя на этаж влезает не больше MAX_LEVEL_SPAWNS тварей. Бюджет уходит не в количество тварей, а в их силу, и доход упирается в потолок расстановки с сорокового этажа. Глубина даёт не количество материалов, а качество предметов — и вместе с ним цену доводки. Залом замерен на тысяче попыток: рассыпалось 11.6% при заявленных 12%, плата снимается всегда, слот теряется всегда. «У меня трижды не рассыпалось» не доказательство ни в какую сторону. Пункт меню WORKSHOP проверен --ui-check — той самой проверкой, которая появилась вчера из-за пункта UPGRADES, молча закрывавшего игру. Формат профиля 5: строка mat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 07:07:46 +07:00
Правки по игре: начало, ставка, наёмники (M29) Семь изменений по итогам игры вживую. НАЧАЛО. Первый этаж встречал одинокого оперативника с ножом тремя тварями, из которых одна быстрая: бюджет 3.0 и бегуны с первого этажа. Стало 2.2 (ровно два бредущих), бегуны с четвёртого. Света прибавлено: обзор начинается с 1.30 вместо 1.0, у этажа появилась своя яркость пройденного (0.42 наверху против 0.13 на дне). Наверху ещё работает освещение, и хоррор первого этажа в том, что игрок один, а не в том, что не видно стен. Темнота это инструмент глубины, и тратить её на первый этаж значит не иметь запаса дальше. СТАВКА. Опыт платится за убийства и сразу: выносить надо ВЕЩИ. Вайп больше не отнимает всё - доезжает около половины добычи, и чем вещь реже, тем охотнее теряется. Прежнее "вайп не возвращает ничего" делало единственным разумным поведением уход наверх при первой царапине. ВЫХОД. Аномалия возврата переехала из точки входа к спуску: развилка "вниз или наверх" теперь там же, где этаж кончился. Обратный путь через растревоженный этаж ничего не решал - это был ещё раз тот же этаж. ЧЕКПОИНТЫ через 50 этажей вместо пяти: забег это долгий спуск, а не перебежка между сохранениями. НАЁМНИКИ. Кнопка "нанять за опыт" убрана, оперативники находятся в капсулах на этажах: в среднем раз в сотню этажей и не чаще раза в двадцать. У каждого свои три прибавки из одного бюджета - разница в характере, а не в ранге. Покупка была скучной ровно в том смысле, в каком скучна любая кнопка "стать сильнее": ничего не рассказывает и никуда не зовёт. ЗАМЕРЫ. Вайп на сотне повторов: доезжает 52% (обычное 69%, реликты 25%). Первая версия проверки считала одну сумку и выдала "обычного доехало 112%" - спасение забирает ещё и надетое. Опыт: банк 558 -> 578 за убийство без эвакуации. Капсула поднимает отряд до двух и даёт наёмнику его числа. Формат профиля 6: строки merc и rescued. Не сделано и обсуждается: ростер сверх пятерых, задания для свободных, пассивные бонусы от сидящих на базе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 08:09:26 +07:00
// И СРАЗУ в профиль. Опыт больше не надо выносить: он платится за
// работу, а не за удачный выход. Вынос — про ВЕЩИ, и ставка осталась
// на них; заставлять игрока терять ещё и понимание того, что он уже
// сделал, значит наказывать дважды за одну смерть.
if (profile) profile->bankedXp += danger * scale;
Материалы и мастерская: вещь можно доработать (M27) Аффиксы сделали находку решением, но решением одноразовым: выпало как выпало. Материалы дают второй способ получить нужное — не «повезло», а «доработал». Пять материалов, пять операций, экран WORKSHOP. Главное правило экономики — материал добывается ТОЛЬКО В СВОЕЙ ПОЛОСЕ: био-ядра с плотских этажей, крио с мёрзлых. Это единственная причина возвращаться на средние глубины, когда рекорд ушёл вниз; без полос весь контент, кроме последнего акта, умирает в день прохождения. Материалы — валюта, а не предметы: не занимают ячеек, не весят, в схроне не лежат. Но теряются как добыча: зачисляются только при эвакуации. Три уточнения операций, каждое против конкретного обхода: перековка меняет только значения (иначе это новый бросок предмета в обход редкости), прививка не ставит уже стоящий тег (иначе «+урон» шесть раз), возвышается случайный аффикс (иначе все предметы сойдутся к одному). --tool craft поймал, что материалы бесплатны. С твари падало по её danger, и вылазка на восьмидесятом этаже приносила тысячу ядер при цене доводки вещи в шестьсот: 0.6 вылазки на предмет вместо восьми. Правило заменено на «одно ядро с мелочи, два с крупной» — стало 6.2. Сам инструмент при этом тоже считал неверно: брал доход из бюджета опасности этажа, хотя на этаж влезает не больше MAX_LEVEL_SPAWNS тварей. Бюджет уходит не в количество тварей, а в их силу, и доход упирается в потолок расстановки с сорокового этажа. Глубина даёт не количество материалов, а качество предметов — и вместе с ним цену доводки. Залом замерен на тысяче попыток: рассыпалось 11.6% при заявленных 12%, плата снимается всегда, слот теряется всегда. «У меня трижды не рассыпалось» не доказательство ни в какую сторону. Пункт меню WORKSHOP проверен --ui-check — той самой проверкой, которая появилась вчера из-за пункта UPGRADES, молча закрывавшего игру. Формат профиля 5: строка mat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 07:07:46 +07:00
// Ядра падают только со «своих» этажей — тема и глубина решают, что
// именно (sim/craft.h). Это и есть причина возвращаться на средние
// глубины, когда рекорд ушёл вниз.
const MaterialDrop d = KillDrop(depth, int(level.floor.theme), danger);
for (int m = 0; m < MATERIAL_COUNT; ++m) runMaterials[m] += d.amount[m];
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
}
}
Правки по игре: начало, ставка, наёмники (M29) Семь изменений по итогам игры вживую. НАЧАЛО. Первый этаж встречал одинокого оперативника с ножом тремя тварями, из которых одна быстрая: бюджет 3.0 и бегуны с первого этажа. Стало 2.2 (ровно два бредущих), бегуны с четвёртого. Света прибавлено: обзор начинается с 1.30 вместо 1.0, у этажа появилась своя яркость пройденного (0.42 наверху против 0.13 на дне). Наверху ещё работает освещение, и хоррор первого этажа в том, что игрок один, а не в том, что не видно стен. Темнота это инструмент глубины, и тратить её на первый этаж значит не иметь запаса дальше. СТАВКА. Опыт платится за убийства и сразу: выносить надо ВЕЩИ. Вайп больше не отнимает всё - доезжает около половины добычи, и чем вещь реже, тем охотнее теряется. Прежнее "вайп не возвращает ничего" делало единственным разумным поведением уход наверх при первой царапине. ВЫХОД. Аномалия возврата переехала из точки входа к спуску: развилка "вниз или наверх" теперь там же, где этаж кончился. Обратный путь через растревоженный этаж ничего не решал - это был ещё раз тот же этаж. ЧЕКПОИНТЫ через 50 этажей вместо пяти: забег это долгий спуск, а не перебежка между сохранениями. НАЁМНИКИ. Кнопка "нанять за опыт" убрана, оперативники находятся в капсулах на этажах: в среднем раз в сотню этажей и не чаще раза в двадцать. У каждого свои три прибавки из одного бюджета - разница в характере, а не в ранге. Покупка была скучной ровно в том смысле, в каком скучна любая кнопка "стать сильнее": ничего не рассказывает и никуда не зовёт. ЗАМЕРЫ. Вайп на сотне повторов: доезжает 52% (обычное 69%, реликты 25%). Первая версия проверки считала одну сумку и выдала "обычного доехало 112%" - спасение забирает ещё и надетое. Опыт: банк 558 -> 578 за убийство без эвакуации. Капсула поднимает отряд до двух и даёт наёмнику его числа. Формат профиля 6: строки merc и rescued. Не сделано и обсуждается: ростер сверх пятерых, задания для свободных, пассивные бонусы от сидящих на базе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 08:09:26 +07:00
// -----------------------------------------------------------------------------
// Капсула вскрыта: свободный слот занимает человек.
//
// Он приходит СО СВОИМИ числами (Profile::merc) и со стартовым снаряжением:
// найденный в капсуле не таскает с собой плиту и магнум. Числа сохранятся в
// профиле только если он ДОЙДЁТ до выхода живым — как и всё остальное.
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// Капсула на этаже. Редко и с двумя границами (sim/profile.h):
//
// * в среднем раз в RESCUE_EVERY этажей — иначе пятый оперативник перестаёт
// быть событием забега;
// * не чаще раза в RESCUE_COOLDOWN — иначе две подряд закрывают полотряда за
// один спуск, и вся редкость оказывается на бумаге.
//
// Кладётся В ТОЧКУ ЛУТА: там гарантированно достижимый пол. Своя раскладка тут
// не нужна — капсула и есть находка, просто вместо вещи в ней человек.
// -----------------------------------------------------------------------------
void Game::MaybeSpawnCapsule()
{
if (!profile || !combatMode || !profile->CanRescue()) return;
if (level.loot.empty()) return;
const int since = depth - profile->lastRescueDepth;
if (profile->lastRescueDepth > 0 && since < RESCUE_COOLDOWN) return;
// Бросок от сида уровня: тот же этаж обязан давать ту же встречу.
uint32_t rng = uint32_t(levelSeed) * 2246822519u + uint32_t(depth) * 3266489917u;
rng = rng * 1664525u + 1013904223u;
const float roll = float((rng >> 8) & 0xFFFFu) / 65536.0f;
if (roll >= 1.0f / float(RESCUE_EVERY)) return;
const size_t at = size_t(rng % uint32_t(level.loot.size()));
SpawnFloorItem(world, ItemInstance(ItemId::CAPSULE), level.loot[at].pos);
}
void Game::RescueOperative(int finder)
{
(void)finder;
if (!profile || !profile->CanRescue()) return;
// Первый незанятый слот. Слоты по порядку, а не «любой»: строй читается по
// номерам, и дыра в середине означала бы, что клин собран не по правилам.
int slot = -1;
for (int i = 0; i < NUM_AGENTS && slot < 0; ++i)
if (!squad.agents[i].present) slot = i;
if (slot < 0) return;
uint32_t rng = uint32_t(levelSeed) * 40503u + uint32_t(depth) * 7919u + uint32_t(slot);
profile->merc[slot] = RollMerc(rng);
++profile->squadSize;
profile->lastRescueDepth = depth;
Agent& a = squad.agents[slot];
a.present = true;
a.alive = true;
squad.roster = std::max(squad.roster, slot + 1);
// Снаряжение — стартовое: человек выходит из капсулы, а не из оружейной.
Profile fresh;
fresh.ResetLoadouts();
if (Inventory* inv = world.Get<Inventory>(a.self))
{
*inv = fresh.loadout[slot];
EquipWeapon(world, a, inv->weapon);
}
a.merc = profile->merc[slot];
SyncLoadout(world, a);
a.hp = a.maxHp;
a.ammoReserve = combatMode ? int(g_tune.startAmmo) : -1;
events.Push(CombatEventKind::LOOT_TAKEN, slot, a.pos, Vec2{});
}
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
void Game::UpdateExit()
{
if (!levelGenerated) { atExit = false; atExtract = false; return; }
const float r = g_tune.descendRadius;
const bool nearExit = Distance(squad.anchor, level.exitPos) <= r;
const bool nearOut = Distance(squad.anchor, level.extractPos) <= r;
// События пишутся на ФРОНТЕ: иначе журнал забивался бы одним и тем же
// сообщением каждый кадр, пока отряд стоит на месте.
if (nearExit && !atExit) events.Push(CombatEventKind::DESCEND, -1, level.exitPos, Vec2{});
if (nearOut && !atExtract) events.Push(CombatEventKind::EXTRACT, -1, level.extractPos, Vec2{});
atExit = nearExit;
atExtract = nearOut;
}
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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 Game::UpdateAim(float dt)
{
if (in.toggleAimMode)
{
aim.manual = !aim.manual;
if (aim.manual)
{
// Стартуем от текущего направления отряда.
aim.dir = AngleOf(squad.forward);
aim.assignedTargetId = -1;
}
else
{
aim.assignedTargetId = -1;
}
}
if (!aim.manual)
{
aim.assignedTargetId = -1;
return;
}
aim.dir = WrapPi(aim.dir + in.aimTurn * g_tune.aimTurnRateDeg * DEG2RAD_F * dt);
// Назначенная цель — ближайшая мишень в конусе +-aimConeDeg от вектора прицела.
const float cone = g_tune.aimConeDeg * DEG2RAD_F;
// Собираем кандидатов в конусе, отсортированных по дальности от якоря.
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 inCone[MAX_TARGETS];
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
int coneCount = 0;
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
for (int i = 0; i < int(targets.size()) && coneCount < MAX_TARGETS; ++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 (!targets[i].alive) continue;
const Vec2 d = targets[i].pos - squad.anchor;
if (std::fabs(AngleDiff(aim.dir, AngleOf(d))) > cone) continue;
inCone[coneCount++] = i;
}
for (int i = 1; i < coneCount; ++i) // вставками, список крошечный
{
const int v = inCone[i];
int j = i - 1;
while (j >= 0 &&
DistanceSq(targets[inCone[j]].pos, squad.anchor) > DistanceSq(targets[v].pos, squad.anchor))
{
inCone[j + 1] = inCone[j];
--j;
}
inCone[j + 1] = v;
}
if (coneCount == 0)
{
aim.assignedTargetId = -1;
return;
}
int current = -1;
for (int i = 0; i < coneCount; ++i)
if (inCone[i] == aim.assignedTargetId) current = i;
if (current < 0) aim.assignedTargetId = inCone[0];
else if (in.cycleTarget) aim.assignedTargetId = inCone[(current + 1) % coneCount];
}
// -----------------------------------------------------------------------------
// Огонь: перезарядка, увод ствола, спуск.
//
// Про разброс. Раньше пуля летела РОВНО по проверенному отрезку, и это было
// главным аргументом за «FRIENDLY_HITS = 0 без исключений». Теперь у выстрела
// есть конус, и аргумент остался тем же — просто проверяется не отрезок, а весь
// конус (ai/firing_solver.cpp, ConeClearOfAllies). Пуля физически не может уйти
// туда, где не проверяли: угол отклонения берётся из ТОЙ ЖЕ величины SpreadRad,
// по которой считалась ширина проверки.
//
// Разброс не случаен в смысле генератора: это чистый хеш от (стрелок, номер
// выстрела). Прогон воспроизводим, --headless по-прежнему годится как тест.
// -----------------------------------------------------------------------------
void Game::FireWeapons(const SolverContext& ctx, float dt)
{
for (int i = 0; i < NUM_AGENTS; ++i)
{
Agent& a = squad.agents[i];
a.fired = false;
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.IsMelee()) continue;
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
Firearm& g = a.gun;
g.cooldown = std::max(0.0f, g.cooldown - dt);
// Увод ствола оседает сам: очередь «раскрывается», пауза её собирает.
g.recoilDeg = std::max(0.0f, g.recoilDeg - g.def.recoilDecay * dt);
// --- перезарядка ------------------------------------------------------
if (g.Reloading())
{
g.reloadTimer -= dt;
if (g.reloadTimer <= 0.0f)
{
g.reloadTimer = 0.0f;
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
// Магазин набивается ИЗ ЗАПАСА. Запас отрицательный — песочница
// приёмки, там патроны бесконечны (sim/agent.h).
if (a.ammoReserve < 0)
{
g.ammo = g.def.magazine;
}
else
{
const int take = std::min(g.def.magazine, a.ammoReserve);
g.ammo = take;
a.ammoReserve -= take;
}
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
events.Push(CombatEventKind::RELOAD_DONE, i, a.pos, FromAngle(a.facing));
}
continue;
}
if (g.Empty())
{
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.ammoReserve == 0) continue;
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
g.reloadTimer = g.def.reloadTime;
events.Push(CombatEventKind::RELOAD_START, i, a.pos, FromAngle(a.facing));
++metrics.reloads;
continue;
}
// Бег с курсором на вытянутом поводке — это приказ идти, а не драться.
if (squad.sprinting) continue;
if (!a.hasLane || a.currentTargetId < 0) continue;
if (g.cooldown > 0.0f) continue;
const Vec2 tp = targets[a.currentTargetId].pos;
// Ствол должен быть доведён до цели — иначе выстрел выглядит враньём.
if (std::fabs(AngleDiff(a.facing, AngleOf(tp - a.pos))) >
g_tune.aimToleranceDeg * DEG2RAD_F)
continue;
// Повторная проверка конуса по позициям этого шага: «никаких исключений».
if (!LaneClearFrom(a.pos, i, tp, ctx)) continue;
const Vec2 muzzle = a.MuzzleTowards(tp);
// И проверка на время полёта пули — свой не должен успеть влезть.
if (AllyInterceptsShot(muzzle, tp, i, ctx)) continue;
// Отклонение внутри уже проверенного конуса.
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
//
// Картечь ничего не ослабляет: ВСЕ дробины уходят в тот же конус
// SpreadRad, который только что проверили на своих. Поэтому у дробовика
// конус широкий, и выстрел отсекается чаще, — это его цена, а не
// послабление правила 6.3.
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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 spread = a.SpreadRad();
const float axis = AngleOf(Normalized(tp - muzzle, FromAngle(a.facing)));
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
const float travel = Distance(muzzle, tp);
const int pellets = std::max(1, g.def.pellets);
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
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
Vec2 dir{};
for (int p = 0; p < pellets; ++p)
{
dir = FromAngle(axis + g.ShotOffset(i, spread, p));
Редкости и аффиксы: находка стала решением (M25) Предмет перестал быть строкой каталога. ItemInstance несёт редкость и до шести аффиксов, и «карабин» с первого этажа больше не равен «карабину» с сорокового. Четыре редкости, десять тегов, тир и бюджет от глубины. Аффиксы не заводят новых каналов влияния: каждый тег ложится в то, что уже есть — урон оружия, смесь стихий, пробитие, четыре числа бойца, ресурсы вылазки. У предмета по-прежнему нет ручки к правилу «не бить своих», и появиться ей неоткуда. ItemInstance неявно приводится к ItemId, поэтому полторы сотни мест, которым нужен ВИД вещи, остались как были. Цена решения — присваивание в ItemId молча теряет рулоны; таких мест три (подбор, схрон, запись профиля), и все они работают с экземпляром целиком. Замеры поймали три расхождения дизайн-документа с самим собой: * формула редкости противоречила таблице рядом с ней: 2.7% находок с аффиксами на первом этаже против 22%. Выбрана формула, наклон поднят до схождения с таблицей на дне (12% обычных на сотом); * бюджет пускал восемь высших аффиксов вместо обещанных шести; * предельная вещь собиралась к семидесятому этажу вместо сотого. Числа пересчитаны, документ исправлен: 6·cost(9) = 294 <= B(100) = 295 < 7·cost(9) = 343. Предельная вещь существует ровно на дне и ровно одна. Проверка в --descend меряет не бросок, а ПРОВОДКУ до бойца — там, где дважды умирали вес и стихии: аффиксы: урон 12.0 -> 18.0, здоровье 100 -> 140, броня 0.10 -> 0.25, пробитие 0.20, KIN 1.00 -> 0.60, CRY 0.40, сумма долей 1.000 Последнее число не украшение: конверсия обязана переводить урон, а не добавлять седьмую долю сверх единицы. Попутно, на глубине 30 и только там: * «зачистка поля» в проверках не держалась — у твари есть откат воскрешения, и с нулевым таймером она оживала тем же шагом. Лечение вне схватки и расход света на стоянке молча мерили бой. На восьмом этаже тварей мало, и проверка проходила по удаче; * порог «стоянка бесплатна» стоял в единицах заряда, а тот же шаг стоит дороже с глубиной. Переведён в тайлы — и сразу показал, что отряд в строю проходит 0.8 тайла за две секунды «стоянки». Свет теперь списывается только в ADVANCING: топтание и драка бесплатны, тратит РАЗВЕДКА. Без этого таймер возвращался через чёрный ход. Формат профиля 4: токен предмета вида rifle@relic+dmg7,0:0.63. Миграции не требует — у старых токенов нет хвоста после '@'. --accept PASS без изменений: в песочнице находок нет вовсе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:35:00 +07:00
SpawnBullet(world, g.def, muzzle, dir, travel, i, a.pen);
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
events.Push(CombatEventKind::SHOT, i, muzzle, dir);
g.cooldown = g.def.fireInterval;
g.ammo = std::max(0, g.ammo - 1);
++g.shotSeq;
g.recoilDeg = std::min(g.def.spreadRecoilMax, g.recoilDeg + g.def.spreadRecoilDeg);
a.fired = true;
++metrics.shotsFired;
}
}
// -----------------------------------------------------------------------------
// Ближний бой: замах -> удар -> восстановление.
//
// Правило то же, что и у стрельбы, только в угловой форме: свой в секторе —
// не бьём. Оно проверяется дважды, как и у ствола:
// 1) на старте замаха — сектор reach x arcDeg обязан быть чист;
// 2) каждый шаг активного окна — если свой всё-таки влез под лезвие,
// взмах ОБРЫВАЕТСЯ. Урон союзнику не наносится ни при каком раскладе,
// поэтому FRIENDLY_HITS клинок физически поднять не может.
//
// Урон идёт не «по факту начала удара», а по реально пройденной за шаг полосе
// лезвия: то, что видно на экране, и то, что получило урон, — одна и та же дуга.
// -----------------------------------------------------------------------------
void Game::UpdateMelee(const SolverContext& ctx, float dt)
{
const float bladeHalf = g_tune.meleeBladeDeg * 0.5f * DEG2RAD_F;
for (int i = 0; i < NUM_AGENTS; ++i)
{
Agent& a = squad.agents[i];
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.IsMelee()) continue;
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
MeleeWeapon& m = a.melee;
m.cooldown = std::max(0.0f, m.cooldown - dt);
a.fired = false;
switch (m.state)
{
case MeleeState::IDLE:
{
// Связка живёт своим окном: не продолжил вовремя — начинаешь
// серию сначала.
m.comboTimer = std::max(0.0f, m.comboTimer - dt);
if (m.comboTimer <= 0.0f) m.comboStep = 0;
// На бегу боец не гоняется за целями (за него это решает
// Squad::GoalFor — он идёт в слот), но если цель сама оказалась
// под рукой, клинок работает. Бой на ходу — это нормально.
if (m.targetId < 0 || !m.inReach) break;
if (m.cooldown > 0.0f) break;
if (m.arcBlockedByAlly) break; // посчитано в EvaluateMeleeTarget
const Vec2 tp = targets[m.targetId].pos;
const float axis = AngleOf(tp - a.pos);
// Корпус должен быть довёрнут — иначе удар выглядит враньём
// ровно так же, как выстрел мимо ствола.
if (std::fabs(AngleDiff(a.facing, axis)) >
g_tune.meleeAimToleranceDeg * DEG2RAD_F)
break;
const MeleeComboStep& cs = m.NextComboStep();
m.BakeSwing(cs); // ширина, урон и тайминги — на весь взмах
m.state = MeleeState::WINDUP;
m.phase = m.curWindup;
m.dir = axis;
if (cs.flipSide) m.sweepSign = -m.sweepSign;
m.hitMask = 0;
// Интервал считается от НАЧАЛА замаха; внутри связки он короче.
m.cooldown = m.interval * cs.intervalMul;
break;
}
case MeleeState::WINDUP:
{
m.phase -= dt;
if (m.phase > 0.0f) break;
m.state = MeleeState::STRIKE;
m.phase = m.curStrike;
events.Push(CombatEventKind::MELEE_SWING, i, a.pos, FromAngle(m.dir));
++metrics.meleeSwings;
if (m.comboStep >= 0 && m.comboStep < MELEE_COMBO_STEPS)
++metrics.meleeComboSwings[m.comboStep];
break;
}
case MeleeState::STRIKE:
{
const float p0 = m.Progress();
m.phase -= dt;
const float p1 = m.Progress();
const float ang0 = m.BladeAngleAt(p0);
const float ang1 = m.BladeAngleAt(p1);
// 1. свой под лезвием -> взмах оборван
bool aborted = false;
for (int k = 0; k < ctx.agentCount; ++k)
{
if (k == i) continue;
const Agent& ally = ctx.agents[k];
if (!BladeSweepHits(a.pos, ang0, ang1, bladeHalf,
m.reach + g_tune.meleeAllyMargin, ally.pos,
ally.bodyRadius + g_tune.meleeAllyMargin))
continue;
aborted = true;
break;
}
if (aborted)
{
events.Push(CombatEventKind::MELEE_ABORT, i, a.pos, FromAngle(ang1));
++metrics.meleeAborts;
m.state = MeleeState::RECOVER;
m.phase = m.curRecover;
break;
}
// 2. мишени, по одной за взмах
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
// MAX_TARGETS — это ширина hitMask, а не «столько влезет»:
// за пределами маски удар прошёл бы по одной цели дважды.
for (int t = 0; t < int(targets.size()) && t < MAX_TARGETS; ++t)
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
{
Target& tg = targets[t];
if (!tg.alive) continue;
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 (m.hitMask & (uint64_t(1) << t)) continue;
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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 (!BladeSweepHits(a.pos, ang0, ang1, bladeHalf, m.reach, tg.pos, tg.radius))
continue;
if (map.RaycastBlocked(a.pos, tg.pos)) continue; // сквозь стену не рубим
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
m.hitMask |= (uint64_t(1) << t);
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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.fired = true;
const Vec2 dir = Normalized(tg.pos - a.pos, FromAngle(m.dir));
const Vec2 contact = tg.pos - dir * tg.radius;
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
const bool prop = tg.IsProp();
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
Стихии и вторая поверхность инструментов (M21, E2) СТИХИИ (src/sim/element.h) Семь элементов, сопротивления свойством ВИДА, одна формула урона на пулю и на клинок. Зачем они в игре про бесконечный спуск: сила отряда растёт медленнее угрозы (закон масштаба, docs/20-gdd.md), и разрыв обязан закрываться знанием. Стихии — первый такой канал: верное оружие убивает втрое быстрее, и цифра эта не растёт от прокачки. Ручных действий не заводится: стихия — свойство оружия, а не кнопка. Всё решается тем, что отряд взял вниз. Стихии есть только у НАХОДИМОГО оружия. Карабин, пистолет и сабля остаются чистой кинетикой, потому что на них меряется приёмка. ИНСТРУМЕНТЫ БАЛАНСА (src/debug/tools.cpp) Вторая поверхность: `squad_proto --tool weapons|enemies|ttk|economy`. Отдельная от редакторской не по вкусу — редактор линкуется только с движком и правил боя не видит, а всё это считает урон. Инструменты делятся по ПРЕДМЕТУ: содержимое у редактора, баланс у игры. ЧТО НАШЛА ПРОВЕРКА Первая версия --tool ttk считала разброс по времени убийства и показывала уверенные 2.8x на каждой твари. Это была разница МОЩНОСТИ стволов: магнум лучше пистолета против всех, стихии ни при чём. С вынесенной за скобки мощностью оружия правда оказалась другой: 1.04-1.36x, то есть состав удара не значил почти ничего. Механика была декоративной ровно в том же смысле, в каком когда-то был вес. Чтобы разброс появился, у твари должны быть И уязвимость, И сопротивление среди тех стихий, которые вообще носит оружие: одной уязвимости мало, если ни один ствол не бьёт тем, что она держит. После правки содержимого все пять видов в коридоре 1.90-3.06x, и картина читается — дробовик жжёт бредущих, дуга бьёт плевков, лук разъедает громилу, карабин против громилы бесполезен. Коридор в инструменте 1.8-8.0, а не заявленные в документе 3-6: до тройки дотянут почти чистые стихийные сборки, которые приходят с аффиксами. Занижать цель в документе нельзя, поэтому в инструменте стоит достижимое. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:25:44 +07:00
// Через ту же формулу, что и пуля: клинок отличается
// формой удара, а не правилами урона.
Статусы стихий и реакции (M22) Стихии сами по себе были только множителем урона: «верный ствол бьёт втрое больнее». Этого мало, чтобы они стали ЗНАНИЕМ, ради которого заводились — множитель одинаково работает у того, кто понимает систему, и у того, кто взял ствол посильнее. Статусы и реакции добавляют слой, который работает только у первого: наложить ДВЕ разные стихии на одну цель — это решение о составе отряда, а не о характеристиках оружия. sim/status.h правила, потолки стаков, реакции, числа sim/reactions.* наложение и запуск, единственное место UpdateStatuses тик горения и сгорание стаков, из UpdateTargets СИЛА РЕАКЦИИ СЧИТАЕТСЯ ОТ ГЛУБИНЫ, А НЕ ОТ УРОНА ОРУЖИЯ. Считай она от оружия — на сотом этаже стала бы округлением, и весь слой стихий превратился бы в украшение. От глубины она растёт сама, но только у того, кто умеет её запускать. ТРИ ОГРАНИЧИТЕЛЯ, И ВСЕ ПРОВЕРЯЮТСЯ потолок стаков один стак не значит ничего, полный набор меняет бой откат на цели вторая пара внутри 1.2 c даёт 0.0 урона запрет рекурсии урон ОТ реакции не кладёт статусов Третий не теоретический: без него цепочка «реакция бьёт по площади -> урон кладёт стихию -> новая пара -> реакция» уходит в самоповтор на первом же скоплении. Именно так ломаются все игры с триггерами. Четыре реакции из десяти. Остальные ждут систем, которых нет (крит, воскрешение, притяжение), и дописывать их «пока не работает» значило бы завести шесть мёртвых веток. Два отступления от документа, оба сознательные: кислота и дуга увеличивают получаемый урон вместо снятия брони и метки крита (у тварей нет ни брони как числа, ни крита), SUPERCOND кладёт стаки пустоты вместо отдельного среза сопротивлений — пустота в этой игре и означает «защита не работает». Шаг симуляции 2.9 мкс из бюджета 16666: статусы на 64 целях ничего не стоят. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:45:17 +07:00
tg.hp -= ResolveDamage(m.curDamage, a.melee.elem, tg.resist,
Редкости и аффиксы: находка стала решением (M25) Предмет перестал быть строкой каталога. ItemInstance несёт редкость и до шести аффиксов, и «карабин» с первого этажа больше не равен «карабину» с сорокового. Четыре редкости, десять тегов, тир и бюджет от глубины. Аффиксы не заводят новых каналов влияния: каждый тег ложится в то, что уже есть — урон оружия, смесь стихий, пробитие, четыре числа бойца, ресурсы вылазки. У предмета по-прежнему нет ручки к правилу «не бить своих», и появиться ей неоткуда. ItemInstance неявно приводится к ItemId, поэтому полторы сотни мест, которым нужен ВИД вещи, остались как были. Цена решения — присваивание в ItemId молча теряет рулоны; таких мест три (подбор, схрон, запись профиля), и все они работают с экземпляром целиком. Замеры поймали три расхождения дизайн-документа с самим собой: * формула редкости противоречила таблице рядом с ней: 2.7% находок с аффиксами на первом этаже против 22%. Выбрана формула, наклон поднят до схождения с таблицей на дне (12% обычных на сотом); * бюджет пускал восемь высших аффиксов вместо обещанных шести; * предельная вещь собиралась к семидесятому этажу вместо сотого. Числа пересчитаны, документ исправлен: 6·cost(9) = 294 <= B(100) = 295 < 7·cost(9) = 343. Предельная вещь существует ровно на дне и ровно одна. Проверка в --descend меряет не бросок, а ПРОВОДКУ до бойца — там, где дважды умирали вес и стихии: аффиксы: урон 12.0 -> 18.0, здоровье 100 -> 140, броня 0.10 -> 0.25, пробитие 0.20, KIN 1.00 -> 0.60, CRY 0.40, сумма долей 1.000 Последнее число не украшение: конверсия обязана переводить урон, а не добавлять седьмую долю сверх единицы. Попутно, на глубине 30 и только там: * «зачистка поля» в проверках не держалась — у твари есть откат воскрешения, и с нулевым таймером она оживала тем же шагом. Лечение вне схватки и расход света на стоянке молча мерили бой. На восьмом этаже тварей мало, и проверка проходила по удаче; * порог «стоянка бесплатна» стоял в единицах заряда, а тот же шаг стоит дороже с глубиной. Переведён в тайлы — и сразу показал, что отряд в строю проходит 0.8 тайла за две секунды «стоянки». Свет теперь списывается только в ADVANCING: топтание и драка бесплатны, тратит РАЗВЕДКА. Без этого таймер возвращался через чёрный ход. Формат профиля 4: токен предмета вида rifle@relic+dmg7,0:0.63. Миграции не требует — у старых токенов нет хвоста после '@'. --accept PASS без изменений: в песочнице находок нет вовсе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:35:00 +07:00
ResistCutOf(tg.status) + a.pen) *
Статусы стихий и реакции (M22) Стихии сами по себе были только множителем урона: «верный ствол бьёт втрое больнее». Этого мало, чтобы они стали ЗНАНИЕМ, ради которого заводились — множитель одинаково работает у того, кто понимает систему, и у того, кто взял ствол посильнее. Статусы и реакции добавляют слой, который работает только у первого: наложить ДВЕ разные стихии на одну цель — это решение о составе отряда, а не о характеристиках оружия. sim/status.h правила, потолки стаков, реакции, числа sim/reactions.* наложение и запуск, единственное место UpdateStatuses тик горения и сгорание стаков, из UpdateTargets СИЛА РЕАКЦИИ СЧИТАЕТСЯ ОТ ГЛУБИНЫ, А НЕ ОТ УРОНА ОРУЖИЯ. Считай она от оружия — на сотом этаже стала бы округлением, и весь слой стихий превратился бы в украшение. От глубины она растёт сама, но только у того, кто умеет её запускать. ТРИ ОГРАНИЧИТЕЛЯ, И ВСЕ ПРОВЕРЯЮТСЯ потолок стаков один стак не значит ничего, полный набор меняет бой откат на цели вторая пара внутри 1.2 c даёт 0.0 урона запрет рекурсии урон ОТ реакции не кладёт статусов Третий не теоретический: без него цепочка «реакция бьёт по площади -> урон кладёт стихию -> новая пара -> реакция» уходит в самоповтор на первом же скоплении. Именно так ломаются все игры с триггерами. Четыре реакции из десяти. Остальные ждут систем, которых нет (крит, воскрешение, притяжение), и дописывать их «пока не работает» значило бы завести шесть мёртвых веток. Два отступления от документа, оба сознательные: кислота и дуга увеличивают получаемый урон вместо снятия брони и метки крита (у тварей нет ни брони как числа, ни крита), SUPERCOND кладёт стаки пустоты вместо отдельного среза сопротивлений — пустота в этой игре и означает «защита не работает». Шаг симуляции 2.9 мкс из бюджета 16666: статусы на 64 целях ничего не стоят. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:45:17 +07:00
VulnerabilityOf(tg.status);
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
tg.hurtTimer = g_tune.targetHurtTime;
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
KnockTarget(tg, dir, m.curKnock);
Статусы стихий и реакции (M22) Стихии сами по себе были только множителем урона: «верный ствол бьёт втрое больнее». Этого мало, чтобы они стали ЗНАНИЕМ, ради которого заводились — множитель одинаково работает у того, кто понимает систему, и у того, кто взял ствол посильнее. Статусы и реакции добавляют слой, который работает только у первого: наложить ДВЕ разные стихии на одну цель — это решение о составе отряда, а не о характеристиках оружия. sim/status.h правила, потолки стаков, реакции, числа sim/reactions.* наложение и запуск, единственное место UpdateStatuses тик горения и сгорание стаков, из UpdateTargets СИЛА РЕАКЦИИ СЧИТАЕТСЯ ОТ ГЛУБИНЫ, А НЕ ОТ УРОНА ОРУЖИЯ. Считай она от оружия — на сотом этаже стала бы округлением, и весь слой стихий превратился бы в украшение. От глубины она растёт сама, но только у того, кто умеет её запускать. ТРИ ОГРАНИЧИТЕЛЯ, И ВСЕ ПРОВЕРЯЮТСЯ потолок стаков один стак не значит ничего, полный набор меняет бой откат на цели вторая пара внутри 1.2 c даёт 0.0 урона запрет рекурсии урон ОТ реакции не кладёт статусов Третий не теоретический: без него цепочка «реакция бьёт по площади -> урон кладёт стихию -> новая пара -> реакция» уходит в самоповтор на первом же скоплении. Именно так ломаются все игры с триггерами. Четыре реакции из десяти. Остальные ждут систем, которых нет (крит, воскрешение, притяжение), и дописывать их «пока не работает» значило бы завести шесть мёртвых веток. Два отступления от документа, оба сознательные: кислота и дуга увеличивают получаемый урон вместо снятия брони и метки крита (у тварей нет ни брони как числа, ни крита), SUPERCOND кладёт стаки пустоты вместо отдельного среза сопротивлений — пустота в этой игре и означает «защита не работает». Шаг симуляции 2.9 мкс из бюджета 16666: статусы на 64 целях ничего не стоят. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:45:17 +07:00
ApplyElementalHit(targets, t, a.melee.elem, depth, &events);
Триггеры: четыре условных эффекта на аффиксах (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
// Триггеры клинка — те же, что у ствола, и в том же месте:
// сразу за уроном (sim/trigger.h).
if (!prop)
{
const float onHit = a.mods.trigger[int(TriggerKind::ON_HIT)];
if (tg.alive && TriggerReady(a.trig, TriggerKind::ON_HIT, onHit) &&
a.trig.Chance(onHit))
{
ApplyElementalHit(targets, t, ElementMix::Of(Element::INC, 1.0f),
depth, &events);
TriggerFired(a.trig, TriggerKind::ON_HIT);
}
const float onKill = a.mods.trigger[int(TriggerKind::ON_KILL)];
if (tg.hp <= 0.0f && TriggerReady(a.trig, TriggerKind::ON_KILL, onKill) &&
a.trig.Chance(onKill))
{
a.trig.AddKillStack();
TriggerFired(a.trig, TriggerKind::ON_KILL);
}
}
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
// Щепки вместо крови и мимо счётчика ударов по врагу:
// разрубленный ящик не должен красить пол и накручивать
// MELEE HITS в метриках.
if (prop)
{
events.Push(CombatEventKind::HIT_PROP, i, contact, dir);
}
else
{
events.Push(CombatEventKind::MELEE_HIT, i, contact, dir);
++metrics.meleeHits;
}
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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
// Инвариант сектора: попадание обязано лежать внутри дуги
// ЭТОГО взмаха. Замеряется превышение — оно должно быть ~0.
const float off = std::fabs(AngleDiff(m.dir, AngleOf(tg.pos - a.pos)));
const float allowed = m.curHalfArc + bladeHalf;
metrics.meleeMaxHitExcessDeg =
std::max(metrics.meleeMaxHitExcessDeg,
std::max(0.0f, off - allowed) / DEG2RAD_F);
if (tg.hp <= 0.0f)
{
tg.alive = false;
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
tg.respawnTimer = tg.RespawnDelay();
if (!prop) ++metrics.targetsKilled;
events.Push(prop ? CombatEventKind::PROP_BREAK
: CombatEventKind::HIT_KILL,
i, tg.pos, dir);
Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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 (m.phase <= 0.0f)
{
m.state = MeleeState::RECOVER;
m.phase = m.curRecover;
}
break;
}
case MeleeState::RECOVER:
{
m.phase -= dt;
if (m.phase <= 0.0f)
{
m.state = MeleeState::IDLE;
m.phase = 0.0f;
// Удар доведён до конца — связка продвигается. Окно открыто:
// успел ударить снова, пока оно не истекло, — пойдёт
// следующий удар серии, не успел — начнёшь сначала.
m.comboStep = (m.comboStep + 1) % MELEE_COMBO_STEPS;
m.comboTimer = g_tune.meleeComboWindow;
}
break;
}
}
}
}
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
// -----------------------------------------------------------------------------
// Комплекты снаряжения.
//
// Раздаются ТОЛЬКО в боевом режиме и только здесь. Причина жёсткая: броня несёт
// множитель скорости, а на скорости движения меряются критерии 11.4 и 11.5.
// Отряд приёмки обязан оставаться голым — тогда цифры сравнимы с прежними.
// -----------------------------------------------------------------------------
void Game::EquipKits()
{
struct Kit
{
ItemId blade; // если слот держит клинок
ItemId gun; // если слот держит ствол
ItemId armor;
ItemId trinket;
ItemId bag[3]; // запасное оружие и расходники
};
// Комплект на СЛОТ СТРОЯ, а не на «персонажа»: остриё клина берёт плиту и
// тесак, хвост — лёгкую броню и дальний ствол. Состав читается по строю.
static const Kit KITS[NUM_AGENTS] = {
{ItemId::CLEAVER, ItemId::SHOTGUN, ItemId::PLATE, ItemId::CHARM,
{ItemId::BANDAGE, ItemId::NONE, ItemId::NONE}},
{ItemId::KNIFE, ItemId::SMG, ItemId::SHROUD, ItemId::SPURS,
{ItemId::PISTOL, ItemId::BANDAGE, ItemId::NONE}},
{ItemId::SABER, ItemId::RIFLE, ItemId::VEST, ItemId::LANTERN,
{ItemId::SHOTGUN, ItemId::NONE, ItemId::NONE}},
{ItemId::SABER, ItemId::SHOTGUN, ItemId::VEST, ItemId::CHARM,
{ItemId::RIFLE, ItemId::STIMPACK, ItemId::NONE}},
{ItemId::KNIFE, ItemId::MAGNUM, ItemId::JACKET, ItemId::SPURS,
{ItemId::SMG, ItemId::BANDAGE, ItemId::NONE}},
};
for (int i = 0; i < NUM_AGENTS; ++i)
{
Agent& a = squad.agents[i];
Inventory* inv = world.Get<Inventory>(a.self);
if (!inv) continue;
const Kit& kit = KITS[i];
const bool melee = (i < squad.meleeCount);
*inv = Inventory{};
inv->weapon = melee ? kit.blade : kit.gun;
inv->armor = kit.armor;
inv->trinket = kit.trinket;
// Запасное уходит в ОБЩУЮ сумку: она одна на отряд.
for (int s = 0; s < 3; ++s)
if (kit.bag[s] != ItemId::NONE) squad.bag.Take(kit.bag[s], NUM_AGENTS, 0.0f);
EquipWeapon(world, a, inv->weapon);
a.hp = a.maxHp;
}
}
int Game::AliveAgents() const
{
int n = 0;
for (int i = 0; i < NUM_AGENTS; ++i)
if (squad.agents[i].alive) ++n;
return n;
}