diff --git a/public/sprites/background_2.png b/public/sprites/background_2.png new file mode 100644 index 0000000..e1280b8 Binary files /dev/null and b/public/sprites/background_2.png differ diff --git a/public/sprites/background_3.png b/public/sprites/background_3.png new file mode 100644 index 0000000..22b89c6 Binary files /dev/null and b/public/sprites/background_3.png differ diff --git a/src/config.js b/src/config.js index ae013d8..0755a18 100644 --- a/src/config.js +++ b/src/config.js @@ -22,6 +22,7 @@ export const BRICK_HEIGHT = 10; export const BRICK_ROW_AMOUNT = 5; export const BRICK_COLUMN_AMOUNT = 20; +export const BRICK_POINTS_AMOUNT = { 1: 10, 2: 30 }; export const PERK_WIDTH = 16; export const PERK_HEIGHT = 16; diff --git a/src/game.js b/src/game.js index 1bf0d44..07ee7c7 100644 --- a/src/game.js +++ b/src/game.js @@ -27,6 +27,7 @@ export class Game { */ constructor(levels) { this.livesAmount = 3; + this.score = 0; this.status = 'in_process'; this.levels = levels; this.currentLevel = 0; diff --git a/src/lib/processBrickCollision/processBrickCollision.js b/src/lib/processBrickCollision/processBrickCollision.js index ef2a3cf..24c8352 100644 --- a/src/lib/processBrickCollision/processBrickCollision.js +++ b/src/lib/processBrickCollision/processBrickCollision.js @@ -1,4 +1,4 @@ -import { PERK_DROP_CHANCE, PERK_HEIGHT, PERK_WIDTH } from '../../config'; +import { BRICK_POINTS_AMOUNT, PERK_DROP_CHANCE, PERK_HEIGHT, PERK_WIDTH } from '../../config'; import { Ball } from '../../entities/ball/ball'; import { Game } from '../../game'; import { calculateAABBCollision } from '../calculateAABBCollision/calculateAABBCollision'; @@ -55,6 +55,10 @@ export function processBrickCollision(game, ball) { ball.verticalSpeed *= directions[1]; brick.kill(); + if (!brick.alive) { + game.score += BRICK_POINTS_AMOUNT[brick.type] ?? 0; + } + if (!brick.alive && Math.random() < PERK_DROP_CHANCE) { const perk = spawnRandomPerk( brick.x + brick.width / 2 - PERK_WIDTH / 2, diff --git a/src/main.js b/src/main.js index 5aa3915..6cf4d53 100644 --- a/src/main.js +++ b/src/main.js @@ -15,7 +15,13 @@ import { } from './config'; import { LEVELS } from './const/levels'; import { Game } from './game'; -import { createGameView, managePerkViewsLifetime, rebuildBrickViews, syncronizeViewsWithGame } from './view'; +import { + createGameView, + managePerkViewsLifetime, + rebuildBrickViews, + syncronizeViewsWithGame, + updateBackgroundView, +} from './view'; (async () => { const app = new Application(); @@ -33,7 +39,16 @@ import { createGameView, managePerkViewsLifetime, rebuildBrickViews, syncronizeV const textures = await Assets.load([ '/sprites/fire_ball_1.png', + '/sprites/fire_ball_2.png', + '/sprites/fire_ball_3.png', + '/sprites/fire_ball_4.png', + '/sprites/fire_ball_5.png', + '/sprites/fire_ball_6.png', + '/sprites/fire_ball_7.png', + '/sprites/fire_ball_8.png', '/sprites/background_1.png', + '/sprites/background_2.png', + '/sprites/background_3.png', '/sprites/paddle.png', '/sprites/block_1.png', '/sprites/block_2.png', @@ -62,13 +77,14 @@ import { createGameView, managePerkViewsLifetime, rebuildBrickViews, syncronizeV if (game.currentLevel !== currentLevel) { rebuildBrickViews(views, game, container, textures); + updateBackgroundView(views, game, textures); currentLevel = game.currentLevel; } managePerkViewsLifetime(views, game, container, textures); syncronizeViewsWithGame(views, game); if (game.status === 'over' || game.status === 'completed') { - showEndScreen(game.status === 'completed' ? 'Победа!' : 'Игра окончена!'); + showEndScreen(game.status === 'completed' ? 'Победа!' : 'Игра окончена!', game.score); app.ticker.stop(); } }); @@ -77,11 +93,12 @@ import { createGameView, managePerkViewsLifetime, rebuildBrickViews, syncronizeV /** * Показывает финальный экран с сообщением и кнопкой рестарта (перезагрузка страницы). * @param {string} message текст результата игры + * @param {number} score итоговые очки */ -function showEndScreen(message) { +function showEndScreen(message, score) { const overlay = document.createElement('div'); overlay.className = 'end-screen'; - overlay.innerHTML = `

${message}

`; + overlay.innerHTML = `

${message}

Очки: ${score}

`; overlay.querySelector('button').addEventListener('click', () => location.reload()); document.body.appendChild(overlay); } diff --git a/src/view.js b/src/view.js index 8ce1416..b26c151 100644 --- a/src/view.js +++ b/src/view.js @@ -1,4 +1,4 @@ -import { Container, Graphics, NineSliceSprite, Sprite, Text } from 'pixi.js'; +import { AnimatedSprite, Container, Graphics, NineSliceSprite, Sprite, Text } from 'pixi.js'; import { CONTAINER_HEIGHT, CONTAINER_WIDTH } from './config'; import { Ball } from './entities/ball/ball'; import { Brick } from './entities/brick/brick'; @@ -12,11 +12,23 @@ import { Game } from './game'; * @param {object} textures объект с текстурами для визуального отображения */ function createBallView(ball, textures) { - const texture = textures['/sprites/fire_ball_1.png']; - const ballSprite = new Sprite(texture); + const ballFrames = [ + textures['/sprites/fire_ball_1.png'], + textures['/sprites/fire_ball_2.png'], + textures['/sprites/fire_ball_3.png'], + textures['/sprites/fire_ball_4.png'], + textures['/sprites/fire_ball_5.png'], + textures['/sprites/fire_ball_6.png'], + textures['/sprites/fire_ball_7.png'], + textures['/sprites/fire_ball_8.png'], + ]; + + const ballSprite = new AnimatedSprite(ballFrames); + ballSprite.animationSpeed = 0.15; ballSprite.anchor.set(0.5); ballSprite.width = ball.radius * 2; ballSprite.height = ball.radius * 2; + ballSprite.play(); return ballSprite; } @@ -29,8 +41,9 @@ function createBallView(ball, textures) { function createPaddleView(paddle, textures) { const texture = textures['/sprites/paddle.png']; const paddleSprite = new NineSliceSprite({ texture, leftWidth: 150, rightWidth: 150, topHeight: 0, bottomHeight: 0 }); - paddleSprite.width = paddle.width; - paddleSprite.height = paddle.height; + const paddleAspectRatio = paddle.height / texture.height; + paddleSprite.scale.set(paddleAspectRatio); + paddleSprite.width = paddle.width / paddleAspectRatio; return paddleSprite; } @@ -89,6 +102,21 @@ function createPerkView(perk, textures) { return perkSprite; } +/** + * Возвращает текстуру фона для текущего уровня или дефолтную текстуру фона + * @param {Game} game экземпляр класса игра + * @param {object} textures объект с текстурами для визуального отображения + */ +function backgroundTextureForLevel(game, textures) { + const levelBackground = textures[`/sprites/background_${game.currentLevel + 1}.png`]; + + if (!levelBackground) { + return textures['/sprites/background_1.png']; + } + + return levelBackground; +} + /** * Создает визуальное отображение всех сущностей в игре с помощью Pixi.js и добавляет в контейнер * @param {Game} game экземпляр класса игра @@ -97,7 +125,7 @@ function createPerkView(perk, textures) { */ export function createGameView(game, container, textures) { try { - const background = new Sprite(textures['/sprites/background_1.png']); + const background = new Sprite(backgroundTextureForLevel(game, textures)); background.width = CONTAINER_WIDTH; background.height = CONTAINER_HEIGHT; container.addChildAt(background, 0); @@ -126,6 +154,7 @@ export function createGameView(game, container, textures) { const perks = new Map(); return { + background, header, balls, paddle, @@ -146,8 +175,9 @@ export function createGameView(game, container, textures) { * @param {object} textures объект с текстурами для визуального отображения */ export function managePerkViewsLifetime(views, game, container, textures) { - if (views.paddle.width !== game.paddle.width) { - views.paddle.width = game.paddle.width; + // Изменяем ширину отображения ракетки основываясь на aspect ratio по оси X и ширине ракетки + if (views.paddle.width * views.paddle.scale.x !== game.paddle.width) { + views.paddle.width = game.paddle.width / views.paddle.scale.x; } for (const [ball, ballView] of views.balls) { @@ -194,7 +224,7 @@ export function syncronizeViewsWithGame(views, game) { throw new Error('Аргумент game должен быть экземпляром класса Game'); } - views.header.text = `Уровень: ${game.currentLevel + 1} Количество жизней: ${game.livesAmount}`; + views.header.text = `Уровень: ${game.currentLevel + 1} Количество жизней: ${game.livesAmount} Очки: ${game.score}`; for (const ball of game.balls) { const ballView = views.balls.get(ball); @@ -223,6 +253,16 @@ export function syncronizeViewsWithGame(views, game) { } } +/** + * Меняет текстуру фона под текущий уровень игры. + * @param {object} views объект с визуальными отображениями сущностей игры + * @param {Game} game экземпляр класса игра + * @param {object} textures объект с текстурами для визуального отображения + */ +export function updateBackgroundView(views, game, textures) { + views.background.texture = backgroundTextureForLevel(game, textures); +} + /** * Пересоздает отображения кирпичей под текущий уровень игры. * @param {object} views объект с визуальными отображениями сущностей игры