From e3035352501fcbe00230fba831a26da3246ac95f Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sat, 18 Jul 2026 12:49:21 +0300 Subject: [PATCH 01/30] =?UTF-8?q?feat(ball):=20=D0=92=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82=D0=BE=D1=80=20=D0=BA=D0=BB?= =?UTF-8?q?=D0=B0=D1=81=D1=81=D0=B0=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D1=81=D0=BA=D0=BE=D1=80=D0=BE=D1=81=D1=82=D0=B8,=20?= =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D1=81?= =?UTF-8?q?=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B0?= =?UTF-8?q?=D1=80=D0=B3=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B0=20=D0=B8=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20reset,=20?= =?UTF-8?q?=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D1=83=D1=8E=D1=89?= =?UTF-8?q?=D0=B8=D0=B9=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D0=B4=D0=B5=D1=84=D0=BE=D0=BB=D1=82=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/ball/ball.js | 23 ++++++++++++++++++++--- src/game.js | 11 +++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/entities/ball/ball.js b/src/entities/ball/ball.js index 0a0c3d4..84deaa0 100644 --- a/src/entities/ball/ball.js +++ b/src/entities/ball/ball.js @@ -7,13 +7,20 @@ export class Ball { * @param {number} x координата положения мяча по оси X * @param {number} y координата положения мяча по оси Y * @param {number} radius положительное числовое значение радиуса мяча + * @param {number} verticalSpeed вектор движения мяча по оси Y + * @param {number} horizontalSpeed вектор движения мяча по оси X */ - constructor(x, y, radius) { + constructor(x, y, radius, verticalSpeed = 0, horizontalSpeed = 0) { this.x = x; this.y = y; - this.horizontalSpeed = 0; - this.verticalSpeed = 0; this.radius = radius; + this.verticalSpeed = verticalSpeed; + this.horizontalSpeed = horizontalSpeed; + + this.defaultX = x; + this.defaultY = y; + this.defaultVerticalSpeed = verticalSpeed; + this.defaultHorizontalSpeed = horizontalSpeed; } /** @@ -24,4 +31,14 @@ export class Ball { this.x += this.horizontalSpeed * deltaTime; this.y += this.verticalSpeed * deltaTime; } + + /** + * Возвращает значения к дефолтным + */ + reset() { + this.x = this.defaultX; + this.y = this.defaultY; + this.verticalSpeed = this.defaultVerticalSpeed; + this.horizontalSpeed = this.defaultHorizontalSpeed; + } } diff --git a/src/game.js b/src/game.js index 45a8bae..9cc511f 100644 --- a/src/game.js +++ b/src/game.js @@ -23,10 +23,13 @@ export class Game { * @param {number} rowAmount количество кирпичей по оси Y, неотрицателное, целое число */ 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); - + this.ball = new Ball( + 100, + 100, + BALL_RADIUS, + BALL_SPEED * Math.cos(BALL_INITIAL_ANGLE), + -1 * BALL_SPEED * Math.sin(BALL_INITIAL_ANGLE), + ); this.paddle = new Paddle(0, CONTAINER_HEIGHT - PADDLE_HEIGHT, PADDLE_WIDTH, PADDLE_HEIGHT); this.bricks = layBricks(columnAmount, rowAmount, BRICK_WIDTH, BRICK_HEIGHT); From c7721f2b1aa9f1117289fec3cc907325ab023de2 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sat, 18 Jul 2026 12:54:32 +0300 Subject: [PATCH 02/30] =?UTF-8?q?feat(ball):=20=D0=98=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=B0=D1=87=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=BE=D0=B5=20=D0=BF=D0=BE=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B8=20=D0=BD=D0=B0=D1=87=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=83=D0=B3=D0=BE=D0=BB=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D0=B5=D1=82=D0=B0=20=D0=BC=D1=8F=D1=87=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config.js | 2 +- src/game.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config.js b/src/config.js index 20c039a..85861db 100644 --- a/src/config.js +++ b/src/config.js @@ -6,7 +6,7 @@ export const PADDLE_HEIGHT = 10; export const BALL_RADIUS = 10; export const BALL_SPEED = 3; -export const BALL_INITIAL_ANGLE = 180; +export const BALL_INITIAL_ANGLE = 0; export const BRICK_WIDTH = 40; export const BRICK_HEIGHT = 10; diff --git a/src/game.js b/src/game.js index 9cc511f..22c28cb 100644 --- a/src/game.js +++ b/src/game.js @@ -24,8 +24,8 @@ export class Game { */ constructor(columnAmount, rowAmount) { this.ball = new Ball( - 100, - 100, + CONTAINER_WIDTH / 2 - BALL_RADIUS, + CONTAINER_HEIGHT / 2 - BALL_RADIUS, BALL_RADIUS, BALL_SPEED * Math.cos(BALL_INITIAL_ANGLE), -1 * BALL_SPEED * Math.sin(BALL_INITIAL_ANGLE), From fea05e6b97056115600f4263a173495e4458398f Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sat, 18 Jul 2026 13:01:43 +0300 Subject: [PATCH 03/30] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=B1=D0=B0=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B8=D0=B3=D1=80=D1=8B=D1=88=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/ball/ball.js | 2 ++ src/game.js | 16 ++++++++++++++++ src/lib/tick/tick.js | 12 +++++++++--- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/entities/ball/ball.js b/src/entities/ball/ball.js index 84deaa0..f2c96c0 100644 --- a/src/entities/ball/ball.js +++ b/src/entities/ball/ball.js @@ -16,6 +16,7 @@ export class Ball { this.radius = radius; this.verticalSpeed = verticalSpeed; this.horizontalSpeed = horizontalSpeed; + this.isOut = false; this.defaultX = x; this.defaultY = y; @@ -40,5 +41,6 @@ export class Ball { this.y = this.defaultY; this.verticalSpeed = this.defaultVerticalSpeed; this.horizontalSpeed = this.defaultHorizontalSpeed; + this.isOut = false; } } diff --git a/src/game.js b/src/game.js index 22c28cb..b6cfa4b 100644 --- a/src/game.js +++ b/src/game.js @@ -23,6 +23,9 @@ export class Game { * @param {number} rowAmount количество кирпичей по оси Y, неотрицателное, целое число */ constructor(columnAmount, rowAmount) { + this.livesAmount = 3; + this.status = 'in_process'; + this.ball = new Ball( CONTAINER_WIDTH / 2 - BALL_RADIUS, CONTAINER_HEIGHT / 2 - BALL_RADIUS, @@ -40,6 +43,19 @@ export class Game { * @param {*} deltaTime изменение времени из Ticker */ update(deltaTime) { + if (this.status !== 'in_process') { + return; + } tick(this, CONTAINER_WIDTH, CONTAINER_HEIGHT, deltaTime); + + if (this.ball.isOut) { + this.livesAmount -= 1; + + if (this.livesAmount === 0) { + this.status = 'over'; + } else { + this.ball.reset(); + } + } } } diff --git a/src/lib/tick/tick.js b/src/lib/tick/tick.js index 3ca17bc..ca0b9c1 100644 --- a/src/lib/tick/tick.js +++ b/src/lib/tick/tick.js @@ -43,12 +43,18 @@ export function tick(game, containerWidth, containerHeight, deltaTime) { ball.horizontalSpeed *= -1; } - // Не даем мячу выйти за границы стен сверху / снизу и меняем направление - if (ball.y <= topBoundary || ball.y >= bottomBoundary) { - ball.y = ball.y <= topBoundary ? topBoundary : bottomBoundary; + // Не даем мячу выйти за границу стены сверху и меняем направление + if (ball.y <= topBoundary) { + ball.y = topBoundary; ball.verticalSpeed *= -1; } + // Проверяем выход за границу стены снизу + if (ball.y >= bottomBoundary) { + game.ball.isOut = true; + return; + } + // Базоввое взаимодействие мяча и кирпича for (const row of bricks) { for (const brick of row) { From 274cc93c550c0014dda643f37da5b77aff8f3286 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sat, 18 Jul 2026 13:14:47 +0300 Subject: [PATCH 04/30] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=B1=D0=B0=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=B8=D0=B3=D1=80=D1=8B=D1=88=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/game.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/game.js b/src/game.js index b6cfa4b..3ea8d1f 100644 --- a/src/game.js +++ b/src/game.js @@ -57,5 +57,11 @@ export class Game { this.ball.reset(); } } + + const isAnyBrickAlive = this.bricks.some((row) => row.some((brick) => brick.alive)); + + if (!isAnyBrickAlive) { + this.status = 'completed'; + } } } From 562805b35c58215adbc14a4393fce5651857974a Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sat, 18 Jul 2026 13:32:54 +0300 Subject: [PATCH 05/30] =?UTF-8?q?refactor(tick):=20=D0=92=20=D1=83=D1=81?= =?UTF-8?q?=D0=BB=D0=BE=D0=B2=D0=B8=D1=8F=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20return=20=D0=B4=D0=BB=D1=8F=20=D0=BE?= =?UTF-8?q?=D1=82=D1=81=D0=B5=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BB?= =?UTF-8?q?=D0=B8=D1=88=D0=BD=D0=B8=D1=85=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/tick/tick.js | 4 +++- src/lib/tick/tick.test.js | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib/tick/tick.js b/src/lib/tick/tick.js index ca0b9c1..95ac1f8 100644 --- a/src/lib/tick/tick.js +++ b/src/lib/tick/tick.js @@ -41,12 +41,14 @@ export function tick(game, containerWidth, containerHeight, deltaTime) { 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; } // Проверяем выход за границу стены снизу @@ -84,7 +86,7 @@ export function tick(game, containerWidth, containerHeight, deltaTime) { ball.horizontalSpeed *= directions[0]; ball.verticalSpeed *= directions[1]; brick.kill(); - break; + return; } } } diff --git a/src/lib/tick/tick.test.js b/src/lib/tick/tick.test.js index 879c79f..6e3b181 100644 --- a/src/lib/tick/tick.test.js +++ b/src/lib/tick/tick.test.js @@ -50,7 +50,7 @@ describe('tick', () => { const game = new Game(1, 1); const brick = game.bricks[0][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; @@ -65,7 +65,7 @@ describe('tick', () => { const game = new Game(2, 1); const [firstBrick, secondBrick] = game.bricks[0]; - 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; From b2a76b0c74bf007495b07383cc2e8c2931ee40bc Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sat, 18 Jul 2026 14:55:19 +0300 Subject: [PATCH 06/30] =?UTF-8?q?feat:=20=D0=A4=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D0=BA=D0=BE=D0=BD=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=82=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D0=B4=D1=83=D1=81=D0=BE=D0=B2=20=D0=B2=20=D1=80=D0=B0=D0=B4?= =?UTF-8?q?=D0=B8=D0=B0=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/toRadians/toRadians.js | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/lib/toRadians/toRadians.js diff --git a/src/lib/toRadians/toRadians.js b/src/lib/toRadians/toRadians.js new file mode 100644 index 0000000..f8ea6ec --- /dev/null +++ b/src/lib/toRadians/toRadians.js @@ -0,0 +1,8 @@ +/** + * Конвертирует значение угла из градусов в радианы + * @param {number} degrees значение угла в градусах + * @returns {number} + */ +export function toRadians(angleInDegrees) { + return (angleInDegrees * Math.PI) / 180; +} From fbdd865da836546f0cfdb2355c7f2058f0790854 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sat, 18 Jul 2026 14:57:37 +0300 Subject: [PATCH 07/30] =?UTF-8?q?fix(game):=20=D0=98=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D1=82=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B9=20Math.sim,?= =?UTF-8?q?=20Math.cos=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D1=83?= =?UTF-8?q?=D0=B5=D0=BC=D1=8B=D1=85=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D1=8B?= =?UTF-8?q?=D1=87=D0=B8=D1=81=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D0=BA?= =?UTF-8?q?=D0=BE=D1=80=D0=BE=D1=81=D1=82=D0=B5=D0=B9=20=D0=BC=D1=8F=D1=87?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=20=D0=BE=D1=81=D1=8F=D0=BC=20X=20=D0=B8?= =?UTF-8?q?=20Y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/game.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/game.js b/src/game.js index 3ea8d1f..f6b7296 100644 --- a/src/game.js +++ b/src/game.js @@ -13,6 +13,7 @@ import { Ball } from './entities/ball/ball'; import { layBricks } from './entities/brick/layBricks/layBricks'; import { Paddle } from './entities/paddle/paddle'; import { tick } from './lib/tick/tick'; +import { toRadians } from './lib/toRadians/toRadians'; /** * Класс игры c информацией о всех игровых сущностях @@ -30,8 +31,8 @@ export class Game { CONTAINER_WIDTH / 2 - BALL_RADIUS, CONTAINER_HEIGHT / 2 - BALL_RADIUS, BALL_RADIUS, - BALL_SPEED * Math.cos(BALL_INITIAL_ANGLE), - -1 * BALL_SPEED * Math.sin(BALL_INITIAL_ANGLE), + 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); From b0bbb6df96d2091bf5d33a21d6df57659889c5db Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sat, 18 Jul 2026 15:39:36 +0300 Subject: [PATCH 08/30] =?UTF-8?q?feat(processReflection):=20=D0=94=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD?= =?UTF-8?q?=D0=BA=D1=86=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=B8=D1=8F=20=D0=B3=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=BD=D1=82=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=B8=20=D0=B2=D0=B5=D1=80=D1=82=D0=B8=D0=BA=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9=20=D1=81=D0=BA=D0=BE=D1=80=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B8=20=D0=BC=D1=8F=D1=87=D0=B0=20=D0=B2=20=D0=B7=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B8=20=D0=BE=D1=82?= =?UTF-8?q?=20=D0=BC=D0=B5=D1=81=D1=82=D0=B0=20=D0=BF=D0=BE=D0=BF=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B2=20=D1=80=D0=B0=D0=BA?= =?UTF-8?q?=D0=B5=D1=82=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../processReflection/processReflection.js | 76 ++++++ .../processReflection.test.js | 223 ++++++++++++++++++ 2 files changed, 299 insertions(+) create mode 100644 src/lib/processReflection/processReflection.js create mode 100644 src/lib/processReflection/processReflection.test.js diff --git a/src/lib/processReflection/processReflection.js b/src/lib/processReflection/processReflection.js new file mode 100644 index 0000000..6d1223e --- /dev/null +++ b/src/lib/processReflection/processReflection.js @@ -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; + } +} diff --git a/src/lib/processReflection/processReflection.test.js b/src/lib/processReflection/processReflection.test.js new file mode 100644 index 0000000..e623a9f --- /dev/null +++ b/src/lib/processReflection/processReflection.test.js @@ -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); + }); +}); From a405a91f6508112f704b9d877ea2833315ad7d45 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sat, 18 Jul 2026 15:40:42 +0300 Subject: [PATCH 09/30] =?UTF-8?q?feat(game):=20=D0=97=D0=B0=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B8=20=D0=B2?= =?UTF-8?q?=D0=B7=D0=B0=D0=B8=D0=BC=D0=BE=D0=B4=D0=B5=D0=B9=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D0=B8=D1=8F=20=D0=BC=D1=8F=D1=87=D0=B0=20=D0=B8=20=D1=80?= =?UTF-8?q?=D0=B0=D0=BA=D0=B5=D1=82=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config.js | 3 +++ src/lib/tick/tick.js | 28 ++++++++++++++++++---------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/config.js b/src/config.js index 85861db..14bae67 100644 --- a/src/config.js +++ b/src/config.js @@ -3,6 +3,9 @@ export const CONTAINER_HEIGHT = 600; export const PADDLE_WIDTH = 50; 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; diff --git a/src/lib/tick/tick.js b/src/lib/tick/tick.js index 95ac1f8..bab86bc 100644 --- a/src/lib/tick/tick.js +++ b/src/lib/tick/tick.js @@ -1,7 +1,15 @@ -import { CONTAINER_HEIGHT, CONTAINER_WIDTH } from '../../config'; +import { + BALL_SPEED, + CONTAINER_HEIGHT, + CONTAINER_WIDTH, + 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 { processReflection } from '../processReflection/processReflection'; /** * Функция для вычисления взаимодействий сущностей игры в зависимости от времени из Ticker @@ -92,15 +100,15 @@ export function tick(game, containerWidth, containerHeight, deltaTime) { } } - // Базоввое взаимодействие мяча и ракетки - if ( - ball.verticalSpeed > 0 && - ball.y + ball.radius >= paddle.y && - ball.x >= paddle.x && - ball.x <= paddle.x + paddle.width - ) { - ball.verticalSpeed *= -1; - } + // Взаимодействие мяча и ракетки - обновление горизонтальной и вертикальной скорости в зависимости от сектора попадания + processReflection( + paddle, + ball, + PADDLE_SECTOR_AMOUNT, + MIN_PADDLE_REFLECTION_ANGLE, + MAX_PADDLE_REFLECTION_ANGLE, + BALL_SPEED, + ); } catch (err) { console.error(err); return null; From ecc3ea1e34f87986a5132d03254d75643a2d01d3 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sat, 18 Jul 2026 15:42:10 +0300 Subject: [PATCH 10/30] =?UTF-8?q?fix(game):=20=D0=98=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=B0=D1=87=D0=B0?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D0=BA=D0=BE=D0=BE=D1=80=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD=D0=B0=D1=82=D1=8B=20=D0=BC=D1=8F=D1=87=D0=B0=20-?= =?UTF-8?q?=20X=20=D0=B8=20Y=20=D0=BA=D0=BE=D0=BE=D1=80=D0=B4=D0=B8=D0=BD?= =?UTF-8?q?=D0=B0=D1=82=D1=8B=20=D0=B2=20=D1=86=D0=B5=D0=BD=D1=82=D1=80?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/game.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game.js b/src/game.js index f6b7296..c0c6fc7 100644 --- a/src/game.js +++ b/src/game.js @@ -28,8 +28,8 @@ export class Game { this.status = 'in_process'; this.ball = new Ball( - CONTAINER_WIDTH / 2 - BALL_RADIUS, - CONTAINER_HEIGHT / 2 - BALL_RADIUS, + 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)), From 71583d0fdea8630ee602ffbdd62aa7865c7758b4 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sat, 18 Jul 2026 17:40:14 +0300 Subject: [PATCH 11/30] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D1=81=D0=BF=D0=B8=D1=81=D0=BE=D0=BA=20?= =?UTF-8?q?=D1=83=D1=80=D0=BE=D0=B2=D0=BD=D0=B5=D0=B9=20=D0=B2=20=D1=84?= =?UTF-8?q?=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D0=B5=20=D0=BC=D0=B0=D1=81=D1=81?= =?UTF-8?q?=D0=B8=D0=B2=D0=B0=20=D0=BA=D0=B0=D1=80=D1=82=20=D1=83=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=BD=D0=B5=D0=B9.=20=D0=9A=D0=B0=D1=80=D1=82?= =?UTF-8?q?=D0=B0=20=D1=83=D1=80=D0=BE=D0=B2=D0=BD=D1=8F=20-=20=D0=B2?= =?UTF-8?q?=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9=20=D0=BC=D0=B0?= =?UTF-8?q?=D1=81=D1=81=D0=B8=D0=B2=20=D0=B3=D0=BB=D1=83=D0=B1=D0=B8=D0=BD?= =?UTF-8?q?=D1=8B=202,=20=D0=B3=D0=B4=D0=B5=20=D1=87=D0=B8=D1=81=D0=BB?= =?UTF-8?q?=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=81=D0=BE=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D1=83=D1=8E=D1=82=20=D0=BA=D0=B8=D1=80=D0=BF=D0=B8?= =?UTF-8?q?=D1=87=D0=B0=D0=BC=20=D1=80=D0=B0=D0=B7=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D1=82=D0=B8=D0=BF=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/const/levels.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/const/levels.js diff --git a/src/const/levels.js b/src/const/levels.js new file mode 100644 index 0000000..9d9ca20 --- /dev/null +++ b/src/const/levels.js @@ -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], + ], +]; From 4436661547c4e83c57bd62dfb785c5faa4cc907e Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sat, 18 Jul 2026 17:47:33 +0300 Subject: [PATCH 12/30] =?UTF-8?q?refactor:=20=D0=A4=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D0=B0=D1=82=20=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=BA=D0=B8=D1=80=D0=BF=D0=B8=D1=87=D0=B5=D0=B9=20=D0=B2=20?= =?UTF-8?q?=D0=BA=D0=BB=D0=B0=D1=81=D1=81=D0=B5=20Game=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=20=D1=81=20=D0=B2?= =?UTF-8?q?=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BC?= =?UTF-8?q?=D0=B0=D1=81=D1=81=D0=B8=D0=B2=D0=B0=20=D0=BD=D0=B0=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=8B=D1=87=D0=BD=D1=8B=D0=B9=20=D0=BC=D0=B0=D1=81=D1=81?= =?UTF-8?q?=D0=B8=D0=B2.=20=D0=92=D1=85=D0=BE=D0=B4=D0=BD=D1=8B=D0=B5=20?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=B6=D0=B4=D0=BE=D0=B3=D0=BE=20=D0=BA=D0=B8=D1=80=D0=BF=D0=B8?= =?UTF-8?q?=D1=87=D0=B0=20=D0=B1=D0=B5=D1=80=D1=83=D1=82=D1=81=D1=8F=20?= =?UTF-8?q?=D0=B8=D0=B7=20=D0=BA=D0=B0=D1=80=D1=82=D1=8B=20=D1=83=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=BD=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/brick/layBricks/layBricks.js | 40 +++++------ .../brick/layBricks/layBricks.test.js | 69 +++++++++++-------- src/game.js | 12 ++-- src/lib/tick/tick.js | 46 ++++++------- src/lib/tick/tick.test.js | 24 ++++--- src/main.js | 3 +- src/view.js | 24 +++---- 7 files changed, 113 insertions(+), 105 deletions(-) diff --git a/src/entities/brick/layBricks/layBricks.js b/src/entities/brick/layBricks/layBricks.js index 9dc722b..1e2858f 100644 --- a/src/entities/brick/layBricks/layBricks.js +++ b/src/entities/brick/layBricks/layBricks.js @@ -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)); + } + } + } return bricks; } catch (err) { diff --git a/src/entities/brick/layBricks/layBricks.test.js b/src/entities/brick/layBricks/layBricks.test.js index 7e665bb..0a67871 100644 --- a/src/entities/brick/layBricks/layBricks.test.js +++ b/src/entities/brick/layBricks/layBricks.test.js @@ -5,54 +5,63 @@ 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++; } } }); diff --git a/src/game.js b/src/game.js index c0c6fc7..71df444 100644 --- a/src/game.js +++ b/src/game.js @@ -20,12 +20,13 @@ import { toRadians } from './lib/toRadians/toRadians'; */ export class Game { /** - * @param {number} columnAmount количество кирпичей по оси X, неотрицателное, целое число - * @param {number} rowAmount количество кирпичей по оси Y, неотрицателное, целое число + * @param {number[][][]} levels список уровней, каждый элемент которого - карта расположения блоков */ - constructor(columnAmount, rowAmount) { + constructor(levels) { this.livesAmount = 3; this.status = 'in_process'; + this.currentLevel = 0; + this.maxLevel = levels.length - 1; this.ball = new Ball( CONTAINER_WIDTH / 2 - BALL_RADIUS / 2, @@ -35,8 +36,7 @@ export class Game { -1 * BALL_SPEED * Math.sin(toRadians(BALL_INITIAL_ANGLE)), ); this.paddle = new Paddle(0, CONTAINER_HEIGHT - PADDLE_HEIGHT, PADDLE_WIDTH, PADDLE_HEIGHT); - - this.bricks = layBricks(columnAmount, rowAmount, BRICK_WIDTH, BRICK_HEIGHT); + this.bricks = layBricks(levels[this.currentLevel], BRICK_WIDTH, BRICK_HEIGHT); } /** @@ -59,7 +59,7 @@ export class Game { } } - const isAnyBrickAlive = this.bricks.some((row) => row.some((brick) => brick.alive)); + const isAnyBrickAlive = this.bricks.some((brick) => brick.alive); if (!isAnyBrickAlive) { this.status = 'completed'; diff --git a/src/lib/tick/tick.js b/src/lib/tick/tick.js index bab86bc..21cd7d2 100644 --- a/src/lib/tick/tick.js +++ b/src/lib/tick/tick.js @@ -66,36 +66,34 @@ export function tick(game, containerWidth, containerHeight, deltaTime) { } // Базоввое взаимодействие мяча и кирпича - for (const row of bricks) { - for (const brick of row) { - if (!brick.alive) { - continue; - } + for (const brick of bricks) { + 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 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 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 ballObject = { left: ballLeft, right: ballRight, top: ballTop, bottom: ballBottom }; + const brickObject = { left: brickLeft, right: brickRight, top: brickTop, bottom: brickBottom }; - const isCollided = calculateCollision(ballObject, brickObject); + const isCollided = calculateCollision(ballObject, brickObject); - if (isCollided) { - const directions = calculateDirection(ballObject, brickObject); + if (isCollided) { + const directions = calculateDirection(ballObject, brickObject); - if (directions !== null) { - ball.horizontalSpeed *= directions[0]; - ball.verticalSpeed *= directions[1]; - brick.kill(); - return; - } + if (directions !== null) { + ball.horizontalSpeed *= directions[0]; + ball.verticalSpeed *= directions[1]; + brick.kill(); + return; } } } diff --git a/src/lib/tick/tick.test.js b/src/lib/tick/tick.test.js index 6e3b181..3c3a047 100644 --- a/src/lib/tick/tick.test.js +++ b/src/lib/tick/tick.test.js @@ -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,8 +51,9 @@ 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 + game.ball.radius + 1; game.ball.y = brick.y + brick.height + game.ball.radius + 1; @@ -62,8 +67,9 @@ 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 + game.ball.radius + 1; game.ball.y = firstBrick.y + firstBrick.height + game.ball.radius + 1; @@ -72,7 +78,7 @@ describe('tick', () => { 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); }); }); diff --git a/src/main.js b/src/main.js index ed9fa34..2631997 100644 --- a/src/main.js +++ b/src/main.js @@ -13,6 +13,7 @@ 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'; @@ -39,7 +40,7 @@ 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); container.on('pointermove', (event) => { diff --git a/src/view.js b/src/view.js index 4aaec1b..99b76af 100644 --- a/src/view.js +++ b/src/view.js @@ -41,13 +41,11 @@ export function createGameView(game, container) { 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; + }); return { ball, @@ -78,13 +76,11 @@ export function syncronizeViewsWithGame(views, game) { 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; } } catch (err) { console.error(err); From 111167b60000e1c7c4187a22cc4228e7fb5f88d3 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sat, 18 Jul 2026 18:03:56 +0300 Subject: [PATCH 13/30] =?UTF-8?q?feat(game):=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=B5=D1=80=D0=B5=D1=85=D0=BE=D0=B4=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=83=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B5=D0=BD=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/game.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/game.js b/src/game.js index 71df444..f37876d 100644 --- a/src/game.js +++ b/src/game.js @@ -25,6 +25,7 @@ export class Game { constructor(levels) { this.livesAmount = 3; this.status = 'in_process'; + this.levels = levels; this.currentLevel = 0; this.maxLevel = levels.length - 1; @@ -36,7 +37,7 @@ export class Game { -1 * BALL_SPEED * Math.sin(toRadians(BALL_INITIAL_ANGLE)), ); this.paddle = new Paddle(0, CONTAINER_HEIGHT - PADDLE_HEIGHT, PADDLE_WIDTH, PADDLE_HEIGHT); - this.bricks = layBricks(levels[this.currentLevel], BRICK_WIDTH, BRICK_HEIGHT); + this.bricks = layBricks(this.levels[this.currentLevel], BRICK_WIDTH, BRICK_HEIGHT); } /** @@ -47,6 +48,7 @@ export class Game { if (this.status !== 'in_process') { return; } + tick(this, CONTAINER_WIDTH, CONTAINER_HEIGHT, deltaTime); if (this.ball.isOut) { @@ -57,12 +59,28 @@ export class Game { } else { this.ball.reset(); } + + return; } const isAnyBrickAlive = this.bricks.some((brick) => brick.alive); if (!isAnyBrickAlive) { - this.status = 'completed'; + this.currentLevel += 1; + + if (this.currentLevel <= this.maxLevel) { + this._proceedToNextLevel(); + } else { + this.status = 'completed'; + } } } + + /** + * Запускает переход на новый уровень, возвращает мяч в дефоотное положение и отрисовывает кирпичи по карте уровня. + */ + _proceedToNextLevel() { + this.ball.reset(); + this.bricks = layBricks(this.levels[this.currentLevel], BRICK_WIDTH, BRICK_HEIGHT); + } } From 5e259f7efc82270ea90738cb6e22105cbacb4abf Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 19 Jul 2026 10:51:58 +0300 Subject: [PATCH 14/30] =?UTF-8?q?feat(brick):=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B8=D0=BF=D1=8B=20?= =?UTF-8?q?=D0=BA=D0=B8=D1=80=D0=BF=D0=B8=D1=87=D0=B5=D0=B9:=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=8B=D1=87=D0=BD=D1=8B=D0=B9,=20=D1=81=202=20=D0=B6?= =?UTF-8?q?=D0=B8=D0=B7=D0=BD=D1=8F=D0=BC=D0=B8,=20=D0=BD=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D1=80=D1=83=D1=88=D0=B0=D0=B5=D0=BC=D1=8B=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/brick/brick.js | 16 ++++++++++++-- src/entities/brick/layBricks/layBricks.js | 2 +- .../brick/layBricks/layBricks.test.js | 22 +++++++++++++++++++ src/game.js | 12 ++++++++-- src/view.js | 13 ++++++++++- 5 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/entities/brick/brick.js b/src/entities/brick/brick.js index 8151678..bf770e5 100644 --- a/src/entities/brick/brick.js +++ b/src/entities/brick/brick.js @@ -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; + } + } } } diff --git a/src/entities/brick/layBricks/layBricks.js b/src/entities/brick/layBricks/layBricks.js index 1e2858f..1a26e50 100644 --- a/src/entities/brick/layBricks/layBricks.js +++ b/src/entities/brick/layBricks/layBricks.js @@ -26,7 +26,7 @@ export function layBricks(levelMap, brickWidth, brickHeight) { 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)); + bricks.push(new Brick(j * brickWidth, i * brickHeight, brickWidth, brickHeight, levelMap[i][j])); } } } diff --git a/src/entities/brick/layBricks/layBricks.test.js b/src/entities/brick/layBricks/layBricks.test.js index 0a67871..70fe366 100644 --- a/src/entities/brick/layBricks/layBricks.test.js +++ b/src/entities/brick/layBricks/layBricks.test.js @@ -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++; + } + } + }); }); diff --git a/src/game.js b/src/game.js index f37876d..d73919f 100644 --- a/src/game.js +++ b/src/game.js @@ -63,9 +63,9 @@ export class Game { return; } - const isAnyBrickAlive = this.bricks.some((brick) => brick.alive); + const isLevelComplete = this._checkLevelCompletion(); - if (!isAnyBrickAlive) { + if (isLevelComplete) { this.currentLevel += 1; if (this.currentLevel <= this.maxLevel) { @@ -76,6 +76,14 @@ export class Game { } } + /** + * Проверяет завершен ли текущий уровень + * @returns {boolean} + */ + _checkLevelCompletion() { + return this.bricks.every((brick) => brick.type === 3 || !brick.alive); + } + /** * Запускает переход на новый уровень, возвращает мяч в дефоотное положение и отрисовывает кирпичи по карте уровня. */ diff --git a/src/view.js b/src/view.js index 99b76af..7fcb508 100644 --- a/src/view.js +++ b/src/view.js @@ -23,9 +23,20 @@ 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'); + } } /** From 500803db847b5176b27a393b472e58c56e3574c5 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 19 Jul 2026 10:53:40 +0300 Subject: [PATCH 15/30] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B8=20=D0=BE=D1=82=D0=BE=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=BA=D0=B8=D1=80?= =?UTF-8?q?=D0=BF=D0=B8=D1=87=D0=B5=D0=B9=20=D0=B8=20=D1=83=D0=B4=D0=B0?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BE=D1=81=D1=82=D0=B0=D0=B2?= =?UTF-8?q?=D1=88=D0=B8=D1=85=D1=81=D1=8F=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=BA=D0=B8=D1=80=D0=BF?= =?UTF-8?q?=D0=B8=D1=87=D0=B5=D0=B9=20=D0=B8=D0=B7=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=D0=B9=D0=BD=D0=B5=D1=80=D0=B0=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=20=D1=81=D0=BC=D0=B5=D0=BD=D0=B5=20=D1=83=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=BD=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.js | 10 ++++++---- src/view.js | 21 ++++++++++++++++++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/main.js b/src/main.js index 2631997..65c0f5c 100644 --- a/src/main.js +++ b/src/main.js @@ -17,7 +17,7 @@ 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, rebuildBrickViews, syncronizeViewsWithGame } from './view'; (async () => { // Create a new application @@ -42,17 +42,19 @@ import { createGameView, syncronizeViewsWithGame } from './view'; const game = new Game(LEVELS); const views = createGameView(game, container); + const 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); - app.ticker.add((time) => { game.update(time.deltaTime); + + if (game.currentLevel !== currentLevel) { + rebuildBrickViews(views, game, container); + } syncronizeViewsWithGame(views, game); - console.log(game.paddle.x, game.paddle.y); }); })(); diff --git a/src/view.js b/src/view.js index 7fcb508..ab34c88 100644 --- a/src/view.js +++ b/src/view.js @@ -42,7 +42,7 @@ function createBrickView(brick) { /** * Создает визуальное отображение всех сущностей в игре с помощью Pixi.js и добавляет в контейнер * @param {Game} game экземпляр класса игра - * @param {Container} container контейнер Pixi.js + * @param {Container} container экземпляр класса контейнер Pixi.js */ export function createGameView(game, container) { try { @@ -97,3 +97,22 @@ export function syncronizeViewsWithGame(views, game) { 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; + }); +} From e7b52bdbd9456b79f2891d0fa6ea4b2348c75940 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 19 Jul 2026 11:10:48 +0300 Subject: [PATCH 16/30] =?UTF-8?q?fix:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=82=D0=B5=D0=BA=D1=83=D1=89=D0=B5=D0=B3=D0=BE?= =?UTF-8?q?=20=D1=83=D1=80=D0=BE=D0=B2=D0=BD=D1=8F=20=D0=B2=20main.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 65c0f5c..e19c5b8 100644 --- a/src/main.js +++ b/src/main.js @@ -42,7 +42,7 @@ import { createGameView, rebuildBrickViews, syncronizeViewsWithGame } from './vi const game = new Game(LEVELS); const views = createGameView(game, container); - const currentLevel = game.currentLevel; + let currentLevel = game.currentLevel; container.on('pointermove', (event) => { const localPosition = container.toLocal(event.global); @@ -54,6 +54,7 @@ import { createGameView, rebuildBrickViews, syncronizeViewsWithGame } from './vi if (game.currentLevel !== currentLevel) { rebuildBrickViews(views, game, container); + currentLevel = game.currentLevel; } syncronizeViewsWithGame(views, game); }); From 0f057c1add8e5467625f36264256dfc76eb9e0ab Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 19 Jul 2026 11:24:37 +0300 Subject: [PATCH 17/30] =?UTF-8?q?feat(ball):=20=D0=A4=D0=BB=D0=B0=D0=B3=20?= =?UTF-8?q?isOut=20=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=D0=BB=D0=B5=20status,=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=BE=20=D0=BD=D0=B0=D1=87=D0=B0?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=BE=D0=B5=20=D0=BF=D0=BE=D0=BB=D0=BE=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BC=D1=8F=D1=87=D0=B0=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=20=D1=81=D1=82=D0=B0=D1=80=D1=82=D0=B5=20=D0=B8?= =?UTF-8?q?=D0=B3=D1=80=D1=8B,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=D0=B0=20=D0=BC=D1=8F?= =?UTF-8?q?=D1=87=D0=B0=20=D0=BF=D0=BE=20=D0=BA=D0=BB=D0=B8=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/ball/ball.js | 19 ++++++++++++++----- src/game.js | 27 +++++++++++++++++++++------ src/lib/tick/tick.js | 2 +- src/main.js | 4 ++++ 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/entities/ball/ball.js b/src/entities/ball/ball.js index f2c96c0..55b9582 100644 --- a/src/entities/ball/ball.js +++ b/src/entities/ball/ball.js @@ -16,7 +16,7 @@ export class Ball { this.radius = radius; this.verticalSpeed = verticalSpeed; this.horizontalSpeed = horizontalSpeed; - this.isOut = false; + this.status = 'idle'; this.defaultX = x; this.defaultY = y; @@ -36,11 +36,20 @@ export class Ball { /** * Возвращает значения к дефолтным */ - reset() { - this.x = this.defaultX; - this.y = this.defaultY; + reset(x = this.defaultX, y = this.defaultY) { + this.x = x; + this.y = y; this.verticalSpeed = this.defaultVerticalSpeed; this.horizontalSpeed = this.defaultHorizontalSpeed; - this.isOut = false; + this.status = 'idle'; + } + + /** + * Запускает мяч с ракетки - переводит из ожидания в игру + */ + launch() { + if (this.status === 'idle') { + this.status = 'moving'; + } } } diff --git a/src/game.js b/src/game.js index d73919f..1f9bb4b 100644 --- a/src/game.js +++ b/src/game.js @@ -29,15 +29,17 @@ export class Game { this.currentLevel = 0; this.maxLevel = levels.length - 1; + this.paddle = new Paddle(0, CONTAINER_HEIGHT - PADDLE_HEIGHT, PADDLE_WIDTH, PADDLE_HEIGHT); this.ball = new Ball( - CONTAINER_WIDTH / 2 - BALL_RADIUS / 2, - CONTAINER_HEIGHT / 2 - BALL_RADIUS / 2, + 0, + 0, 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.bricks = layBricks(this.levels[this.currentLevel], BRICK_WIDTH, BRICK_HEIGHT); + + this._placeBallOnPaddle(); } /** @@ -49,15 +51,21 @@ export class Game { return; } + // Пока мяч не запущен - держим его на ракетке и не считаем физику + if (this.ball.status === 'idle') { + this._placeBallOnPaddle(); + return; + } + tick(this, CONTAINER_WIDTH, CONTAINER_HEIGHT, deltaTime); - if (this.ball.isOut) { + if (this.ball.status === 'out') { this.livesAmount -= 1; if (this.livesAmount === 0) { this.status = 'over'; } else { - this.ball.reset(); + this._placeBallOnPaddle(); } return; @@ -88,7 +96,14 @@ export class Game { * Запускает переход на новый уровень, возвращает мяч в дефоотное положение и отрисовывает кирпичи по карте уровня. */ _proceedToNextLevel() { - this.ball.reset(); + this._placeBallOnPaddle(); this.bricks = layBricks(this.levels[this.currentLevel], BRICK_WIDTH, BRICK_HEIGHT); } + + /** + * Ставит мяч по центру ракетки + */ + _placeBallOnPaddle() { + this.ball.reset(this.paddle.x + this.paddle.width / 2, this.paddle.y - this.ball.radius); + } } diff --git a/src/lib/tick/tick.js b/src/lib/tick/tick.js index 21cd7d2..8394cf6 100644 --- a/src/lib/tick/tick.js +++ b/src/lib/tick/tick.js @@ -61,7 +61,7 @@ export function tick(game, containerWidth, containerHeight, deltaTime) { // Проверяем выход за границу стены снизу if (ball.y >= bottomBoundary) { - game.ball.isOut = true; + game.ball.status = 'out'; return; } diff --git a/src/main.js b/src/main.js index e19c5b8..36313b2 100644 --- a/src/main.js +++ b/src/main.js @@ -49,6 +49,10 @@ import { createGameView, rebuildBrickViews, syncronizeViewsWithGame } from './vi game.paddle.moveTo(localPosition.x, 0, CONTAINER_WIDTH); }); + container.on('pointerdown', () => { + game.ball.launch(); + }); + app.ticker.add((time) => { game.update(time.deltaTime); From 62abd8b77dcb141019f9eb77b3089164b95e99ca Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 19 Jul 2026 13:15:50 +0300 Subject: [PATCH 18/30] =?UTF-8?q?refactor(ball):=20=D0=92=D1=8B=D1=87?= =?UTF-8?q?=D0=B8=D1=81=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B5=D1=80=D1=82?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B9=20=D0=B8=20?= =?UTF-8?q?=D0=B3=D1=80=D0=BE=D0=B8=D0=B7=D0=BE=D0=BD=D1=82=D0=B0=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE=D0=B9=20=D1=81=D0=BA=D0=BE=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=B5=D1=81=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=B2=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=20Ball?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/ball/ball.js | 18 +++++++++++------- src/game.js | 8 +------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/entities/ball/ball.js b/src/entities/ball/ball.js index 55b9582..edeab9b 100644 --- a/src/entities/ball/ball.js +++ b/src/entities/ball/ball.js @@ -1,3 +1,5 @@ +import { toRadians } from '../../lib/toRadians/toRadians'; + /** * Класс сущности "мяч" * @class @@ -7,21 +9,23 @@ export class Ball { * @param {number} x координата положения мяча по оси X * @param {number} y координата положения мяча по оси Y * @param {number} radius положительное числовое значение радиуса мяча - * @param {number} verticalSpeed вектор движения мяча по оси Y - * @param {number} horizontalSpeed вектор движения мяча по оси X + * @param {number} speed положительное числовое значение скорости мяча + * @param {number} angle угол направления движения мяча в градусах */ - constructor(x, y, radius, verticalSpeed = 0, horizontalSpeed = 0) { + constructor(x, y, radius, speed = 0, angle = 0) { this.x = x; this.y = y; this.radius = radius; - this.verticalSpeed = verticalSpeed; - this.horizontalSpeed = horizontalSpeed; + 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 = verticalSpeed; - this.defaultHorizontalSpeed = horizontalSpeed; + this.defaultVerticalSpeed = this.verticalSpeed; + this.defaultHorizontalSpeed = this.horizontalSpeed; } /** diff --git a/src/game.js b/src/game.js index 1f9bb4b..9fd9dff 100644 --- a/src/game.js +++ b/src/game.js @@ -30,13 +30,7 @@ export class Game { this.maxLevel = levels.length - 1; this.paddle = new Paddle(0, CONTAINER_HEIGHT - PADDLE_HEIGHT, PADDLE_WIDTH, PADDLE_HEIGHT); - this.ball = new Ball( - 0, - 0, - BALL_RADIUS, - BALL_SPEED * Math.cos(toRadians(BALL_INITIAL_ANGLE)), - -1 * BALL_SPEED * Math.sin(toRadians(BALL_INITIAL_ANGLE)), - ); + this.ball = new Ball(0, 0, BALL_RADIUS, BALL_SPEED, BALL_INITIAL_ANGLE); this.bricks = layBricks(this.levels[this.currentLevel], BRICK_WIDTH, BRICK_HEIGHT); this._placeBallOnPaddle(); From b05fb89bc55da219c2db2768e70b50ba3207ef4c Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 19 Jul 2026 13:20:10 +0300 Subject: [PATCH 19/30] =?UTF-8?q?feat(ball):=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=B1=D0=B0=D0=B7=D0=BE=D0=B2=D0=BE=D0=B9=20=D1=81=D0=BA?= =?UTF-8?q?=D0=BE=D1=80=D0=BE=D1=81=D1=82=D0=B8=20=D0=BC=D1=8F=D1=87=D0=B0?= =?UTF-8?q?.=20=D0=A1=20=D1=82=D0=B5=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=D0=BC?= =?UTF-8?q?=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=B8=20=D0=B8=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BA=D0=B0=D0=B6=D0=B4=D0=BE=D0=BC=20=D1=81=D0=BB?= =?UTF-8?q?=D0=B5=D0=B4=D1=83=D1=8E=D1=89=D0=B5=D0=BC=20=D1=83=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=BD=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config.js | 2 ++ src/entities/ball/ball.js | 18 ++++++++++++++++++ src/game.js | 6 +++++- src/lib/tick/tick.js | 3 +-- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/config.js b/src/config.js index 14bae67..c61245a 100644 --- a/src/config.js +++ b/src/config.js @@ -9,6 +9,8 @@ export const PADDLE_SECTOR_AMOUNT = 20; export const BALL_RADIUS = 10; 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 BRICK_WIDTH = 40; diff --git a/src/entities/ball/ball.js b/src/entities/ball/ball.js index edeab9b..a265bc2 100644 --- a/src/entities/ball/ball.js +++ b/src/entities/ball/ball.js @@ -56,4 +56,22 @@ export class Ball { 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; + } } diff --git a/src/game.js b/src/game.js index 9fd9dff..d1d25a7 100644 --- a/src/game.js +++ b/src/game.js @@ -2,6 +2,8 @@ import { BALL_INITIAL_ANGLE, BALL_RADIUS, BALL_SPEED, + BALL_SPEED_LEVEL_STEP, + BALL_SPEED_TIME_STEP, BRICK_HEIGHT, BRICK_WIDTH, CONTAINER_HEIGHT, @@ -13,7 +15,6 @@ import { Ball } from './entities/ball/ball'; import { layBricks } from './entities/brick/layBricks/layBricks'; import { Paddle } from './entities/paddle/paddle'; import { tick } from './lib/tick/tick'; -import { toRadians } from './lib/toRadians/toRadians'; /** * Класс игры c информацией о всех игровых сущностях @@ -65,6 +66,8 @@ export class Game { return; } + this.ball.increaseSpeed(BALL_SPEED_TIME_STEP * deltaTime); + const isLevelComplete = this._checkLevelCompletion(); if (isLevelComplete) { @@ -90,6 +93,7 @@ export class Game { * Запускает переход на новый уровень, возвращает мяч в дефоотное положение и отрисовывает кирпичи по карте уровня. */ _proceedToNextLevel() { + this.ball.increaseSpeed(BALL_SPEED_LEVEL_STEP); this._placeBallOnPaddle(); this.bricks = layBricks(this.levels[this.currentLevel], BRICK_WIDTH, BRICK_HEIGHT); } diff --git a/src/lib/tick/tick.js b/src/lib/tick/tick.js index 8394cf6..5e951dc 100644 --- a/src/lib/tick/tick.js +++ b/src/lib/tick/tick.js @@ -1,5 +1,4 @@ import { - BALL_SPEED, CONTAINER_HEIGHT, CONTAINER_WIDTH, MAX_PADDLE_REFLECTION_ANGLE, @@ -105,7 +104,7 @@ export function tick(game, containerWidth, containerHeight, deltaTime) { PADDLE_SECTOR_AMOUNT, MIN_PADDLE_REFLECTION_ANGLE, MAX_PADDLE_REFLECTION_ANGLE, - BALL_SPEED, + ball.speed, ); } catch (err) { console.error(err); From b2f0810ec2febe5c00b0afe3f8c55f28ffd23053 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 19 Jul 2026 14:13:05 +0300 Subject: [PATCH 20/30] =?UTF-8?q?feat(perk):=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=81=D1=83=D1=89=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D1=8C=20=D0=B1=D0=BE=D0=BD=D1=83=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/perk/perk.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/entities/perk/perk.js diff --git a/src/entities/perk/perk.js b/src/entities/perk/perk.js new file mode 100644 index 0000000..56c473c --- /dev/null +++ b/src/entities/perk/perk.js @@ -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; + } +} From a4c4e2a1929e8282fa4a7511ceb5b4f99b42334d Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 19 Jul 2026 14:31:35 +0300 Subject: [PATCH 21/30] =?UTF-8?q?feat(spawnRandomPerk):=20=D0=94=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD?= =?UTF-8?q?=D0=BA=D1=86=D0=B8=D1=8F=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=81=D0=BB=D1=83=D1=87=D0=B0=D0=B9=D0=BD=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=B1=D0=BE=D0=BD=D1=83=D1=81=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D1=8B=D0=BC=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BE=D1=80=D0=B4=D0=B8=D0=BD=D0=B0=D1=82=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config.js | 6 ++++ src/lib/spawnRandomPerk/spawnRandomPerk.js | 28 +++++++++++++++++++ .../spawnRandomPerk/spawnRandomPerk.test.js | 24 ++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 src/lib/spawnRandomPerk/spawnRandomPerk.js create mode 100644 src/lib/spawnRandomPerk/spawnRandomPerk.test.js diff --git a/src/config.js b/src/config.js index c61245a..bd1abe1 100644 --- a/src/config.js +++ b/src/config.js @@ -18,3 +18,9 @@ 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; diff --git a/src/lib/spawnRandomPerk/spawnRandomPerk.js b/src/lib/spawnRandomPerk/spawnRandomPerk.js new file mode 100644 index 0000000..8d97713 --- /dev/null +++ b/src/lib/spawnRandomPerk/spawnRandomPerk.js @@ -0,0 +1,28 @@ +import { PERK_FALL_SPEED, PERK_HEIGHT, PERK_WIDTH } from '../../config'; +import { Perk } from '../../entities/perk/perk'; + +/** + * Типы бонусов + */ +const DROPPABLE_PERKS = ['slow']; + +/** + * Создает случайный бонус в заданной точке + * @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 = DROPPABLE_PERKS[Math.floor(Math.random() * DROPPABLE_PERKS.length)]; + + return new Perk(x, y, PERK_WIDTH, PERK_HEIGHT, type, PERK_FALL_SPEED); + } catch (err) { + console.error(err); + return null; + } +} diff --git a/src/lib/spawnRandomPerk/spawnRandomPerk.test.js b/src/lib/spawnRandomPerk/spawnRandomPerk.test.js new file mode 100644 index 0000000..1e42b82 --- /dev/null +++ b/src/lib/spawnRandomPerk/spawnRandomPerk.test.js @@ -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); + }); +}); From 511b20c9397de149cbd4a164a6d9e6cca8e16dad Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 19 Jul 2026 15:09:56 +0300 Subject: [PATCH 22/30] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D1=81=D1=82=D0=BE=D0=BB=D0=BA=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=80=D0=B0=D0=BA=D0=B5=D1=82?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B8=20=D0=B1=D0=BE=D0=BD=D1=83=D1=81=D0=B0,?= =?UTF-8?q?=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B8=20=D0=B1=D0=BE=D0=BD=D1=83?= =?UTF-8?q?=D1=81=D0=BE=D0=B2=20=D0=BF=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B2=20tick?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/game.js | 2 + src/lib/tick/tick.js | 19 +++++++ src/lib/updatePerks/updatePerks.js | 67 +++++++++++++++++++++++++ src/lib/updatePerks/updatePerks.test.js | 47 +++++++++++++++++ src/main.js | 3 +- src/view.js | 48 ++++++++++++++++++ 6 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 src/lib/updatePerks/updatePerks.js create mode 100644 src/lib/updatePerks/updatePerks.test.js diff --git a/src/game.js b/src/game.js index d1d25a7..67945a7 100644 --- a/src/game.js +++ b/src/game.js @@ -10,6 +10,7 @@ import { 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'; @@ -33,6 +34,7 @@ export class Game { this.paddle = new Paddle(0, CONTAINER_HEIGHT - PADDLE_HEIGHT, PADDLE_WIDTH, PADDLE_HEIGHT); this.ball = 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._placeBallOnPaddle(); } diff --git a/src/lib/tick/tick.js b/src/lib/tick/tick.js index 5e951dc..973ef92 100644 --- a/src/lib/tick/tick.js +++ b/src/lib/tick/tick.js @@ -4,11 +4,16 @@ import { MAX_PADDLE_REFLECTION_ANGLE, MIN_PADDLE_REFLECTION_ANGLE, PADDLE_SECTOR_AMOUNT, + PERK_DROP_CHANCE, + PERK_HEIGHT, + PERK_WIDTH, } from '../../config'; import { Game } from '../../game'; import { calculateCollision } from '../calculateCollision/calculateCollision'; import { calculateDirection } from '../calculateDirection/calculateDirection'; import { processReflection } from '../processReflection/processReflection'; +import { spawnRandomPerk } from '../spawnRandomPerk/spawnRandomPerk'; +import { updatePerks } from '../updatePerks/updatePerks'; /** * Функция для вычисления взаимодействий сущностей игры в зависимости от времени из Ticker @@ -39,6 +44,9 @@ export function tick(game, containerWidth, containerHeight, deltaTime) { ball.moveForward(deltaTime); + // Бонусы падают и ловятся ракеткой каждый кадр + updatePerks(game, containerHeight, deltaTime); + const leftBoundary = ball.radius; const rightBoundary = containerWidth - ball.radius; const topBoundary = ball.radius; @@ -92,6 +100,17 @@ export function tick(game, containerWidth, containerHeight, deltaTime) { ball.horizontalSpeed *= directions[0]; ball.verticalSpeed *= directions[1]; brick.kill(); + + if (!brick.alive) { + if (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; } } diff --git a/src/lib/updatePerks/updatePerks.js b/src/lib/updatePerks/updatePerks.js new file mode 100644 index 0000000..baf63cb --- /dev/null +++ b/src/lib/updatePerks/updatePerks.js @@ -0,0 +1,67 @@ +import { PERK_BALL_SPEED_DECREASE } from '../../config'; +import { Game } from '../../game'; +import { calculateCollision } from '../calculateCollision/calculateCollision'; + +/** + * Обновляет состояние бонусов, меняет положение, проверяет столкновение с ракеткой и выход за границу поля + * @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 = calculateCollision(perkObject, paddleObject); + + if (isCollided) { + switch (perk.type) { + case 'slow': + if (game.ball.speed - PERK_BALL_SPEED_DECREASE > 0) { + game.ball.increaseSpeed(-1 * PERK_BALL_SPEED_DECREASE); + } + 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; + } +} diff --git a/src/lib/updatePerks/updatePerks.test.js b/src/lib/updatePerks/updatePerks.test.js new file mode 100644 index 0000000..cdbd25a --- /dev/null +++ b/src/lib/updatePerks/updatePerks.test.js @@ -0,0 +1,47 @@ +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('Убирает бонус, улетевший за нижнюю границу', () => { + 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); + }); +}); diff --git a/src/main.js b/src/main.js index 36313b2..3a74731 100644 --- a/src/main.js +++ b/src/main.js @@ -17,7 +17,7 @@ import { LEVELS } from './const/levels'; import { Game } from './game'; import { calculateCollision } from './lib/calculateCollision/calculateCollision'; import { calculateDirection } from './lib/calculateDirection/calculateDirection'; -import { createGameView, rebuildBrickViews, syncronizeViewsWithGame } from './view'; +import { createGameView, managePerkViewsLifetime, rebuildBrickViews, syncronizeViewsWithGame } from './view'; (async () => { // Create a new application @@ -60,6 +60,7 @@ import { createGameView, rebuildBrickViews, syncronizeViewsWithGame } from './vi rebuildBrickViews(views, game, container); currentLevel = game.currentLevel; } + managePerkViewsLifetime(views, game, container); syncronizeViewsWithGame(views, game); }); })(); diff --git a/src/view.js b/src/view.js index ab34c88..4eea620 100644 --- a/src/view.js +++ b/src/view.js @@ -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'; /** @@ -39,6 +40,22 @@ function createBrickView(brick) { } } +/** + * Создает визуальное отображение бонуса с помощью 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'); + default: + return perkView.fill('#ffffff'); + } +} + /** * Создает визуальное отображение всех сущностей в игре с помощью Pixi.js и добавляет в контейнер * @param {Game} game экземпляр класса игра @@ -62,6 +79,7 @@ export function createGameView(game, container) { ball, paddle, bricks, + perks: new Map(), }; } catch (err) { console.error(err); @@ -69,6 +87,30 @@ export function createGameView(game, container) { } } +/** + * Создает отображения для новых бонусов и удаляет отображения исчезнувших + * @param {object} views объект с визуальными отображениями сущностей игры + * @param {Game} game экземпляр класса игра + * @param {Container} container экземпляр класса контейнер Pixi.js + */ +export function managePerkViewsLifetime(views, game, container) { + 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 объект с визуальными отображениями сущностей игры @@ -93,6 +135,12 @@ export function syncronizeViewsWithGame(views, game) { 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); } From b7e16f86ead739f230ac5efc5ff40b811775b7d9 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 19 Jul 2026 15:32:09 +0300 Subject: [PATCH 23/30] =?UTF-8?q?feat(perk):=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=B1=D0=BE=D0=BD=D1=83=D1=81=20?= =?UTF-8?q?=D1=83=D0=B2=D0=B5=D0=BB=D0=B8=D1=87=D0=B8=D0=B2=D0=B0=D1=8E?= =?UTF-8?q?=D1=89=D0=B8=D0=B9=20=D1=80=D0=B0=D0=B7=D0=BC=D0=B5=D1=80=20?= =?UTF-8?q?=D1=80=D0=B0=D0=BA=D0=B5=D1=82=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config.js | 2 ++ src/lib/spawnRandomPerk/spawnRandomPerk.js | 9 ++------- src/lib/updatePerks/updatePerks.js | 6 +++++- src/lib/updatePerks/updatePerks.test.js | 12 ++++++++++++ src/view.js | 13 ++++++++++++- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/config.js b/src/config.js index bd1abe1..54bc0ab 100644 --- a/src/config.js +++ b/src/config.js @@ -2,6 +2,7 @@ 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; @@ -24,3 +25,4 @@ 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']; diff --git a/src/lib/spawnRandomPerk/spawnRandomPerk.js b/src/lib/spawnRandomPerk/spawnRandomPerk.js index 8d97713..2aa5fc6 100644 --- a/src/lib/spawnRandomPerk/spawnRandomPerk.js +++ b/src/lib/spawnRandomPerk/spawnRandomPerk.js @@ -1,11 +1,6 @@ -import { PERK_FALL_SPEED, PERK_HEIGHT, PERK_WIDTH } from '../../config'; +import { PERK_FALL_SPEED, PERK_HEIGHT, PERK_TYPES, PERK_WIDTH } from '../../config'; import { Perk } from '../../entities/perk/perk'; -/** - * Типы бонусов - */ -const DROPPABLE_PERKS = ['slow']; - /** * Создает случайный бонус в заданной точке * @param {number} x координата бонуса по оси X @@ -18,7 +13,7 @@ export function spawnRandomPerk(x, y) { throw new Error('Координаты бонуса должны являться числами'); } - const type = DROPPABLE_PERKS[Math.floor(Math.random() * DROPPABLE_PERKS.length)]; + 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) { diff --git a/src/lib/updatePerks/updatePerks.js b/src/lib/updatePerks/updatePerks.js index baf63cb..def5fcf 100644 --- a/src/lib/updatePerks/updatePerks.js +++ b/src/lib/updatePerks/updatePerks.js @@ -1,4 +1,4 @@ -import { PERK_BALL_SPEED_DECREASE } from '../../config'; +import { CONTAINER_WIDTH, PADDLE_WIDE_WIDTH, PERK_BALL_SPEED_DECREASE } from '../../config'; import { Game } from '../../game'; import { calculateCollision } from '../calculateCollision/calculateCollision'; @@ -49,6 +49,10 @@ export function updatePerks(game, containerHeight, deltaTime) { game.ball.increaseSpeed(-1 * PERK_BALL_SPEED_DECREASE); } break; + case 'wide': + paddle.width = PADDLE_WIDE_WIDTH; + paddle.moveTo(paddle.x, 0, CONTAINER_WIDTH); + break; default: break; } diff --git a/src/lib/updatePerks/updatePerks.test.js b/src/lib/updatePerks/updatePerks.test.js index cdbd25a..ef1c87f 100644 --- a/src/lib/updatePerks/updatePerks.test.js +++ b/src/lib/updatePerks/updatePerks.test.js @@ -1,3 +1,4 @@ +import { PADDLE_WIDE_WIDTH } from '../../config'; import { Perk } from '../../entities/perk/perk'; import { Game } from '../../game'; import { updatePerks } from './updatePerks'; @@ -36,6 +37,17 @@ describe('updatePerks', () => { 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('Убирает бонус, улетевший за нижнюю границу', () => { const game = new Game([[[1]]]); game.perks.push(new Perk(0, 601, 10, 10, 'slow', 0)); diff --git a/src/view.js b/src/view.js index 4eea620..781f7a3 100644 --- a/src/view.js +++ b/src/view.js @@ -51,6 +51,8 @@ function createPerkView(perk) { switch (perk.type) { case 'slow': return perkView.fill('#00ffff'); + case 'wide': + return perkView.fill('#0f0f0f'); default: return perkView.fill('#ffffff'); } @@ -75,11 +77,13 @@ export function createGameView(game, container) { return brickView; }); + const perks = new Map(); + return { ball, paddle, bricks, - perks: new Map(), + perks, }; } catch (err) { console.error(err); @@ -94,6 +98,13 @@ export function createGameView(game, container) { * @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 [perk, perkView] of views.perks) { if (!game.perks.includes(perk)) { container.removeChild(perkView); From 8d15f3996f9cce4d7089455505e1ec488c563fda Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 19 Jul 2026 15:37:42 +0300 Subject: [PATCH 24/30] =?UTF-8?q?feat(perk):=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=B1=D0=BE=D0=BD=D1=83=D1=81=20?= =?UTF-8?q?=D0=B4=D0=B0=D1=8E=D1=89=D0=B8=D0=B9=20=D0=B6=D0=B8=D0=B7=D0=BD?= =?UTF-8?q?=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config.js | 2 +- src/lib/updatePerks/updatePerks.js | 3 +++ src/view.js | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/config.js b/src/config.js index 54bc0ab..0ec5eb8 100644 --- a/src/config.js +++ b/src/config.js @@ -25,4 +25,4 @@ 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']; +export const PERK_TYPES = ['slow', 'wide', 'life']; diff --git a/src/lib/updatePerks/updatePerks.js b/src/lib/updatePerks/updatePerks.js index def5fcf..f50e5a3 100644 --- a/src/lib/updatePerks/updatePerks.js +++ b/src/lib/updatePerks/updatePerks.js @@ -53,6 +53,9 @@ export function updatePerks(game, containerHeight, deltaTime) { paddle.width = PADDLE_WIDE_WIDTH; paddle.moveTo(paddle.x, 0, CONTAINER_WIDTH); break; + case 'life': + game.livesAmount += 1; + break; default: break; } diff --git a/src/view.js b/src/view.js index 781f7a3..737c0fe 100644 --- a/src/view.js +++ b/src/view.js @@ -53,6 +53,8 @@ function createPerkView(perk) { return perkView.fill('#00ffff'); case 'wide': return perkView.fill('#0f0f0f'); + case 'life': + return perkView.fill('#f0f0f0'); default: return perkView.fill('#ffffff'); } From d2cfee3ec3598dd036d3d458edfb91efd0298310 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 19 Jul 2026 17:00:23 +0300 Subject: [PATCH 25/30] =?UTF-8?q?refactor:=20=D0=9F=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B8=D0=BC=D0=B5=D0=BD=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D1=84?= =?UTF-8?q?=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F=20calculateCollision=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=B9=20?= =?UTF-8?q?=D1=81=D0=B5=D0=BC=D0=B0=D0=BD=D1=82=D0=B8=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../calculateAABBCollision.js} | 2 +- .../calculateAABBCollision.test.js} | 18 +++++++++--------- .../calculateDirection/calculateDirection.js | 4 ++-- src/lib/tick/tick.js | 4 ++-- src/lib/updatePerks/updatePerks.js | 4 ++-- src/main.js | 2 -- 6 files changed, 16 insertions(+), 18 deletions(-) rename src/lib/{calculateCollision/calculateCollision.js => calculateAABBCollision/calculateAABBCollision.js} (97%) rename src/lib/{calculateCollision/calculateCollision.test.js => calculateAABBCollision/calculateAABBCollision.test.js} (77%) diff --git a/src/lib/calculateCollision/calculateCollision.js b/src/lib/calculateAABBCollision/calculateAABBCollision.js similarity index 97% rename from src/lib/calculateCollision/calculateCollision.js rename to src/lib/calculateAABBCollision/calculateAABBCollision.js index 1ed41b9..c3ea9a4 100644 --- a/src/lib/calculateCollision/calculateCollision.js +++ b/src/lib/calculateAABBCollision/calculateAABBCollision.js @@ -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, diff --git a/src/lib/calculateCollision/calculateCollision.test.js b/src/lib/calculateAABBCollision/calculateAABBCollision.test.js similarity index 77% rename from src/lib/calculateCollision/calculateCollision.test.js rename to src/lib/calculateAABBCollision/calculateAABBCollision.test.js index c2581cf..efe9920 100644 --- a/src/lib/calculateCollision/calculateCollision.test.js +++ b/src/lib/calculateAABBCollision/calculateAABBCollision.test.js @@ -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); }); }); diff --git a/src/lib/calculateDirection/calculateDirection.js b/src/lib/calculateDirection/calculateDirection.js index 5ebf113..a0f93bc 100644 --- a/src/lib/calculateDirection/calculateDirection.js +++ b/src/lib/calculateDirection/calculateDirection.js @@ -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; diff --git a/src/lib/tick/tick.js b/src/lib/tick/tick.js index 973ef92..49f87fe 100644 --- a/src/lib/tick/tick.js +++ b/src/lib/tick/tick.js @@ -9,7 +9,7 @@ import { PERK_WIDTH, } from '../../config'; import { Game } from '../../game'; -import { calculateCollision } from '../calculateCollision/calculateCollision'; +import { calculateAABBCollision } from '../calculateAABBCollision/calculateAABBCollision'; import { calculateDirection } from '../calculateDirection/calculateDirection'; import { processReflection } from '../processReflection/processReflection'; import { spawnRandomPerk } from '../spawnRandomPerk/spawnRandomPerk'; @@ -91,7 +91,7 @@ export function tick(game, containerWidth, containerHeight, deltaTime) { 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); + const isCollided = calculateAABBCollision(ballObject, brickObject); if (isCollided) { const directions = calculateDirection(ballObject, brickObject); diff --git a/src/lib/updatePerks/updatePerks.js b/src/lib/updatePerks/updatePerks.js index f50e5a3..613e9d6 100644 --- a/src/lib/updatePerks/updatePerks.js +++ b/src/lib/updatePerks/updatePerks.js @@ -1,6 +1,6 @@ import { CONTAINER_WIDTH, PADDLE_WIDE_WIDTH, PERK_BALL_SPEED_DECREASE } from '../../config'; import { Game } from '../../game'; -import { calculateCollision } from '../calculateCollision/calculateCollision'; +import { calculateAABBCollision } from '../calculateAABBCollision/calculateAABBCollision'; /** * Обновляет состояние бонусов, меняет положение, проверяет столкновение с ракеткой и выход за границу поля @@ -40,7 +40,7 @@ export function updatePerks(game, containerHeight, deltaTime) { bottom: paddle.y + paddle.height, }; - const isCollided = calculateCollision(perkObject, paddleObject); + const isCollided = calculateAABBCollision(perkObject, paddleObject); if (isCollided) { switch (perk.type) { diff --git a/src/main.js b/src/main.js index 3a74731..21961f7 100644 --- a/src/main.js +++ b/src/main.js @@ -15,8 +15,6 @@ import { } 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, managePerkViewsLifetime, rebuildBrickViews, syncronizeViewsWithGame } from './view'; (async () => { From e637d4e62eeb46d63729468ea95bb3754034b8c9 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 19 Jul 2026 17:11:41 +0300 Subject: [PATCH 26/30] =?UTF-8?q?feat(clone):=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D0=BA=D0=BB=D0=BE=D0=BD?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BC=D1=8F?= =?UTF-8?q?=D1=87=D0=B0=20=D0=B8=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B3=D0=BE=D1=80=D0=B8=D0=B7=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B9=20=D0=B8=20=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=82=D0=B8=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9=20=D1=81=D0=BA=D0=BE=D1=80=D0=BE=D1=81=D1=82=D0=B8=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B3=D0=BB=D0=B0=D1=81=D0=BD=D0=BE=20=D1=83=D0=BA?= =?UTF-8?q?=D0=B0=D0=B7=D0=B0=D0=BD=D0=BE=D0=BC=D1=83=20=D1=83=D0=B3=D0=BB?= =?UTF-8?q?=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/ball/clone/clone.js | 36 +++++++++++++++++++++++++++ src/entities/ball/clone/clone.test.js | 26 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/entities/ball/clone/clone.js create mode 100644 src/entities/ball/clone/clone.test.js diff --git a/src/entities/ball/clone/clone.js b/src/entities/ball/clone/clone.js new file mode 100644 index 0000000..058afd8 --- /dev/null +++ b/src/entities/ball/clone/clone.js @@ -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; + } +} diff --git a/src/entities/ball/clone/clone.test.js b/src/entities/ball/clone/clone.test.js new file mode 100644 index 0000000..61451f4 --- /dev/null +++ b/src/entities/ball/clone/clone.test.js @@ -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); + }); +}); From dccdb02f738967d9613ae0f822ad135438823767 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 19 Jul 2026 17:28:56 +0300 Subject: [PATCH 27/30] =?UTF-8?q?refactor:=20=D0=A1=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F?= =?UTF-8?q?=20processBall=20=D0=B4=D0=BB=D1=8F=20=D0=B8=D0=BD=D0=BA=D0=B0?= =?UTF-8?q?=D0=BF=D1=81=D1=83=D0=BB=D1=8F=D1=86=D0=B8=D0=B8=20=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=D0=B8=D0=BA=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BA=D0=B8=20=D1=81=D1=82=D0=BE=D0=BB=D0=BA=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BC=D1=8F=D1=87=D0=B0=20=D1=81?= =?UTF-8?q?=D0=BE=20=D1=81=D1=82=D0=B5=D0=BD=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/processBall/processBall.js | 54 +++++++++++++++++++++++++ src/lib/processBall/processBall.test.js | 45 +++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 src/lib/processBall/processBall.js create mode 100644 src/lib/processBall/processBall.test.js diff --git a/src/lib/processBall/processBall.js b/src/lib/processBall/processBall.js new file mode 100644 index 0000000..568a212 --- /dev/null +++ b/src/lib/processBall/processBall.js @@ -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; + } +} diff --git a/src/lib/processBall/processBall.test.js b/src/lib/processBall/processBall.test.js new file mode 100644 index 0000000..b34fd80 --- /dev/null +++ b/src/lib/processBall/processBall.test.js @@ -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'); + }); +}); From a734600390574315e58caa94e4e09402ecd893fe Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 19 Jul 2026 17:41:51 +0300 Subject: [PATCH 28/30] =?UTF-8?q?refactor:=20=D0=A1=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F?= =?UTF-8?q?=20processBrickCollision=20=D0=B4=D0=BB=D1=8F=20=D0=B8=D0=BD?= =?UTF-8?q?=D0=BA=D0=B0=D0=BF=D1=81=D1=83=D0=BB=D1=8F=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=BA=D0=B8=20=D1=81=D1=82=D0=BE=D0=BB=D0=BA=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BC=D1=8F=D1=87=D0=B0?= =?UTF-8?q?=20=D1=81=20=D0=BA=D0=B8=D1=80=D0=BF=D0=B8=D1=87=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../processBrickCollision.js | 74 +++++++++++++++++++ .../processBrickCollision.test.js | 33 +++++++++ 2 files changed, 107 insertions(+) create mode 100644 src/lib/processBrickCollision/processBrickCollision.js create mode 100644 src/lib/processBrickCollision/processBrickCollision.test.js diff --git a/src/lib/processBrickCollision/processBrickCollision.js b/src/lib/processBrickCollision/processBrickCollision.js new file mode 100644 index 0000000..ef2a3cf --- /dev/null +++ b/src/lib/processBrickCollision/processBrickCollision.js @@ -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; + } +} diff --git a/src/lib/processBrickCollision/processBrickCollision.test.js b/src/lib/processBrickCollision/processBrickCollision.test.js new file mode 100644 index 0000000..e0321fb --- /dev/null +++ b/src/lib/processBrickCollision/processBrickCollision.test.js @@ -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); + }); +}); From b72583290d04ecadf1bbbd33737ef1688db22fb4 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 19 Jul 2026 17:47:01 +0300 Subject: [PATCH 29/30] =?UTF-8?q?feat(perk):=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=B1=D0=BE=D0=BD=D1=83=D1=81=20clo?= =?UTF-8?q?ne=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D0=B9=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D1=8F=D0=B5=D1=82=202=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D0=BC=D1=8F=D1=87=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config.js | 3 +- src/game.js | 24 +++++- src/lib/tick/tick.js | 110 +++++------------------- src/lib/updatePerks/updatePerks.js | 13 ++- src/lib/updatePerks/updatePerks.test.js | 11 +++ src/view.js | 33 +++++-- 6 files changed, 94 insertions(+), 100 deletions(-) diff --git a/src/config.js b/src/config.js index 0ec5eb8..7c30ecd 100644 --- a/src/config.js +++ b/src/config.js @@ -13,6 +13,7 @@ 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_SPLIT_ANGLE = 20; export const BRICK_WIDTH = 40; export const BRICK_HEIGHT = 10; @@ -25,4 +26,4 @@ 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']; +export const PERK_TYPES = ['slow', 'wide', 'life', 'clone']; diff --git a/src/game.js b/src/game.js index 67945a7..3f52fc9 100644 --- a/src/game.js +++ b/src/game.js @@ -32,13 +32,21 @@ export class Game { this.maxLevel = levels.length - 1; this.paddle = new Paddle(0, CONTAINER_HEIGHT - PADDLE_HEIGHT, PADDLE_WIDTH, PADDLE_HEIGHT); - this.ball = new Ball(0, 0, BALL_RADIUS, BALL_SPEED, BALL_INITIAL_ANGLE); + 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._placeBallOnPaddle(); } + /** + * Основной (первый) мяч. Пока играет один мяч - это он. + * @returns {Ball} + */ + get ball() { + return this.balls[0]; + } + /** * Изменяет значения сущностей в зависимости от времени * @param {*} deltaTime изменение времени из Ticker @@ -56,7 +64,10 @@ export class Game { tick(this, CONTAINER_WIDTH, CONTAINER_HEIGHT, deltaTime); - if (this.ball.status === 'out') { + // Убираем улетевшие мячи. Жизнь теряется только когда не осталось ни одного + const survivingBalls = this.balls.filter((ball) => ball.status !== 'out'); + + if (survivingBalls.length === 0) { this.livesAmount -= 1; if (this.livesAmount === 0) { @@ -68,7 +79,11 @@ export class Game { return; } - this.ball.increaseSpeed(BALL_SPEED_TIME_STEP * deltaTime); + this.balls = survivingBalls; + + for (const ball of this.balls) { + ball.increaseSpeed(BALL_SPEED_TIME_STEP * deltaTime); + } const isLevelComplete = this._checkLevelCompletion(); @@ -101,9 +116,10 @@ export class Game { } /** - * Ставит мяч по центру ракетки + * Ставит мяч по центру ракетки, оставляя один мяч в игре */ _placeBallOnPaddle() { + this.balls = [this.ball]; this.ball.reset(this.paddle.x + this.paddle.width / 2, this.paddle.y - this.ball.radius); } } diff --git a/src/lib/tick/tick.js b/src/lib/tick/tick.js index 49f87fe..34545a1 100644 --- a/src/lib/tick/tick.js +++ b/src/lib/tick/tick.js @@ -1,18 +1,8 @@ -import { - CONTAINER_HEIGHT, - CONTAINER_WIDTH, - MAX_PADDLE_REFLECTION_ANGLE, - MIN_PADDLE_REFLECTION_ANGLE, - PADDLE_SECTOR_AMOUNT, - PERK_DROP_CHANCE, - PERK_HEIGHT, - PERK_WIDTH, -} from '../../config'; +import { MAX_PADDLE_REFLECTION_ANGLE, MIN_PADDLE_REFLECTION_ANGLE, PADDLE_SECTOR_AMOUNT } from '../../config'; import { Game } from '../../game'; -import { calculateAABBCollision } from '../calculateAABBCollision/calculateAABBCollision'; -import { calculateDirection } from '../calculateDirection/calculateDirection'; +import { processBall } from '../processBall/processBall'; +import { processBrickCollision } from '../processBrickCollision/processBrickCollision'; import { processReflection } from '../processReflection/processReflection'; -import { spawnRandomPerk } from '../spawnRandomPerk/spawnRandomPerk'; import { updatePerks } from '../updatePerks/updatePerks'; /** @@ -40,91 +30,37 @@ export function tick(game, containerWidth, containerHeight, deltaTime) { throw new Error('Значение изменения времени должно быть положительным числом'); } - const { ball, paddle, bricks } = game; - - ball.moveForward(deltaTime); + const { paddle } = game; // Бонусы падают и ловятся ракеткой каждый кадр 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; - return; - } - - // Не даем мячу выйти за границу стены сверху и меняем направление - if (ball.y <= topBoundary) { - ball.y = topBoundary; - ball.verticalSpeed *= -1; - return; - } - - // Проверяем выход за границу стены снизу - if (ball.y >= bottomBoundary) { - game.ball.status = 'out'; - return; - } - - // Базоввое взаимодействие мяча и кирпича - for (const brick of bricks) { - if (!brick.alive) { + if (hitWallOrWentOut) { 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 hitBrick = processBrickCollision(game, ball); - 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 = calculateAABBCollision(ballObject, brickObject); - - if (isCollided) { - const directions = calculateDirection(ballObject, brickObject); - - if (directions !== null) { - ball.horizontalSpeed *= directions[0]; - ball.verticalSpeed *= directions[1]; - brick.kill(); - - if (!brick.alive) { - if (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; - } + if (hitBrick) { + continue; } - } - // Взаимодействие мяча и ракетки - обновление горизонтальной и вертикальной скорости в зависимости от сектора попадания - processReflection( - paddle, - ball, - PADDLE_SECTOR_AMOUNT, - MIN_PADDLE_REFLECTION_ANGLE, - MAX_PADDLE_REFLECTION_ANGLE, - ball.speed, - ); + // Взаимодействие мяча и ракетки - обновление горизонтальной и вертикальной скорости в зависимости от сектора попадания + processReflection( + paddle, + ball, + PADDLE_SECTOR_AMOUNT, + MIN_PADDLE_REFLECTION_ANGLE, + MAX_PADDLE_REFLECTION_ANGLE, + ball.speed, + ); + } } catch (err) { console.error(err); return null; diff --git a/src/lib/updatePerks/updatePerks.js b/src/lib/updatePerks/updatePerks.js index 613e9d6..f18ae79 100644 --- a/src/lib/updatePerks/updatePerks.js +++ b/src/lib/updatePerks/updatePerks.js @@ -1,4 +1,5 @@ -import { CONTAINER_WIDTH, PADDLE_WIDE_WIDTH, PERK_BALL_SPEED_DECREASE } from '../../config'; +import { BALL_SPLIT_ANGLE, CONTAINER_WIDTH, PADDLE_WIDE_WIDTH, PERK_BALL_SPEED_DECREASE } from '../../config'; +import { clone } from '../../entities/ball/clone/clone'; import { Game } from '../../game'; import { calculateAABBCollision } from '../calculateAABBCollision/calculateAABBCollision'; @@ -45,8 +46,10 @@ export function updatePerks(game, containerHeight, deltaTime) { if (isCollided) { switch (perk.type) { case 'slow': - if (game.ball.speed - PERK_BALL_SPEED_DECREASE > 0) { - game.ball.increaseSpeed(-1 * PERK_BALL_SPEED_DECREASE); + for (const ball of game.balls) { + if (ball.speed - PERK_BALL_SPEED_DECREASE > 0) { + ball.increaseSpeed(-1 * PERK_BALL_SPEED_DECREASE); + } } break; case 'wide': @@ -56,6 +59,10 @@ export function updatePerks(game, containerHeight, deltaTime) { 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; } diff --git a/src/lib/updatePerks/updatePerks.test.js b/src/lib/updatePerks/updatePerks.test.js index ef1c87f..8c0b8c0 100644 --- a/src/lib/updatePerks/updatePerks.test.js +++ b/src/lib/updatePerks/updatePerks.test.js @@ -48,6 +48,17 @@ describe('updatePerks', () => { 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)); diff --git a/src/view.js b/src/view.js index 737c0fe..e7e2f51 100644 --- a/src/view.js +++ b/src/view.js @@ -67,8 +67,12 @@ function createPerkView(perk) { */ 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); @@ -82,7 +86,7 @@ export function createGameView(game, container) { const perks = new Map(); return { - ball, + balls, paddle, bricks, perks, @@ -107,6 +111,22 @@ export function managePerkViewsLifetime(views, game, container) { 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); @@ -135,8 +155,11 @@ 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; From 48fb992d04f58deff56bdfad9a4b43c221f882e1 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 19 Jul 2026 18:02:33 +0300 Subject: [PATCH 30/30] =?UTF-8?q?feat(perk):=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=82=D0=B0=D0=B9=D0=BC=D0=B0=D1=83?= =?UTF-8?q?=D1=82=20=D0=B4=D0=BB=D1=8F=20=D0=B1=D0=BE=D0=BD=D1=83=D1=81?= =?UTF-8?q?=D0=B0=20=D1=83=D0=B2=D0=B5=D0=BB=D0=B8=D1=87=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D1=80=D0=B0=D0=B7=D0=BC=D0=B5=D1=80=D0=B0=20=D1=80?= =?UTF-8?q?=D0=B0=D0=BA=D0=B5=D1=82=D0=BA=D0=B8=20=D0=B8=20=D0=B1=D0=BE?= =?UTF-8?q?=D0=BD=D1=83=D1=81=D0=B0=20=D0=B7=D0=B0=D0=BC=D0=B5=D0=B4=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D0=BA=D0=BE=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D0=BC=D1=8F=D1=87=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config.js | 1 + src/lib/updatePerks/updatePerks.js | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/config.js b/src/config.js index 7c30ecd..5265679 100644 --- a/src/config.js +++ b/src/config.js @@ -27,3 +27,4 @@ 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; diff --git a/src/lib/updatePerks/updatePerks.js b/src/lib/updatePerks/updatePerks.js index f18ae79..1f54a7b 100644 --- a/src/lib/updatePerks/updatePerks.js +++ b/src/lib/updatePerks/updatePerks.js @@ -1,4 +1,11 @@ -import { BALL_SPLIT_ANGLE, CONTAINER_WIDTH, PADDLE_WIDE_WIDTH, PERK_BALL_SPEED_DECREASE } from '../../config'; +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'; @@ -51,10 +58,18 @@ export function updatePerks(game, containerHeight, deltaTime) { 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;