Статья состоит из двух частей: что и почему изменено и добавлено (для тех, кому интересны проблемы в интерфейсе), как именно вносились изменения (для желающих узнать про внутренности игры и технические подробности). Защита многоэтажного подземелья, геймплей пошаговый: строим ловушки и комнаты, расставляем свои отряды, вторгается противник и пытается уничтожить ядро на последнем уровне, по пути уничтожая комнаты и ловушки. Бои тоже пошаговые, максимум по 3 отряда с каждой стороны, до 6 юнитов в каждом отряде. Особенность в том, что непосредственное влияние игрока ограничено: расставленные отряды стоят на месте, в бою управление юнитами недоступно, в начале хода можно выбрать только приоритетную цель и применить способность командира, если он есть в отряде. Поэтому основной геймплей состоит в правильном подборе отрядов для усиления своих юнитов и ослабления вражеских. Сверху скриншот оригинальной версии, снизу модификации. Всплывающая подсказка для скиллов (перенос функционала из следующей игры серии) В оригинале маленький знак вопроса в левом нижнем углу по нажатию показывает/скрывает описание скиллов юнита. Добавлена всплывающая подсказка при наведении курсора на скилл на всех экранах с отображением скиллов.  Экран "найма": выбор юнита ("Найм" юнита состоит из трех экранов/шагов: выбор матери, выбор юнита, выбор аркан.) В оригинале нет никаких возможностей по поиску юнитов, выбора расы (категории) таким не назовешь, потому что она очень отдаленно связана только с типами юнитов (в расе "Beast" будут юниты с типом звери, но не все звери будут в этой расе), поэтому поиск юнитов по разным параметрам жизненно необходим. Почему подбор бойцов так важен: люди будут получать усиление от скилла "Human Boost" у юнитов в отряде, и ослабление от "Sap Human" у противника. А каждое совпадение типа защищающегося в "Slay" атакующего дает +1 к множителю урона и скилл "Slay ..." дает прибавку к множителю равную значению скилла. Пример: русалка (если лидер отряда) нанесет рыцарю (Type: Human, Man) шестикратный урон: базовый, +1 от Human в Slay, +1 от Man в Slay, +3 от скилла "Slay Man: 3" Примечание: нанять несколько одинаковых юнитов нельзя. 2.1 Круглые кнопки 1 и 2 слева вверху списка юнитов для переключения между юнитами заменены на прокрутку колесом мыши, таким образом можно показать более 60 юнитов. 2.2 Добавлен фильтр юнитов по стартовым скиллам, поиск не чувствителен к регистру, скиллы можно перечислять через запятую, полное совпадение названий не требуется. Фильтр помещен в единственное свободное место: левый нижний угол. Позже была добавлена кнопка "x" для сброса фильтра вместо ввода пустого значения. 2.3 Выбор расы (категории) юнитов сделан множественным вместо одиночного, так же убрана выбранная категория по умолчанию. 2.4 Список юнитов уменьшен на две колонки: стало 24 юнита вместо 30.2.5 На освободившееся место добавлены фильтры: по классу, тип, экипировке, счастью. Для всех доступен множественный выбор.  2.6 Добавлена возможность скрытия нанятых юнитов (включена по умолчанию). 2.7 Добавлена кнопка сброса фильтра. 2.8 Добавлена сортировка юнитов по стоимости, возможности найма, звезде (луне/месяцу, в каждой частью перевод термина разный).  2.9 Для юнитов в списке добавлена иконка счастья. От счастья юнита зависят его характеристики, стоимость найма и содержания, возможность усиления арканами. Счастье юнита зависит от положения звезд, времени суток и матери. 2.10 Добавлена смена матери кликом по портрету, вместо возврата на предыдущий экран.  2.11 (Отключаемая опция) Убран лимит найма в 36 юнитов на класс. Весьма вероятно, лимит был введен из-за интерфейса, а не по причинам баланса, поскольку в оригинале на других экранах можно показать только 36 юнитов одного класса. 2.13 Добавлен фильтр по арканам: юниты открываются созданием нового типа аркан, но в оригинале нет возможности узнать какие конкретно юниты открылись. Так же открытые арканы подсвечиваются. Экран "найма": выбор аркан Добавлена подсветка аркан, имеющих титулы с указанными скиллами.  Экран составления отрядов В оригинале нет никакого поиска по юнитам, так же, как и на экране найма. 4.1 Список юнитов уменьшен на один ряд: 30 юнитов вместо 36 4.2 На освободившееся место добавлен фильтр по типу и перенесен фильтр по классам. 4.3 Добавлен фильтр юнитов по всем скиллам, включая скиллы от аркан/титулов и экипировки. 4.4 Добавлена прокрутка списка юнитов. 4.5 Добавлен фильтр по обычным юнитам, командирам и свободным юнитам.  4.6 В оригинале лидер отряда меняется через выбор отряда и последующем нажатием на квадрат рядом с именем в левом списке. Добавлена смена лидера нажатием средней кнопки мыши в списке отрядов (перенос функционала из следующей игры серии).  4.7 Добавлена подсветка юнита в отряде при выборе юнита в правом списке.Причина: найти отфильтрованный юнит в списке отрядов для перемещения его в другой отряд бывает тяжело.  4.8 Скиллы, действующие на весь отряд, суммируются, но в оригинале посмотреть их значения можно только в бою. Добавлен просмотр этих скиллов при наведении на номер отряда.  4.9 (Отключаемая опция) В оригинале кнопки 1-4 под составом отряда позволяют сохранить и загрузить состав отрядов, но только всех сразу, а не выбранного, как может показаться по расположению кнопок. Добавлена сохранение/загрузка состава только выбранного отряда.  4.10 Добавлена всплывающая подсказа с составом сохранённого отряда. Экран тактики 5.1 Отряды, выставленные на карту, затемнены, аналогично экрану составления отрядов. 5.2 Добавлена смена лидера нажатием средней кнопки мыши в списке отрядов (аналог 4.6). 5.3 Добавлена просмотр скиллов отрядов при наведении на номер отряда (аналог 4.8). Экран экипировки В оригинале опять нет нормального фильтра юнитов, из удобств только подсветка категорий предметов для выбранного юнита. 6.1 Список юнитов уменьшен на один ряд: 30 юнитов вместо 36 (аналог 4.1). 6.2 На освободившееся место добавлен фильтр по типу и перенесен фильтр по классам (аналог 4.2). 6.3 Добавлен фильтр юнитов по всем скиллам, включая скиллы от аркан/титулов и экипировки (аналог 4.3). 6.4 Добавлена прокрутка списка юнитов (аналог 4.4). 6.5 Добавлен фильтр по обычным юнитам, командирам и отрядам. Фильтры по классу и типу блокируются при выборе фильтра по отрядам. 6.6 Добавлена подсветка юнитов, у которых экипирован выбранный предмет.  6.7 Добавлена подсказка со списком юнитов и их отрядом, у которых экипирован выбранный предмет. Количество высокоуровневой экипировки ограничено, на максимальном уровне доступна только одна штука, без этого поиски юнита, которому выдан нужный предмет занимают много времени. 6.8 Добавлен опциональный фильтр юнитов по выбранной категории экипировки.  6.9 В оригинале наличие экипировки на юните никак не отображается. Для юнитов добавлена красная рамка у значка типа экипировки, если слот занят (перенос функционала из следующей игры серии).  6.10 В оригинале для смены экипировки надо выбрать юнита, и потом перетащить предметы в слоты. Добавлено перетаскивание предметов прямо на список юнитов (перенос функционала из следующей игры серии).  6.11 Добавлена подсветка красным ресурсов, требуемых для покупки, если их недостаточно для покупки предмета.  6.12 Добавлена подсветка категорий и предметов с указанными скиллами.  Карта7.1 (Отключаемая опция) Добавлено предупреждение при низком уровне здоровья ядраПомимо врагов в подземелье, есть еще противники на поверхности (мини карта слева вверху), которые оказывают поддержку, в том числе бомбардировкой, которая наносит урон ловушкам, комнатам и ядру. Эти бомбардировки легко упустить, поэтому добавлено предупреждение, когда здоровье ядра падает ниже определенного уровня.Игра при Game Over даже не объясняет причину провала: то ли ядро разрушено, то ли ходы закончились, то ли другое условие миссии провалено. 7.2 (Отключаемая опция) Добавлено отображение состава всего отряда и их здоровье во всплывающей подсказке на карте.В оригинале при наведении на отряды показывается только суммарное здоровье и лидеры отрядов. Для детального просмотра необходим переход на другой экран.  Клетка карты Добавлено отображение скиллов отряда для своих отрядов и противника. В оригинале эта информация доступна только в бою.  Выбор титула Командиров, в отличии от обычных юнитов нельзя нанять и выбрать для них арканы, вместо этого при выполнение определенных условий можно выбрать титул.В оригинале узнать, какие скиллы дает титул можно только после его выбора, а бонусы к характеристикам и прочие изменения только вычислить самому. Добавлена всплывающая подсказка с текущим состояния персонажа и новыми скиллами с прочими изменениями.  (Отключаемая опция) Быстрое переключение между экранами вместо длительной анимации Добавлены горячие клавиши для сохранения/загрузки Настройки В начале создания мода настройки можно было менять только в специально созданном ui-mod.ini, но под конец опций уже стало больше и поэтому решил создать для них отдельную страницу настроек.  Игра рассчитана на несколько прохождений, но в начале каждого необходимо заново отстраивать подземелье. Планировал сделать шаблоны строительства для сохранения/загрузки строений на выбранном этаже. Но в игре доступно максимум 10 действий (найм, строительство, расстановка отрядов, события) за один ход, на этажах мест для строений больше 10, то есть построить все сразу невозможно. А выбор первоочередных строений из шаблона будет лишь немногим удобнее стандартной постройки. По этой причине от идеи пришлось отказаться. Встретившиеся проблемы: списки без поиска/фильтрации не информативность (отсутствие всплывающих подсказок, индикаторов экипировки, Game Over без объяснений причин поражения) медленные анимации интерфейса отсутствие базовых функций (горячие клавиши) местами над удобством просто не думали (смена лидера, смена экипировки), но потому исправили в следующей части Игра сделана на KiriKiri2/KAG3, популярный движок для визуальных новелл. Это сильно упрощает создание мода, поскольку инструментов для распаковки и запаковки ресурсов хватает, и игровые скрипты доступны в текстовом виде. Так же упрощает, что имена файлов должны быть уникальными в рамках проекта. Документация доступна, но на японском: TJS2 (скриптовый язык)  KiriKiri2 (стандартная библиотека) KAG В движке два типа скриптов: скрипты на TJS2, расширение *.tjs сценарии KAG, так же позволяющие вставки на TJS2, расширение *.ks В идеальном мире в сценариях была бы верстка интерфейса, а сами скрипты интерфейса в *.tjs. В реальности разработчики всю работу с экранами засунули в сценарии: в начале сценария задается определение класса, если его экземпляр не создан, а потом верстка. Примечание: название класса _SCREEN_TEMP_CLASS одинаковое для всех экранов. TJS2 в целом типичный скриптовый язык, но с неожиданными особенностями: запятая имеет значение:  это массив из двух элементов 0 и void, а  это . Из-за этого несколько раз возникали проблемы, когда после долгих поисков причиной бага оказывалась запятая в конце массива, поставленная по привычке из других языков. Для функций запятая работает схожим образом, но будет передаваться значение по умолчанию вместо void. словари есть, но работа с ними за пределами чтения/записи значений не тривиальна: вызов методов словаря доступен только через инструкцию incontextofvar dict = %[key: 'value']; (Dictionary.clear incontextof dict)(); получить ключи словаря или итерировать по нему нельзя, но можно скопировать в массив и получить одномерный массив типа [ключ 0, значение 0, ключ 1, значение 1] и работать уже с нимиvar a = []; a.assign(dict); // a == ['key', 'value', 'key2', 'value2']; конструктор класса остается доступен как обычная функция, поэтому создание экземпляра класса внутри этого же класса возможно только через доступ к global Со сценариями получше: в макросы можно передавать значения переменных из скриптов:  Но это работает с числами и строками, передать функцию таким образом нельзя. Поэтому вызов функция при событиях сделан через передачу имени и eval. в парсинге сценариев есть трудновоспроизводимый баг: комментарии в конце строки иногда вызывают зависание[macro ...] ; commentsНо комментарии с новой строки работают без проблем; commentsНе знаю уникальная ли это проблема игры или проблема движка изменения в сценариях применяются без перезапуска игры для распаковки Gabro или AE VN Tools для запаковки KirikiriTools для работы с кодом Visual Studio Code с расширениями KAG/KAGEX и TJS, с ними будет расцветка кода и сворачивание блоков, давно уже привычных валидации, рефакторинга, поиска использований и т. д. нет. Понадобилось три копии игры: оригинальная версия, на ней проверялась итоговая модификация, распакованная версия оригинала, в которой включен режим отладки, и на нее будут устанавливаться патчи, копия предыдущей версии, в ней проходила работа над модом. Сперва распаковал data.xp3 в папку data и удалил оригинальный файл (в нем хранятся игровые скрипты и сценарии). Затем распаковал patch.xp3, заменил файлы в папке data и переместил новые файлы в корень папки data, и опять удалил оригинальный файл. (Для слияния data с патчем был сделан отдельный скрипт на Python.) Режим отладки включается убиранием строки в startup.tjs: @set (K2COMPAT_PURGE_DEBUG = 1) После этого окно отладки доступно при запуске с аргументом -debug. Дополнительно наткнулся на проблему с Mercurial: не поддерживает иероглифы в именах файлов. Поскольку это были изображения, решил их просто исключить из контроля версий. В коде всё как обычно: однобуквенные переменные, повторы кода, кучи закомментированных блоков, магические константы, смешивание кода и верстки, злоупотребление массивами и словарями (функция возвращает массив, каждый элемент которого другого типа и имеют разный смысл), и так далее. Для вывода в консоль есть класс Debug, в игре используется синоним dm() для Debug.message(). Ошибки, вызывающие падение игры, можно найти в логах в папке сохранений (путь к ним можно узнать из System.dataPath). Игра сильно полагается на использование массивов и словарей, маленькие массивы и словари можно изучать из консоли, большие проще сохранить в файл: метод array.save('../log.txt') сохраняет значения массива в файл, каждое значение на новое строке метод (Dictionary.saveStruct incontextof %[])('../log.txt') сохраняет словарь в файл с форматированием После изучения кода удалось выяснить следующее: в движке отображение графики сделано слоями со своей иерархией, прозрачностью, активностью, вывод изображений и текста из скрипта через методы слоя или вспомогательные методы игры по факту понадобились всего четыре команды: нарисовать изображение draw_image_file(layer, ...), нарисовать текст text_draw_super(layer, ...), залить указанную область цветом layer.fillRect(), очистить указанную область layer.clear(), вывод изображений из сценария через макросы типа , , текст растровый, моноширинный, доступно несколько фиксированных размеров, ширина символа в приблизительно в два раза меньше размера шрифта, глобальные переменные DYNTXT, DYNTXT2 это игровые слои, предназначенные для вывода интерфейса из скриптов, для пользовательского ввода текста есть только типовое системное окно, для работы с пользовательским вводом есть кнопки-макросы , ,  (и еще куча с чуть другими последними буквами), у них есть пара общих атрибутов: no - уникальный номер кнопки на экране, по которому кнопку можно найти в массиве DYNBT, x, y - координаты на слое. графика для большинства кнопок — это изображения из сетки 3x1 или 3x2 для разных состояний кнопок: по умолчанию, нажата, фокус; второй ряд для тех же состояний в случае, когда кнопка активна; блокирование состояние кнопки делается в коде через затемнение, для работы со списками используется макрос , которому передается одномерный массив левых верхних углов ячейки  и размеры ячейки, в обработчик передается номер выбранной ячейки, название текущего экрана хранится в переменной _DSCRN, объект интерфейса текущего экрана в _DSCR во всех обработчиках событий для  надо вызывать DYN.unlock(); иначе интерфейс игры будет заблокирован. Имена файлов уникальные в рамках проекта, поэтому поиск файлов выполняется только в корне папки data или в папках, заранее указанных в скриптах. Складывать все файлы модификации в корень неудобно, поэтому создал отдельные папки для изображений и скриптов и подключил через: Storages.addAutoPath("ui-mod/images/");Storages.addAutoPath("ui-mod/scripts/");потом каждый новый скрипт подключал отдельно через:Scripts.execStorage("ModVersion.tjs"); Для вывода данных юнита, в том числе скиллов, в верхнем блоке (универсальном для многих экранов) используется функция: ca3.top_status_chars_update2(unit, layer). Но на разных экранах используется разный слой: на одних DYNTXT, а на других DYNTXT2. По этой причине пришлось добавить еще один слой: DYNTXT3, который дальше использовал для всех всплывающих подсказок. Для получения событий наведения/покидания курсором используется макрос , в который передаются заранее рассчитанные координаты ячеек скиллов. Функция вывода текста не разбивает строку на заданную ширину, поэтому для вывода текста в несколько строк сперва надо разбить текст на эти строки, а потом выводить их по одной. В оригинальном коде списки юнитов, и не только их, выводятся просто: функция с циклом по всему списку, проверка на единственное условие для отображения (нужный тип), и тут же рисование данных на слое. То есть помимо смешивания логики и представления ещё и пересчет происходит при смене страницы списка. В моде это переписал: сделал отдельный класс фильтра, в котором задаются условия для фильтрации. При изменении условий происходит поиск подходящих юнитов и в отдельную переменную сохраняется список индексов найденных юнитов. Ещё один класс отвечает за обновление состояние кнопок (по заданным условиям фильтра) и прочих элементов интерфейса, типа иконок счастья юнитов или текущего типа сортировки. И третий класс отвечает за прокрутку: получает список индексов и возвращает срез индексов, видимых на текущей странице, так же он рисует полосу прокрутки. В итоге логика работы и представление разделены, переделанная функция в цикле проходит по индексам, полученным от класса прокрутки и отображает необходимые данные. И на всех экранах используется один и тот же класс фильтра для юнитов, разница только в исходном списке. Отдельно стоит упомянуть фильтр по скиллам: в данных юнитов/аркан/предметов указаны названия скиллов на японском, поэтому при сперва идет поиск по скиллам для получения японских названий по английскому, а потом уже найденные скиллы используются в фильтре. Перевод части игровых данных хранится в данных юнита/предмета/скилла и т. п. Для получения перевода игра проходит по всем массиву и ищет нужный элемент по названию или id. Все подобные функции были переделаны на использование словаря с поиском по ключу. Благодаря передаче аргументов из скриптов удалось сделать это относительно просто: добавить множитель к длительности анимации.  А вот поменять таким образом все анимации для всех экранов было уже утомительнее. Эта часть доставила проблем из-за того, что слои типа DYNTXT не отображаются в этом режиме игры. Включить геймплейный интерфейс получалось только целиком, а включить только нужный слой не выходило. В итоге используется костыль: при отображении подсказки для DYNTXT3 меняется родительский слой, при скрытии возвращается оригинальный. Но на этом проблемы не закончились: после нажатия кнопки выбора титула кнопка уничтожается и событие ухода курсора с элемента не происходит - подсказка остается висеть. Добавил скрытие подсказки в коде, куда происходит переход по нажатию кнопки, но возможно проще было добавить скрытие подсказки по нажатию кнопки. Та же проблема, когда вызывается окно сохранения или загрузки по горячей клавише: событие не происходит когда кнопка перекрыта другим элементов, поэтому опять пришлось использовать костыль для скрытия подсказки в коде обработке горячих клавиш. Тут почти просто: добавил обработчик событий на onKeyUp для окна.Сохраняться и загружаться в игре можно не везде: и по причинам баланса и потому что состояние игры некорректно сохраняется и загружается из некоторых мест. Поэтому добавил проверку по названию текущего экрана из _DSCRN. В отличии от остальных экранов настройки это чистый TJS класс. При инициализации создаются слои для отображения текста и кнопки для переключения настроек: Buttons[0] = new ConfigScreenButtonLayer(kag, this, true, SetMapDivisionAllUnits, 280, 50, "ui_bt_cfg03"); Buttons[1] = new ConfigScreenButtonLayer(kag, this, false, SetMapDivisionAllUnits, 355, 50, "ui_bt_cfg04"); где третий параметр — это значение, а четвертый это функция для обновления конкретной настройки и вызова сохранения всех настроек. Сами настройки хранятся в файле ui-mod.ini, чтение и сохранение сделано через методы массива. Наличие параметров в файле и самого файла не обязательно, настройки по умолчанию есть в скрипте, а при сохранении файл будет создан, если отсутствует. Смену горячих клавиш в самой игре решил не делать, только через редактирование ini файла, но используемые кнопки выводятся корректно при смене. Без багов при создании мода не обошлось. Выше уже упоминал проблемы с парсером сценариев, ещё перепутал пару иконок типов (ожидаемо, учитывая их количество). Файл с настройками не работал в базовой версии, оказалось исходная папка меняется в зависимости от типа запуска: или папка с .exe или папка data, если распакованная версия. Указал System.exePath + 'ui-mod.ini' вместо '../ui-mod.ini'. Краш игры при увольнении юнита на экране составления отрядов и переключение матери юнита (при специфичных условиях). Плохо протестировал смену экипировки при перетаскивании предметов на юнита: было падение при перетаскивании предмета на пустую ячейку и можно было сменить экипировки для выставленного на карту юнита, что не должно быть доступным. Наиболее проблемными оказались подсказки для титулов: у них отдельный сценарий на ~2700 строк. Тут разработчики решили сэкономить пару десятков строк и частично объединили обработку двух командиров. В итоге были перепутаны командиры (из-за чего было еще одно падение) и их скиллы. За время активной работы надо модом вышло два патча, и позже вышло ещё два. И тут появляется проблема слияния официальных изменений с модом. Ничего лучше использования для этого WinMerge для второй копией с новым патчем и третьей с модификацией и ручным переносом всех изменений не придумал. Сравнение патчей было бы проще, но в патчи добавлялись новые файлы, на которых сравнение не поможет. Отдельных проблем добавил китайский перевод: в игре три языка - японский, английский, китайский, но к первоначальному релизу китайский перевод не был полностью готов и вместо него в большинстве мест заглушки на японском. Что не доставило бы проблем, если бы перевод был в отдельных файлах, но часть перевода вынесена в различные массивы в общем скрипте, а часть банальный if по текущему языку посередине функций, когда требуется показать сообщение или текст. Примечание: китайский перевод был готов к выходу игры в Steam, но китайцам он не понравился, поэтому у игры такой низкий рейтинг. Движок игры автоматически загружает файлы patch.xp3, patch2.xp3, patch3.xp3 и т. д., и файлы из них имеют приоритет над оригинальными и патчами с меньшим номером. Достаточно собрать изменённые и новые файлы в отдельную папку и запаковать их. Для этого создал ещё один скрипт на Python, которые сравнивает файлы второй и третьей копии по наличию, дате изменения, размеру и содержимому, складывает необходимые в папку, запаковывает в патч, затем в архив вместе с readme и файлом настроек. Дальше проверял мод на оригинальной базовой версии и выкладывал в общий доступ. Не стоит использовать макрос image для показа изображений модификации:Это ломает совместимость сохранений с оригинальной версией: будет ошибка из-за отсутствующего изображения при загрузке сохранений в базовой версии. Количество символов в строке ввода окна отладки ограничено шириной окна отладки, то есть если влезает 40 символов, то команду в 60 символов не получится ввести. Но размер окна отладки легко меняется, поэтому особых проблем ограничение не доставляет. Разработка мода заняла около полутора недель по 3–4 часа в день с последующими небольшими улучшениями и исправлениями. Суммарный размер новых скриптов игры составил 71 килобайт, размер изменений в существующих скриптах подсчитать затруднительно, и 73 новых изображения для интерфейса.