#pragma once #include "sim/element.h" // ----------------------------------------------------------------------------- // Снаряжение бойца: РАБОЧЕЕ состояние оружия. // // Разделение простое и важное: // * ЧТО такое ствол (дальность, урон, разброс, магазин, вид вспышки) — // это FirearmDef на сущности-оружии, sim/weapon_defs.h; // * В КАКОМ ОН СЕЙЧАС СОСТОЯНИИ (откат, патроны, перезарядка, увод ствола) — // это Firearm здесь, и живёт оно на бойце. // Подменили сущность оружия — поменялись все статы разом, состояние обнулилось, // и ни одно правило боя об этом не узнало. // // Правила боя от класса НЕ зависят. И стрелок, и боец ближнего боя обязаны // проверить своих перед ударом; меняется только геометрия проверки — // конус «дуло -> цель» против сектора замаха (docs/11-melee.md). // ----------------------------------------------------------------------------- #include #include "engine/hash.h" #include "engine/math.h" #include "sim/weapon_defs.h" #include "tuning.h" // --- дальнобойное ------------------------------------------------------------ struct Firearm { FirearmDef def{}; // копия статов сущности-оружия, обновляется каждый шаг // --- состояние --- float cooldown = 0.0f; int ammo = 0; float reloadTimer = 0.0f; // > 0 — идёт перезарядка float recoilDeg = 0.0f; // увод ствола, гаснет сам uint32_t shotSeq = 0; // номер выстрела: сеет детерминированный разброс bool Reloading() const { return reloadTimer > 0.0f; } bool Empty() const { return ammo <= 0; } // Доля магазина — для оверлея и HUD. float AmmoFrac() const { return (def.magazine > 0) ? Clampf(float(ammo) / float(def.magazine), 0.0f, 1.0f) : 0.0f; } // Разброс в радианах: стоя — минимальный, на ходу растёт до spreadMoveDeg, // сверху ложится накопленный увод ствола. // // Это ЕДИНСТВЕННОЕ место, где считается разброс. Его же читает проверка // линии огня: конус, по которому реально уйдёт пуля, обязан быть тем самым // конусом, который проверили на своих (см. ai/firing_solver.cpp). float SpreadRad(float speed) const { const float m = Clampf(speed / std::max(0.01f, MaxAgentSpeed()), 0.0f, 1.0f); const float deg = Lerpf(def.spreadStandDeg, def.spreadMoveDeg, m) + recoilDeg; return std::max(0.0f, deg) * DEG2RAD_F * std::max(0.0f, g_tune.spreadScale); } // Отклонение конкретного выстрела внутри конуса. Не генератор: чистый хеш // от (владелец, номер выстрела), поэтому прогон воспроизводим побитово. // pellet различает дробины одного залпа: без него вся картечь ушла бы // в одну точку и была бы неотличима от одиночного выстрела. float ShotOffset(int ownerIndex, float spreadRad, int pellet = 0) const { const uint32_t h = Hash3(uint32_t(ownerIndex) * 2654435761u, shotSeq, uint32_t(pellet) * 0x85EBCA6Bu); // Треугольное распределение: центр конуса вероятнее краёв. const float a = HashSigned(h); const float b = HashSigned(HashU32(h ^ 0x9E3779B9u)); return 0.5f * (a + b) * spreadRad; } }; // --- ближний бой ------------------------------------------------------------- // Три фазы, как в любом ощутимом ударе: замах видно заранее, урон живёт // в коротком окне, после удара боец открыт. Без разделения удар превращается // в мгновенный хитскан и не читается на 480x270. enum class MeleeState : uint8_t { IDLE = 0, // подходит или ждёт перезарядку WINDUP, // замах: ноги вкопаны, ось удара уже зафиксирована STRIKE, // активное окно: лезвие метёт дугу, есть рывок вперёд RECOVER, // после удара — уязвим и медленный }; struct MeleeWeapon { // Из чего состоит удар клинка. Копируется с сущности-оружия, как и всё // остальное ниже. ElementMix elem; // --- параметры (копируются с сущности-оружия каждый шаг) --- float reach = 1.15f; // от центра тела до конца замаха float arcDeg = 100.0f; // ширина сектора float windup = 0.18f; float strike = 0.12f; float recover = 0.26f; float interval = 0.62f; // между началами замахов float damage = 110.0f; float knockback = 2.4f; // импульс мишени ДО её сопротивления отбросу // --- состояние --- MeleeState state = MeleeState::IDLE; float phase = 0.0f; // остаток текущей фазы, сек float cooldown = 0.0f; float dir = 0.0f; // ось удара, зафиксирована на замахе float sweepSign = 1.0f; // с какой стороны заходит лезвие (чередуется) int targetId = -1; bool inReach = false; // цель в зоне удара прямо сейчас bool arcBlockedByAlly = false; // в секторе свой -> бить нельзя (аналог 6.3) // Кого уже задели ЭТИМ взмахом. Шестьдесят четыре бита, а не тридцать два: // ширина маски — это и есть потолок числа целей в мире (MAX_TARGETS), а // этажи на глубине упираются именно в него. uint64_t hitMask = 0; // --- связка --- int comboStep = 0; // какой удар связки будет следующим float comboTimer = 0.0f; // сколько ещё держится связка // --- запечённые параметры ТЕКУЩЕГО взмаха --- // Связка гнёт ширину, урон и тайминги, а поля выше переписываются из // снаряжения КАЖДЫЙ шаг (SyncLoadoutFromTuning). Поэтому значения взмаха // фиксируются на старте замаха и живут отдельно — иначе удар менял бы // свойства прямо в полёте. float curHalfArc = 0.0f; float curDamage = 0.0f; float curKnock = 0.0f; float curWindup = 0.0f; float curStrike = 0.0f; float curRecover = 0.0f; bool Committed() const { return state == MeleeState::WINDUP || state == MeleeState::STRIKE; } const MeleeComboStep& NextComboStep() const { return MELEE_COMBO[(comboStep >= 0 && comboStep < MELEE_COMBO_STEPS) ? comboStep : 0]; } // Полуширина сектора. Во время взмаха — запечённая, иначе — та, что будет // у СЛЕДУЮЩЕГО удара связки. Все потребители (проверка сектора, дуга, // оверлей, резерв линии) обязаны видеть одно и то же число: иначе // нарисованное и то, что бьёт, разъедутся. float HalfArc() const { if (Committed() || state == MeleeState::RECOVER) return curHalfArc; return arcDeg * 0.5f * DEG2RAD_F * NextComboStep().arcMul; } // Запечь параметры взмаха из базовых характеристик и шага связки. void BakeSwing(const MeleeComboStep& cs) { curHalfArc = arcDeg * 0.5f * DEG2RAD_F * cs.arcMul; curDamage = damage * cs.dmgMul; curKnock = knockback * cs.knockMul; curWindup = windup * cs.windupMul; curStrike = strike * cs.strikeMul; curRecover = recover * cs.recoverMul; } float PhaseLen() const { switch (state) { case MeleeState::WINDUP: return curWindup; case MeleeState::STRIKE: return curStrike; case MeleeState::RECOVER: return curRecover; default: return 1.0f; } } // 0 в начале фазы, 1 в конце. float Progress() const { return Clampf(1.0f - phase / std::max(1e-4f, PhaseLen()), 0.0f, 1.0f); } // Где лезвие при заданном прогрессе взмаха: от одного края сектора к другому. float BladeAngleAt(float progress) const { const float half = curHalfArc; return WrapPi(dir + sweepSign * (-half + 2.0f * half * progress)); } };