diff --git a/public/sprites/background_1.png b/public/sprites/background_1.png new file mode 100644 index 0000000..074cba4 Binary files /dev/null and b/public/sprites/background_1.png differ diff --git a/public/sprites/block_1.png b/public/sprites/block_1.png new file mode 100644 index 0000000..484e54b Binary files /dev/null and b/public/sprites/block_1.png differ diff --git a/public/sprites/block_2.png b/public/sprites/block_2.png new file mode 100644 index 0000000..f483564 Binary files /dev/null and b/public/sprites/block_2.png differ diff --git a/public/sprites/block_3.png b/public/sprites/block_3.png new file mode 100644 index 0000000..f8ec2cd Binary files /dev/null and b/public/sprites/block_3.png differ diff --git a/public/sprites/fire_ball_1.png b/public/sprites/fire_ball_1.png new file mode 100644 index 0000000..45ebc0e Binary files /dev/null and b/public/sprites/fire_ball_1.png differ diff --git a/public/sprites/fire_ball_2.png b/public/sprites/fire_ball_2.png new file mode 100644 index 0000000..f6ad171 Binary files /dev/null and b/public/sprites/fire_ball_2.png differ diff --git a/public/sprites/fire_ball_3.png b/public/sprites/fire_ball_3.png new file mode 100644 index 0000000..99b9b10 Binary files /dev/null and b/public/sprites/fire_ball_3.png differ diff --git a/public/sprites/fire_ball_4.png b/public/sprites/fire_ball_4.png new file mode 100644 index 0000000..15f71a4 Binary files /dev/null and b/public/sprites/fire_ball_4.png differ diff --git a/public/sprites/fire_ball_5.png b/public/sprites/fire_ball_5.png new file mode 100644 index 0000000..06454a5 Binary files /dev/null and b/public/sprites/fire_ball_5.png differ diff --git a/public/sprites/fire_ball_6.png b/public/sprites/fire_ball_6.png new file mode 100644 index 0000000..5c6dfe3 Binary files /dev/null and b/public/sprites/fire_ball_6.png differ diff --git a/public/sprites/fire_ball_7.png b/public/sprites/fire_ball_7.png new file mode 100644 index 0000000..3e59b16 Binary files /dev/null and b/public/sprites/fire_ball_7.png differ diff --git a/public/sprites/fire_ball_8.png b/public/sprites/fire_ball_8.png new file mode 100644 index 0000000..518d7d0 Binary files /dev/null and b/public/sprites/fire_ball_8.png differ diff --git a/public/sprites/paddle.png b/public/sprites/paddle.png new file mode 100644 index 0000000..87ef471 Binary files /dev/null and b/public/sprites/paddle.png differ diff --git a/public/sprites/perk_1.png b/public/sprites/perk_1.png new file mode 100644 index 0000000..b26d7cb Binary files /dev/null and b/public/sprites/perk_1.png differ diff --git a/public/sprites/perk_2.png b/public/sprites/perk_2.png new file mode 100644 index 0000000..0390515 Binary files /dev/null and b/public/sprites/perk_2.png differ diff --git a/public/sprites/perk_3.png b/public/sprites/perk_3.png new file mode 100644 index 0000000..6e85156 Binary files /dev/null and b/public/sprites/perk_3.png differ diff --git a/public/sprites/perk_4.png b/public/sprites/perk_4.png new file mode 100644 index 0000000..dd1d541 Binary files /dev/null and b/public/sprites/perk_4.png differ diff --git a/src/config.js b/src/config.js index 5265679..ae013d8 100644 --- a/src/config.js +++ b/src/config.js @@ -15,6 +15,8 @@ export const BALL_SPEED_TIME_STEP = 0.002; export const BALL_INITIAL_ANGLE = 0; export const BALL_SPLIT_ANGLE = 20; +export const HEADER_HEIGHT = 30; + export const BRICK_WIDTH = 40; export const BRICK_HEIGHT = 10; diff --git a/src/entities/brick/layBricks/layBricks.js b/src/entities/brick/layBricks/layBricks.js index 1a26e50..46d0765 100644 --- a/src/entities/brick/layBricks/layBricks.js +++ b/src/entities/brick/layBricks/layBricks.js @@ -5,9 +5,10 @@ import { Brick } from '../brick'; * @param {number[][]} levelMap карта уровня в формате массива * @param {number} brickWidth ширина кирпича, неотрицателное число * @param {number} brickHeight длина кирпича, неотрицателное число + * @param {number} marginTop отступ сверху, неотрицательное число * @returns {Brick[]} массив кирпичей */ -export function layBricks(levelMap, brickWidth, brickHeight) { +export function layBricks(levelMap, brickWidth, brickHeight, marginTop = 0) { try { if (!(Array.isArray(levelMap) && levelMap.every(Array.isArray))) { throw new Error('Значение карты уровня должно являться вложенным масивом чисел глубины 2'); @@ -21,12 +22,16 @@ export function layBricks(levelMap, brickWidth, brickHeight) { throw new Error('Значения ширины и высоты кирпича должны являться положительными целыми числами'); } + if (typeof marginTop !== 'number' || marginTop < 0) { + throw new Error('Значение отступа сверху должно быть неотрицательным числом'); + } + const bricks = []; for (let i = 0; i < levelMap.length; i++) { for (let j = 0; j < levelMap[i].length; j++) { if (levelMap[i][j] !== 0) { - bricks.push(new Brick(j * brickWidth, i * brickHeight, brickWidth, brickHeight, levelMap[i][j])); + bricks.push(new Brick(j * brickWidth, marginTop + i * brickHeight, brickWidth, brickHeight, levelMap[i][j])); } } } diff --git a/src/entities/brick/layBricks/layBricks.test.js b/src/entities/brick/layBricks/layBricks.test.js index 70fe366..754bc42 100644 --- a/src/entities/brick/layBricks/layBricks.test.js +++ b/src/entities/brick/layBricks/layBricks.test.js @@ -24,6 +24,16 @@ describe('layBricks', () => { expect(layBricks(levelMap, 1, 1)).toBeNull(); }); + it('Возвращает корректное значение в случае некоректного отступа', () => { + const levelMap = [ + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + ]; + + expect(layBricks(levelMap, 1, 1, -1)).toBeNull(); + }); + it('Возвращает массив корректных размеров', () => { const levelMap = [ [1, 1, 1, 1, 1], diff --git a/src/game.js b/src/game.js index 3f52fc9..1bf0d44 100644 --- a/src/game.js +++ b/src/game.js @@ -8,6 +8,7 @@ import { BRICK_WIDTH, CONTAINER_HEIGHT, CONTAINER_WIDTH, + HEADER_HEIGHT, PADDLE_HEIGHT, PADDLE_WIDTH, PERK_BALL_SPEED_DECREASE, @@ -33,7 +34,7 @@ export class Game { this.paddle = new Paddle(0, CONTAINER_HEIGHT - PADDLE_HEIGHT, PADDLE_WIDTH, PADDLE_HEIGHT); this.balls = [new Ball(0, 0, BALL_RADIUS, BALL_SPEED, BALL_INITIAL_ANGLE)]; - this.bricks = layBricks(this.levels[this.currentLevel], BRICK_WIDTH, BRICK_HEIGHT); + this.bricks = layBricks(this.levels[this.currentLevel], BRICK_WIDTH, BRICK_HEIGHT, HEADER_HEIGHT); this.perks = []; this._placeBallOnPaddle(); @@ -112,7 +113,7 @@ export class Game { _proceedToNextLevel() { this.ball.increaseSpeed(BALL_SPEED_LEVEL_STEP); this._placeBallOnPaddle(); - this.bricks = layBricks(this.levels[this.currentLevel], BRICK_WIDTH, BRICK_HEIGHT); + this.bricks = layBricks(this.levels[this.currentLevel], BRICK_WIDTH, BRICK_HEIGHT, HEADER_HEIGHT); } /** diff --git a/src/main.js b/src/main.js index 21961f7..04620bb 100644 --- a/src/main.js +++ b/src/main.js @@ -38,8 +38,21 @@ import { createGameView, managePerkViewsLifetime, rebuildBrickViews, syncronizeV app.stage.addChild(container); + const textures = await Assets.load([ + '/sprites/fire_ball_1.png', + '/sprites/background_1.png', + '/sprites/paddle.png', + '/sprites/block_1.png', + '/sprites/block_2.png', + '/sprites/block_3.png', + '/sprites/perk_1.png', + '/sprites/perk_2.png', + '/sprites/perk_3.png', + '/sprites/perk_4.png', + ]); + const game = new Game(LEVELS); - const views = createGameView(game, container); + const views = createGameView(game, container, textures); let currentLevel = game.currentLevel; container.on('pointermove', (event) => { @@ -55,10 +68,10 @@ import { createGameView, managePerkViewsLifetime, rebuildBrickViews, syncronizeV game.update(time.deltaTime); if (game.currentLevel !== currentLevel) { - rebuildBrickViews(views, game, container); + rebuildBrickViews(views, game, container, textures); currentLevel = game.currentLevel; } - managePerkViewsLifetime(views, game, container); + managePerkViewsLifetime(views, game, container, textures); syncronizeViewsWithGame(views, game); }); })(); diff --git a/src/style.css b/src/style.css index 0fa0f01..4d5e079 100644 --- a/src/style.css +++ b/src/style.css @@ -1 +1,12 @@ @import "./reset.css"; + +body { + min-height: 100vh; +} + +canvas { + width: 100vw; + height: 100vh; + display: block; + object-fit: contain; +} diff --git a/src/view.js b/src/view.js index e7e2f51..8ce1416 100644 --- a/src/view.js +++ b/src/view.js @@ -1,4 +1,5 @@ -import { Container, Graphics } from 'pixi.js'; +import { 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'; import { Paddle } from './entities/paddle/paddle'; @@ -8,77 +9,116 @@ import { Game } from './game'; /** * Создает визуальное отображение мяча с помощью Pixi.js * @param {Ball} ball экземпляр класса мяч + * @param {object} textures объект с текстурами для визуального отображения */ -function createBallView(ball) { - return new Graphics().circle(0, 0, ball.radius).fill('#ffffff'); +function createBallView(ball, textures) { + const texture = textures['/sprites/fire_ball_1.png']; + const ballSprite = new Sprite(texture); + ballSprite.anchor.set(0.5); + ballSprite.width = ball.radius * 2; + ballSprite.height = ball.radius * 2; + + return ballSprite; } /** * Создает визуальное отображение ракетки с помощью Pixi.js * @param {Paddle} paddle экземпляр класса ракетка + * @param {object} textures объект с текстурами для визуального отображения */ -function createPaddleView(paddle) { - return new Graphics().rect(0, 0, paddle.width, paddle.height).fill('#fff000'); +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; + + return paddleSprite; } /** * Создает визуальное отображение кирпича с помощью Pixi.js * @param {Brick} brick экземпляр класса кирпич * @returns {Graphics} графическое отображение кирпича + * @param {object} textures объект с текстурами для визуального отображения */ -function createBrickView(brick) { - const brickView = new Graphics().rect(0, 0, brick.width, brick.height); +function createBrickView(brick, textures) { + const bricksTextures = [ + textures['/sprites/block_1.png'], + textures['/sprites/block_2.png'], + textures['/sprites/block_3.png'], + ]; - switch (brick.type) { - case 2: - return brickView.fill('#00ff00'); - case 3: - return brickView.fill('#ff00ff'); - case 1: - default: - return brickView.fill('#000fff'); - } + const brickSprite = new Sprite(bricksTextures[brick.type - 1]); + brickSprite.width = brick.width; + brickSprite.height = brick.height; + + return brickSprite; } /** * Создает визуальное отображение бонуса с помощью Pixi.js * @param {Perk} perk экземпляр класса бонус * @returns {Graphics} графическое отображение бонуса + * @param {object} textures текстуры блоков */ -function createPerkView(perk) { - const perkView = new Graphics().rect(0, 0, perk.width, perk.height); +function createPerkView(perk, textures) { + let texture; switch (perk.type) { case 'slow': - return perkView.fill('#00ffff'); + texture = textures['/sprites/perk_1.png']; + break; case 'wide': - return perkView.fill('#0f0f0f'); + texture = textures['/sprites/perk_2.png']; + break; case 'life': - return perkView.fill('#f0f0f0'); + texture = textures['/sprites/perk_3.png']; + break; + case 'clone': + texture = textures['/sprites/perk_3.png']; + break; default: - return perkView.fill('#ffffff'); + texture = textures['/sprites/perk_1.png']; + break; } + + const perkSprite = new Sprite(texture); + perkSprite.width = perk.width; + perkSprite.height = perk.height; + + return perkSprite; } /** * Создает визуальное отображение всех сущностей в игре с помощью Pixi.js и добавляет в контейнер * @param {Game} game экземпляр класса игра * @param {Container} container экземпляр класса контейнер Pixi.js + * @param {object} textures объект с текстурами для визуального отображения */ -export function createGameView(game, container) { +export function createGameView(game, container, textures) { try { + const background = new Sprite(textures['/sprites/background_1.png']); + background.width = CONTAINER_WIDTH; + background.height = CONTAINER_HEIGHT; + container.addChildAt(background, 0); + + const header = new Text({ style: { fill: '#ffffff', fontSize: 16 } }); + header.x = 8; + header.y = 8; + container.addChild(header); + const balls = new Map(); for (const ball of game.balls) { - const ballView = createBallView(ball); + const ballView = createBallView(ball, textures); container.addChild(ballView); balls.set(ball, ballView); } - const paddle = createPaddleView(game.paddle); + const paddle = createPaddleView(game.paddle, textures); container.addChild(paddle); const bricks = game.bricks.map((brick) => { - const brickView = createBrickView(brick); + const brickView = createBrickView(brick, textures); container.addChild(brickView); return brickView; }); @@ -86,6 +126,7 @@ export function createGameView(game, container) { const perks = new Map(); return { + header, balls, paddle, bricks, @@ -102,13 +143,11 @@ export function createGameView(game, container) { * @param {object} views объект с визуальными отображениями сущностей игры * @param {Game} game экземпляр класса игра * @param {Container} container экземпляр класса контейнер Pixi.js + * @param {object} textures объект с текстурами для визуального отображения */ -export function managePerkViewsLifetime(views, game, container) { +export function managePerkViewsLifetime(views, game, container, textures) { if (views.paddle.width !== game.paddle.width) { - container.removeChild(views.paddle); - views.paddle.destroy(); - views.paddle = createPaddleView(game.paddle); - container.addChild(views.paddle); + views.paddle.width = game.paddle.width; } for (const [ball, ballView] of views.balls) { @@ -121,7 +160,7 @@ export function managePerkViewsLifetime(views, game, container) { for (const ball of game.balls) { if (!views.balls.has(ball)) { - const ballView = createBallView(ball); + const ballView = createBallView(ball, textures); container.addChild(ballView); views.balls.set(ball, ballView); } @@ -137,7 +176,7 @@ export function managePerkViewsLifetime(views, game, container) { for (const perk of game.perks) { if (!views.perks.has(perk)) { - const perkView = createPerkView(perk); + const perkView = createPerkView(perk, textures); container.addChild(perkView); views.perks.set(perk, perkView); } @@ -155,6 +194,8 @@ export function syncronizeViewsWithGame(views, game) { throw new Error('Аргумент game должен быть экземпляром класса Game'); } + views.header.text = `Уровень: ${game.currentLevel + 1} Количество жизней: ${game.livesAmount}`; + for (const ball of game.balls) { const ballView = views.balls.get(ball); ballView.x = ball.x; @@ -187,15 +228,16 @@ export function syncronizeViewsWithGame(views, game) { * @param {object} views объект с визуальными отображениями сущностей игры * @param {Game} game экземпляр класса игра * @param {Container} container экземпляр класса контейнер Pixi.js + * @param {object} textures объект с текстурами для визуального отображения */ -export function rebuildBrickViews(views, game, container) { +export function rebuildBrickViews(views, game, container, textures) { for (const brickView of views.bricks) { container.removeChild(brickView); brickView.destroy(); } views.bricks = game.bricks.map((brick) => { - const brickView = createBrickView(brick); + const brickView = createBrickView(brick, textures); container.addChild(brickView); return brickView; });