- PVSM.RU - https://www.pvsm.ru -

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 1

TL;DR для тех, кому некогда читать™:


Итак, началось всё [13] с хорошо продуманного плана [14]… Подождите, это из другой вселенной. В нашей вселенной всё началось с 2020 года, когда смартфоны ещё продавались официально, но на работу ходить уже было нельзя. В какой-то момент я понял, что нужно переключить свою голову с логической оценки происходящего и проблемы четырёх стен на любую — какую угодно! — задачу трёх тел [15] (главное [16], чтобы [17] не [18] буквально [19]). Или, перефразируя отца ядерной физики, идея должна была быть достаточно безумной, чтобы за неё взяться [20]. Идея нашлась — и винить в этом следует популярнейшую статью 2018 года «Герои Меча и Магии» в браузере: долго, сложно и невыносимо интересно [21] — написать «клон» третьих «Героев». За месяц.

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 2

Дисклеймер: Я часто привожу написанное число строчек, затраченное число часов и даже личные мысли. Делаю это не ради хвастовства, а для более глубокого понимания происходящего. КМК, данная статья — очень редкий случай, когда, благодаря детальным записям с первого дня, можно проследить эволюцию крупного проекта от самого зарождения. Смотря лишь на конечный результат, никогда не поймёшь, что человек чувствовал и пережил ради достижения поставленной цели, а без этого можно сделать превратные выводы как о самом человеке, так и о собственных амбициозных прожектах.

(1) День первый

А жив ли мальчик? Расчехляем OSINT

NB: В этой статье я использую две параллельные группы заголовков: одна временна́я, другая смысловая. Чтобы облегчить восприятие, номера дней я указываю так, как будто бы работа шла без перерыва каждый день. Однако дни не нормализованы — в один день я мог провести за проектом 1 час, а в иной — 11 (мой личный рекорд).

Начал я со сбора информации о существующих проектах. Сходу нашёл две очень старые и поныне живущие русские пошаговые MMO (heroeswm [22] с заявленным онлайном в 5 000 игроков и heroesland [23] с онлайном поменьше), русскую же экономическую инкарнацию в виде mlgame [24], а также heroes-online.com от Ubisoft, аккурат в 2020 благополучно почившую в бозе [25]. Из открытых движков, помимо всем известного VCMI [26], был относительно новый и очень активный fheroes2 [27] (в отличие от первого, воссоздаёт не третьих «Героев», а вторых). Этим список живых проектов исчерпывался, если не считать многочисленных модов на основе оригинальных «Героев», кучи [28] мобильных [29] игр [30], мимикрирующих под оригинал, и появившегося в 2020 FreeHeroes [31] от Владимира Смирнова (mapron [32]).

Зато сайтов по тематике игры — великое множество:

Даже на прогрессивном Хабре астрологи регулярно объявляют месяц статей о «Героях» — в прошлый раз это было [60] в феврале [61] (что, впрочем, не удивительно [62]).

Поясню для тех, кто не в курсе нынешней кухни «Героев». Самым старым модом считается WoG (In the Wake of Gods) — в прошлом году была даже статья автора [63] на 20-летие проекта. В середине нулевых WoG перестал развиваться, но на его основе появился скриптовый движок ERA, встраивающийся внутрь процесса «Героев» и творящий всевозможные безобразия нестандартные вещи (с точки зрения оригинала).

На базе ERA делается много мелких модификаций, но более масштабные обычно используют свою платформу. В первую очередь это HotA (Horn of the Abyss), воспринимаемая многими как «современные Герои 3», в особенности по части PvP. (ИЧЗХ, и WoG, и HotA имеют русские корни.)

Совершенно невероятно, но, и об этом говорилось уже много [64] раз [65]: спустя 24 года, в экосистеме «Героев» каждый год [66] появляется [67] что-то [68] принципиально [69] новое [70].

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

— Очень приятно, Ниша

Ситуация вокруг «Героев» сложилась любопытная. В одном рейтинге игр для ПК [71] «Герои 3» стоят на 4-м месте, обгоняя Warcraft 3 и Mass Effect — но даже исповедуя здоровый скептицизм, нельзя не признать, что игра до сих пор популярна.

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 3

