Доброго дня, читатели. Сегодня продолжим совершенствовать нашу игрушку и реализуем возможность атаковать карты противника, а так же некий профит в использовании ShadowDOM для админки.
В реализации атаки нам неожиданно поможет наш же способ реализации очереди из прошлой статьи (в противовес сообщению игроков по WebSockets).

Оговорюсь сразу, что развивать будем версию на MatreshkaJS, а не на Angular.

image

Что имеем на данный момент


На настоящее время у нас реализована возможность вызывать противника на бой и уже в игре выкладывать карты на арену (стол). Но какая же это игра, если нельзя взаимодействовать с противником?

Набросаем алгоритм:
  • Выбираем карту, которой собираемся сделать ход, кликая на нее.
  • При клике на другую карту прочие карты unвыделяются.
  • При клике на карту соперника выделенная карта (если такая есть) выполняет событие атаки (card.attack(opponentCard))
  • Показываем эти действия сопернику.

И да, существа не могут атаковать в тот ход, в который их разыграли.
А еще стоимость маны… Но давайте все по порядку.

Админка и ShadowDOM


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

Поскольку мы хотим сделать все вкусности нашего прототипа (HeartStone) как то: «боевой клич», «предсмертный хрип» и тд, — нам необходимо хранить в отдельной коллекции описания различных инструкций для этих событий. Напомню, что связь между коллекциями в DataBoom устанавливается как свойство объекта коллекции, в котором мы указываем массив объектов вида
{ "id": "obj_ID"}

где obj_ID — ID привязанного объекта.

В официальной документации пример установления таких связей выглядит следующим образом:
    var pers = [{ name: 'John' }, { name: 'Jane' }]
    pers[0].wife = pers[1];
    pers[1].husband = pers[0];

То есть мы присваиваем некоему свойству другой объект не заморачиваясь, как DataBoom их связывает. В базе это выглядит так, как описано выше:
{"wife":[{ "id": "a1303015-77ae-472d-8961-94ea2838b9b2"}]}

Используя это знание мы можем не переписывая в корне нашу админку сделать возможность добавления связи.
Что по сути нам нужно? Заменить стандартный input при редактировании записи (мы используем Webix виджет gridpanel) на select со списком IDшников нужной коллекции. И делать будем это с помощью ShadowDOM.

Что такое ShadowDOM?


ShadowDOM, как следует из названия, это теневая конструкция DOM.
Под словом «теневая» подразумевается то, что то, что в тени мы не видим, это как бы бэкэнд на фронтэнде. Пользователь взаимодействует с тем, что мы ему показываем. Но стоит помнить, что мы меняем только визуальное отображение. структура DOM при этом остается неизменной. Мы лишь заменяем отображение обычного input[type=text] на более сложный и удобный для нас элемент ввода. Более развернуто тут.

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

Инициализируем корень теневого дерева на нашем инпуте
var root = elem.createShadowRoot();

Это действие уже скроет содержимое элемента elem, но чтобы в нем что-то отобразить, надо наполнить его innerHTML.
Поскольку у нас там будет выпадающий список, получим данные для него из коллекции:
db.load("death_xpun").then(function(data){
	var selectOptions = '<select>';
	for(key in data){
		selectOptions += '<option value="' + data[key].id + '">' + data[key].name + '</option>'
	}
	selectOptions += '</select>';
});

// Заполним теневое древо
root.innerHTML = selectOptions;

Пока мы только сделали отображение, вместо текстового инпута у нас виден select, но данные для записи в базу берутся именно из инпута, который, к слову сказать, никуда не делся, а находится на том же месте в DOM дереве. Повесим на него обработчик событий:
root.querySelector('select').onchange = function() {
	var ourValue = {}
	ourValue.id = this.value;
	elem.value = JSON.stringify(ourValue); // Не забываем преобразовать в JSON, иначе получим "[Object object]"
}

Атакуем!


Выбираем карту, которой собираемся сделать ход


Тут алгоритм довольно простой, но я его опишу для понимания общей картины.

В модуле myUnits в модели карт на арене забиндим событие клика по карте, то есть по самой песочнице (MatreshkaJS, напоминаю). Активацию карты дополнительно свяжем с подсветкой, то есть будет добавлять/удалять класс active:
this.bindNode('class',':sandbox',{ // Свойство class объекта 
	on: 'click',   // Изменения происходят по событию click
	getValue: function(){
		return this.className;
	},
	setValue: function(v){ // Когда устанавливаешь: this.class = value
		this.className = v;
	},
	initialize: function(){
		$(this).on('click',function(){
			if ($(this).attr('enable') == 'disable') return;
			if ($(this).hasClass('active')) {
				$(this).removeClass('active');
				return false;
			}
			$('#myUnits .active').removeClass('active');
			$(this).addClass('active');
			readyToAttack = myUnits.indexOf($(this)); // Флаг готового к атаке юнита
		});
	}
});

