Feature/advanced game mechanics #5

Merged
ilia merged 30 commits from feature/advanced-game-mechanics into main 2026-07-19 15:09:22 +00:00
2 changed files with 299 additions and 0 deletions
Showing only changes of commit b0bbb6df96 - Show all commits
@@ -0,0 +1,76 @@
import { Ball } from '../../entities/ball/ball';
import { Paddle } from '../../entities/paddle/paddle';
import { toRadians } from '../toRadians/toRadians';
/**
* Проверяет произошло ли столкновение ракетки и мяча
* @param {Paddle} paddle
* @param {Ball} ball
* @returns {boolean}
*/
function didCollide(paddle, ball) {
return (
ball.verticalSpeed > 0 &&
ball.y + ball.radius >= paddle.y &&
ball.x >= paddle.x &&
ball.x <= paddle.x + paddle.width
);
}
/**
* Изменяет направление полета мяча в зависимости от места попадания в ракетку
* @param {Paddle} paddle экземпляр класса ракетка
* @param {Ball} ball экземпляр класса мяч
* @param {number} paddleSectorAmount количество секторов ракетки, положительное целое число
* @param {number} minAngle минимальный угол отскока мяча (от 0 до 180)
* @param {number} maxAngle максимальный угол отскока мяча (от 0 до 180)
* @param {number} ballSpeed константа скорости мяча, положительное число
*/
export function processReflection(paddle, ball, paddleSectorAmount, minAngle, maxAngle, ballSpeed) {
try {
if (!(paddle instanceof Paddle)) {
throw new Error('Аргумерт paddle должен являться экземпляром класса Paddle');
}
if (!(ball instanceof Ball)) {
throw new Error('Аргумерт ball должен являться экземпляром класса Ball');
}
if (typeof paddleSectorAmount !== 'number' || paddleSectorAmount <= 0 || paddleSectorAmount % 1 !== 0) {
throw new Error('Аргумент paddleSectorAmount должен являться положительным целым числом');
}
if (typeof minAngle !== 'number' || minAngle < 0 || minAngle > 180) {
throw new Error('Аргумент minAngle должен являться числом в промежутке от 0 до 180');
}
if (typeof maxAngle !== 'number' || maxAngle < 0 || maxAngle > 180) {
throw new Error('Аргумент maxAngle должен являться числом в промежутке от 0 до 180');
}
if (minAngle >= maxAngle) {
throw new Error('Значение аргумента maxAngle должено быть строго больше значения аргумента minAngle');
}
if (typeof ballSpeed !== 'number' || ballSpeed <= 0) {
throw new Error('Значение аргумента ballSpeed должно быть положительным целым числом');
}
if (!didCollide(paddle, ball)) {
return;
}
const step = paddle.width / paddleSectorAmount;
const sectorIndex = Math.floor((ball.x - paddle.x) / step);
const normalizedSectorIndex = Math.min(Math.max(sectorIndex, 0), paddleSectorAmount - 1);
const angleStep = (maxAngle - minAngle) / (paddleSectorAmount - 1);
const angleInRadians = toRadians(maxAngle - angleStep * normalizedSectorIndex);
ball.horizontalSpeed = ballSpeed * Math.cos(angleInRadians);
ball.verticalSpeed = -1 * ballSpeed * Math.sin(angleInRadians);
ball.y = paddle.y - ball.radius;
} catch (err) {
console.error(err);
return null;
}
}
@@ -0,0 +1,223 @@
import { Ball } from '../../entities/ball/ball';
import { Paddle } from '../../entities/paddle/paddle';
import { processReflection } from './processReflection';
describe('processReflection', () => {
it('Корректно обрабатывает неверное значения аргумента paddle', () => {
const ball = new Ball(0, 0, 1, 1, 0);
const sectorAmount = 1;
const minAngle = 1;
const maxAngle = 179;
const speed = 1;
const ballX = ball.x;
const ballY = ball.y;
const verticalSpeed = ball.verticalSpeed;
const horizontalSpeed = ball.horizontalSpeed;
const result = processReflection(null, ball, sectorAmount, minAngle, maxAngle, speed);
expect(result).toBeNull();
expect(ball.x).toEqual(ballX);
expect(ball.y).toEqual(ballY);
expect(ball.verticalSpeed).toEqual(verticalSpeed);
expect(ball.horizontalSpeed).toEqual(horizontalSpeed);
});
it('Корректно обрабатывает неверное значения аргумента ball', () => {
const paddle = new Paddle(0, 0, 2, 1);
const sectorAmount = 1;
const minAngle = 1;
const maxAngle = 179;
const speed = 1;
const result = processReflection(paddle, null, sectorAmount, minAngle, maxAngle, speed);
expect(result).toBeNull();
});
it('Корректно обрабатывает неверное значения аргумента paddleSectorAmount', () => {
const paddle = new Paddle(0, 0, 2, 1);
const ball = new Ball(0, 0, 1, 1, 0);
const minAngle = 1;
const maxAngle = 179;
const speed = 1;
const ballX = ball.x;
const ballY = ball.y;
const verticalSpeed = ball.verticalSpeed;
const horizontalSpeed = ball.horizontalSpeed;
const result = processReflection(paddle, ball, null, minAngle, maxAngle, speed);
expect(result).toBeNull();
expect(ball.x).toEqual(ballX);
expect(ball.y).toEqual(ballY);
expect(ball.verticalSpeed).toEqual(verticalSpeed);
expect(ball.horizontalSpeed).toEqual(horizontalSpeed);
expect(processReflection(paddle, ball, 0, minAngle, maxAngle, speed)).toBeNull();
expect(processReflection(paddle, ball, -1, minAngle, maxAngle, speed)).toBeNull();
expect(processReflection(paddle, ball, 1.5, minAngle, maxAngle, speed)).toBeNull();
});
it('Корректно обрабатывает неверное значения аргумента minAngle', () => {
const paddle = new Paddle(0, 0, 2, 1);
const ball = new Ball(0, 0, 1, 1, 0);
const sectorAmount = 1;
const maxAngle = 178;
const speed = 1;
const ballX = ball.x;
const ballY = ball.y;
const verticalSpeed = ball.verticalSpeed;
const horizontalSpeed = ball.horizontalSpeed;
const result = processReflection(paddle, ball, sectorAmount, null, maxAngle, speed);
expect(result).toBeNull();
expect(ball.x).toEqual(ballX);
expect(ball.y).toEqual(ballY);
expect(ball.verticalSpeed).toEqual(verticalSpeed);
expect(ball.horizontalSpeed).toEqual(horizontalSpeed);
expect(processReflection(paddle, ball, sectorAmount, -1, maxAngle, speed)).toBeNull();
expect(processReflection(paddle, ball, sectorAmount, 360, maxAngle, speed)).toBeNull();
const minAngleBiggerThanMaxAngle = 179;
expect(processReflection(paddle, ball, sectorAmount, minAngleBiggerThanMaxAngle, maxAngle, speed)).toBeNull();
});
it('Корректно обрабатывает неверное значения аргумента maxAngle', () => {
const paddle = new Paddle(0, 0, 2, 1);
const ball = new Ball(0, 0, 1, 1, 0);
const sectorAmount = 1;
const minAngle = 1;
const speed = 1;
const ballX = ball.x;
const ballY = ball.y;
const verticalSpeed = ball.verticalSpeed;
const horizontalSpeed = ball.horizontalSpeed;
const result = processReflection(paddle, ball, sectorAmount, minAngle, null, speed);
expect(result).toBeNull();
expect(ball.x).toEqual(ballX);
expect(ball.y).toEqual(ballY);
expect(ball.verticalSpeed).toEqual(verticalSpeed);
expect(ball.horizontalSpeed).toEqual(horizontalSpeed);
expect(processReflection(paddle, ball, sectorAmount, minAngle, -1, speed)).toBeNull();
expect(processReflection(paddle, ball, sectorAmount, minAngle, 360, speed)).toBeNull();
});
it('Корректно обрабатывает неверное значения аргумента speed', () => {
const paddle = new Paddle(0, 0, 2, 1);
const ball = new Ball(0, 0, 1, 1, 0);
const sectorAmount = 1;
const minAngle = 1;
const maxAngle = 179;
const ballX = ball.x;
const ballY = ball.y;
const verticalSpeed = ball.verticalSpeed;
const horizontalSpeed = ball.horizontalSpeed;
const result = processReflection(paddle, ball, sectorAmount, minAngle, maxAngle, null);
expect(result).toBeNull();
expect(ball.x).toEqual(ballX);
expect(ball.y).toEqual(ballY);
expect(ball.verticalSpeed).toEqual(verticalSpeed);
expect(ball.horizontalSpeed).toEqual(horizontalSpeed);
expect(processReflection(paddle, ball, sectorAmount, minAngle, maxAngle, -1)).toBeNull();
});
it('Корректно обрабатывает неверное значения аргумента speed', () => {
const paddle = new Paddle(0, 0, 2, 1);
const ball = new Ball(0, 0, 1, 1, 0);
const sectorAmount = 1;
const minAngle = 1;
const maxAngle = 179;
const ballX = ball.x;
const ballY = ball.y;
const verticalSpeed = ball.verticalSpeed;
const horizontalSpeed = ball.horizontalSpeed;
const result = processReflection(paddle, ball, sectorAmount, minAngle, maxAngle, null);
expect(result).toBeNull();
expect(ball.x).toEqual(ballX);
expect(ball.y).toEqual(ballY);
expect(ball.verticalSpeed).toEqual(verticalSpeed);
expect(ball.horizontalSpeed).toEqual(horizontalSpeed);
expect(processReflection(paddle, ball, sectorAmount, minAngle, maxAngle, -1)).toBeNull();
});
it('Ничего не изменяет при отсутствии столкновения ракетки и мяча', () => {
const paddle = new Paddle(0, 0, 2, 1);
const ball = new Ball(10, 10, 1, 1, 0);
const sectorAmount = 1;
const minAngle = 1;
const maxAngle = 179;
const speed = 1;
const ballX = ball.x;
const ballY = ball.y;
const verticalSpeed = ball.verticalSpeed;
const horizontalSpeed = ball.horizontalSpeed;
const result = processReflection(paddle, ball, sectorAmount, minAngle, maxAngle, speed);
expect(ball.x).toEqual(ballX);
expect(ball.y).toEqual(ballY);
expect(ball.verticalSpeed).toEqual(verticalSpeed);
expect(ball.horizontalSpeed).toEqual(horizontalSpeed);
});
it('Отражает мяч влево при попадании в левый сектор ракетки', () => {
const paddle = new Paddle(0, 100, 90, 10);
const ball = new Ball(5, 99, 1, 5, 0); // левый край, летит вниз
const sectorAmount = 3;
const minAngle = 30;
const maxAngle = 150;
const speed = 10;
processReflection(paddle, ball, sectorAmount, minAngle, maxAngle, speed);
expect(ball.horizontalSpeed).toBeLessThan(0);
expect(ball.verticalSpeed).toBeLessThan(0);
});
it('Отражает мяч вправо при попадании в правый сектор ракетки', () => {
const paddle = new Paddle(0, 100, 90, 10);
const ball = new Ball(85, 99, 1, 5, 0);
const sectorAmount = 3;
const minAngle = 30;
const maxAngle = 150;
const speed = 10;
processReflection(paddle, ball, sectorAmount, minAngle, maxAngle, speed);
expect(ball.horizontalSpeed).toBeGreaterThan(0);
expect(ball.verticalSpeed).toBeLessThan(0);
});
it('Отражает мяч вертикально вверх при попадании в центральный сектор', () => {
const paddle = new Paddle(0, 100, 90, 10);
const ball = new Ball(45, 99, 1, 5, 0);
const sectorAmount = 3;
const minAngle = 30;
const maxAngle = 150;
const speed = 10;
processReflection(paddle, ball, sectorAmount, minAngle, maxAngle, speed);
expect(ball.horizontalSpeed).toBeCloseTo(0);
expect(ball.verticalSpeed).toBeCloseTo(-speed);
});
});