#pragma once // ----------------------------------------------------------------------------- // Статусы стихий и реакции между ними. // // Стихии сами по себе (sim/element.h) — это только множитель урона: «верный // ствол бьёт втрое больнее». Этого мало, чтобы стихии стали ЗНАНИЕМ, ради // которого они заводились: множитель работает одинаково у того, кто понимает // систему, и у того, кто просто взял ствол посильнее. // // Статусы и реакции добавляют второй слой, который работает только у первого: // наложить ДВЕ разные стихии на одну цель и получить реакцию — это решение о // составе отряда, а не о характеристиках оружия. // // ТРИ ОГРАНИЧИТЕЛЯ, И ВСЕ ОБЯЗАТЕЛЬНЫЕ (docs/21-damage.md §4.1): // // 1. потолок стаков — один стак не значит ничего, полный набор меняет бой; // 2. внутренний откат реакции на цель — иначе очередь из сорока пуль даёт // сорок взрывов за секунду; // 3. запрет рекурсии — урон ОТ реакции не накладывает статусов и не может // запустить следующую реакцию. // // Третий пункт не теоретический. Без него цепочка «реакция бьёт по площади → // урон кладёт стихию → складывается новая пара → реакция» уходит в самоповтор // на первом же скоплении, и кадр умирает. Именно так ломаются все игры с // триггерами, и чинить это потом дороже, чем запретить сразу. // ----------------------------------------------------------------------------- #include #include #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; }