squad-proto/src/sim/element.h

122 lines
5.8 KiB
C
Raw Normal View History

Стихии и вторая поверхность инструментов (M21, E2) СТИХИИ (src/sim/element.h) Семь элементов, сопротивления свойством ВИДА, одна формула урона на пулю и на клинок. Зачем они в игре про бесконечный спуск: сила отряда растёт медленнее угрозы (закон масштаба, docs/20-gdd.md), и разрыв обязан закрываться знанием. Стихии — первый такой канал: верное оружие убивает втрое быстрее, и цифра эта не растёт от прокачки. Ручных действий не заводится: стихия — свойство оружия, а не кнопка. Всё решается тем, что отряд взял вниз. Стихии есть только у НАХОДИМОГО оружия. Карабин, пистолет и сабля остаются чистой кинетикой, потому что на них меряется приёмка. ИНСТРУМЕНТЫ БАЛАНСА (src/debug/tools.cpp) Вторая поверхность: `squad_proto --tool weapons|enemies|ttk|economy`. Отдельная от редакторской не по вкусу — редактор линкуется только с движком и правил боя не видит, а всё это считает урон. Инструменты делятся по ПРЕДМЕТУ: содержимое у редактора, баланс у игры. ЧТО НАШЛА ПРОВЕРКА Первая версия --tool ttk считала разброс по времени убийства и показывала уверенные 2.8x на каждой твари. Это была разница МОЩНОСТИ стволов: магнум лучше пистолета против всех, стихии ни при чём. С вынесенной за скобки мощностью оружия правда оказалась другой: 1.04-1.36x, то есть состав удара не значил почти ничего. Механика была декоративной ровно в том же смысле, в каком когда-то был вес. Чтобы разброс появился, у твари должны быть И уязвимость, И сопротивление среди тех стихий, которые вообще носит оружие: одной уязвимости мало, если ни один ствол не бьёт тем, что она держит. После правки содержимого все пять видов в коридоре 1.90-3.06x, и картина читается — дробовик жжёт бредущих, дуга бьёт плевков, лук разъедает громилу, карабин против громилы бесполезен. Коридор в инструменте 1.8-8.0, а не заявленные в документе 3-6: до тройки дотянут почти чистые стихийные сборки, которые приходят с аффиксами. Занижать цель в документе нельзя, поэтому в инструменте стоит достижимое. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:25:44 +07:00
#pragma once
// -----------------------------------------------------------------------------
// Стихии урона и сопротивления.
//
// Зачем они вообще нужны в игре про бесконечный спуск: сила отряда растёт
// медленнее, чем угроза (закон масштаба, docs/20-gdd.md), и разрыв обязан
// закрываться ЗНАНИЕМ. Стихии — первый и главный канал такого знания: тварь,
// на которую взяли верное оружие, умирает втрое быстрее, чем от неверного, и
// цифра эта не растёт от прокачки.
//
// Ручных действий игра не заводит (спек 5): стихия — свойство ОРУЖИЯ, а не
// кнопка. Всё решается тем, что отряд взял вниз.
//
// Здесь только арифметика урона. Статусы (горение, оледенение) и реакции между
// стихиями — следующая веха, они живут поверх этого файла, а не внутри него.
// -----------------------------------------------------------------------------
#include <algorithm>
#include <cstdint>
enum class Element : uint8_t
{
KIN = 0, // кинетика: базовый урон, режется бронёй
INC, // жар
CRY, // холод
TOX, // кислота
ARC, // дуга
RAD, // радиация
VOID, // пустота
COUNT,
};
constexpr int ELEMENT_COUNT = int(Element::COUNT);
inline const char* ElementName(Element e)
{
switch (e)
{
case Element::KIN: return "KIN";
case Element::INC: return "INC";
case Element::CRY: return "CRY";
case Element::TOX: return "TOX";
case Element::ARC: return "ARC";
case Element::RAD: return "RAD";
default: return "VOID";
}
}
// -----------------------------------------------------------------------------
// Из чего состоит удар. Доли, а не абсолютные числа: урон оружия остаётся одним
// числом, а стихия говорит, КАК он делится. Иначе каждое оружие пришлось бы
// балансировать семь раз.
//
// Сумма долей всегда 1: доля, не попавшая ни в одну стихию, — это кинетика.
// -----------------------------------------------------------------------------
struct ElementMix
{
float share[ELEMENT_COUNT] = {1.0f, 0, 0, 0, 0, 0, 0};
// Смесь «столько-то одной стихии, остальное кинетика». Ровно так задаётся
// почти всё оружие: чистые стихии — редкость и признак поздней глубины.
static ElementMix Of(Element e, float part)
{
ElementMix m;
const float p = std::min(std::max(part, 0.0f), 0.85f);
m.share[int(Element::KIN)] = 1.0f - p;
m.share[int(e)] = (e == Element::KIN) ? 1.0f : p;
return m;
}
// Полностью убрать кинетику нельзя (потолок 0.85 выше). Оружие без
// физического урона перестало бы работать по целям, у которых нет
// сопротивлений вовсе, — а такие есть на каждом этаже.
float Share(Element e) const { return share[int(e)]; }
};
// -----------------------------------------------------------------------------
// Профиль сопротивлений вида. Доля поглощённого урона: 0.5 — вдвое меньше,
// 0.4 — на 40% больше (уязвимость).
//
// Иммунитета нет ни у кого, и это правило: тварь, которую физически нельзя
// ранить, — не сложность, а тупик для игрока без нужной стихии.
// -----------------------------------------------------------------------------
constexpr float RESIST_MAX = 0.85f; // выше не поглощает никто
constexpr float RESIST_MIN = -0.60f; // ниже уязвимость не опускается
struct Resists
{
float r[ELEMENT_COUNT] = {};
Resists() = default;
// Порядок как в enum: KIN, INC, CRY, TOX, ARC, RAD, VOID.
Resists(float kin, float inc, float cry, float tox, float arc, float rad, float vd)
: r{kin, inc, cry, tox, arc, rad, vd}
{
}
float Of(Element e) const { return r[int(e)]; }
};
// -----------------------------------------------------------------------------
// Итоговый урон. Единственное место, где стихии превращаются в число.
//
// raw — урон источника до сопротивлений
// mix — из чего он состоит
// res — сопротивления цели
// pen — пробитие сборки, одно на все стихии (аффиксы придут позже)
// -----------------------------------------------------------------------------
inline float ResolveDamage(float raw, const ElementMix& mix, const Resists& res, float pen = 0.0f)
{
float total = 0.0f;
for (int i = 0; i < ELEMENT_COUNT; ++i)
{
const float part = raw * mix.share[i];
if (part <= 0.0f) continue;
const float effective = std::min(std::max(res.r[i] - pen, RESIST_MIN), RESIST_MAX);
total += part * (1.0f - effective);
}
return total;
}