implement basic arkanoid version
This commit is contained in:
+2
-2
@@ -95,8 +95,8 @@ module.exports = {
|
||||
'linebreak-style': 'error',
|
||||
'lines-around-comment': 0,
|
||||
'lines-around-directive': 'error',
|
||||
'lines-between-class-members': 'error',
|
||||
'max-classes-per-file': 'error',
|
||||
'lines-between-class-members': 0,
|
||||
'max-classes-per-file': 0,
|
||||
'max-depth': 'error',
|
||||
'max-len': ['error', 180],
|
||||
'max-lines': 'error',
|
||||
|
||||
Generated
+1214
-13819
File diff suppressed because it is too large
Load Diff
+1
-4
@@ -14,11 +14,8 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"parcel": "^2.0.0-rc.0",
|
||||
"parcel": "^2.0.1",
|
||||
"tslib": "^2.0.3",
|
||||
"typescript": "^4.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint-plugin-react": "^7.26.0"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import { Game } from '../Game';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
game: Game;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Puck } from './Puck';
|
||||
import { Board } from './Board';
|
||||
import { Bomb } from './Bomb';
|
||||
import { Element } from './Element';
|
||||
import { Position, Size } from './types';
|
||||
import { Ball } from './Ball';
|
||||
import * as sound from './sound';
|
||||
|
||||
export class Block extends Element {
|
||||
bomb: Bomb | undefined;
|
||||
|
||||
private constructor(
|
||||
public board: Board,
|
||||
public ball: Ball,
|
||||
public puck: Puck,
|
||||
pos: Position,
|
||||
size: Size = { width: 70, height: 20 }
|
||||
) {
|
||||
super(size, pos);
|
||||
const { style } = this;
|
||||
|
||||
style.position = 'absolute';
|
||||
style.zIndex = '1';
|
||||
style.width = `${size.width}px`;
|
||||
style.height = `${size.height}px`;
|
||||
style.borderRadius = '10%';
|
||||
style.backgroundColor = '#e836ca';
|
||||
style.boxShadow = ' 2px 3px 5px #333, inset -2px -2px 4px black, inset 2px 2px 4px white';
|
||||
this.translate();
|
||||
}
|
||||
|
||||
static create(board: Board, ball: Ball, puck: Puck, pos: Position, size?: Size): Block {
|
||||
return new Block(board, ball, puck, pos, size);
|
||||
}
|
||||
|
||||
reset(): this {
|
||||
super.reset();
|
||||
this.isDestroyed = false;
|
||||
this.style.display = 'block';
|
||||
return this;
|
||||
}
|
||||
|
||||
update(): this {
|
||||
super.update();
|
||||
// const { board, puck, ball, pos, destroy, isDestroyed: destroyed, bounce } = this;
|
||||
const { ball, destroy, isDestroyed: destroyed, bounce } = this;
|
||||
|
||||
if (destroyed) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (bounce(ball)) {
|
||||
// board.appendElement(Bomb.create(board, puck, pos));
|
||||
destroy();
|
||||
sound.block();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { Board } from './Board';
|
||||
import { Element } from './Element';
|
||||
import { Puck } from './Puck';
|
||||
import { Position, Size, Velocity } from './types';
|
||||
|
||||
export class Bomb extends Element {
|
||||
static SIZE: Size = { width: 25, height: 25 };
|
||||
static VELOCITY: Velocity = Velocity.create(0, 5);
|
||||
|
||||
private constructor(public board: Board, public puck: Puck, pos: Position) {
|
||||
super(Bomb.SIZE, pos, Bomb.VELOCITY);
|
||||
|
||||
const { style, size } = this;
|
||||
|
||||
style.position = 'absolute';
|
||||
style.width = `${size.width}px`;
|
||||
style.height = `${size.height}px`;
|
||||
style.borderRadius = '50%';
|
||||
style.backgroundColor = '#000000';
|
||||
// style.boxShadow = 'inset -2px -2px 8px #aaaaaa88, inset 2px 2px 8px #ffffff';
|
||||
style.boxShadow = 'rgb(170 170 170 / 40%) -1px -1px 4px inset, rgb(255 255 255 / 40%) 1px 1px 4px inset';
|
||||
|
||||
this.translate();
|
||||
}
|
||||
|
||||
static create(board: Board, puck: Puck, pos: Position): Bomb {
|
||||
return new Bomb(board, puck, pos);
|
||||
}
|
||||
|
||||
update(): this {
|
||||
super.update();
|
||||
const { puck, vel, pos, board, destroy, intersects } = this;
|
||||
if (!board.intersects(this)) {
|
||||
return destroy();
|
||||
}
|
||||
if (intersects(puck)) {
|
||||
board.reset();
|
||||
return this;
|
||||
}
|
||||
pos.x += vel.x;
|
||||
pos.y += vel.y;
|
||||
return this.translate();
|
||||
}
|
||||
|
||||
reset(): this {
|
||||
super.reset();
|
||||
return this.destroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Ball } from './Ball';
|
||||
import { Element } from './Element';
|
||||
import { Game } from './Game';
|
||||
import { Position, Size } from './types';
|
||||
|
||||
export class DebugPanel extends Element {
|
||||
ballPosition: HTMLDivElement;
|
||||
level: HTMLDivElement;
|
||||
frameCount: HTMLDivElement;
|
||||
ball: Ball;
|
||||
|
||||
private constructor(public game: Game) {
|
||||
super(Size.create(), Position.create());
|
||||
|
||||
const { style, instance } = this;
|
||||
|
||||
this.ball = game.ball;
|
||||
|
||||
style.position = 'fixed';
|
||||
style.top = '0px';
|
||||
style.left = '0px';
|
||||
style.fontSize = '18pt';
|
||||
|
||||
this.ballPosition = document.createElement('div');
|
||||
instance.appendChild(this.ballPosition);
|
||||
|
||||
this.frameCount = document.createElement('div');
|
||||
instance.appendChild(this.frameCount);
|
||||
|
||||
this.level = document.createElement('div');
|
||||
instance.appendChild(this.level);
|
||||
}
|
||||
|
||||
static create(game: Game): DebugPanel {
|
||||
return new DebugPanel(game);
|
||||
}
|
||||
|
||||
update(): this {
|
||||
super.update();
|
||||
if (this.game.frameCount % 10) {
|
||||
return this;
|
||||
}
|
||||
this.ballPosition.innerText = `Position: ${this.ball.pos.x}:${this.ball.pos.y}`;
|
||||
this.level.innerText = `Level: ${this.game.levels.currentLevelIndex + 1}`;
|
||||
this.frameCount.innerText = `Frame count: ${this.game.frameCount}`;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import { Position, Size, Velocity } from './types';
|
||||
import { between } from './utils';
|
||||
|
||||
export abstract class Element {
|
||||
instance: HTMLElement;
|
||||
elements: Element[] = [];
|
||||
style: CSSStyleDeclaration;
|
||||
isDestroyed = false;
|
||||
|
||||
protected constructor(public size: Size, public pos: Position, public vel: Velocity = Velocity.create()) {
|
||||
this.instance = document.createElement('div');
|
||||
this.style = this.instance.style;
|
||||
this.style.transition = 'transform linear 20ms';
|
||||
this.translate = this.translate.bind(this);
|
||||
this.bounce = this.bounce.bind(this);
|
||||
this.update = this.update.bind(this);
|
||||
this.reset = this.reset.bind(this);
|
||||
this.destroy = this.destroy.bind(this);
|
||||
this.intersects = this.intersects.bind(this);
|
||||
}
|
||||
|
||||
destroy(): this {
|
||||
this.isDestroyed = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
get top(): number {
|
||||
return this.pos.y;
|
||||
}
|
||||
get right(): number {
|
||||
return this.pos.x + this.size.width;
|
||||
}
|
||||
get bottom(): number {
|
||||
return this.pos.y + this.size.height;
|
||||
}
|
||||
get left(): number {
|
||||
return this.pos.x;
|
||||
}
|
||||
|
||||
appendElement(element: Element): this {
|
||||
this.elements.push(element);
|
||||
this.instance.appendChild(element.instance);
|
||||
return this;
|
||||
}
|
||||
removeElement(element: Element): this {
|
||||
this.elements.splice(this.elements.indexOf(element), 1);
|
||||
this.instance.removeChild(element.instance);
|
||||
return this;
|
||||
}
|
||||
removeAllElements(): this {
|
||||
while (this.instance.childNodes.length) {
|
||||
this.instance.removeChild(this.instance.childNodes[0]);
|
||||
}
|
||||
this.elements = [];
|
||||
return this;
|
||||
}
|
||||
|
||||
move(pos: Position): this {
|
||||
this.pos = pos;
|
||||
return this;
|
||||
}
|
||||
|
||||
translate(): this {
|
||||
this.style.transform = `translate(${this.pos.x}px, ${this.pos.y}px)`;
|
||||
return this;
|
||||
}
|
||||
|
||||
isInBoundsX(x: number, w: number): boolean {
|
||||
return x >= 0 && x + w <= this.size.width;
|
||||
}
|
||||
|
||||
isInBoundsY(y: number, h: number): boolean {
|
||||
return y >= 0 && y + h <= this.size.height;
|
||||
}
|
||||
|
||||
isInBounds(pos: Position, size: Size): boolean {
|
||||
return this.isInBoundsX(pos.x, size.width) && this.isInBoundsY(pos.y, size.height);
|
||||
}
|
||||
|
||||
intersectsTop(element: Element): boolean {
|
||||
let yIntersects = false;
|
||||
let xIntersects = false;
|
||||
|
||||
if (between(element.bottom, this.top, this.top + Math.round(this.size.height / 2))) {
|
||||
yIntersects = true;
|
||||
}
|
||||
|
||||
if (between(element.left, this.left, this.right) || between(element.right, this.left, this.right)) {
|
||||
xIntersects = true;
|
||||
}
|
||||
|
||||
return yIntersects && xIntersects;
|
||||
}
|
||||
|
||||
intersectsBottom(element: Element): boolean {
|
||||
let yIntersects = false;
|
||||
let xIntersects = false;
|
||||
|
||||
if (between(element.top, this.bottom, this.bottom - Math.round(this.size.height / 2))) {
|
||||
yIntersects = true;
|
||||
}
|
||||
|
||||
if (between(element.left, this.left, this.right) || between(element.right, this.left, this.right)) {
|
||||
xIntersects = true;
|
||||
}
|
||||
|
||||
return yIntersects && xIntersects;
|
||||
}
|
||||
|
||||
intersectsLeft(element: Element): boolean {
|
||||
let yIntersects = false;
|
||||
let xIntersects = false;
|
||||
|
||||
if (between(element.right, this.left, this.left + Math.round(this.size.width / 2))) {
|
||||
xIntersects = true;
|
||||
}
|
||||
|
||||
if (between(element.top, this.top, this.bottom) || between(element.bottom, this.top, this.bottom)) {
|
||||
yIntersects = true;
|
||||
}
|
||||
|
||||
return yIntersects && xIntersects;
|
||||
}
|
||||
|
||||
intersectsRight(element: Element): boolean {
|
||||
let yIntersects = false;
|
||||
let xIntersects = false;
|
||||
|
||||
if (between(element.left, this.right - Math.round(this.size.width / 2), this.right)) {
|
||||
xIntersects = true;
|
||||
}
|
||||
|
||||
if (between(element.top, this.top, this.bottom) || between(element.bottom, this.top, this.bottom)) {
|
||||
yIntersects = true;
|
||||
}
|
||||
|
||||
return yIntersects && xIntersects;
|
||||
}
|
||||
|
||||
intersects(element: Element): boolean {
|
||||
return (
|
||||
this.intersectsTop(element) ||
|
||||
this.intersectsRight(element) ||
|
||||
this.intersectsLeft(element) ||
|
||||
this.intersectsBottom(element)
|
||||
);
|
||||
}
|
||||
|
||||
bounce(element: Element): boolean {
|
||||
if (this.intersectsTop(element)) {
|
||||
element.vel.y = Math.abs(element.vel.y) * -1;
|
||||
return true;
|
||||
} else if (this.intersectsBottom(element)) {
|
||||
element.vel.y = Math.abs(element.vel.y);
|
||||
return true;
|
||||
} else if (this.intersectsLeft(element)) {
|
||||
element.vel.x = Math.abs(element.vel.x) * -1;
|
||||
return true;
|
||||
} else if (this.intersectsRight(element)) {
|
||||
element.vel.x = Math.abs(element.vel.x);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
update(): this {
|
||||
this.elements.forEach(element => {
|
||||
if (element.isDestroyed) {
|
||||
this.removeElement(element);
|
||||
} else {
|
||||
element.update();
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
reset(): this {
|
||||
this.elements.forEach(element => element.reset());
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+44
-38
@@ -1,45 +1,51 @@
|
||||
import { board } from './board';
|
||||
import { puck } from './puck';
|
||||
import { Position, Size, Element, Velocity } from './types';
|
||||
import { Board } from './Board';
|
||||
import { Element } from './Element';
|
||||
import { Position, Velocity } from './types';
|
||||
|
||||
export const ball = ((): Element => {
|
||||
const instance = document.createElement('div');
|
||||
const { style } = instance;
|
||||
export class Ball extends Element {
|
||||
static VELOCITY: Velocity = Velocity.create(3, 5);
|
||||
|
||||
const size: Size = { width: 20, height: 20 };
|
||||
const vel: Velocity = { x: 5, y: 5 };
|
||||
const pos = new Position(Math.round(board.size.width / 2), Math.round(board.size.height / 2));
|
||||
private constructor(public board: Board, pos?: Position) {
|
||||
super({ width: 20, height: 20 }, pos || Position.create(), Ball.VELOCITY.clone());
|
||||
|
||||
style.position = 'absolute';
|
||||
style.transition = 'transform linear 20ms';
|
||||
style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
||||
style.width = `${size.width}px`;
|
||||
style.height = `${size.height}px`;
|
||||
style.borderRadius = '50%';
|
||||
style.backgroundColor = '#ff5656';
|
||||
const { style, size } = this;
|
||||
|
||||
style.position = 'absolute';
|
||||
style.width = `${size.width}px`;
|
||||
style.height = `${size.height}px`;
|
||||
style.borderRadius = '50%';
|
||||
style.backgroundColor = '#eeeeee';
|
||||
style.boxShadow = 'inset -2px -2px 8px #aaaaaa88, inset 2px 2px 8px #ffffff';
|
||||
|
||||
this.reset().translate();
|
||||
}
|
||||
|
||||
static create(board: Board, pos?: Position): Ball {
|
||||
return new Ball(board, pos);
|
||||
}
|
||||
|
||||
reset(): this {
|
||||
const { board } = this;
|
||||
this.vel = Ball.VELOCITY.clone();
|
||||
this.move(Position.create(Math.round(board.size.width / 2), Math.round(board.size.height / 2) + 50));
|
||||
return this;
|
||||
}
|
||||
|
||||
update(): this {
|
||||
super.update();
|
||||
const { vel, pos, size, board, translate } = this;
|
||||
|
||||
const update = () => {
|
||||
if (!board.isInBoundsX(pos.x + vel.x, size.width)) {
|
||||
vel.x *= -1;
|
||||
}
|
||||
if (!board.isInBoundsY(pos.y + vel.y, size.height)) {
|
||||
vel.y *= -1;
|
||||
}
|
||||
pos.x += vel.x;
|
||||
pos.y += vel.y;
|
||||
if (puck.intersects(ball)) {
|
||||
vel.y = Math.abs(vel.y) * -1;
|
||||
}
|
||||
if (pos.y + size.height >= board.size.height) {
|
||||
pos.move(Math.round(board.size.width / 2), Math.round(board.size.height / 2));
|
||||
}
|
||||
style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
||||
};
|
||||
|
||||
return {
|
||||
size,
|
||||
pos,
|
||||
instance,
|
||||
update,
|
||||
};
|
||||
})();
|
||||
if (!board.isInBoundsX(pos.x, size.width)) {
|
||||
vel.x *= -1;
|
||||
}
|
||||
|
||||
if (!board.isInBoundsY(pos.y, size.height)) {
|
||||
vel.y *= -1;
|
||||
}
|
||||
|
||||
return translate();
|
||||
}
|
||||
}
|
||||
|
||||
+29
-20
@@ -1,26 +1,35 @@
|
||||
import { Element } from './Element';
|
||||
import { Position, Size } from './types';
|
||||
|
||||
export const board = (() => {
|
||||
const instance = document.createElement('div');
|
||||
const { style } = instance;
|
||||
export class Board extends Element {
|
||||
static SIZE: Size = { width: 833, height: 800 };
|
||||
|
||||
const boardSize: Size = { width: 433, height: 800 };
|
||||
private constructor() {
|
||||
super(Board.SIZE, Position.create(0, 0));
|
||||
const { style, size } = this;
|
||||
|
||||
style.width = `${boardSize.width}px`;
|
||||
style.height = `${boardSize.height}px`;
|
||||
style.backgroundColor = '#2d2d2d';
|
||||
style.margin = '40px auto';
|
||||
style.width = `${size.width}px`;
|
||||
style.height = `${size.height}px`;
|
||||
style.backgroundColor = '#2d2d2d';
|
||||
style.overflow = 'hidden';
|
||||
style.margin = '40px auto';
|
||||
style.backgroundColor = '#3a457b';
|
||||
// style.opacity = '0.8';
|
||||
style.backgroundImage = `linear-gradient(30deg, #0a1941 12%, transparent 12.5%, transparent 87%, #0a1941 87.5%, #0a1941),
|
||||
linear-gradient(150deg, #0a1941 12%, transparent 12.5%, transparent 87%, #0a1941 87.5%, #0a1941),
|
||||
linear-gradient(30deg, #0a1941 12%, transparent 12.5%, transparent 87%, #0a1941 87.5%, #0a1941),
|
||||
linear-gradient(150deg, #0a1941 12%, transparent 12.5%, transparent 87%, #0a1941 87.5%, #0a1941),
|
||||
linear-gradient(60deg, #0a194177 25%, transparent 25.5%, transparent 75%, #0a194177 75%, #0a194177),
|
||||
linear-gradient(60deg, #0a194177 25%, transparent 25.5%, transparent 75%, #0a194177 75%, #0a194177)
|
||||
`;
|
||||
// style.backgroundSize = '40px 70px';
|
||||
style.backgroundSize = '20% 50%';
|
||||
style.backgroundPosition = '0 0, 0 0, 20px 35px, 20px 35px, 0 0, 20px 35px';
|
||||
|
||||
const isInBoundsX = (x: number, w: number): boolean => x >= 0 && x + w <= boardSize.width;
|
||||
const isInBoundsY = (y: number, h: number): boolean => y >= 0 && y + h <= boardSize.height;
|
||||
const isInBounds = (pos: Position, size: Size): boolean =>
|
||||
isInBoundsX(pos.x, size.width) && isInBoundsY(pos.y, size.height);
|
||||
this.reset().translate();
|
||||
}
|
||||
|
||||
return {
|
||||
size: boardSize,
|
||||
instance,
|
||||
isInBoundsX,
|
||||
isInBoundsY,
|
||||
isInBounds,
|
||||
};
|
||||
})();
|
||||
static create(): Board {
|
||||
return new Board();
|
||||
}
|
||||
}
|
||||
|
||||
+59
-1
@@ -1 +1,59 @@
|
||||
export const game = { paused: false };
|
||||
import { Ball } from './Ball';
|
||||
import { Board } from './Board';
|
||||
import { Element } from './Element';
|
||||
import { Levels } from './levels';
|
||||
import { Puck } from './Puck';
|
||||
import { Position, Size } from './types';
|
||||
|
||||
export class Game extends Element {
|
||||
public paused = false;
|
||||
public frameCount = 0;
|
||||
public level = 0;
|
||||
|
||||
private constructor(
|
||||
public instance: HTMLElement,
|
||||
public board: Board,
|
||||
public puck: Puck,
|
||||
public ball: Ball,
|
||||
public levels: Levels
|
||||
) {
|
||||
super(Size.create(), Position.create());
|
||||
this.style.position = 'relative';
|
||||
|
||||
this.appendElement(board);
|
||||
board.appendElement(levels);
|
||||
board.appendElement(this.puck);
|
||||
board.appendElement(this.ball);
|
||||
}
|
||||
|
||||
gameLoop(updateGameLoop: () => void): this {
|
||||
const triggerGameLoop = (): void =>
|
||||
void requestAnimationFrame((): void => {
|
||||
this.frameCount += 1;
|
||||
try {
|
||||
return updateGameLoop();
|
||||
} finally {
|
||||
triggerGameLoop();
|
||||
}
|
||||
});
|
||||
|
||||
triggerGameLoop();
|
||||
return this;
|
||||
}
|
||||
|
||||
update(): this {
|
||||
super.update();
|
||||
|
||||
const { board, ball, reset } = this;
|
||||
|
||||
if (ball.pos.y + ball.size.height >= board.size.height) {
|
||||
return reset();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
static create(instance: HTMLElement, board: Board, puck: Puck, ball: Ball, levels: Levels): Game {
|
||||
return new Game(instance, board, puck, ball, levels);
|
||||
}
|
||||
}
|
||||
|
||||
+37
-21
@@ -1,30 +1,46 @@
|
||||
import { keyboard, Keys } from './keyboard';
|
||||
import { board } from './board';
|
||||
import { puck } from './puck';
|
||||
import { ball } from './ball';
|
||||
import { game } from './game';
|
||||
|
||||
const triggerGameLoop = () =>
|
||||
requestAnimationFrame(() => {
|
||||
if (keyboard.isKeyPressed(Keys.P)) {
|
||||
game.paused = !game.paused;
|
||||
}
|
||||
if (!game.paused) {
|
||||
puck.update();
|
||||
ball.update();
|
||||
}
|
||||
triggerGameLoop();
|
||||
});
|
||||
import { Game } from './Game';
|
||||
import { Board } from './Board';
|
||||
// import { DebugPanel } from './DebugPanel';
|
||||
import { Levels, Level1, Level2 } from './levels';
|
||||
import { Ball } from './Ball';
|
||||
import { Puck } from './Puck';
|
||||
import * as sound from './sound';
|
||||
|
||||
const run = (): void => {
|
||||
const gameElement = document.querySelector('#game');
|
||||
const gameElement = document.querySelector<HTMLElement>('#game');
|
||||
|
||||
if (!gameElement) {
|
||||
return console.error('No dom element with id game found');
|
||||
}
|
||||
gameElement.appendChild(board.instance);
|
||||
board.instance.appendChild(puck.instance);
|
||||
board.instance.appendChild(ball.instance);
|
||||
return void triggerGameLoop();
|
||||
|
||||
const board = Board.create();
|
||||
const ball = Ball.create(board);
|
||||
const puck = Puck.create(board, ball);
|
||||
const levels = Levels.create(board, ball, puck, [Level1, Level2]);
|
||||
|
||||
const game = Game.create(gameElement, board, puck, ball, levels);
|
||||
window.game = game;
|
||||
|
||||
// const debugPanel = DebugPanel.create(game);
|
||||
// game.appendElement(debugPanel);
|
||||
|
||||
return void game.gameLoop(() => {
|
||||
if (keyboard.isKeyPressed(Keys.r)) {
|
||||
game.reset();
|
||||
}
|
||||
if (keyboard.isKeyPressed(Keys.p)) {
|
||||
game.paused = !game.paused;
|
||||
}
|
||||
if (keyboard.isKeyPressed(Keys.s)) {
|
||||
sound.toggleEnabledDisabled();
|
||||
}
|
||||
if (game.paused) {
|
||||
return;
|
||||
}
|
||||
game.update();
|
||||
// debugPanel.update();
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', run);
|
||||
|
||||
+16
-4
@@ -1,22 +1,34 @@
|
||||
export enum Keys {
|
||||
ARROW_RIGHT = 'ArrowRight',
|
||||
ARROW_LEFT = 'ArrowLeft',
|
||||
P = 'p',
|
||||
p = 'p',
|
||||
s = 's',
|
||||
r = 'r',
|
||||
}
|
||||
|
||||
const downKeys: Partial<Record<Keys, boolean>> = {};
|
||||
const pressedKeys: Partial<Record<Keys, boolean>> = {};
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
downKeys[e.key as Keys] = true;
|
||||
});
|
||||
|
||||
document.addEventListener('keypress', e => {
|
||||
pressedKeys[e.key as Keys] = true;
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', e => {
|
||||
pressedKeys[e.key as Keys] = false;
|
||||
downKeys[e.key as Keys] = false;
|
||||
});
|
||||
|
||||
export const keyboard = {
|
||||
pressedKeys,
|
||||
pressedKeys: downKeys,
|
||||
isKeyPressed(key: Keys): boolean {
|
||||
return Boolean(pressedKeys[key]);
|
||||
const isPressed = Boolean(pressedKeys[key]);
|
||||
pressedKeys[key] = false;
|
||||
return isPressed;
|
||||
},
|
||||
isKeyDown(key: Keys): boolean {
|
||||
return Boolean(downKeys[key]);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Level } from './types';
|
||||
|
||||
export const Level1 = Level.create([
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
|
||||
[0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
] as const);
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Level } from './types';
|
||||
|
||||
export const Level2: Level = Level.create([
|
||||
[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, 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, 1, 1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
] as const);
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Puck } from '../Puck';
|
||||
import { Board } from '../Board';
|
||||
import { Level, LevelInstance } from './types';
|
||||
import { Position, Size } from '../types';
|
||||
import { Element } from '../Element';
|
||||
import { Block } from '../Block';
|
||||
import { Ball } from '../Ball';
|
||||
|
||||
export class Levels extends Element {
|
||||
currentLevelIndex = -1;
|
||||
currentLevelInstance: LevelInstance | undefined;
|
||||
blocks: Block[] = [];
|
||||
|
||||
private constructor(public board: Board, public ball: Ball, public puck: Puck, public levels: Level[]) {
|
||||
super(board.size, Position.create());
|
||||
const { style } = this;
|
||||
|
||||
style.position = 'absolute';
|
||||
style.top = '0';
|
||||
style.left = '0';
|
||||
style.width = '100%';
|
||||
style.height = '100%';
|
||||
|
||||
if (levels.length === 0) {
|
||||
throw new Error('No levels have been specified');
|
||||
}
|
||||
|
||||
this.update = this.update.bind(this);
|
||||
this.gotoNextLevel = this.gotoNextLevel.bind(this);
|
||||
|
||||
this.gotoNextLevel();
|
||||
}
|
||||
|
||||
get hasNextLevel(): boolean {
|
||||
return this.levels.length > this.currentLevelIndex + 1;
|
||||
}
|
||||
|
||||
gotoNextLevel(): this {
|
||||
if (!this.hasNextLevel) {
|
||||
return this;
|
||||
}
|
||||
this.currentLevelIndex += 1;
|
||||
return this.resetLevel();
|
||||
}
|
||||
|
||||
resetLevel(): this {
|
||||
const { board, size, puck, ball } = this;
|
||||
this.currentLevelInstance = this.levels[this.currentLevelIndex].create();
|
||||
|
||||
const { grid } = this.currentLevelInstance;
|
||||
|
||||
const numberOfRows = grid.length;
|
||||
const numberOfBlocksPerRow = grid[0].length;
|
||||
|
||||
const blockHeight = Math.round((size.height - 10) / 2 / numberOfRows) - 5;
|
||||
const blockWidth = Math.round((size.width - 10) / numberOfBlocksPerRow) - 5;
|
||||
|
||||
this.removeAllElements();
|
||||
for (let x = 0; x < numberOfBlocksPerRow; x += 1) {
|
||||
for (let y = 0; y < numberOfRows; y += 1) {
|
||||
const blockValue = grid[y][x];
|
||||
if (blockValue) {
|
||||
const block = Block.create(
|
||||
board,
|
||||
ball,
|
||||
puck,
|
||||
Position.create(5 + x * (blockWidth + 5), 5 + y * (blockHeight + 5)),
|
||||
Size.create(blockWidth, blockHeight)
|
||||
);
|
||||
this.appendElement(block);
|
||||
this.blocks.push(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
update(): this {
|
||||
super.update();
|
||||
// this.blocks.forEach(block => block.update());
|
||||
const allDestroyed = this.blocks.reduce((all, block) => all && block.isDestroyed, true);
|
||||
|
||||
if (allDestroyed) {
|
||||
this.gotoNextLevel();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
reset(): this {
|
||||
super.reset();
|
||||
this.currentLevelIndex = 0;
|
||||
return this.resetLevel();
|
||||
}
|
||||
|
||||
static create(board: Board, ball: Ball, puck: Puck, levels: Level[]): Levels {
|
||||
return new Levels(board, ball, puck, levels);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './types';
|
||||
export * from './Levels';
|
||||
export * from './Level1';
|
||||
export * from './Level2';
|
||||
@@ -0,0 +1,55 @@
|
||||
export type BlockState = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
||||
export type RowState = [
|
||||
BlockState,
|
||||
BlockState,
|
||||
BlockState,
|
||||
BlockState,
|
||||
BlockState,
|
||||
BlockState,
|
||||
BlockState,
|
||||
BlockState,
|
||||
BlockState,
|
||||
BlockState,
|
||||
BlockState,
|
||||
BlockState,
|
||||
BlockState,
|
||||
BlockState
|
||||
];
|
||||
export type GridState = [
|
||||
RowState,
|
||||
RowState,
|
||||
RowState,
|
||||
RowState,
|
||||
RowState,
|
||||
RowState,
|
||||
RowState,
|
||||
RowState,
|
||||
RowState,
|
||||
RowState
|
||||
];
|
||||
|
||||
export type Block = Readonly<0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9>;
|
||||
export type Row = Readonly<
|
||||
[Block, Block, Block, Block, Block, Block, Block, Block, Block, Block, Block, Block, Block, Block]
|
||||
>;
|
||||
export type Grid = Readonly<[Row, Row, Row, Row, Row, Row, Row, Row, Row, Row]>;
|
||||
|
||||
export class LevelInstance {
|
||||
constructor(public grid: GridState) {}
|
||||
|
||||
static create(grid: GridState): LevelInstance {
|
||||
return new LevelInstance(grid);
|
||||
}
|
||||
}
|
||||
|
||||
export class Level {
|
||||
constructor(private grid: Grid) {}
|
||||
|
||||
create(): LevelInstance {
|
||||
return LevelInstance.create(this.grid as GridState);
|
||||
}
|
||||
|
||||
static create(grid: Grid): Level {
|
||||
return new Level(grid);
|
||||
}
|
||||
}
|
||||
+59
-57
@@ -1,60 +1,62 @@
|
||||
import { board } from './board';
|
||||
import { Ball } from './Ball';
|
||||
import { Board } from './Board';
|
||||
import { keyboard, Keys } from './keyboard';
|
||||
import { Position, Size, Element } from './types';
|
||||
import { Element } from './Element';
|
||||
import { Position, Size } from './types';
|
||||
import * as sound from './sound';
|
||||
|
||||
export interface Puck extends Element {
|
||||
intersects(element: Element): boolean;
|
||||
export class Puck extends Element {
|
||||
static BOTTOM_MARGIN = 30;
|
||||
static SIZE: Size = { width: 90, height: 30 };
|
||||
static STEP_SIZE = 10;
|
||||
|
||||
private constructor(public board: Board, public ball: Ball, pos?: Position) {
|
||||
super(Puck.SIZE, pos || Position.create());
|
||||
const { style, size } = this;
|
||||
|
||||
style.position = 'absolute';
|
||||
style.zIndex = '1';
|
||||
style.width = `${size.width}px`;
|
||||
style.height = `${size.height}px`;
|
||||
style.borderRadius = '6px';
|
||||
style.backgroundColor = '#36afe8';
|
||||
style.boxShadow = ' 2px 3px 5px #333, inset -2px -2px 4px black, inset 2px 2px 4px white';
|
||||
|
||||
this.reset().translate();
|
||||
}
|
||||
|
||||
static create(board: Board, ball: Ball, pos?: Position): Puck {
|
||||
return new Puck(board, ball, pos);
|
||||
}
|
||||
|
||||
reset(): this {
|
||||
super.reset();
|
||||
const { board, size } = this;
|
||||
this.move(
|
||||
Position.create(
|
||||
Math.round((board.size.width - size.width) / 2),
|
||||
board.size.height - size.height - Puck.BOTTOM_MARGIN
|
||||
)
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
update(): this {
|
||||
super.update();
|
||||
const { translate, board, pos, size, ball, bounce } = this;
|
||||
|
||||
if (keyboard.isKeyDown(Keys.ARROW_RIGHT) && board.isInBounds(pos.clone().addX(Puck.STEP_SIZE), size)) {
|
||||
pos.addX(Puck.STEP_SIZE);
|
||||
}
|
||||
|
||||
if (keyboard.isKeyDown(Keys.ARROW_LEFT) && board.isInBounds(pos.clone().addX(-Puck.STEP_SIZE), size)) {
|
||||
pos.addX(-Puck.STEP_SIZE);
|
||||
}
|
||||
|
||||
if (bounce(ball)) {
|
||||
sound.puck();
|
||||
}
|
||||
|
||||
return translate();
|
||||
}
|
||||
}
|
||||
|
||||
export const puck = ((): Puck => {
|
||||
const instance = document.createElement('div');
|
||||
const { style } = instance;
|
||||
|
||||
const bottomMargin = 30;
|
||||
const size: Size = { width: 70, height: 20 };
|
||||
const pos = new Position(0, board.size.height - size.height - bottomMargin);
|
||||
const stepSize = 10;
|
||||
|
||||
style.position = 'absolute';
|
||||
style.zIndex = '1';
|
||||
style.transition = 'transform linear 20ms';
|
||||
style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
||||
style.width = `${size.width}px`;
|
||||
style.height = `${size.height}px`;
|
||||
style.borderRadius = '10%';
|
||||
style.backgroundColor = '#36afe8';
|
||||
|
||||
const update = () => {
|
||||
if (keyboard.isKeyPressed(Keys.ARROW_RIGHT) && board.isInBounds(pos.clone().addX(stepSize), size)) {
|
||||
pos.addX(stepSize);
|
||||
} else if (keyboard.isKeyPressed(Keys.ARROW_LEFT) && board.isInBounds(pos.clone().addX(-stepSize), size)) {
|
||||
pos.addX(-stepSize);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
||||
};
|
||||
|
||||
const intersects = (element: Element): boolean => {
|
||||
let yIntersects = false;
|
||||
let xIntersects = false;
|
||||
if (element.pos.y + element.size.height >= pos.y && element.pos.y + element.size.height <= pos.y + 1) {
|
||||
yIntersects = true;
|
||||
}
|
||||
if (element.pos.x > pos.x && element.pos.x < pos.x + size.width) {
|
||||
xIntersects = true;
|
||||
}
|
||||
if (element.pos.x + element.size.width > pos.x && element.pos.x + element.size.width < pos.x + size.width) {
|
||||
xIntersects = true;
|
||||
}
|
||||
return yIntersects && xIntersects;
|
||||
};
|
||||
|
||||
return {
|
||||
size,
|
||||
pos,
|
||||
instance,
|
||||
update,
|
||||
intersects,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
const audioContext = new AudioContext();
|
||||
|
||||
let isSoundDisabled = true;
|
||||
|
||||
export const disable = (): void => void (isSoundDisabled = true);
|
||||
export const enable = (): void => void (isSoundDisabled = false);
|
||||
export const toggleEnabledDisabled = (): void => void (isSoundDisabled = !isSoundDisabled);
|
||||
|
||||
export const puck = (): void => {
|
||||
if (isSoundDisabled) {
|
||||
return;
|
||||
}
|
||||
const oscillator = audioContext.createOscillator();
|
||||
oscillator.type = 'triangle';
|
||||
const gainNode = audioContext.createGain();
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
gainNode.gain.value = 0;
|
||||
oscillator.frequency.value = 200;
|
||||
oscillator.start(0);
|
||||
oscillator.frequency.value = 100;
|
||||
gainNode.gain.value = 0.1;
|
||||
setTimeout(() => {
|
||||
gainNode.gain.value = 0;
|
||||
}, 250);
|
||||
oscillator.frequency.value += 1;
|
||||
};
|
||||
|
||||
export const block = (): void => {
|
||||
if (isSoundDisabled) {
|
||||
return;
|
||||
}
|
||||
const oscillator = audioContext.createOscillator();
|
||||
oscillator.type = 'triangle';
|
||||
const gainNode = audioContext.createGain();
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
gainNode.gain.value = 0;
|
||||
oscillator.frequency.value = 200;
|
||||
oscillator.start(0);
|
||||
oscillator.frequency.value = 200;
|
||||
gainNode.gain.value = 0.1;
|
||||
setTimeout(() => {
|
||||
gainNode.gain.value = 0;
|
||||
}, 250);
|
||||
oscillator.frequency.value += 1;
|
||||
};
|
||||
+27
-12
@@ -1,11 +1,30 @@
|
||||
export interface Velocity {
|
||||
x: number;
|
||||
y: number;
|
||||
export class Velocity {
|
||||
constructor(public x: number, public y: number) {}
|
||||
|
||||
get absX(): number {
|
||||
return Math.abs(this.x);
|
||||
}
|
||||
|
||||
get absY(): number {
|
||||
return Math.abs(this.y);
|
||||
}
|
||||
|
||||
clone(): Velocity {
|
||||
return Velocity.create(this.x, this.y);
|
||||
}
|
||||
|
||||
static create(x?: number, y?: number): Velocity {
|
||||
return new Velocity(x || 0, y || 0);
|
||||
}
|
||||
}
|
||||
|
||||
export class Position {
|
||||
constructor(public x: number, public y: number) {}
|
||||
|
||||
static create(x?: number, y?: number): Position {
|
||||
return new Position(x || 0, y || 0);
|
||||
}
|
||||
|
||||
clone(): Position {
|
||||
return new Position(this.x, this.y);
|
||||
}
|
||||
@@ -41,14 +60,10 @@ export class Position {
|
||||
}
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
export class Size {
|
||||
constructor(public width: number, public height: number) {}
|
||||
|
||||
export interface Element {
|
||||
size: Size;
|
||||
pos: Position;
|
||||
instance: HTMLElement;
|
||||
update(): void;
|
||||
static create(width?: number, height?: number): Size {
|
||||
return new Size(width || 0, height || 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const between = (value: number, boundary1: number, boundary2: number): boolean =>
|
||||
boundary1 < boundary2 ? boundary1 <= value && boundary2 >= value : boundary2 <= value && boundary1 >= value;
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
"downlevelIteration": true,
|
||||
"noEmit": true,
|
||||
"importHelpers": true,
|
||||
"typeRoots": ["./src/types", "./types", "./node_modules/@types"]
|
||||
"typeRoots": ["./src/@types", "./node_modules/@types"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
Reference in New Issue
Block a user