195 lines
9.6 KiB
C
195 lines
9.6 KiB
C
|
|
#pragma once
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
// Статусы стихий и реакции между ними.
|
|||
|
|
//
|
|||
|
|
// Стихии сами по себе (sim/element.h) — это только множитель урона: «верный
|
|||
|
|
// ствол бьёт втрое больнее». Этого мало, чтобы стихии стали ЗНАНИЕМ, ради
|
|||
|
|
// которого они заводились: множитель работает одинаково у того, кто понимает
|
|||
|
|
// систему, и у того, кто просто взял ствол посильнее.
|
|||
|
|
//
|
|||
|
|
// Статусы и реакции добавляют второй слой, который работает только у первого:
|
|||
|
|
// наложить ДВЕ разные стихии на одну цель и получить реакцию — это решение о
|
|||
|
|
// составе отряда, а не о характеристиках оружия.
|
|||
|
|
//
|
|||
|
|
// ТРИ ОГРАНИЧИТЕЛЯ, И ВСЕ ОБЯЗАТЕЛЬНЫЕ (docs/21-damage.md §4.1):
|
|||
|
|
//
|
|||
|
|
// 1. потолок стаков — один стак не значит ничего, полный набор меняет бой;
|
|||
|
|
// 2. внутренний откат реакции на цель — иначе очередь из сорока пуль даёт
|
|||
|
|
// сорок взрывов за секунду;
|
|||
|
|
// 3. запрет рекурсии — урон ОТ реакции не накладывает статусов и не может
|
|||
|
|
// запустить следующую реакцию.
|
|||
|
|
//
|
|||
|
|
// Третий пункт не теоретический. Без него цепочка «реакция бьёт по площади →
|
|||
|
|
// урон кладёт стихию → складывается новая пара → реакция» уходит в самоповтор
|
|||
|
|
// на первом же скоплении, и кадр умирает. Именно так ломаются все игры с
|
|||
|
|
// триггерами, и чинить это потом дороже, чем запретить сразу.
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
#include <algorithm>
|
|||
|
|
#include <cstdint>
|
|||
|
|
|
|||
|
|
#include "sim/element.h"
|
|||
|
|
|
|||
|
|
// Сколько стаков держит статус и сколько живёт один стак.
|
|||
|
|
struct StatusRule
|
|||
|
|
{
|
|||
|
|
int maxStacks;
|
|||
|
|
float duration;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Потолки подобраны под правило «полный набор меняет бой»: горение в пять
|
|||
|
|
// стаков заметно, в один — нет.
|
|||
|
|
inline StatusRule StatusRuleOf(Element e)
|
|||
|
|
{
|
|||
|
|
switch (e)
|
|||
|
|
{
|
|||
|
|
case Element::INC: return {5, 4.0f};
|
|||
|
|
case Element::CRY: return {10, 3.0f};
|
|||
|
|
case Element::TOX: return {8, 5.0f};
|
|||
|
|
case Element::ARC: return {4, 3.0f};
|
|||
|
|
case Element::RAD: return {6, 6.0f};
|
|||
|
|
case Element::VOID: return {3, 4.0f};
|
|||
|
|
default: return {0, 0.0f}; // KIN статуса не кладёт
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Состояние статусов на одной цели. Плоские массивы, без аллокаций: их столько
|
|||
|
|
// же, сколько целей, и они трогаются каждый шаг.
|
|||
|
|
struct StatusState
|
|||
|
|
{
|
|||
|
|
int stacks[ELEMENT_COUNT] = {};
|
|||
|
|
float timer[ELEMENT_COUNT] = {}; // до сгорания ОДНОГО стака
|
|||
|
|
|
|||
|
|
// Откат реакции на этой цели. Один на все пары: две разные реакции подряд
|
|||
|
|
// на одной твари за долю секунды — это тот же взрыв, только с двух сторон.
|
|||
|
|
float reactionIcd = 0.0f;
|
|||
|
|
|
|||
|
|
// Отложенный урон от статусов за шаг: тики горения и кислоты. Копится
|
|||
|
|
// здесь, а применяется там же, где обычный урон, — чтобы смерть от тика
|
|||
|
|
// проходила через тот же путь, что смерть от пули.
|
|||
|
|
float pendingDamage = 0.0f;
|
|||
|
|
|
|||
|
|
bool Has(Element e) const { return stacks[int(e)] > 0; }
|
|||
|
|
|
|||
|
|
void Clear()
|
|||
|
|
{
|
|||
|
|
for (int i = 0; i < ELEMENT_COUNT; ++i) { stacks[i] = 0; timer[i] = 0.0f; }
|
|||
|
|
reactionIcd = 0.0f;
|
|||
|
|
pendingDamage = 0.0f;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
// Реакции. Пара разных стихий на одной цели.
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
enum class Reaction : uint8_t
|
|||
|
|
{
|
|||
|
|
NONE = 0,
|
|||
|
|
SHATTER, // INC + CRY: всплеск урона на месте
|
|||
|
|
IGNITE, // INC + TOX: облако, бьёт по площади
|
|||
|
|
SUPERCOND, // ARC + CRY: срезает сопротивления
|
|||
|
|
DECAY, // RAD + INC: заражает соседей
|
|||
|
|
COUNT,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
inline const char* ReactionName(Reaction r)
|
|||
|
|
{
|
|||
|
|
switch (r)
|
|||
|
|
{
|
|||
|
|
case Reaction::SHATTER: return "SHATTER";
|
|||
|
|
case Reaction::IGNITE: return "IGNITE";
|
|||
|
|
case Reaction::SUPERCOND: return "SUPERCOND";
|
|||
|
|
case Reaction::DECAY: return "DECAY";
|
|||
|
|
default: return "-";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Какая реакция получается из пары. Порядок наложения не важен: «сначала холод,
|
|||
|
|
// потом жар» и наоборот — это одно и то же событие.
|
|||
|
|
inline Reaction ReactionOf(Element a, Element b)
|
|||
|
|
{
|
|||
|
|
const Element lo = (a < b) ? a : b;
|
|||
|
|
const Element hi = (a < b) ? b : a;
|
|||
|
|
|
|||
|
|
if (lo == Element::INC && hi == Element::CRY) return Reaction::SHATTER;
|
|||
|
|
if (lo == Element::INC && hi == Element::TOX) return Reaction::IGNITE;
|
|||
|
|
if (lo == Element::CRY && hi == Element::ARC) return Reaction::SUPERCOND;
|
|||
|
|
if (lo == Element::INC && hi == Element::RAD) return Reaction::DECAY;
|
|||
|
|
return Reaction::NONE;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Сила реакции считается ОТ ГЛУБИНЫ, а не от урона оружия.
|
|||
|
|
//
|
|||
|
|
// Это ключевое решение всей системы (docs/21-damage.md §3.1). Считай реакция от
|
|||
|
|
// оружия — на сотом этаже она стала бы округлением, и весь слой стихий
|
|||
|
|
// превратился бы в украшение. Считая от глубины, реакция растёт сама — но
|
|||
|
|
// только у того, кто умеет её запускать.
|
|||
|
|
inline float ReactionPower(int depth)
|
|||
|
|
{
|
|||
|
|
return 4.0f + 0.12f * float(std::max(1, depth));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Множитель конкретной реакции к базовой силе.
|
|||
|
|
inline float ReactionScale(Reaction r)
|
|||
|
|
{
|
|||
|
|
switch (r)
|
|||
|
|
{
|
|||
|
|
case Reaction::SHATTER: return 2.50f; // точечный всплеск
|
|||
|
|
case Reaction::IGNITE: return 1.80f; // по площади, потому слабее
|
|||
|
|
case Reaction::SUPERCOND: return 0.60f; // берёт не уроном, а срезом
|
|||
|
|
case Reaction::DECAY: return 1.20f;
|
|||
|
|
default: return 0.0f;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
constexpr float REACTION_ICD = 1.2f; // откат реакций на одной цели
|
|||
|
|
constexpr float REACTION_RADIUS = 2.6f; // площадь у тех, что бьют по площади
|
|||
|
|
|
|||
|
|
// Бьёт ли реакция по соседям.
|
|||
|
|
inline bool ReactionIsArea(Reaction r)
|
|||
|
|
{
|
|||
|
|
return r == Reaction::IGNITE || r == Reaction::DECAY;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
// Эффекты статусов, которые читают другие системы.
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
// Урон за секунду от горения: доля максимального здоровья цели за стак.
|
|||
|
|
// От максимума, а не от урона удара: иначе горение на громиле не значило бы
|
|||
|
|
// ничего, а на мелочи убивало бы её само.
|
|||
|
|
constexpr float BURN_PER_STACK = 0.012f;
|
|||
|
|
|
|||
|
|
// Кислота: цель получает больше урона. В игре у тварей нет брони как числа,
|
|||
|
|
// поэтому «снятие брони» из документа выражается прибавкой к получаемому
|
|||
|
|
// урону — эффект тот же, а лишней сущности не заводится.
|
|||
|
|
constexpr float CORRODE_PER_STACK = 0.035f;
|
|||
|
|
|
|||
|
|
// Дуга: то же самое, но слабее и короче — она берёт цепью, а не числом.
|
|||
|
|
constexpr float CHARGE_PER_STACK = 0.020f;
|
|||
|
|
|
|||
|
|
// Холод: замедление за стак.
|
|||
|
|
constexpr float CHILL_PER_STACK = 0.040f;
|
|||
|
|
|
|||
|
|
// Пустота: срез сопротивлений за стак.
|
|||
|
|
constexpr float COLLAPSE_PER_STACK = 0.080f;
|
|||
|
|
|
|||
|
|
// Во сколько раз цель получает больше урона из-за статусов.
|
|||
|
|
inline float VulnerabilityOf(const StatusState& st)
|
|||
|
|
{
|
|||
|
|
return 1.0f + float(st.stacks[int(Element::TOX)]) * CORRODE_PER_STACK +
|
|||
|
|
float(st.stacks[int(Element::ARC)]) * CHARGE_PER_STACK;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Насколько цель замедлена.
|
|||
|
|
inline float SlowOf(const StatusState& st)
|
|||
|
|
{
|
|||
|
|
const float slow = float(st.stacks[int(Element::CRY)]) * CHILL_PER_STACK;
|
|||
|
|
return std::max(0.35f, 1.0f - slow); // ниже трети не тормозим: это не паралич
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Срез сопротивлений от пустоты.
|
|||
|
|
inline float ResistCutOf(const StatusState& st)
|
|||
|
|
{
|
|||
|
|
return float(st.stacks[int(Element::VOID)]) * COLLAPSE_PER_STACK;
|
|||
|
|
}
|