squad-proto/src/sim/tree.h
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

164 lines
9.0 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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);
};