squad-proto/src/sim/element.h
z.kirill 745cb2a3fd Стихии и вторая поверхность инструментов (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 05:25:44 +03:00

122 lines
5.8 KiB
C++
Raw 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.

#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;
}