#pragma once // ----------------------------------------------------------------------------- // Профиль: всё, что переживает забег. // // Игра стала экстракшен-шутером, и это меняет смысл спуска целиком. Раньше // глубина была прогрессом: чем дальше, тем лучше. Теперь она СТАВКА — унесённое // остаётся, потерянное не возвращается, и каждый этаж это заново заданный // вопрос «ещё ниже или назад». // // Отсюда всё устройство файла: // * снаряжение бойцов лежит ЗДЕСЬ, а не собирается заново при старте. То, в // чём отряд ушёл вниз, — это то, в чём он вернулся с прошлой вылазки; // * схрон — общий запас сверх надетого. Он и есть накопленный результат; // * максимальная глубина открывает чекпоинты. Без них десятитысячный этаж // недостижим не по сложности, а по времени. // // Формат текстовый и построчный по той же причине, что у комнат: профиль лежит // рядом с игрой, его правят руками при отладке, и он обязан читаться глазами. // // Профиль — состояние ИГРОКА, а не мира, поэтому он не в ecs::World и не // сбрасывается ни Reset, ни Descend. // ----------------------------------------------------------------------------- #include #include #include "sim/craft.h" #include "sim/merc.h" #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; }; // ----------------------------------------------------------------------------- // Во что вкладывается вынесенный опыт. // // Улучшения РЕСУРСНЫЕ, а не силовые: больше патронов, больше переноски, дальше // видно, больше запаса здоровья. Игра про исчерпание, и вкладываться в неё надо // в то, что кончается, — иначе прокачка съедает сам жанр: отряд, который бьёт // вдвое сильнее, перестаёт бояться, а страх тут единственная механика, которую // нельзя восстановить балансом. // // Улучшения ОТРЯДНЫЕ, а не на бойца. Не из экономии: личным в этой игре // остаётся снаряжение, и второе личное измерение превратило бы сборы перед // вылазкой в бухгалтерию на пятерых. // ----------------------------------------------------------------------------- enum class Upgrade : uint8_t { AMMO = 0, // запас патронов на вылазку CARRY, // норма переноски до перегруза SIGHT, // радиус обзора VIGOR, // запас здоровья COUNT, }; constexpr int UPGRADE_MAX_LEVEL = 5; // ----------------------------------------------------------------------------- // Наёмники. НАХОДЯТСЯ, а не покупаются. // // Покупка за опыт была первой версией и оказалась скучной ровно в том смысле, // в каком скучна любая кнопка «стать сильнее»: она ничего не рассказывает и // никуда не зовёт. Найденный человек — это место, до которого надо дойти, // и риск, который надо пережить вместе с ним. // // Встречается редко: примерно раз в сто этажей и не чаще, чем раз в двадцать. // Редко намеренно — пятый оперативник обязан быть событием забега, а не // строкой в списке покупок. // // У каждого свои числа. Не «класс» и не «профессия»: три небольшие прибавки, // которые видно на экране отряда и которые делают ЭТОГО человека отличным от // соседа. Роль и специализация — следующая веха, тут нарочно минимум. // ----------------------------------------------------------------------------- constexpr int RESCUE_EVERY = 100; // средняя редкость встречи, в этажах constexpr int RESCUE_COOLDOWN = 20; // ближе этого две капсулы не встречаются // Бросок наёмника. Сумма прибавок примерно одинакова, а распределение — нет: // один крепкий, другой быстрый, третий глазастый. Так двое найденных не // оказываются одним и тем же человеком с другим именем. MercStats RollMerc(uint32_t& rng); // Шаг улучшения за уровень. constexpr float UPGRADE_AMMO_STEP = 20.0f; constexpr float UPGRADE_CARRY_STEP = 1.5f; constexpr float UPGRADE_SIGHT_STEP = 0.06f; constexpr float UPGRADE_VIGOR_STEP = 12.0f; // Цена следующего уровня. Растёт линейно: первая покупка обязана случиться на // второй-третьей вылазке, иначе опыт снова «просто число», а десятая — стоить // заметно, иначе кривая кончается. inline float UpgradeCost(int currentLevel) { return 150.0f * float(currentLevel + 1); } const char* UpgradeName(Upgrade u); const char* UpgradeEffect(Upgrade u); // ----------------------------------------------------------------------------- // Версия формата профиля. // // Заведена не «на будущее»: формат уже ломался дважды за три вехи — когда сумка // стала общей и когда появились улучшения. Оба раза старый файл читался молча и // частично, а игрок видел это как «схрон куда-то делся». // // 1 — до общей сумки: у каждого бойца свои шесть ячеек в строке agent // 2 — общая сумка, улучшения отряда // 3 — размер отряда (найм): раньше бойцов всегда было пять // 4 — экземпляры предметов: редкость и аффиксы в токене после '@' // 5 — материалы: строка mat // 6 — наёмники: строка merc и глубина последней встречи // // Правило чтения: файл СТАРШЕ игры читается с миграцией, файл НОВЕЕ игры не // читается вовсе. Второе важнее первого: молча обнулить профиль, который // записала более новая сборка, — это стереть десятки часов, а отказ читать // всего лишь просит не запускать старую сборку. // ----------------------------------------------------------------------------- constexpr int PROFILE_VERSION = 6; struct Profile { // Какой версией записан прочитанный файл. Новый профиль сразу текущей. int version = PROFILE_VERSION; // Уровни улучшений, по одному на каждое. Живут в профиле, а не в игре: // это то, что переживает вылазку. int upgrade[int(Upgrade::COUNT)] = {}; int UpgradeLevel(Upgrade u) const { return upgrade[int(u)]; } bool CanBuy(Upgrade u) const { return upgrade[int(u)] < UPGRADE_MAX_LEVEL && bankedXp >= UpgradeCost(upgrade[int(u)]); } bool Buy(Upgrade u) { if (!CanBuy(u)) return false; bankedXp -= UpgradeCost(upgrade[int(u)]); ++upgrade[int(u)]; return true; } // Сколько оперативников в отряде. Игра начинается ОДНИМ (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 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();