Все четыре официальных продолжения провалились [72]. Новых мало кто ждёт. Реинкарнации от сторонних разработчиков появляются регулярно, но славы оригинала никто не стяжал даже близко. Весь фандом сосредоточен исключительно на модах к оригинальной игре, что накладывает свои ограничения (только Windows, неадаптивный UI, жёсткие рамки оригинального геймплея, крайняя трудоёмкость разработки).

Получается, миру нужны современные оригинальные «Герои» с широкими возможностями для моддинга, а также с чем-то, чего нет ни у VCMI с его 15-летней историей и 3 057 звёздами на GitHub [73], ни у очень вкусного [74] fheroes2 с 1 971 звездой [27]. Почему народ не спешит портировать туда свои модификации? Что я могу предложить нового? И есть ли шанс в одиночку довести дело хотя бы до альфа-версии?

Ответ напрашивался сам собой…

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 4

Если вы хотите расширяемости, то это не про C++. Это даже не очень про Java (см. Xposed [75]). Но это очень даже про JavaScript, особенно если использовать Sqimitive [76]. Вместо API — вся кодовая база; вызывай что хочешь, перекрывай как знаешь. Кто-то спросит — разве можно так писать? Да, если трактористы — женщины! [77] ведь не забудьте: у нас уже есть альтернативы на C++, написанные по всем правилам. Значит, будем писать не по правилам!

Минутка самопиара. Я очень люблю универсальные технологии. Фреймворк Sqimitive [78] — на котором построено всё здание HeroWO и несколько моих проектов поменьше — основан на идее, что любой метод любого объекта есть событие, а на события можно подписываться (с заданным приоритетом), перекрывать имеющиеся обработчики, откладывать их вызов и даже пакетировать (batch). Это позволяет решать множество прикладных задач через единый механизм: например, наследование класса есть просто изменение списка слушателей в подклассе, а множественное наследование — серия таких изменений, и делать их можно после объявления класса (aka mix-ins). Размер всего фреймворка — порядка 1 500 строк, зато документация, описывающая возможности применения — порядка 200 страниц.

Интерпретируемый язык сам по себе сократит время разработки и объём кода, но ведь мы говорим о JavaScript в браузере — а современные браузеры специально предназначены для презентации (очень) сложного контента. HTML и CSS для не очень динамичной игры вроде «Героев» — это что-то уровня червоточины в пространстве: с их помощью я смогу перепрыгнуть через целые пласты игровых подсистем. А проект, может быть, даже сможет выжить.

Продолжая мысль, если расширяемость ставить во главу угла, то ядро движка должно быть гибким, иначе мы получим тех же «Героев» или VCMI, только в профиль. И чем более гибким, тем больше шанс, что в будущем «Герои» в форме HeroWO выйдут-таки из стазиса и станут современной игрой с хардкорным олдскульным нутром, а может даже вберут в себя существующие модификации, а то и целиком старые RTS вроде Disciples!

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 5

Картинка получалось захватывающая. Решив, что корованы того стоят, я взялся за дело.

(2) День второй

Игровые архивы и форматы данных

Как-то так получилось, что имея пример великолепной статьи lekzd [79] перед глазами, я изначально документировал весь процесс разработки, так что восстановить последовательность событий не составляет труда.

Бо́льшую часть второго дня я провёл за безуспешными попытками скомпилировать h3m-map-convertor [80] и его зависимость homm3tools [81]. «Герои» хранят игровые карты в файлах с расширением .h3m («Heroes 3 Map»), и, насколько мне известно, общедоступных парсеров этого формата существует ровно один — h3mlib от homm3tools (в 2020 ещё появился FreeHeroes [31] со своим парсером). Качество кода h3mlib удручает: ручной разбор данных на Си — занятие неблагодарное, но когда это Си с макросами, то призыв четырёх пони апокалипсиса становится вопросом времени. Впрочем, главная проблема была в том, что даже после танцев с бубном и успешной компиляции библиотека отказывалась читать официальные карты, причём ошибка возникала где-то внутрях Zlib. В процессе всего этого акта меня не покидало ощущение, что h3m-map-convertor, автором которого, собственно, и является lekzd [79], базировался на какой-то подпольной более новой версии homm3tools, которая не была доступна простым смертным вроде меня.

