DefBall (codename). Месяц разработки

Header 02.png

Всем привет!

Никогда не писал девлогов, но пост на Гамине к этому подтолкнул. Не знаю, насколько удачна эта затея, но попробовать давно стоило. Ну так вот.

Психолочное нематематичное вступление

Начну с того, что у меня есть вредная привычка. Время от времени я захожу на какой-нибудь CrazyGames и тешу свой мозг дофаминовыми играми с тупым геймплеем. Бывает, дело затягивается до четырёх утра, а на следующий день я «ни о чём не жалею». Чуть больше месяца назад шальной нейрон в моей голове решил повторить затею.

02-battle-button-clicker.gif

Игра называлась (и до сих пор называется) Battle Button Clicker. И так как я любитель несколько извращенных развлечений, спать в ту ночь я лёг с уставшим указательным пальцем правой руки. Но, что важнее, не только с ним, но и с парой мыслей:

  • закликивание — зло;
  • я ведь мог бы сделать так же, только лучше.

Разработчики бывают разные, и каждый случай уникальный. Я не исключение. 10-го мая 2024 года в моей голове роились

Есть чувство неудовлетворённости. Тяжесть в груди, как обычно бывает. Занятия, которые я себе придумываю, как будто не имеют шанса принести мне в конце концов желаемый профит.

Я снова начал копать в сторону Purple Dreams (вернее, Омнидельты). Неделю назад пришла в голову идея, и всё тут. Я вспоминаю, как начинал писать движок для реинкарнации Аиды на Си. Это долго. Это не то чтобы сложно. Но это долго. Это очень долго. Когда я начинаю подсчитывать, сколько времени у меня ушло просто на то, чтобы освоить азы кастомной аллокации и сколько времени прошло с начала работы над проектом, мне становится тяжело. Появляется мысль: я не успею. Непонятно, до чего не успею — до своей смерти или до скачка технологий, или ещё до чего-то.

С Дельтой — ну… Она подарила вдохновение на какое-то время. А теперь я опять не знаю, за что зацепиться, чтобы начать развитие проекта. Как будто я ищу вдохновения, радости, интриги; я как будто бы пытаюсь сам себя заинтересовать. Придумать сцены, которые заставят меня интересоваться.

А сейчас я испытываю тяжесть. Она мешает мне хотя бы просто начать… нет, она как будто бы лишает меня самой возможности чувствовать что-то другое, ведь она лежит сверху, а интерес лежит снизу. Как я почувствую интерес, если сверху — тяжесть?

Я знаю, что это было 10 мая, потому что сейчас передо мной лежит телефон, а в нём открытый SimpleMind с заметкой. Именно в тот день я возобновил привычку ежедневно выгружать свои мысли на физический носитель, по возможности устанавливать себе глобальные цели и постоянно себе о них напоминать.

SimpleMind 01.png

Если кому-то покажется, что мысли были унылыми и депрессивными, то не переживайте. Уже на следующий день

Похоже, для меня решение в деньгах.

Сейчас это кажется таким простым. Как будто бы. Наверное, на деле не является. Но может быть, мне на самом деле надо просто взять готовый говно-движок и забабахать стрёмный вырвиглазный неотёсанный проект длиной в месяц (условно). Чтобы он просто начал приносить деньги. Ведь так проще. Не париться ни о чём. Просто что-нибудь сделать. Минуя всякие принципы, вопросы «а почему», «как лучше» и всё такое. Как было с финалкой.

Конечно, все мы, находящиеся здесь, понимаем, что решение для нас не только и не столько в деньгах. Если бы нам были нужны только деньги, мы бы точно не выбрали геймдев. Геймдев — это низкомаржинально. Геймдев — это высокорисково. Но мы раз за разом возвращаемся к играм, потому что у нас не остаётся другого выбора: нам нравится делать игры, и мы ничего не можем с этим поделать. Извращённые развлечения — действительно мой профиль, стоит признать это ещё раз.

Выбор движка

