Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7ca56c99c | ||
|
|
b72583290d | ||
|
|
a734600390 | ||
|
|
dccdb02f73 | ||
|
|
e637d4e62e | ||
|
|
d2cfee3ec3 | ||
|
|
8d15f3996f | ||
|
|
b7e16f86ea | ||
|
|
511b20c939 | ||
|
|
a4c4e2a192 | ||
|
|
b2f0810ec2 | ||
|
|
b05fb89bc5 | ||
|
|
62abd8b77d | ||
|
|
0f057c1add | ||
|
|
e7b52bdbd9 | ||
|
|
500803db84 | ||
|
|
5e259f7efc |
@@ -2,6 +2,7 @@ export const CONTAINER_WIDTH = 800;
|
|||||||
export const CONTAINER_HEIGHT = 600;
|
export const CONTAINER_HEIGHT = 600;
|
||||||
|
|
||||||
export const PADDLE_WIDTH = 50;
|
export const PADDLE_WIDTH = 50;
|
||||||
|
export const PADDLE_WIDE_WIDTH = 100;
|
||||||
export const PADDLE_HEIGHT = 10;
|
export const PADDLE_HEIGHT = 10;
|
||||||
export const MIN_PADDLE_REFLECTION_ANGLE = 20;
|
export const MIN_PADDLE_REFLECTION_ANGLE = 20;
|
||||||
export const MAX_PADDLE_REFLECTION_ANGLE = 160;
|
export const MAX_PADDLE_REFLECTION_ANGLE = 160;
|
||||||
@@ -9,10 +10,21 @@ export const PADDLE_SECTOR_AMOUNT = 20;
|
|||||||
|
|
||||||
export const BALL_RADIUS = 10;
|
export const BALL_RADIUS = 10;
|
||||||
export const BALL_SPEED = 3;
|
export const BALL_SPEED = 3;
|
||||||
|
export const BALL_SPEED_LEVEL_STEP = 2;
|
||||||
|
export const BALL_SPEED_TIME_STEP = 0.002;
|
||||||
export const BALL_INITIAL_ANGLE = 0;
|
export const BALL_INITIAL_ANGLE = 0;
|
||||||
|
export const BALL_SPLIT_ANGLE = 20;
|
||||||
|
|
||||||
export const BRICK_WIDTH = 40;
|
export const BRICK_WIDTH = 40;
|
||||||
export const BRICK_HEIGHT = 10;
|
export const BRICK_HEIGHT = 10;
|
||||||
|
|
||||||
export const BRICK_ROW_AMOUNT = 5;
|
export const BRICK_ROW_AMOUNT = 5;
|
||||||
export const BRICK_COLUMN_AMOUNT = 20;
|
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;
|
||||||
|
|||||||
+43
-12
@@ -1,3 +1,5 @@
|
|||||||
|
import { toRadians } from '../../lib/toRadians/toRadians';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Класс сущности "мяч"
|
* Класс сущности "мяч"
|
||||||
* @class
|
* @class
|
||||||
@@ -7,21 +9,23 @@ export class Ball {
|
|||||||
* @param {number} x координата положения мяча по оси X
|
* @param {number} x координата положения мяча по оси X
|
||||||
* @param {number} y координата положения мяча по оси Y
|
* @param {number} y координата положения мяча по оси Y
|
||||||
* @param {number} radius положительное числовое значение радиуса мяча
|
* @param {number} radius положительное числовое значение радиуса мяча
|
||||||
* @param {number} verticalSpeed вектор движения мяча по оси Y
|
* @param {number} speed положительное числовое значение скорости мяча
|
||||||
* @param {number} horizontalSpeed вектор движения мяча по оси X
|
* @param {number} angle угол направления движения мяча в градусах
|
||||||
*/
|
*/
|
||||||
constructor(x, y, radius, verticalSpeed = 0, horizontalSpeed = 0) {
|
constructor(x, y, radius, speed = 0, angle = 0) {
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
this.radius = radius;
|
this.radius = radius;
|
||||||
this.verticalSpeed = verticalSpeed;
|
this.speed = speed;
|
||||||
this.horizontalSpeed = horizontalSpeed;
|
this.angle = angle;
|
||||||
this.isOut = false;
|
this.verticalSpeed = speed * Math.cos(toRadians(angle));
|
||||||
|
this.horizontalSpeed = -1 * speed * Math.sin(toRadians(angle));
|
||||||
|
this.status = 'idle';
|
||||||
|
|
||||||
this.defaultX = x;
|
this.defaultX = x;
|
||||||
this.defaultY = y;
|
this.defaultY = y;
|
||||||
this.defaultVerticalSpeed = verticalSpeed;
|
this.defaultVerticalSpeed = this.verticalSpeed;
|
||||||
this.defaultHorizontalSpeed = horizontalSpeed;
|
this.defaultHorizontalSpeed = this.horizontalSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,11 +40,38 @@ export class Ball {
|
|||||||
/**
|
/**
|
||||||
* Возвращает значения к дефолтным
|
* Возвращает значения к дефолтным
|
||||||
*/
|
*/
|
||||||
reset() {
|
reset(x = this.defaultX, y = this.defaultY) {
|
||||||
this.x = this.defaultX;
|
this.x = x;
|
||||||
this.y = this.defaultY;
|
this.y = y;
|
||||||
this.verticalSpeed = this.defaultVerticalSpeed;
|
this.verticalSpeed = this.defaultVerticalSpeed;
|
||||||
this.horizontalSpeed = this.defaultHorizontalSpeed;
|
this.horizontalSpeed = this.defaultHorizontalSpeed;
|
||||||
this.isOut = false;
|
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} y координата положения кирпича по оси Y
|
||||||
* @param {number} width положительное числовое значение ширины кирпича
|
* @param {number} width положительное числовое значение ширины кирпича
|
||||||
* @param {number} height положительное числовое значение высоты кирпича
|
* @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.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
|
this.type = type;
|
||||||
this.alive = true;
|
this.alive = true;
|
||||||
|
this.livesAmount = this.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* В зависимости от типа и оставшихся жизней убивает кирпич
|
||||||
|
*/
|
||||||
kill() {
|
kill() {
|
||||||
this.alive = false;
|
if (this.type !== 3) {
|
||||||
|
this.livesAmount -= 1;
|
||||||
|
|
||||||
|
if (this.livesAmount === 0) {
|
||||||
|
this.alive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function layBricks(levelMap, brickWidth, brickHeight) {
|
|||||||
for (let i = 0; i < levelMap.length; i++) {
|
for (let i = 0; i < levelMap.length; i++) {
|
||||||
for (let j = 0; j < levelMap[i].length; j++) {
|
for (let j = 0; j < levelMap[i].length; j++) {
|
||||||
if (levelMap[i][j] !== 0) {
|
if (levelMap[i][j] !== 0) {
|
||||||
bricks.push(new Brick(j * brickWidth, i * brickHeight, brickWidth, brickHeight));
|
bricks.push(new Brick(j * brickWidth, i * brickHeight, brickWidth, brickHeight, levelMap[i][j]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,4 +65,26 @@ describe('layBricks', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
-13
@@ -2,18 +2,20 @@ import {
|
|||||||
BALL_INITIAL_ANGLE,
|
BALL_INITIAL_ANGLE,
|
||||||
BALL_RADIUS,
|
BALL_RADIUS,
|
||||||
BALL_SPEED,
|
BALL_SPEED,
|
||||||
|
BALL_SPEED_LEVEL_STEP,
|
||||||
|
BALL_SPEED_TIME_STEP,
|
||||||
BRICK_HEIGHT,
|
BRICK_HEIGHT,
|
||||||
BRICK_WIDTH,
|
BRICK_WIDTH,
|
||||||
CONTAINER_HEIGHT,
|
CONTAINER_HEIGHT,
|
||||||
CONTAINER_WIDTH,
|
CONTAINER_WIDTH,
|
||||||
PADDLE_HEIGHT,
|
PADDLE_HEIGHT,
|
||||||
PADDLE_WIDTH,
|
PADDLE_WIDTH,
|
||||||
|
PERK_BALL_SPEED_DECREASE,
|
||||||
} from './config';
|
} from './config';
|
||||||
import { Ball } from './entities/ball/ball';
|
import { Ball } from './entities/ball/ball';
|
||||||
import { layBricks } from './entities/brick/layBricks/layBricks';
|
import { layBricks } from './entities/brick/layBricks/layBricks';
|
||||||
import { Paddle } from './entities/paddle/paddle';
|
import { Paddle } from './entities/paddle/paddle';
|
||||||
import { tick } from './lib/tick/tick';
|
import { tick } from './lib/tick/tick';
|
||||||
import { toRadians } from './lib/toRadians/toRadians';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Класс игры c информацией о всех игровых сущностях
|
* Класс игры c информацией о всех игровых сущностях
|
||||||
@@ -29,15 +31,20 @@ export class Game {
|
|||||||
this.currentLevel = 0;
|
this.currentLevel = 0;
|
||||||
this.maxLevel = levels.length - 1;
|
this.maxLevel = levels.length - 1;
|
||||||
|
|
||||||
this.ball = new Ball(
|
|
||||||
CONTAINER_WIDTH / 2 - BALL_RADIUS / 2,
|
|
||||||
CONTAINER_HEIGHT / 2 - BALL_RADIUS / 2,
|
|
||||||
BALL_RADIUS,
|
|
||||||
BALL_SPEED * Math.cos(toRadians(BALL_INITIAL_ANGLE)),
|
|
||||||
-1 * BALL_SPEED * Math.sin(toRadians(BALL_INITIAL_ANGLE)),
|
|
||||||
);
|
|
||||||
this.paddle = new Paddle(0, CONTAINER_HEIGHT - PADDLE_HEIGHT, PADDLE_WIDTH, PADDLE_HEIGHT);
|
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.bricks = layBricks(this.levels[this.currentLevel], BRICK_WIDTH, BRICK_HEIGHT);
|
||||||
|
this.perks = [];
|
||||||
|
|
||||||
|
this._placeBallOnPaddle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Основной (первый) мяч. Пока играет один мяч - это он.
|
||||||
|
* @returns {Ball}
|
||||||
|
*/
|
||||||
|
get ball() {
|
||||||
|
return this.balls[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,23 +56,38 @@ export class Game {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Пока мяч не запущен - держим его на ракетке и не считаем физику
|
||||||
|
if (this.ball.status === 'idle') {
|
||||||
|
this._placeBallOnPaddle();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
tick(this, CONTAINER_WIDTH, CONTAINER_HEIGHT, deltaTime);
|
tick(this, CONTAINER_WIDTH, CONTAINER_HEIGHT, deltaTime);
|
||||||
|
|
||||||
if (this.ball.isOut) {
|
// Убираем улетевшие мячи. Жизнь теряется только когда не осталось ни одного
|
||||||
|
const survivingBalls = this.balls.filter((ball) => ball.status !== 'out');
|
||||||
|
|
||||||
|
if (survivingBalls.length === 0) {
|
||||||
this.livesAmount -= 1;
|
this.livesAmount -= 1;
|
||||||
|
|
||||||
if (this.livesAmount === 0) {
|
if (this.livesAmount === 0) {
|
||||||
this.status = 'over';
|
this.status = 'over';
|
||||||
} else {
|
} else {
|
||||||
this.ball.reset();
|
this._placeBallOnPaddle();
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAnyBrickAlive = this.bricks.some((brick) => brick.alive);
|
this.balls = survivingBalls;
|
||||||
|
|
||||||
if (!isAnyBrickAlive) {
|
for (const ball of this.balls) {
|
||||||
|
ball.increaseSpeed(BALL_SPEED_TIME_STEP * deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLevelComplete = this._checkLevelCompletion();
|
||||||
|
|
||||||
|
if (isLevelComplete) {
|
||||||
this.currentLevel += 1;
|
this.currentLevel += 1;
|
||||||
|
|
||||||
if (this.currentLevel <= this.maxLevel) {
|
if (this.currentLevel <= this.maxLevel) {
|
||||||
@@ -76,11 +98,28 @@ export class Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет завершен ли текущий уровень
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
_checkLevelCompletion() {
|
||||||
|
return this.bricks.every((brick) => brick.type === 3 || !brick.alive);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Запускает переход на новый уровень, возвращает мяч в дефоотное положение и отрисовывает кирпичи по карте уровня.
|
* Запускает переход на новый уровень, возвращает мяч в дефоотное положение и отрисовывает кирпичи по карте уровня.
|
||||||
*/
|
*/
|
||||||
_proceedToNextLevel() {
|
_proceedToNextLevel() {
|
||||||
this.ball.reset();
|
this.ball.increaseSpeed(BALL_SPEED_LEVEL_STEP);
|
||||||
|
this._placeBallOnPaddle();
|
||||||
this.bricks = layBricks(this.levels[this.currentLevel], BRICK_WIDTH, BRICK_HEIGHT);
|
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 координата второго объекта
|
* @param {number} secondObject.bottom - Max Y координата второго объекта
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function calculateCollision(firstObject, secondObject) {
|
export function calculateAABBCollision(firstObject, secondObject) {
|
||||||
try {
|
try {
|
||||||
const coordinates = [
|
const coordinates = [
|
||||||
firstObject.left,
|
firstObject.left,
|
||||||
+9
-9
@@ -1,6 +1,6 @@
|
|||||||
import { calculateCollision } from './calculateCollision';
|
import { calculateAABBCollision } from './calculateAABBCollision';
|
||||||
|
|
||||||
describe('calculateCollision', () => {
|
describe('calculateAABBCollision', () => {
|
||||||
it('Корректно обрабатывает невозможные кейсы (левая координата больше правой)', () => {
|
it('Корректно обрабатывает невозможные кейсы (левая координата больше правой)', () => {
|
||||||
const firstObject = {
|
const firstObject = {
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -16,7 +16,7 @@ describe('calculateCollision', () => {
|
|||||||
bottom: 10,
|
bottom: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(calculateCollision(firstObject, secondObject)).toBeNull();
|
expect(calculateAABBCollision(firstObject, secondObject)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Корректно обрабатывает неверный формат данных', () => {
|
it('Корректно обрабатывает неверный формат данных', () => {
|
||||||
@@ -34,7 +34,7 @@ describe('calculateCollision', () => {
|
|||||||
bottom: 10,
|
bottom: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(calculateCollision(firstObject, secondObject)).toBeNull();
|
expect(calculateAABBCollision(firstObject, secondObject)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Корректно обрабатывает отсутствие пересечения по X', () => {
|
it('Корректно обрабатывает отсутствие пересечения по X', () => {
|
||||||
@@ -52,7 +52,7 @@ describe('calculateCollision', () => {
|
|||||||
bottom: 10,
|
bottom: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(calculateCollision(firstObject, secondObject)).toBe(false);
|
expect(calculateAABBCollision(firstObject, secondObject)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Корректно обрабатывает отсутствие пересечения по Y', () => {
|
it('Корректно обрабатывает отсутствие пересечения по Y', () => {
|
||||||
@@ -70,7 +70,7 @@ describe('calculateCollision', () => {
|
|||||||
bottom: 30,
|
bottom: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(calculateCollision(firstObject, secondObject)).toBe(false);
|
expect(calculateAABBCollision(firstObject, secondObject)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Корректно обрабатывает отсутствие пересечения по X и Y', () => {
|
it('Корректно обрабатывает отсутствие пересечения по X и Y', () => {
|
||||||
@@ -88,7 +88,7 @@ describe('calculateCollision', () => {
|
|||||||
bottom: 30,
|
bottom: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(calculateCollision(firstObject, secondObject)).toBe(false);
|
expect(calculateAABBCollision(firstObject, secondObject)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Корректно обрабатывает пересечение', () => {
|
it('Корректно обрабатывает пересечение', () => {
|
||||||
@@ -106,7 +106,7 @@ describe('calculateCollision', () => {
|
|||||||
bottom: 30,
|
bottom: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(calculateCollision(firstObject, secondObject)).toBe(true);
|
expect(calculateAABBCollision(firstObject, secondObject)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Корректно обрабатывает вхождение', () => {
|
it('Корректно обрабатывает вхождение', () => {
|
||||||
@@ -124,6 +124,6 @@ describe('calculateCollision', () => {
|
|||||||
bottom: 15,
|
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 множителей для изменения координат
|
* Вычисляет направление наибольшего пересечения по осям и возвращает tuple множителей для изменения координат
|
||||||
@@ -17,7 +17,7 @@ import { calculateCollision } from '../calculateCollision/calculateCollision';
|
|||||||
export function calculateDirection(firstObject, secondObject) {
|
export function calculateDirection(firstObject, secondObject) {
|
||||||
try {
|
try {
|
||||||
// Запускаем для проверки формата аргументов
|
// Запускаем для проверки формата аргументов
|
||||||
const isCollided = calculateCollision(firstObject, secondObject);
|
const isCollided = calculateAABBCollision(firstObject, secondObject);
|
||||||
|
|
||||||
if (isCollided === null) {
|
if (isCollided === null) {
|
||||||
return 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,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);
|
||||||
|
});
|
||||||
|
});
|
||||||
+26
-72
@@ -1,15 +1,9 @@
|
|||||||
import {
|
import { MAX_PADDLE_REFLECTION_ANGLE, MIN_PADDLE_REFLECTION_ANGLE, PADDLE_SECTOR_AMOUNT } from '../../config';
|
||||||
BALL_SPEED,
|
|
||||||
CONTAINER_HEIGHT,
|
|
||||||
CONTAINER_WIDTH,
|
|
||||||
MAX_PADDLE_REFLECTION_ANGLE,
|
|
||||||
MIN_PADDLE_REFLECTION_ANGLE,
|
|
||||||
PADDLE_SECTOR_AMOUNT,
|
|
||||||
} from '../../config';
|
|
||||||
import { Game } from '../../game';
|
import { Game } from '../../game';
|
||||||
import { calculateCollision } from '../calculateCollision/calculateCollision';
|
import { processBall } from '../processBall/processBall';
|
||||||
import { calculateDirection } from '../calculateDirection/calculateDirection';
|
import { processBrickCollision } from '../processBrickCollision/processBrickCollision';
|
||||||
import { processReflection } from '../processReflection/processReflection';
|
import { processReflection } from '../processReflection/processReflection';
|
||||||
|
import { updatePerks } from '../updatePerks/updatePerks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Функция для вычисления взаимодействий сущностей игры в зависимости от времени из Ticker
|
* Функция для вычисления взаимодействий сущностей игры в зависимости от времени из Ticker
|
||||||
@@ -36,77 +30,37 @@ export function tick(game, containerWidth, containerHeight, deltaTime) {
|
|||||||
throw new Error('Значение изменения времени должно быть положительным числом');
|
throw new Error('Значение изменения времени должно быть положительным числом');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { ball, paddle, bricks } = game;
|
const { paddle } = game;
|
||||||
|
|
||||||
ball.moveForward(deltaTime);
|
// Бонусы падают и ловятся ракеткой каждый кадр
|
||||||
|
updatePerks(game, containerHeight, deltaTime);
|
||||||
|
|
||||||
const leftBoundary = ball.radius;
|
for (const ball of game.balls) {
|
||||||
const rightBoundary = containerWidth - ball.radius;
|
ball.moveForward(deltaTime);
|
||||||
const topBoundary = ball.radius;
|
// Проверка столкновения со стенами и вылета за границу поля снизу
|
||||||
const bottomBoundary = containerHeight - ball.radius;
|
const hitWallOrWentOut = processBall(ball, containerWidth, containerHeight);
|
||||||
|
|
||||||
// Не даем мячу выйти за границы стен слева / справав и меняем направление
|
if (hitWallOrWentOut) {
|
||||||
if (ball.x <= leftBoundary || ball.x >= rightBoundary) {
|
|
||||||
ball.x = ball.x <= leftBoundary ? leftBoundary : rightBoundary;
|
|
||||||
ball.horizontalSpeed *= -1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Не даем мячу выйти за границу стены сверху и меняем направление
|
|
||||||
if (ball.y <= topBoundary) {
|
|
||||||
ball.y = topBoundary;
|
|
||||||
ball.verticalSpeed *= -1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем выход за границу стены снизу
|
|
||||||
if (ball.y >= bottomBoundary) {
|
|
||||||
game.ball.isOut = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Базоввое взаимодействие мяча и кирпича
|
|
||||||
for (const brick of bricks) {
|
|
||||||
if (!brick.alive) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ballLeft = ball.x - ball.radius;
|
// Проверка столкновения с кирпичами
|
||||||
const ballRight = ball.x + ball.radius;
|
const hitBrick = processBrickCollision(game, ball);
|
||||||
const ballTop = ball.y - ball.radius;
|
|
||||||
const ballBottom = ball.y + ball.radius;
|
|
||||||
|
|
||||||
const brickLeft = brick.x;
|
if (hitBrick) {
|
||||||
const brickRight = brick.x + brick.width;
|
continue;
|
||||||
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();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Взаимодействие мяча и ракетки - обновление горизонтальной и вертикальной скорости в зависимости от сектора попадания
|
// Взаимодействие мяча и ракетки - обновление горизонтальной и вертикальной скорости в зависимости от сектора попадания
|
||||||
processReflection(
|
processReflection(
|
||||||
paddle,
|
paddle,
|
||||||
ball,
|
ball,
|
||||||
PADDLE_SECTOR_AMOUNT,
|
PADDLE_SECTOR_AMOUNT,
|
||||||
MIN_PADDLE_REFLECTION_ANGLE,
|
MIN_PADDLE_REFLECTION_ANGLE,
|
||||||
MAX_PADDLE_REFLECTION_ANGLE,
|
MAX_PADDLE_REFLECTION_ANGLE,
|
||||||
BALL_SPEED,
|
ball.speed,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ponytail: wall-clock timer, game never pauses so no need to tie to deltaTime
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
+11
-5
@@ -15,9 +15,7 @@ import {
|
|||||||
} from './config';
|
} from './config';
|
||||||
import { LEVELS } from './const/levels';
|
import { LEVELS } from './const/levels';
|
||||||
import { Game } from './game';
|
import { Game } from './game';
|
||||||
import { calculateCollision } from './lib/calculateCollision/calculateCollision';
|
import { createGameView, managePerkViewsLifetime, rebuildBrickViews, syncronizeViewsWithGame } from './view';
|
||||||
import { calculateDirection } from './lib/calculateDirection/calculateDirection';
|
|
||||||
import { createGameView, syncronizeViewsWithGame } from './view';
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
// Create a new application
|
// Create a new application
|
||||||
@@ -42,17 +40,25 @@ import { createGameView, syncronizeViewsWithGame } from './view';
|
|||||||
|
|
||||||
const game = new Game(LEVELS);
|
const game = new Game(LEVELS);
|
||||||
const views = createGameView(game, container);
|
const views = createGameView(game, container);
|
||||||
|
let currentLevel = game.currentLevel;
|
||||||
|
|
||||||
container.on('pointermove', (event) => {
|
container.on('pointermove', (event) => {
|
||||||
const localPosition = container.toLocal(event.global);
|
const localPosition = container.toLocal(event.global);
|
||||||
game.paddle.moveTo(localPosition.x, 0, CONTAINER_WIDTH);
|
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) => {
|
app.ticker.add((time) => {
|
||||||
game.update(time.deltaTime);
|
game.update(time.deltaTime);
|
||||||
|
|
||||||
|
if (game.currentLevel !== currentLevel) {
|
||||||
|
rebuildBrickViews(views, game, container);
|
||||||
|
currentLevel = game.currentLevel;
|
||||||
|
}
|
||||||
|
managePerkViewsLifetime(views, game, container);
|
||||||
syncronizeViewsWithGame(views, game);
|
syncronizeViewsWithGame(views, game);
|
||||||
console.log(game.paddle.x, game.paddle.y);
|
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
+121
-7
@@ -2,6 +2,7 @@ import { Container, Graphics } from 'pixi.js';
|
|||||||
import { Ball } from './entities/ball/ball';
|
import { Ball } from './entities/ball/ball';
|
||||||
import { Brick } from './entities/brick/brick';
|
import { Brick } from './entities/brick/brick';
|
||||||
import { Paddle } from './entities/paddle/paddle';
|
import { Paddle } from './entities/paddle/paddle';
|
||||||
|
import { Perk } from './entities/perk/perk';
|
||||||
import { Game } from './game';
|
import { Game } from './game';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,20 +24,55 @@ function createPaddleView(paddle) {
|
|||||||
/**
|
/**
|
||||||
* Создает визуальное отображение кирпича с помощью Pixi.js
|
* Создает визуальное отображение кирпича с помощью Pixi.js
|
||||||
* @param {Brick} brick экземпляр класса кирпич
|
* @param {Brick} brick экземпляр класса кирпич
|
||||||
|
* @returns {Graphics} графическое отображение кирпича
|
||||||
*/
|
*/
|
||||||
function createBrickView(brick) {
|
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 и добавляет в контейнер
|
* Создает визуальное отображение всех сущностей в игре с помощью Pixi.js и добавляет в контейнер
|
||||||
* @param {Game} game экземпляр класса игра
|
* @param {Game} game экземпляр класса игра
|
||||||
* @param {Container} container контейнер Pixi.js
|
* @param {Container} container экземпляр класса контейнер Pixi.js
|
||||||
*/
|
*/
|
||||||
export function createGameView(game, container) {
|
export function createGameView(game, container) {
|
||||||
try {
|
try {
|
||||||
const ball = createBallView(game.ball);
|
const balls = new Map();
|
||||||
container.addChild(ball);
|
for (const ball of game.balls) {
|
||||||
|
const ballView = createBallView(ball);
|
||||||
|
container.addChild(ballView);
|
||||||
|
balls.set(ball, ballView);
|
||||||
|
}
|
||||||
|
|
||||||
const paddle = createPaddleView(game.paddle);
|
const paddle = createPaddleView(game.paddle);
|
||||||
container.addChild(paddle);
|
container.addChild(paddle);
|
||||||
@@ -47,10 +83,13 @@ export function createGameView(game, container) {
|
|||||||
return brickView;
|
return brickView;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const perks = new Map();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ball,
|
balls,
|
||||||
paddle,
|
paddle,
|
||||||
bricks,
|
bricks,
|
||||||
|
perks,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -58,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 с логикой игры (координаты и тд.)
|
* Синхронизирует отображение сущностей Pixi.js с логикой игры (координаты и тд.)
|
||||||
* @param {object} views объект с визуальными отображениями сущностей игры
|
* @param {object} views объект с визуальными отображениями сущностей игры
|
||||||
@@ -69,8 +155,11 @@ export function syncronizeViewsWithGame(views, game) {
|
|||||||
throw new Error('Аргумент game должен быть экземпляром класса Game');
|
throw new Error('Аргумент game должен быть экземпляром класса Game');
|
||||||
}
|
}
|
||||||
|
|
||||||
views.ball.x = game.ball.x;
|
for (const ball of game.balls) {
|
||||||
views.ball.y = game.ball.y;
|
const ballView = views.balls.get(ball);
|
||||||
|
ballView.x = ball.x;
|
||||||
|
ballView.y = ball.y;
|
||||||
|
}
|
||||||
|
|
||||||
views.paddle.x = game.paddle.x;
|
views.paddle.x = game.paddle.x;
|
||||||
views.paddle.y = game.paddle.y;
|
views.paddle.y = game.paddle.y;
|
||||||
@@ -82,7 +171,32 @@ export function syncronizeViewsWithGame(views, game) {
|
|||||||
brickView.y = brick.y;
|
brickView.y = brick.y;
|
||||||
brickView.visible = brick.alive;
|
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) {
|
} catch (err) {
|
||||||
console.error(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