Так как карта не должна иметь возможность атаковать в тот же ход, в конструктор добавим некий флаг enabled, который по умолчанию равен «disabled». Попозже подумаю, не лучше ли заменить ли это на true/false:
constructor: function(data){
	this.enable = 'disable';
}

Активируются карты на арене в начале хода. По этой логике необходимо в начале каждого хова вызывать некое событие enableMyUnits, которое мы оформим в виде директивы:
Actions/myTimerStart.js
define(['Directive', 'timer', 'myUnits', 'mana'],function(Directive, timer, myUnits, mana){

	var action = {
		run: function(){

			timer.start(); // Стартуем таймер
			myUnits.enableAll(); // Активируем всех юнитов
			Directive.run('getCard'); // Берем карту

			mana.setAllActive(); // Активируем все кристаллы маны
			mana.add(); // Добавляем кристалл маны

		}
	}

	return action;
}) 


Названия методов говорящие, тут и добавить нечего.

Итак, карта активна и готова к атаке, кликнем по карте соперника, которую хотим атаковать. Для этого, разумеется, допишем модуль opUnits (юниты оппонента), тк кликать будем по ним:
this.on('click::sandbox',function(){
	if($('#myUnits .active').length){ // Если есть активная карта
		var readyToAttack = myUnits.filter(this.filterActive);
		if (readyToAttack.length != 1) return false;
		var agressorIndex = myUnits.indexOf(readyToAttack[0]);
		Directive.run('attack', { // Запускаем механизм атаки
			agressor: myUnits[agressorIndex],
			victim: this
		});
	}
});

Механизм атаки я вынес в отдельную директиву по той простой причине, что событие атаки мы будем вызывать еще и при получении соответствующей инструкции с сервера.
Actions/attack.js
define(['Directive', 'stack', 'User'],function(Directive, stack, User){

	var action = {
		run: function(args){

			args.agressor.attacking(args.victim);

			var moreProps = {
				agressor: args.agressor.getIndex(),
				victim: args.victim.getIndex()
			}

			stack.push(User.opponent, 'opAttack', null, moreProps); // Не забываем показать противнику эти действия

		}
	}

	return action;
})


Так как DataBoom содержит коллекции а не таблицы, то мы можем сохранять туда совершенно различные данные, с различным набором параметров. Это одна из причин, по которой я выбрал этот сервис. В примере выше мы расширяем объект, который собираемся записывать в базу объектом moreProps, который может содержать какие угодно параметры. Но стоит помнить что некоторые параметры перезаписывать нельзя. Попозже сделаю проверку на попытку изменить такие «системные» параметры. Но это попооооозже.

Сам метод attacking() модуля myUnits в пилотной версии выглядит довольно топорно
Код
attacking: function(victim){
	var agressor = this;

	victim.sandbox.style.zIndex = 5;
	agressor.sandbox.style.zIndex = 10;

	var yPos = victim.sandbox.offsetTop - (agressor.sandbox.offsetTop + $('#opUnits')[0].offsetHeight) + 100;
	var xPos = victim.sandbox.offsetLeft - agressor.sandbox.offsetLeft;

	agressor.sandbox.style.top = yPos + 'px';
	agressor.sandbox.style.left = xPos + 'px';

	var at = setTimeout(function(){
		agressor.sandbox.style.top = 0;
		agressor.sandbox.style.left = 0;
		clearTimeout(at);
	},200); 

	agressor.enable = 'disable';
	victim.health = victim.health - agressor.attack;
	agressor.health = agressor.health - victim.attack;

}


Мы просто сдвигаем с помощью стилей карту так, чтобы она оказалась поверх карты-жертвы, а потом обратно на свое место. Естественно, для этого карта должна иметь position:relative.

Получая инструкцию об атаке из нашей очереди событий с сервера, запускаем ее подобным образом, просто вызывая метод attacking на объекте атакубщей карты, а аргументом передаем жертву:
define(['opUnits', 'myUnits'],function(opUnits, myUnits){

	var action = {
		run: function(args){

			var agressor = opUnits[args.agressor];
			var victim = myUnits[args.victim];
			agressor.attacking(victim);

		}
	}

	return action;
})

Не буду утомлять, на этом закончим данную статью.
Поиграйтесь в еще полный косяков пример.
Буду крайне благодарен, если подскажете, где почитать, как отключить ShadowDOM, чтобы можно было играться: вкл/выкл.

Ресурсы


Комментарии (4)


  1. Londeren
    18.09.2015 10:10

    Поделитесь, пожалуйста, ссылкой на предыдущий пост



  1. Grammidin
    18.09.2015 10:37

    Неплохо было бы ввести тестовый бой с компьютером. Иначе видишь только список игроков и вечное «Ожидание ответа от...»


    1. seokirill
      18.09.2015 11:07

      Да? Протестирую. Сервер настроить надо, на локальном все работает.