Сегодня расскажу о том, как устроены классы Hero и Enemy.
class Hero {
constructor(name, imgPath, imgWidth, imgHeight, scaleMultiplier, baseAttack, health, deathSprites, takeHitSprites, armor = 1) {
this.name = name;
this.attack = baseAttack;
this.health = health;
this.imgPath = imgPath;
this.imgWidth = imgWidth * scaleMultiplier;
this.imgHeight = imgHeight * scaleMultiplier;
this.numberOfSprites = imgWidth / imgHeight;
this.animationTime = 800;
this.animationTick = imgHeight * scaleMultiplier;
this.animationInterval = 800 / (imgWidth / imgHeight);
this.deathSprites = deathSprites;
this.takeHitSprites = takeHitSprites;
this.armor = armor;
}
}
Сразу бросается в глаза, что конструктор у класса слишком длинный, и это действительно довольно неудобно, но я пока не придумал, как грамотно уменьшить количество параметров. Скорее всего эта строчка в ближайшее время будет становиться всё длиннее, несмотря на возмущения линтера. Описывать каждый параметр смысла не имеет, лучше я покажу их применение в методах класса.
drawHero() {
heroHTML.style.width = `${this.imgHeight}px`;
heroHTML.style.height = `${this.imgHeight}px`;
heroHpHTML.innerHTML = `${this.health}`;
heroArmorHTML.innerHTML = `${this.armor}`;
}
drawHero нужен для задания размеров контейнера персонажа и отображения здоровья и брони.
startTurn() {
heroStatsHTML.style.pointerEvents = 'all';
}
endTurn() {
heroStatsHTML.style.pointerEvents = 'none';
}
Эти методы нужны для того, чтобы блокировать интерфейс игрока во время хода противника, и, соответственно, "оживлять" интерфейс после.
animateIdle() {
heroHTML.style.backgroundImage = `url(${this.imgPath}/Idle.png)`;
let position = 0;
this.idle = setInterval(() => {
heroHTML.style.backgroundPosition = `-${position}px`;
if (position < this.imgWidth - this.animationTick) {
position += this.animationTick;
} else {
position = 0;
}
}, this.animationInterval);
}
stopAnimationIdle() {
clearInterval(this.idle);
}
animateIdle создаёт анимацию персонажа во время покоя. Изначально у нас есть картинка с несколькими спрайтами, через определённый промежуток времени мы сдвигаем картинку так, чтобы в фокусе оказался следующий спрайт. Как только мы дошли до конца картинки, то возвращаемся к исходной позиции и повторяем всё вновь.
stopAnimationIdle прерывает анимацию покоя.
Анимации атаки, получения урона и смерти персонажа почти ничем не отличаются от анимации покоя. В них мы вызываем clearInterval после того, как проходимся по всем спрайтам.
animateRun() {
heroHTML.style.backgroundImage = `url(${this.imgPath}/Run.png)`;
let position = 0;
const translateTick = (100 / 2) / this.numberOfSprites;
let translateX = 0;
const run = setInterval(() => {
heroHTML.style.backgroundPosition = `-${position}px`;
heroHTML.style.transform = `translateX(${translateX}vw)`;
if (position < this.imgWidth - this.animationTick) {
position += this.animationTick;
translateX += translateTick;
} else {
position = 0;
clearInterval(run);
}
}, this.animationInterval);
}
animateRun помимо анимации бега, сдвигает контейнер персонажа, поэтому тут добавляется переменная translateTick, в которой есть такая математическая операция (100 / 2). Пример того, как не надо писать код. Мне пришлось изрядно напрячься, чтобы вспомнить, что 50 это 50vw, т.е. половина экрана - место докуда добегает персонаж. "Половину экрана" мы делим на количество спрайтов анимации бега, которое нам пришлось указать в конструкторе, и в итоге персонаж "пробегает" все спрайты к концу своего забега.
animateRunBack() {
heroHTML.style.backgroundImage = `url(${this.imgPath}/RunBack.png)`;
let position = 0;
const translateTick = (100 / 2) / this.numberOfSprites;
let translateX = (100 / 2) - translateTick;
const runBack = setInterval(() => {
heroHTML.style.backgroundPosition = `-${position}px`;
heroHTML.style.transform = `translateX(${translateX}vw)`;
if (position < this.imgWidth - this.animationTick) {
position += this.animationTick;
translateX -= translateTick;
} else {
position = 0;
clearInterval(runBack);
this.animateIdle();
}
}, this.animationInterval);
}
animateRunBack бег в обратную сторону. Тут лишь меняем начальные координаты контейнера и изменяем их в обратную сторону.
animateHitBar() {
heroHitBarHTML.style.animation = 'hit 2.5s';
setTimeout(() => {
heroHitBarHTML.style.animation = 'none';
}, 2500);
}
// css
@keyframes hit {
0% {
transform: translateY(0);
opacity: 1;
}
100% {
transform: translateY(-50px);
opacity: 0;
}
}
animateHitBar вызывает css анимацию нанесённых повреждений при получении урона. Здесь я использовал css анимацию, т.к. хотел чтобы контейнер с уроном исчезал постепенно, и другого способа придумать не смог.
Класс Enemy принципиально ничем не отличается от Hero, поэтому смысла рассматривать его отдельно нет.
Получилось очень поверхностно описать классы и всё что происходит в методах, но если у кого-то возникли вопросы, то вы всегда можете задать их в комментариях, и я с удовольствием вам отвечу.
Код проекта на GitHub.
А здесь можно поиграть в текущую версию.