«Вагон-вагон»: клубок технологий внутри простой текстовой игры, часть вторая

Продолжение рассказа о создании интерактивной новеллы для мобильных телефонов.

«Вагон-вагон»: клубок технологий внутри простой текстовой игры, часть вторая

Сценарные и визуальные тесты, перевод на английский, сборка мобильного приложения, тягучее болото плагинов для Apache Cordova и страдания при заливке в AppStore.

Первую часть статьи читайте здесь.

В предыдущей серии

“Вагон-Вагон” - это на первый взгляд простая текстовая игра. Разработку начинали с нуля, финальной целью поставили релиз на мобильных телефонах. Важной промежуточной точкой выбрали конкурс русскоязычной интерактивной литературы - КРИЛ-2017. К нему хотели подготовить полноценную веб-версию и узнать реакцию настоящих игроков, которые лично с нами незнакомы.

Для создания веб-версии я решил использовать:

  • Текстовый движок Ink
  • Визуальный движок Phaser
  • Cистему автоматизации PyDoit

Также выделил отдельно связующий слой игровой логики и назвал его GameLogic. Сам слой писал на Python, транслировал в JavaScript с помощью конвертера Transcrypt.

Сценарные тесты. Jasmine

Прежде чем продолжить, нужно сказать пару слов о структуре игры. Эта структура окончательно сформировалась к КРИЛ и позже почти не менялась.

В каждой попытке изменить судьбу главных героев есть четыре развилки. В каждой развилке игроку нужно сделать выбор из трёх вариантов развития диалога. Наивный подсчёт показывает, что общее число путей прохождения игры: 3*3*3*3=81. Эти пути ведут к семи различным концовкам. Плюс есть длинная “истинная” концовка, которая открывается только тогда, когда все обычные концовки открыты. Плюс есть две первых особых попытки изменить судьбу, когда игрок только знакомится с игрой, и число вариантов выбора ограничено.

Полное вдумчивое прохождение игры со всеми вариантами и развилками занимает около двух часов.

<p>Скриншот игры с типичными вариантами выбора. Три существенных и один тупиковый, он вернёт игрока на тот же самый экран.</p>

Скриншот игры с типичными вариантами выбора. Три существенных и один тупиковый, он вернёт игрока на тот же самый экран.

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

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

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

Нас выручили автоматические тесты. Но уже не юнит-, а тесты более высокого уровня, сценарные. Для них я использовал фреймворк Jasmine. Фактически я сделал урезанную html-версию игры без визуальной части (Ink + GameLogic), запустил её в браузере и стал прогонять на ней различные игровые сценарии. Jasmine предоставил для этого необходимую инфраструктуру.

<p>Пример теста на то, что проходима первая обучающая попытка изменить судьбу.</p>

Пример теста на то, что проходима первая обучающая попытка изменить судьбу.

Сценарии, на которые написал тесты:

  • Обучение проходимо
  • Все концовки можно найти
  • “Истинная” концовка открывается только тогда, когда все остальные концовки открыты
  • Работает сохранение и загрузка текущего состояния истории из локального хранилища браузера
  • Распределение концовок должно быть равномерным. Например, не должно быть такого, что половина из 81 вариантов прохождения приводит к одной и той же концовке игры, а на вторую половину приходятся остальные шесть/
Тест на то, что игра проходима от начала до конца: обучение, семь концовок, финал и даже титры.
Тест на то, что игра проходима от начала до конца: обучение, семь концовок, финал и даже титры.

Эти тесты были чрезвычайно полезны. Они позволили без боязни вносить правки в структуру истории даже на поздних стадиях разработки. Моих скромных знаний JavaScript было вполне достаточно для того, чтобы их писать и поддерживать.

Альтернатив Jasmine много, но я их не изучал. Решающим фактором стало то, что установить Jasmine можно через PyPI (Python Package Index). Для меня это самый быстрый и естественный способ установки, так как основная часть игры написана на Python.

Визуальные тесты. SikuliX

