squad-proto/src/sim/spawn_catalog.cpp

164 lines
6.0 KiB
C++
Raw Normal View History

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 "sim/spawn_catalog.h"
Редкости и аффиксы: находка стала решением (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
#include "sim/affix.h"
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 "sim/loot.h"
namespace
{
// Порядок здесь не важен, важна полнота: каждый вид, который может встретиться
// в комнате, обязан иметь id. Мишень приёмки (DUMMY) в каталоге движка есть
// намеренно — ею удобно ставить неподвижную цель, не заводя тварь.
struct Row
{
const char* id;
EnemyKind kind;
};
const Row ROWS[] = {
{"dummy", EnemyKind::DUMMY},
Личинки, парирование и инструмент --wall (M30) --WALL. Новый прогон отвечает на вопрос, появившийся вместе с чекпоинтами через пятьдесят этажей: растёт ли достижимая глубина от вылазки к вылазке. Отряд из одного, трёх и пяти, одетый по глубине, ставится в скопление тварей и живёт полторы минуты без управления. Первая стена: одиночка D7, трое D30, пятеро D30 - петля работает. И он же нашёл настоящую ошибку: PlaceSquad воскрешал ВСЕ пять слотов, в том числе незанятые. Строка писалась, когда отряд всегда был впятером; после появления ростера одиночка выходил на этаж с четырьмя невидимыми телохранителями. Из-за них прежний замер "одиночка выжил на первом этаже, 100 -> 41" был боем впятером. ЛИЧИНКИ. С исправленным замером выяснилось, что одиночка с ножом проигрывает двум бредущим: 210 здоровья против ножа это девять секунд работы под ответным уроном. Первые этажи населены личинками (55 здоровья, 7 урона, медленные), бредущий уехал на третий этаж, бегун на четвёртый. Новый ВИД, а не ослабленный бредущий с множителем: глубина в этой игре меняет, кто выходит, а не числа у одного и того же. ПАРИРОВАНИЕ. Отдельная характеристика клинка, и чем оружие легче, тем она выше: нож 34%, сабля 20%, тесак 8%. Не выведена из веса намеренно - её двигает аффикс PARRY, и лёгкий тесак обязан быть возможен как находка. Отбить можно, только если удары СОПРИКОСНУЛИСЬ: боец сам должен быть в замахе или в ударе. Иначе клинок стал бы щитом, работающим, пока стоишь. Удавшийся отбив снимает урон, бьёт тварь ударом бойца целиком и запускает повторную атаку с начала фазы, НЕ сбрасывая откат. Последнее и есть суть размена: сбрасывай отбив откат, выгоднее было бы подставляться под удары. Плевок отбивается тоже: летит назад и несёт урон бойца, а не твари. Это его удар, просто по летящей цели. Проверка меряет размен целиком, включая обратную половину правила: "стоя столбом не отбивает". Без неё условие про соприкосновение существовало бы только в комментарии. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 09:04:35 +07:00
{"larva", EnemyKind::LARVA},
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
{"shambler", EnemyKind::SHAMBLER},
{"rusher", EnemyKind::RUSHER},
{"spitter", EnemyKind::SPITTER},
{"brute", EnemyKind::BRUTE},
{"lurker", EnemyKind::LURKER},
Семь новых тварей: акты II-VI перестали быть пустыми (M34) Каталог кончался громилой на седьмом этаже: глубже бюджет опасности выбирался теми же пятью видами, то есть спуск переставал показывать новое ровно там, где обязан был начать. CINDER (D9) горит сама - дробовик против неё бесполезен, первый повод носить второй ствол. STINGER (D21) не убивает, а не даёт стоять. SWARM (D36) толпа в одном теле, сектор 160 градусов - вбок не обойти, берётся площадью. HUSK (D36) стена: 0.70 по кинетике, вскрывается только кислотой. SLEEPER (D44) с радиусом агро 3.4 - мимо неё МОЖНО пройти, а разбуженная бьёт кругом на 62 по всем, кто шёл кучей. WAILER (D53) меняет порядок целей: пока жив, павшие рядом встают вшестеро быстрее. VOIDLING (D73) ровные 0.45 по шести стихиям - верного ствола нет, берётся срезанием сопротивлений. Аура воскрешения у плакальщика РАБОТАЕТ, а не описана: список поднимающих собирается раз в шаг, берётся наибольший множитель, а не сумма. В песочнице приёмки список всегда пуст. Ни одного множителя от глубины, ни одного иммунитета, ни одного числа вне EnemyDefOf. ИНСТРУМЕНТЫ ЗНАЛИ ПОЛОВИНУ ИГРЫ. Список KINDS в tools.cpp был захардкожен пятью видами и не содержал даже личинки: --tool enemies и --tool ttk молчали про остальных. Теперь там весь бестиарий, тринадцать видов. Личинка из коридора стихий исключена намеренно и с пометкой "учебная": она населяет первые этажи и умирает от чего угодно - в этом её работа. Требовать от неё разброса значило бы требовать знания стихий на первом этаже. Проверено: --descend 80 собирает все глубины, --tool ttk в коридоре, --accept PASS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 23:41:49 +07:00
{"cinder", EnemyKind::CINDER},
{"stinger", EnemyKind::STINGER},
{"swarm", EnemyKind::SWARM},
{"husk", EnemyKind::HUSK},
{"sleeper", EnemyKind::SLEEPER},
{"wailer", EnemyKind::WAILER},
{"voidling", EnemyKind::VOIDLING},
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
{"crate", EnemyKind::CRATE},
{"barrel", EnemyKind::BARREL},
{"locker", EnemyKind::LOCKER},
};
// Вторая половина того же моста: id каталога -> предмет игры. Отдельная
// таблица, а не поле в первой: тварь и предмет — разные пулы и разные
// перечисления, и объединяющий их «универсальный id» пришлось бы разбирать
// в каждом месте использования.
struct LootRow
{
const char* id;
ItemId item;
};
const LootRow LOOT_ROWS[] = {
{"rifle", ItemId::RIFLE}, {"pistol", ItemId::PISTOL},
{"shotgun", ItemId::SHOTGUN}, {"smg", ItemId::SMG},
{"magnum", ItemId::MAGNUM}, {"bow", ItemId::BOW},
{"saber", ItemId::SABER}, {"knife", ItemId::KNIFE},
{"cleaver", ItemId::CLEAVER},
Лут перестал быть мусором; 19 новых вещей (M35) ЧТО ДЕЛАЛО ЛУТ МУСОРОМ. Три вещи, и все три чинятся в броске: 1. Оружие могло выкатиться БЕЗ ЕДИНОГО боевого аффикса. Редкость обещала силу, а рулон целиком уходил в снабжение - и "реликт" оказывался хуже обычного карабина. Теперь первым всегда идёт ключевой тег вида: у оружия боевой, у брони защитный. Замер: 935 небанальных стволов, ноль без боевых аффиксов. 2. Тир брался равномерно до потолка глубины: на восьмидесятом этаже половина аффиксов была первого-второго тира, то есть глубина меняла число строк в описании, а не их силу. Теперь из двух бросков берётся больший - средний тир на дне поднялся с 5.0 до 6.5. 3. Все находки одного этажа имели один бюджет и различались только рулонами. Теперь уровень предмета гуляет -2..+3 от глубины: плюс три сверху дают находку, опережающую глубину, а это лучшая причина спускаться. КОНТЕНТ. 19 новых предметов: шесть стволов (гвоздомёт, пулемёт, плазма, рельса, огнемёт, кислота), четыре клинка (стилет с парированием 0.52, молот на 430 урона и парированием 0.02, цепной, крио), четыре брони и пять карманных. У каждой вещи свой минус, ни одна не доминирует старые; клинки уложены в одну ось "вес против парирования". ИНСТРУМЕНТЫ СЧИТАЛИ ТРЕТЬ ИГРЫ. Список стволов в tools.cpp был захардкожен шестью значениями: --tool weapons и --tool ttk молчали про новое оружие ровно так же, как про новых тварей. Теперь там весь арсенал. Каталог проекта пополнен всеми новыми вещами по тирам и глубинам (3..31), чтобы они реально падали, а не лежали в перечислении. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 23:47:13 +07:00
{"nailer", ItemId::NAILER}, {"hmg", ItemId::HMG},
{"plasma", ItemId::PLASMA}, {"rail", ItemId::RAIL},
{"flamer", ItemId::FLAMER}, {"acid", ItemId::ACID},
{"stiletto", ItemId::STILETTO}, {"maul", ItemId::MAUL},
{"ripper", ItemId::RIPPER}, {"cryoedge", ItemId::CRYOEDGE},
{"recon", ItemId::RECON}, {"helm", ItemId::HELM},
{"carapace", ItemId::CARAPACE}, {"weave", ItemId::WEAVE},
{"thermal", ItemId::THERMAL}, {"pauldron", ItemId::PAULDRON},
{"clamp", ItemId::CLAMP}, {"adrenal", ItemId::ADRENAL},
{"harness", ItemId::HARNESS},
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
{"jacket", ItemId::JACKET}, {"vest", ItemId::VEST},
{"plate", ItemId::PLATE}, {"shroud", ItemId::SHROUD},
{"lantern", ItemId::LANTERN}, {"charm", ItemId::CHARM},
{"spurs", ItemId::SPURS},
Свет как расходник: заряд тратится за путь, а не за время (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
{"ammo", ItemId::AMMO}, {"battery", ItemId::BATTERY},
Правки по игре: начало, ставка, наёмники (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
{"capsule", ItemId::CAPSULE},
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
{"bandage", ItemId::BANDAGE}, {"stimpack", ItemId::STIMPACK},
};
} // namespace
bool EnemyKindOfId(const std::string& id, EnemyKind& out)
{
for (const Row& r : ROWS)
{
if (id != r.id) continue;
out = r.kind;
return true;
}
return false;
}
bool ItemIdOfId(const std::string& id, ItemId& out)
{
for (const LootRow& r : LOOT_ROWS)
{
if (id != r.id) continue;
out = r.item;
return true;
}
return false;
}
const char* IdOfEnemyKind(EnemyKind kind)
{
for (const Row& r : ROWS)
if (r.kind == kind) return r.id;
return "?";
}
const char* IdOfItem(ItemId item)
{
for (const LootRow& r : LOOT_ROWS)
if (r.item == item) return r.id;
return "-";
}
int SpawnFromLevel(ecs::World& world, const std::vector<LevelSpawn>& list, const Tilemap& map,
int* unknown)
{
int placed = 0;
int skipped = 0;
for (const LevelSpawn& s : list)
{
EnemyKind kind = EnemyKind::DUMMY;
if (!EnemyKindOfId(s.id, kind)) { ++skipped; continue; }
// Потолок тот же, что у захардкоженной расстановки: маска задетых целей
// в клинке 32-битная, и тридцать третья цель молча получала бы удары
// дважды за взмах (tuning.h).
if (placed >= MAX_TARGETS) { ++skipped; continue; }
// Сопротивление отбросу — свойство вида: расписывать его по слотам,
// как у мишеней приёмки, здесь не по чему, слотов нет.
MakeTarget(world, kind, s.pos, -1.0f, &map);
++placed;
}
if (unknown) *unknown = skipped;
return placed;
}
Редкости и аффиксы: находка стала решением (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
int SpawnLootFromLevel(ecs::World& world, const std::vector<LevelSpawn>& list, int depth,
uint32_t seed, int* unknown)
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 placed = 0;
int skipped = 0;
Редкости и аффиксы: находка стала решением (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
// Сид броска — от сида уровня и глубины: тот же уровень обязан дать те же
// находки. Иначе «переиграть с начала» (R) выдавало бы другой лут на той же
// карте, и сравнить два прохода стало бы нечем.
uint32_t rng = seed * 2654435761u + uint32_t(depth) * 40503u + 12345u;
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 (const LevelSpawn& s : list)
{
ItemId item = ItemId::NONE;
if (!ItemIdOfId(s.id, item)) { ++skipped; continue; }
// Из стены предмет не выталкивается, в отличие от твари: генератор
// кладёт лут только на достижимый пол, и «поправить» его здесь значило
// бы прятать ошибку раскладки вместо того, чтобы её увидеть.
Редкости и аффиксы: находка стала решением (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
SpawnFloorItem(world, RollItem(item, depth, rng), s.pos);
Tile2D: движок отдельным продуктом, игра — проектом в нём (M11-M20) Десять вех одним состоянием: разделять их пофайлово нечем, потому что src/game.cpp менялся почти в каждой. Собирается начисто без предупреждений, все проверки проходят. ДВИЖОК (engine/, цель tile2d) Каталог свой, и решение «это движок» принимается тем, куда положен файл. Компилятор нашёл три зависимости движка от игры, и все сняты: размеры кадра и карты уехали в engine/config.h, потолок населения этажа стал своим (MAX_LEVEL_SPAWNS против игровой MAX_TARGETS), запасная точка старта стала центром карты вместо позиции отряда. engine/projection.* единственное место, где движок знает про изометрию; видов сразу два — абстракция с одной реализацией ничем не проверена и обычно оказывается неправильной engine/project.* манифест проекта: пути, вид, кривая глубины engine/app.* окно, кадр, fixed timestep — вместо двух копий цикла engine/tilemap_draw движок научился показывать свою карту РЕДАКТОР (editor/, цель tile2d_editor) Открывает любой проект и ни одной игры по имени не знает. --tool одиннадцать безоконных команд: комнаты, каталог, кривая, сборка этажа, ASCII-карта, проверка набора. Правку делают файлами, последствия читают командой (docs/19-agent.md) терминал снизу: те же инструменты, git, сборка и запросы к claude/codex, открытый проект уходит потомкам через TILE2D_PROJECT ПРИМЕР (samples/walk, цель tile2d_walk) Второй потребитель движка. Игра и редактор самостоятельность движка проверить не могут: редактор мира не рисует, а игра — то место, куда общий код и уезжает незаметно. ИГРА (src/) Экстракшен: вес и два порога переноски, конечный боезапас, расходники применяются сами, общая сумка отряда, трата опыта на улучшения. Каждое из правил проверяется прогоном, а не глазами, и проверки поймали: вес дважды не связывал ничего (мерил по бинтам прогона, а не по добыче); боец с клинком не тратил найденные патроны при запасном стволе в сумке; массив виджетов экрана переполнился и молча съел хвост схрона; проверка улучшений сама ломала забег, вызывая NewRun посреди спуска. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 01:07:19 +07:00
++placed;
}
if (unknown) *unknown = skipped;
return placed;
}