squad-proto/engine/project.cpp
z.kirill ef103050b1 Правки по игре: начало, ставка, наёмники (M29)
Семь изменений по итогам игры вживую.

НАЧАЛО. Первый этаж встречал одинокого оперативника с ножом тремя тварями,
из которых одна быстрая: бюджет 3.0 и бегуны с первого этажа. Стало 2.2
(ровно два бредущих), бегуны с четвёртого. Света прибавлено: обзор
начинается с 1.30 вместо 1.0, у этажа появилась своя яркость пройденного
(0.42 наверху против 0.13 на дне). Наверху ещё работает освещение, и
хоррор первого этажа в том, что игрок один, а не в том, что не видно
стен. Темнота это инструмент глубины, и тратить её на первый этаж значит
не иметь запаса дальше.

СТАВКА. Опыт платится за убийства и сразу: выносить надо ВЕЩИ. Вайп
больше не отнимает всё - доезжает около половины добычи, и чем вещь реже,
тем охотнее теряется. Прежнее "вайп не возвращает ничего" делало
единственным разумным поведением уход наверх при первой царапине.

ВЫХОД. Аномалия возврата переехала из точки входа к спуску: развилка
"вниз или наверх" теперь там же, где этаж кончился. Обратный путь через
растревоженный этаж ничего не решал - это был ещё раз тот же этаж.

ЧЕКПОИНТЫ через 50 этажей вместо пяти: забег это долгий спуск, а не
перебежка между сохранениями.

НАЁМНИКИ. Кнопка "нанять за опыт" убрана, оперативники находятся в
капсулах на этажах: в среднем раз в сотню этажей и не чаще раза в
двадцать. У каждого свои три прибавки из одного бюджета - разница в
характере, а не в ранге. Покупка была скучной ровно в том смысле, в каком
скучна любая кнопка "стать сильнее": ничего не рассказывает и никуда не
зовёт.

ЗАМЕРЫ. Вайп на сотне повторов: доезжает 52% (обычное 69%, реликты 25%).
Первая версия проверки считала одну сумку и выдала "обычного доехало
112%" - спасение забирает ещё и надетое. Опыт: банк 558 -> 578 за
убийство без эвакуации. Капсула поднимает отряд до двух и даёт наёмнику
его числа.

Формат профиля 6: строки merc и rescued.

Не сделано и обсуждается: ростер сверх пятерых, задания для свободных,
пассивные бонусы от сидящих на базе.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 04:09:26 +03:00

233 lines
9.0 KiB
C++
Raw Permalink 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.