В конце концов, я оставил попытки собрать собственный конвертер .h3m в JSON, решив, что 11-ти уже сконвертированных карт, которые я смог вытащить с демо-сайта [82] lekzd [79], мне вполне хватит для начала работы. Забегая вперёд, скажу: мне хватило лишь одной, той самой — «Adventures of Jared Haret» (можете запустить её в HeroWO [83]), а на 105-й день я написал свой парсер [1], с рулеткой и мета-данными, сотнями проверок, компилятором и прочими шплюшками.

(4) День четвёртый

Поскольку я не собирался делать клон движка lekzd [79], то рассматривал его JSON-ы как промежуточный формат. В этот день я написал конвертер «lekzd-json» в формат HeroWO, где сделал самое простое преобразование: пересчёт координат объектов с указания правой нижней точки (как в оригинальных «Героях» и во многих других старых играх) на указание левой верхней точки.

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 6

В этот же день я изучил файлы, задающие параметры различных игровых механик. Вкратце, «Герои» хранят данные в архивах с расширением .lod (а также идентичных .snd и .vid). Таких архивов имеется четыре (открываются через MMArchive [84]):

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 7

  • H3bitmap.lod — статические изображения (например, портреты героев) и текстовые файлы с константами. Текстовики можно открыть либо в Excel, либо в TxtEdit [85]. Рисунки — 8-битные PCX (это такой BMP эпохи DOS). Да, в «Героях» используется всего 256 цветов! Сразу и не скажешь, правда?

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 8

  • H3sprite.lod — анимированные изображения с расширением .def. По сути, 8-битная графика с разными способами сжатия. DefPreview [86] умеет их показывать и экспортировать в BMP, а DefTool [87] умеет их собирать.

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 9

  • Heroes3.snd — игровые звуки (музыка находится в стандартных MP3-шках в самой папке игры); стандартный WAV, но в модуляции DVI-ADPCM — браузеры и многий софт (включая oggenc) её не понимают. Получить «обычный» WAV можно с помощью моей утилитки adpcm2pcm [88] или Audacity [89] или SoX [90].
  • VIDEO.VID — видеоролики в ныне канувшем в лету (купленном Epic [91]), а на рубеже веков чрезвычайно популярном формате Bink Video. Официальные RAD Game Tools [92] до сих пор запускаются на Windows 10 и позволяют экспортировать кадры и звук.

В репозитории HeroWO есть папка databank [93] с текстовыми файлами — в них я детально описываю данные «Героев», в том числе содержимое архивов и типы используемых файлов [94] и назначение графических [95] и звуковых [96] файлов. В Сети до сих пор нет единого места с такой информацией, поэтому если вы можете что-то дополнить или исправить — пожалуйста, присылайте PR! Обещаю, никто не уйдёт обиженным.

Пользуясь случаем, хочу сказать спасибо нашему соотечественнику Сергею Роженко aka grayface [97], создавшему в нулевые годы десятки утилит для работы с данными «Героев», исходники которых он выложил на GitHub [98]. Сергей, да пребудет с тобой навечно Сила Delphi!

Разобравшись с форматами в начальном приближении, под конец дня я написал самый первый код для клиентской части: 230 строк (под спойлером), отрисовывающих карту «Приключений Жареда Харета» в базовой версии (как на КДПВ [99]). В последующие годы я до того насмотрелся на эту карту, что мог бы по памяти нарисовать стартовый экран, где героя в проходе зажали Троглодиты… но мы отвлеклись.

Скрытый текст

