Четыре строки покупок (патроны, переноска, обзор, живучесть) удалены
целиком. Выбора в них не было: купить в итоге можно было всё, порядок ни
на что не влиял, и через две вылазки экран превращался в кнопку
"продолжить".
Дерево: пять ветвей по восемь узлов, у каждого пять уровней. Ветви - это
пять способов закрыть разрыв из закона масштаба: стволы, клинки, стихии,
снабжение, отряд. Ветвь открывается цепочкой - узел доступен, когда взят
предыдущий в этой же ветви. Свободная сетка читалась бы как список;
цепочка читается как путь.
Ни один узел не заводит нового канала влияния: каждый кладёт своё число
туда, куда уже кладут снаряжение и аффиксы. Дерево будет расти, и первый
же "особенный" узел со своей веткой в коде превратил бы 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>
213 lines
13 KiB
C++
213 lines
13 KiB
C++
#pragma once
|
||
// -----------------------------------------------------------------------------
|
||
// Профиль: всё, что переживает забег.
|
||
//
|
||
// Игра стала экстракшен-шутером, и это меняет смысл спуска целиком. Раньше
|
||
// глубина была прогрессом: чем дальше, тем лучше. Теперь она СТАВКА — унесённое
|
||
// остаётся, потерянное не возвращается, и каждый этаж это заново заданный
|
||
// вопрос «ещё ниже или назад».
|
||
//
|
||
// Отсюда всё устройство файла:
|
||
// * снаряжение бойцов лежит ЗДЕСЬ, а не собирается заново при старте. То, в
|
||
// чём отряд ушёл вниз, — это то, в чём он вернулся с прошлой вылазки;
|
||
// * схрон — общий запас сверх надетого. Он и есть накопленный результат;
|
||
// * максимальная глубина открывает чекпоинты. Без них десятитысячный этаж
|
||
// недостижим не по сложности, а по времени.
|
||
//
|
||
// Формат текстовый и построчный по той же причине, что у комнат: профиль лежит
|
||
// рядом с игрой, его правят руками при отладке, и он обязан читаться глазами.
|
||
//
|
||
// Профиль — состояние ИГРОКА, а не мира, поэтому он не в ecs::World и не
|
||
// сбрасывается ни Reset, ни Descend.
|
||
// -----------------------------------------------------------------------------
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
#include "sim/craft.h"
|
||
#include "sim/merc.h"
|
||
#include "sim/tree.h"
|
||
#include <cstdint>
|
||
#include "sim/items.h"
|
||
#include "tuning.h"
|
||
|
||
// Через сколько этажей ставится чекпоинт. Пятьдесят: забег — это ДОЛГИЙ спуск,
|
||
// а не пятиэтажная петля. Чекпоинт через каждые пять превращал вылазку в
|
||
// перебежку между сохранениями, и весь риск умещался в пять этажей.
|
||
constexpr int CHECKPOINT_STEP = 50;
|
||
|
||
// Сколько предметов помещается в схрон. Не «на всякий случай»: схрон без
|
||
// потолка превращает экстракшен в склад, и решение «что взять вниз» исчезает.
|
||
constexpr int STASH_CAP = 48;
|
||
|
||
// Чем комплектуется боец, у которого не осталось ничего. Это дно, ниже
|
||
// которого забег не проваливается: без него полный вайп означал бы отряд с
|
||
// голыми руками и конец игры вместо нового захода.
|
||
struct StarterKit
|
||
{
|
||
ItemId weapon = ItemId::RIFLE;
|
||
ItemId armor = ItemId::JACKET;
|
||
ItemId trinket = ItemId::NONE;
|
||
};
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// Наёмники. НАХОДЯТСЯ, а не покупаются: капсула на этаже — это место, до
|
||
// которого надо дойти, и риск, который надо пережить вместе с ним.
|
||
//
|
||
// Встречается редко: примерно раз в сто этажей и не чаще, чем раз в двадцать.
|
||
// Пятый оперативник обязан быть событием забега, а не строкой в списке покупок.
|
||
// -----------------------------------------------------------------------------
|
||
constexpr int RESCUE_EVERY = 100; // средняя редкость встречи, в этажах
|
||
constexpr int RESCUE_COOLDOWN = 20; // ближе этого две капсулы не встречаются
|
||
|
||
// Бросок наёмника. Сумма прибавок примерно одинакова, а распределение — нет:
|
||
// один крепкий, другой быстрый, третий глазастый. Так двое найденных не
|
||
// оказываются одним и тем же человеком с другим именем.
|
||
MercStats RollMerc(uint32_t& rng);
|
||
|
||
// Прежние четыре строки покупок (Upgrade::AMMO/CARRY/SIGHT/VIGOR) удалены
|
||
// целиком: их заменило дерево (sim/tree.h). Держать обе системы значило бы
|
||
// платить дважды за одно и то же и объяснять игроку, чем строка «+20 патронов»
|
||
// отличается от узла «DEEP POCKETS».
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// Версия формата профиля.
|
||
//
|
||
// Заведена не «на будущее»: формат уже ломался дважды за три вехи — когда сумка
|
||
// стала общей и когда появились улучшения. Оба раза старый файл читался молча и
|
||
// частично, а игрок видел это как «схрон куда-то делся».
|
||
//
|
||
// 1 — до общей сумки: у каждого бойца свои шесть ячеек в строке agent
|
||
// 2 — общая сумка, улучшения отряда
|
||
// 3 — размер отряда (найм): раньше бойцов всегда было пять
|
||
// 4 — экземпляры предметов: редкость и аффиксы в токене после '@'
|
||
// 5 — материалы: строка mat
|
||
// 6 — наёмники: строка merc и глубина последней встречи
|
||
// 7 — дерево улучшений вместо четырёх строк покупок
|
||
//
|
||
// Правило чтения: файл СТАРШЕ игры читается с миграцией, файл НОВЕЕ игры не
|
||
// читается вовсе. Второе важнее первого: молча обнулить профиль, который
|
||
// записала более новая сборка, — это стереть десятки часов, а отказ читать
|
||
// всего лишь просит не запускать старую сборку.
|
||
// -----------------------------------------------------------------------------
|
||
constexpr int PROFILE_VERSION = 7;
|
||
|
||
struct Profile
|
||
{
|
||
// Какой версией записан прочитанный файл. Новый профиль сразу текущей.
|
||
int version = PROFILE_VERSION;
|
||
|
||
// Уровни узлов дерева. Живут в профиле, а не в игре: это то, что
|
||
// переживает вылазку (sim/tree.h).
|
||
int node[TREE_NODES] = {};
|
||
|
||
// Сколько всего точек вложено в дерево. По ней считается общее удорожание
|
||
// (sim/tree.h): каждая взятая точка поднимает цену следующих.
|
||
int TreeRanks() const
|
||
{
|
||
int n = 0;
|
||
for (int i = 0; i < TREE_NODES; ++i) n += node[i];
|
||
return n;
|
||
}
|
||
|
||
int NodeRank(int index) const
|
||
{
|
||
return (index >= 0 && index < TREE_NODES) ? node[index] : 0;
|
||
}
|
||
|
||
// Узел открыт, если предыдущий в его ветви взят хотя бы на уровень. Цепочка,
|
||
// а не свободная сетка: ветвь обязана читаться как путь.
|
||
bool NodeUnlocked(int index) const
|
||
{
|
||
const int req = TreeNodeRequires(index);
|
||
return req < 0 || NodeRank(req) > 0;
|
||
}
|
||
|
||
bool CanBuyNode(int index) const
|
||
{
|
||
if (index < 0 || index >= TREE_NODES) return false;
|
||
if (!NodeUnlocked(index)) return false;
|
||
const int rank = NodeRank(index);
|
||
if (rank >= TreeNodeAt(index).maxRank) return false;
|
||
return bankedXp >= TreeNodeCost(index, rank, TreeRanks());
|
||
}
|
||
|
||
bool BuyNode(int index)
|
||
{
|
||
if (!CanBuyNode(index)) return false;
|
||
bankedXp -= TreeNodeCost(index, NodeRank(index), TreeRanks());
|
||
++node[index];
|
||
return true;
|
||
}
|
||
|
||
// Сумма всего взятого. Считается при сборке этажа, дальше читаются готовые
|
||
// числа: правила боя про дерево не знают.
|
||
TreeBonus Tree() const
|
||
{
|
||
TreeBonus b;
|
||
for (int i = 0; i < TREE_NODES; ++i)
|
||
if (node[i] > 0) b.Add(TreeNodeAt(i).effect, TreeNodeAt(i).perRank * float(node[i]));
|
||
return b;
|
||
}
|
||
|
||
// Сколько оперативников в отряде. Игра начинается ОДНИМ (docs/20-gdd.md),
|
||
// потолок — NUM_AGENTS. Живёт в профиле, потому что это накопленный
|
||
// результат, а не состояние вылазки: нанятый не теряется при вайпе, он
|
||
// теряет только снаряжение, как и все.
|
||
int squadSize = 1;
|
||
|
||
// Числа найденных оперативников по слотам. У первого их нет: он и есть
|
||
// тот, с кого игра начинается.
|
||
MercStats merc[NUM_AGENTS];
|
||
|
||
// На какой глубине последний раз встретилась капсула. Нужна редкости:
|
||
// «не чаще раза в двадцать этажей» считается отсюда.
|
||
int lastRescueDepth = 0;
|
||
|
||
// Место в отряде ещё есть?
|
||
bool CanRescue() const { return squadSize < NUM_AGENTS; }
|
||
|
||
// Снаряжение бойцов ровно в том виде, в каком они вернулись. Массив всегда
|
||
// на все слоты: незанятые просто пустые.
|
||
Inventory loadout[NUM_AGENTS];
|
||
|
||
// Общий запас сверх надетого. Экземпляры, а не виды: схрон — это то, из
|
||
// чего собирают следующую вылазку, и вещь без аффиксов в нём была бы уже
|
||
// другой вещью (sim/affix.h).
|
||
std::vector<ItemInstance> stash;
|
||
|
||
// Материалы. ВАЛЮТА: не занимают ячеек, не весят, в схроне не лежат
|
||
// (sim/craft.h). Зачисляются только при эвакуации — ставка одна на всё.
|
||
int material[MATERIAL_COUNT] = {};
|
||
|
||
// --- прогресс -------------------------------------------------------------
|
||
int maxDepth = 1; // самый глубокий ДОСТИГНУТЫЙ этаж
|
||
int runs = 0; // сколько вылазок начато
|
||
int extractions = 0; // сколько закончилось эвакуацией
|
||
int wipes = 0;
|
||
float bankedXp = 0.0f; // опыт, вынесенный из успешных вылазок
|
||
|
||
// --- чекпоинты ------------------------------------------------------------
|
||
// Самый глубокий чекпоинт, с которого можно начать. Глубина 1 доступна
|
||
// всегда: начать сверху — законный выбор, а не наказание.
|
||
int DeepestCheckpoint() const;
|
||
// Чекпоинт номер i (0 — первый этаж). Возвращает глубину.
|
||
static int CheckpointDepth(int index) { return 1 + index * CHECKPOINT_STEP; }
|
||
int CheckpointCount() const;
|
||
|
||
// --- схрон ----------------------------------------------------------------
|
||
bool StashPush(const ItemInstance& id); // false — схрон полон
|
||
bool StashRemoveAt(int index);
|
||
int StashCount() const { return int(stash.size()); }
|
||
|
||
// Сбросить снаряжение к стартовому комплекту. Схрон и прогресс не трогает:
|
||
// потерять вылазку — не то же самое, что потерять всё нажитое.
|
||
void ResetLoadouts();
|
||
|
||
bool Load(const std::string& path, std::string* err = nullptr);
|
||
bool Save(const std::string& path, std::string* err = nullptr) const;
|
||
};
|
||
|
||
// Где лежит профиль: рядом с бинарником или в корне репозитория. Возвращает
|
||
// путь, даже если файла ещё нет, — по нему и будет первая запись.
|
||
std::string DefaultProfilePath();
|