From b4daf7203641d0d12de9f4911c4f69e818ff0fd7 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 17 Jul 2026 12:30:09 +0300 Subject: [PATCH] =?UTF-8?q?feat(tick)=20=D0=B4=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=20tick=20=D0=BE=D0=BF=D0=B8=D1=81=D1=8B=D0=B2=D0=B0?= =?UTF-8?q?=D1=8E=D1=89=D0=B0=D1=8F=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D1=83?= =?UTF-8?q?=20=D0=B2=D0=B7=D0=B0=D0=B8=D0=BC=D0=BE=D0=B4=D0=B5=D0=B9=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D0=B8=D1=8F=20=D1=81=D1=83=D1=89=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B5=D0=B9=20=D0=B8=D0=B3=D1=80=D1=8B=20=D0=B2=D0=BE=20?= =?UTF-8?q?=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/tick/tick.js | 100 ++++++++++++++++++++++++++++++++++++++ src/lib/tick/tick.test.js | 78 +++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 src/lib/tick/tick.js create mode 100644 src/lib/tick/tick.test.js diff --git a/src/lib/tick/tick.js b/src/lib/tick/tick.js new file mode 100644 index 0000000..49715d6 --- /dev/null +++ b/src/lib/tick/tick.js @@ -0,0 +1,100 @@ +import { CONTAINER_HEIGHT, CONTAINER_WIDTH } from '../../config'; +import { Game } from '../../game'; +import { calculateCollision } from '../calculateCollision/calculateCollision'; +import { calculateDirection } from '../calculateDirection/calculateDirection'; + +/** + * Функция для вычисления взаимодействий сущностей игры в зависимости от времени из Ticker + * @param {Game} game экземпляр класса игры + * @param {number} containerWidth ширина игрового контейнера в пикселях + * @param {number} containerHeight высота игрового контейнера в пикселях + * @param {number} deltaTime изменение времени из Ticker + */ +export function tick(game, containerWidth, containerHeight, deltaTime) { + try { + if (!(game instanceof Game)) { + throw new Error('Аргумент game должен быть экземпляром класса игры'); + } + + if ([containerWidth, containerHeight].some((element) => typeof element !== 'number')) { + throw new Error('Значения размеров игрового контейнера должены являться числами'); + } + + if (containerWidth <= 0 || containerHeight <= 0) { + throw new Error('Размеры игрового контейнера должены быть положительными числами'); + } + + if (typeof deltaTime !== 'number' || deltaTime <= 0) { + throw new Error('Значение изменения времени должно быть положительным числом'); + } + + const { ball, paddle, bricks } = game; + + ball.moveForward(deltaTime); + + 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; + } + + // Не даем мячу выйти за границы стен сверху / снизу и меняем направление + if (ball.y <= topBoundary || ball.y >= bottomBoundary) { + ball.y = ball.y <= topBoundary ? topBoundary : bottomBoundary; + ball.verticalSpeed *= -1; + } + + // Базоввое взаимодействие мяча и кирпича + for (const row of bricks) { + for (const brick of row) { + if (!brick.alive) { + continue; + } + + const ballLeft = ball.x - ball.radius; + const ballRight = ball.x + ball.radius; + const ballTop = ball.y - ball.radius; + const ballBottom = ball.y + ball.radius; + + const brickLeft = brick.x; + const brickRight = brick.x + brick.width; + const brickTop = brick.y; + const brickBottom = brick.y + brick.height; + + const ballObject = { left: ballLeft, right: ballRight, top: ballTop, bottom: ballBottom }; + const brickObject = { left: brickLeft, right: brickRight, top: brickTop, bottom: brickBottom }; + + const isCollided = calculateCollision(ballObject, brickObject); + + if (isCollided) { + const directions = calculateDirection(ballObject, brickObject); + + if (directions !== null) { + ball.horizontalSpeed *= directions[0]; + ball.verticalSpeed *= directions[1]; + brick.kill(); + break; + } + } + } + } + + // Базоввое взаимодействие мяча и ракетки + if ( + ball.verticalSpeed > 0 && + ball.y + ball.radius >= paddle.y && + ball.x >= paddle.x && + ball.x <= paddle.x + paddle.width + ) { + verticalSpeed *= -1; + } + } catch (err) { + console.error(err); + return null; + } +} diff --git a/src/lib/tick/tick.test.js b/src/lib/tick/tick.test.js new file mode 100644 index 0000000..879c79f --- /dev/null +++ b/src/lib/tick/tick.test.js @@ -0,0 +1,78 @@ +import { CONTAINER_HEIGHT, CONTAINER_WIDTH } from '../../config'; +import { Game } from '../../game'; +import { tick } from './tick'; + +describe('tick', () => { + it('Возвращает корректное значние при неверных типах аргументов', () => { + expect(tick(null, 1, 1, 1)).toBeNull(); + expect(tick(1, null, 1, 1)).toBeNull(); + expect(tick(1, 1, null, 1)).toBeNull(); + expect(tick(1, 1, 1, null)).toBeNull(); + }); + + it('Возвращает корректное значние и не изменяет свойства game при неверых размерах контейнера', () => { + const game = new Game(1, 1); + const ballX = game.ball.x; + const ballY = game.ball.y; + expect(tick(game, -1, -1, 1)).toBeNull(); + expect(game.ball.x).toBe(ballX); + expect(game.ball.y).toBe(ballY); + }); + + it('Возвращает корректное значние и не изменяет свойства game при неверном значении времени', () => { + const game = new Game(1, 1); + const ballX = game.ball.x; + const ballY = game.ball.y; + expect(tick(game, 1, 1, -1)).toBeNull(); + expect(game.ball.x).toBe(ballX); + expect(game.ball.y).toBe(ballY); + }); + + it('Меняет направление мяча при столкновении со стеной', () => { + const game = new Game(1, 1); + game.ball.x = 0; + game.ball.horizontalSpeed = -10; + tick(game, CONTAINER_WIDTH, CONTAINER_HEIGHT, 1); + expect(game.ball.x).toBe(10); + expect(game.ball.horizontalSpeed).toBe(10); + }); + + it('Меняет направление мяча при столкновении с потолком', () => { + const game = new Game(0, 0); + game.ball.y = 0; + game.ball.verticalSpeed = -10; + tick(game, CONTAINER_WIDTH, CONTAINER_HEIGHT, 1); + expect(game.ball.y).toBe(game.ball.radius); + expect(game.ball.verticalSpeed).toBe(10); + }); + + it('Убивает кирпич при столкновении и отражает мяч', () => { + const game = new Game(1, 1); + const brick = game.bricks[0][0]; + + game.ball.x = brick.x + 1; + game.ball.y = brick.y + brick.height + game.ball.radius + 1; + game.ball.horizontalSpeed = 0; + game.ball.verticalSpeed = -10; + + tick(game, CONTAINER_WIDTH, CONTAINER_HEIGHT, 0.1); + + expect(brick.alive).toBe(false); + expect(game.ball.verticalSpeed).toBe(10); + }); + + it('Убивает только один кирпич за тик', () => { + const game = new Game(2, 1); + const [firstBrick, secondBrick] = game.bricks[0]; + + game.ball.x = firstBrick.x + 1; + game.ball.y = firstBrick.y + firstBrick.height + game.ball.radius + 1; + game.ball.horizontalSpeed = 0; + game.ball.verticalSpeed = -10; + + tick(game, CONTAINER_WIDTH, CONTAINER_HEIGHT, 0.1); + + const killedCount = game.bricks[0].filter((b) => !b.alive).length; + expect(killedCount).toBe(1); + }); +});