Хорошо, я более-менее застраховался от ошибок в игровой логике и в истории, но как быть с ошибками в визуальной части? Что если я неправильно создал кнопку в Phaser, или текст исчез после открытия занавеса и не появился обратно (реальный случай)? Опять же, чем раньше я узнаю, что после очередного моего изменения в коде что-то в графике испортилось, тем быстрее смогу найти причину ошибки. А каждый раз запускать браузер и протыкивать все варианты скучно и долго.

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

Часто графическая библиотека предоставляет внешний интерфейс для обращения извне к графическому интерфейсу. Например, для обращения к окнам Windows есть механизм Windows Messages.

В Phaser, к сожалению, ничего подобного нет. Тут надо уточнить, что я использую Phaser 2, а в Phaser 3 автор движка обещал над этой проблемой поработать. Надо будет проверить, получилось у него или нет.

Так или иначе в то время, когда я готовил “Вагоны” к конкурсу, Phaser 3 ещё не вышел, и у меня была единственная альтернатива - тесты на основе распознавания образов.

Принцип таких тестов простой: делаем скриншоты объектов/кнопок, назначаем для этих объектов определённые действия игрока (нажатия, перетаскивания и т.д.), формируем из этих действий последовательность, дальше запускаем игру и пытаемся эту последовательность выполнить. Если на каком-то шаге нужного объекта на экране не нашлось, значит, тест завершается с ошибкой.

SikuliX - отличная программа для формирования и запуска таких тестов. Бесплатных альтернатив я не нашёл.

Тестируем первые три выбора игрока. За функцией skip_until_and_click скрывается цикл, в котором игрок постоянно нажимаем кнопку “Дальше”.
Тестируем первые три выбора игрока. За функцией skip_until_and_click скрывается цикл, в котором игрок постоянно нажимаем кнопку “Дальше”.

Самое впечатляющее в этих тестах это то, как они выглядят в редакторе. Наглядней некуда.

Но практической пользы от них мало. Вот почему:

  • Скриншоты не всегда распознавались корректно, случались ложные срабатывания.
  • SikuliX устроен так, что он “захватывает” курсор мыши и двигает им вместо пользователя. Параллельно работать в это время невозможно. Мышка всё время куда-то уползает, а тесты не проходят.
  • Поддерживать их сложно. Изменился шрифт или цвет фона - все скриншоты надо переделывать.

Реализовал один единственный визуальный тест на обучение и отключил его от основного процесса сборки. Проводил его только перед отправкой новой публичной версии на сервер. И то скорее из паранойи, так как перед этим сам вручную проходил большую часть игры.

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

Кстати, больше всего крови выпили у меня ошибки даже не в графике, а в звуке: зацикливание, прерывание, наложение… Интересно, можно ли их покрыть тестами? Скажем, озвучку можно тестировать через распознавание речи… Кажется, родилась идея для стартапа…

Работа с текстом. LanguageTool

Графики у нас мало, зато много текста (~27 тыс. знаков с пробелами). При этом в Ink не встроена проверка орфографии для русского языка. Как следствие было море опечаток. Я пытался полностью копировать исходный текст Ink в условный Word, править там ошибки, а потом копировать обратно, но это было очень неэффективно.

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

Очередное небольшое исследование привело меня к LanguageTool. Программа проста, популярна, работает из командной строки, поддерживает русский и английский языки, есть бесплатная версия. Смотрел альтернативы: Grammarly, Ginger, Орфограммка - но одновременно всех этих свойств нет ни у кого.

<p>Результат проверки фразы: “Герои сидят в темноте рядом, смотрят на уплывающий город. И боятся шелохнутся.”</p>

Результат проверки фразы: “Герои сидят в темноте рядом, смотрят на уплывающий город. И боятся шелохнутся.”

На практике оказалось, что LanguageTool проверяет не только орфографию, но и пунктуацию, и даже согласование частей речи.

