Четыре строки покупок (патроны, переноска, обзор, живучесть) удалены
целиком. Выбора в них не было: купить в итоге можно было всё, порядок ни
на что не влиял, и через две вылазки экран превращался в кнопку
"продолжить".
Дерево: пять ветвей по восемь узлов, у каждого пять уровней. Ветви - это
пять способов закрыть разрыв из закона масштаба: стволы, клинки, стихии,
снабжение, отряд. Ветвь открывается цепочкой - узел доступен, когда взят
предыдущий в этой же ветви. Свободная сетка читалась бы как список;
цепочка читается как путь.
Ни один узел не заводит нового канала влияния: каждый кладёт своё число
туда, куда уже кладут снаряжение и аффиксы. Дерево будет расти, и первый
же "особенный" узел со своей веткой в коде превратил бы 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>
164 lines
9.0 KiB
C++
164 lines
9.0 KiB
C++
#pragma once
|
||
// -----------------------------------------------------------------------------
|
||
// Дерево улучшений отряда.
|
||
//
|
||
// ЧТО БЫЛО И ПОЧЕМУ ЭТОГО МАЛО. До этой вехи опыт тратился на четыре строки:
|
||
// патроны, переноска, обзор, живучесть. Каждая — просто «+число за уровень».
|
||
// Выбора там не было вообще: купить в итоге можно всё, порядок ни на что не
|
||
// влияет, и через две вылазки экран улучшений превращался в кнопку «продолжить».
|
||
//
|
||
// Дерево устроено иначе, и разница ровно в одном: ВЫБОР ЕСТЬ, ПОТОМУ ЧТО ВСЁ НЕ
|
||
// ВЗЯТЬ. Узлы стоят дорого и дорожают, ветвь открывается по цепочке, и опыт —
|
||
// единственная валюта на все пять ветвей сразу.
|
||
//
|
||
// Пять ветвей — это пять способов закрыть разрыв из закона масштаба
|
||
// (docs/20-gdd.md §2): угроза растёт квадратично, сила отряда — линейно, и
|
||
// разрыв закрывается ЗНАНИЕМ. Ветви и есть виды знания:
|
||
//
|
||
// СТВОЛ — про огонь: урон, пробитие, боезапас;
|
||
// КЛИНОК — про ближний бой: урон, парирование, скорость;
|
||
// СТИХИИ — про реакции и статусы: то, чем берут защищённых;
|
||
// СНАБЖЕНИЕ — про ресурсы вылазки: свет, переноска, патроны;
|
||
// ОТРЯД — про живучесть и людей: здоровье, броня, ярость.
|
||
//
|
||
// НИ ОДИН УЗЕЛ НЕ ЗАВОДИТ НОВОГО КАНАЛА ВЛИЯНИЯ. Каждый кладёт своё число туда,
|
||
// куда уже кладут снаряжение и аффиксы (sim/affix.h): статы бойца, множители
|
||
// оружия, ресурсы вылазки. Это то же правило, что и у предметов, и оно тут
|
||
// важнее вдвойне — дерево растёт, и первый же «особенный» узел со своей веткой
|
||
// в коде превратил бы SyncLoadout в свалку.
|
||
// -----------------------------------------------------------------------------
|
||
#include <cstdint>
|
||
|
||
// Ветви. Порядок — порядок столбцов на экране.
|
||
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);
|
||
};
|