Хронология доработки прошлой версии коктейля молотова.
Этап 1. Аудит и критические исправления
Первым делом был проведён полный аудит существующего кода на утечки энтити, валидацию указателей и корректность работы с движком GoldSrc.
Что было найдено:
- Энтити с бесконечными
Think-циклами не имели механизма самоочистки — при определённых условиях они жили вечно и съедали лимит edicts
- Отсутствовала проверка
is_nullent() перед обращением к энтити — race condition с FL_KILLME флагом (движок не удаляет энтити мгновенно, Think может вызваться повторно после постановки флага)
- Зоны тушения дымом
SmokeRadius не имели самоочистки
Что было сделано:
- Добавлена валидация указателей перед всеми операциями с энтити
- Реализована самоочистка
SmokeRadius через SetThink с таймером
- Добавлена проверка линии прямой видимости (line-of-sight) для режима урона 1
- Внедрена система
EnableHookChain / DisableHookChain — хуки урона активны только при наличии живых зон огня, в остальное время отключены для экономии ресурсов
Ключевое решение: хранение состояния через entity var fields
var_iuser1..var_iuser4,
var_fuser1..var_fuser3,
var_vuser1..var_vuser2. Это стандартный паттерн GoldSrc — каждая энтити несёт свои данные в встроенных полях
entvars_t, без внешних массивов.
Этап 2. Зажигательная граната и разделение по командам
Главная фича этапа — у ТТ и КТ разные гранаты с разными моделями, спрайтами и визуальными эффектами.
Реализовано:
- ТТ получают молотов: горящий фитиль в полёте
env_sprite + var_aiment, вспышки при удержании, оранжево-фиолетовые спрайты огня
- КТ получают зажигательную гранату: без фитиля и вспышек, другие спрайты огня, эффект искр
TE_SPARKS (12.5% шанс за тик)
- Разные HUD-иконки:
weapon_molotov.txt для ТТ, weapon_incendiary.txt для КТ
- Нарастающий урон: чем дольше игрок стоит в огне, тем сильнее урон (множитель растёт от 1.0 до
molotov_damage_max_mult)
- Дымовая граната тушит огонь, зона тушения живёт
molotov_smoke_duration секунд
Проблема: идентификация типа гранаты. Первая реализация определяла тип по текущей команде владельца
get_member(id, m_iTeam). Это ломалось, когда КТ подбирал молотов ТТ — граната становилась зажигательной.
Решение: тип записывается один раз при выдаче в
var_iuser4 на энтити оружия и «путешествует» по цепочке: оружие в инвентаре → летящая граната → зона огня. Всё поведение читает из var_iuser4, никогда не из команды текущего владельца.
Проблема: WeaponList и HUD-иконки. GoldSrc отправляет
WeaponList MSG_INIT один раз для всех клиентов при старте карты. Нельзя отправить разные иконки разным игрокам через
MSG_INIT.
Решение: при выдаче гранаты конкретному игроку отправляется
WeaponList MSG_ONE с правильным именем оружия. Клиент обновляет определение слота только у себя. Перехватчик
HookWeaponList трогает только
MSG_INIT, пропуская
MSG_ONE без изменений.
Этап 3. Вода и грабли движка
Реализация поведения гранаты в воде оказалась сложнее, чем казалось.
Проблема: SOLID_TRIGGER иногда не срабатывал в воде. Видимо в GoldSrc энтити с
SOLID_TRIGGER могут тонуть в воде, а
Touch коллбэки перестают вызываться. Граната, упавшая в воду, просто лежала на дне и не могла быть подобрана.
Решение: для надежности заменили
Touch на
SetThink с поллингом — каждые 0.3 секунды проверяем ближайших игроков через
FindEntityInSphere. Это надёжнее, чем коллизия.
Проблема: модель гранаты в воде.** КТ граната, упавшая в воду, показывала модель молотова вместо зажигательной.
Решение: тип гранаты читается из
var_iuser4 при создании визуала, не из текущей команды владельца (тот же паттерн, что и везде).
Этап 4. Цилиндрическая модель урона
Стандартная сферическая проверка урона в GoldSrc
FindEntityInSphere наносила урон игрокам этажом выше или ниже зоны огня.
Проблема: игрок на втором этаже получал урон от огня на первом, потому что его 3D-расстояние до центра огня было меньше радиуса.
Решение: цилиндрическая проверка:
Горизонтальное расстояние: sqrt(dx*dx + dy*dy) <= flRadius + 32
Вертикальное окно: dz >= -64 и dz <= +120
Почему именно [-64, +120]: центр огня на уровне пола, центр модели стоящего игрока на ~36 юнитов выше пола. Ступеньки добавляют 32-64 юнита. Второй этаж — обычно 128+ юнитов. Окно +120 покрывает все одноуровневые ситуации, но не задевает этаж выше.
Та же проверка применена к размещению визуальных эффектов — спрайты дебриса и модели огня не рисуются на поверхностях за пределами вертикального окна.
Этап 5. Режим разгорания
Новая механика: после половины времени горения радиус зоны огня увеличивается.
Реализация:
var_fuser2 на зоне огня хранит текущий эффективный радиус
var_fuser3 хранит gametime, когда нужно расшириться (или
-1.0 если уже расширились / режим выключен)
- В
ThinkFire проверяется: если
flCurTime >= flSpreadTime — радиус увеличивается,
SetSize обновляется,
var_fuser3 ставится в
-1.0
Это однократное расширение — граната горит маленьким пятном первую половину, затем «разгорается» и занимает больше площади.
Этап 6. Равномерное распределение частиц
Спрайты дебриса
TE_SPRITE должны заполнять весь диск зоны горения равномерно. Первая реализация использовала случайные углы и
MakeVectors — точки кластировались по краям.
Попытка 1: спираль Вогеля (Vogel spiral).
r = sqrt((i + 0.5) / N) * R
theta = i * 2.39996 (золотой угол ~137.5 градусов)
Отличный алгоритм для статичного размещения. Но
TE_SPRITE — одноразовый клиентский эффект, обновляется каждый тик. С фиксированными координатами из спирали получилось 4 неподвижных кластера, потому что каждый тик рисовались одни и те же 4 точки.
Попытка 2 (финальная): случайная равномерная выборка по диску.
r = sqrt(random_float(0.0, 1.0)) * flRadius
theta = random_float(0.0, 6.28318)
Каждый тик — новые случайные координаты.
sqrt(rand) корректирует распределение по площади — без него точки кластировались бы в центре, потому что при малом радиусе длина окружности меньше, но вероятность та же. С
sqrt плотность точек одинаковая по всей площади диска.
Этап 7. Случайный поворот моделей пола
Модели огня на полу выглядели однообразно — все повёрнуты в одну сторону. Добавлен случайный yaw (поворот вокруг вертикальной оси).
Проблема: первая реализация ставила
angles[1] = random_float(0.0, 360.0) ПОСЛЕ вызова
FloorOriginAngles(). Функция
FloorOriginAnglesвычисляет pitch и roll на основе нормали поверхности, используя входящий yaw как опорное направление. Перезапись yaw после расчёта ломала выравнивание — модели начинали «вылезать» из текстур на наклонных поверхностях.
Решение: рандомизировать yaw
ДО вызова
FloorOriginAngles. Функция использует его как опорный вектор и корректно рассчитывает pitch/roll для наклонной поверхности в этом направлении.
Этап 8. Килфид с разделением по типу
В киллфиде к нику убийцы добавляется приставка:
[ᴍᴏʟᴏᴛᴏᴠ] для ТТ,
[ɪɴᴄ] для КТ.
Тип определяется по
var_iuser4 инфликтора (зоны огня), не по команде атакующего.
Этап 10. Система покупки и мультиязычность
Массив команд покупки: вместо одного
register_clcmd("molotov", ...) — массив
BUY_COMMANDS[][], который регистрирует все команды в цикле. Добавить новую команду — одна строка в массиве.
Локализация: все строки вынесены в lang файл с ключами CVAR_* (для описаний кваров) и NTF_* (для уведомлений игрокам). Поддержка [ru] и [en].
Технические паттерны, выбранные для плагина
Entity var fields. Каждая сущность несёт весь свой контекст:
var_iuser4 — тип гранаты,
var_fuser2 / var_fuser3 — радиус и время разгорания, var_iuser2 — время конца горения,
var_iuser3 — счётчик созданных эффектов. Нет глобальных массивов, привязанных к entity ID.
Цепочка наследования типа. Тип записывается при
giveNade и копируется: weapon entity → flying grenade → fire zone. Любое поведение (модели, спрайты, звуки, урон, килфид) читает из entity, не из игрока.
Селективные хуки. HookChain для
CBasePlayer_TakeDamage активен только когда на карте есть живые зоны огня. Это убирает overhead от перехвата каждого вызова урона в игре.