4.1. Удаление плиток
Сначала удалим все плитки из собранных комбинаций:
export class Game {
// ...
swap(selectedTile, tile) {
// ...
const matches = this.combinationManager.getMatches();
if (matches.length) {
this.processMatches(matches);
}
});
}
processMatches(matches) {
this.removeMatches(matches);
}
removeMatches(matches) {
matches.forEach(match => {
match.forEach(tile => {
tile.remove();
});
});
}
}
Реализуем метод remove в классе Tile:
remove() {
if (!this.sprite) {
return;
}
this.sprite.destroy();
this.sprite = null;
if (this.field) {
this.field.tile = null;
this.field = null;
}
}
Если у объекта больше нет спрайта, он уже удалён. Иначе удаляем спрайт и ссылку на клетку. У самой клетки также удаляем ссылку на текущую плитку.
4.2. Падение оставшихся плиток
После срабатывания комбинации и удаления её плиток оставшиеся плитки нужно опустить на поле. Добавьте вызов processFallDown в processMatches:
processMatches(matches) {
this.removeMatches(matches);
this.processFallDown();
}
Начиная с нижней строки поля, проверяем каждую клетку на наличие плитки. Если клетка пуста, сдвигаем вниз по столбцу все плитки над ней:
processFallDown() {
return new Promise(resolve => {
let completed = 0;
let started = 0;
// check all fields of the board, starting from the bottom row
for (let row = this.board.rows - 1; row >= 0; row--) {
for (let col = this.board.cols - 1; col >= 0; col--) {
const field = this.board.getField(row, col);
// if there is no tile in the field
if (!field.tile) {
++started;
// shift all tiles that are in the same column in all rows above
this.fallDownTo(field).then(() => {
++completed;
if (completed >= started) {
resolve();
}
});
}
}
}
});
}
Перемещение плиток вниз по столбцу реализуем в fallDownTo:
fallDownTo(emptyField) {
// checking all board fields in the found empty field column, but in all higher rows
for (let row = emptyField.row - 1; row >= 0; row--) {
let fallingField = this.board.getField(row, emptyField.col);
// find the first field with a tile
if (fallingField.tile) {
// the first found tile will be placed in the current empty field
const fallingTile = fallingField.tile;
fallingTile.field = emptyField;
emptyField.tile = fallingTile;
fallingField.tile = null;
// run the tile move method and stop searching a tile for that empty field
return fallingTile.fallDownTo(emptyField.position);
}
}
return Promise.resolve();
}
Добавим метод fallDownTo в класс Tile:
fallDownTo(position, delay) {
return this.moveTo(position, 0.5, delay, "bounce.out");
}
4.3. Добавление и падение новых плиток
После завершения падения оставшихся плиток нужно создать новые над полем, чтобы они упали в образовавшиеся пустые клетки:
processMatches(matches) {
this.removeMatches(matches);
this.processFallDown()
.then(() => this.addTiles())
}
Для создания и падения новых плиток получим все клетки поля, оставшиеся без плиток. Для каждой пустой клетки создадим плитку, расположим её выше первой строки поля и запустим анимацию перемещения в нужную пустую клетку:
addTiles() {
return new Promise(resolve => {
// get all fields that don't have tiles
const fields = this.board.fields.filter(field => field.tile === null);
let total = fields.length;
let completed = 0;
// for each empty field
fields.forEach(field => {
// create a new tile
const tile = this.board.createTile(field);
// put it above the board
tile.sprite.y = -500;
const delay = Math.random() * 2 / 10 + 0.3 / (field.row + 1);
// start the movement of the tile in the given empty field with the given delay
tile.fallDownTo(field.position, delay).then(() => {
++completed;
if (completed >= total) {
resolve();
}
});
});
});
}
4.4. Проверка комбинаций после падения
После завершения падения новых плиток на поле могут снова появиться комбинации.
Новые комбинации тоже нужно обработать: собрать их, опустить плитки и создать новые. Всё это уже реализовано в processMatches. Будем вызывать его рекурсивно, пока после очередного падения новых плиток на поле не останется комбинаций:
processMatches(matches) {
this.removeMatches(matches);
this.processFallDown()
.then(() => this.addTiles())
.then(() => this.onFallDownOver());
}
onFallDownOver() {
const matches = this.combinationManager.getMatches();
if (matches.length) {
this.processMatches(matches)
} else {
this.disabled = false;
}
}
5. Сбор комбинаций при запуске
Комбинации могут появиться не только после обмена двух плиток, но и при начальном заполнении поля. Такие комбинации нужно автоматически обработать без анимации падения и заменить другими плитками до показа стартового поля игроку.
Вся необходимая функциональность для обработки стартовых комбинаций уже есть:
:one: Найти все комбинации на поле через combinationManager.
:two: Удалить все найденные совпадения
:three: Создать новые плитки в пустых клетках
:four: Если после добавления новых плиток появились комбинации, повторить. Иначе начать игру.
// ...
export class Game {
constructor() {
// ...
this.removeStartMatches();
}
removeStartMatches() {
let matches = this.combinationManager.getMatches(); // find combinations to collect
while(matches.length) { // as long as there are combinations
this.removeMatches(matches); // remove tiles in combinations
const fields = this.board.fields.filter(field => field.tile === null); // find empty fields
fields.forEach(field => { // in each empty field
this.board.createTile(field); // create a new random tile
});
matches = this.combinationManager.getMatches(); // looking for combinations again after adding new tiles
}
}
}
6. Обратный обмен
Если после обмена на поле не появилась комбинация, плитки нужно вернуть обратно. Метод swap уже реализует перемещение плиток. Изменим его так, чтобы выполнять обратный ход, если после основного не найдены комбинации. Добавьте флаг reverse третьим параметром swap:
// ...
export class Game {
// ...
swap(selectedTile, tile, reverse) {
this.disabled = true;
selectedTile.sprite.zIndex = 2;
selectedTile.moveTo(tile.field.position, 0.2);
this.clearSelection();
tile.moveTo(selectedTile.field.position, 0.2).then(() => {
this.board.swap(selectedTile, tile);
// after the swap, check if it was the main swap or reverse
if (!reverse) {
// if this is the main swap, then we are looking for combinations
const matches = this.combinationManager.getMatches();
if (matches.length) {
// if there are combinations, then process them
this.processMatches(matches);
} else {
// if there are no combinations after the main swap, then perform a reverse swap by running the same method, but with the reverse parameter
this.swap(tile, selectedTile, true);
}
} else {
// in this condition, by the reverse flag, we understand that the swap was reversed, so there is no need to look for combinations.
// all you need to do is unlock the board, because here the movement is already completed and there are no other animations
this.disabled = false;
}
});
}
}