Были и ложные срабатывания. Например, на именах собственных, латинских цитатах или фантастических терминах, таких как Прогресс-программа. Все эти срабатывания я занёс в список исключений, добавил прогон LanguageTool в систему автоматизации и привязал его к изменению исходных текстов.

Получилась хорошая защита от мелких опечаток. Всем рекомендую.

Обфускация и разные версии браузеров. Closure Compiler

Приближался конкурс. Мы закончили работу над сценарием и игровой логикой. Нужно было выкладывать игру в публичный доступ. Выкладывали на собственном сайте - были планы по его развитию. Но планы так и остались планами, и сейчас могу сказать, что намного логичнее было бы пойти на стороннюю площадку. Например, на itch.io.

В любом случае, для быстрой загрузки сайта нам нужно было максимально уменьшить размер файлов, содержащих JavaScript-код. Для этого я использовал Google Closure Compiler.

Этот инструмент превращает код в более компактный, не меняя при этом его смысла: сокращает имена переменных, убирает комментарии и т.д. Например, исходники Phaser занимают 3,3 Мб. После сжатия через применения Closure Compiler они превращаются в 800 Кб. А вся наша игра весит в сумме 5,5 Мб. Почувствуйте эффект.

Почему выбрал именно Closure Compiler? Потому что он уже встроен в Transcrypt. Т.е. Для сжатия кода, который транслировался из Питона, мне достаточно было выставить нужную опцию в Transcrypt. А сжимать мне нужно было только внешний JavaScript-код, например связанный с загрузкой Ink. Странно было бы это делать другой программой-паковщиком.

Пример JavaScript-кода до обработки Closure Compiler.
Пример JavaScript-кода до обработки Closure Compiler.
<p>И тот же код после обработки Closure Compiler.</p>

И тот же код после обработки Closure Compiler.

Также Closure Compiler умеет преобразовывать код более позднего стандарта JavaScript к более раннему стандарту. Это важно, скажем, для поддержки Internet Explorer и браузеров на старых Андроидах.

Узнал я об этом полезном свойстве и применил его уже после конкурса. Мотивировали жалобы игроков.

Думал я и о настоящей обфускации - т.е. о защите от декомпилирования. Но тут далеко не продвинулся. После пары статей по теме понял, что игра не стоит свеч.

И да, был один игрок, который попал в тупик при прохождении и действительно принялся читать исходный код, чтобы разобраться что к чему. И это было даже… приятно.

Перевод на английский

Мы поучаствовали в конкурсе КРИЛ. Выступили достойно, заняли не последнее место, получили много разных отзывов.

Среди отзывов был один особенный. Прочитав его, мы поверили, что делаем игру хорошую и что надо доводить дело до конца. То есть переводить игру на английский и делать полноценный релиз на iOS и Android. Подробнее об этом писали в отдельной статье.

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

Интересно, что наше совещание и решение идти до конца пришлось ровно на середину всего срока разработки. Прошло 11 месяцев со старта нашего приключения, а релиз случился ещё через 11 месяцев. При этом мы рассчитывали закончить месяцев за 6. С запасом... Классика.

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

Технически у меня было три варианта внедрения в игру английских текстов:

  • Делать два набора исходных ink-файлов: на русском и на английском. Загружать один из этих наборов при старте игры в зависимости от выбранного пользователем языка.
  • Оставить один набор ink-файлов, но все строки заменить на именованные константы. Константы превращать в строку на определенном языке на уровне GameLogic.
  • Ничего не менять на уровне Ink. На уровне GameLogic либо оставлять строки как есть, если язык русский, либо использовать русские строки как ключи словаря и получать по ним английские.

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

Самым простым и удобным казался третий. Я его и выбрал.

Итоговый словарь для локализации - простой файл в формате csv. Слева - ключ. Справа - значение.
Итоговый словарь для локализации - простой файл в формате csv. Слева - ключ. Справа - значение.