<!DOCTYPE html>
<html>
  <head>
    <title>HeroWO</title>
  </head>
  <body>
    <div id="herowo"></div>

    <script src="nodash.min.js"></script>
    <script src="sqimitive.min.js"></script>

    <script>
      var ObjectStore = Sqimitive.Base.extend({
        _schema: {},
        _schemaLength: 0,
        _layers: [],
        _layerLength: 0,
        _maxLayer: 0,
        _strideX: 0,
        _strideY: 0,
        _strideXY: 0,
        _maxZ: 0,

        events: {
          // opt:
          //> schema    {prop1: 0, prop2: 1, prop3: 1}
          //> layers    [ [o1p1, o1p2, o2p1, o2p2, ...], [o3p1, o3p2, null, null] ]
          //> strideX   set to 1 if not using that coord (1D array)
          //> strideY   set to 1 if not using that coord (2D array)
          init: function (opt) {
            // Passed arrays are not cloned for performance. Clone them before
            // passing if planning to change.
            this._schema = opt.schema
            this._schemaLength = _.max(_.filter(this._schema, function (v, k) {
              return k.indexOf('.') == -1
            })) + 1
            this._layers = opt.layers
            this._layerLength = this._layers[0].length
            this._maxLayer = this._layers.length - 1
            this._strideX = opt.strideX
            this._strideY = opt.strideY
            this._strideXY = opt.strideX * opt.strideY
            this._maxZ = this._layerLength / this._strideXY / this._schemaLength - 1
            if (Math.floor(this._maxZ) !== this._maxZ) {
              throw new Error('Stride parameters do not match the number of members.')
            }
          },
        },

        // prop - either resolved to integer or name of the outermost prop
        // (not 'foo.bar'). It's used in other methods; numeric argument works
        // faster so pre-resolve property index when doing heavy calculations.
        // Doesn't check if prop exists in the schema.
        // There's no "propertyByIndex()" because multiple properties may live
        // on one index ("union").
        propertyIndex: function (prop) {
          return typeof prop == 'number' ? prop : this._schema[prop]
        },

        // Unlike advance(), doesn't check if x/y/z are within the boundaries.
        toContiguous: function (x, y, z, prop) {
          return (z * this._strideXY + y * this._strideX + x)
                  * this._schemaLength + this.propertyIndex(prop)
        },

        fromContiguous: function (n) {
          var prop = n % this._schemaLength
          n = (n - prop) / this._schemaLength
          var x = n % this._strideY
          n = (n - x) / this._strideY
          var y = n % this._strideX
          n = (n - y) / this._strideX
          return {z: n, y: y, x: x, prop: prop}
        },

        // Wraps around. Stop when returns negative.
        //
        // for (var n = toContiguous(1, 2, 3, 'foo'); n >= 0; n = advance(n, -2))
        //   for (var fooValue, l = 0; null != (fooValue = atContiguous(n, l)); l++)
        //     alert(fooValue)
        advance: function (n, by) {
          n += by * this._schemaLength
          return n >= this._layerLength ? -1 : n
        },

        // If need to retrieve multiple properties of the same object, give
        // prop = 0 and use the passed n:
        // var prop = propertyIndex('foo')
        // findWithin(..., 0, function (..., l, n) { atContiguous(n + prop, l) })
        findWithin: function (sx, sy, sz, ex, ey, ez, prop, func, cx) {
          cx = cx || this
          for (var n = this.toContiguous(sx, sy, sz, prop);
               n >= 0 && (sx != ex || sy != ey || sz != ez);
               n = this.advance(n, +1)) {
            for (var value, l = 0; null != (value = this.atContiguous(n, l)); l++) {
              value = func.call(cx, value, sx, sy, sz, l, n)
              if (value) { return value }
            }
            if (this._strideX <= ++sx) {
              sx = 0
              if (this._strideY <= ++sy) {
                sy = 0
                sz++
              }
            }
          }
        },

        find: function (prop, func, cx) {
          return this.findWithin(0, 0, 0, Infinity, Infinity, Infinity, prop, func, cx)
        },

        // Convention: x/y/z - coords, l - layer (depth), prop - property index
        // or name, n - contiguous index of x/y/z/prop (but not l).
        atCoords: function (x, y, z, prop, l) {
          return this.atContiguous(this.toContiguous(x, y, z, prop), l)
        },

        // Returns == null when there are no more objects at l and below.
        // n must be within boundaries.
        atContiguous: function (n, l) {
          return l > this._maxLayer ? null : this._layers[l][n]
        },
      })

      var HMap = Sqimitive.Base.extend({
        objects: null,    // ObjectStore; do not set

        _opt: {
          state: 'created',   // created, loading, loaded
          url: '',
          format: 0,
          origin: '',
          width: 0,
          height: 0,
          levels: 0,
          difficulty: 0,
          title: 0,
          description: 0,
        },

        fetch: function (url) {
          if (this.get('state') != 'created') {
            throw new Error('Must fetch() only on a new Map.')
          }

          this.set('url', url)
          this.set('state', 'loading')
          var async = new Sqimitive.Async.Fetch({dataType: 'json', url: url + 'map.json'})

          return async
            .whenSuccess(function () {
              this.assignResp(async.response)
              this._fetchObjects()
            }, this)
        },

        _fetchObjects: function () {
          var async = new Sqimitive.Async
          async.nest('o', new Sqimitive.Async.Fetch({dataType: 'json', url: this.get('url') + 'objects.json'}))
          //async.nest('c', new Sqimitive.Async.Fetch({dataType: 'json', url: this.get('url') + 'classes.json'}))

          return async
            .whenSuccess(function () {
              var objects = async.nested('o').response
              this.objects = new ObjectStore(objects)
              this.set('state', 'loaded')
            }, this)
        },
      })

      var el = $('#herowo')
      $('body').css('background', 'cyan')
      var map = new HMap

      map.on('change_state', function (now) {
        if (now == 'loaded') {
          var oclass = this.objects.propertyIndex('class')
          var osubclass = this.objects.propertyIndex('subclass')
          var otexture = this.objects.propertyIndex('texture')
          var owidth = this.objects.propertyIndex('width')
          var oheight = this.objects.propertyIndex('height')
          var ox = this.objects.propertyIndex('x')
          var oy = this.objects.propertyIndex('y')
          var oz = this.objects.propertyIndex('z')
          var oabove = this.objects.propertyIndex('isAbove')
          var omirrorX = this.objects.propertyIndex('mirrorX')
          var omirrorY = this.objects.propertyIndex('mirrorY')
          window.o = this.objects
          this.objects.find(0, function (val, x, y, z, l, n) {
            z = this.atContiguous(n + oz, l)
            if (z == 1) {
              var tn = 0
              var c = this.atContiguous(n + oclass, l)
              var s = this.atContiguous(n + osubclass, l)
              if (c >= 256) {
                tn = s
              }
              x = this.atContiguous(n + ox, l)
              y = this.atContiguous(n + oy, l)
              $('<div>')
                .css({
                  position: 'absolute',
                  left: x * 32,
                  top: y * 32,
                  width: this.atContiguous(n + owidth, l) * 32,
                  height: this.atContiguous(n + oheight, l) * 32,
                  zIndex: this.atContiguous(n + oabove, l) * 10000 + ( (y + this.atContiguous(n + oheight, l)) * 10 + l + 1 ),
                  background: 'rgba(255,0,0,.0) url(../../def-png/' + this.atContiguous(n + otexture, l) + '/0-' + tn + '.png)',
                  //outline: '1px dashed rgba(255, 0, 0, .2)',
                  transform:
                    'scale(' +
                    (this.atContiguous(n + omirrorX, l) ? -1 : +1) +
                    ', ' +
                    (this.atContiguous(n + omirrorY, l) ? -1 : +1) +
                    ')',
                })
                .attr({
                  title: x + ':' + y + ' ' + this.atContiguous(n + oabove, l) + ' ' + this.atContiguous(n + otexture, l)
                })
                .appendTo(el)
            }
          })
        }
      })

      map.fetch('maps/converted/')
    </script>
  </body>
