squad-proto/docs/17-extraction.md
z.kirill 3ebd1f96fc Дерево улучшений вместо четырёх кнопок (M32)
Четыре строки покупок (патроны, переноска, обзор, живучесть) удалены
целиком. Выбора в них не было: купить в итоге можно было всё, порядок ни
на что не влиял, и через две вылазки экран превращался в кнопку
"продолжить".

Дерево: пять ветвей по восемь узлов, у каждого пять уровней. Ветви - это
пять способов закрыть разрыв из закона масштаба: стволы, клинки, стихии,
снабжение, отряд. Ветвь открывается цепочкой - узел доступен, когда взят
предыдущий в этой же ветви. Свободная сетка читалась бы как список;
цепочка читается как путь.

Ни один узел не заводит нового канала влияния: каждый кладёт своё число
туда, куда уже кладут снаряжение и аффиксы. Дерево будет расти, и первый
же "особенный" узел со своей веткой в коде превратил бы SyncLoadout в
свалку.

ЭКОНОМИКА. --tool economy поймал, что первая версия дерева стоит 9%
одного прохода 1..100: опыт растёт с глубиной квадратично, а дерево с
фиксированными ценами выкупается целиком за половину глубокого прохода, и
выбор кончается к сороковому этажу навсегда.

Введено общее удорожание: каждая вложенная точка поднимает цену всех
следующих (цена = база · 1.5^уровень · (1 + 0.15 · вложено)). Полное
дерево стоит 34 млн против 41 млн, заложенных в дизайне, то есть около
шести с половиной полных проходов.

Старые покупки из профиля читаются, но не применяются: потраченный на них
опыт возвращается в банк. Молча съесть чужие двадцать часов нельзя.

Проверка меряет ПРОВОДКУ по всем пяти ветвям, а не таблицу узлов:

  дерево: куплено уровней 45, закрытый узел закрыт
          урон ствола 12.0 -> 14.4, здоровье 100 -> 150, скорость 1.06 -> 1.22
          парирование 0.34 -> 0.49, свет 420 -> 620

Попутно: в проекте hollow бегуны и хваты уехали с первых этажей глубже -
там одиночка с ножом гиб на первом же этаже по той же причине, по которой
это было в основном проекте.

Формат профиля 7: строки tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 19:24:50 +03:00

34 KiB
Raw Permalink Blame History

17 — Экстракшен: спуск как ставка

Что изменилось в игре

До этой вехи глубина была прогрессом: чем дальше, тем лучше. Теперь она ставка. Отряд уходит вниз с тем, что нажил, и каждый этаж — заново заданный вопрос: ещё ниже или назад, пока есть что выносить.

Три правила, из которых следует всё остальное:

  1. Унести можно только то, что на выживших. Павший забирает своё с собой.
  2. Полный вайп оставляет около половины добычи, и чем вещь реже, тем охотнее она теряется: обычное доезжает в 68% случаев, реликт — в 24%.
  3. Опыт платится за убийства и сразу. Выносить надо ВЕЩИ.
  4. Достигнутая глубина остаётся навсегда. Она открывает чекпоинты — и это единственное, чего смерть не отнимает.

Второе и третье правила заменили прежние «вайп не возвращает ничего» и «опыт зачисляется при эвакуации». Причина одна на оба: вылазка, кончившаяся вайпом на тридцатом этаже, не оставляла ни следа, и единственным разумным поведением становилось уходить наверх при первой царапине. Половина сохраняет ставку (терять по-прежнему больно) и оставляет смысл идти глубже, а отнимать за смерть ещё и понимание того, что игрок уже сделал, незачем.

Замерено на сотне вайпов: доезжает 52% добычи. На одном вайпе это не считается — «у меня уцелел реликт» и «у меня не уцелел» одинаково ничего не значат.

Третье правило — ответ на «а если этажей десять тысяч». Проходить их заново каждую вылазку не нужно; терять при этом можно только снаряжение.

Два выхода с этажа

