Merge pull request 'Feature/advanced game mechanics' (#5) from feature/advanced-game-mechanics into main
Build and push / build (push) Successful in 40s
Build and push / build (push) Successful in 40s
Reviewed-on: #5
This commit is contained in:
+16
-1
@@ -2,14 +2,29 @@ export const CONTAINER_WIDTH = 800;
|
||||
export const CONTAINER_HEIGHT = 600;
|
||||
|
||||
export const PADDLE_WIDTH = 50;
|
||||
export const PADDLE_WIDE_WIDTH = 100;
|
||||
export const PADDLE_HEIGHT = 10;
|
||||
export const MIN_PADDLE_REFLECTION_ANGLE = 20;
|
||||
export const MAX_PADDLE_REFLECTION_ANGLE = 160;
|
||||
export const PADDLE_SECTOR_AMOUNT = 20;
|
||||
|
||||
export const BALL_RADIUS = 10;
|
||||
export const BALL_SPEED = 3;
|
||||
export const BALL_INITIAL_ANGLE = 180;
|
||||
export const BALL_SPEED_LEVEL_STEP = 2;
|
||||
export const BALL_SPEED_TIME_STEP = 0.002;
|
||||
export const BALL_INITIAL_ANGLE = 0;
|
||||
export const BALL_SPLIT_ANGLE = 20;
|
||||
|
||||
export const BRICK_WIDTH = 40;
|
||||
export const BRICK_HEIGHT = 10;
|
||||
|
||||
export const BRICK_ROW_AMOUNT = 5;
|
||||
export const BRICK_COLUMN_AMOUNT = 20;
|
||||
|
||||
export const PERK_WIDTH = 16;
|
||||
export const PERK_HEIGHT = 16;
|
||||
export const PERK_FALL_SPEED = 3;
|
||||
export const PERK_DROP_CHANCE = 0.3;
|
||||
export const PERK_BALL_SPEED_DECREASE = 2;
|
||||
export const PERK_TYPES = ['slow', 'wide', 'life', 'clone'];
|
||||
export const PERK_DURATION_SEC = 20;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Список уровней игры в форме массива
|
||||
* 0 - пустое пространство
|
||||
* 1 - обычный блок
|
||||
* 2 - блок с несколькими жизнями
|
||||
* 3 - неразрушимый блок
|
||||
*/
|
||||
export const LEVELS = [
|
||||
[
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
|
||||
],
|
||||
[
|
||||
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
|
||||
[1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2],
|
||||
[2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[0, 2, 1, 2, 0, 0, 1, 2, 1, 0, 0, 2, 1, 2, 1, 0, 1, 2, 1, 0],
|
||||
[0, 0, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, 0],
|
||||
],
|
||||
[
|
||||
[1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2],
|
||||
[2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1],
|
||||
[1, 2, 1, 2, 1, 2, 1, 2, 3, 3, 3, 3, 1, 2, 1, 2, 1, 2, 1, 2],
|
||||
[2, 1, 2, 1, 2, 1, 2, 3, 3, 3, 3, 3, 3, 1, 2, 1, 2, 1, 2, 1],
|
||||
[1, 2, 0, 2, 1, 2, 1, 0, 3, 3, 3, 3, 0, 2, 1, 2, 1, 0, 1, 2],
|
||||
[2, 0, 0, 0, 2, 1, 0, 0, 0, 1, 2, 0, 0, 0, 2, 1, 0, 0, 0, 1],
|
||||
],
|
||||
];
|
||||
@@ -1,3 +1,5 @@
|
||||
import { toRadians } from '../../lib/toRadians/toRadians';
|
||||
|
||||
/**
|
||||
* Класс сущности "мяч"
|
||||
* @class
|
||||
@@ -7,13 +9,23 @@ export class Ball {
|
||||
* @param {number} x координата положения мяча по оси X
|
||||
* @param {number} y координата положения мяча по оси Y
|
||||
* @param {number} radius положительное числовое значение радиуса мяча
|
||||
* @param {number} speed положительное числовое значение скорости мяча
|
||||
* @param {number} angle угол направления движения мяча в градусах
|
||||
*/
|
||||
constructor(x, y, radius) {
|
||||
constructor(x, y, radius, speed = 0, angle = 0) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.horizontalSpeed = 0;
|
||||
this.verticalSpeed = 0;
|
||||
this.radius = radius;
|
||||
this.speed = speed;
|
||||
this.angle = angle;
|
||||
this.verticalSpeed = speed * Math.cos(toRadians(angle));
|
||||
this.horizontalSpeed = -1 * speed * Math.sin(toRadians(angle));
|
||||
this.status = 'idle';
|
||||
|
||||
this.defaultX = x;
|
||||
this.defaultY = y;
|
||||
this.defaultVerticalSpeed = this.verticalSpeed;
|
||||
this.defaultHorizontalSpeed = this.horizontalSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,4 +36,42 @@ export class Ball {
|
||||
this.x += this.horizontalSpeed * deltaTime;
|
||||
this.y += this.verticalSpeed * deltaTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает значения к дефолтным
|
||||
*/
|
||||
reset(x = this.defaultX, y = this.defaultY) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.verticalSpeed = this.defaultVerticalSpeed;
|
||||
this.horizontalSpeed = this.defaultHorizontalSpeed;
|
||||
this.status = 'idle';
|
||||
}
|
||||
|
||||
/**
|
||||
* Запускает мяч с ракетки - переводит из ожидания в игру
|
||||
*/
|
||||
launch() {
|
||||
if (this.status === 'idle') {
|
||||
this.status = 'moving';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Увеличивает скорость мяча не меняя направление
|
||||
* @param {number} step положительное числовое значение прибавки к скорости
|
||||
*/
|
||||
increaseSpeed(step) {
|
||||
if (this.speed <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const magnifyingCoeficient = (this.speed + step) / this.speed;
|
||||
this.speed += step;
|
||||
|
||||
this.verticalSpeed *= magnifyingCoeficient;
|
||||
this.horizontalSpeed *= magnifyingCoeficient;
|
||||
this.defaultVerticalSpeed *= magnifyingCoeficient;
|
||||
this.defaultHorizontalSpeed *= magnifyingCoeficient;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { toRadians } from '../../../lib/toRadians/toRadians';
|
||||
import { Ball } from '../ball';
|
||||
|
||||
/**
|
||||
* Создает копию мяча в той же точке с направлением, повернутым на заданный угол
|
||||
* @param {Ball} ball исходный мяч
|
||||
* @param {number} angle угол поворота направления в градусах
|
||||
* @returns {Ball | null} новый мяч с повернутой скоростью
|
||||
*/
|
||||
export function clone(ball, angle) {
|
||||
try {
|
||||
if (!(ball instanceof Ball)) {
|
||||
throw new Error('Аргумент ball должен быть экземпляром класса мяча');
|
||||
}
|
||||
|
||||
if (typeof angle !== 'number') {
|
||||
throw new Error('Угол поворота должен являться числом');
|
||||
}
|
||||
|
||||
const clonedBall = new Ball(ball.x, ball.y, ball.radius, ball.speed);
|
||||
const angleInRadians = toRadians(angle);
|
||||
const cos = Math.cos(angleInRadians);
|
||||
const sin = Math.sin(angleInRadians);
|
||||
|
||||
clonedBall.status = ball.status;
|
||||
clonedBall.horizontalSpeed = ball.horizontalSpeed * cos - ball.verticalSpeed * sin;
|
||||
clonedBall.verticalSpeed = ball.horizontalSpeed * sin + ball.verticalSpeed * cos;
|
||||
clonedBall.defaultHorizontalSpeed = clonedBall.horizontalSpeed;
|
||||
clonedBall.defaultVerticalSpeed = clonedBall.verticalSpeed;
|
||||
|
||||
return clonedBall;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Ball } from '../ball';
|
||||
import { clone } from './clone';
|
||||
|
||||
describe('clone', () => {
|
||||
it('Возвращает корректное значение при неверных типах аргументов', () => {
|
||||
expect(clone(null, 20)).toBeNull();
|
||||
expect(clone(new Ball(0, 0, 10, 3), '20')).toBeNull();
|
||||
});
|
||||
|
||||
it('Клонирует мяч и задает корректное значение скоростей по осям X и Y', () => {
|
||||
const ball = new Ball(100, 200, 10, 90);
|
||||
ball.status = 'moving';
|
||||
ball.horizontalSpeed = 3;
|
||||
ball.verticalSpeed = 4;
|
||||
|
||||
const clonedBall = clone(ball, 90);
|
||||
|
||||
expect(clonedBall).toBeInstanceOf(Ball);
|
||||
expect(clonedBall).not.toEqual(ball);
|
||||
expect(clonedBall.x).toBe(100);
|
||||
expect(clonedBall.y).toBe(200);
|
||||
expect(clonedBall.status).toBe('moving');
|
||||
expect(clonedBall.horizontalSpeed).toBeCloseTo(-4);
|
||||
expect(clonedBall.verticalSpeed).toBeCloseTo(3);
|
||||
});
|
||||
});
|
||||
@@ -9,16 +9,28 @@ export class Brick {
|
||||
* @param {number} y координата положения кирпича по оси Y
|
||||
* @param {number} width положительное числовое значение ширины кирпича
|
||||
* @param {number} height положительное числовое значение высоты кирпича
|
||||
* @param {number} type тип блока. 1 - обычный, 2 - больше 1 жизни, 3 - неразрушаемый
|
||||
*/
|
||||
constructor(x, y, width, height) {
|
||||
constructor(x, y, width, height, type) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.type = type;
|
||||
this.alive = true;
|
||||
this.livesAmount = this.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* В зависимости от типа и оставшихся жизней убивает кирпич
|
||||
*/
|
||||
kill() {
|
||||
this.alive = false;
|
||||
if (this.type !== 3) {
|
||||
this.livesAmount -= 1;
|
||||
|
||||
if (this.livesAmount === 0) {
|
||||
this.alive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,35 @@
|
||||
import { Brick } from '../brick';
|
||||
|
||||
/**
|
||||
* Ложит кирпичи по заданым размерам
|
||||
* @param {number} columnAmount количество кирпичей по оси X, неотрицателное, целое число
|
||||
* @param {number} rowAmount количество кирпичей по оси Y, неотрицателное, целое число
|
||||
* Создает массив кирпичей проинициализированных значениями в зависимости от расположения на карте уровня
|
||||
* @param {number[][]} levelMap карта уровня в формате массива
|
||||
* @param {number} brickWidth ширина кирпича, неотрицателное число
|
||||
* @param {number} brickHeight длина кирпича, неотрицателное число
|
||||
* @returns {Brick[][]} массив с массивами кирпичей
|
||||
* @returns {Brick[]} массив кирпичей
|
||||
*/
|
||||
export function layBricks(columnAmount, rowAmount, brickWidth, brickHeight) {
|
||||
export function layBricks(levelMap, brickWidth, brickHeight) {
|
||||
try {
|
||||
const someArgsArentNumbers = [columnAmount, rowAmount, brickWidth, brickHeight].some(
|
||||
(arg) => typeof arg !== 'number',
|
||||
);
|
||||
if (someArgsArentNumbers) {
|
||||
throw new Error('Параметры кладки кирпичей должны являться числами');
|
||||
if (!(Array.isArray(levelMap) && levelMap.every(Array.isArray))) {
|
||||
throw new Error('Значение карты уровня должно являться вложенным масивом чисел глубины 2');
|
||||
}
|
||||
|
||||
const someArgsLessThanZero = [columnAmount, rowAmount, brickWidth, brickHeight].some((arg) => arg < 0);
|
||||
if (someArgsLessThanZero) {
|
||||
throw new Error('Параметры кладки кирпичей должны являться положительными целыми числами');
|
||||
if (!(typeof brickWidth === 'number' && typeof brickHeight === 'number')) {
|
||||
throw new Error('Значения ширины и высоты кирпича должны являться числами');
|
||||
}
|
||||
|
||||
const rowsOrColumnsAreFloat = [columnAmount, rowAmount].some((amount) => amount % 1 !== 0);
|
||||
if (rowsOrColumnsAreFloat) {
|
||||
throw new Error('Размеры рядов и колонок должны являться целыми числами');
|
||||
if (brickWidth < 0 || brickHeight < 0) {
|
||||
throw new Error('Значения ширины и высоты кирпича должны являться положительными целыми числами');
|
||||
}
|
||||
|
||||
const bricks = Array.from({ length: rowAmount }).map((_, rowIndex) =>
|
||||
Array.from({ length: columnAmount }).map(
|
||||
(_, columnIndex) => new Brick(columnIndex * brickWidth, rowIndex * brickHeight, brickWidth, brickHeight),
|
||||
),
|
||||
);
|
||||
const bricks = [];
|
||||
|
||||
for (let i = 0; i < levelMap.length; i++) {
|
||||
for (let j = 0; j < levelMap[i].length; j++) {
|
||||
if (levelMap[i][j] !== 0) {
|
||||
bricks.push(new Brick(j * brickWidth, i * brickHeight, brickWidth, brickHeight, levelMap[i][j]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bricks;
|
||||
} catch (err) {
|
||||
|
||||
@@ -5,54 +5,85 @@ describe('layBricks', () => {
|
||||
it('Возвращает корректное значение в случае неверного типа параметра', () => {
|
||||
const wrongType = 'wrong';
|
||||
|
||||
expect(layBricks(wrongType, 1, 1, 1)).toBeNull();
|
||||
expect(layBricks(1, wrongType, 1, 1)).toBeNull();
|
||||
expect(layBricks(1, 1, wrongType, 1)).toBeNull();
|
||||
expect(layBricks(1, 1, 1, wrongType)).toBeNull();
|
||||
expect(layBricks(wrongType, 1, 1)).toBeNull();
|
||||
expect(layBricks(1, wrongType, 1)).toBeNull();
|
||||
expect(layBricks(1, 1, wrongType)).toBeNull();
|
||||
});
|
||||
|
||||
it('Возвращает корректное значение в случае неверных размеров кирпича', () => {
|
||||
const wrongWidth = -1;
|
||||
const wrongHeight = -1;
|
||||
|
||||
expect(layBricks(1, 1, wrongWidth, 1)).toBeNull();
|
||||
expect(layBricks(1, 1, 1, wrongHeight)).toBeNull();
|
||||
expect(layBricks(1, wrongWidth, 1)).toBeNull();
|
||||
expect(layBricks(1, 1, wrongHeight)).toBeNull();
|
||||
});
|
||||
|
||||
it('Возвращает корректное значение в случае неверных значений рядов и колонок', () => {
|
||||
const negativeRow = -1;
|
||||
const negativeColumn = -1;
|
||||
const decimalRow = 1.5;
|
||||
const decimalColumn = 1.5;
|
||||
it('Возвращает корректное значение в случае некоректной карты уровня', () => {
|
||||
const levelMap = ['brick', 'brick'];
|
||||
|
||||
expect(layBricks(negativeRow, 1, 1, 1)).toBeNull();
|
||||
expect(layBricks(1, negativeColumn, 1, 1)).toBeNull();
|
||||
expect(layBricks(decimalRow, 1, 1, 1)).toBeNull();
|
||||
expect(layBricks(1, decimalColumn, 1, 1)).toBeNull();
|
||||
expect(layBricks(levelMap, 1, 1)).toBeNull();
|
||||
});
|
||||
|
||||
it('Возвращает массив корректных размеров', () => {
|
||||
const columnAmount = 10;
|
||||
const rowAmount = 5;
|
||||
const levelMap = [
|
||||
[1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1],
|
||||
];
|
||||
|
||||
const bricks = layBricks(columnAmount, rowAmount, 1, 1);
|
||||
expect(bricks.length).toEqual(rowAmount);
|
||||
for (const row of bricks) {
|
||||
expect(row.length).toEqual(columnAmount);
|
||||
}
|
||||
const bricks = layBricks(levelMap, 1, 1);
|
||||
expect(bricks.length).toBe(levelMap.flat().length);
|
||||
|
||||
const emptyLevelMap = [
|
||||
[0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0],
|
||||
];
|
||||
const emptyBricksArray = layBricks(emptyLevelMap, 1, 1);
|
||||
expect(emptyBricksArray.length).toBe(0);
|
||||
});
|
||||
|
||||
it('Задает корректные координаты кирпичам', () => {
|
||||
const bricks = layBricks(3, 3, 1, 1);
|
||||
const columnAmount = 10;
|
||||
const rowAmount = 5;
|
||||
const levelMap = [
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
];
|
||||
const brickWidth = 1;
|
||||
const brickHeight = 1;
|
||||
|
||||
const brickss = layBricks(columnAmount, rowAmount, 1, 1);
|
||||
const bricks = layBricks(levelMap, brickWidth, brickHeight);
|
||||
let index = 0;
|
||||
|
||||
for (let i = 0; i < rowAmount; i++) {
|
||||
for (let j = 0; j < columnAmount; j++) {
|
||||
expect(brickss[i][j].x).toEqual(j);
|
||||
expect(brickss[i][j]?.y).toEqual(i);
|
||||
for (let i = 0; i < levelMap.length; i++) {
|
||||
for (let j = 0; j < levelMap[i].length; j++) {
|
||||
if (levelMap[i][j] === 0) {
|
||||
continue;
|
||||
}
|
||||
expect(bricks[index].x).toBe(j * brickWidth);
|
||||
expect(bricks[index].y).toBe(i * brickHeight);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('Верно записывает тип кирпича', () => {
|
||||
const levelMap = [
|
||||
[2, 1, 3],
|
||||
[1, 2, 0],
|
||||
];
|
||||
const brickWidth = 1;
|
||||
const brickHeight = 1;
|
||||
|
||||
const bricks = layBricks(levelMap, brickWidth, brickHeight);
|
||||
let index = 0;
|
||||
|
||||
for (let i = 0; i < levelMap.length; i++) {
|
||||
for (let j = 0; j < levelMap[i].length; j++) {
|
||||
if (levelMap[i][j] === 0) {
|
||||
continue;
|
||||
}
|
||||
expect(bricks[index].type).toBe(levelMap[i][j]);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Класс сущности "бонус"
|
||||
* @class
|
||||
*/
|
||||
export class Perk {
|
||||
/**
|
||||
* @param {number} x координата положения бонуса по оси X
|
||||
* @param {number} y координата положения бонуса по оси Y
|
||||
* @param {number} width положительное числовое значение ширины бонуса
|
||||
* @param {number} height положительное числовое значение высоты бонуса
|
||||
* @param {string} type тип бонуса
|
||||
* @param {number} fallSpeed скорость падения бонуса по оси Y
|
||||
*/
|
||||
constructor(x, y, width, height, type, fallSpeed) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.type = type;
|
||||
this.fallSpeed = fallSpeed;
|
||||
this.alive = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Смещает бонус вниз в зависимости от времени
|
||||
* @param {number} deltaTime изменение времени из Ticker
|
||||
*/
|
||||
moveForward(deltaTime) {
|
||||
this.y += this.fallSpeed * deltaTime;
|
||||
}
|
||||
}
|
||||
+90
-7
@@ -2,12 +2,15 @@ import {
|
||||
BALL_INITIAL_ANGLE,
|
||||
BALL_RADIUS,
|
||||
BALL_SPEED,
|
||||
BALL_SPEED_LEVEL_STEP,
|
||||
BALL_SPEED_TIME_STEP,
|
||||
BRICK_HEIGHT,
|
||||
BRICK_WIDTH,
|
||||
CONTAINER_HEIGHT,
|
||||
CONTAINER_WIDTH,
|
||||
PADDLE_HEIGHT,
|
||||
PADDLE_WIDTH,
|
||||
PERK_BALL_SPEED_DECREASE,
|
||||
} from './config';
|
||||
import { Ball } from './entities/ball/ball';
|
||||
import { layBricks } from './entities/brick/layBricks/layBricks';
|
||||
@@ -19,17 +22,29 @@ import { tick } from './lib/tick/tick';
|
||||
*/
|
||||
export class Game {
|
||||
/**
|
||||
* @param {number} columnAmount количество кирпичей по оси X, неотрицателное, целое число
|
||||
* @param {number} rowAmount количество кирпичей по оси Y, неотрицателное, целое число
|
||||
* @param {number[][][]} levels список уровней, каждый элемент которого - карта расположения блоков
|
||||
*/
|
||||
constructor(columnAmount, rowAmount) {
|
||||
this.ball = new Ball(100, 100, BALL_RADIUS);
|
||||
this.ball.horizontalSpeed = BALL_SPEED * Math.cos(BALL_INITIAL_ANGLE);
|
||||
this.ball.verticalSpeed = -1 * BALL_SPEED * Math.sin(BALL_INITIAL_ANGLE);
|
||||
constructor(levels) {
|
||||
this.livesAmount = 3;
|
||||
this.status = 'in_process';
|
||||
this.levels = levels;
|
||||
this.currentLevel = 0;
|
||||
this.maxLevel = levels.length - 1;
|
||||
|
||||
this.paddle = new Paddle(0, CONTAINER_HEIGHT - PADDLE_HEIGHT, PADDLE_WIDTH, PADDLE_HEIGHT);
|
||||
this.balls = [new Ball(0, 0, BALL_RADIUS, BALL_SPEED, BALL_INITIAL_ANGLE)];
|
||||
this.bricks = layBricks(this.levels[this.currentLevel], BRICK_WIDTH, BRICK_HEIGHT);
|
||||
this.perks = [];
|
||||
|
||||
this.bricks = layBricks(columnAmount, rowAmount, BRICK_WIDTH, BRICK_HEIGHT);
|
||||
this._placeBallOnPaddle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Основной (первый) мяч. Пока играет один мяч - это он.
|
||||
* @returns {Ball}
|
||||
*/
|
||||
get ball() {
|
||||
return this.balls[0];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,6 +52,74 @@ export class Game {
|
||||
* @param {*} deltaTime изменение времени из Ticker
|
||||
*/
|
||||
update(deltaTime) {
|
||||
if (this.status !== 'in_process') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Пока мяч не запущен - держим его на ракетке и не считаем физику
|
||||
if (this.ball.status === 'idle') {
|
||||
this._placeBallOnPaddle();
|
||||
return;
|
||||
}
|
||||
|
||||
tick(this, CONTAINER_WIDTH, CONTAINER_HEIGHT, deltaTime);
|
||||
|
||||
// Убираем улетевшие мячи. Жизнь теряется только когда не осталось ни одного
|
||||
const survivingBalls = this.balls.filter((ball) => ball.status !== 'out');
|
||||
|
||||
if (survivingBalls.length === 0) {
|
||||
this.livesAmount -= 1;
|
||||
|
||||
if (this.livesAmount === 0) {
|
||||
this.status = 'over';
|
||||
} else {
|
||||
this._placeBallOnPaddle();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.balls = survivingBalls;
|
||||
|
||||
for (const ball of this.balls) {
|
||||
ball.increaseSpeed(BALL_SPEED_TIME_STEP * deltaTime);
|
||||
}
|
||||
|
||||
const isLevelComplete = this._checkLevelCompletion();
|
||||
|
||||
if (isLevelComplete) {
|
||||
this.currentLevel += 1;
|
||||
|
||||
if (this.currentLevel <= this.maxLevel) {
|
||||
this._proceedToNextLevel();
|
||||
} else {
|
||||
this.status = 'completed';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет завершен ли текущий уровень
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_checkLevelCompletion() {
|
||||
return this.bricks.every((brick) => brick.type === 3 || !brick.alive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Запускает переход на новый уровень, возвращает мяч в дефоотное положение и отрисовывает кирпичи по карте уровня.
|
||||
*/
|
||||
_proceedToNextLevel() {
|
||||
this.ball.increaseSpeed(BALL_SPEED_LEVEL_STEP);
|
||||
this._placeBallOnPaddle();
|
||||
this.bricks = layBricks(this.levels[this.currentLevel], BRICK_WIDTH, BRICK_HEIGHT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ставит мяч по центру ракетки, оставляя один мяч в игре
|
||||
*/
|
||||
_placeBallOnPaddle() {
|
||||
this.balls = [this.ball];
|
||||
this.ball.reset(this.paddle.x + this.paddle.width / 2, this.paddle.y - this.ball.radius);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
* @param {number} secondObject.bottom - Max Y координата второго объекта
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function calculateCollision(firstObject, secondObject) {
|
||||
export function calculateAABBCollision(firstObject, secondObject) {
|
||||
try {
|
||||
const coordinates = [
|
||||
firstObject.left,
|
||||
+9
-9
@@ -1,6 +1,6 @@
|
||||
import { calculateCollision } from './calculateCollision';
|
||||
import { calculateAABBCollision } from './calculateAABBCollision';
|
||||
|
||||
describe('calculateCollision', () => {
|
||||
describe('calculateAABBCollision', () => {
|
||||
it('Корректно обрабатывает невозможные кейсы (левая координата больше правой)', () => {
|
||||
const firstObject = {
|
||||
left: 0,
|
||||
@@ -16,7 +16,7 @@ describe('calculateCollision', () => {
|
||||
bottom: 10,
|
||||
};
|
||||
|
||||
expect(calculateCollision(firstObject, secondObject)).toBeNull();
|
||||
expect(calculateAABBCollision(firstObject, secondObject)).toBeNull();
|
||||
});
|
||||
|
||||
it('Корректно обрабатывает неверный формат данных', () => {
|
||||
@@ -34,7 +34,7 @@ describe('calculateCollision', () => {
|
||||
bottom: 10,
|
||||
};
|
||||
|
||||
expect(calculateCollision(firstObject, secondObject)).toBeNull();
|
||||
expect(calculateAABBCollision(firstObject, secondObject)).toBeNull();
|
||||
});
|
||||
|
||||
it('Корректно обрабатывает отсутствие пересечения по X', () => {
|
||||
@@ -52,7 +52,7 @@ describe('calculateCollision', () => {
|
||||
bottom: 10,
|
||||
};
|
||||
|
||||
expect(calculateCollision(firstObject, secondObject)).toBe(false);
|
||||
expect(calculateAABBCollision(firstObject, secondObject)).toBe(false);
|
||||
});
|
||||
|
||||
it('Корректно обрабатывает отсутствие пересечения по Y', () => {
|
||||
@@ -70,7 +70,7 @@ describe('calculateCollision', () => {
|
||||
bottom: 30,
|
||||
};
|
||||
|
||||
expect(calculateCollision(firstObject, secondObject)).toBe(false);
|
||||
expect(calculateAABBCollision(firstObject, secondObject)).toBe(false);
|
||||
});
|
||||
|
||||
it('Корректно обрабатывает отсутствие пересечения по X и Y', () => {
|
||||
@@ -88,7 +88,7 @@ describe('calculateCollision', () => {
|
||||
bottom: 30,
|
||||
};
|
||||
|
||||
expect(calculateCollision(firstObject, secondObject)).toBe(false);
|
||||
expect(calculateAABBCollision(firstObject, secondObject)).toBe(false);
|
||||
});
|
||||
|
||||
it('Корректно обрабатывает пересечение', () => {
|
||||
@@ -106,7 +106,7 @@ describe('calculateCollision', () => {
|
||||
bottom: 30,
|
||||
};
|
||||
|
||||
expect(calculateCollision(firstObject, secondObject)).toBe(true);
|
||||
expect(calculateAABBCollision(firstObject, secondObject)).toBe(true);
|
||||
});
|
||||
|
||||
it('Корректно обрабатывает вхождение', () => {
|
||||
@@ -124,6 +124,6 @@ describe('calculateCollision', () => {
|
||||
bottom: 15,
|
||||
};
|
||||
|
||||
expect(calculateCollision(firstObject, secondObject)).toBe(true);
|
||||
expect(calculateAABBCollision(firstObject, secondObject)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { calculateCollision } from '../calculateCollision/calculateCollision';
|
||||
import { calculateAABBCollision } from '../calculateAABBCollision/calculateAABBCollision';
|
||||
|
||||
/**
|
||||
* Вычисляет направление наибольшего пересечения по осям и возвращает tuple множителей для изменения координат
|
||||
@@ -17,7 +17,7 @@ import { calculateCollision } from '../calculateCollision/calculateCollision';
|
||||
export function calculateDirection(firstObject, secondObject) {
|
||||
try {
|
||||
// Запускаем для проверки формата аргументов
|
||||
const isCollided = calculateCollision(firstObject, secondObject);
|
||||
const isCollided = calculateAABBCollision(firstObject, secondObject);
|
||||
|
||||
if (isCollided === null) {
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Ball } from '../../entities/ball/ball';
|
||||
|
||||
/**
|
||||
* Двигает мяч и обрабатывает отскок от стен. Возвращает true, если мяч уже отскочил или вышел за нижнюю границу
|
||||
* @param {Ball} ball мяч
|
||||
* @param {number} containerWidth ширина игрового контейнера в пикселях
|
||||
* @param {number} containerHeight высота игрового контейнера в пикселях
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function processBall(ball, containerWidth, containerHeight) {
|
||||
try {
|
||||
if (!(ball instanceof Ball)) {
|
||||
throw new Error('Аргумент ball должен быть экземпляром класса мяча');
|
||||
}
|
||||
|
||||
if (typeof containerWidth !== 'number' || typeof containerHeight !== 'number') {
|
||||
throw new Error('Значения размеров игрового контейнера должены являться числами');
|
||||
}
|
||||
|
||||
if (containerWidth <= 0 || containerHeight <= 0) {
|
||||
throw new Error('Размеры игрового контейнера должены быть положительными числами');
|
||||
}
|
||||
|
||||
const leftBoundary = ball.radius;
|
||||
const rightBoundary = containerWidth - ball.radius;
|
||||
const topBoundary = ball.radius;
|
||||
const bottomBoundary = containerHeight - ball.radius;
|
||||
|
||||
// Не даем мячу выйти за границы стен слева / справав и меняем направление
|
||||
if (ball.x <= leftBoundary || ball.x >= rightBoundary) {
|
||||
ball.x = ball.x <= leftBoundary ? leftBoundary : rightBoundary;
|
||||
ball.horizontalSpeed *= -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Не даем мячу выйти за границу стены сверху и меняем направление
|
||||
if (ball.y <= topBoundary) {
|
||||
ball.y = topBoundary;
|
||||
ball.verticalSpeed *= -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Проверяем выход за границу стены снизу
|
||||
if (ball.y >= bottomBoundary) {
|
||||
ball.status = 'out';
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Ball } from '../../entities/ball/ball';
|
||||
import { processBall } from './processBall';
|
||||
|
||||
describe('processBall', () => {
|
||||
it('Возвращает null при неверных типах аргументов', () => {
|
||||
const ball = new Ball(100, 100, 10, 3);
|
||||
|
||||
expect(processBall(null, 800, 600)).toBeNull();
|
||||
expect(processBall(ball, '800', 600)).toBeNull();
|
||||
expect(processBall(ball, -1, 600)).toBeNull();
|
||||
expect(processBall(ball, 800, -1)).toBeNull();
|
||||
});
|
||||
|
||||
it('Возвращает false, если мяч не касается стен', () => {
|
||||
const ball = new Ball(400, 300, 10, 3);
|
||||
|
||||
expect(processBall(ball, 800, 600)).toBe(false);
|
||||
});
|
||||
|
||||
it('Отражает мяч от боковой стены и возвращает true', () => {
|
||||
const ball = new Ball(10, 300, 10, 3);
|
||||
ball.horizontalSpeed = -10;
|
||||
|
||||
expect(processBall(ball, 800, 600)).toBe(true);
|
||||
expect(ball.x).toBe(ball.radius);
|
||||
expect(ball.horizontalSpeed).toBe(10);
|
||||
});
|
||||
|
||||
it('Отражает мяч от верхней стены и возвращает true', () => {
|
||||
const ball = new Ball(400, 10, 10, 3);
|
||||
ball.horizontalSpeed = 0;
|
||||
ball.verticalSpeed = -10;
|
||||
|
||||
expect(processBall(ball, 800, 600)).toBe(true);
|
||||
expect(ball.y).toBe(ball.radius);
|
||||
expect(ball.verticalSpeed).toBe(10);
|
||||
});
|
||||
|
||||
it('Помечает мяч вышедшим за нижнюю границу', () => {
|
||||
const ball = new Ball(400, 595, 10, 3);
|
||||
|
||||
expect(processBall(ball, 800, 600)).toBe(true);
|
||||
expect(ball.status).toBe('out');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { PERK_DROP_CHANCE, PERK_HEIGHT, PERK_WIDTH } from '../../config';
|
||||
import { Ball } from '../../entities/ball/ball';
|
||||
import { Game } from '../../game';
|
||||
import { calculateAABBCollision } from '../calculateAABBCollision/calculateAABBCollision';
|
||||
import { calculateDirection } from '../calculateDirection/calculateDirection';
|
||||
import { spawnRandomPerk } from '../spawnRandomPerk/spawnRandomPerk';
|
||||
|
||||
/**
|
||||
* Базовое взаимодействие мяча и кирпичей: меняет направление, разрушает кирпич и роняет бонус
|
||||
* @param {Game} game экземпляр класса игры
|
||||
* @param {Ball} ball мяч
|
||||
* @returns {boolean | null} true, если произошло столкновение с кирпичом
|
||||
*/
|
||||
export function processBrickCollision(game, ball) {
|
||||
try {
|
||||
if (!(game instanceof Game)) {
|
||||
throw new Error('Аргумент game должен быть экземпляром класса игра');
|
||||
}
|
||||
|
||||
if (!(ball instanceof Ball)) {
|
||||
throw new Error('Аргумент ball должен быть экземпляром класса мяч');
|
||||
}
|
||||
|
||||
for (const brick of game.bricks) {
|
||||
if (!brick.alive) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ballObject = {
|
||||
left: ball.x - ball.radius,
|
||||
right: ball.x + ball.radius,
|
||||
top: ball.y - ball.radius,
|
||||
bottom: ball.y + ball.radius,
|
||||
};
|
||||
const brickObject = {
|
||||
left: brick.x,
|
||||
right: brick.x + brick.width,
|
||||
top: brick.y,
|
||||
bottom: brick.y + brick.height,
|
||||
};
|
||||
|
||||
const isCollided = calculateAABBCollision(ballObject, brickObject);
|
||||
|
||||
if (!isCollided) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const directions = calculateDirection(ballObject, brickObject);
|
||||
|
||||
if (directions === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ball.horizontalSpeed *= directions[0];
|
||||
ball.verticalSpeed *= directions[1];
|
||||
brick.kill();
|
||||
|
||||
if (!brick.alive && Math.random() < PERK_DROP_CHANCE) {
|
||||
const perk = spawnRandomPerk(
|
||||
brick.x + brick.width / 2 - PERK_WIDTH / 2,
|
||||
brick.y + brick.height / 2 - PERK_HEIGHT / 2,
|
||||
);
|
||||
game.perks.push(perk);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Game } from '../../game';
|
||||
import { processBrickCollision } from './processBrickCollision';
|
||||
|
||||
describe('processBrickCollision', () => {
|
||||
it('Возвращает корректное значение при неверных типах аргументов', () => {
|
||||
const game = new Game([[[1]]]);
|
||||
|
||||
expect(processBrickCollision(null, game.ball)).toBeNull();
|
||||
expect(processBrickCollision(game, null)).toBeNull();
|
||||
});
|
||||
|
||||
it('Возвращает креектное значение если мяч не касается кирпичей', () => {
|
||||
const game = new Game([[[1]]]);
|
||||
game.ball.x = 5;
|
||||
game.ball.y = 500;
|
||||
|
||||
expect(processBrickCollision(game, game.ball)).toBe(false);
|
||||
});
|
||||
|
||||
it('При столкновении разрушает кирпич и меняет направление мяча', () => {
|
||||
const game = new Game([[[1]]]);
|
||||
const brick = game.bricks[0];
|
||||
game.ball.x = brick.x + game.ball.radius + 1;
|
||||
game.ball.y = brick.y + brick.height + game.ball.radius + 1;
|
||||
game.ball.horizontalSpeed = 0;
|
||||
game.ball.verticalSpeed = -10;
|
||||
game.ball.moveForward(1);
|
||||
|
||||
expect(processBrickCollision(game, game.ball)).toBe(true);
|
||||
expect(brick.alive).toBe(false);
|
||||
expect(game.ball.verticalSpeed).toBe(10);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Ball } from '../../entities/ball/ball';
|
||||
import { Paddle } from '../../entities/paddle/paddle';
|
||||
import { toRadians } from '../toRadians/toRadians';
|
||||
|
||||
/**
|
||||
* Проверяет произошло ли столкновение ракетки и мяча
|
||||
* @param {Paddle} paddle
|
||||
* @param {Ball} ball
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function didCollide(paddle, ball) {
|
||||
return (
|
||||
ball.verticalSpeed > 0 &&
|
||||
ball.y + ball.radius >= paddle.y &&
|
||||
ball.x >= paddle.x &&
|
||||
ball.x <= paddle.x + paddle.width
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Изменяет направление полета мяча в зависимости от места попадания в ракетку
|
||||
* @param {Paddle} paddle экземпляр класса ракетка
|
||||
* @param {Ball} ball экземпляр класса мяч
|
||||
* @param {number} paddleSectorAmount количество секторов ракетки, положительное целое число
|
||||
* @param {number} minAngle минимальный угол отскока мяча (от 0 до 180)
|
||||
* @param {number} maxAngle максимальный угол отскока мяча (от 0 до 180)
|
||||
* @param {number} ballSpeed константа скорости мяча, положительное число
|
||||
*/
|
||||
export function processReflection(paddle, ball, paddleSectorAmount, minAngle, maxAngle, ballSpeed) {
|
||||
try {
|
||||
if (!(paddle instanceof Paddle)) {
|
||||
throw new Error('Аргумерт paddle должен являться экземпляром класса Paddle');
|
||||
}
|
||||
|
||||
if (!(ball instanceof Ball)) {
|
||||
throw new Error('Аргумерт ball должен являться экземпляром класса Ball');
|
||||
}
|
||||
|
||||
if (typeof paddleSectorAmount !== 'number' || paddleSectorAmount <= 0 || paddleSectorAmount % 1 !== 0) {
|
||||
throw new Error('Аргумент paddleSectorAmount должен являться положительным целым числом');
|
||||
}
|
||||
|
||||
if (typeof minAngle !== 'number' || minAngle < 0 || minAngle > 180) {
|
||||
throw new Error('Аргумент minAngle должен являться числом в промежутке от 0 до 180');
|
||||
}
|
||||
|
||||
if (typeof maxAngle !== 'number' || maxAngle < 0 || maxAngle > 180) {
|
||||
throw new Error('Аргумент maxAngle должен являться числом в промежутке от 0 до 180');
|
||||
}
|
||||
|
||||
if (minAngle >= maxAngle) {
|
||||
throw new Error('Значение аргумента maxAngle должено быть строго больше значения аргумента minAngle');
|
||||
}
|
||||
|
||||
if (typeof ballSpeed !== 'number' || ballSpeed <= 0) {
|
||||
throw new Error('Значение аргумента ballSpeed должно быть положительным целым числом');
|
||||
}
|
||||
|
||||
if (!didCollide(paddle, ball)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const step = paddle.width / paddleSectorAmount;
|
||||
const sectorIndex = Math.floor((ball.x - paddle.x) / step);
|
||||
const normalizedSectorIndex = Math.min(Math.max(sectorIndex, 0), paddleSectorAmount - 1);
|
||||
|
||||
const angleStep = (maxAngle - minAngle) / (paddleSectorAmount - 1);
|
||||
const angleInRadians = toRadians(maxAngle - angleStep * normalizedSectorIndex);
|
||||
ball.horizontalSpeed = ballSpeed * Math.cos(angleInRadians);
|
||||
ball.verticalSpeed = -1 * ballSpeed * Math.sin(angleInRadians);
|
||||
ball.y = paddle.y - ball.radius;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { Ball } from '../../entities/ball/ball';
|
||||
import { Paddle } from '../../entities/paddle/paddle';
|
||||
import { processReflection } from './processReflection';
|
||||
|
||||
describe('processReflection', () => {
|
||||
it('Корректно обрабатывает неверное значения аргумента paddle', () => {
|
||||
const ball = new Ball(0, 0, 1, 1, 0);
|
||||
const sectorAmount = 1;
|
||||
const minAngle = 1;
|
||||
const maxAngle = 179;
|
||||
const speed = 1;
|
||||
|
||||
const ballX = ball.x;
|
||||
const ballY = ball.y;
|
||||
const verticalSpeed = ball.verticalSpeed;
|
||||
const horizontalSpeed = ball.horizontalSpeed;
|
||||
|
||||
const result = processReflection(null, ball, sectorAmount, minAngle, maxAngle, speed);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(ball.x).toEqual(ballX);
|
||||
expect(ball.y).toEqual(ballY);
|
||||
expect(ball.verticalSpeed).toEqual(verticalSpeed);
|
||||
expect(ball.horizontalSpeed).toEqual(horizontalSpeed);
|
||||
});
|
||||
|
||||
it('Корректно обрабатывает неверное значения аргумента ball', () => {
|
||||
const paddle = new Paddle(0, 0, 2, 1);
|
||||
const sectorAmount = 1;
|
||||
const minAngle = 1;
|
||||
const maxAngle = 179;
|
||||
const speed = 1;
|
||||
|
||||
const result = processReflection(paddle, null, sectorAmount, minAngle, maxAngle, speed);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('Корректно обрабатывает неверное значения аргумента paddleSectorAmount', () => {
|
||||
const paddle = new Paddle(0, 0, 2, 1);
|
||||
const ball = new Ball(0, 0, 1, 1, 0);
|
||||
const minAngle = 1;
|
||||
const maxAngle = 179;
|
||||
const speed = 1;
|
||||
|
||||
const ballX = ball.x;
|
||||
const ballY = ball.y;
|
||||
const verticalSpeed = ball.verticalSpeed;
|
||||
const horizontalSpeed = ball.horizontalSpeed;
|
||||
|
||||
const result = processReflection(paddle, ball, null, minAngle, maxAngle, speed);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(ball.x).toEqual(ballX);
|
||||
expect(ball.y).toEqual(ballY);
|
||||
expect(ball.verticalSpeed).toEqual(verticalSpeed);
|
||||
expect(ball.horizontalSpeed).toEqual(horizontalSpeed);
|
||||
|
||||
expect(processReflection(paddle, ball, 0, minAngle, maxAngle, speed)).toBeNull();
|
||||
expect(processReflection(paddle, ball, -1, minAngle, maxAngle, speed)).toBeNull();
|
||||
expect(processReflection(paddle, ball, 1.5, minAngle, maxAngle, speed)).toBeNull();
|
||||
});
|
||||
|
||||
it('Корректно обрабатывает неверное значения аргумента minAngle', () => {
|
||||
const paddle = new Paddle(0, 0, 2, 1);
|
||||
const ball = new Ball(0, 0, 1, 1, 0);
|
||||
const sectorAmount = 1;
|
||||
const maxAngle = 178;
|
||||
const speed = 1;
|
||||
|
||||
const ballX = ball.x;
|
||||
const ballY = ball.y;
|
||||
const verticalSpeed = ball.verticalSpeed;
|
||||
const horizontalSpeed = ball.horizontalSpeed;
|
||||
|
||||
const result = processReflection(paddle, ball, sectorAmount, null, maxAngle, speed);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(ball.x).toEqual(ballX);
|
||||
expect(ball.y).toEqual(ballY);
|
||||
expect(ball.verticalSpeed).toEqual(verticalSpeed);
|
||||
expect(ball.horizontalSpeed).toEqual(horizontalSpeed);
|
||||
|
||||
expect(processReflection(paddle, ball, sectorAmount, -1, maxAngle, speed)).toBeNull();
|
||||
expect(processReflection(paddle, ball, sectorAmount, 360, maxAngle, speed)).toBeNull();
|
||||
|
||||
const minAngleBiggerThanMaxAngle = 179;
|
||||
expect(processReflection(paddle, ball, sectorAmount, minAngleBiggerThanMaxAngle, maxAngle, speed)).toBeNull();
|
||||
});
|
||||
|
||||
it('Корректно обрабатывает неверное значения аргумента maxAngle', () => {
|
||||
const paddle = new Paddle(0, 0, 2, 1);
|
||||
const ball = new Ball(0, 0, 1, 1, 0);
|
||||
const sectorAmount = 1;
|
||||
const minAngle = 1;
|
||||
const speed = 1;
|
||||
|
||||
const ballX = ball.x;
|
||||
const ballY = ball.y;
|
||||
const verticalSpeed = ball.verticalSpeed;
|
||||
const horizontalSpeed = ball.horizontalSpeed;
|
||||
|
||||
const result = processReflection(paddle, ball, sectorAmount, minAngle, null, speed);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(ball.x).toEqual(ballX);
|
||||
expect(ball.y).toEqual(ballY);
|
||||
expect(ball.verticalSpeed).toEqual(verticalSpeed);
|
||||
expect(ball.horizontalSpeed).toEqual(horizontalSpeed);
|
||||
|
||||
expect(processReflection(paddle, ball, sectorAmount, minAngle, -1, speed)).toBeNull();
|
||||
expect(processReflection(paddle, ball, sectorAmount, minAngle, 360, speed)).toBeNull();
|
||||
});
|
||||
|
||||
it('Корректно обрабатывает неверное значения аргумента speed', () => {
|
||||
const paddle = new Paddle(0, 0, 2, 1);
|
||||
const ball = new Ball(0, 0, 1, 1, 0);
|
||||
const sectorAmount = 1;
|
||||
const minAngle = 1;
|
||||
const maxAngle = 179;
|
||||
|
||||
const ballX = ball.x;
|
||||
const ballY = ball.y;
|
||||
const verticalSpeed = ball.verticalSpeed;
|
||||
const horizontalSpeed = ball.horizontalSpeed;
|
||||
|
||||
const result = processReflection(paddle, ball, sectorAmount, minAngle, maxAngle, null);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(ball.x).toEqual(ballX);
|
||||
expect(ball.y).toEqual(ballY);
|
||||
expect(ball.verticalSpeed).toEqual(verticalSpeed);
|
||||
expect(ball.horizontalSpeed).toEqual(horizontalSpeed);
|
||||
|
||||
expect(processReflection(paddle, ball, sectorAmount, minAngle, maxAngle, -1)).toBeNull();
|
||||
});
|
||||
|
||||
it('Корректно обрабатывает неверное значения аргумента speed', () => {
|
||||
const paddle = new Paddle(0, 0, 2, 1);
|
||||
const ball = new Ball(0, 0, 1, 1, 0);
|
||||
const sectorAmount = 1;
|
||||
const minAngle = 1;
|
||||
const maxAngle = 179;
|
||||
|
||||
const ballX = ball.x;
|
||||
const ballY = ball.y;
|
||||
const verticalSpeed = ball.verticalSpeed;
|
||||
const horizontalSpeed = ball.horizontalSpeed;
|
||||
|
||||
const result = processReflection(paddle, ball, sectorAmount, minAngle, maxAngle, null);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(ball.x).toEqual(ballX);
|
||||
expect(ball.y).toEqual(ballY);
|
||||
expect(ball.verticalSpeed).toEqual(verticalSpeed);
|
||||
expect(ball.horizontalSpeed).toEqual(horizontalSpeed);
|
||||
|
||||
expect(processReflection(paddle, ball, sectorAmount, minAngle, maxAngle, -1)).toBeNull();
|
||||
});
|
||||
|
||||
it('Ничего не изменяет при отсутствии столкновения ракетки и мяча', () => {
|
||||
const paddle = new Paddle(0, 0, 2, 1);
|
||||
const ball = new Ball(10, 10, 1, 1, 0);
|
||||
const sectorAmount = 1;
|
||||
const minAngle = 1;
|
||||
const maxAngle = 179;
|
||||
const speed = 1;
|
||||
|
||||
const ballX = ball.x;
|
||||
const ballY = ball.y;
|
||||
const verticalSpeed = ball.verticalSpeed;
|
||||
const horizontalSpeed = ball.horizontalSpeed;
|
||||
|
||||
const result = processReflection(paddle, ball, sectorAmount, minAngle, maxAngle, speed);
|
||||
|
||||
expect(ball.x).toEqual(ballX);
|
||||
expect(ball.y).toEqual(ballY);
|
||||
expect(ball.verticalSpeed).toEqual(verticalSpeed);
|
||||
expect(ball.horizontalSpeed).toEqual(horizontalSpeed);
|
||||
});
|
||||
|
||||
it('Отражает мяч влево при попадании в левый сектор ракетки', () => {
|
||||
const paddle = new Paddle(0, 100, 90, 10);
|
||||
const ball = new Ball(5, 99, 1, 5, 0); // левый край, летит вниз
|
||||
const sectorAmount = 3;
|
||||
const minAngle = 30;
|
||||
const maxAngle = 150;
|
||||
const speed = 10;
|
||||
|
||||
processReflection(paddle, ball, sectorAmount, minAngle, maxAngle, speed);
|
||||
|
||||
expect(ball.horizontalSpeed).toBeLessThan(0);
|
||||
expect(ball.verticalSpeed).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('Отражает мяч вправо при попадании в правый сектор ракетки', () => {
|
||||
const paddle = new Paddle(0, 100, 90, 10);
|
||||
const ball = new Ball(85, 99, 1, 5, 0);
|
||||
const sectorAmount = 3;
|
||||
const minAngle = 30;
|
||||
const maxAngle = 150;
|
||||
const speed = 10;
|
||||
|
||||
processReflection(paddle, ball, sectorAmount, minAngle, maxAngle, speed);
|
||||
|
||||
expect(ball.horizontalSpeed).toBeGreaterThan(0);
|
||||
expect(ball.verticalSpeed).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('Отражает мяч вертикально вверх при попадании в центральный сектор', () => {
|
||||
const paddle = new Paddle(0, 100, 90, 10);
|
||||
const ball = new Ball(45, 99, 1, 5, 0);
|
||||
const sectorAmount = 3;
|
||||
const minAngle = 30;
|
||||
const maxAngle = 150;
|
||||
const speed = 10;
|
||||
|
||||
processReflection(paddle, ball, sectorAmount, minAngle, maxAngle, speed);
|
||||
|
||||
expect(ball.horizontalSpeed).toBeCloseTo(0);
|
||||
expect(ball.verticalSpeed).toBeCloseTo(-speed);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { PERK_FALL_SPEED, PERK_HEIGHT, PERK_TYPES, PERK_WIDTH } from '../../config';
|
||||
import { Perk } from '../../entities/perk/perk';
|
||||
|
||||
/**
|
||||
* Создает случайный бонус в заданной точке
|
||||
* @param {number} x координата бонуса по оси X
|
||||
* @param {number} y координата бонуса по оси Y
|
||||
* @returns {Perk | null}
|
||||
*/
|
||||
export function spawnRandomPerk(x, y) {
|
||||
try {
|
||||
if (typeof x !== 'number' || typeof y !== 'number') {
|
||||
throw new Error('Координаты бонуса должны являться числами');
|
||||
}
|
||||
|
||||
const type = PERK_TYPES[Math.floor(Math.random() * PERK_TYPES.length)];
|
||||
|
||||
return new Perk(x, y, PERK_WIDTH, PERK_HEIGHT, type, PERK_FALL_SPEED);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { PERK_FALL_SPEED, PERK_HEIGHT, PERK_WIDTH } from '../../config';
|
||||
import { Perk } from '../../entities/perk/perk';
|
||||
import { spawnRandomPerk } from './spawnRandomPerk';
|
||||
|
||||
describe('spawnRandomPerk', () => {
|
||||
it('Возвращает корректное значение при неверных типах аргументов', () => {
|
||||
expect(spawnRandomPerk(null, 0)).toBeNull();
|
||||
expect(spawnRandomPerk(0, null)).toBeNull();
|
||||
expect(spawnRandomPerk('10', 0)).toBeNull();
|
||||
expect(spawnRandomPerk(0, undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it('Создает бонус в заданной точке с размерами и скоростью из конфига', () => {
|
||||
const perk = spawnRandomPerk(10, 20);
|
||||
|
||||
expect(perk).toBeInstanceOf(Perk);
|
||||
expect(perk.x).toBe(10);
|
||||
expect(perk.y).toBe(20);
|
||||
expect(perk.width).toBe(PERK_WIDTH);
|
||||
expect(perk.height).toBe(PERK_HEIGHT);
|
||||
expect(perk.fallSpeed).toBe(PERK_FALL_SPEED);
|
||||
expect(perk.alive).toBe(true);
|
||||
});
|
||||
});
|
||||
+30
-62
@@ -1,7 +1,9 @@
|
||||
import { CONTAINER_HEIGHT, CONTAINER_WIDTH } from '../../config';
|
||||
import { MAX_PADDLE_REFLECTION_ANGLE, MIN_PADDLE_REFLECTION_ANGLE, PADDLE_SECTOR_AMOUNT } from '../../config';
|
||||
import { Game } from '../../game';
|
||||
import { calculateCollision } from '../calculateCollision/calculateCollision';
|
||||
import { calculateDirection } from '../calculateDirection/calculateDirection';
|
||||
import { processBall } from '../processBall/processBall';
|
||||
import { processBrickCollision } from '../processBrickCollision/processBrickCollision';
|
||||
import { processReflection } from '../processReflection/processReflection';
|
||||
import { updatePerks } from '../updatePerks/updatePerks';
|
||||
|
||||
/**
|
||||
* Функция для вычисления взаимодействий сущностей игры в зависимости от времени из Ticker
|
||||
@@ -28,70 +30,36 @@ export function tick(game, containerWidth, containerHeight, deltaTime) {
|
||||
throw new Error('Значение изменения времени должно быть положительным числом');
|
||||
}
|
||||
|
||||
const { ball, paddle, bricks } = game;
|
||||
const { paddle } = game;
|
||||
|
||||
ball.moveForward(deltaTime);
|
||||
// Бонусы падают и ловятся ракеткой каждый кадр
|
||||
updatePerks(game, containerHeight, deltaTime);
|
||||
|
||||
const leftBoundary = ball.radius;
|
||||
const rightBoundary = containerWidth - ball.radius;
|
||||
const topBoundary = ball.radius;
|
||||
const bottomBoundary = containerHeight - ball.radius;
|
||||
for (const ball of game.balls) {
|
||||
ball.moveForward(deltaTime);
|
||||
// Проверка столкновения со стенами и вылета за границу поля снизу
|
||||
const hitWallOrWentOut = processBall(ball, containerWidth, containerHeight);
|
||||
|
||||
// Не даем мячу выйти за границы стен слева / справав и меняем направление
|
||||
if (ball.x <= leftBoundary || ball.x >= rightBoundary) {
|
||||
ball.x = ball.x <= leftBoundary ? leftBoundary : rightBoundary;
|
||||
ball.horizontalSpeed *= -1;
|
||||
}
|
||||
|
||||
// Не даем мячу выйти за границы стен сверху / снизу и меняем направление
|
||||
if (ball.y <= topBoundary || ball.y >= bottomBoundary) {
|
||||
ball.y = ball.y <= topBoundary ? topBoundary : bottomBoundary;
|
||||
ball.verticalSpeed *= -1;
|
||||
}
|
||||
|
||||
// Базоввое взаимодействие мяча и кирпича
|
||||
for (const row of bricks) {
|
||||
for (const brick of row) {
|
||||
if (!brick.alive) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ballLeft = ball.x - ball.radius;
|
||||
const ballRight = ball.x + ball.radius;
|
||||
const ballTop = ball.y - ball.radius;
|
||||
const ballBottom = ball.y + ball.radius;
|
||||
|
||||
const brickLeft = brick.x;
|
||||
const brickRight = brick.x + brick.width;
|
||||
const brickTop = brick.y;
|
||||
const brickBottom = brick.y + brick.height;
|
||||
|
||||
const ballObject = { left: ballLeft, right: ballRight, top: ballTop, bottom: ballBottom };
|
||||
const brickObject = { left: brickLeft, right: brickRight, top: brickTop, bottom: brickBottom };
|
||||
|
||||
const isCollided = calculateCollision(ballObject, brickObject);
|
||||
|
||||
if (isCollided) {
|
||||
const directions = calculateDirection(ballObject, brickObject);
|
||||
|
||||
if (directions !== null) {
|
||||
ball.horizontalSpeed *= directions[0];
|
||||
ball.verticalSpeed *= directions[1];
|
||||
brick.kill();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hitWallOrWentOut) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Базоввое взаимодействие мяча и ракетки
|
||||
if (
|
||||
ball.verticalSpeed > 0 &&
|
||||
ball.y + ball.radius >= paddle.y &&
|
||||
ball.x >= paddle.x &&
|
||||
ball.x <= paddle.x + paddle.width
|
||||
) {
|
||||
ball.verticalSpeed *= -1;
|
||||
// Проверка столкновения с кирпичами
|
||||
const hitBrick = processBrickCollision(game, ball);
|
||||
|
||||
if (hitBrick) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Взаимодействие мяча и ракетки - обновление горизонтальной и вертикальной скорости в зависимости от сектора попадания
|
||||
processReflection(
|
||||
paddle,
|
||||
ball,
|
||||
PADDLE_SECTOR_AMOUNT,
|
||||
MIN_PADDLE_REFLECTION_ANGLE,
|
||||
MAX_PADDLE_REFLECTION_ANGLE,
|
||||
ball.speed,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
+17
-11
@@ -11,7 +11,8 @@ describe('tick', () => {
|
||||
});
|
||||
|
||||
it('Возвращает корректное значние и не изменяет свойства game при неверых размерах контейнера', () => {
|
||||
const game = new Game(1, 1);
|
||||
const levels = [[[1]]];
|
||||
const game = new Game(levels);
|
||||
const ballX = game.ball.x;
|
||||
const ballY = game.ball.y;
|
||||
expect(tick(game, -1, -1, 1)).toBeNull();
|
||||
@@ -20,7 +21,8 @@ describe('tick', () => {
|
||||
});
|
||||
|
||||
it('Возвращает корректное значние и не изменяет свойства game при неверном значении времени', () => {
|
||||
const game = new Game(1, 1);
|
||||
const levels = [[[1]]];
|
||||
const game = new Game(levels);
|
||||
const ballX = game.ball.x;
|
||||
const ballY = game.ball.y;
|
||||
expect(tick(game, 1, 1, -1)).toBeNull();
|
||||
@@ -29,7 +31,8 @@ describe('tick', () => {
|
||||
});
|
||||
|
||||
it('Меняет направление мяча при столкновении со стеной', () => {
|
||||
const game = new Game(1, 1);
|
||||
const levels = [[[1]]];
|
||||
const game = new Game(levels);
|
||||
game.ball.x = 0;
|
||||
game.ball.horizontalSpeed = -10;
|
||||
tick(game, CONTAINER_WIDTH, CONTAINER_HEIGHT, 1);
|
||||
@@ -38,7 +41,8 @@ describe('tick', () => {
|
||||
});
|
||||
|
||||
it('Меняет направление мяча при столкновении с потолком', () => {
|
||||
const game = new Game(0, 0);
|
||||
const levels = [[[1]]];
|
||||
const game = new Game(1);
|
||||
game.ball.y = 0;
|
||||
game.ball.verticalSpeed = -10;
|
||||
tick(game, CONTAINER_WIDTH, CONTAINER_HEIGHT, 1);
|
||||
@@ -47,10 +51,11 @@ describe('tick', () => {
|
||||
});
|
||||
|
||||
it('Убивает кирпич при столкновении и отражает мяч', () => {
|
||||
const game = new Game(1, 1);
|
||||
const brick = game.bricks[0][0];
|
||||
const levels = [[[1]]];
|
||||
const game = new Game(levels);
|
||||
const brick = game.bricks[0];
|
||||
|
||||
game.ball.x = brick.x + 1;
|
||||
game.ball.x = brick.x + game.ball.radius + 1;
|
||||
game.ball.y = brick.y + brick.height + game.ball.radius + 1;
|
||||
game.ball.horizontalSpeed = 0;
|
||||
game.ball.verticalSpeed = -10;
|
||||
@@ -62,17 +67,18 @@ describe('tick', () => {
|
||||
});
|
||||
|
||||
it('Убивает только один кирпич за тик', () => {
|
||||
const game = new Game(2, 1);
|
||||
const [firstBrick, secondBrick] = game.bricks[0];
|
||||
const levels = [[[1, 1]]];
|
||||
const game = new Game(levels);
|
||||
const [firstBrick, secondBrick] = game.bricks;
|
||||
|
||||
game.ball.x = firstBrick.x + 1;
|
||||
game.ball.x = firstBrick.x + game.ball.radius + 1;
|
||||
game.ball.y = firstBrick.y + firstBrick.height + game.ball.radius + 1;
|
||||
game.ball.horizontalSpeed = 0;
|
||||
game.ball.verticalSpeed = -10;
|
||||
|
||||
tick(game, CONTAINER_WIDTH, CONTAINER_HEIGHT, 0.1);
|
||||
|
||||
const killedCount = game.bricks[0].filter((b) => !b.alive).length;
|
||||
const killedCount = game.bricks.filter((b) => !b.alive).length;
|
||||
expect(killedCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Конвертирует значение угла из градусов в радианы
|
||||
* @param {number} degrees значение угла в градусах
|
||||
* @returns {number}
|
||||
*/
|
||||
export function toRadians(angleInDegrees) {
|
||||
return (angleInDegrees * Math.PI) / 180;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
BALL_SPLIT_ANGLE,
|
||||
CONTAINER_WIDTH,
|
||||
PADDLE_WIDE_WIDTH,
|
||||
PADDLE_WIDTH,
|
||||
PERK_BALL_SPEED_DECREASE,
|
||||
PERK_DURATION_SEC,
|
||||
} from '../../config';
|
||||
import { clone } from '../../entities/ball/clone/clone';
|
||||
import { Game } from '../../game';
|
||||
import { calculateAABBCollision } from '../calculateAABBCollision/calculateAABBCollision';
|
||||
|
||||
/**
|
||||
* Обновляет состояние бонусов, меняет положение, проверяет столкновение с ракеткой и выход за границу поля
|
||||
* @param {Game} game экземпляр класса игры
|
||||
* @param {number} containerHeight высота игрового контейнера в пикселях
|
||||
* @param {number} deltaTime изменение времени из Ticker
|
||||
*/
|
||||
export function updatePerks(game, containerHeight, deltaTime) {
|
||||
try {
|
||||
if (!(game instanceof Game)) {
|
||||
throw new Error('Аргумент game должен быть экземпляром класса игры');
|
||||
}
|
||||
|
||||
if (typeof containerHeight !== 'number' || containerHeight <= 0) {
|
||||
throw new Error('Высота игрового контейнера должна быть положительным числом');
|
||||
}
|
||||
|
||||
if (typeof deltaTime !== 'number' || deltaTime <= 0) {
|
||||
throw new Error('Значение изменения времени должно быть положительным числом');
|
||||
}
|
||||
|
||||
const { paddle } = game;
|
||||
|
||||
for (const perk of game.perks) {
|
||||
perk.moveForward(deltaTime);
|
||||
|
||||
const perkObject = {
|
||||
left: perk.x,
|
||||
right: perk.x + perk.width,
|
||||
top: perk.y,
|
||||
bottom: perk.y + perk.height,
|
||||
};
|
||||
const paddleObject = {
|
||||
left: paddle.x,
|
||||
right: paddle.x + paddle.width,
|
||||
top: paddle.y,
|
||||
bottom: paddle.y + paddle.height,
|
||||
};
|
||||
|
||||
const isCollided = calculateAABBCollision(perkObject, paddleObject);
|
||||
|
||||
if (isCollided) {
|
||||
switch (perk.type) {
|
||||
case 'slow':
|
||||
for (const ball of game.balls) {
|
||||
if (ball.speed - PERK_BALL_SPEED_DECREASE > 0) {
|
||||
ball.increaseSpeed(-1 * PERK_BALL_SPEED_DECREASE);
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
for (const ball of game.balls) {
|
||||
ball.increaseSpeed(PERK_BALL_SPEED_DECREASE);
|
||||
}
|
||||
}, PERK_DURATION_SEC * 1000);
|
||||
break;
|
||||
case 'wide':
|
||||
paddle.width = PADDLE_WIDE_WIDTH;
|
||||
paddle.moveTo(paddle.x, 0, CONTAINER_WIDTH);
|
||||
setTimeout(() => {
|
||||
paddle.width = PADDLE_WIDTH;
|
||||
}, PERK_DURATION_SEC * 1000);
|
||||
break;
|
||||
case 'life':
|
||||
game.livesAmount += 1;
|
||||
break;
|
||||
case 'clone':
|
||||
// Основной мяч делится на два клона, разлетающихся под углом
|
||||
game.balls.push(clone(game.ball, BALL_SPLIT_ANGLE), clone(game.ball, -BALL_SPLIT_ANGLE));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
perk.alive = false;
|
||||
} else if (perk.y > containerHeight) {
|
||||
perk.alive = false;
|
||||
}
|
||||
}
|
||||
|
||||
game.perks = game.perks.filter((perk) => perk.alive);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { PADDLE_WIDE_WIDTH } from '../../config';
|
||||
import { Perk } from '../../entities/perk/perk';
|
||||
import { Game } from '../../game';
|
||||
import { updatePerks } from './updatePerks';
|
||||
|
||||
describe('updatePerks', () => {
|
||||
it('Возвращает корректное значение при неверных типах аргументов', () => {
|
||||
const game = new Game([[[1]]]);
|
||||
|
||||
expect(updatePerks(null, 100, 1)).toBeNull();
|
||||
expect(updatePerks(game, null, 1)).toBeNull();
|
||||
expect(updatePerks(game, -1, 1)).toBeNull();
|
||||
expect(updatePerks(game, 100, null)).toBeNull();
|
||||
expect(updatePerks(game, 100, -1)).toBeNull();
|
||||
});
|
||||
|
||||
it('Двигает бонус вниз в зависимости от времени', () => {
|
||||
const game = new Game([[[1]]]);
|
||||
const perk = new Perk(500, 10, 10, 10, 'slow', 3);
|
||||
game.perks.push(perk);
|
||||
|
||||
updatePerks(game, 600, 1);
|
||||
|
||||
expect(perk.y).toBe(13);
|
||||
expect(game.perks).toContain(perk);
|
||||
});
|
||||
|
||||
it('Ловит бонус ракеткой, применяет эффект и убирает его', () => {
|
||||
const game = new Game([[[1]]]);
|
||||
const speedBefore = game.ball.speed;
|
||||
const { paddle } = game;
|
||||
game.perks.push(new Perk(paddle.x, paddle.y - 1, 10, 10, 'slow', 0));
|
||||
|
||||
updatePerks(game, 600, 1);
|
||||
|
||||
expect(game.perks).toHaveLength(0);
|
||||
expect(game.ball.speed).toBeLessThan(speedBefore);
|
||||
});
|
||||
|
||||
it('Ловит бонус wide, расширяет ракетку до фиксированной ширины', () => {
|
||||
const game = new Game([[[1]]]);
|
||||
const { paddle } = game;
|
||||
game.perks.push(new Perk(paddle.x, paddle.y - 1, 10, 10, 'wide', 0));
|
||||
|
||||
updatePerks(game, 600, 1);
|
||||
|
||||
expect(game.perks).toHaveLength(0);
|
||||
expect(paddle.width).toBe(PADDLE_WIDE_WIDTH);
|
||||
});
|
||||
|
||||
it('Ловит бонус clone, разделяет мяч на три', () => {
|
||||
const game = new Game([[[1]]]);
|
||||
const { paddle } = game;
|
||||
game.perks.push(new Perk(paddle.x, paddle.y - 1, 10, 10, 'clone', 0));
|
||||
|
||||
updatePerks(game, 600, 1);
|
||||
|
||||
expect(game.perks).toHaveLength(0);
|
||||
expect(game.balls).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('Убирает бонус, улетевший за нижнюю границу', () => {
|
||||
const game = new Game([[[1]]]);
|
||||
game.perks.push(new Perk(0, 601, 10, 10, 'slow', 0));
|
||||
|
||||
updatePerks(game, 600, 1);
|
||||
|
||||
expect(game.perks).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
+13
-6
@@ -13,10 +13,9 @@ import {
|
||||
PADDLE_HEIGHT,
|
||||
PADDLE_WIDTH,
|
||||
} from './config';
|
||||
import { LEVELS } from './const/levels';
|
||||
import { Game } from './game';
|
||||
import { calculateCollision } from './lib/calculateCollision/calculateCollision';
|
||||
import { calculateDirection } from './lib/calculateDirection/calculateDirection';
|
||||
import { createGameView, syncronizeViewsWithGame } from './view';
|
||||
import { createGameView, managePerkViewsLifetime, rebuildBrickViews, syncronizeViewsWithGame } from './view';
|
||||
|
||||
(async () => {
|
||||
// Create a new application
|
||||
@@ -39,19 +38,27 @@ import { createGameView, syncronizeViewsWithGame } from './view';
|
||||
|
||||
app.stage.addChild(container);
|
||||
|
||||
const game = new Game(BRICK_COLUMN_AMOUNT, BRICK_ROW_AMOUNT);
|
||||
const game = new Game(LEVELS);
|
||||
const views = createGameView(game, container);
|
||||
let currentLevel = game.currentLevel;
|
||||
|
||||
container.on('pointermove', (event) => {
|
||||
const localPosition = container.toLocal(event.global);
|
||||
game.paddle.moveTo(localPosition.x, 0, CONTAINER_WIDTH);
|
||||
});
|
||||
|
||||
console.log(game.paddle.x, game.paddle.y);
|
||||
container.on('pointerdown', () => {
|
||||
game.ball.launch();
|
||||
});
|
||||
|
||||
app.ticker.add((time) => {
|
||||
game.update(time.deltaTime);
|
||||
|
||||
if (game.currentLevel !== currentLevel) {
|
||||
rebuildBrickViews(views, game, container);
|
||||
currentLevel = game.currentLevel;
|
||||
}
|
||||
managePerkViewsLifetime(views, game, container);
|
||||
syncronizeViewsWithGame(views, game);
|
||||
console.log(game.paddle.x, game.paddle.y);
|
||||
});
|
||||
})();
|
||||
|
||||
+131
-21
@@ -2,6 +2,7 @@ import { Container, Graphics } from 'pixi.js';
|
||||
import { Ball } from './entities/ball/ball';
|
||||
import { Brick } from './entities/brick/brick';
|
||||
import { Paddle } from './entities/paddle/paddle';
|
||||
import { Perk } from './entities/perk/perk';
|
||||
import { Game } from './game';
|
||||
|
||||
/**
|
||||
@@ -23,36 +24,72 @@ function createPaddleView(paddle) {
|
||||
/**
|
||||
* Создает визуальное отображение кирпича с помощью Pixi.js
|
||||
* @param {Brick} brick экземпляр класса кирпич
|
||||
* @returns {Graphics} графическое отображение кирпича
|
||||
*/
|
||||
function createBrickView(brick) {
|
||||
return new Graphics().rect(0, 0, brick.width, brick.height).fill('#000fff');
|
||||
const brickView = new Graphics().rect(0, 0, brick.width, brick.height);
|
||||
|
||||
switch (brick.type) {
|
||||
case 2:
|
||||
return brickView.fill('#00ff00');
|
||||
case 3:
|
||||
return brickView.fill('#ff00ff');
|
||||
case 1:
|
||||
default:
|
||||
return brickView.fill('#000fff');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает визуальное отображение бонуса с помощью Pixi.js
|
||||
* @param {Perk} perk экземпляр класса бонус
|
||||
* @returns {Graphics} графическое отображение бонуса
|
||||
*/
|
||||
function createPerkView(perk) {
|
||||
const perkView = new Graphics().rect(0, 0, perk.width, perk.height);
|
||||
|
||||
switch (perk.type) {
|
||||
case 'slow':
|
||||
return perkView.fill('#00ffff');
|
||||
case 'wide':
|
||||
return perkView.fill('#0f0f0f');
|
||||
case 'life':
|
||||
return perkView.fill('#f0f0f0');
|
||||
default:
|
||||
return perkView.fill('#ffffff');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает визуальное отображение всех сущностей в игре с помощью Pixi.js и добавляет в контейнер
|
||||
* @param {Game} game экземпляр класса игра
|
||||
* @param {Container} container контейнер Pixi.js
|
||||
* @param {Container} container экземпляр класса контейнер Pixi.js
|
||||
*/
|
||||
export function createGameView(game, container) {
|
||||
try {
|
||||
const ball = createBallView(game.ball);
|
||||
container.addChild(ball);
|
||||
const balls = new Map();
|
||||
for (const ball of game.balls) {
|
||||
const ballView = createBallView(ball);
|
||||
container.addChild(ballView);
|
||||
balls.set(ball, ballView);
|
||||
}
|
||||
|
||||
const paddle = createPaddleView(game.paddle);
|
||||
container.addChild(paddle);
|
||||
|
||||
const bricks = game.bricks.map((row) =>
|
||||
row.map((brick) => {
|
||||
const brickView = createBrickView(brick);
|
||||
container.addChild(brickView);
|
||||
return brickView;
|
||||
}),
|
||||
);
|
||||
const bricks = game.bricks.map((brick) => {
|
||||
const brickView = createBrickView(brick);
|
||||
container.addChild(brickView);
|
||||
return brickView;
|
||||
});
|
||||
|
||||
const perks = new Map();
|
||||
|
||||
return {
|
||||
ball,
|
||||
balls,
|
||||
paddle,
|
||||
bricks,
|
||||
perks,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -60,6 +97,53 @@ export function createGameView(game, container) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает отображения для новых бонусов и удаляет отображения исчезнувших
|
||||
* @param {object} views объект с визуальными отображениями сущностей игры
|
||||
* @param {Game} game экземпляр класса игра
|
||||
* @param {Container} container экземпляр класса контейнер Pixi.js
|
||||
*/
|
||||
export function managePerkViewsLifetime(views, game, container) {
|
||||
if (views.paddle.width !== game.paddle.width) {
|
||||
container.removeChild(views.paddle);
|
||||
views.paddle.destroy();
|
||||
views.paddle = createPaddleView(game.paddle);
|
||||
container.addChild(views.paddle);
|
||||
}
|
||||
|
||||
for (const [ball, ballView] of views.balls) {
|
||||
if (!game.balls.includes(ball)) {
|
||||
container.removeChild(ballView);
|
||||
ballView.destroy();
|
||||
views.balls.delete(ball);
|
||||
}
|
||||
}
|
||||
|
||||
for (const ball of game.balls) {
|
||||
if (!views.balls.has(ball)) {
|
||||
const ballView = createBallView(ball);
|
||||
container.addChild(ballView);
|
||||
views.balls.set(ball, ballView);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [perk, perkView] of views.perks) {
|
||||
if (!game.perks.includes(perk)) {
|
||||
container.removeChild(perkView);
|
||||
perkView.destroy();
|
||||
views.perks.delete(perk);
|
||||
}
|
||||
}
|
||||
|
||||
for (const perk of game.perks) {
|
||||
if (!views.perks.has(perk)) {
|
||||
const perkView = createPerkView(perk);
|
||||
container.addChild(perkView);
|
||||
views.perks.set(perk, perkView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Синхронизирует отображение сущностей Pixi.js с логикой игры (координаты и тд.)
|
||||
* @param {object} views объект с визуальными отображениями сущностей игры
|
||||
@@ -71,22 +155,48 @@ export function syncronizeViewsWithGame(views, game) {
|
||||
throw new Error('Аргумент game должен быть экземпляром класса Game');
|
||||
}
|
||||
|
||||
views.ball.x = game.ball.x;
|
||||
views.ball.y = game.ball.y;
|
||||
for (const ball of game.balls) {
|
||||
const ballView = views.balls.get(ball);
|
||||
ballView.x = ball.x;
|
||||
ballView.y = ball.y;
|
||||
}
|
||||
|
||||
views.paddle.x = game.paddle.x;
|
||||
views.paddle.y = game.paddle.y;
|
||||
|
||||
for (let i = 0; i < game.bricks.length; i++) {
|
||||
for (let j = 0; j < game.bricks[i].length; j++) {
|
||||
const brick = game.bricks[i][j];
|
||||
const brickView = views.bricks[i][j];
|
||||
brickView.x = brick.x;
|
||||
brickView.y = brick.y;
|
||||
brickView.visible = brick.alive;
|
||||
}
|
||||
const brick = game.bricks[i];
|
||||
const brickView = views.bricks[i];
|
||||
brickView.x = brick.x;
|
||||
brickView.y = brick.y;
|
||||
brickView.visible = brick.alive;
|
||||
}
|
||||
|
||||
for (const perk of game.perks) {
|
||||
const perkView = views.perks.get(perk);
|
||||
perkView.x = perk.x;
|
||||
perkView.y = perk.y;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Пересоздает отображения кирпичей под текущий уровень игры.
|
||||
* @param {object} views объект с визуальными отображениями сущностей игры
|
||||
* @param {Game} game экземпляр класса игра
|
||||
* @param {Container} container экземпляр класса контейнер Pixi.js
|
||||
*/
|
||||
export function rebuildBrickViews(views, game, container) {
|
||||
for (const brickView of views.bricks) {
|
||||
container.removeChild(brickView);
|
||||
brickView.destroy();
|
||||
}
|
||||
|
||||
views.bricks = game.bricks.map((brick) => {
|
||||
const brickView = createBrickView(brick);
|
||||
container.addChild(brickView);
|
||||
return brickView;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user