</html>

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 10

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 11

Части этого кода пережили все рефакторинги и их можно найти даже в текущей версии: ObjectStore [100], Map [101], DOM.Map [102].

Отрисовка вскрыла и первые проблемы, вторая из которых не решена до сих пор:

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

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 12

  • Z-координата (глубина/дальность от глаз пользователя) вычисляется неясным образом. Вначале я думал, что она зависит от порядка следования объектов внутри .h3m и их координат, но после многочасовых экспериментов с редактором карт я оставил попытки разобраться, как же именно она определяется, так что текущая формула [103] выглядит не очень. Кто знает, расскажите!

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 13

ObjectStore: универсальное хранилище данных HeroWO

В этот день было принято первое судьбоносное решение, которое легло в основу движка и, как говорят за океаном, драматично упростившее ряд вещей в дальнейшем. Решение это мне подсказала всё та же статья [21] (раздел «Хранение данных»). Я приведу оттуда слайд, отлично иллюстрирующий концепцию:

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 14

Я уже говорил, что люблю универсальные технологии? Так вот, в HeroWO вообще все данные хранятся в виде пары десятков огромных массивов внутри объектов класса док [104]ObjectStore. Например, objects.json — хранилище объектов на карте приключений:

О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 15

У каждого хранилища есть своя схема (таблица с именами полей для каждого индекса), размеры и массив слоёв, в каждом из которых «россыпью» хранятся данные отдельных объектов. На скриншоте выше показано начало объекта-героя «Жареда Харета»: первая выделенная строка — значение для свойства actionable (список клеток в границах объекта, с которыми можно взаимодействовать), вторая — actionableFromTop (флаг, допускающий взаимодействие с объектом с меньшим Y) и так далее.

