# 17 — Экстракшен: спуск как ставка ## Что изменилось в игре До этой вехи глубина была прогрессом: чем дальше, тем лучше. Теперь она **ставка**. Отряд уходит вниз с тем, что нажил, и каждый этаж — заново заданный вопрос: ещё ниже или назад, пока есть что выносить. Три правила, из которых следует всё остальное: 1. **Унести можно только то, что на выживших.** Павший забирает своё с собой. 2. **Полный вайп не возвращает ничего.** Снаряжение сбрасывается к стартовому. 3. **Достигнутая глубина остаётся навсегда.** Она открывает чекпоинты — и это единственное, чего смерть не отнимает. Третье правило — ответ на «а если этажей десять тысяч». Проходить их заново каждую вылазку не нужно; терять при этом можно только снаряжение. ## Два выхода с этажа | | Где | Клавиша | Что делает | |---|---|---|---| | **спуск** | самая дальняя достижимая точка | `F` | следующий этаж, отряд как есть | | **эвакуация** | там, где отряд вошёл на этаж | `E` | забег окончен, добыча в схроне | Эвакуация совпадает с точкой входа не из экономии: это лестница наверх, и в этом вся суть напряжения. Спуск лежит на дальнем конце этажа — решив уйти, отряд идёт обратно через всё, что успел растревожить, раненый и с полными руками. Кольца на полу разного цвета: зелёное — вниз, голубое — наверх. Оба видны даже в полной темноте. Спутать их означает уйти не туда, и цена ошибки здесь — весь забег, поэтому подсказки в HUD подкрашены теми же цветами. Ни то ни другое не срабатывает от захода на клетку. Спуск и эвакуация — решения игрока: этаж не зачищен, лут не собран, уходить рано — это нормальный выбор, и отряд может стоять на выходе сколько угодно. ## Профиль [src/sim/profile.h](../src/sim/profile.h) — всё, что переживает забег: ``` maxdepth 13 runs 6 extractions 3 agent 0 cleaver plate charm bandage stimpack - - - - stash saber plate vest smg bow charm lantern spurs ``` - **снаряжение пяти бойцов** лежит здесь, а не собирается заново при старте. То, в чём отряд ушёл вниз, — это то, в чём он вернулся с прошлой вылазки; - **схрон** — общий запас сверх надетого, потолок `STASH_CAP`. Без потолка экстракшен превращается в склад, и решение «что взять вниз» исчезает; - **максимальная глубина** открывает чекпоинты. Профиль — состояние ИГРОКА, а не мира: он не в `ecs::World` и не сбрасывается ни `Reset`, ни `Descend`. Записывается он только в конце забега — авария посреди вылазки означает потерю, и это часть контракта, а не недоделка. Формат текстовый по той же причине, что у комнат: файл лежит рядом с игрой, его правят руками при отладке, и он обязан читаться глазами. ## Чекпоинты Каждые `CHECKPOINT_STEP` (5) этажей. Достигнутая глубина открывает все чекпоинты не глубже неё: дошёл до 13-го — доступны D1, D6, D11. ![выбор глубины](ui-depth.png) Рекорд глубины засчитывается **любым исходом**. Дошёл — значит дошёл; требовать за чекпоинт ещё и удачную эвакуацию значило бы наказывать дважды за одну смерть. Пять — это «достаточно редко, чтобы дойти до следующего было событием, и достаточно часто, чтобы потеря забега не откатывала в самое начало». ## Сумка одна на отряд Раньше каждый боец таскал свои шесть ячеек, и это порождало проблему, которую нечем было решить: пачка патронов, поднятая бойцом с клинком, доставалась только ему и лежала мёртвым грузом до конца забега. Перекладывать между бойцами игра не умела. Теперь сумка одна — шестнадцать ячеек на пятерых. Личным остаётся **надетое**: руки, броня, карман. Их не разделить, они и есть «кто этот боец». **Вес делится на живых.** Потерять бойца — значит взвалить его долю на остальных: обратно наверх отряд пойдёт медленнее, чем шёл сюда. Это не побочный эффект, а то, ради чего сумка общая. Правило «унести можно только то, что на выживших» этим не ослаблено, оно стало точнее: надетое павшего теряется по-прежнему, а общая ноша на нём не висела — её тащил отряд. Вайп по-прежнему не выносит ничего: тогда выживших нет вовсе, и сумку некому донести. ## Схрон и сборы ![экран отряда со схроном](ui-squad-stash.png) Экран отряда (`I`) стал местом сборов: снаряжение бойца слева, общая сумка справа, схрон двумя рядами внизу. Предметы перетаскиваются мышью, `Q`/`E` листают схрон, правая кнопка по вещи выбрасывает её под ноги. **Наведи — и увидишь, что это.** Подсказка под курсором показывает вес и все эффекты предмета, а на строках характеристик объясняет, откуда берётся число. До неё вещь была именем и цветным квадратиком: чем VEST отличается от JACKET, знал только каталог в коде. С сумкой на шестнадцать мелких ячеек это перестало быть терпимым — в ячейку не влезает даже имя. Схрон полон — обмен не состоится целиком, и вещь останется в слоте. Молча терять её нельзя: «некуда положить» обязано быть видно. ## Вес: что унести Шесть ячеек в сумке отвечали на вопрос «сколько вещей», но не на вопрос «чего они стоят»: шесть бинтов и шесть плит обходились одинаково. Теперь у каждой вещи есть вес, а у бойца — два порога. | | | |---|---| | до `CARRY_LIMIT` (7.5) | отряд идёт как шёл | | дальше | тем медленнее, чем больше нагрёб, до 55% на пределе | | выше `CARRY_MAX` (14) | не поднимет: вещь остаётся лежать | Порогов ДВА намеренно. Один жёсткий предел означал бы «сумка кончилась» — то же самое, что счёт ячеек, только считать труднее. Настоящий выбор возникает там, где лишнее взять МОЖНО, но за это платят: а идти с этим — обратно через весь этаж, который уже растревожен. **Надетое не считается.** За броню уже заплачено множителем скорости (плита — 0.82), и считать её вес вторично значило бы наказывать за один выбор дважды. Хуже того: плита съедала бы почти весь запас, и боец в тяжёлой броне не мог бы поднять вообще ничего — вместо решения вышел бы запрет. А вот плита, НАЙДЕННАЯ и лежащая в сумке, весит полной мерой: тащить её наверх — это и есть выбор. Выбросить лишнее можно правой кнопкой по ячейке на экране отряда: вещь ложится под ноги и подбирается обратно. Из рук бросать нельзя — безоружный боец не бежит быстрее, он просто перестаёт быть бойцом. ## То, что кончается Наверх гонит не таймер. Часы заставляли бы торопиться механически, а это хоррор: давить должно **исчерпание**, а задача — уйти как можно глубже. **Патроны конечны.** Запас (`startAmmo`, 120 на бойца) выдаётся на вылазку и едет вниз вместе с отрядом. Перезарядка тянет из него; кончился — стрелок молчит, и перезарядка даже не начинается: боец с пустым стволом не изображает работу. Восполнить можно только находкой — пачкой на 40 патронов. **Расходники применяются сами.** Ручных действий в игре нет (спек 5), поэтому бинт и пачка — не кнопка, а поведение: боец перевязывается, когда ранен ниже `healAtFrac` и рядом никого нет, и набивает подсумок, когда стрелять нечем. Лечиться в схватке нельзя — перевязка посреди боя обесценила бы урон, который тварь только что нанесла. До этой вехи `heal` у бинта был мёртвым числом: расходники не применялись нигде, и бинт был просто грузом. С весом (выше) он стал грузом буквально. Патроны нужны тому, у кого **вообще** есть ствол, а не только тому, кто держит его сейчас: иначе боец с клинком в руках и запасным карабином в сумке таскал бы найденную пачку до конца забега. Это нашла проверка, а не глаз. ### Свет: второй ресурс, и тратится он за ПУТЬ Фонарь садится. Полного заряда (`LIGHT_FULL`, 420) хватает примерно на **четыре прямых пути до спуска** — то есть на этаж с осмотром, но не на два этажа подряд. Кончился — обзор падает до `DARK_SIGHT_MUL` (35%), а твари начинают замечать отряд в `DARK_AGGRO_MUL` раз дальше. Отряд не слепнет: он меняется с тварями местами, и первым теперь видит не он. **Главное решение — заряд тратится за пройденное расстояние, а не за время.** Расход по секундам был бы таймером, только без циферблата: садящийся сам собой фонарь торопил бы игрока ровно так же, а думать над этажом стало бы дорого. Здесь думать бесплатно. Стоять и разглядывать — бесплатно. Платить приходится за то, что **обошёл этаж целиком**, а не за то, что задержался. Получается тот же размен, что и с весом, только с другой стороны: | ресурс | что он делает дорогим | |---|---| | вес | унести всё | | свет | обшарить всё | Прямая дорога к спуску дёшева обоими. Полная зачистка — дорога обоими сразу. Чем глубже, тем дороже шаг (`LightDrain`): на сотом этаже заряда хватает вдвое меньше, чем на первом, а сами этажи там длиннее. Заряд едет вниз вместе с отрядом и **не обновляется на новом этаже** — иначе он был бы ресурсом этажа, а не вылазки, и никуда бы не гнал. Батарея (0.3 веса) — расходник по тем же правилам, что бинт и пачка: встаёт в фонарь сама, в тот же шаг, когда свет погас. Держать её в сумке при погасшем фонаре отряду незачем, а темнота ради темноты — не решение игрока. Погасший фонарь не даёт **ничего**: его 35% обзора снимаются вместе с зарядом. Иначе «сел» означало бы «стал слабее», а не «кончился». ## Отряд из одного ![экран улучшений](ui-upgrades.png) Игра начинается **одним оперативником-исследователем**: тактический нож, слабый фонарь, куртка. Не отрядом — человеком. Остальные четверо нанимаются за опыт, и это самая дорогая покупка в игре: 600 / 2400 / 9600 / 38400, всего 51000 против дохода в 726 за этаж на десятом (`--tool economy`). Дорого намеренно. Одиночка с ножом — это первое впечатление от игры, и продать второго бойца за одну вылазку значило бы выбросить весь хоррор начала: вдвоём уже не страшно. По кривой второй приходит через две-три ранние вылазки, третий — когда отряд уверенно берёт десятый этаж, пятый — ближе к двадцать пятому. Замерено в `--descend`: одинокий оперативник с ножом на первом этаже **выживает и убивает двух тварей, теряя 59 из 100 здоровья**. Это ровно та цена, ради которой всё и затевалось: он справляется, но после второй схватки решение «ещё этаж или наверх» принимается всерьёз. Внутри кода размер отряда — это ДВА разных понятия, и путать их нельзя: | | что значит | кто читает | |---|---|---| | `NUM_AGENTS` | сколько слотов БЫВАЕТ (5, время сборки) | все циклы, массивы, метрики | | `Squad::roster` | сколько слотов ЗАНЯТО сейчас | сборка этажа | | `Agent::present` | этот слот занят человеком | рендер, HUD, экраны, профиль | | `Agent::alive` | этот человек ещё жив | правила боя, движение, твари | Правила боя читают только `alive`: для них пустой слот и павший неразличимы, и это верно. Разница нужна там, где показывают ЧЕЛОВЕКА — у павшего есть тело на полу и перечёркнутая карточка, у пустого слота нет ни того, ни другого. Иначе игрок с одним оперативником видел бы вокруг него четыре чужих трупа. Делать `NUM_AGENTS` динамическим не стали: это сто с лишним мест ради числа, которое всё равно упирается в пять. ## Во что вкладывается опыт ![экран улучшений](ui-upgrades.png) Опыт копился с вехи M12 и до сих пор был честной заглушкой: число, которое ни на что не влияет. Теперь он тратится — на четыре улучшения отряда. | | что даёт | почему это | |---|---|---| | AMMO RESERVE | +20 патронов на вылазку | | | CARRY LIMIT | +1.5 до перегруза | | | SIGHT RANGE | +6% обзора | | | VIGOR | +12 здоровья | | Все четыре — **ресурсные**, и ни одного силового. Игра про исчерпание, и вкладываться в ней надо в то, что кончается: отряд, который бьёт вдвое сильнее, перестаёт бояться, а страх тут единственная механика, которую нельзя восстановить балансом. Улучшения ОТРЯДНЫЕ, а не на бойца. Не из экономии: личным в этой игре остаётся снаряжение, и второе личное измерение превратило бы сборы в бухгалтерию на пятерых. Опыт зачисляется только за **успешную эвакуацию** (`FinishRun`): вайп не приносит ничего. Это та же ставка, что и с добычей. Приёмки это не касается вообще: улучшения живут в профиле, а `--accept` его не подключает — там нет ни снаряжения, ни улучшений, и критерии меряются на том же, на чём мерялись всегда. ## Итог забега ![итог вылазки](ui-summary.png) Две цифры, ради которых экран и существует: **сколько вынес** и **сколько осталось внизу**. Плюс глубина, выжившие и открытый чекпоинт. Забег закрывается ДО показа экрана — в момент вайпа или нажатия `E`. Симуляция при этом ничего не решает: она только отмечает `atExit`/`atExtract`, а конец забега объявляет UI. Смена мира посреди шага, между движением и стрельбой, оставила бы половину систем со ссылками на снесённые сущности. ## Как играть ```bash squad_proto.exe --rooms ``` `NEW RAID` → выбрать чекпоинт → вылазка. `I` перед выходом — разобрать схрон. | Ключ | Что делает | |---|---| | `--profile <файл>` | другой профиль (полезно для проверок) | | `--no-profile` | не читать и не писать профиль вообще | Без `--rooms` игра работает как раньше: одна карта, никакого забега. Приёмка и `--headless` профиль не подключают — критерии меряются на прежнем снаряжении. ## Проверки ```bash squad_proto.exe --descend 7 --no-profile ``` Прогон проверяет всю цепочку: спуск по этажам, перенос сумок, эвакуацию, **круговой прогон профиля через запись** — и правило переноски. Переноска проверяется на ВЫДУМАННОМ инвентаре, а не по грузу в прогоне: в сумки кладутся бинты, чтобы проверить перенос между этажами, и по ним вес выглядит втрое меньше настоящего. Первые две попытки выставить пороги делались как раз по этой цифре — и обе дали числа, которые не ограничивали ничего. ``` расходники: патроны набиты, лечение сработало (33 -> 58), из сумки ушли обе перезарядка: запас 40 -> 16 (магазин 24) свет: стоянка 0.2, ход 1.10 за тайл при 1.10 в каталоге (прошли 12.7, продвинулись 1.6) глубина 8: заряда на 383 тайлов, это 4.0 прямых пути до спуска (средний 95, здешний 155) обзор 1.00 -> 0.35 в темноте, батарея встала сама переноска: налегке 6.9 (скорость 100%), с грузом 12.9 (скорость 63%), плит влезло 1 ``` Расход патронов проверяется тут же и отдельно: спуск ни с кем не воюет, столбец «патронов» в таблице стоит на месте — и без прямой проверки бесконечный боезапас выглядел бы ровно так же. У света первая строка проверки — **не про баланс, а про жанр**: заряд на стоянке обязан стоять. Написать таймер случайно очень легко (одна строка со `dt` вместо пути), а заметить на глаз невозможно — цифра в HUD тает одинаково. Расход сравнивается с числом из каталога, и первая версия проверки показала 1.55 за тайл при 1.10 в правилах. Расходился не свет, а измеритель: он делил потраченное на **смещение** отряда, а отряд шёл не по прямой. Считать надо путь пошагово — тогда сходится до сотой. Хватает ли заряда на этаж — считается в **этажах**, а не в тайлах, и по средней длине этажей забега: путь до спуска скачет вдвое от сида, и порог, выставленный по одному этажу, ловил бы удачу, а не баланс. ``` 7 RUST 13 19 3 149 живых 5, в сумках 30 эвакуация: вынесено 40, потеряно 0, выживших 5, чекпоинт D6 профиль: запись и чтение сходятся ``` Круговой прогон здесь не для полноты: запись профиля — единственный путь, которым игра может стереть всё нажитое, и ошибка в нём выглядит как «схрон куда-то делся», а не как падение. ### Как круговой прогон три вехи подтверждал то, чего не было Он сверял три поля из десяти — глубину, схрон и снаряжение — и всё это время `Profile::Save` **не писала ни версию формата, ни улучшения**. То есть купленный запас патронов действовал до конца запуска и молча пропадал при следующем чтении, а профиль без строки `version` читался как формат 1 и на каждом выходе прогонялся через все миграции. Пока миграции были пустыми, это ничего не портило и потому не всплывало; с приходом размера отряда такая запись начала бы возвращать игроку пятерых бойцов после каждой вылазки. Отсюда правило: **круговой прогон сверяет поля списком, и список растёт вместе с форматом**. Выборочная сверка проверяет не формат, а память того, кто её писал. И второе: прогон делается на НАБИТОМ профиле. Пустое поле сходится с пустым при любой поломке записи — «0 == 0» подтверждало работу того, чего в файле не было. ## Чего ещё нет - **Схватки за добычу.** В настоящем экстракшене выход можно не отдать — здесь на этаже нет никого, кто охотится именно за вынесенным. - **Страховки и торговли.** Схрон только копится; девать его некуда. - **Траты опыта** — она по-прежнему ждёт своей вехи ([16-descent.md](16-descent.md)).