#include "engine/project.h"
#include <algorithm>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include "engine/text_parse.h"
namespace fs = std::filesystem;
namespace
{
constexpr const char* MANIFEST = "project.txt";
constexpr const char* PROJECTS_DIR = "projects";
// Откуда смотрим. Игра запускается и из корня репозитория, и из build/bin —
// оба способа рабочие, и требовать за это ключ было бы налогом на каждый запуск.
const char* const BASES[] = {".", "..", "../..", "../../.."};
bool HasManifest(const fs::path& dir)
{
std::error_code ec;
return fs::exists(dir / MANIFEST, ec);
}
// Число с плавающей точкой из лексемы по индексу; нет лексемы — прежнее значение
// остаётся. Так частично заполненная строка не обнуляет незаданные поля.
void Take(const std::vector<std::string>& parts, size_t i, float& out)
{
if (i < parts.size()) out = text::ToFloat(parts[i], out);
}
void Take(const std::vector<std::string>& parts, size_t i, int& out)
{
if (i < parts.size()) out = text::ToInt(parts[i], out);
}
} // namespace
bool Project::Load(const std::string& projectDir, std::string* err)
{
auto fail = [&](const std::string& why) {
if (err) *err = why;
return false;
};
const fs::path root(projectDir);
const fs::path manifest = root / MANIFEST;
std::ifstream in(manifest);
if (!in) return fail("нет " + manifest.string());
*this = Project{};
dir = projectDir;
std::string line;
int lineNo = 0;
std::string rooms = "rooms";
std::string catalog = "catalog/entities.txt";
while (std::getline(in, line))
{
++lineNo;
const std::vector<std::string> parts = text::Split(text::Trim(text::StripComment(line)));
if (parts.empty()) continue;
const std::string& key = parts[0];
if (key == "name") name = text::JoinFrom(parts, 1);
else if (key == "rooms") rooms = text::JoinFrom(parts, 1);
else if (key == "catalog") catalog = text::JoinFrom(parts, 1);
else if (key == "startdepth") Take(parts, 1, startDepth);
else if (key == "view")
{
// Неизвестный вид — это ошибка, а не повод молча остаться в
// изометрии: проект, написанный под вид сверху, в изометрии
// выглядит не «чуть иначе», а сломанным.
if (parts.size() < 2 || !ParseViewMode(parts[1].c_str(), view))
return fail(manifest.string() + ": строка " + std::to_string(lineNo) +
": неизвестный вид отображения");
}
else if (key == "danger")
{
Take(parts, 1, curve.dangerBase);
Take(parts, 2, curve.dangerLinear);
Take(parts, 3, curve.dangerQuad);
}
else if (key == "density")
{
Take(parts, 1, curve.densityPerFloor);
Take(parts, 2, curve.densityCap);
}
else if (key == "loot")
{
Take(parts, 1, curve.lootBase);
Take(parts, 2, curve.floorsPerDrop);
Take(parts, 3, curve.floorsPerTier);
}
else if (key == "sight")
{
Take(parts, 1, curve.sightPerFloor);
Take(parts, 2, curve.sightFloorAt);
Take(parts, 3, curve.sightStart); // необязательный третий: с чего начинать
}
else if (key == "ambient")
{
Take(parts, 1, curve.ambientStart);
Take(parts, 2, curve.ambientPerFloor);
Take(parts, 3, curve.ambientFloorAt);
}
else if (key == "xp") Take(parts, 1, curve.xpPerFloor);
else if (key == "themes")
{
for (int i = 0; i < THEME_COUNT; ++i) Take(parts, size_t(i) + 1, curve.themeFrom[i]);
}
else
{
// Опечатка в ключе — самая дорогая ошибка в текстовом формате:
// строка просто не действует, и это выглядит как «движок не
// слушается». Поэтому она валит загрузку, а не пропускается.
return fail(manifest.string() + ": строка " + std::to_string(lineNo) +
": неизвестный ключ " + key);
}
}
roomsDir = (root / rooms).string();
catalogPath = (root / catalog).string();
std::error_code ec;
if (!fs::is_directory(fs::path(roomsDir), ec))
return fail("нет каталога комнат " + roomsDir);
if (!fs::exists(fs::path(catalogPath), ec))
return fail("нет каталога сущностей " + catalogPath);
// Полосы отделки обязаны идти по возрастанию: тема с большим номером — это
// «глубже». Перепутанные границы дали бы этаж, у которого тема зависит от
// порядка сравнений, а не от глубины.
for (int i = 1; i < THEME_COUNT; ++i)
if (curve.themeFrom[i] < curve.themeFrom[i - 1])
return fail(manifest.string() + ": полосы отделки идут не по возрастанию");
if (startDepth < 1) startDepth = 1;
return true;
}
void Project::Apply() const
{
g_proj = Projection::For(view);
g_curve = curve;
}
std::string FindProjectDir(const std::string& wantedArg)
{
std::error_code ec;
// Ключ важнее окружения, окружение важнее угадывания. Переменная нужна
// терминалу редактора и запущенным из него агентам: инструмент, вызванный
// изнутри открытого проекта, обязан говорить про ЭТОТ проект, а дописывать
// --project в чужие команды строковой подстановкой — способ однажды
// подставить его не туда.
std::string wanted = wantedArg;
if (wanted.empty())
{
#ifdef _MSC_VER
// getenv_s, а не getenv: под /W4 второй — предупреждение, а собираемся
// мы без предупреждений, и глушить их ради одной строки нельзя.
char buf[512] = {};
size_t len = 0;
if (getenv_s(&len, buf, sizeof(buf), "TILE2D_PROJECT") == 0 && len > 1) wanted = buf;
#else
const char* env = std::getenv("TILE2D_PROJECT");
if (env && *env) wanted = env;
#endif
}
// 1. Явный путь — как дали, так и берём.
if (!wanted.empty())
{
if (HasManifest(fs::path(wanted))) return wanted;
// 2. Или это имя проекта: <база>/projects/<имя>.
for (const char* b : BASES)
{
const fs::path p = fs::path(b) / PROJECTS_DIR / wanted;
if (HasManifest(p)) return p.string();
}
return {};
}
// 3. Без имени: проект под ногами, потом единственный в projects/.
for (const char* b : BASES)
{
if (HasManifest(fs::path(b))) return fs::path(b).string();
const fs::path pdir = fs::path(b) / PROJECTS_DIR;
if (!fs::is_directory(pdir, ec)) continue;
std::vector<std::string> found;
for (const fs::directory_entry& e : fs::directory_iterator(pdir, ec))
if (e.is_directory(ec) && HasManifest(e.path())) found.push_back(e.path().string());
// Несколько проектов и ни одного названного — выбирать за игрока
// нельзя: молча взятый «первый попавшийся» читается как «движок открыл
// не то». Пусть скажут, какой.
if (found.size() == 1) return found[0];
if (found.size() > 1) return {};
}
return {};
}
std::vector<std::string> AvailableProjects()
{
std::error_code ec;
std::vector<std::string> names;
for (const char* b : BASES)
{
const fs::path pdir = fs::path(b) / PROJECTS_DIR;
if (!fs::is_directory(pdir, ec)) continue;
for (const fs::directory_entry& e : fs::directory_iterator(pdir, ec))
if (e.is_directory(ec) && HasManifest(e.path()))
names.push_back(e.path().filename().string());
if (!names.empty()) break; // ближайший каталог projects/ и есть нужный
}
std::sort(names.begin(), names.end());
return names;
}