Тут всё оказалось совсем несложно. Я бы даже сказал, что не было никакого процесса выбора; вместо него был процесс попроще.

  1. Скачать и установить Unity.
  2. Создать пробный проект.
  3. Подождать 15 минут, пока пробный проект откроется.
  4. Удивиться количеству прошедшего времени, закрыть проект, и открыть его повторно.
  5. Подождать 10 минут, пока готовый проект откроется.
  6. Удивиться ещё раз, закрыть проект и открыть его повторно.
  7. Подождать 10 минут, пока готовый проект откроется, и сделать вывод, что 10 минут — это действительно 10 минут.
  8. Закрыть проект на Unity.
  9. Вспомнить про то, что когда-то пробовал Godot.
  10. Скачать и установить Godot.
  11. Создать пробный проект.
  12. Подождать несколько секунд, пока пробный проект откроется.
  13. Закрыть пробный проект.
  14. Открыть пробный проект.
  15. Обрадоваться.
  16. Снести Unity к херам.

Вот честно, ни разу не позавидовал Unity-разработчикам. Unity уже не тот. Конечно, по поводу Godot у меня тоже есть сомнения, но он в сравнении с Unity — как Blender четырёхлетней давности в сравнении с 3DS Max.

Ну, раньше Blender действительно летал и не говорил тебе:

Blender (end of an era).jpg

Мол, сноси Семёрку и ставь Десятку.

Может быть, вы ещё не слышали, но я пришел сюда для развязывания священных войн, так что скажувброшу прямо:

мне сразу же не понравился GDScript:

  • обязательные точки с запятой, обозначающие конец строки и принятые во многих других нормальных языках, отменены; приходится переносить строки с помощью обратного слеша, и выглядит это настолько же уродски, как в макросах C++;
  • фигурные скобки, обозначающие область видимости и границы if...else, заменены индентацией;
  • Visual Studio? Нет. Приходится пользоваться тем автокомплитом, который имеется в самом Godot (хотя я задумывался о том, чтобы попробовать VS Code плюс godot-tools);
  • По той же причине забудьте об удобном рефакторинге — он даже в рамках одного скрипта не работает (есть Ctrl-D и Ctrl-Shift-F, но это совсем другое);
  • нет интерфейсов;
  • класс из одного скрипта не всегда узнаёт о только что появившихся свойствах и методах класса в другом скрипте; спасает перезагрузка проекта (благо, длится она несколько секунд);
  • присваивание внутри более сложного выражения не поддерживается, так что нельзя сделать some_method(a = some_other_method()) (но этой фичи не было и в C#, так что можно и потерпеть);
  • автокомплит ломается при использовании $, хотя я таким всё равно не пользуюсь;
  • GDScript является интерпретируемым; я узнал об этом спустя месяц разработки;
  • GDScript слизан с Python. for i in 1e18: Python.burn_in_hell_with_exclamation_mark()

С другой стороны:

  • к встроенному автокомплиту привыкаешь за неделю — ничего страшного, когда-то давно я и без него обходился;
  • используя GDScript, никаких ограничений, связанных с .NET, не испытываешь и вообще о них не думаешь;
  • по словам разрабов, из всех поддерживаемых языков GDScript имеет наиболее тесную интеграцию с движком, но я не проверял;
  • так как большую часть времени я занимаюсь написанием кода, Godot стал для меня настоящим IDE с большой буквы… нет, со всех трёх заглавных букв. Ощущения от него примерно такие же, как от пользования Flash IDE в далеких нулевых: мне не приходится покидать редактор, чтобы начать писать код.

По большей части, ощущения от движка ровные и приятные. Можно сказать даже, что я испытываю удовольствие от его использования. Как минимум, мне не надо ждать десяти минут, чтобы открыть пустой проект.

Игры-референсы

В качестве референса был выбран DefBall образца 2016 года, который выглядел так:

04-DefBall-2016.gif

Позже я развил идею в другую игру с кодовым названием Aida:

Aida-(a).gif

Моей задачей было сделать игру, похожую на Battle Button Clicker и использующую подходы из ДефБолла и Аиды. Про общую концепцию я напишу в одном из следующих постов, если они, конечно, будут. А пока что в этом тексте и так слишком много букв.

Начало разработки

Первым делом я вспомнил, что у меня 4K-монитор, а это, как оказалось, не всегда приятно. Я задумался, а как вообще делаются 2D-игры под разные разрешения, и вышел на статью, лежащую в доках Godot, как раз про это. Решил пока что рендерить в одно разрешение и растягивать отрендеренное по размеру окна.

Что дальше? Надо сделать главного героя — зонд игрока. Я засел в After Effects и сделал тестовый ассет. Импортировал его в движок, научил поворачиваться в сторону мышки. Но было видно, что вращение всего спрайта приводит к нехорошим искажениям, так что в дальнейшем надо было сделать по-другому.

06-Probe-asset,-rotation.gif

Оказалось, Godot умеет рисовать круги программно и неплохо с этим справляется. Правда, без антиналожения. Метод рендеринга поддерживает нецелые радиусы, в результате круг уменьшается не рывками, а плавно:

05-Circle-from-code.gif

Чтобы избежать использования ассетов на этапе разработки, я решил пока что рисовать блобы с помощью четырех кругов, как это было в Аиде. Тут же реализовал shrinking — растворение блоба в агрессивной окружающей среде и, как следствие, уменьшение его радиуса.

07-Blob-drawing-and-shrinking-behavior.gif

Затем я приделал к блобу физическое представление. Я унаследовал блоб от RidigBody2D (чтобы в дальнейшем об этом пожалеть). А потом столкнулся с проблемой: при изменении радиуса соседние блобы перестают друг с другом взаимодействовать. Это происходило из-за того, что Shape2D является ресурсом, а ресурсы не уникальны. Для того, чтобы можно было устанавливать радиус каждому блобу индивидуально, нужно было создавать дубликат формы, назначенной в сцене.

08-Shape2D-duplication,-non-unique-Resource-problem-solved.gif

Дальше я попробовал создавать экземпляры CircleShape2D в рантайме и навешивать их на блобы. Всё получилось. Но до полного создания блоба из-под кода, минуя сцены, я так и не добрался.

09-CircleShape2D-being-created-at-runtime.gif

На тот момент меня ещё беспокоила зависимость геймплея от разрешения. Физика в Godot пиксельная, радиусы блобов тоже измеряются в пикселях, и при изменении разрешения (например, при игре на другом мониторе) какие-то вещи перестанут работать так, как надо. Я решил ввести аналог метрической системы. Отныне радиусы блобов задавались в условных внутриигровых юнитах, а при ините переводились в пиксели. То есть при создании блоба я устанавливал радиус radius в нужное значение, а блоб уже затем рассчитывал значение _radius_px. Ох и натерпелся я потом с этой системой. И ещё натерплюсь. Но, пожалуй, оно того всё же стоит.

Own unit scale _radius_px 01.png

Own unit scale _radius_px 02.png

Снаряды, враги и взрывы

После этого я реализовал снаряды, пока без пушек. Долго думал, как же быть с децентрализованной обработкой столкновений. Не придумал ничего лучше, чем просто смириться. В итоге не пожалел (но это только пока). Реализовал функционал появления блобов: их непрозрачность увеличивается от нуля до единицы после добавления на сцену.

10-Scene,-Probe,-Bullet,-rotation-without-lookat.gif

И наконец-то добавил врагов. При столкновении с зондом враги должны были отнимать часть его здоровья (вернее, массы) и самоуничтожаться. При этом изначально я хотел сделать зонд неподвижным, как в оригинале; но после коллизии враг передавал зонду свой импульс, и тот начинал двигаться. В оригинальной Аиде у меня была самописная физика, и неподвижные объекты делались просто: я устанавливал массу блоба в +Infinity, и он получался статическим; то ли я специально обрабатывал такие ситуации в движке, то ли просто все расчёты ускорения автоматически возвращали ноль из-за бесконечной массы — я уже не помню, да и лень смотреть. Так как в Godot у меня не было своей кастомной физики, я не мог просто установить массу зонда в бесконечность: физика руинилась, блобы пропадали с экрана. В итоге придумал применять к зонду силу, похожую на силу упругости: чем дальше от изначального положения, тем она выше. Выглядело интересно, и я эту штуку оставил.

11-Enemies,-Bullets,-central-position.gif

Ввел понятия «логическая масса» и «физическая масса». Если вкратце, то они нужны были для того, чтобы отделить здоровье блоба от его поведения в физической подсистеме. Если подробнее, то вот

Возможно, придется вводить две массы: логическую и физическую. (Нужно заменить термин «логическая».) Логическая масса — это аналог здоровья блоба; она является произведением площади блоба на логическую плотность его материала. Физическая масса — это масса физического тела (внутри Godot’а); это произведение площади блоба и его физической плотности. Физическую массу я решил отделить от логической для того, чтобы было проще управлять поведением блобов при столкновении. Например, я хочу, чтобы у снаряда было много здоровья (логической массы), но при этом чтобы он не отталкивал врагов при столкновении; в этом случае логическую плотность я устанавливаю в обычное (или большое) значение, а физическую — в ноль (или в малое значение).

К слову, термин «логическая» я так и не заменил.

И всё-таки физика, сделанная за тебя — одновременно благословение и проклятие. Ты получаешь множество вещей из коробки, но лишаешься контроля. Если бы я захотел добавить в игру статические блобы, мне пришлось бы наследовать их не от RigidBody2D, а от AnimatableBody2D. Вроде бы, один класс блобов, просто одни неподвижны, а другие двигаются. Но в GDScript нет множественного наследования (может быть, оно к лучшему). Единственное решение, которое я придумал, — вводить классы поведений Behavior и цеплять их к объектам, как компоненты в Unity. Но до этого я не дошёл, потому что пришлось бы полностью переписывать код.

Была ещё одна неприятная штука: снаряды после столкновения отлетали назад или начинали двигаться с меньшей скоростью; в оригинале такого не было.

12-Physical-and-logical-densities,-Bullets-bouce-back.gif

Сделал отрисовку взрывной волны с помощью GradientTexture2D. Меня беспокоило то, что при больших радиусах взрыва придется создавать слишком большую текстуру, и рендеринг будет тормозить. Поэтому я ограничил размер текстуры максимальным значением, а для больших взрывов просто растягивал спрайт, на который эта текстура была натянута. Но теперь на ней были видны пиксели; позже я решил эту проблему, просто установив фильтрацию в нужное значение: оказалось, и на низких разрешениях градиенты смотрятся отлично. Это ж градиенты.

После того, как с отрисовкой взрыва было покончено (но не насовсем), я добавил раздельные слои Node2D для разных типов объектов: враги добавлялись на один слой, взрыв — на другой и так далее.

13-Explosion-rendering,-separate-layers.gif

Реализовал расталкивающее поведение взрыва. Мне не нравилось, как взрыв себя ведёт, но на пока этого было достаточно.

14-Explosion-behavior.gif

К этому моменту я посмотрел на результаты своих трудов и подумал: пожалуй, для первой версии фич хватает. Стало быть, пора официально завершить работу над версией v0.0.0 и переходить к v0.0.1.

v0-0-0.jpg

(На дату тега в TortoiseGit можно не смотреть, потому что я не пушил теги на гитхаб, а так как проект гулял с домашнего компа на рабочий и обратно, в какой-то момент локальный проект остался без тегов. Тег для версии v0.0.0 я создавал вручную гораздо позже.)

В описании к версии я указал:

Probe, Enemies, Bullets and Explosions are implemented.

Такая маленькая строчка, а сколько работы под ней спрятано!

Молния, пушки и приятные ништяки

Пришло время приступать к другим вещам. Я реализовал молнию, пока без визуальной части

15-Lightning,-behavior-only.gif

а потом с ней. Отрисовку производил программно с помощью draw_line().

16-Lightning-with-visuals.gif

Наконец-то я решил проблему со снарядами, которая не давала мне покоя. Чтобы они не отскакивали и не теряли скорость при столкновениях, в _process() я кэшировал текущую скорость в _prev_linear_velocity, а в методе _on_body_entered() восстанавливал ее из закэшированного _prev_linear_velocity.

Bullet velocity fix 01.png

Bullet velocity fix 02.png

Да, это лечится настолько просто.

17-Bullet-velocity-fix.gif

Сделал гансеты и пушки к ним. Наконец-то всё стало выглядеть, как в оригинале.

18-Gunsets.gif

Гансет — это набор пушек. Пока что расстановка пушек контролируется относительным расстоянием до центра блоба-владельца и дельтой угла. Но такой подход не всегда красиво выглядит, и в будущем нужно будет ввести дополнительное условие для расстановки пушек.

К тому времени меня стала беспокоить ещё одна вещь: fluent interface. Несмотря на «неоптимизированность» самого подхода, он мне нравится: с его помощью можно удобно инициализировать сложные иерархии объектов, в большинстве случаев не прибегая к их именованию. Вот, например, как были сделаны мозги босса в Аиде:

Fluent 04.png

В GDScript разделителем строк служит не точка с запятой, а, собственно, переход на новую строку; нельзя просто взять и разделить строку на несколько частей, нажав на Enter, дописав нужную часть строки и поставив ; (как это делается в C / C++ / C# / JavaScript и много где ещё). Перенести строку можно, используя обратный слеш, но это выглядит так себе. Решение, как всегда, оказалось простым: мы делаем простой метод fluent():

Fluent 01.png

подготавливаем методы-инициализаторы, как обычно:

Fluent 02.png

а потом оборачиваем всю инициализацию в вызов fluent(), не забыв прикастовать результат к нужному типу:

Fluent 03.png

Всё, что находится между открывающей и закрывающей скобкой при вызове любого метода, не будет делиться на части при переносе строк; GDScript понимает, что выражение ещё не завершено, поэтому нам не надо ставить обратные слеши в конце каждой незавершенной строки.

Вроде бы, мелочь, а приятно.

Мана и особые снаряды

Затем я реализовал методы _draw_circle() и _draw_truncated_circle() для отрисовки усеченного круга и круга с заданным количеством точек.

19-Truncated-circle.gif

Это позволило мне добавить в игру ману. Казалось бы, отрисовка маны — простая задача, но, как оказалось, всё сложнее, и даже в оригинале я рисовал ее четырьмя частями.

20-Mana.gif

Взрывные снаряды — то, чего не было в оригинале. Из-за того, что новые блобы появляются близко к центру взрыва, импульс, действующий на них, получается слишком большим, и их уносит далеко-далеко.

21-Explosive-bullets,-too-high-speeds.gif

Искрящиеся снаряды — снаряды с молнией. Из-за отсутствия интерфейсов пришлось приделать способность обладания молнией вообще всем блобам на свете.

22-Lightning-bullets.gif

Тут я посмотрел, что у меня получилось, и снова сказал: молодец, могёшь!

  • Lightning, GunSets with Guns, Probe’s mana, explosive Bullets and sparkling Bullets were implemented.
  • Bullets don’t bounce off the Enemies after collision.
  • Explosion now deals direct damage.
  • Attenuation object was implemented; now it’s used inside Explosion.
  • Enemies don’t stop accelerating towards the player’s Probe after being hit by Explosion.

Еще больше мелких ништяков

С новыми силами я сел за рисование задника. Вернее, не рисование, потому что рисовать я, видимо, не очень люблю. Я использовал After Effects и процедурный шум. На первое время пойдет.

23-Background.gif

На тот момент не было способа извне кастомизировать взрывы и молнии, приделанные к снарядам. Я ввел объекты-определения, наследующиеся от базового класса Def; это легковесные объекты, подобные определениям в старом добром Box2D, которые содержат только данные, необходимые для инициализации объекта, и больше ничего.

Explosion Def.png

Потом враги научились наносить урон зонду игрока. Вместе с тем появилась необходимость отображать минимальный и максимальный радиус зонда. Вообще мне хотелось придерживаться подхода, при котором GUI минимален или совсем отсутствует, а всю инфу о состоянии персонажа можно узнать, просто взглянув на него.

24-Enemies-deal-damage,-Probe

Затем я стал думать о том, как сделать врагов-пустышек, не наносящих урона зонду игрока. Такие враги могли бы пригодится на обучающих уровнях игры. Рассуждения привели меня к мысли о том, что придется вводить коэффициент прямого урона, как это было в оригинале. И хотя обсуждение камикадзе-взаимодействий должно было остаться за кадром, я всё же приведу цитату из лога:

Ввожу коэффициент прямого урона. Он необходим, так как если я захочу сделать пустышки, не наносящие урона, придется устанавливать логическую плотность в ноль. Это приведет к неопределенности радиуса. (…) Здесь логическую плотность можно рассматривать как защиту блоба, а коэффициент прямого урона — как атаку. (…) На самом деле, не всё так просто. Я не зря вводил понятие камиказде-взаимодействия: в случае попарного расчета урона могут возникать ситуации, когда оба блоба остаются живы. Камикадзе-взаимодействие решает эту проблему. Сначала блоб-камикадзе стремится истратить всю свою массу на прямой урон по противнику; если противник не выживает, камикадзе летит дальше. В этом случае расчет урона ведется в определенном порядке, и порядок его применения имеет большое значение.

Получается, я считал камикадзе-взаимодействия пережитком прошлого и хотел отказаться от него, а получилось так, что ранее ввел их неспроста. Даже собственные проекты со временем сильно забываются. Тем более проекты восьмилетней давности.

Последная фича, которую я реализовал, — эффекты. К ним относятся эффекты горения, оглушения, заморозки и прочие штуки, которые можно «прицепить» на объект и обновлять их вместе с ним. Я реализовал их следующим образом:

  • каждый блоб обладает собственным экземпляром эффектора;
  • эффектор, в свою очередь, может содержать ссылки на вложенные интеграторы;
  • интеграторы могут быть разных типов: интегратор горения, интегратор оглушения и так далее;
  • интегратор содержит эффекты своего типа: например, интегратор горения содержит эффекты горения;
  • интегратор сам решает, каким образом ему обращаться со своими эффектами. Он может использовать самый мощный или самый продолжительный эффект, или объединять несколько маленьких эффектов в один большой;
  • при добавлении эффекта к блобу мы просим эффектор: эй, эффектор, добавь эффект горения к блобу. Эффектор создает интегратор горения, если он ещё не создан, и добавляет к нему эффект.
  • при обновлении блоб обновляет и эффектор, но блоб сам ответственен за применение эффектов к себе. В данный момент реализация именно такова.

25-Burn-effect.gif

Итоги

Конечно, было много другой работы, о которой я здесь не написал. Если навскидку, то:

  • я портировал из старых проектов методы перевода цвета из HSL в RGB, потому что имеющиеся в Godot мне не подошли (или скорее, я торопился);
  • дописал дополнительный класс для затухания взрывной волны; вместо степенной функции он использовал кривую Безье;
  • более-менее поднастроил затухание взрывной волны (мда, на настройку механик иногда уходит слишком много времени);
  • написал кастомный HSLA-градиент — нативный градиент Godot почему-то не справлялся с альфа-каналом;
  • написал простую систему частиц для эффекта горения.

В итоге получилась играбельная технодемка, которой я зачем-то ни с кем не поделюсь, потому что устал и не буду билдить бинарники за просто так:

FINAL.gif

На этом всё. Я чуть-чуть замучился и хочу спать. Пишите, пожалуйста, в комментариях, любые мысли по поводу игры и девлога, куда мне следовало бы двигаться дальше, к какому издателю обращаться (хотя подождите. какой издатель. мы же инди), и вообще все мысли по поводу. Насколько перспективна задумка. Может, кому-то хочется поиграть в демку. Если будет надо, конечно же, соберу.

Ну, в общем, всё. Хорошо, что этот текст заканчивается. Подумали мы все.

Наверное, это моветон писать постскриптумы, так что.

P. S. Писать девлог, как оказалось, — большой труд. Не представляю, как это делают авторы на YouTube.

P. P. P. S. Не знаю, зачем я навыделял всякие фрагменты текста жирным; мне показалось, что так будет чуть читабельнее.

Вот теперь точно всё. Всем добра и удачи!

Обновление поста от 2024−05−23

Сделал билд под Винду. Скачать можно здесь.

Управление: стрельба — ЛКМ, взрыв — пробел.

  • Зонд
    • Максимальный радиус: 32
    • Радиус смерти: 8.
    • Логическая плотность: 1.
    • Физическая плотность: 0.1.
  • Гансет
    • Характеристики всех снарядов.
      • Скорость растворения всех снарядов, юнитов в секунду: 2.
      • Логическая плотность: 1.
      • Сопротивление среды (Godot-native, работает плохо): 0.6
    • Главная пушка.
      • Начальный радиус снарядов: 6.
      • Начальная скорость снарядов: 200.
      • Разрывные снаряды.
        • Вероятность выпуска разрывных снарядов: 0.25.
        • Дельта радиуса: 45.
        • Импульс воздействия у эпицентра: 40.
        • Прямой урон у эпицентра: 5.
        • Время жизни взрыва: 0.4 секунды.
      • Искрящиеся снаряды.
        • Вероятность выпуска искрящихся снарядов: 0.25.
        • Количество искр: 3.
        • Собственный радиус молнии: 20.
        • Урон на искру: 10.
    • Две дополнительные пушки.
      • Начальный радиус снарядов: 4.
      • Начальная скорость снарядов: 200.
    • Задняя пушка.
      • Начальный радиус снарядов: 5.
      • Начальная скорость снарядов: 200.
    • Воспламеняющие снаряды.
      • Все снаряды имеют 15-процентный шанс стать воспламеняющими.
      • Урон в секунду: 50 единиц массы.
      • Длительность горения: 2 секунды.
  • Взрывной модуль.
    • Дельта радиуса: 100.
    • Кулдаун: 0.5 секунды.
    • Стоимость: 30 единиц маны.
  • Искровой модуль.
    • Радиус: 40.
    • Количество искр: 5.
    • Урон на искру в секунду: 50.