Где Клавиша Что делает
спуск самая дальняя достижимая точка 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 (50) этажей. Достигнутая глубина открывает все чекпоинты не глубже неё: дошёл до 120-го — доступны D1, D51, D101.

Пятьдесят, а не пять: забег — это ДОЛГИЙ спуск, а не пятиэтажная петля. С шагом в пять вылазка превращалась в перебежку между сохранениями, и весь риск умещался в пять этажей.

выбор глубины

Рекорд глубины засчитывается любым исходом. Дошёл — значит дошёл; требовать за чекпоинт ещё и удачную эвакуацию значило бы наказывать дважды за одну смерть.

Пять — это «достаточно редко, чтобы дойти до следующего было событием, и достаточно часто, чтобы потеря забега не откатывала в самое начало».

Сумка одна на отряд

Раньше каждый боец таскал свои шесть ячеек, и это порождало проблему, которую нечем было решить: пачка патронов, поднятая бойцом с клинком, доставалась только ему и лежала мёртвым грузом до конца забега. Перекладывать между бойцами игра не умела.

Теперь сумка одна — шестнадцать ячеек на пятерых. Личным остаётся надетое: руки, броня, карман. Их не разделить, они и есть «кто этот боец».

Вес делится на живых. Потерять бойца — значит взвалить его долю на остальных: обратно наверх отряд пойдёт медленнее, чем шёл сюда. Это не побочный эффект, а то, ради чего сумка общая.

Правило «унести можно только то, что на выживших» этим не ослаблено, оно стало точнее: надетое павшего теряется по-прежнему, а общая ноша на нём не висела — её тащил отряд. Вайп по-прежнему не выносит ничего: тогда выживших нет вовсе, и сумку некому донести.

Схрон и сборы

экран отряда со схроном