В «Приключениях» всего 3 161 объект (включая тайлы земли); размер схемы (число элементов в массиве на один объект) — 45; итого, длина layers равняется 142 245 элементам. Второе и последнее большое хранилище — effects.json (о нём позже), там 3 046 объектов и 231 496 элементов. Если в Chrome сравнить потребление памяти с массивом из объектов (вида {actionable: "000010", actionableFromTop: true, ...}), то увидим выгоду в 32%: 1 205 004 байта против 822 840 у док [104]ObjectStore.

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

Инкапсуляция всех данных в структуре одного типа позволяет делать разные полезные штуки. Например, через месяц после начала работы над проектом я добавил поддержку вложенных хранилищ (массивы слоёв с собственной схемой) — на скриншоте это поле garrison сразу под выделением, где 7 есть creature, а 10 — count. Дальше, в конце схемы (стрелка влево) видно, что два поля имеют одинаковый индекс — это объединение (union) для опциональных полей, которые не могут использоваться одновременно и потому хранятся в общей ячейке (в нашем случае, message существует только у квестовых объектов, а available — только у городов, которые ими не являются). Объединения экономят место и создаются автоматически путём анализа пересечений использования свойств в схеме; например (слева — тип значения, справа — разъединяющая характеристика объектов):

$message:
    array  *str    - quest
            str    - quest
    array  *str    - treasure
            str    - treasure
    array  *str    - event
            str    - event
    array  *str    - monster
            str    - monster
$available:
            non-layered 1D sub-store - town
            non-layered 1D sub-store - dwelling
            non-layered 1D sub-store - hero

В своём движке lekzd [79] использовал такую систему вынужденно ради скорости; я же с самого начала положил её в основу проекта, полагая, что она радикально облегчит сериализацию игрового мира. Для примера, конвертер карт оригинальных «Героев» (h3m2json.php [1]) занимает 4 475 строчек, плюс 602 строки с комментариями. В то же время, сохранение и загрузка карты в формате HeroWO — это просто череда вызовов JSON.stringify()/JSON.parse() без какой-либо подготовки данных. Подкупало и то, что наличие единой точки доступа к данным (в лице док [104]ObjectStore) должно было сильно упростить синхронизацию клиентов в многопользовательской игре — и действительно, сейчас там порядка 650 строк (сервер [105], клиент [106]), что в разы меньше, чем один только парсер карт в homm3tools.

Оглядываясь назад, я вижу, что это решение было одним из краеугольных камней, благодаря которому получилось довести дело до выпуска. Я постоянно находил новые выгоды от него — например, хранение данных в виде плоского массива числовых значений потенциально позволяет использовать его напрямую в WebAssemly, что открывает дорогу для оптимизации другого фундаментального механизма HeroWO: калькуляторов игровых эффектов, о которых мы поговорим значительно позже.

Прошло всего 4 дня, а карта уже, считай, готова. График, вроде, выдерживаем, ещё недельку — и ка-ак зарелизим! Казалось бы, что могло пойти не так… О, «Герои»? Дайте две! Как я писал очередной браузерный клон легендарной стратегии, в который уже почти* можно играть - 16


На днях выйдет вторая часть. Если вам понравилось — пожалуйста, подпишитесь на блог компании [107], где будут публиковаться все остальные части! Ну, и жду бурления конструктивной полемики в комментариях!

herowo.game [8]Форум [10]Discord [11]YouTube [108]GitHub [109]

Автор: Павел

Источник [110]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/384175

Ссылки в тексте:

[1] h3m2json.php: https://github.com/HeroWO-js/h3m2json

[2] Мультиплеер: https://youtu.be/4FPShDK39uo

[3] Любое разрешение экрана и платформа: https://youtu.be/p8KIeIfkwgU

[4] Неограниченное число участников и размер поля в битве: https://youtu.be/Sv41KuMesQE

[5] вдохновлялся: https://images.gog-statics.com/5af56094055dcfa03f7cc2e69cd78ca85018ed8f7fcdc959e60e3f284d72a69c.jpg

[6] Произвольное число уровней на карте приключений: https://youtu.be/FBiKVuTR9lY

[7] Все исходники под Unlicense на GitHub: https://github.com/HeroWO-js/Workbench

