СТИХИИ (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>
122 lines
5.8 KiB
C++
122 lines
5.8 KiB
C++
#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;
|
||
}
|