178 lines
9.0 KiB
C
178 lines
9.0 KiB
C
|
|
#pragma once
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
// Снаряжение бойца: РАБОЧЕЕ состояние оружия.
|
|||
|
|
//
|
|||
|
|
// Разделение простое и важное:
|
|||
|
|
// * ЧТО такое ствол (дальность, урон, разброс, магазин, вид вспышки) —
|
|||
|
|
// это FirearmDef на сущности-оружии, sim/weapon_defs.h;
|
|||
|
|
// * В КАКОМ ОН СЕЙЧАС СОСТОЯНИИ (откат, патроны, перезарядка, увод ствола) —
|
|||
|
|
// это Firearm здесь, и живёт оно на бойце.
|
|||
|
|
// Подменили сущность оружия — поменялись все статы разом, состояние обнулилось,
|
|||
|
|
// и ни одно правило боя об этом не узнало.
|
|||
|
|
//
|
|||
|
|
// Правила боя от класса НЕ зависят. И стрелок, и боец ближнего боя обязаны
|
|||
|
|
// проверить своих перед ударом; меняется только геометрия проверки —
|
|||
|
|
// конус «дуло -> цель» против сектора замаха (docs/11-melee.md).
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
#include <cstdint>
|
|||
|
|
|
|||
|
|
#include "core/hash.h"
|
|||
|
|
#include "core/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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Отклонение конкретного выстрела внутри конуса. Не генератор: чистый хеш
|
|||
|
|
// от (владелец, номер выстрела), поэтому прогон воспроизводим побитово.
|
|||
|
|
float ShotOffset(int ownerIndex, float spreadRad) const
|
|||
|
|
{
|
|||
|
|
const uint32_t h = Hash2(uint32_t(ownerIndex) * 2654435761u, shotSeq);
|
|||
|
|
// Треугольное распределение: центр конуса вероятнее краёв.
|
|||
|
|
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
|
|||
|
|
{
|
|||
|
|
// --- параметры (копируются с сущности-оружия каждый шаг) ---
|
|||
|
|
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)
|
|||
|
|
uint32_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));
|
|||
|
|
}
|
|||
|
|
};
|