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); }