В этом подходе есть свои недостатки. Так, если делать правки в исходном тексте игры, то потом их надо не забыть перенести в таблицу локализации. Чтобы обойти этот недостаток я написал сценарный тест, который прогоняет всю игру и каждую фразу пытается перевести с русского на английский. Если фразы не нашлось в словаре, то тест завершается с ошибкой.

Ещё были некоторые проблемы с подстановками в языке Ink. Например, конструкция "Номер поездки: {get_repeat_count_for_player()}" означает, что в итоговом тексте вместо “{get_repeat_count_for_player()}” должно подставиться число совершённых попыток изменить судьбу. Причём число попыток рассчитывалось на уровне Ink, и в GameLogic уже приходила строка с зашитым числом: “Номер поездки: 10”. И как по такому ключу можно было отыскать что-то в словаре?..

В итоге я перенёс логику подстановки текста из Ink на уровень GameLogic. Из Ink теперь приходил текст "Номер поездки: [[repeat_count_for_player]]”, я его превращал в "Trip number: [[repeat_count_for_player]]” и уже после этого делал замену “[[repeat_count_for_player]]” на число поездок. Соответственно число поездок тоже пришлось считать и хранить на уровне GameLogic.

“Вагон-Вагон” превратился в Railways of Love.
“Вагон-Вагон” превратился в Railways of Love.

Как организовывать локализацию, если пользоваться текстовой подстановкой Ink на полную катушку - не знаю. Для нашей игры хватило компромиссного решения.

Сборка мобильного приложения. Apache Cordova

Мы поучаствовали в конкурсе IFComp. Выступили достойно, заняли не последнее место, получили много разных отзывов. Убедились, что идём правильный дорогой. Перевод удался, и мы смотрелись уместно среди состоявшихся англоязычных авторов. Проигрывали в тонкостях использования языка, но выглядели самобытно. Это мы тоже подробно описывали в отдельной статье.

Обложка игры для IFComp.
Обложка игры для IFComp.

Теперь нас ждала финальная цель - релиз.

К этому времени я уже относительно неплохо ориентировался в html5-разработке и знал, что есть стандартный способ для превращения web-страницы в мобильное приложение - фреймворк Apache Cordova.

Фреймворк устроен так: для каждой платформы (iOS, Android, Desktop) есть шаблон простого приложения, которое умеет делать одно - запускать внутри себя аналог браузера и загружать в этом браузере вашу html-страницу. Дальше Cordova подключает к странице свою JavaScript-библиотеку. Через эту библиотеку можно обращаться к нативным функциям операционной системы. Например, работать с камерой iPhone. Вся работа организована через командную строку.

Сначала Apache Cordova меня очаровала своими достоинствами:

  • Лёгкий старт. Я сделал первую версию мобильного “Вагон-Вагона” буквально через день-два чтения документации. И он был полностью проходим от начала до конца и вообще замечательно работал. Только в этот момент я окончательно оставил мысли о портировании на Unity или PyKivy.
  • Open Source. Исходники фреймворка доступны, при желании можно модифицировать платформенную часть как угодно.
  • Есть сервис PhoneGap Build. С его помощью можно удалённо собирать приложения, созданные на базе Apache Cordova. И в том числе iOS-приложения! Это важно, потому что иначе для сборки под iOS понадобился бы Мак. Которого у меня нет. И который стоит дорого.
  • Плагинная архитектура в лучших традициях Роберта Мартина. Есть небольшое ядро системы. Есть несколько официальных и много пользовательских плагинов, которые можно гибко комбинировать.
<p>Конфигурация фреймворка для “Вагон-Вагона”. Из списка плагинов можно составить представление, какие особенности мобильного приложения мы настраивали.</p>

Конфигурация фреймворка для “Вагон-Вагона”. Из списка плагинов можно составить представление, какие особенности мобильного приложения мы настраивали.