[8] Заходите на herowo.game для игры: https://herowo.game

[9] вот Tutorial: https://herowo.game/#VHV0b3JpYWwv

[10] форум: https://herowo.io/forum

[11] Discord: https://discord.gg/UcGCNhJEUx

[12] Как ещё помочь?: https://forum.herowo.net/t/topic/15

[13] началось всё: https://youtu.be/KLZbbHbeSos?t=29

[14] с хорошо продуманного плана: https://vk.com/video198565620_164224436

[15] задачу трёх тел: https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%B4%D0%B0%D1%87%D0%B0_%D1%82%D1%80%D1%91%D1%85_%D1%82%D0%B5%D0%BB_(%D1%80%D0%BE%D0%BC%D0%B0%D0%BD)

[16] главное: https://habr.com/ru/post/716564

[17] чтобы: https://habr.com/ru/post/725432

[18] не: https://habr.com/ru/post/722716

[19] буквально: https://habr.com/ru/post/720634

[20] идея должна была быть достаточно безумной, чтобы за неё взяться: https://en.wikiquote.org/wiki/Niels_Bohr

[21] «Герои Меча и Магии» в браузере: долго, сложно и невыносимо интересно: https://habr.com/ru/post/354014

[22] heroeswm: https://www.heroeswm.ru

[23] heroesland: https://www.heroesland.com

[24] mlgame: http://mlgame.ru

[25] благополучно почившую в бозе: https://web.archive.org/web/20210303172956/https://clb.heroes-online.com/en-US/news/end-mmho-1

[26] VCMI: https://vcmi.eu

[27] fheroes2: https://github.com/ihhub/fheroes2

[28] кучи: https://play.google.com/store/apps/details?id=com.stgl.global

[29] мобильных: https://play.google.com/store/apps/details?id=com.homm.cgames.app

[30] игр: https://play.google.com/store/apps/details?id=heroes.battle.strategy.card.game

[31] FreeHeroes: https://github.com/mapron/FreeHeroes

[32] mapron: https://habr.com/ru/users/mapron/

[33] heroesworld.ru: https://heroesworld.ru

[34] heroescommunity.com: http://heroescommunity.com

[35] celestialheavens.com: https://www.celestialheavens.com

[36] df2.ru: https://forum.df2.ru/index.php?showforum=4

[37] h3.gg: https://www.h3.gg

[38] Форум на GOG: https://www.gog.com/forum/heroes_of_might_and_magic_series

[39] heroes35.net: http://wforum.heroes35.net

[40] Discord-сервер: https://discord.gg/horn-of-the-abyss-519540110195425280

[41] HotA: https://h3hota.com

[42] сервер мододелов WoG и ERA: https://discord.gg/W3r6Xun

[43] «Физмиг» aka «Физика мира Героев»: https://vk.com/fizmig

[44] схожий проект handbookhmm: https://handbookhmm.ru

[45] maps4heroes.com: https://maps4heroes.com

[46] hommdb.com: http://www.hommdb.com

[47] библиотека идей для новых городов: https://heroes3towns.com

[48] и прочая: http://mmgames.ru

[49] и прочая: https://heroes3wog.net

[50] и прочая: https://heroesportal.net

[51] и прочая: http://www.might-and-magic.ru

[52] и прочая: https://homm3sod.ru

[53] и прочая: http://h3wog.narod.ru

[54] и прочая: http://packa.ucoz.ru

[55] на польском: http://h3.heroes.net.pl

[56] ещё на польском: https://www.acidcave.net

[57] а вот на чешском: https://worldofheroes.cz

[58] даже: http://heroworld.gamerhome.com

[59] на китайском: https://www.h3wog.com

[60] это было: https://habr.com/ru/post/715614

[61] в феврале: https://habr.com/ru/post/711288

[62] не удивительно: https://habr.com/ru/post/716866

[63] статья автора: https://habr.com/ru/post/665654

[64] много: https://www.mirf.ru/games/videogames/20-let-geroyam-mecha-i-magii-3-chem-zhivyot-kultovaya-igra-segodnya/

[65] раз: https://habr.com/ru/post/442158

[66] каждый год: http://creaturequest.com

[67] появляется: https://www.youtube.com/watch?v=ePM7Jkjt6wM

[68] что-то: https://www.songsofconquest.com

[69] принципиально: https://homm3boardgame.com

