implement basic arkanoid version

This commit is contained in:
2021-11-23 21:10:22 +01:00
parent 814c564a85
commit e738cdd9be
23 changed files with 2070 additions and 13979 deletions
+2 -2
View File
@@ -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',
+1206 -13811
View File
File diff suppressed because it is too large Load Diff
+1 -4
View File
@@ -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"
}
}
+7
View File
@@ -0,0 +1,7 @@
import { Game } from '../Game';
declare global {
interface Window {
game: Game;
}
}
+60
View File
@@ -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
View File
@@ -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();
}
}
+48
View File
@@ -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
View File
@@ -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;
}
}
+38 -32
View File
@@ -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());
const { style, size } = this;
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';
style.backgroundColor = '#eeeeee';
style.boxShadow = 'inset -2px -2px 8px #aaaaaa88, inset 2px 2px 8px #ffffff';
const update = () => {
if (!board.isInBoundsX(pos.x + vel.x, size.width)) {
vel.x *= -1;
this.reset().translate();
}
if (!board.isInBoundsY(pos.y + vel.y, size.height)) {
vel.y *= -1;
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;
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();
}
}
+27 -18
View File
@@ -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.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
View File
@@ -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
View File
@@ -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
View File
@@ -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]);
},
};
+14
View File
@@ -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);
+14
View File
@@ -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);
+98
View File
@@ -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);
}
}
+4
View File
@@ -0,0 +1,4 @@
export * from './types';
export * from './Levels';
export * from './Level1';
export * from './Level2';
+55
View File
@@ -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);
}
}
+48 -46
View File
@@ -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;
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;
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.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.borderRadius = '6px';
style.backgroundColor = '#36afe8';
style.boxShadow = ' 2px 3px 5px #333, inset -2px -2px 4px black, inset 2px 2px 4px white';
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;
this.reset().translate();
}
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;
static create(board: Board, ball: Ball, pos?: Position): Puck {
return new Puck(board, ball, pos);
}
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,
};
})();
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();
}
}
+49
View File
@@ -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
View File
@@ -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);
}
}
+2
View File
@@ -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
View File
@@ -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"]