Однако дьявол скрывался в мелочах. И на каждый плюс нашёлся свой существенный минус:

  • Если что-то сразу не заработало, то разбираться в причинах можно неделями. Требуются знания сразу во многих областях. Например, на определённой версии Android в игре нет звука. Кто виноват? Phaser, ядро Cordova, внешний плагин, сервис сборки, сама операционная система?.. Универсальный ответ - кривые руки программиста, но решать проблемы он, к сожалению, не помогает.
  • Сервис PhoneGap Build устроен иначе, чем десктопная Cordova. И отличия эти важны. Так, например, нельзя использовать некоторые хаки, которые позволяют обходить баги ядра на десктопе. Приходится честно ждать, пока, во-первых, выйдет официальное обновление ядра, а во-вторых, пока это обновление доберётся до сервиса. Часто это здорово тормозит разработку.
  • Пользовательские плагины очень быстро устаревают. Не поддерживают новые версии операционных систем. Поэтому другие пользователи пишут новые плагины, которые часто поддерживают новые версии ОС, но не поддерживают старые. Поэтому выбор правильного плагина часто превращается в угадайку. И всё это в условиях мультиплатформенности.

Самая показательная проблема, с которой я лично столкнулся при разработке - нельзя по-нормальному локализовать имя игры под iOS. Есть три пользовательских плагина для этого. И все не работают из-за бага ядра. Есть хак, который позволяет этот баг обойти. Но сервис сборки защищён от этого хака. Занавес.

Я потратил на эту проблему недели две. Разобрался, как можно обойти защиту сервиса, и даже написал свой супер-костыльный плагин, который гвоздями забивает английское название игры. Как же я был счастлив, когда всё заработало!

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

Иконки, экраны загрузки. ImageMagick

Немного о неучтённых мелочах.

Возьмём иконки. Для поддержки разных моделей телефонов и планшетов мне пришлось сгенерировать 24 иконки разного размера. Причём их нужно ещё правильно назвать. Ошибёшься в разрешении или в названии - в лучшем случае получишь ошибку при загрузке приложения в магазин. Так получилось с AppStore. В худшем - возьмётся иконка с неправильным разрешением. Будет размытой. Такое видел на Андроиде.

Многообразие иконок для iOS-версии “Вагон-Вагона”. В магазине, в меню настроек, в списке уведомлений, на главном экране - это всё разные иконки. Умножаем на число разных моделей - получаем то, что получаем.
Многообразие иконок для iOS-версии “Вагон-Вагона”. В магазине, в меню настроек, в списке уведомлений, на главном экране - это всё разные иконки. Умножаем на число разных моделей - получаем то, что получаем.

С экраном загрузки те же проблемы. Для каждой модели устройства свой размер и своё уникальное название.

<p>Многообразие экранов загрузки для iOS-версии.</p>

Многообразие экранов загрузки для iOS-версии.

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

Очень удобно.

Релиз

C грехом пополам мы доползли до релиз-кандидата.

Это было мучительно долго и нудно. Я собирал по друзьям и знакомым телефоны для тестирования, правил для них специфичные ошибки; потом заполнял карточку игры в AppStore и Google Play, а там тоже свои нюансы, чего только стоит политика конфиденциальности; читал юридические статьи, заводил счёт в банке и т.д. и т.п.

Но вот релиз-кандидат, наконец, готов. Осталось отправить его в магазин. C Google Play всё прошло на удивление гладко, а вот заливка в AppStore - это была целая эпопея.

PhoneGap Build позволяет удалённо собрать и даже подписать архив с игрой релизным сертификатом, но для отправки архива в магазин всё равно нужен Мак. Поиски в Интернете привели меня к трём возможным решениям проблемы:

  • Воспользоваться сервисом App Uploader. Это делать я испугался. Ведь через этот сервис в какой-то момент придётся передать пару логин-пароль для Apple-аккаунта. И ничто не мешает сервису эту пару прочитать и у себя сохранить. Возможно, от этого сценария можно защититься, но я решил не рисковать.
  • Попросить Мак у друзей. Способ хороший, но, к сожалению, подходящих друзей не нашлось.
  • Арендовать Мак онлайн и подключиться к нему удалённо. Компаний, предоставляющих такие услуги много, но у большинства муторный процесс регистрации. Например, часто требуют привязать банковскую карту даже для стартового бесплатного периода. Поэтому я выбрал MacMiniVault. Там для пробы нужна только почта. Действительно, мне очень быстро дали доступ к тестовому Mac Mini. Я не учёл только того, что физически он находился в Милуоки...

