Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
494878c14b | ||
|
|
6302fd2f91 | ||
|
|
4505a846bb | ||
|
|
4d572e8c86 | ||
|
|
8cfe62a064 | ||
|
|
3432d02eaf | ||
|
|
27f230d8f6 | ||
|
|
00cb33b9f9 | ||
|
|
8153233529 | ||
|
|
a16f316b13 | ||
|
|
a11ab8cd5d | ||
|
|
5e9b70e603 | ||
|
|
7a36d23393 | ||
|
|
89bf5289b1 | ||
|
|
5739575200 | ||
|
|
4608e044b0 | ||
|
|
6430470478 | ||
|
|
2dc8735fe6 | ||
|
|
004720acd6 | ||
|
|
2cbff5b924 | ||
|
|
8f593c6a6f | ||
|
|
afe028430f | ||
|
|
48fb992d04 | ||
|
|
b72583290d | ||
|
|
a734600390 | ||
|
|
dccdb02f73 | ||
|
|
e637d4e62e | ||
|
|
d2cfee3ec3 | ||
|
|
8d15f3996f | ||
|
|
b7e16f86ea | ||
|
|
511b20c939 | ||
|
|
a4c4e2a192 | ||
|
|
b2f0810ec2 | ||
|
|
b05fb89bc5 | ||
|
|
62abd8b77d | ||
|
|
0f057c1add | ||
|
|
e7b52bdbd9 | ||
|
|
500803db84 | ||
|
|
5e259f7efc | ||
|
|
111167b600 | ||
|
|
4436661547 | ||
|
|
71583d0fde |
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/install-state.gz
|
||||||
|
dist
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
:3000 {
|
||||||
|
root * /usr/share/caddy
|
||||||
|
|
||||||
|
encode {
|
||||||
|
zstd
|
||||||
|
gzip
|
||||||
|
match {
|
||||||
|
header Content-Type text/*
|
||||||
|
header Content-Type application/javascript*
|
||||||
|
header Content-Type application/json*
|
||||||
|
header Content-Type image/svg+xml*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@assets path /assets/*
|
||||||
|
header @assets Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
|
||||||
|
@html path / /index.html
|
||||||
|
header @html Cache-Control "no-cache"
|
||||||
|
|
||||||
|
file_server
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
# Enable Corepack so we can use Yarn v4 (pinned to match lockfile)
|
||||||
|
RUN corepack enable && corepack prepare yarn@4.11.0 --activate
|
||||||
|
# Force Yarn to use node_modules instead of PnP
|
||||||
|
ENV YARN_NODE_LINKER=node-modules
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
RUN yarn install --immutable
|
||||||
|
COPY . .
|
||||||
|
RUN yarn build && ls -la dist
|
||||||
|
|
||||||
|
# Production stage - Caddy
|
||||||
|
FROM caddy:2-alpine
|
||||||
|
WORKDIR /usr/share/caddy
|
||||||
|
# Copy built static files from the builder stage
|
||||||
|
COPY --from=builder /app/dist .
|
||||||
|
# Copy our local Caddyfile config
|
||||||
|
COPY Caddyfile /etc/caddy/Caddyfile
|
||||||
|
EXPOSE 3000
|
||||||
|
# Start caddy using the config file
|
||||||
|
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Arkanoid
|
||||||
|
|
||||||
|
Вариация игры арканоид.
|
||||||
|
- Три уровня нарастающих по сложности (по крайней мере так задумано)
|
||||||
|
- Ракетка двигается по движению курсора
|
||||||
|
- Логика отскока мяча от ракетки реализована через разбиение ракетки на сектора и различным углом отскока
|
||||||
|
- Блоки трех типов: обычный, повышеной прочности и неразбиваемый
|
||||||
|
- Разные очки за обычный блок и блок повышеной прочности
|
||||||
|
- Скорость мяча увеличивается скачками от уровня к уровню и постепенно в течение одного уровня
|
||||||
|
- Из разбитых блоков со случайным шансом выпадают различные типы бонусов: замедление мяча, дополнительная жизнь, увеличение ширины ракетки, добавление двух дополнительных мячей.
|
||||||
|
- Текстуры сущностей взяты из публично доступных бесплатных источников
|
||||||
|
|
||||||
|
## Запуск проекта
|
||||||
|
|
||||||
|
Установка зависимостей: `yarn install`
|
||||||
|
|
||||||
|
Запуск в dev режиме: `yarn dev`
|
||||||
|
|
||||||
|
Сборка: `yarn build`
|
||||||
|
|
||||||
|
Запуск тестов: `yarn test`
|
||||||
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 267 KiB |
|
After Width: | Height: | Size: 270 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 278 KiB |
@@ -2,6 +2,7 @@ export const CONTAINER_WIDTH = 800;
|
|||||||
export const CONTAINER_HEIGHT = 600;
|
export const CONTAINER_HEIGHT = 600;
|
||||||
|
|
||||||
export const PADDLE_WIDTH = 50;
|
export const PADDLE_WIDTH = 50;
|
||||||
|
export const PADDLE_WIDE_WIDTH = 100;
|
||||||
export const PADDLE_HEIGHT = 10;
|
export const PADDLE_HEIGHT = 10;
|
||||||
export const MIN_PADDLE_REFLECTION_ANGLE = 20;
|
export const MIN_PADDLE_REFLECTION_ANGLE = 20;
|
||||||
export const MAX_PADDLE_REFLECTION_ANGLE = 160;
|
export const MAX_PADDLE_REFLECTION_ANGLE = 160;
|
||||||
@@ -9,10 +10,24 @@ export const PADDLE_SECTOR_AMOUNT = 20;
|
|||||||
|
|
||||||
export const BALL_RADIUS = 10;
|
export const BALL_RADIUS = 10;
|
||||||
export const BALL_SPEED = 3;
|
export const BALL_SPEED = 3;
|
||||||
|
export const BALL_SPEED_LEVEL_STEP = 2;
|
||||||
|
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 HEADER_HEIGHT = 30;
|
||||||
|
|
||||||
export const BRICK_WIDTH = 40;
|
export const BRICK_WIDTH = 40;
|
||||||
export const BRICK_HEIGHT = 10;
|
export const BRICK_HEIGHT = 10;
|
||||||
|
|
||||||
export const BRICK_ROW_AMOUNT = 5;
|
export const BRICK_ROW_AMOUNT = 5;
|
||||||
export const BRICK_COLUMN_AMOUNT = 20;
|
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;
|
||||||
|
export const PERK_FALL_SPEED = 3;
|
||||||
|
export const PERK_DROP_CHANCE = 0.3;
|
||||||
|
export const PERK_BALL_SPEED_DECREASE = 2;
|
||||||
|
export const PERK_TYPES = ['slow', 'wide', 'life', 'clone'];
|
||||||
|
export const PERK_DURATION_SEC = 20;
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Список уровней игры в форме массива
|
||||||
|
* 0 - пустое пространство
|
||||||
|
* 1 - обычный блок
|
||||||
|
* 2 - блок с несколькими жизнями
|
||||||
|
* 3 - неразрушимый блок
|
||||||
|
*/
|
||||||
|
export const LEVELS = [
|
||||||
|
[
|
||||||
|
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||||
|
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||||
|
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||||
|
[0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
|
||||||
|
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
|
||||||
|
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
|
||||||
|
[1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2],
|
||||||
|
[2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||||
|
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||||
|
[0, 2, 1, 2, 0, 0, 1, 2, 1, 0, 0, 2, 1, 2, 1, 0, 1, 2, 1, 0],
|
||||||
|
[0, 0, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, 0],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2],
|
||||||
|
[2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1],
|
||||||
|
[1, 2, 1, 2, 1, 2, 1, 2, 3, 3, 3, 3, 1, 2, 1, 2, 1, 2, 1, 2],
|
||||||
|
[2, 1, 2, 1, 2, 1, 2, 3, 3, 3, 3, 3, 3, 1, 2, 1, 2, 1, 2, 1],
|
||||||
|
[1, 2, 0, 2, 1, 2, 1, 0, 3, 3, 3, 3, 0, 2, 1, 2, 1, 0, 1, 2],
|
||||||
|
[2, 0, 0, 0, 2, 1, 0, 0, 0, 1, 2, 0, 0, 0, 2, 1, 0, 0, 0, 1],
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { toRadians } from '../../lib/toRadians/toRadians';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Класс сущности "мяч"
|
* Класс сущности "мяч"
|
||||||
* @class
|
* @class
|
||||||
@@ -7,21 +9,23 @@ export class Ball {
|
|||||||
* @param {number} x координата положения мяча по оси X
|
* @param {number} x координата положения мяча по оси X
|
||||||
* @param {number} y координата положения мяча по оси Y
|
* @param {number} y координата положения мяча по оси Y
|
||||||
* @param {number} radius положительное числовое значение радиуса мяча
|
* @param {number} radius положительное числовое значение радиуса мяча
|
||||||
* @param {number} verticalSpeed вектор движения мяча по оси Y
|
* @param {number} speed положительное числовое значение скорости мяча
|
||||||
* @param {number} horizontalSpeed вектор движения мяча по оси X
|
* @param {number} angle угол направления движения мяча в градусах
|
||||||
*/
|
*/
|
||||||
constructor(x, y, radius, verticalSpeed = 0, horizontalSpeed = 0) {
|
constructor(x, y, radius, speed = 0, angle = 0) {
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
this.radius = radius;
|
this.radius = radius;
|
||||||
this.verticalSpeed = verticalSpeed;
|
this.speed = speed;
|
||||||
this.horizontalSpeed = horizontalSpeed;
|
this.angle = angle;
|
||||||
this.isOut = false;
|
this.verticalSpeed = speed * Math.cos(toRadians(angle));
|
||||||
|
this.horizontalSpeed = -1 * speed * Math.sin(toRadians(angle));
|
||||||
|
this.status = 'idle';
|
||||||
|
|
||||||
this.defaultX = x;
|
this.defaultX = x;
|
||||||
this.defaultY = y;
|
this.defaultY = y;
|
||||||
this.defaultVerticalSpeed = verticalSpeed;
|
this.defaultVerticalSpeed = this.verticalSpeed;
|
||||||
this.defaultHorizontalSpeed = horizontalSpeed;
|
this.defaultHorizontalSpeed = this.horizontalSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,11 +40,38 @@ export class Ball {
|
|||||||
/**
|
/**
|
||||||
* Возвращает значения к дефолтным
|
* Возвращает значения к дефолтным
|
||||||
*/
|
*/
|
||||||
reset() {
|
reset(x = this.defaultX, y = this.defaultY) {
|
||||||
this.x = this.defaultX;
|
this.x = x;
|
||||||
this.y = this.defaultY;
|
this.y = y;
|
||||||
this.verticalSpeed = this.defaultVerticalSpeed;
|
this.verticalSpeed = this.defaultVerticalSpeed;
|
||||||
this.horizontalSpeed = this.defaultHorizontalSpeed;
|
this.horizontalSpeed = this.defaultHorizontalSpeed;
|
||||||
this.isOut = false;
|
this.status = 'idle';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запускает мяч с ракетки - переводит из ожидания в игру
|
||||||
|
*/
|
||||||
|
launch() {
|
||||||
|
if (this.status === 'idle') {
|
||||||
|
this.status = 'moving';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Увеличивает скорость мяча не меняя направление
|
||||||
|
* @param {number} step положительное числовое значение прибавки к скорости
|
||||||
|
*/
|
||||||
|
increaseSpeed(step) {
|
||||||
|
if (this.speed <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const magnifyingCoeficient = (this.speed + step) / this.speed;
|
||||||
|
this.speed += step;
|
||||||
|
|
||||||
|
this.verticalSpeed *= magnifyingCoeficient;
|
||||||
|
this.horizontalSpeed *= magnifyingCoeficient;
|
||||||
|
this.defaultVerticalSpeed *= magnifyingCoeficient;
|
||||||
|
this.defaultHorizontalSpeed *= magnifyingCoeficient;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { toRadians } from '../../../lib/toRadians/toRadians';
|
||||||
|
import { Ball } from '../ball';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает копию мяча в той же точке с направлением, повернутым на заданный угол
|
||||||
|
* @param {Ball} ball исходный мяч
|
||||||
|
* @param {number} angle угол поворота направления в градусах
|
||||||
|
* @returns {Ball | null} новый мяч с повернутой скоростью
|
||||||
|
*/
|
||||||
|
export function clone(ball, angle) {
|
||||||
|
try {
|
||||||
|
if (!(ball instanceof Ball)) {
|
||||||
|
throw new Error('Аргумент ball должен быть экземпляром класса мяча');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof angle !== 'number') {
|
||||||
|
throw new Error('Угол поворота должен являться числом');
|
||||||
|
}
|
||||||
|
|
||||||
|
const clonedBall = new Ball(ball.x, ball.y, ball.radius, ball.speed);
|
||||||
|
const angleInRadians = toRadians(angle);
|
||||||
|
const cos = Math.cos(angleInRadians);
|
||||||
|
const sin = Math.sin(angleInRadians);
|
||||||
|
|
||||||
|
clonedBall.status = ball.status;
|
||||||
|
clonedBall.horizontalSpeed = ball.horizontalSpeed * cos - ball.verticalSpeed * sin;
|
||||||
|
clonedBall.verticalSpeed = ball.horizontalSpeed * sin + ball.verticalSpeed * cos;
|
||||||
|
clonedBall.defaultHorizontalSpeed = clonedBall.horizontalSpeed;
|
||||||
|
clonedBall.defaultVerticalSpeed = clonedBall.verticalSpeed;
|
||||||
|
|
||||||
|
return clonedBall;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Ball } from '../ball';
|
||||||
|
import { clone } from './clone';
|
||||||
|
|
||||||
|
describe('clone', () => {
|
||||||
|
it('Возвращает корректное значение при неверных типах аргументов', () => {
|
||||||
|
expect(clone(null, 20)).toBeNull();
|
||||||
|
expect(clone(new Ball(0, 0, 10, 3), '20')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Клонирует мяч и задает корректное значение скоростей по осям X и Y', () => {
|
||||||
|
const ball = new Ball(100, 200, 10, 90);
|
||||||
|
ball.status = 'moving';
|
||||||
|
ball.horizontalSpeed = 3;
|
||||||
|
ball.verticalSpeed = 4;
|
||||||
|
|
||||||
|
const clonedBall = clone(ball, 90);
|
||||||
|
|
||||||
|
expect(clonedBall).toBeInstanceOf(Ball);
|
||||||
|
expect(clonedBall).not.toEqual(ball);
|
||||||
|
expect(clonedBall.x).toBe(100);
|
||||||
|
expect(clonedBall.y).toBe(200);
|
||||||
|
expect(clonedBall.status).toBe('moving');
|
||||||
|
expect(clonedBall.horizontalSpeed).toBeCloseTo(-4);
|
||||||
|
expect(clonedBall.verticalSpeed).toBeCloseTo(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,16 +9,28 @@ export class Brick {
|
|||||||
* @param {number} y координата положения кирпича по оси Y
|
* @param {number} y координата положения кирпича по оси Y
|
||||||
* @param {number} width положительное числовое значение ширины кирпича
|
* @param {number} width положительное числовое значение ширины кирпича
|
||||||
* @param {number} height положительное числовое значение высоты кирпича
|
* @param {number} height положительное числовое значение высоты кирпича
|
||||||
|
* @param {number} type тип блока. 1 - обычный, 2 - больше 1 жизни, 3 - неразрушаемый
|
||||||
*/
|
*/
|
||||||
constructor(x, y, width, height) {
|
constructor(x, y, width, height, type) {
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
|
this.type = type;
|
||||||
this.alive = true;
|
this.alive = true;
|
||||||
|
this.livesAmount = this.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* В зависимости от типа и оставшихся жизней убивает кирпич
|
||||||
|
*/
|
||||||
kill() {
|
kill() {
|
||||||
this.alive = false;
|
if (this.type !== 3) {
|
||||||
|
this.livesAmount -= 1;
|
||||||
|
|
||||||
|
if (this.livesAmount === 0) {
|
||||||
|
this.alive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,40 @@
|
|||||||
import { Brick } from '../brick';
|
import { Brick } from '../brick';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ложит кирпичи по заданым размерам
|
* Создает массив кирпичей проинициализированных значениями в зависимости от расположения на карте уровня
|
||||||
* @param {number} columnAmount количество кирпичей по оси X, неотрицателное, целое число
|
* @param {number[][]} levelMap карта уровня в формате массива
|
||||||
* @param {number} rowAmount количество кирпичей по оси Y, неотрицателное, целое число
|
|
||||||
* @param {number} brickWidth ширина кирпича, неотрицателное число
|
* @param {number} brickWidth ширина кирпича, неотрицателное число
|
||||||
* @param {number} brickHeight длина кирпича, неотрицателное число
|
* @param {number} brickHeight длина кирпича, неотрицателное число
|
||||||
* @returns {Brick[][]} массив с массивами кирпичей
|
* @param {number} marginTop отступ сверху, неотрицательное число
|
||||||
|
* @returns {Brick[]} массив кирпичей
|
||||||
*/
|
*/
|
||||||
export function layBricks(columnAmount, rowAmount, brickWidth, brickHeight) {
|
export function layBricks(levelMap, brickWidth, brickHeight, marginTop = 0) {
|
||||||
try {
|
try {
|
||||||
const someArgsArentNumbers = [columnAmount, rowAmount, brickWidth, brickHeight].some(
|
if (!(Array.isArray(levelMap) && levelMap.every(Array.isArray))) {
|
||||||
(arg) => typeof arg !== 'number',
|
throw new Error('Значение карты уровня должно являться вложенным масивом чисел глубины 2');
|
||||||
);
|
|
||||||
if (someArgsArentNumbers) {
|
|
||||||
throw new Error('Параметры кладки кирпичей должны являться числами');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const someArgsLessThanZero = [columnAmount, rowAmount, brickWidth, brickHeight].some((arg) => arg < 0);
|
if (!(typeof brickWidth === 'number' && typeof brickHeight === 'number')) {
|
||||||
if (someArgsLessThanZero) {
|
throw new Error('Значения ширины и высоты кирпича должны являться числами');
|
||||||
throw new Error('Параметры кладки кирпичей должны являться положительными целыми числами');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const rowsOrColumnsAreFloat = [columnAmount, rowAmount].some((amount) => amount % 1 !== 0);
|
if (brickWidth < 0 || brickHeight < 0) {
|
||||||
if (rowsOrColumnsAreFloat) {
|
throw new Error('Значения ширины и высоты кирпича должны являться положительными целыми числами');
|
||||||
throw new Error('Размеры рядов и колонок должны являться целыми числами');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const bricks = Array.from({ length: rowAmount }).map((_, rowIndex) =>
|
if (typeof marginTop !== 'number' || marginTop < 0) {
|
||||||
Array.from({ length: columnAmount }).map(
|
throw new Error('Значение отступа сверху должно быть неотрицательным числом');
|
||||||
(_, columnIndex) => new Brick(columnIndex * brickWidth, rowIndex * brickHeight, brickWidth, brickHeight),
|
}
|
||||||
),
|
|
||||||
);
|
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, marginTop + i * brickHeight, brickWidth, brickHeight, levelMap[i][j]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return bricks;
|
return bricks;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -5,54 +5,95 @@ describe('layBricks', () => {
|
|||||||
it('Возвращает корректное значение в случае неверного типа параметра', () => {
|
it('Возвращает корректное значение в случае неверного типа параметра', () => {
|
||||||
const wrongType = 'wrong';
|
const wrongType = 'wrong';
|
||||||
|
|
||||||
expect(layBricks(wrongType, 1, 1, 1)).toBeNull();
|
expect(layBricks(wrongType, 1, 1)).toBeNull();
|
||||||
expect(layBricks(1, wrongType, 1, 1)).toBeNull();
|
expect(layBricks(1, wrongType, 1)).toBeNull();
|
||||||
expect(layBricks(1, 1, wrongType, 1)).toBeNull();
|
expect(layBricks(1, 1, wrongType)).toBeNull();
|
||||||
expect(layBricks(1, 1, 1, wrongType)).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Возвращает корректное значение в случае неверных размеров кирпича', () => {
|
it('Возвращает корректное значение в случае неверных размеров кирпича', () => {
|
||||||
const wrongWidth = -1;
|
const wrongWidth = -1;
|
||||||
const wrongHeight = -1;
|
const wrongHeight = -1;
|
||||||
|
|
||||||
expect(layBricks(1, 1, wrongWidth, 1)).toBeNull();
|
expect(layBricks(1, wrongWidth, 1)).toBeNull();
|
||||||
expect(layBricks(1, 1, 1, wrongHeight)).toBeNull();
|
expect(layBricks(1, 1, wrongHeight)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Возвращает корректное значение в случае неверных значений рядов и колонок', () => {
|
it('Возвращает корректное значение в случае некоректной карты уровня', () => {
|
||||||
const negativeRow = -1;
|
const levelMap = ['brick', 'brick'];
|
||||||
const negativeColumn = -1;
|
|
||||||
const decimalRow = 1.5;
|
|
||||||
const decimalColumn = 1.5;
|
|
||||||
|
|
||||||
expect(layBricks(negativeRow, 1, 1, 1)).toBeNull();
|
expect(layBricks(levelMap, 1, 1)).toBeNull();
|
||||||
expect(layBricks(1, negativeColumn, 1, 1)).toBeNull();
|
});
|
||||||
expect(layBricks(decimalRow, 1, 1, 1)).toBeNull();
|
|
||||||
expect(layBricks(1, decimalColumn, 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('Возвращает массив корректных размеров', () => {
|
it('Возвращает массив корректных размеров', () => {
|
||||||
const columnAmount = 10;
|
const levelMap = [
|
||||||
const rowAmount = 5;
|
[1, 1, 1, 1, 1],
|
||||||
|
[1, 1, 1, 1, 1],
|
||||||
|
[1, 1, 1, 1, 1],
|
||||||
|
];
|
||||||
|
|
||||||
const bricks = layBricks(columnAmount, rowAmount, 1, 1);
|
const bricks = layBricks(levelMap, 1, 1);
|
||||||
expect(bricks.length).toEqual(rowAmount);
|
expect(bricks.length).toBe(levelMap.flat().length);
|
||||||
for (const row of bricks) {
|
|
||||||
expect(row.length).toEqual(columnAmount);
|
const emptyLevelMap = [
|
||||||
}
|
[0, 0, 0, 0, 0],
|
||||||
|
[0, 0, 0, 0, 0],
|
||||||
|
[0, 0, 0, 0, 0],
|
||||||
|
];
|
||||||
|
const emptyBricksArray = layBricks(emptyLevelMap, 1, 1);
|
||||||
|
expect(emptyBricksArray.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Задает корректные координаты кирпичам', () => {
|
it('Задает корректные координаты кирпичам', () => {
|
||||||
const bricks = layBricks(3, 3, 1, 1);
|
const levelMap = [
|
||||||
const columnAmount = 10;
|
[1, 1],
|
||||||
const rowAmount = 5;
|
[1, 1],
|
||||||
|
];
|
||||||
|
const brickWidth = 1;
|
||||||
|
const brickHeight = 1;
|
||||||
|
|
||||||
const brickss = layBricks(columnAmount, rowAmount, 1, 1);
|
const bricks = layBricks(levelMap, brickWidth, brickHeight);
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
for (let i = 0; i < rowAmount; i++) {
|
for (let i = 0; i < levelMap.length; i++) {
|
||||||
for (let j = 0; j < columnAmount; j++) {
|
for (let j = 0; j < levelMap[i].length; j++) {
|
||||||
expect(brickss[i][j].x).toEqual(j);
|
if (levelMap[i][j] === 0) {
|
||||||
expect(brickss[i][j]?.y).toEqual(i);
|
continue;
|
||||||
|
}
|
||||||
|
expect(bricks[index].x).toBe(j * brickWidth);
|
||||||
|
expect(bricks[index].y).toBe(i * brickHeight);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Верно записывает тип кирпича', () => {
|
||||||
|
const levelMap = [
|
||||||
|
[2, 1, 3],
|
||||||
|
[1, 2, 0],
|
||||||
|
];
|
||||||
|
const brickWidth = 1;
|
||||||
|
const brickHeight = 1;
|
||||||
|
|
||||||
|
const bricks = layBricks(levelMap, brickWidth, brickHeight);
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < levelMap.length; i++) {
|
||||||
|
for (let j = 0; j < levelMap[i].length; j++) {
|
||||||
|
if (levelMap[i][j] === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
expect(bricks[index].type).toBe(levelMap[i][j]);
|
||||||
|
index++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Класс сущности "бонус"
|
||||||
|
* @class
|
||||||
|
*/
|
||||||
|
export class Perk {
|
||||||
|
/**
|
||||||
|
* @param {number} x координата положения бонуса по оси X
|
||||||
|
* @param {number} y координата положения бонуса по оси Y
|
||||||
|
* @param {number} width положительное числовое значение ширины бонуса
|
||||||
|
* @param {number} height положительное числовое значение высоты бонуса
|
||||||
|
* @param {string} type тип бонуса
|
||||||
|
* @param {number} fallSpeed скорость падения бонуса по оси Y
|
||||||
|
*/
|
||||||
|
constructor(x, y, width, height, type, fallSpeed) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.type = type;
|
||||||
|
this.fallSpeed = fallSpeed;
|
||||||
|
this.alive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Смещает бонус вниз в зависимости от времени
|
||||||
|
* @param {number} deltaTime изменение времени из Ticker
|
||||||
|
*/
|
||||||
|
moveForward(deltaTime) {
|
||||||
|
this.y += this.fallSpeed * deltaTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,41 +2,51 @@ import {
|
|||||||
BALL_INITIAL_ANGLE,
|
BALL_INITIAL_ANGLE,
|
||||||
BALL_RADIUS,
|
BALL_RADIUS,
|
||||||
BALL_SPEED,
|
BALL_SPEED,
|
||||||
|
BALL_SPEED_LEVEL_STEP,
|
||||||
|
BALL_SPEED_TIME_STEP,
|
||||||
BRICK_HEIGHT,
|
BRICK_HEIGHT,
|
||||||
BRICK_WIDTH,
|
BRICK_WIDTH,
|
||||||
CONTAINER_HEIGHT,
|
CONTAINER_HEIGHT,
|
||||||
CONTAINER_WIDTH,
|
CONTAINER_WIDTH,
|
||||||
|
HEADER_HEIGHT,
|
||||||
PADDLE_HEIGHT,
|
PADDLE_HEIGHT,
|
||||||
PADDLE_WIDTH,
|
PADDLE_WIDTH,
|
||||||
|
PERK_BALL_SPEED_DECREASE,
|
||||||
} from './config';
|
} from './config';
|
||||||
import { Ball } from './entities/ball/ball';
|
import { Ball } from './entities/ball/ball';
|
||||||
import { layBricks } from './entities/brick/layBricks/layBricks';
|
import { layBricks } from './entities/brick/layBricks/layBricks';
|
||||||
import { Paddle } from './entities/paddle/paddle';
|
import { Paddle } from './entities/paddle/paddle';
|
||||||
import { tick } from './lib/tick/tick';
|
import { tick } from './lib/tick/tick';
|
||||||
import { toRadians } from './lib/toRadians/toRadians';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Класс игры c информацией о всех игровых сущностях
|
* Класс игры c информацией о всех игровых сущностях
|
||||||
*/
|
*/
|
||||||
export class Game {
|
export class Game {
|
||||||
/**
|
/**
|
||||||
* @param {number} columnAmount количество кирпичей по оси X, неотрицателное, целое число
|
* @param {number[][][]} levels список уровней, каждый элемент которого - карта расположения блоков
|
||||||
* @param {number} rowAmount количество кирпичей по оси Y, неотрицателное, целое число
|
|
||||||
*/
|
*/
|
||||||
constructor(columnAmount, rowAmount) {
|
constructor(levels) {
|
||||||
this.livesAmount = 3;
|
this.livesAmount = 3;
|
||||||
|
this.score = 0;
|
||||||
this.status = 'in_process';
|
this.status = 'in_process';
|
||||||
|
this.levels = levels;
|
||||||
|
this.currentLevel = 0;
|
||||||
|
this.maxLevel = levels.length - 1;
|
||||||
|
|
||||||
this.ball = new Ball(
|
|
||||||
CONTAINER_WIDTH / 2 - BALL_RADIUS / 2,
|
|
||||||
CONTAINER_HEIGHT / 2 - BALL_RADIUS / 2,
|
|
||||||
BALL_RADIUS,
|
|
||||||
BALL_SPEED * Math.cos(toRadians(BALL_INITIAL_ANGLE)),
|
|
||||||
-1 * BALL_SPEED * Math.sin(toRadians(BALL_INITIAL_ANGLE)),
|
|
||||||
);
|
|
||||||
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.balls = [new Ball(0, 0, BALL_RADIUS, BALL_SPEED, BALL_INITIAL_ANGLE)];
|
||||||
|
this.bricks = layBricks(this.levels[this.currentLevel], BRICK_WIDTH, BRICK_HEIGHT, HEADER_HEIGHT);
|
||||||
|
this.perks = [];
|
||||||
|
|
||||||
this.bricks = layBricks(columnAmount, rowAmount, BRICK_WIDTH, BRICK_HEIGHT);
|
this._placeBallOnPaddle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Основной (первый) мяч. Пока играет один мяч - это он.
|
||||||
|
* @returns {Ball}
|
||||||
|
*/
|
||||||
|
get ball() {
|
||||||
|
return this.balls[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,22 +57,71 @@ export class Game {
|
|||||||
if (this.status !== 'in_process') {
|
if (this.status !== 'in_process') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Пока мяч не запущен - держим его на ракетке и не считаем физику
|
||||||
|
if (this.ball.status === 'idle') {
|
||||||
|
this._placeBallOnPaddle();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
tick(this, CONTAINER_WIDTH, CONTAINER_HEIGHT, deltaTime);
|
tick(this, CONTAINER_WIDTH, CONTAINER_HEIGHT, deltaTime);
|
||||||
|
|
||||||
if (this.ball.isOut) {
|
// Убираем улетевшие мячи. Жизнь теряется только когда не осталось ни одного
|
||||||
|
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) {
|
||||||
this.status = 'over';
|
this.status = 'over';
|
||||||
} else {
|
} else {
|
||||||
this.ball.reset();
|
this._placeBallOnPaddle();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.balls = survivingBalls;
|
||||||
|
|
||||||
|
for (const ball of this.balls) {
|
||||||
|
ball.increaseSpeed(BALL_SPEED_TIME_STEP * deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLevelComplete = this._checkLevelCompletion();
|
||||||
|
|
||||||
|
if (isLevelComplete) {
|
||||||
|
this.currentLevel += 1;
|
||||||
|
|
||||||
|
if (this.currentLevel <= this.maxLevel) {
|
||||||
|
this._proceedToNextLevel();
|
||||||
|
} else {
|
||||||
|
this.status = 'completed';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isAnyBrickAlive = this.bricks.some((row) => row.some((brick) => brick.alive));
|
/**
|
||||||
|
* Проверяет завершен ли текущий уровень
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
_checkLevelCompletion() {
|
||||||
|
return this.bricks.every((brick) => brick.type === 3 || !brick.alive);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAnyBrickAlive) {
|
/**
|
||||||
this.status = 'completed';
|
* Запускает переход на новый уровень, возвращает мяч в дефоотное положение и отрисовывает кирпичи по карте уровня.
|
||||||
}
|
*/
|
||||||
|
_proceedToNextLevel() {
|
||||||
|
this.ball.increaseSpeed(BALL_SPEED_LEVEL_STEP);
|
||||||
|
this._placeBallOnPaddle();
|
||||||
|
this.bricks = layBricks(this.levels[this.currentLevel], BRICK_WIDTH, BRICK_HEIGHT, HEADER_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ставит мяч по центру ракетки, оставляя один мяч в игре
|
||||||
|
*/
|
||||||
|
_placeBallOnPaddle() {
|
||||||
|
this.balls = [this.ball];
|
||||||
|
this.ball.reset(this.paddle.x + this.paddle.width / 2, this.paddle.y - this.ball.radius);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* @param {number} secondObject.bottom - Max Y координата второго объекта
|
* @param {number} secondObject.bottom - Max Y координата второго объекта
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function calculateCollision(firstObject, secondObject) {
|
export function calculateAABBCollision(firstObject, secondObject) {
|
||||||
try {
|
try {
|
||||||
const coordinates = [
|
const coordinates = [
|
||||||
firstObject.left,
|
firstObject.left,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { calculateCollision } from './calculateCollision';
|
import { calculateAABBCollision } from './calculateAABBCollision';
|
||||||
|
|
||||||
describe('calculateCollision', () => {
|
describe('calculateAABBCollision', () => {
|
||||||
it('Корректно обрабатывает невозможные кейсы (левая координата больше правой)', () => {
|
it('Корректно обрабатывает невозможные кейсы (левая координата больше правой)', () => {
|
||||||
const firstObject = {
|
const firstObject = {
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -16,7 +16,7 @@ describe('calculateCollision', () => {
|
|||||||
bottom: 10,
|
bottom: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(calculateCollision(firstObject, secondObject)).toBeNull();
|
expect(calculateAABBCollision(firstObject, secondObject)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Корректно обрабатывает неверный формат данных', () => {
|
it('Корректно обрабатывает неверный формат данных', () => {
|
||||||
@@ -34,7 +34,7 @@ describe('calculateCollision', () => {
|
|||||||
bottom: 10,
|
bottom: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(calculateCollision(firstObject, secondObject)).toBeNull();
|
expect(calculateAABBCollision(firstObject, secondObject)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Корректно обрабатывает отсутствие пересечения по X', () => {
|
it('Корректно обрабатывает отсутствие пересечения по X', () => {
|
||||||
@@ -52,7 +52,7 @@ describe('calculateCollision', () => {
|
|||||||
bottom: 10,
|
bottom: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(calculateCollision(firstObject, secondObject)).toBe(false);
|
expect(calculateAABBCollision(firstObject, secondObject)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Корректно обрабатывает отсутствие пересечения по Y', () => {
|
it('Корректно обрабатывает отсутствие пересечения по Y', () => {
|
||||||
@@ -70,7 +70,7 @@ describe('calculateCollision', () => {
|
|||||||
bottom: 30,
|
bottom: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(calculateCollision(firstObject, secondObject)).toBe(false);
|
expect(calculateAABBCollision(firstObject, secondObject)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Корректно обрабатывает отсутствие пересечения по X и Y', () => {
|
it('Корректно обрабатывает отсутствие пересечения по X и Y', () => {
|
||||||
@@ -88,7 +88,7 @@ describe('calculateCollision', () => {
|
|||||||
bottom: 30,
|
bottom: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(calculateCollision(firstObject, secondObject)).toBe(false);
|
expect(calculateAABBCollision(firstObject, secondObject)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Корректно обрабатывает пересечение', () => {
|
it('Корректно обрабатывает пересечение', () => {
|
||||||
@@ -106,7 +106,7 @@ describe('calculateCollision', () => {
|
|||||||
bottom: 30,
|
bottom: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(calculateCollision(firstObject, secondObject)).toBe(true);
|
expect(calculateAABBCollision(firstObject, secondObject)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Корректно обрабатывает вхождение', () => {
|
it('Корректно обрабатывает вхождение', () => {
|
||||||
@@ -124,6 +124,6 @@ describe('calculateCollision', () => {
|
|||||||
bottom: 15,
|
bottom: 15,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(calculateCollision(firstObject, secondObject)).toBe(true);
|
expect(calculateAABBCollision(firstObject, secondObject)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { calculateCollision } from '../calculateCollision/calculateCollision';
|
import { calculateAABBCollision } from '../calculateAABBCollision/calculateAABBCollision';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Вычисляет направление наибольшего пересечения по осям и возвращает tuple множителей для изменения координат
|
* Вычисляет направление наибольшего пересечения по осям и возвращает tuple множителей для изменения координат
|
||||||
@@ -17,7 +17,7 @@ import { calculateCollision } from '../calculateCollision/calculateCollision';
|
|||||||
export function calculateDirection(firstObject, secondObject) {
|
export function calculateDirection(firstObject, secondObject) {
|
||||||
try {
|
try {
|
||||||
// Запускаем для проверки формата аргументов
|
// Запускаем для проверки формата аргументов
|
||||||
const isCollided = calculateCollision(firstObject, secondObject);
|
const isCollided = calculateAABBCollision(firstObject, secondObject);
|
||||||
|
|
||||||
if (isCollided === null) {
|
if (isCollided === null) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Ball } from '../../entities/ball/ball';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Двигает мяч и обрабатывает отскок от стен. Возвращает true, если мяч уже отскочил или вышел за нижнюю границу
|
||||||
|
* @param {Ball} ball мяч
|
||||||
|
* @param {number} containerWidth ширина игрового контейнера в пикселях
|
||||||
|
* @param {number} containerHeight высота игрового контейнера в пикселях
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function processBall(ball, containerWidth, containerHeight) {
|
||||||
|
try {
|
||||||
|
if (!(ball instanceof Ball)) {
|
||||||
|
throw new Error('Аргумент ball должен быть экземпляром класса мяча');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof containerWidth !== 'number' || typeof containerHeight !== 'number') {
|
||||||
|
throw new Error('Значения размеров игрового контейнера должены являться числами');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containerWidth <= 0 || containerHeight <= 0) {
|
||||||
|
throw new Error('Размеры игрового контейнера должены быть положительными числами');
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Не даем мячу выйти за границу стены сверху и меняем направление
|
||||||
|
if (ball.y <= topBoundary) {
|
||||||
|
ball.y = topBoundary;
|
||||||
|
ball.verticalSpeed *= -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем выход за границу стены снизу
|
||||||
|
if (ball.y >= bottomBoundary) {
|
||||||
|
ball.status = 'out';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Ball } from '../../entities/ball/ball';
|
||||||
|
import { processBall } from './processBall';
|
||||||
|
|
||||||
|
describe('processBall', () => {
|
||||||
|
it('Возвращает null при неверных типах аргументов', () => {
|
||||||
|
const ball = new Ball(100, 100, 10, 3);
|
||||||
|
|
||||||
|
expect(processBall(null, 800, 600)).toBeNull();
|
||||||
|
expect(processBall(ball, '800', 600)).toBeNull();
|
||||||
|
expect(processBall(ball, -1, 600)).toBeNull();
|
||||||
|
expect(processBall(ball, 800, -1)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Возвращает false, если мяч не касается стен', () => {
|
||||||
|
const ball = new Ball(400, 300, 10, 3);
|
||||||
|
|
||||||
|
expect(processBall(ball, 800, 600)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Отражает мяч от боковой стены и возвращает true', () => {
|
||||||
|
const ball = new Ball(10, 300, 10, 3);
|
||||||
|
ball.horizontalSpeed = -10;
|
||||||
|
|
||||||
|
expect(processBall(ball, 800, 600)).toBe(true);
|
||||||
|
expect(ball.x).toBe(ball.radius);
|
||||||
|
expect(ball.horizontalSpeed).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Отражает мяч от верхней стены и возвращает true', () => {
|
||||||
|
const ball = new Ball(400, 10, 10, 3);
|
||||||
|
ball.horizontalSpeed = 0;
|
||||||
|
ball.verticalSpeed = -10;
|
||||||
|
|
||||||
|
expect(processBall(ball, 800, 600)).toBe(true);
|
||||||
|
expect(ball.y).toBe(ball.radius);
|
||||||
|
expect(ball.verticalSpeed).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Помечает мяч вышедшим за нижнюю границу', () => {
|
||||||
|
const ball = new Ball(400, 595, 10, 3);
|
||||||
|
|
||||||
|
expect(processBall(ball, 800, 600)).toBe(true);
|
||||||
|
expect(ball.status).toBe('out');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
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';
|
||||||
|
import { calculateDirection } from '../calculateDirection/calculateDirection';
|
||||||
|
import { spawnRandomPerk } from '../spawnRandomPerk/spawnRandomPerk';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Базовое взаимодействие мяча и кирпичей: меняет направление, разрушает кирпич и роняет бонус
|
||||||
|
* @param {Game} game экземпляр класса игры
|
||||||
|
* @param {Ball} ball мяч
|
||||||
|
* @returns {boolean | null} true, если произошло столкновение с кирпичом
|
||||||
|
*/
|
||||||
|
export function processBrickCollision(game, ball) {
|
||||||
|
try {
|
||||||
|
if (!(game instanceof Game)) {
|
||||||
|
throw new Error('Аргумент game должен быть экземпляром класса игра');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(ball instanceof Ball)) {
|
||||||
|
throw new Error('Аргумент ball должен быть экземпляром класса мяч');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const brick of game.bricks) {
|
||||||
|
if (!brick.alive) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ballObject = {
|
||||||
|
left: ball.x - ball.radius,
|
||||||
|
right: ball.x + ball.radius,
|
||||||
|
top: ball.y - ball.radius,
|
||||||
|
bottom: ball.y + ball.radius,
|
||||||
|
};
|
||||||
|
const brickObject = {
|
||||||
|
left: brick.x,
|
||||||
|
right: brick.x + brick.width,
|
||||||
|
top: brick.y,
|
||||||
|
bottom: brick.y + brick.height,
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCollided = calculateAABBCollision(ballObject, brickObject);
|
||||||
|
|
||||||
|
if (!isCollided) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const directions = calculateDirection(ballObject, brickObject);
|
||||||
|
|
||||||
|
if (directions === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ball.horizontalSpeed *= directions[0];
|
||||||
|
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,
|
||||||
|
brick.y + brick.height / 2 - PERK_HEIGHT / 2,
|
||||||
|
);
|
||||||
|
game.perks.push(perk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Game } from '../../game';
|
||||||
|
import { processBrickCollision } from './processBrickCollision';
|
||||||
|
|
||||||
|
describe('processBrickCollision', () => {
|
||||||
|
it('Возвращает корректное значение при неверных типах аргументов', () => {
|
||||||
|
const game = new Game([[[1]]]);
|
||||||
|
|
||||||
|
expect(processBrickCollision(null, game.ball)).toBeNull();
|
||||||
|
expect(processBrickCollision(game, null)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Возвращает креектное значение если мяч не касается кирпичей', () => {
|
||||||
|
const game = new Game([[[1]]]);
|
||||||
|
game.ball.x = 5;
|
||||||
|
game.ball.y = 500;
|
||||||
|
|
||||||
|
expect(processBrickCollision(game, game.ball)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('При столкновении разрушает кирпич и меняет направление мяча', () => {
|
||||||
|
const game = new Game([[[1]]]);
|
||||||
|
const brick = game.bricks[0];
|
||||||
|
game.ball.x = brick.x + game.ball.radius + 1;
|
||||||
|
game.ball.y = brick.y + brick.height + game.ball.radius + 1;
|
||||||
|
game.ball.horizontalSpeed = 0;
|
||||||
|
game.ball.verticalSpeed = -10;
|
||||||
|
game.ball.moveForward(1);
|
||||||
|
|
||||||
|
expect(processBrickCollision(game, game.ball)).toBe(true);
|
||||||
|
expect(brick.alive).toBe(false);
|
||||||
|
expect(game.ball.verticalSpeed).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { PERK_FALL_SPEED, PERK_HEIGHT, PERK_TYPES, PERK_WIDTH } from '../../config';
|
||||||
|
import { Perk } from '../../entities/perk/perk';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает случайный бонус в заданной точке
|
||||||
|
* @param {number} x координата бонуса по оси X
|
||||||
|
* @param {number} y координата бонуса по оси Y
|
||||||
|
* @returns {Perk | null}
|
||||||
|
*/
|
||||||
|
export function spawnRandomPerk(x, y) {
|
||||||
|
try {
|
||||||
|
if (typeof x !== 'number' || typeof y !== 'number') {
|
||||||
|
throw new Error('Координаты бонуса должны являться числами');
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = PERK_TYPES[Math.floor(Math.random() * PERK_TYPES.length)];
|
||||||
|
|
||||||
|
return new Perk(x, y, PERK_WIDTH, PERK_HEIGHT, type, PERK_FALL_SPEED);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { PERK_FALL_SPEED, PERK_HEIGHT, PERK_WIDTH } from '../../config';
|
||||||
|
import { Perk } from '../../entities/perk/perk';
|
||||||
|
import { spawnRandomPerk } from './spawnRandomPerk';
|
||||||
|
|
||||||
|
describe('spawnRandomPerk', () => {
|
||||||
|
it('Возвращает корректное значение при неверных типах аргументов', () => {
|
||||||
|
expect(spawnRandomPerk(null, 0)).toBeNull();
|
||||||
|
expect(spawnRandomPerk(0, null)).toBeNull();
|
||||||
|
expect(spawnRandomPerk('10', 0)).toBeNull();
|
||||||
|
expect(spawnRandomPerk(0, undefined)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Создает бонус в заданной точке с размерами и скоростью из конфига', () => {
|
||||||
|
const perk = spawnRandomPerk(10, 20);
|
||||||
|
|
||||||
|
expect(perk).toBeInstanceOf(Perk);
|
||||||
|
expect(perk.x).toBe(10);
|
||||||
|
expect(perk.y).toBe(20);
|
||||||
|
expect(perk.width).toBe(PERK_WIDTH);
|
||||||
|
expect(perk.height).toBe(PERK_HEIGHT);
|
||||||
|
expect(perk.fallSpeed).toBe(PERK_FALL_SPEED);
|
||||||
|
expect(perk.alive).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,15 +1,9 @@
|
|||||||
import {
|
import { MAX_PADDLE_REFLECTION_ANGLE, MIN_PADDLE_REFLECTION_ANGLE, PADDLE_SECTOR_AMOUNT } from '../../config';
|
||||||
BALL_SPEED,
|
|
||||||
CONTAINER_HEIGHT,
|
|
||||||
CONTAINER_WIDTH,
|
|
||||||
MAX_PADDLE_REFLECTION_ANGLE,
|
|
||||||
MIN_PADDLE_REFLECTION_ANGLE,
|
|
||||||
PADDLE_SECTOR_AMOUNT,
|
|
||||||
} from '../../config';
|
|
||||||
import { Game } from '../../game';
|
import { Game } from '../../game';
|
||||||
import { calculateCollision } from '../calculateCollision/calculateCollision';
|
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 { updatePerks } from '../updatePerks/updatePerks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Функция для вычисления взаимодействий сущностей игры в зависимости от времени из Ticker
|
* Функция для вычисления взаимодействий сущностей игры в зависимости от времени из Ticker
|
||||||
@@ -36,79 +30,37 @@ 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);
|
||||||
|
|
||||||
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) {
|
continue;
|
||||||
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.isOut = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Базоввое взаимодействие мяча и кирпича
|
|
||||||
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();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Взаимодействие мяча и ракетки - обновление горизонтальной и вертикальной скорости в зависимости от сектора попадания
|
// Проверка столкновения с кирпичами
|
||||||
processReflection(
|
const hitBrick = processBrickCollision(game, ball);
|
||||||
paddle,
|
|
||||||
ball,
|
if (hitBrick) {
|
||||||
PADDLE_SECTOR_AMOUNT,
|
continue;
|
||||||
MIN_PADDLE_REFLECTION_ANGLE,
|
}
|
||||||
MAX_PADDLE_REFLECTION_ANGLE,
|
|
||||||
BALL_SPEED,
|
// Взаимодействие мяча и ракетки - обновление горизонтальной и вертикальной скорости в зависимости от сектора попадания
|
||||||
);
|
processReflection(
|
||||||
|
paddle,
|
||||||
|
ball,
|
||||||
|
PADDLE_SECTOR_AMOUNT,
|
||||||
|
MIN_PADDLE_REFLECTION_ANGLE,
|
||||||
|
MAX_PADDLE_REFLECTION_ANGLE,
|
||||||
|
ball.speed,
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ describe('tick', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Возвращает корректное значние и не изменяет свойства game при неверых размерах контейнера', () => {
|
it('Возвращает корректное значние и не изменяет свойства game при неверых размерах контейнера', () => {
|
||||||
const game = new Game(1, 1);
|
const levels = [[[1]]];
|
||||||
|
const game = new Game(levels);
|
||||||
const ballX = game.ball.x;
|
const ballX = game.ball.x;
|
||||||
const ballY = game.ball.y;
|
const ballY = game.ball.y;
|
||||||
expect(tick(game, -1, -1, 1)).toBeNull();
|
expect(tick(game, -1, -1, 1)).toBeNull();
|
||||||
@@ -20,7 +21,8 @@ describe('tick', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Возвращает корректное значние и не изменяет свойства game при неверном значении времени', () => {
|
it('Возвращает корректное значние и не изменяет свойства game при неверном значении времени', () => {
|
||||||
const game = new Game(1, 1);
|
const levels = [[[1]]];
|
||||||
|
const game = new Game(levels);
|
||||||
const ballX = game.ball.x;
|
const ballX = game.ball.x;
|
||||||
const ballY = game.ball.y;
|
const ballY = game.ball.y;
|
||||||
expect(tick(game, 1, 1, -1)).toBeNull();
|
expect(tick(game, 1, 1, -1)).toBeNull();
|
||||||
@@ -29,7 +31,8 @@ describe('tick', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Меняет направление мяча при столкновении со стеной', () => {
|
it('Меняет направление мяча при столкновении со стеной', () => {
|
||||||
const game = new Game(1, 1);
|
const levels = [[[1]]];
|
||||||
|
const game = new Game(levels);
|
||||||
game.ball.x = 0;
|
game.ball.x = 0;
|
||||||
game.ball.horizontalSpeed = -10;
|
game.ball.horizontalSpeed = -10;
|
||||||
tick(game, CONTAINER_WIDTH, CONTAINER_HEIGHT, 1);
|
tick(game, CONTAINER_WIDTH, CONTAINER_HEIGHT, 1);
|
||||||
@@ -38,7 +41,8 @@ describe('tick', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Меняет направление мяча при столкновении с потолком', () => {
|
it('Меняет направление мяча при столкновении с потолком', () => {
|
||||||
const game = new Game(0, 0);
|
const levels = [[[1]]];
|
||||||
|
const game = new Game(1);
|
||||||
game.ball.y = 0;
|
game.ball.y = 0;
|
||||||
game.ball.verticalSpeed = -10;
|
game.ball.verticalSpeed = -10;
|
||||||
tick(game, CONTAINER_WIDTH, CONTAINER_HEIGHT, 1);
|
tick(game, CONTAINER_WIDTH, CONTAINER_HEIGHT, 1);
|
||||||
@@ -47,8 +51,9 @@ describe('tick', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Убивает кирпич при столкновении и отражает мяч', () => {
|
it('Убивает кирпич при столкновении и отражает мяч', () => {
|
||||||
const game = new Game(1, 1);
|
const levels = [[[1]]];
|
||||||
const brick = game.bricks[0][0];
|
const game = new Game(levels);
|
||||||
|
const brick = game.bricks[0];
|
||||||
|
|
||||||
game.ball.x = brick.x + game.ball.radius + 1;
|
game.ball.x = brick.x + game.ball.radius + 1;
|
||||||
game.ball.y = brick.y + brick.height + game.ball.radius + 1;
|
game.ball.y = brick.y + brick.height + game.ball.radius + 1;
|
||||||
@@ -62,8 +67,9 @@ describe('tick', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Убивает только один кирпич за тик', () => {
|
it('Убивает только один кирпич за тик', () => {
|
||||||
const game = new Game(2, 1);
|
const levels = [[[1, 1]]];
|
||||||
const [firstBrick, secondBrick] = game.bricks[0];
|
const game = new Game(levels);
|
||||||
|
const [firstBrick, secondBrick] = game.bricks;
|
||||||
|
|
||||||
game.ball.x = firstBrick.x + game.ball.radius + 1;
|
game.ball.x = firstBrick.x + game.ball.radius + 1;
|
||||||
game.ball.y = firstBrick.y + firstBrick.height + game.ball.radius + 1;
|
game.ball.y = firstBrick.y + firstBrick.height + game.ball.radius + 1;
|
||||||
@@ -72,7 +78,7 @@ describe('tick', () => {
|
|||||||
|
|
||||||
tick(game, CONTAINER_WIDTH, CONTAINER_HEIGHT, 0.1);
|
tick(game, CONTAINER_WIDTH, CONTAINER_HEIGHT, 0.1);
|
||||||
|
|
||||||
const killedCount = game.bricks[0].filter((b) => !b.alive).length;
|
const killedCount = game.bricks.filter((b) => !b.alive).length;
|
||||||
expect(killedCount).toBe(1);
|
expect(killedCount).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
BALL_SPLIT_ANGLE,
|
||||||
|
CONTAINER_WIDTH,
|
||||||
|
PADDLE_WIDE_WIDTH,
|
||||||
|
PADDLE_WIDTH,
|
||||||
|
PERK_BALL_SPEED_DECREASE,
|
||||||
|
PERK_DURATION_SEC,
|
||||||
|
} from '../../config';
|
||||||
|
import { clone } from '../../entities/ball/clone/clone';
|
||||||
|
import { Game } from '../../game';
|
||||||
|
import { calculateAABBCollision } from '../calculateAABBCollision/calculateAABBCollision';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновляет состояние бонусов, меняет положение, проверяет столкновение с ракеткой и выход за границу поля
|
||||||
|
* @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 = calculateAABBCollision(perkObject, paddleObject);
|
||||||
|
|
||||||
|
if (isCollided) {
|
||||||
|
switch (perk.type) {
|
||||||
|
case 'slow':
|
||||||
|
for (const ball of game.balls) {
|
||||||
|
if (ball.speed - PERK_BALL_SPEED_DECREASE > 0) {
|
||||||
|
ball.increaseSpeed(-1 * PERK_BALL_SPEED_DECREASE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
for (const ball of game.balls) {
|
||||||
|
ball.increaseSpeed(PERK_BALL_SPEED_DECREASE);
|
||||||
|
}
|
||||||
|
}, PERK_DURATION_SEC * 1000);
|
||||||
|
break;
|
||||||
|
case 'wide':
|
||||||
|
paddle.width = PADDLE_WIDE_WIDTH;
|
||||||
|
paddle.moveTo(paddle.x, 0, CONTAINER_WIDTH);
|
||||||
|
setTimeout(() => {
|
||||||
|
paddle.width = PADDLE_WIDTH;
|
||||||
|
}, PERK_DURATION_SEC * 1000);
|
||||||
|
break;
|
||||||
|
case 'life':
|
||||||
|
game.livesAmount += 1;
|
||||||
|
break;
|
||||||
|
case 'clone':
|
||||||
|
// Основной мяч делится на два клона, разлетающихся под углом
|
||||||
|
game.balls.push(clone(game.ball, BALL_SPLIT_ANGLE), clone(game.ball, -BALL_SPLIT_ANGLE));
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { PADDLE_WIDE_WIDTH } from '../../config';
|
||||||
|
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('Ловит бонус wide, расширяет ракетку до фиксированной ширины', () => {
|
||||||
|
const game = new Game([[[1]]]);
|
||||||
|
const { paddle } = game;
|
||||||
|
game.perks.push(new Perk(paddle.x, paddle.y - 1, 10, 10, 'wide', 0));
|
||||||
|
|
||||||
|
updatePerks(game, 600, 1);
|
||||||
|
|
||||||
|
expect(game.perks).toHaveLength(0);
|
||||||
|
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('Убирает бонус, улетевший за нижнюю границу', () => {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,45 +13,92 @@ import {
|
|||||||
PADDLE_HEIGHT,
|
PADDLE_HEIGHT,
|
||||||
PADDLE_WIDTH,
|
PADDLE_WIDTH,
|
||||||
} from './config';
|
} from './config';
|
||||||
|
import { LEVELS } from './const/levels';
|
||||||
import { Game } from './game';
|
import { Game } from './game';
|
||||||
import { calculateCollision } from './lib/calculateCollision/calculateCollision';
|
import {
|
||||||
import { calculateDirection } from './lib/calculateDirection/calculateDirection';
|
createGameView,
|
||||||
import { createGameView, syncronizeViewsWithGame } from './view';
|
managePerkViewsLifetime,
|
||||||
|
rebuildBrickViews,
|
||||||
|
syncronizeViewsWithGame,
|
||||||
|
updateBackgroundView,
|
||||||
|
} from './view';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
// Create a new application
|
|
||||||
const app = new Application();
|
const app = new Application();
|
||||||
|
|
||||||
// Initialize the application
|
|
||||||
await app.init({ background: '#1099bb', width: CONTAINER_WIDTH, height: CONTAINER_HEIGHT });
|
await app.init({ background: '#1099bb', width: CONTAINER_WIDTH, height: CONTAINER_HEIGHT });
|
||||||
|
|
||||||
// Append the application canvas to the document body
|
|
||||||
document.body.appendChild(app.canvas);
|
document.body.appendChild(app.canvas);
|
||||||
|
|
||||||
// Create and add a container to the stage
|
|
||||||
const container = new Container({
|
const container = new Container({
|
||||||
eventMode: 'static',
|
eventMode: 'static',
|
||||||
hitArea: app.screen,
|
hitArea: app.screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
container.x = 0;
|
container.x = 0;
|
||||||
container.y = 0;
|
container.y = 0;
|
||||||
|
|
||||||
app.stage.addChild(container);
|
app.stage.addChild(container);
|
||||||
|
|
||||||
const game = new Game(BRICK_COLUMN_AMOUNT, BRICK_ROW_AMOUNT);
|
const textures = await Assets.load([
|
||||||
const views = createGameView(game, container);
|
'/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',
|
||||||
|
'/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, textures);
|
||||||
|
let currentLevel = game.currentLevel;
|
||||||
|
|
||||||
container.on('pointermove', (event) => {
|
container.on('pointermove', (event) => {
|
||||||
const localPosition = container.toLocal(event.global);
|
const localPosition = container.toLocal(event.global);
|
||||||
game.paddle.moveTo(localPosition.x, 0, CONTAINER_WIDTH);
|
game.paddle.moveTo(localPosition.x, 0, CONTAINER_WIDTH);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(game.paddle.x, game.paddle.y);
|
container.on('pointerdown', () => {
|
||||||
|
game.ball.launch();
|
||||||
|
});
|
||||||
|
|
||||||
app.ticker.add((time) => {
|
app.ticker.add((time) => {
|
||||||
game.update(time.deltaTime);
|
game.update(time.deltaTime);
|
||||||
|
|
||||||
|
if (game.currentLevel !== currentLevel) {
|
||||||
|
rebuildBrickViews(views, game, container, textures);
|
||||||
|
updateBackgroundView(views, game, textures);
|
||||||
|
currentLevel = game.currentLevel;
|
||||||
|
}
|
||||||
|
managePerkViewsLifetime(views, game, container, textures);
|
||||||
syncronizeViewsWithGame(views, game);
|
syncronizeViewsWithGame(views, game);
|
||||||
console.log(game.paddle.x, game.paddle.y);
|
|
||||||
|
if (game.status === 'over' || game.status === 'completed') {
|
||||||
|
showEndScreen(game.status === 'completed' ? 'Победа!' : 'Игра окончена!', game.score);
|
||||||
|
app.ticker.stop();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Показывает финальный экран с сообщением и кнопкой рестарта (перезагрузка страницы).
|
||||||
|
* @param {string} message текст результата игры
|
||||||
|
* @param {number} score итоговые очки
|
||||||
|
*/
|
||||||
|
function showEndScreen(message, score) {
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'end-screen';
|
||||||
|
overlay.innerHTML = `<p>${message}</p><p>Очки: ${score}</p><button class="button" type="button">Начать заново</button>`;
|
||||||
|
overlay.querySelector('button').addEventListener('click', () => location.reload());
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,41 @@
|
|||||||
@import "./reset.css";
|
@import "./reset.css";
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: block;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-screen {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 3rem;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-screen button {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-family: sans-serif;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
transition: all 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-screen button:hover {
|
||||||
|
color: #ffffff88;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,58 +1,165 @@
|
|||||||
import { Container, Graphics } 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 { Ball } from './entities/ball/ball';
|
||||||
import { Brick } from './entities/brick/brick';
|
import { Brick } from './entities/brick/brick';
|
||||||
import { Paddle } from './entities/paddle/paddle';
|
import { Paddle } from './entities/paddle/paddle';
|
||||||
|
import { Perk } from './entities/perk/perk';
|
||||||
import { Game } from './game';
|
import { Game } from './game';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Создает визуальное отображение мяча с помощью Pixi.js
|
* Создает визуальное отображение мяча с помощью Pixi.js
|
||||||
* @param {Ball} ball экземпляр класса мяч
|
* @param {Ball} ball экземпляр класса мяч
|
||||||
|
* @param {object} textures объект с текстурами для визуального отображения
|
||||||
*/
|
*/
|
||||||
function createBallView(ball) {
|
function createBallView(ball, textures) {
|
||||||
return new Graphics().circle(0, 0, ball.radius).fill('#ffffff');
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Создает визуальное отображение ракетки с помощью Pixi.js
|
* Создает визуальное отображение ракетки с помощью Pixi.js
|
||||||
* @param {Paddle} paddle экземпляр класса ракетка
|
* @param {Paddle} paddle экземпляр класса ракетка
|
||||||
|
* @param {object} textures объект с текстурами для визуального отображения
|
||||||
*/
|
*/
|
||||||
function createPaddleView(paddle) {
|
function createPaddleView(paddle, textures) {
|
||||||
return new Graphics().rect(0, 0, paddle.width, paddle.height).fill('#fff000');
|
const texture = textures['/sprites/paddle.png'];
|
||||||
|
const paddleSprite = new NineSliceSprite({ texture, leftWidth: 150, rightWidth: 150, topHeight: 0, bottomHeight: 0 });
|
||||||
|
const paddleAspectRatio = paddle.height / texture.height;
|
||||||
|
paddleSprite.scale.set(paddleAspectRatio);
|
||||||
|
paddleSprite.width = paddle.width / paddleAspectRatio;
|
||||||
|
|
||||||
|
return paddleSprite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Создает визуальное отображение кирпича с помощью Pixi.js
|
* Создает визуальное отображение кирпича с помощью Pixi.js
|
||||||
* @param {Brick} brick экземпляр класса кирпич
|
* @param {Brick} brick экземпляр класса кирпич
|
||||||
|
* @returns {Graphics} графическое отображение кирпича
|
||||||
|
* @param {object} textures объект с текстурами для визуального отображения
|
||||||
*/
|
*/
|
||||||
function createBrickView(brick) {
|
function createBrickView(brick, textures) {
|
||||||
return new Graphics().rect(0, 0, brick.width, brick.height).fill('#000fff');
|
const bricksTextures = [
|
||||||
|
textures['/sprites/block_1.png'],
|
||||||
|
textures['/sprites/block_2.png'],
|
||||||
|
textures['/sprites/block_3.png'],
|
||||||
|
];
|
||||||
|
|
||||||
|
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, textures) {
|
||||||
|
let texture;
|
||||||
|
|
||||||
|
switch (perk.type) {
|
||||||
|
case 'slow':
|
||||||
|
texture = textures['/sprites/perk_1.png'];
|
||||||
|
break;
|
||||||
|
case 'wide':
|
||||||
|
texture = textures['/sprites/perk_2.png'];
|
||||||
|
break;
|
||||||
|
case 'life':
|
||||||
|
texture = textures['/sprites/perk_3.png'];
|
||||||
|
break;
|
||||||
|
case 'clone':
|
||||||
|
texture = textures['/sprites/perk_3.png'];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
texture = textures['/sprites/perk_1.png'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const perkSprite = new Sprite(texture);
|
||||||
|
perkSprite.width = perk.width;
|
||||||
|
perkSprite.height = perk.height;
|
||||||
|
|
||||||
|
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 и добавляет в контейнер
|
* Создает визуальное отображение всех сущностей в игре с помощью Pixi.js и добавляет в контейнер
|
||||||
* @param {Game} game экземпляр класса игра
|
* @param {Game} game экземпляр класса игра
|
||||||
* @param {Container} container контейнер Pixi.js
|
* @param {Container} container экземпляр класса контейнер Pixi.js
|
||||||
|
* @param {object} textures объект с текстурами для визуального отображения
|
||||||
*/
|
*/
|
||||||
export function createGameView(game, container) {
|
export function createGameView(game, container, textures) {
|
||||||
try {
|
try {
|
||||||
const ball = createBallView(game.ball);
|
const background = new Sprite(backgroundTextureForLevel(game, textures));
|
||||||
container.addChild(ball);
|
background.width = CONTAINER_WIDTH;
|
||||||
|
background.height = CONTAINER_HEIGHT;
|
||||||
|
container.addChildAt(background, 0);
|
||||||
|
|
||||||
const paddle = createPaddleView(game.paddle);
|
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, textures);
|
||||||
|
container.addChild(ballView);
|
||||||
|
balls.set(ball, ballView);
|
||||||
|
}
|
||||||
|
|
||||||
|
const paddle = createPaddleView(game.paddle, textures);
|
||||||
container.addChild(paddle);
|
container.addChild(paddle);
|
||||||
|
|
||||||
const bricks = game.bricks.map((row) =>
|
const bricks = game.bricks.map((brick) => {
|
||||||
row.map((brick) => {
|
const brickView = createBrickView(brick, textures);
|
||||||
const brickView = createBrickView(brick);
|
container.addChild(brickView);
|
||||||
container.addChild(brickView);
|
return brickView;
|
||||||
return brickView;
|
});
|
||||||
}),
|
|
||||||
);
|
const perks = new Map();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ball,
|
background,
|
||||||
|
header,
|
||||||
|
balls,
|
||||||
paddle,
|
paddle,
|
||||||
bricks,
|
bricks,
|
||||||
|
perks,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -60,6 +167,52 @@ 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, textures) {
|
||||||
|
// Изменяем ширину отображения ракетки основываясь на 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) {
|
||||||
|
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, textures);
|
||||||
|
container.addChild(ballView);
|
||||||
|
views.balls.set(ball, ballView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, textures);
|
||||||
|
container.addChild(perkView);
|
||||||
|
views.perks.set(perk, perkView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Синхронизирует отображение сущностей Pixi.js с логикой игры (координаты и тд.)
|
* Синхронизирует отображение сущностей Pixi.js с логикой игры (координаты и тд.)
|
||||||
* @param {object} views объект с визуальными отображениями сущностей игры
|
* @param {object} views объект с визуальными отображениями сущностей игры
|
||||||
@@ -71,22 +224,61 @@ export function syncronizeViewsWithGame(views, game) {
|
|||||||
throw new Error('Аргумент game должен быть экземпляром класса Game');
|
throw new Error('Аргумент game должен быть экземпляром класса Game');
|
||||||
}
|
}
|
||||||
|
|
||||||
views.ball.x = game.ball.x;
|
views.header.text = `Уровень: ${game.currentLevel + 1} Количество жизней: ${game.livesAmount} Очки: ${game.score}`;
|
||||||
views.ball.y = game.ball.y;
|
|
||||||
|
for (const ball of game.balls) {
|
||||||
|
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;
|
||||||
|
|
||||||
for (let i = 0; i < game.bricks.length; i++) {
|
for (let i = 0; i < game.bricks.length; i++) {
|
||||||
for (let j = 0; j < game.bricks[i].length; j++) {
|
const brick = game.bricks[i];
|
||||||
const brick = game.bricks[i][j];
|
const brickView = views.bricks[i];
|
||||||
const brickView = views.bricks[i][j];
|
brickView.x = brick.x;
|
||||||
brickView.x = brick.x;
|
brickView.y = brick.y;
|
||||||
brickView.y = brick.y;
|
brickView.visible = brick.alive;
|
||||||
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) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Меняет текстуру фона под текущий уровень игры.
|
||||||
|
* @param {object} views объект с визуальными отображениями сущностей игры
|
||||||
|
* @param {Game} game экземпляр класса игра
|
||||||
|
* @param {object} textures объект с текстурами для визуального отображения
|
||||||
|
*/
|
||||||
|
export function updateBackgroundView(views, game, textures) {
|
||||||
|
views.background.texture = backgroundTextureForLevel(game, textures);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Пересоздает отображения кирпичей под текущий уровень игры.
|
||||||
|
* @param {object} views объект с визуальными отображениями сущностей игры
|
||||||
|
* @param {Game} game экземпляр класса игра
|
||||||
|
* @param {Container} container экземпляр класса контейнер Pixi.js
|
||||||
|
* @param {object} textures объект с текстурами для визуального отображения
|
||||||
|
*/
|
||||||
|
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, textures);
|
||||||
|
container.addChild(brickView);
|
||||||
|
return brickView;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||