#include "sim/reactions.h" #include #include "tuning.h" namespace { // Урон реакции по одной цели. Идёт мимо сопротивлений: реакция — это не удар // стихией, а её распад, и «сопротивление растрескиванию» пришлось бы заводить // восьмым столбцом в каталоге ради одного случая. void ReactionDamage(Target& t, float amount) { if (!t.alive || t.IsProp()) return; t.hp -= amount * VulnerabilityOf(t.status); t.hurtTimer = g_tune.targetHurtTime; } } // namespace void ApplyElementalHit(std::vector& targets, int index, const ElementMix& mix, int depth, CombatEventLog* events, bool fromReaction) { if (index < 0 || index >= int(targets.size())) return; Target& t = targets[size_t(index)]; if (!t.alive || t.IsProp()) return; // Запрет рекурсии: урон от реакции не кладёт статусов, а значит и новую // пару сложить не может. if (fromReaction) return; // 1. Что именно легло этим ударом. Кинетика статуса не кладёт. Element applied[ELEMENT_COUNT]; int appliedCount = 0; for (int e = 1; e < ELEMENT_COUNT; ++e) { if (mix.share[e] <= 0.0f) continue; const Element el = Element(e); const StatusRule rule = StatusRuleOf(el); if (rule.maxStacks <= 0) continue; StatusState& st = t.status; const bool wasEmpty = st.stacks[e] == 0; st.stacks[e] = std::min(rule.maxStacks, st.stacks[e] + 1); st.timer[e] = rule.duration; // свежий стак продлевает весь набор if (wasEmpty || true) applied[appliedCount++] = el; } if (appliedCount == 0) return; // 2. Сложилась ли пара. Ищем среди УЖЕ висящих стихий ту, что даёт реакцию // с только что наложенной. if (t.status.reactionIcd > 0.0f) return; for (int i = 0; i < appliedCount; ++i) { const Element fresh = applied[i]; for (int e = 1; e < ELEMENT_COUNT; ++e) { const Element other = Element(e); if (other == fresh || !t.status.Has(other)) continue; const Reaction r = ReactionOf(fresh, other); if (r == Reaction::NONE) continue; // 3. Реакция состоялась: гасим обе стихии и ставим откат. t.status.stacks[int(fresh)] = 0; t.status.stacks[int(other)] = 0; t.status.reactionIcd = REACTION_ICD; const float power = ReactionPower(depth) * ReactionScale(r); if (r == Reaction::SUPERCOND) { // Срезает сопротивления вместо урона: кладёт стаки пустоты, // потому что именно она в этой игре означает «защита не // работает». Отдельной сущности для среза не заводим. const StatusRule vd = StatusRuleOf(Element::VOID); t.status.stacks[int(Element::VOID)] = vd.maxStacks; t.status.timer[int(Element::VOID)] = vd.duration; } ReactionDamage(t, power); if (ReactionIsArea(r)) { for (size_t k = 0; k < targets.size(); ++k) { if (int(k) == index) continue; Target& other2 = targets[k]; if (!other2.alive || other2.IsProp()) continue; if (DistanceSq(other2.pos, t.pos) > REACTION_RADIUS * REACTION_RADIUS) continue; // Соседям достаётся половина и НИКАКИХ статусов: это урон // от реакции, он рекурсию не запускает. ReactionDamage(other2, power * 0.5f); } } if (events) events->Push(CombatEventKind::REACTION, -1, t.pos, Vec2{}); return; // одна реакция за удар: две подряд — это тот же взрыв дважды } } } void UpdateStatuses(std::vector& targets, float dt) { for (Target& t : targets) { StatusState& st = t.status; if (!t.alive || t.IsProp()) { if (st.stacks[0] != 0 || st.reactionIcd > 0.0f) st.Clear(); continue; } st.reactionIcd = std::max(0.0f, st.reactionIcd - dt); // Стаки сгорают по одному: набор тает постепенно, а не исчезает разом. for (int e = 1; e < ELEMENT_COUNT; ++e) { if (st.stacks[e] <= 0) continue; st.timer[e] -= dt; if (st.timer[e] <= 0.0f) { --st.stacks[e]; st.timer[e] = StatusRuleOf(Element(e)).duration; } } // Горение считается от МАКСИМАЛЬНОГО здоровья цели: иначе на громиле // оно не значило бы ничего, а мелочь дожигало бы само. const int burn = st.stacks[int(Element::INC)]; if (burn > 0) t.hp -= t.maxHp * BURN_PER_STACK * float(burn) * dt; } }