Создание игрового калькулятора на примере игры Aion

в 17:03, , рубрики: Веб-разработка

Введение

Любой кто увлекается программированием играя в игру всегда хочет прикрутить туда что-то своё или сделать что-то полезное. Кто-то делает модификации игры, кто-то трейнеры или читы, кто-то вики или фансайты. Не обошло это увлечение стороной и меня.
Играю в настоящий момент в ММО Aion: Tower of Eternety (далее для простоты “Аион”) от корейского разработчика NCSOFT. Именно про неё и пойдёт разговор.

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

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

Третье — различного рода “читерские” приспособления. К чести разработчиков чего-то действительно стоящего простой модификацией ресурсов игры получить нельзя, всё перепроверяется сервером и на клиенте циферки скорее для справки пользователю и синхронизации действий на экране с расчётами на сервере. Можно побаловаться увеличением размеров каких либо неприметных объектов в игре, например сундуки сокровищ или именных монстров. Но это чревато глюками и опять же нарушает ПС как и п.2.

Остаётся творить вне игры, но на её тему, но нас, конечно же, интересует то, что можно закодить, а не фанарты или статейки. Баз разной степени актуальности и паршивости по аиону в интернете предостаточно. А вот чего действительно не было до меня — так это калькулятора экипировки персонажа, по крайней мере, онлайн версии мне не попадалось. С вводом новых классов в игре стали также неактуальны существующие калькуляторы умений (стигм). Калькулятор экипировки достаточно сложный чтобы с него начать разбор, поэтому поговорим сначала про умения.

Определение требований

Итак в качестве проекта был выбран калькулятор. Первое с чем стоит определиться в данном случае — это инструменты разработки. Онлайн направленность игры сразу отсекает оффлайн версию приложения и подразумевает web, да и поднабрать немного опыта в web-программировании хотелось, благо времени было достаточно чтобы посидеть и разобраться, никакие начальники никуда не торопили. Отчасти из-за этого и началось всё с выбора инструментов.

Для творения нам понадобится:

  • Игровой клиент и утилиты для распаковки ресурсов
  • Любимый язык программирования для разбора и конвертации данных игры локально на своём компьютере. Лично я предпочитаю Perl.
  • Средний уровень владения HTML+CSS и JavaScript(jQuery). Серверное программирование не нужно ввиду простоты калькулятора.
  • Простенький графический редактор для обработки изображений интерфейса из игры, например Paint.NET.
  • Пакетный конвертер изображений из dds в png для преобразования иконок умений. Например ImageConverter+ (хватит и триала, он не ставит водяные знаки на изображения размера 40х40)
  • Перебороть лень. Хотя, пожалуй, это главный пункт, а не последний.

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

Умения в игре делятся на 2 категории: изучаемые из книг и получаемые установкой стигм. Стигмы в свою очередь делятся на 2 вида: обычные и улучшенные. Книжные умения одинаковы для всех персонажей одного класса, стигмы, как правило, сильные умения но имеют ограничение на их использование — фиксированное число слотов под них. Стигмы и экипировка полностью определяют роль персонажа в игре и группе, так называемый “билд” (от англ. build — сборка) персонажа. Простые стигмы могут устанавливаться в любой свободный слот, улучшенные только в слот для улучшенных и требуют наличие 1 или 2х других стигм, образуя таким образом дерево умений.

Стигмы в игре

Стигмы и кусочек окошка с деревом умений моего персонажа

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

Итого по умениям получаем:

  • У каждого класса есть по 2 дерева улучшенных стигм.
  • Количество слотов увеличивается с ростом уровня до максимума по 6 штук каждого вида.
  • Уровень стигм также может повышаться с ростом персонажа.
  • Каждая стигма имеет минимально необходимый уровень персонажа.
  • Некоторые стигмы различаются для разных рас либо названием и иконкой либо эффектом.
  • Каждая стигма имеет стоимость “вставки” в осколках стигм.
  • Улучшенные стигмы имеют стоимость покупки в Очках Бездны (AP, Abyss Points, разновидность игровой валюты связанной с PvP).

Также итоговый калькулятор должен иметь следующие функции:

  • Возможность набора стигм для каждого из игровых классов любого уровня от 20 (открывается первый слот) до максимального.
  • Выбор уровня стигм.
  • Возможность поделиться набором с друзьями или в обсуждении на форумах. Это очень важная составляющая калькуляторов
  • Многоязычность, мне было интересно получить фидбэк от зарубежных игроков.