[70] новое: https://www.fanstratics.com

[71] одном рейтинге игр для ПК: http://www.world-art.ru/games/rating_top.php

[72] провалились: https://club.dns-shop.ru/blog/t-64-videoigryi/35809-istoriya-serii-heroes-of-might-and-magic-ot-rastsveta-do-zakata/

[73] 3 057 звёздами на GitHub: https://github.com/vcmi/vcmi

[74] очень вкусного: https://ihhub.github.io/fheroes2/INSTALL.html

[75] Xposed: https://forum.xda-developers.com/t/xposed-general-info-versions-changelog.2714053/

[76] Sqimitive: https://habr.com/ru/post/239149

[77] если трактористы — женщины!: https://%D0%B1%D0%B0%D1%88%D0%BE%D1%80%D0%B3.%D1%80%D1%84/quote/396626

[78] Sqimitive: https://squizzle.me/js/sqimitive/

[79] lekzd: https://habr.com/ru/users/lekzd/

[80] h3m-map-convertor: https://github.com/lekzd/h3m-map-convertor

[81] homm3tools: https://github.com/potmdehex/homm3tools

[82] демо-сайта: http://homm.lekzd.ru

[83] можете запустить её в HeroWO: https://herowo.game/#QWR2ZW50dXJlcyUyMG9mJTIwSmFyZWQlMjBIYXJldC8

[84] MMArchive: https://github.com/GrayFace/Misc/releases?page=3#hd-f9501864

[85] TxtEdit: https://github.com/GrayFace/Misc/releases?page=3#hd-e1bd7f07

[86] DefPreview: https://github.com/GrayFace/Misc/releases?page=6#hd-02c0dbe4

[87] DefTool: https://github.com/GrayFace/Misc/releases?page=5#hd-7078cf94

[88] adpcm2pcm: https://github.com/HeroWO-js/Workbench/tree/master/tools/adpcm2pcm

[89] Audacity: https://www.audacityteam.org

[90] SoX: https://sox.sourceforge.net

[91] купленном Epic: https://www.epicgames.com/site/en-US/news/epic-acquires-rad-game-tools

[92] RAD Game Tools: http://www.radgametools.com

[93] папка databank: https://github.com/HeroWO-js/Core/tree/master/databank

[94] содержимое архивов и типы используемых файлов: https://github.com/HeroWO-js/Core/blob/master/databank/Formats.txt

[95] графических: https://github.com/HeroWO-js/Core/blob/master/databank/Images.txt

[96] звуковых: https://github.com/HeroWO-js/Core/blob/master/databank/Sounds.txt

[97] Сергею Роженко aka grayface: http://grayface.chat.ru/rus/

[98] GitHub: https://github.com/GrayFace

[99] КДПВ: https://habrastorage.org/webt/2r/fv/5r/2rfv5rdpralxok9zyltejp_0ufa.png

[100] ObjectStore: https://github.com/HeroWO-js/Core/blob/62a8d16f499905b3191658661474405b29f0658b/client/ObjectStore.js#L297-L343

[101] Map: https://github.com/HeroWO-js/Core/blob/62a8d16f499905b3191658661474405b29f0658b/client/Map.js#L349-L357

[102] DOM.Map: https://github.com/HeroWO-js/Core/blob/62a8d16f499905b3191658661474405b29f0658b/client/DOM.Map.js#L533-L538

[103] текущая формула: https://github.com/HeroWO-js/Core/blob/62a8d16f499905b3191658661474405b29f0658b/databank/h3m2herowo.php#L1847-L1861

[104] док: https://github.com/HeroWO-js/Core/blob/62a8d16f499905b3191658661474405b29f0658b/client/ObjectStore.js

[105] сервер: https://github.com/HeroWO-js/Core/blob/62a8d16f499905b3191658661474405b29f0658b/client/RPC.js#L115-L561

[106] клиент: https://github.com/HeroWO-js/Core/blob/62a8d16f499905b3191658661474405b29f0658b/client/RPC.js#L1277-L1453

[107] блог компании: https://habr.com/ru/company/soletude/blog/

[108] YouTube: https://www.youtube.com/@PlayHeroWO

[109] GitHub: https://github.com/HeroWO-js

[110] Источник: https://habr.com/ru/companies/soletude/articles/719280/?utm_source=habrahabr&utm_medium=rss&utm_campaign=719280