squad-proto/src/sim/tree.h

164 lines
9.0 KiB
C
Raw Normal View History

Дерево улучшений вместо четырёх кнопок (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 23:24:50 +07:00
#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);
};