#pragma once // ----------------------------------------------------------------------------- // Дерево улучшений отряда. // // ЧТО БЫЛО И ПОЧЕМУ ЭТОГО МАЛО. До этой вехи опыт тратился на четыре строки: // патроны, переноска, обзор, живучесть. Каждая — просто «+число за уровень». // Выбора там не было вообще: купить в итоге можно всё, порядок ни на что не // влияет, и через две вылазки экран улучшений превращался в кнопку «продолжить». // // Дерево устроено иначе, и разница ровно в одном: ВЫБОР ЕСТЬ, ПОТОМУ ЧТО ВСЁ НЕ // ВЗЯТЬ. Узлы стоят дорого и дорожают, ветвь открывается по цепочке, и опыт — // единственная валюта на все пять ветвей сразу. // // Пять ветвей — это пять способов закрыть разрыв из закона масштаба // (docs/20-gdd.md §2): угроза растёт квадратично, сила отряда — линейно, и // разрыв закрывается ЗНАНИЕМ. Ветви и есть виды знания: // // СТВОЛ — про огонь: урон, пробитие, боезапас; // КЛИНОК — про ближний бой: урон, парирование, скорость; // СТИХИИ — про реакции и статусы: то, чем берут защищённых; // СНАБЖЕНИЕ — про ресурсы вылазки: свет, переноска, патроны; // ОТРЯД — про живучесть и людей: здоровье, броня, ярость. // // НИ ОДИН УЗЕЛ НЕ ЗАВОДИТ НОВОГО КАНАЛА ВЛИЯНИЯ. Каждый кладёт своё число туда, // куда уже кладут снаряжение и аффиксы (sim/affix.h): статы бойца, множители // оружия, ресурсы вылазки. Это то же правило, что и у предметов, и оно тут // важнее вдвойне — дерево растёт, и первый же «особенный» узел со своей веткой // в коде превратил бы SyncLoadout в свалку. // ----------------------------------------------------------------------------- #include // Ветви. Порядок — порядок столбцов на экране. enum class Branch : uint8_t { GUN = 0, BLADE, ELEMENT, SUPPLY, SQUAD, COUNT, }; constexpr int BRANCH_COUNT = int(Branch::COUNT); constexpr int NODES_PER_BRANCH = 8; constexpr int TREE_NODES = BRANCH_COUNT * NODES_PER_BRANCH; inline const char* BranchName(Branch b) { switch (b) { case Branch::GUN: return "FIREARMS"; case Branch::BLADE: return "BLADES"; case Branch::ELEMENT: return "ELEMENTS"; case Branch::SUPPLY: return "SUPPLY"; default: return "SQUAD"; } } // Что узел даёт. Тег — адрес числа в уже существующих каналах, не «новая // механика». Если для эффекта пришлось бы заводить новый канал, такому узлу // в дереве не место, пока канал не появился сам по себе. enum class NodeEffect : uint8_t { GUN_DAMAGE = 0, // множитель урона стволов GUN_PEN, // пробитие сопротивлений AMMO, // запас патронов на вылазку RELOAD, // скорость перезарядки BLADE_DAMAGE, // множитель урона клинков PARRY, // шанс парирования MELEE_SPEED, // темп ударов клинка REACTION, // сила реакций стихий STATUS, // длительность статусов CONVERT, // доля урона в стихию оружия LIGHT, // заряд фонаря CARRY, // норма переноски SALVAGE, // сколько добычи уцелевает при вайпе MATERIALS, // сколько материалов приносит вылазка HEALTH, // запас здоровья ARMOR, // броня SPEED, // скорость хода SIGHT, // радиус обзора RAGE, // ярость отряда за павшего COUNT, }; const char* NodeEffectName(NodeEffect e); // Одна запись дерева. Данные, а не код: узлы правят таблицей, глядя на неё // целиком, и добавление узла не требует трогать ни одного алгоритма. struct TreeNode { const char* name = "-"; Branch branch = Branch::GUN; int row = 0; // место в ветви, оно же порядок открытия NodeEffect effect = NodeEffect::GUN_DAMAGE; float perRank = 0.0f; // сколько даёт один уровень int maxRank = 5; float costBase = 0.0f; // цена первого уровня; дальше растёт }; // Таблица всех узлов. Определена в tree.cpp. const TreeNode& TreeNodeAt(int index); // Индекс узла по ветви и месту в ней. Экран рисует сетку, а профиль хранит // плоский массив — переводит одно в другое эта пара функций. inline int TreeIndex(Branch b, int row) { return int(b) * NODES_PER_BRANCH + row; } // Цена СЛЕДУЮЩЕГО уровня. // // Растёт трижды, и каждый множитель отвечает за своё: // * место узла в ветви — нижние узлы сильнее и дороже, иначе выгодно было бы // докупать верхние по мелочи и никуда не спускаться; // * уровень узла — «добить до максимума» обязано быть отдельным решением; // * СКОЛЬКО ВСЕГО УЖЕ ВЛОЖЕНО — каждая взятая точка поднимает цену всех // следующих. // // Третий множитель — главный, и без него дерево умирает. Опыт растёт с глубиной // квадратично (`--tool economy`: этаж 100 даёт 190 тысяч), а дерево с // фиксированными ценами выкупается целиком за половину одного глубокого // прохода. Первая версия так и вышла: всё дерево стоило 9% одного прохода // 1..100, то есть к сороковому этажу выбор кончался навсегда. constexpr float TREE_ESCALATION = 0.15f; float TreeNodeCost(int index, int currentRank, int totalRanks); // Требование: узел открывается, когда предыдущий в ЕГО ВЕТВИ взят хотя бы на // первый уровень. Цепочка, а не свободная сетка, — потому что ветвь должна // читаться как путь: «я иду в стихии» видно по дереву, а не по таблице цифр. inline int TreeNodeRequires(int index) { const int row = index % NODES_PER_BRANCH; return (row == 0) ? -1 : index - 1; } // ----------------------------------------------------------------------------- // Итог дерева: сумма всех взятых уровней. Считается один раз при сборке этажа, // дальше читается как готовые числа — лазать по узлам из правил боя нельзя, там // про дерево не знают вообще. // ----------------------------------------------------------------------------- struct TreeBonus { float gunDamage = 0.0f; // доля, прибавка к множителю float gunPen = 0.0f; float ammo = 0.0f; float reload = 0.0f; // доля, ускорение перезарядки float bladeDamage = 0.0f; float parry = 0.0f; float meleeSpeed = 0.0f; float reaction = 0.0f; float status = 0.0f; float convert = 0.0f; float light = 0.0f; float carry = 0.0f; float salvage = 0.0f; float materials = 0.0f; float health = 0.0f; float armor = 0.0f; float speed = 0.0f; float sight = 0.0f; float rage = 0.0f; void Add(NodeEffect e, float amount); };