База данных

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

Клиент игры написан на движке CryEngine 1, поэтому в интернете достаточно утилит для распаковки. Подробно этот момент рассматривать здесь не буду, там материала хватит на ещё одну статью. Достать из игры удалось даже модели и анимацию персонажей.
Все цифровые данные по предметам и умениям хранятся в простых XML с кодировкой UTF16-LE, графика в DDS (DirectDraw Surface — формат DirectX). Респект и уважуха разработчикам за то что не прячут данные за семью замками с шифрованием и пользуются человеческими форматами.
Почти все названия полей в XML соответствуют их назначению и много времени на то, чтобы понять что к чему тратить не придётся.

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

<client_item>
...
    <abyss_point>76350</abyss_point>
...
    <priest>55</priest>
...
    <require_skill1>PR_EternalServent</require_skill1>
    <require_skill2>PR_SufferMemory</require_skill2>
    <require_shard>275</require_shard>
    <stigma_type>Enhanced1</stigma_type>
    <gain_skill1>PR_CallLightning_G1</gain_skill1>
…
    <race_permitted>pc_light pc_dark</race_permitted>
</client_item>

Выше приведена улучшенная стигма 55 уровня для целителя за 76350 АП дающая умение CallLightning (Раскаты грома) первого уровня. Для установки требуется 275 осколков стигм, уже установленные стигмы EternalServent (Святая энергия) и SufferMemory (Ослабляющее клеймо) и доступно персонажам обеих рас.

Название умения состоит из префикса класса — “PR” (priest), devname умения — “CallLightning” и уровня умения — “G1” (grade 1). Для необходимых стигм уровень не важен, поэтому не указывается, на самом деле это вообще не название умения, а категория. Подробно про описание умений будет рассказано ближе к концу разработки. Этой информации достаточно для построения дерева и вообще есть всё необходимое кроме описания самих умений и их иконок, к ним мы вернёмся чуть позже.

С помощью небольшого скрипта получаем из XML файла первый вариант JSON базы для калькулятора примерно в таком виде:

var stigmas = {
  'priest': {
    ...
"pr_calllightning":{levels:[55,58,61,64],type:2,require:['pr_eternalservent','pr_suffermemory'],shards:[275,290,732,768],abyss:[76350,91800,187950,205450]},

...

Для каждого класса перечислены умения как свойства, у которых в свою очередь свойствами заданы уровни, соответствующее им число осколков и АП, а так же необходимые стигмы. Всё то что указано в XML выше. Хранение данных в JSON сильно упрощает работу с ними. Не стоит гнаться за минимизацией названия свойств объектов — сократит их сжатие и без нас, а вот работать будет не очень удобно. Если будет непреодолимое желание, то названия можно будет сократить до 1-2 букв, но лучше в конце разработки.

Файл со всеми умениями получился чуть меньше 20кб (к финальному варианту он разросся до 45 кб) и весьма заметно будет ужат deflate’ом, обновляются стигмы редко (каждый крупный патч), а следовательно нету никакого смысла грузить данных с помощью AJAX и можно хранить статично в отдельном js файле. Не стоит также лепить все статичные базы данных в один файл, это затрудняет их обновление.

В случае с предметами такой фокус, конечно же не прошёл бы. Их общее количество превышает 50 000 и организация поиска в такой свалке была бы крайне не эффективная. Без запроса в базу данных в таких случаях не обойтись. Всегда следует выбирать способ хранения соответствующий объёму данных и операциям, которые над ними будут производиться. Также всегда стоит выбирать формат который не устанавливает жёсткие рамки, очень часто случается так, что необходимо вносить изменения которые не были изначально запланированы и ещё лежат где-то в недрах разума разработчиков. Старайтесь оставлять места для безболезненной вставки “костылей”.

Наброски калькулятора

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

image

Черновой набросок интерфейса

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

Уже на самом раннем этапе стоит максимально избавляться от магических чисел и переменных. Я вынес всё в объект-контейнер Calc, там же будут храниться и функции:

Calc = {
    className:'priest',
    race:'pc_light',
    level:65,
    slots:[],
    norAllowed: 6,
    advAllowed: 6,
    norLevel:[20,20,30,40,50,55],
    advLevel:[45,45,50,52,55,58],
    SLOTS_NOR_MAX:6,
    SLOTS_ADV_MAX:6,
    MIN_CHAR_LEVEL: 20,
    MAX_CHAR_LEVEL: 65
}

В значения norAllowed и advAllowed хранится значение доступных обычных и улучшенных слотов соответственно, они рассчитываются от уровня персонажа level на основании массивов norLevel и advLevel содержащих уровни появления слотов.
Для хранения вставленных стигм, на мой взгляд, массив будет самой оптимальной структурой данных. Два массива содержать не рационально, это затрудняет итерацию по слотам и иногда можно сократить до 1 цикла вместо 2х. Например, функция проверяющая вставлена ли определённая стигма или нет, ей не требуется знать доступен ли 4й слот улучшенной стигмы или нет и просто пробегает по всему массиву, о его очистке должна заботится функция удаления.

Использование переменных вместо чисел позволяет легко варьировать общее количество слотов без изменения логики работы функций. Так в моём случае обынчные стигмы хранятся в массиве под индексами с 0 по SLOTS_NOR_MAX — 1, а улучшенные с SLOTS_NOR_MAX до SLOTS_NOR_MAX + SLOTS_ADV_MAX — 1. Таким образом если вдруг слотов станет по 8 вместо 6 нужно будет только добавить их в интерфейсе и задать значения SLOTS_NOR_MAX и SLOTS_ADV_MAX — всё остальное заработает с новыми значениями как-будто так всегда и было.

Доступные стигмы рисуются как блоки содержащими в идентификаторе название стигмы. Это позволяет использовать один обработчик для всех стигм и определять нужную стигмы по идентификатору объекта вызвавшего событие. Уровень умения автоматически выставляется максимальным для выбранного уровня персонажа. Маловероятно что кого-то интересуют более низкие уровни и не за чем заставлять пользователя совершать пару кликов для изменения на максимальный. По этой же причине уровень персонажа изначально тоже стоит выставлять максимально возможным. Хороший интерфейс должен быть спроектирован так чтобы наиболее частые действия были наиболее простыми и приводили к ожидаемому результату.
Большинство пользователей только просматривают готовые варианты составленные другими пользователями и поэтому лучше разгрузить интерфейс такими функциями как выбор уровня и удаление. Лучше их показывать при наведении на выставленную стигму.
Набросок

Вот уже и получилась версия которую можно попробовать самому. Но это только начало.

Деревья умений

При построении деревьев довольно быстро всплыла проблема: некоторые умения относятся только к одной из рас и улучшенная стигма требует установки любого из них. Некоторые из этих умений имели ещё и разные названия. Вариантов решения несколько: добавить свойство с категорией и сверять по нему, а не по названию или перечислять в require несколько вариантов. В итоге я остановился на смешанном решении: всем названиям специфических для расы умений дописал суффикс light или dark и в require добавил оба варианта умения:

"pr_calllightning":{levels:[55,58,61,64],type:2,require:[['pr_eternalservent_light','pr_eternalservent_dark'],'pr_suffermemory'],shards:[275,290,732,768],abyss:[76350,91800,187950,205450]},

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

Почти финальный вид интерфейса

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

Подсказки и описания

С описаниями умений дела обстоят сложнее чем кажется на первый взгляд. Каждое умение представляет из себя описание основных его свойств (стоимость, время применения и повторного использования, цели и тд) и от 1 до 4х эффектов, которые описывают его действие. И если с основным описаем всё понятно, то эффекты описываются набором безымянных полей reserved1-reserved24 смысл и набор которые меняется для каждого типа эффекта. На анализ этих значений времени ушло больше чем на написание всего калькулятора, но для всех умений игровых персонажей нужные значения сопоставить с их логикой в итоге удалось.

Строка с описанием представляет из себя шаблон для подстановки такого вот вида:

Удар магией земли, наносит цели [%e1.SpellATK_Instant.FixDamage] ед. урона. Радиус действия: [%First_Target_Valid_Distance] м. Наносит дополнительно [%e2.SpellATK.Damage] ед. урона с интервалом в [%e2.SpellATK.CheckTime] Время действия: [%e2.SpellATK.RemainTime] [%e4.StatDown.StatName] -[%e4.StatDown.Value]. Если цель умрет во время действия эффекта, она возродится у своего кибелиска или ники.

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

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

"pr_painlinks":{'1':{'e1.spellatk_instant.fixdamage':'532','first_target_valid_distance':'25','e2.spellatk.checktime':'12','cost':'317 MP','cooltime':'300','e4.statdown.statname':'MagicalResist','e2.spellatk.remaintime':'120','e4.statdown.value':'400','casttime':'2','e2.spellatk.damage':'532'}, '2':{'e1.spellatk_instant.fixdamage':'557','cost':'333 MP','e2.spellatk.damage':'557'}, '3':{'e1.spellatk_instant.fixdamage':'590','cost':'355 MP','e2.spellatk.damage':'590'}, '4':{'e1.spellatk_instant.fixdamage':'642','cost':'386 MP','e2.spellatk.damage':'642'}, '5':{'e1.spellatk_instant.fixdamage':'690','cost':'413 MP','e2.spellatk.damage':'690'}, '6':{'e1.spellatk_instant.fixdamage':'738','cost':'442 MP','e2.spellatk.damage':'738'}, '7':{'e1.spellatk_instant.fixdamage':'799','cost':'508 MP','e2.spellatk.damage':'824'}},

Учитывая что большинство значений повторяются для всех уровней умений нет смысла указывать их каждый раз. Так для 1го уровня записываются все свойства, а далее только изменяющиеся. При заполнении шаблона двигаясь от нужного уровня вниз по всем предыдущим получается полная информация о свойствах. Настоятельно рекомендую приводить все идентификаторы к единому регистру если исходные данные не чувствительны к нему — избавляет от ненужных проверок и случайных ошибок.

Подставлять значения проще всего регулярным выражением с callback'ом:

desc.replace(/[%(.*?)]%?/g, function(){
... форматирование значений ...
});

Если свойство хранит какое-то время — оно преобразуется в в человекопонятную строку (например, вместо «90» пишется «1 мин. 30 сек.»), если название характеристики персонажа или какой-то элемент игровой механики (например «оглушение») — то надо подставить его название в соответствии с выбранным языком. Остальные числа остаются как есть.
В завершение из полученной строки надо удалить вхождения двух подряд процентов (процент может быть как в значении атрибута, так и после шаблона в строке описания, переводчики тоже люди и не могут таких вещей знать где надо ставить процент, а где нет) или точек (из-за сокращений «сек.» и тд), которые могли получиться при подстановке.

Время применения, повторного использования и стоимость умения не указаны в тексте описании. Их снизу надо дописать дополнительно.
image

Внешний вид подсказки

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

Позиционирование подсказки

Многоязычность

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

Самый простой вариант — отказаться от динамического изменения языка и делать его полной перезагрузкой страницы. Создаётся 2 отдельных js файла со строками на соответствующем языке и нужный подключается на страницу. Это происходит прозрачно для пользователя потому что при переключении сохраняется хэш в ссылке и при перезагрузке страницы состояние калькулятора точно такое же как и было до смены языка.

При первом заходе на страницу язык стоит определять по Accept-Language заголовку от браузера, далее сохранять в Cookies и пользоваться значением оттуда. В пути куки лучше ставить корень домена и сохранять тем самым настройки языка для всего сайта. Это удобно пользователю и избавит его от лишних манипуляций при неправильно определённом Accept-Language.

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

function getLang(category, name) {
	if (lang[category] && lang[category][name]) {
		var result = lang[category][name];
		for (var i = 1; i < arguments.length -1; ++i) {
			var re = new RegExp('%'+i, 'g');
			result = result.replace(re, arguments[i+1]);
		}
		return result;
	}
	return name;
}
...
lang = $.extend(lang, {
	base : {
...
		'reqlvl':'Доступно с %1 уровня',
...
getLang('base', 'reqlvl', 65)
// Доступно с 65 уровня

Для статического контента локализованные строки подставляются при загрузке страницы основываясь на id контейнера. Логично что текст должен указываться в разметке документа, пусть и таким способом.
Уже после того как я сделал всё в таком виде узнал, что есть в HTML 5 такая вещь как data атрибуты и в id не надо писать ничего. В правильном виде мой способ выглядел бы так:

<span class="iface-string" data-stringid="copyright"></span>

$(".iface-string").html(function() {
	return getLang('interface', $(this).data('stringid'));
});

Такой работы с языками вполне достаточно для маленького проекта, но требует немного кода на php (определения языка, подстановка нужного js).

Сокращать или не сокращать?

Итак основной функционал готов. Осталось только сохранить результат. Первое что необходимо для этого — удобный и короткий идентификатор для классов и умений. Так как игровые идентификаторы явно под это определение не попадают придётся делать свои. Что может быть проще и короче порядкового номера? Да пожалуй ничего. Единственно что надо так это следить чтобы не нарушилась нумерация при обновлении базы.
Количество стигм у нас может варьироваться а вот уровень, раса и класс есть всегда. Стало быть они должны идти в начале строки с результатом, а следом стигмы с указанием уровня. Например вот так:
0,65,0,7,0,18,6,28,2,1,0,12,0,15,0,6,6,13,4,19,6,14,6,32,5,29,3
Короткой такую запись назвать ну никак не получается. Значит надо сократить количество символов придумав алгоритм кодирования.
Если убрать разделители между числами то будет невозможно определить где кончается одно число и начинается новое. Казалось бы проблема решается увеличением основания системы счисления, но кто даст гарантию что через год порядковый номер уместится в это основание? Сейчас максимальное число стигм у класса составляет 39 что уже исключает вариант использования toString(). Нужен другой вариант.
Тогда я решил использовать другое решение. В качестве цифр использовать буквы, причём заглавная буква означает что следующая цифра относится к тому же числу. т.е. число заканчивается строчной буквой. То есть строка «baa» соответвует «1,0,0», а строка «Ba» — «26,0». Таким образом пропадает необходимость в разделителях между числами и почти все стигмы умещаются в 1 символ. Можно нумерацию делать не по порядку, а наиболее редким стигмам ставить больший идентификатор. С таким способом строка содержащая класс, уровень и 12 стигм получается:
.../stigma/#aCnahasgBccbamapaggnetgogBgfBdd
На мой взгляд достаточно короткая ссылка получается в итоге чтобы не делать реализацию сокращения. Любители твиттера пусть пользуются своими сервисами.
Ссылка пишется в location.hash при каждом изменении набора, тут важно не допустить зацикливания если из хэша по событию обновляется набор. Смотреть за хэшом весьма желательно, ведь если пользователь вставит в эту же вкладку в адресную строку ссылку на другой набор то перезагрузки страницы не произойдёт — адрес до хэша полностью совпадает. Соответственно в этом случае без обработчика события ссылка не загрузится, что явно не соответствует ожидаемому результату.
Также для более понятного восприятия ссылку можно продублировать в текстовом поле где-то в интерфейсе, не каждый обращает внимание на адресную строку. При любом клике на поле надо выделать текст целиком и блокировать ввод/редактирование. Либо можно воспользоваться Clipboard API, но не везде оно будет работать без костылей (привет IE). Лучший вариант, пожалуй проверять наличие Clipboard API и выводить кнопку Share или текстовое поле в противном случае, если религия, конечно, позволяет пользоваться черновиками стандартов. Лично мне не позволяет лень, да и с полем со ссылкой как-то очевиднее функционал выглядит.

Резюме

Всего за пару недель по паре часов в день понадобилось для создания законченной версии калькулятора. Структура кода и данных позволяет с лёгкостью добавлять новые классы и умения им, быстро обновлять описания и значения умений. Уже в день выхода обновления на тестовом сервере Кореи можно иметь представление о новых умениях.
Возможность сохранения ссылки на набор стигм помогает в составлении гайдов и публикации видеороликов или хвастаться своим персонажем в подписи на форуме.

В настоящий момент в среднем на калькулятор приходится 4000 посещений в день и 300 уникальных пользователей, при условии что никакой рекламы ресурса не производилось в виду её не надобности — это не коммерческая разработка.
image

Статистика посещений за последний месяц

Надеюсь советы из статьи помогут кому-то при разработке своих проектов или мотивируют на разработку чего-то полезного.

P.S.: Ссылку публиковать не хочу, т. к. хостинг маленький и помрёт. Если кому интересно можно спросить в личку или воспользоваться поисковиками.

P.P.S.: По этой теме также есть ещё калькулятор экипировки с которого всё и начиналось

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

image

и небольшой кусочек «с душком» на WebGL с игровыми модельками и анимацией, заброшенный на стадии «ох нифига! работает! Дальше скучно пилить»

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

image

Если будет время, возможно, этой публикацией всё не закончится.

Автор: aspirineilia

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js