145 lines
5.5 KiB
C++
145 lines
5.5 KiB
C++
|
|
#include "sim/reactions.h"
|
|||
|
|
|
|||
|
|
#include <algorithm>
|
|||
|
|
|
|||
|
|
#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<Target>& 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<Target>& 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;
|
|||
|
|
}
|
|||
|
|
}
|