Предмет перестал быть строкой каталога. 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>
28 KiB
17 — Экстракшен: спуск как ставка
Что изменилось в игре
До этой вехи глубина была прогрессом: чем дальше, тем лучше. Теперь она ставка. Отряд уходит вниз с тем, что нажил, и каждый этаж — заново заданный вопрос: ещё ниже или назад, пока есть что выносить.
Три правила, из которых следует всё остальное:
- Унести можно только то, что на выживших. Павший забирает своё с собой.
- Полный вайп не возвращает ничего. Снаряжение сбрасывается к стартовому.
- Достигнутая глубина остаётся навсегда. Она открывает чекпоинты — и это единственное, чего смерть не отнимает.
Третье правило — ответ на «а если этажей десять тысяч». Проходить их заново каждую вылазку не нужно; терять при этом можно только снаряжение.
Два выхода с этажа
| Где | Клавиша | Что делает | |
|---|---|---|---|
| спуск | самая дальняя достижимая точка | F |
следующий этаж, отряд как есть |
| эвакуация | там, где отряд вошёл на этаж | E |
забег окончен, добыча в схроне |
Эвакуация совпадает с точкой входа не из экономии: это лестница наверх, и в этом вся суть напряжения. Спуск лежит на дальнем конце этажа — решив уйти, отряд идёт обратно через всё, что успел растревожить, раненый и с полными руками.
Кольца на полу разного цвета: зелёное — вниз, голубое — наверх. Оба видны даже в полной темноте. Спутать их означает уйти не туда, и цена ошибки здесь — весь забег, поэтому подсказки в HUD подкрашены теми же цветами.
Ни то ни другое не срабатывает от захода на клетку. Спуск и эвакуация — решения игрока: этаж не зачищен, лут не собран, уходить рано — это нормальный выбор, и отряд может стоять на выходе сколько угодно.
Профиль
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.
Рекорд глубины засчитывается любым исходом. Дошёл — значит дошёл; требовать за чекпоинт ещё и удачную эвакуацию значило бы наказывать дважды за одну смерть.
Пять — это «достаточно редко, чтобы дойти до следующего было событием, и достаточно часто, чтобы потеря забега не откатывала в самое начало».
Сумка одна на отряд
Раньше каждый боец таскал свои шесть ячеек, и это порождало проблему, которую нечем было решить: пачка патронов, поднятая бойцом с клинком, доставалась только ему и лежала мёртвым грузом до конца забега. Перекладывать между бойцами игра не умела.
Теперь сумка одна — шестнадцать ячеек на пятерых. Личным остаётся надетое: руки, броня, карман. Их не разделить, они и есть «кто этот боец».
Вес делится на живых. Потерять бойца — значит взвалить его долю на остальных: обратно наверх отряд пойдёт медленнее, чем шёл сюда. Это не побочный эффект, а то, ради чего сумка общая.
Правило «унести можно только то, что на выживших» этим не ослаблено, оно стало точнее: надетое павшего теряется по-прежнему, а общая ноша на нём не висела — её тащил отряд. Вайп по-прежнему не выносит ничего: тогда выживших нет вовсе, и сумку некому донести.
Схрон и сборы
Экран отряда (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 раз дальше. Отряд не слепнет: он
меняется с тварями местами, и первым теперь видит не он.
Главное решение — заряд тратится за пройденное расстояние, а не за время.
Точнее: за расстояние, пройденное под управлением игрока (состояние
ADVANCING). Топтание в строю не считается — замер показал, что центр отряда за
две секунды «стоянки» проходит около 0.8 тайла, и списывай игра это, заряд таял
бы, пока игрок думает над картой. То есть вернулся бы тот самый таймер. Драка
тоже бесплатна: свет тратит РАЗВЕДКА, а не бой.
Расход по секундам был бы таймером, только без циферблата: садящийся сам собой фонарь торопил бы игрока ровно так же, а думать над этажом стало бы дорого. Здесь думать бесплатно. Стоять и разглядывать — бесплатно. Платить приходится за то, что обошёл этаж целиком, а не за то, что задержался.
Получается тот же размен, что и с весом, только с другой стороны:
| ресурс | что он делает дорогим |
|---|---|
| вес | унести всё |
| свет | обшарить всё |
Прямая дорога к спуску дёшева обоими. Полная зачистка — дорога обоими сразу.
Чем глубже, тем дороже шаг (LightDrain): на сотом этаже заряда хватает вдвое
меньше, чем на первом, а сами этажи там длиннее. Заряд едет вниз вместе с
отрядом и не обновляется на новом этаже — иначе он был бы ресурсом этажа, а
не вылазки, и никуда бы не гнал.
Батарея (0.3 веса) — расходник по тем же правилам, что бинт и пачка: встаёт в фонарь сама, в тот же шаг, когда свет погас. Держать её в сумке при погасшем фонаре отряду незачем, а темнота ради темноты — не решение игрока.
Погасший фонарь не даёт ничего: его 35% обзора снимаются вместе с зарядом. Иначе «сел» означало бы «стал слабее», а не «кончился».
Отряд из одного
Игра начинается одним оперативником-исследователем: тактический нож, слабый
фонарь, куртка. Не отрядом — человеком. Остальные четверо нанимаются за опыт,
и это самая дорогая покупка в игре: 600 / 2400 / 9600 / 38400, всего 51000
против дохода в 726 за этаж на десятом (--tool economy).
Дорого намеренно. Одиночка с ножом — это первое впечатление от игры, и продать второго бойца за одну вылазку значило бы выбросить весь хоррор начала: вдвоём уже не страшно. По кривой второй приходит через две-три ранние вылазки, третий — когда отряд уверенно берёт десятый этаж, пятый — ближе к двадцать пятому.
Замерено в --descend: одинокий оперативник с ножом на первом этаже выживает
и убивает двух тварей, теряя 59 из 100 здоровья. Это ровно та цена, ради
которой всё и затевалось: он справляется, но после второй схватки решение
«ещё этаж или наверх» принимается всерьёз.
Внутри кода размер отряда — это ДВА разных понятия, и путать их нельзя:
| что значит | кто читает | |
|---|---|---|
NUM_AGENTS |
сколько слотов БЫВАЕТ (5, время сборки) | все циклы, массивы, метрики |
Squad::roster |
сколько слотов ЗАНЯТО сейчас | сборка этажа |
Agent::present |
этот слот занят человеком | рендер, HUD, экраны, профиль |
Agent::alive |
этот человек ещё жив | правила боя, движение, твари |
Правила боя читают только alive: для них пустой слот и павший неразличимы, и
это верно. Разница нужна там, где показывают ЧЕЛОВЕКА — у павшего есть тело на
полу и перечёркнутая карточка, у пустого слота нет ни того, ни другого. Иначе
игрок с одним оперативником видел бы вокруг него четыре чужих трупа.
Делать NUM_AGENTS динамическим не стали: это сто с лишним мест ради числа,
которое всё равно упирается в пять.
Во что вкладывается опыт
Опыт копился с вехи M12 и до сих пор был честной заглушкой: число, которое ни на что не влияет. Теперь он тратится — на четыре улучшения отряда.
| что даёт | почему это | |
|---|---|---|
| AMMO RESERVE | +20 патронов на вылазку | |
| CARRY LIMIT | +1.5 до перегруза | |
| SIGHT RANGE | +6% обзора | |
| VIGOR | +12 здоровья |
Все четыре — ресурсные, и ни одного силового. Игра про исчерпание, и вкладываться в ней надо в то, что кончается: отряд, который бьёт вдвое сильнее, перестаёт бояться, а страх тут единственная механика, которую нельзя восстановить балансом.
Улучшения ОТРЯДНЫЕ, а не на бойца. Не из экономии: личным в этой игре остаётся снаряжение, и второе личное измерение превратило бы сборы в бухгалтерию на пятерых.
Опыт зачисляется только за успешную эвакуацию (FinishRun): вайп не
приносит ничего. Это та же ставка, что и с добычей.
Приёмки это не касается вообще: улучшения живут в профиле, а --accept его не
подключает — там нет ни снаряжения, ни улучшений, и критерии меряются на том
же, на чём мерялись всегда.
Итог забега
Две цифры, ради которых экран и существует: сколько вынес и сколько осталось внизу. Плюс глубина, выжившие и открытый чекпоинт.
Забег закрывается ДО показа экрана — в момент вайпа или нажатия E. Симуляция
при этом ничего не решает: она только отмечает atExit/atExtract, а конец
забега объявляет UI. Смена мира посреди шага, между движением и стрельбой,
оставила бы половину систем со ссылками на снесённые сущности.
Как играть
squad_proto.exe --rooms
NEW RAID → выбрать чекпоинт → вылазка. I перед выходом — разобрать схрон.
| Ключ | Что делает |
|---|---|
--profile <файл> |
другой профиль (полезно для проверок) |
--no-profile |
не читать и не писать профиль вообще |
Без --rooms игра работает как раньше: одна карта, никакого забега. Приёмка и
--headless профиль не подключают — критерии меряются на прежнем снаряжении.
Проверки
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
Расход патронов проверяется тут же и отдельно: спуск ни с кем не воюет, столбец «патронов» в таблице стоит на месте — и без прямой проверки бесконечный боезапас выглядел бы ровно так же.
У света первая строка проверки — не про баланс, а про жанр: заряд на
стоянке обязан стоять, и порог у неё в тайлах, а не в заряде. Один и тот же
шаг стоит тем дороже, чем глубже (LightDrain), и порог в единицах заряда
объявлял таймером обычное топтание на шестидесятом этаже, пропуская его на
восьмом. Написать таймер случайно очень легко (одна строка со
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).