Скорость отклика была такой, что после нажатия клавиши на клавиатуре символ появлялся на экране секунд через 5. Ох, какая же это получилась длинная ночь!..

Часа четыре ушло на установку программного обеспечения и первую попытку залить архив в магазин.

Эмоции были сродни игры в старый Prince of Persia, где нажал не вовремя кнопку прыжка, упал в пропасть - начинай всё сначала. А мне нужно было точно ткнуть мышкой в правильное окошко. Ошибся - окошко пропало, открылся какой-нибудь калькулятор, курсор мышки улетел неведомо куда - расхлёбывай потом минут пять, не меньше.

<p>Prince of Persia. Много вспоминал о нём в ту ночь.</p>

Prince of Persia. Много вспоминал о нём в ту ночь.

Но я справился! Причем перезаливал архив несколько раз из-за проблем с иконками и экранами загрузки, о которых уже упоминал.

Ещё в последний момент отключил Google Analytics, потому что плагин с аналитикой по какой-то причине обращался к Adertising Identifier, и робот Apple по этому поводу ругался страшными словами, де, объясните, зачем вам нужен рекламный идентификатор, если самой рекламой вы не пользуетесь. Вопрос был резонным, и я с ним спорить не стал. Так аналитика у нас осталось только на Андроиде…

Но и это ещё не всё. Следующий сюрприз ждал нас в ходе ревью. Нас не пропустили в магазин с формулировкой: “Это не игра, а книга”. И ещё издевательски предложили опубликоваться в iBooks.

Получив этот ответ, я целый день ходил как потерянный. Столько всего было пройдено, столько сил и времени потрачено, столько души вложено в каждый пиксел нашей “не игры, а книги”. И вот такой результат. Было очень-очень-очень обидно.

Я написал апелляцию. Уже начитавшись страшных историй из Интернета и не очень веря в успех... И нас на следующий день без вопросов пропустили.

Появилась долгожданная кнопка “Release this version”. Надо ли описывать, что мы чувствовали в тот момент?..

Итоги

Ох, это было долго...

Эта статья - финальная точка в конце длинного пути разработки “Вагонов”. Разработка на незнакомых тебе технологиях сродни приключению - никогда не знаешь, что встретится за поворотом. Мне было важно описать этот опыт. Во многом для себя. Но надеюсь, что получилось достаточно связно, и этот опыт пригодится кому-то ещё.

Считаю, что он был удачным. Косвенное подтверждение тому наша следующая игра - “Серость”. Скорость её разработки существенно выше - версию для конкурса КРИЛ-2018 мы сделали с нуля за 3 месяца. Впрочем, пока она технологически полностью повторяет “Вагоны”, и мы только собираемся попробовать что-то новое. В первую очередь в области графики. Так что скоро затормозим...

Напомню, что “Вагоны” можно посмотреть в Google Play и App Store, а следить за нашими дальнейшими мытарствами можно в VK, FB, Твиттере и немного в Инстаграме.

Спасибо за внимание!

2626
7 комментариев

Комментарий недоступен

2

Отличная нота для того, чтобы завершить разработку "Вагонов". Спасибо вам большое за комментарий! Теперь у нас впереди только серость, серость, Серость...

1

Спасибо за материал, даже казалось бы простой жанр, на поверку не такой и простой в реализации.

2

Брал вашу игру на андроид, но не зашло((Даже висит в папке игры. Как то не понятно, в чем цимес то в целом. Но игра приятная по ощущениям. Удачи в новых проектах.

1

Жаль, что игра вам не понравилась, но тем более спасибо за покупку! И удача нам понадобится)

1