Feature/advanced game mechanics #5
+2
-1
@@ -13,6 +13,7 @@ export const BALL_SPEED = 3;
|
|||||||
export const BALL_SPEED_LEVEL_STEP = 2;
|
export const BALL_SPEED_LEVEL_STEP = 2;
|
||||||
export const BALL_SPEED_TIME_STEP = 0.002;
|
export const BALL_SPEED_TIME_STEP = 0.002;
|
||||||
export const BALL_INITIAL_ANGLE = 0;
|
export const BALL_INITIAL_ANGLE = 0;
|
||||||
|
export const BALL_SPLIT_ANGLE = 20;
|
||||||
|
|
||||||
export const BRICK_WIDTH = 40;
|
export const BRICK_WIDTH = 40;
|
||||||
export const BRICK_HEIGHT = 10;
|
export const BRICK_HEIGHT = 10;
|
||||||
@@ -25,4 +26,4 @@ export const PERK_HEIGHT = 16;
|
|||||||
export const PERK_FALL_SPEED = 3;
|
export const PERK_FALL_SPEED = 3;
|
||||||
export const PERK_DROP_CHANCE = 0.3;
|
export const PERK_DROP_CHANCE = 0.3;
|
||||||
export const PERK_BALL_SPEED_DECREASE = 2;
|
export const PERK_BALL_SPEED_DECREASE = 2;
|
||||||
export const PERK_TYPES = ['slow', 'wide', 'life'];
|
export const PERK_TYPES = ['slow', 'wide', 'life', 'clone'];
|
||||||
|
|||||||
+20
-4
@@ -32,13 +32,21 @@ export class Game {
|
|||||||
this.maxLevel = levels.length - 1;
|
this.maxLevel = levels.length - 1;
|
||||||
|
|
||||||
this.paddle = new Paddle(0, CONTAINER_HEIGHT - PADDLE_HEIGHT, PADDLE_WIDTH, PADDLE_HEIGHT);
|
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.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);
|
||||||
this.perks = [];
|
this.perks = [];
|
||||||
|
|
||||||
this._placeBallOnPaddle();
|
this._placeBallOnPaddle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Основной (первый) мяч. Пока играет один мяч - это он.
|
||||||
|
* @returns {Ball}
|
||||||
|
*/
|
||||||
|
get ball() {
|
||||||
|
return this.balls[0];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Изменяет значения сущностей в зависимости от времени
|
* Изменяет значения сущностей в зависимости от времени
|
||||||
* @param {*} deltaTime изменение времени из Ticker
|
* @param {*} deltaTime изменение времени из Ticker
|
||||||
@@ -56,7 +64,10 @@ export class Game {
|
|||||||
|
|
||||||
tick(this, CONTAINER_WIDTH, CONTAINER_HEIGHT, deltaTime);
|
tick(this, CONTAINER_WIDTH, CONTAINER_HEIGHT, deltaTime);
|
||||||
|
|
||||||
if (this.ball.status === 'out') {
|
// Убираем улетевшие мячи. Жизнь теряется только когда не осталось ни одного
|
||||||
|
const survivingBalls = this.balls.filter((ball) => ball.status !== 'out');
|
||||||
|
|
||||||
|
if (survivingBalls.length === 0) {
|
||||||
this.livesAmount -= 1;
|
this.livesAmount -= 1;
|
||||||
|
|
||||||
if (this.livesAmount === 0) {
|
if (this.livesAmount === 0) {
|
||||||
@@ -68,7 +79,11 @@ export class Game {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ball.increaseSpeed(BALL_SPEED_TIME_STEP * deltaTime);
|
this.balls = survivingBalls;
|
||||||
|
|
||||||
|
for (const ball of this.balls) {
|
||||||
|
ball.increaseSpeed(BALL_SPEED_TIME_STEP * deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
const isLevelComplete = this._checkLevelCompletion();
|
const isLevelComplete = this._checkLevelCompletion();
|
||||||
|
|
||||||
@@ -101,9 +116,10 @@ export class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ставит мяч по центру ракетки
|
* Ставит мяч по центру ракетки, оставляя один мяч в игре
|
||||||
*/
|
*/
|
||||||
_placeBallOnPaddle() {
|
_placeBallOnPaddle() {
|
||||||
|
this.balls = [this.ball];
|
||||||
this.ball.reset(this.paddle.x + this.paddle.width / 2, this.paddle.y - this.ball.radius);
|
this.ball.reset(this.paddle.x + this.paddle.width / 2, this.paddle.y - this.ball.radius);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-78
@@ -1,18 +1,8 @@
|
|||||||
import {
|
import { MAX_PADDLE_REFLECTION_ANGLE, MIN_PADDLE_REFLECTION_ANGLE, PADDLE_SECTOR_AMOUNT } from '../../config';
|
||||||
CONTAINER_HEIGHT,
|
|
||||||
CONTAINER_WIDTH,
|
|
||||||
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 { Game } from '../../game';
|
||||||
import { calculateAABBCollision } from '../calculateAABBCollision/calculateAABBCollision';
|
import { processBall } from '../processBall/processBall';
|
||||||
import { calculateDirection } from '../calculateDirection/calculateDirection';
|
import { processBrickCollision } from '../processBrickCollision/processBrickCollision';
|
||||||
import { processReflection } from '../processReflection/processReflection';
|
import { processReflection } from '../processReflection/processReflection';
|
||||||
import { spawnRandomPerk } from '../spawnRandomPerk/spawnRandomPerk';
|
|
||||||
import { updatePerks } from '../updatePerks/updatePerks';
|
import { updatePerks } from '../updatePerks/updatePerks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,80 +30,25 @@ export function tick(game, containerWidth, containerHeight, deltaTime) {
|
|||||||
throw new Error('Значение изменения времени должно быть положительным числом');
|
throw new Error('Значение изменения времени должно быть положительным числом');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { ball, paddle, bricks } = game;
|
const { paddle } = game;
|
||||||
|
|
||||||
ball.moveForward(deltaTime);
|
|
||||||
|
|
||||||
// Бонусы падают и ловятся ракеткой каждый кадр
|
// Бонусы падают и ловятся ракеткой каждый кадр
|
||||||
updatePerks(game, containerHeight, deltaTime);
|
updatePerks(game, containerHeight, deltaTime);
|
||||||
|
|
||||||
const leftBoundary = ball.radius;
|
for (const ball of game.balls) {
|
||||||
const rightBoundary = containerWidth - ball.radius;
|
ball.moveForward(deltaTime);
|
||||||
const topBoundary = ball.radius;
|
// Проверка столкновения со стенами и вылета за границу поля снизу
|
||||||
const bottomBoundary = containerHeight - ball.radius;
|
const hitWallOrWentOut = processBall(ball, containerWidth, containerHeight);
|
||||||
|
|
||||||
// Не даем мячу выйти за границы стен слева / справав и меняем направление
|
if (hitWallOrWentOut) {
|
||||||
if (ball.x <= leftBoundary || ball.x >= rightBoundary) {
|
|
||||||
ball.x = ball.x <= leftBoundary ? leftBoundary : rightBoundary;
|
|
||||||
ball.horizontalSpeed *= -1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Не даем мячу выйти за границу стены сверху и меняем направление
|
|
||||||
if (ball.y <= topBoundary) {
|
|
||||||
ball.y = topBoundary;
|
|
||||||
ball.verticalSpeed *= -1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем выход за границу стены снизу
|
|
||||||
if (ball.y >= bottomBoundary) {
|
|
||||||
game.ball.status = 'out';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Базоввое взаимодействие мяча и кирпича
|
|
||||||
for (const brick of bricks) {
|
|
||||||
if (!brick.alive) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ballLeft = ball.x - ball.radius;
|
// Проверка столкновения с кирпичами
|
||||||
const ballRight = ball.x + ball.radius;
|
const hitBrick = processBrickCollision(game, ball);
|
||||||
const ballTop = ball.y - ball.radius;
|
|
||||||
const ballBottom = ball.y + ball.radius;
|
|
||||||
|
|
||||||
const brickLeft = brick.x;
|
if (hitBrick) {
|
||||||
const brickRight = brick.x + brick.width;
|
continue;
|
||||||
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 = calculateAABBCollision(ballObject, brickObject);
|
|
||||||
|
|
||||||
if (isCollided) {
|
|
||||||
const directions = calculateDirection(ballObject, brickObject);
|
|
||||||
|
|
||||||
if (directions !== null) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Взаимодействие мяча и ракетки - обновление горизонтальной и вертикальной скорости в зависимости от сектора попадания
|
// Взаимодействие мяча и ракетки - обновление горизонтальной и вертикальной скорости в зависимости от сектора попадания
|
||||||
@@ -125,6 +60,7 @@ export function tick(game, containerWidth, containerHeight, deltaTime) {
|
|||||||
MAX_PADDLE_REFLECTION_ANGLE,
|
MAX_PADDLE_REFLECTION_ANGLE,
|
||||||
ball.speed,
|
ball.speed,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { CONTAINER_WIDTH, PADDLE_WIDE_WIDTH, PERK_BALL_SPEED_DECREASE } from '../../config';
|
import { BALL_SPLIT_ANGLE, CONTAINER_WIDTH, PADDLE_WIDE_WIDTH, PERK_BALL_SPEED_DECREASE } from '../../config';
|
||||||
|
import { clone } from '../../entities/ball/clone/clone';
|
||||||
import { Game } from '../../game';
|
import { Game } from '../../game';
|
||||||
import { calculateAABBCollision } from '../calculateAABBCollision/calculateAABBCollision';
|
import { calculateAABBCollision } from '../calculateAABBCollision/calculateAABBCollision';
|
||||||
|
|
||||||
@@ -45,8 +46,10 @@ export function updatePerks(game, containerHeight, deltaTime) {
|
|||||||
if (isCollided) {
|
if (isCollided) {
|
||||||
switch (perk.type) {
|
switch (perk.type) {
|
||||||
case 'slow':
|
case 'slow':
|
||||||
if (game.ball.speed - PERK_BALL_SPEED_DECREASE > 0) {
|
for (const ball of game.balls) {
|
||||||
game.ball.increaseSpeed(-1 * PERK_BALL_SPEED_DECREASE);
|
if (ball.speed - PERK_BALL_SPEED_DECREASE > 0) {
|
||||||
|
ball.increaseSpeed(-1 * PERK_BALL_SPEED_DECREASE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'wide':
|
case 'wide':
|
||||||
@@ -56,6 +59,10 @@ export function updatePerks(game, containerHeight, deltaTime) {
|
|||||||
case 'life':
|
case 'life':
|
||||||
game.livesAmount += 1;
|
game.livesAmount += 1;
|
||||||
break;
|
break;
|
||||||
|
case 'clone':
|
||||||
|
// Основной мяч делится на два клона, разлетающихся под углом
|
||||||
|
game.balls.push(clone(game.ball, BALL_SPLIT_ANGLE), clone(game.ball, -BALL_SPLIT_ANGLE));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,17 @@ describe('updatePerks', () => {
|
|||||||
expect(paddle.width).toBe(PADDLE_WIDE_WIDTH);
|
expect(paddle.width).toBe(PADDLE_WIDE_WIDTH);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Ловит бонус clone, разделяет мяч на три', () => {
|
||||||
|
const game = new Game([[[1]]]);
|
||||||
|
const { paddle } = game;
|
||||||
|
game.perks.push(new Perk(paddle.x, paddle.y - 1, 10, 10, 'clone', 0));
|
||||||
|
|
||||||
|
updatePerks(game, 600, 1);
|
||||||
|
|
||||||
|
expect(game.perks).toHaveLength(0);
|
||||||
|
expect(game.balls).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
it('Убирает бонус, улетевший за нижнюю границу', () => {
|
it('Убирает бонус, улетевший за нижнюю границу', () => {
|
||||||
const game = new Game([[[1]]]);
|
const game = new Game([[[1]]]);
|
||||||
game.perks.push(new Perk(0, 601, 10, 10, 'slow', 0));
|
game.perks.push(new Perk(0, 601, 10, 10, 'slow', 0));
|
||||||
|
|||||||
+28
-5
@@ -67,8 +67,12 @@ function createPerkView(perk) {
|
|||||||
*/
|
*/
|
||||||
export function createGameView(game, container) {
|
export function createGameView(game, container) {
|
||||||
try {
|
try {
|
||||||
const ball = createBallView(game.ball);
|
const balls = new Map();
|
||||||
container.addChild(ball);
|
for (const ball of game.balls) {
|
||||||
|
const ballView = createBallView(ball);
|
||||||
|
container.addChild(ballView);
|
||||||
|
balls.set(ball, ballView);
|
||||||
|
}
|
||||||
|
|
||||||
const paddle = createPaddleView(game.paddle);
|
const paddle = createPaddleView(game.paddle);
|
||||||
container.addChild(paddle);
|
container.addChild(paddle);
|
||||||
@@ -82,7 +86,7 @@ export function createGameView(game, container) {
|
|||||||
const perks = new Map();
|
const perks = new Map();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ball,
|
balls,
|
||||||
paddle,
|
paddle,
|
||||||
bricks,
|
bricks,
|
||||||
perks,
|
perks,
|
||||||
@@ -107,6 +111,22 @@ export function managePerkViewsLifetime(views, game, container) {
|
|||||||
container.addChild(views.paddle);
|
container.addChild(views.paddle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const [ball, ballView] of views.balls) {
|
||||||
|
if (!game.balls.includes(ball)) {
|
||||||
|
container.removeChild(ballView);
|
||||||
|
ballView.destroy();
|
||||||
|
views.balls.delete(ball);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ball of game.balls) {
|
||||||
|
if (!views.balls.has(ball)) {
|
||||||
|
const ballView = createBallView(ball);
|
||||||
|
container.addChild(ballView);
|
||||||
|
views.balls.set(ball, ballView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const [perk, perkView] of views.perks) {
|
for (const [perk, perkView] of views.perks) {
|
||||||
if (!game.perks.includes(perk)) {
|
if (!game.perks.includes(perk)) {
|
||||||
container.removeChild(perkView);
|
container.removeChild(perkView);
|
||||||
@@ -135,8 +155,11 @@ export function syncronizeViewsWithGame(views, game) {
|
|||||||
throw new Error('Аргумент game должен быть экземпляром класса Game');
|
throw new Error('Аргумент game должен быть экземпляром класса Game');
|
||||||
}
|
}
|
||||||
|
|
||||||
views.ball.x = game.ball.x;
|
for (const ball of game.balls) {
|
||||||
views.ball.y = game.ball.y;
|
const ballView = views.balls.get(ball);
|
||||||
|
ballView.x = ball.x;
|
||||||
|
ballView.y = ball.y;
|
||||||
|
}
|
||||||
|
|
||||||
views.paddle.x = game.paddle.x;
|
views.paddle.x = game.paddle.x;
|
||||||
views.paddle.y = game.paddle.y;
|
views.paddle.y = game.paddle.y;
|
||||||
|
|||||||
Reference in New Issue
Block a user