Экран отряда (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% обзора снимаются вместе с зарядом. Иначе «сел» означало бы «стал слабее», а не «кончился».

Откуда берутся оперативники

Их находят. Капсула лежит на этаже, как находка, только внутри человек: подошёл — он вышел и встал в строй. С этого момента он такой же боец, как все, и теряется так же.

Редкость двойная (sim/profile.h): в среднем раз в RESCUE_EVERY (100) этажей и не чаще, чем раз в RESCUE_COOLDOWN (20). Редко намеренно — пятый оперативник обязан быть событием забега.

У каждого свои числа: три небольшие прибавки (здоровье, скорость, обзор) из одного бюджета, розданные случайно. Не «класс» и не «ранг»: разница в характере, а не в силе, иначе находка превратилась бы в лотерею, где плохой наёмник хуже пустого слота.

Кнопку «нанять за опыт» убрали. Она была первой версией и оказалась скучной ровно в том смысле, в каком скучна любая кнопка «стать сильнее»: ничего не рассказывает и никуда не зовёт.

Чего ещё нет и что обсуждалось: ростер сверх пятерых, отправка свободных на задания, пассивные бонусы от сидящих на базе.

Отряд из одного

экран улучшений

Игра начинается одним оперативником-исследователем: тактический нож, слабый фонарь, куртка. Не отрядом — человеком. Остальные четверо нанимаются за опыт, и это самая дорогая покупка в игре: 600 / 2400 / 9600 / 38400, всего 51000 против дохода в 726 за этаж на десятом (--tool economy).

Дорого намеренно. Одиночка с ножом — это первое впечатление от игры, и продать второго бойца за одну вылазку значило бы выбросить весь хоррор начала: вдвоём уже не страшно. По кривой второй приходит через две-три ранние вылазки, третий — когда отряд уверенно берёт десятый этаж, пятый — ближе к двадцать пятому.

Замерено в --descend: одинокий оперативник с ножом на первом этаже выживает и убивает двух тварей, теряя 59 из 100 здоровья. Это ровно та цена, ради которой всё и затевалось: он справляется, но после второй схватки решение «ещё этаж или наверх» принимается всерьёз.

Внутри кода размер отряда — это ДВА разных понятия, и путать их нельзя:

что значит кто читает
NUM_AGENTS сколько слотов БЫВАЕТ (5, время сборки) все циклы, массивы, метрики
Squad::roster сколько слотов ЗАНЯТО сейчас сборка этажа
Agent::present этот слот занят человеком рендер, HUD, экраны, профиль
Agent::alive этот человек ещё жив правила боя, движение, твари

Правила боя читают только alive: для них пустой слот и павший неразличимы, и это верно. Разница нужна там, где показывают ЧЕЛОВЕКА — у павшего есть тело на полу и перечёркнутая карточка, у пустого слота нет ни того, ни другого. Иначе игрок с одним оперативником видел бы вокруг него четыре чужих трупа.

Делать NUM_AGENTS динамическим не стали: это сто с лишним мест ради числа, которое всё равно упирается в пять.

Во что вкладывается опыт: дерево

дерево улучшений

Четыре строки покупок (патроны, переноска, обзор, живучесть) удалены целиком. Выбора в них не было: купить в итоге можно было всё, порядок ни на что не влиял, и через две вылазки экран превращался в кнопку «продолжить».

Дерево (sim/tree.h) — пять ветвей по восемь узлов, у каждого узла пять уровней. Разница с прежним ровно одна: выбор есть, потому что всё не взять.

Ветвь Про что Примеры узлов
FIREARMS огонь: урон, пробитие, боезапас STEADY HANDS, HARD ROUNDS, EXECUTIONER
BLADES ближний бой: урон, парирование, темп SIDESTEP, DUELLIST, CLEAVE
ELEMENTS реакции и статусы CATALYST, DEEP BURN, CHAIN THEORY
SUPPLY ресурсы вылазки LONG WICK, PACK RAT, BLACK BOX
SQUAD живучесть и люди VIGOR, PLATING, LAST STAND

Ветвь открывается цепочкой: узел доступен, когда предыдущий в его ветви взят хотя бы на уровень. Свободная сетка читалась бы как список; цепочка читается как путь — «я иду в стихии» видно по дереву, а не по таблице цифр.

Ни один узел не заводит нового канала влияния. Каждый кладёт своё число туда, куда уже кладут снаряжение и аффиксы: статы бойца, множители оружия, ресурсы вылазки. Правило то же, что и у предметов, и здесь оно важнее вдвойне — дерево растёт, и первый же «особенный» узел со своей веткой в коде превратил бы SyncLoadout в свалку.

Цена растёт трижды

цена = база(узла) · 1.5^уровень · (1 + 0.15 · всего_вложеноочек)

Третий множитель — главный, и без него дерево умирает. Опыт растёт с глубиной квадратично (--tool economy: сотый этаж даёт 190 тысяч), и дерево с фиксированными ценами выкупается целиком за половину одного глубокого прохода. Первая версия так и вышла: всё дерево стоило 9% одного прохода 1..100 — то есть к сороковому этажу выбор кончался навсегда. С удорожанием полное дерево стоит 34 млн против 41 млн, заложенных в 20-gdd.md, то есть около шести с половиной полных проходов.

Что стало со старыми покупками

Строка upgrades в профиле читается, но не применяется: потраченный на неё опыт возвращается в банк. Игрок вложит его заново, уже в узлы. Молча съесть чужие покупки нельзя — это те же двадцать часов.

Проверяется в --descend проводкой до боя, а не таблицей узлов:

дерево: куплено уровней 45, закрытый узел закрыт
        урон ствола 12.0 -> 14.4, здоровье 100 -> 150, скорость 1.06 -> 1.22
        парирование 0.34 -> 0.49, свет 420 -> 620

Узел, который красиво нарисован и ничего не делает, выглядит снаружи ровно как работающий. На этом уже дважды попались вес и стихии.

Итог забега

итог вылазки

Две цифры, ради которых экран и существует: сколько вынес и сколько осталось внизу. Плюс глубина, выжившие и открытый чекпоинт.

Забег закрывается ДО показа экрана — в момент вайпа или нажатия 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).