From b0bbb6df96d2091bf5d33a21d6df57659889c5db Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sat, 18 Jul 2026 15:39:36 +0300 Subject: [PATCH] =?UTF-8?q?feat(processReflection):=20=D0=94=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D0=B8=D0=B7=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=D0=B8=D1=8F=20=D0=B3=D0=BE=D1=80=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=BD=D1=82=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B9=20=D0=B8?= =?UTF-8?q?=20=D0=B2=D0=B5=D1=80=D1=82=D0=B8=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D0=BE=D0=B9=20=D1=81=D0=BA=D0=BE=D1=80=D0=BE=D1=81=D1=82=D0=B8?= =?UTF-8?q?=20=D0=BC=D1=8F=D1=87=D0=B0=20=D0=B2=20=D0=B7=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=81=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B8=20=D0=BE=D1=82=20?= =?UTF-8?q?=D0=BC=D0=B5=D1=81=D1=82=D0=B0=20=D0=BF=D0=BE=D0=BF=D0=B0=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B2=20=D1=80=D0=B0=D0=BA=D0=B5?= =?UTF-8?q?=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); + }); +});