-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathbreakout.js
516 lines (468 loc) · 18.7 KB
/
breakout.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
document.getElementById("level").innerText=currentLevel;
document.getElementById("num-levels").innerText=levels.length;
var running = false;
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const width = 1000;
const height = 500;
var timeRemaining, time, prevTime, currentLevel;
var paddle = {
width: 80,
height: 14,
speed: 5
};
paddle.startXPos = paddle.xPos = (width - paddle.width)/2;
paddle.yPos = height - paddle.height;
var ball = {
radius: 8,
angle: Math.PI/2,
speed: 4,
active: false // used to track whether played has pressed space bar to launch ball at start of level
};
ball.centre = {
xPos: paddle.startXPos + paddle.width/2,
yPos: (height - paddle.height - ball.radius)
};
// "helper" method to reduce all angles to the range (-pi, pi]
ball.normaliseAngle = function(angle) {
if (angle>0) {
while (angle>Math.PI) {
angle -= 2*Math.PI;
}
}
if (angle<0) {
while (angle<=-Math.PI) {
angle += 2*Math.PI;
}
}
return angle;
}
// powerup definitions
var plusTimeImg = new Image(20, 15);
plusTimeImg.src = "images/+time.png";
var minusTimeImg = new Image(20, 15);
minusTimeImg.src = "images/-time.png";
var plusPaddleImg = new Image(20, 15);
plusPaddleImg.src = "images/+paddle.png";
var minusPaddleImg = new Image(20, 15);
minusPaddleImg.src = "images/-paddle.png";
var plusSpeedImg = new Image(20, 15);
plusSpeedImg.src = "images/+speed.png";
var minusSpeedImg = new Image(20, 15);
minusSpeedImg.src = "images/-speed.png";
var plusBallImg = new Image(20, 15);
plusBallImg.src = "images/+ball.png";
var minusBallImg = new Image(20, 15);
minusBallImg.src = "images/-ball.png";
var powerups = [];
var powerupTypes = [{name: "+time", img: plusTimeImg},
{name: "-time", img: minusTimeImg},
{name: "+paddle", img: plusPaddleImg},
{name: "-paddle", img: minusPaddleImg},
{name: "+speed", img: plusSpeedImg},
{name: "-speed", img: minusSpeedImg},
{name: "+ball", img: plusBallImg},
{name: "-ball", img: minusBallImg}];
$("body").keydown(function(e) {
if (e.keyCode == 39) {
e.preventDefault();
paddle.goingEast = true;;
}
else if (e.keyCode == 37) {
e.preventDefault();
paddle.goingWest = true;
}
});
$("body").keyup(function(e) {
if (e.keyCode == 39) {
e.preventDefault();
paddle.goingEast = false;
}
else if (e.keyCode == 37) {
e.preventDefault();
paddle.goingWest = false;
}
});
function makeBlocks() {
// use level array to compute actual block positions:
blocks.data = [];
levels[currentLevel-1].blocks.forEach(function(row, rowNo) {
var lastEntry;
row.forEach(function(entry, colNo) {
if (entry>0) {
if (entry === lastEntry) {
// extend previous block's width
blocks.data[blocks.data.length-1].width += blocks.unitWidth;
}
else if (entry != 9) { // 9 will code for a powerup-producing block
// add new block
blocks.data.push({
xPos: blocks.unitWidth*colNo,
yPos: blocks.rowHeight*rowNo + blocks.heightOffset,
width: blocks.unitWidth,
height: blocks.rowHeight,
colour: colours[entry-1],
stillThere: true // used later to track blocks hit by the ball
});
}
else { // power-up block, they will be coloured WHITE
blocks.data.push({
xPos: blocks.unitWidth*colNo,
yPos: blocks.rowHeight*rowNo + blocks.heightOffset,
width: blocks.unitWidth,
height: blocks.rowHeight,
colour: "white",
stillThere: true
});
}
}
lastEntry = entry;
})
});
}
function initialise() {
document.getElementById("level").innerText=currentLevel;
ball.centre = {
xPos: paddle.startXPos + paddle.width/2,
yPos: (height - paddle.height - ball.radius)
};
ball.speed = 4;
ball.angle = Math.PI/2;
ball.active = false;
paddle.width = 80;
paddle.speed = 5;
paddle.xPos = paddle.startXPos;
paddle.goingEast = paddle.goingWest = false;
makeBlocks();
powerups = [];
running = true;
timeRemaining = (levels[currentLevel-1].time+1)*1000; // add extra second to get starting time displayed
// correctly. It is taken off at the end!
time = Date.now();
$("body").keydown(function launch(e) {
// space bar to launch ball
if (e.keyCode == 32) {
e.preventDefault();
ball.active = true;
$("body").off("keydown", launch);
}
});
$("#start").prop("disabled", true);
$("#stop").prop("disabled", false);
gameLoop();
}
function clearCanvas() {
// clears everything from canvas, before redrawing
ctx.clearRect(0, 0, width, height);
}
function drawStuff() {
// draw paddle
if (paddle.gotPowerup) {
ctx.fillStyle = "yellow";
paddle.gotPowerup = false;
}
else {
ctx.fillStyle = "brown";
}
ctx.fillRect(paddle.xPos+paddle.height/2, paddle.yPos, paddle.width-paddle.height, paddle.height);
ctx.beginPath();
ctx.arc(paddle.xPos+paddle.height/2, paddle.yPos+paddle.height/2, paddle.height/2, Math.PI/2, 3*Math.PI/2);
ctx.fill();
ctx.beginPath();
ctx.arc(paddle.xPos+paddle.width-paddle.height/2, paddle.yPos+paddle.height/2, paddle.height/2, -Math.PI/2, Math.PI/2);
ctx.fill();
// draw ball
ctx.lineWidth = 0.5;
ctx.strokeStyle = "black";
ctx.fillStyle = "white";
ctx.beginPath();
ctx.arc(ball.centre.xPos, ball.centre.yPos, ball.radius, 0, 2*Math.PI);
ctx.fill();
ctx.stroke();
// draw blocks
ctx.strokeStyle = "black";
ctx.lineWidth = 0.5;
blocks.data.forEach(function(block) {
ctx.fillStyle = block.colour;
ctx.fillRect(block.xPos, block.yPos, block.width, block.height);
ctx.strokeRect(block.xPos, block.yPos, block.width, block.height);
});
// draw powerups
powerups.forEach(function(powerup) {
ctx.drawImage(powerup.image, powerup.xPos, powerup.yPos);
});
}
function moveStuff() {
// compute new positions of ball and paddle
// paddle:
// first save previous position:
paddle.prevXPos = paddle.xPos;
// calculate new positions
if (paddle.goingEast) {
paddle.xPos += paddle.speed;
}
if (paddle.goingWest) {
paddle.xPos -= paddle.speed;
}
// ball:
if (ball.active) {
ball.centre.xPos += Math.cos(ball.angle)*ball.speed;
ball.centre.yPos -= Math.sin(ball.angle)*ball.speed;
}
else {
ball.centre.xPos = paddle.xPos + paddle.width/2; // ball needs to stick to paddle!
}
// powerups, fall when on screen:
powerups.forEach(function(powerup) {
powerup.yPos += powerup.speed;
});
}
function hitDetection() {
var hitBlockBelow, hitBlockAbove, hitBlockLeft, hitBlockRight;
// first handle powerups!
powerups.forEach(function(powerup) {
// if hitting paddle, they disappear and take effect!
if (powerup.xPos+powerup.width>=paddle.xPos && powerup.xPos<=paddle.xPos+paddle.width
&& powerup.yPos+powerup.height>=paddle.yPos) {
powerup.stillThere = false;
paddle.gotPowerup = true;
if (powerup.text == "+time") {
timeRemaining += 60000;
}
else if (powerup.text == "-time") {
timeRemaining = Math.max(timeRemaining-60000, 1000);
}
else if (powerup.text == "+paddle") {
// make sure the expansion keeps the same centre, not the same left boundary!
oldWidth = paddle.width;
oldXPos = paddle.xPos;
paddle.width = Math.min(paddle.width+20, width);
paddle.xPos = (oldXPos+oldWidth/2)-paddle.width/2;
// paddle movement, below, will move it as needed to fit on the screen!
}
else if (powerup.text == "-paddle") {
oldWidth = paddle.width;
oldXPos = paddle.xPos;
paddle.width = Math.max(paddle.width-20, 20);
paddle.xPos = (oldXPos+oldWidth/2)-paddle.width/2;
}
else if (powerup.text == "+speed") {
paddle.speed *= 1.3;
}
else if (powerup.text == "-speed") {
paddle.speed /= 1.3;
}
else if (powerup.text == "+ball") {
ball.speed *= 1.3;
}
else if (powerup.text == "-ball") {
ball.speed /= 1.3;
}
}
// just disappear without effect if it hits the bottom:
else if (powerup.yPos+powerup.height>=height) {
powerup.stillThere = false;
}
});
// update powerups array by removing ones that are no longer there:
powerups = powerups.filter(powerup => powerup.stillThere);
// next deal with paddle
// paddle can't move beyond right/left walls
if (paddle.xPos<=0) {
paddle.xPos = 0;
}
if (paddle.xPos>=width-paddle.width) {
paddle.xPos = width - paddle.width;
}
// the rest is altering the ball's path - obviously kind of the whole point of the game ;)
// bottom of screen: bounce off paddle if it's there
if (ball.centre.yPos+ball.radius>=height-paddle.height) {
if (paddle.xPos<=ball.centre.xPos+ball.radius && ball.centre.xPos-ball.radius<=paddle.xPos+paddle.width) {
if (ball.angle<0) {
// avoid "wobbling" by not changing direction if ball is already going upwards
// now change angle, depending on position on paddle it hits (not very physicallyl realistic,
// but needed to give player any chance of controlling the ball's path, and usually done)
// we do this by setting the new angle to be that made from the horizontal by a straight line from
// a point 10px below the bottom middle of the paddle to the contact point
// (the very bottom of the ball)
var paddleCentre = {x: paddle.xPos + paddle.width/2, y: height + 10};
var contactPoint = {x: ball.centre.xPos, y: ball.centre.yPos + ball.radius};
var xDist = contactPoint.x - paddleCentre.x;
var yDist = contactPoint.y - paddleCentre.y;
var distance = Math.sqrt(xDist*xDist + yDist*yDist);
ball.angle = Math.acos(xDist/distance);
}
}
}
// bounce off top of screen:
if (ball.centre.yPos-ball.radius<=0) {
// more "wobble avoidance":
if (ball.angle>0) {
ball.angle = ball.normaliseAngle(-ball.angle);
}
}
// if bottom of screen reached, lose the game!
if (ball.centre.yPos+ball.radius>=height) {
running = false;
bootbox.alert("Game over - the ball fell off the bottom!", initialise);
}
// bounce off left and right walls:
if (ball.centre.xPos-ball.radius<=0) {
// wobble avoidance:
if (Math.abs(ball.angle)>Math.PI/2) {
ball.angle = ball.normaliseAngle(Math.PI - ball.angle);
}
}
if (ball.centre.xPos+ball.radius>=width) {
// wobble avoidance
if (Math.abs(ball.angle)<=Math.PI/2) {
ball.angle = ball.normaliseAngle(Math.PI - ball.angle);
}
}
// bounce off blocks, and destroy hit block
// first we'll run over every block, and check for a hit:
blocks.data.forEach(function(block) {
if (ball.centre.xPos+ball.radius>=block.xPos &&
ball.centre.xPos-ball.radius<=block.xPos+block.width &&
ball.centre.yPos+ball.radius>=block.yPos &&
ball.centre.yPos-ball.radius<=block.yPos+block.height) {
// first make sure the block is removed from the screen!
block.stillThere = false;
// then generate a new powerup if the block was white:
if (block.colour == "white") {
var randomPowerup = powerupTypes[Math.floor(powerupTypes.length*Math.random())];
powerups.push({
xPos: (block.xPos+(block.width-blocks.unitWidth)/2),
yPos: block.yPos,
width: blocks.unitWidth,
height: blocks.rowHeight,
speed: 2,
text: randomPowerup.name,
image: randomPowerup.img,
stillThere: true
});
}
// now work out new angle for ball. It depends on whether it hit the block from above/below
// or left/right.
if ((ball.centre.yPos<block.yPos && !hitBlockAbove)
|| (ball.centre.yPos>block.yPos+block.height && !hitBlockBelow)) {
// ball hitting from above or below. We use "hitBlockAbove" and "hitBlockBelow" to make
// sure the ball still bounces after hitting 2 blocks
ball.angle = ball.normaliseAngle(-ball.angle);
if (ball.centre.yPos<block.yPos) {
hitBlockAbove = true;
}
if (ball.centre.yPos>block.yPos+block.height) {
hitBlockBelow = true;
}
}
else if ((ball.centre.xPos<block.xPos && !hitBlockLeft)
|| (ball.centre.xPos>block.xPos+block.width && !hitBlockRight)) {
// hitting from right or left:
ball.angle = ball.normaliseAngle(Math.PI - ball.angle);
if (ball.centre.xPos<block.xPos) {
hitBlockLeft = true;
}
if (ball.centre.xPos>block.xPos+block.width) {
hitBlockRight = true;
}
}
}
});
// update array of blocks:
blocks.data = blocks.data.filter(block => block.stillThere);
if (blocks.data.length == 0) {
clearCanvas();
drawStuff();
running = false;
bootbox.alert("Congratulations - level complete!", function() {
if (currentLevel == levels.length) {
bootbox.alert("Well done, you've completed all currently available levels!", quit);
}
else {
currentLevel++;
initialise();
}
});
}
}
function timer() {
prevTime = time;
time = Date.now();
timeRemaining -= (time - prevTime);
if (timeRemaining<1000) {
ctx.font = "24px Arial";
var minsLeft = Math.floor(timeRemaining/60000);
var leftoverSeconds = Math.floor((timeRemaining - minsLeft*60000)/1000);
var timeString = minsLeft + ":" + (Math.floor(leftoverSeconds)/100).toFixed(2).slice(2);
ctx.fillStyle = "red";
ctx.fillText(timeString, 30, 30);
running = false;
bootbox.alert("Game over - time ran out!", initialise);
}
else {
ctx.font = "24px Arial";
var minsLeft = Math.floor(timeRemaining/60000);
var leftoverSeconds = Math.floor((timeRemaining - minsLeft*60000)/1000);
var timeString = minsLeft + ":" + (leftoverSeconds/100).toFixed(2).slice(2);
if (timeRemaining < 31000) {
ctx.fillStyle = "red";
}
else {
ctx.fillStyle = "black";
}
ctx.fillText(timeString, 30, 30);
}
}
function gameLoop() {
if (running) {
clearCanvas();
timer();
if (running) {
drawStuff();
moveStuff();
hitDetection();
requestAnimationFrame(gameLoop);
}
}
}
function startGame() {
currentLevel = 1;
initialise();
}
function quit() {
// drawStuff();
running = false;
$("#start").prop("disabled", false);
$("#stop").prop("disabled", true);
}
function helpText() {
bootbox.alert({
message: "<p>This is a version of the classic Breakout game. The object is simply to clear the screen of all the coloured blocks before the time runs out.</p>"
+ "<p>At the start of each level, press the <strong>space bar</strong> to launch the ball upwards - after first moving the paddle to your preferred starting location.</p>"
+ "<p>After that, the only controls are the <strong>left</strong> and <strong>right arrow keys</strong>, which move the paddle left and right along the bottom of the screen. If the ball hits the bottom, you lose.</p>"
+ "<p>The angle that the ball bounces back at depends on which part of the paddle it hits - the nearer the edge, the sharper the angle.</p>"
+ "<p>All <span class='powerup'>white</span> blocks release a \"powerup\" when destroyed. They fall down the screen and can be collected with your paddle.</p>"
+ "<p>But many of them are actually bad - be careful what you take!</p>"
+ "<table class='table table-striped'>"
+ "<tbody>"
+ "<tr><td><img src='images/+time.png'></td>"
+ "<td>Increases the time available by 1 minute.</td></tr>"
+ "<tr><td><img src='images/-time.png'></td>"
+ "<td>Decreases the time available by 1 minute - you will instantly lose if you take this with less than 1 minute left!</td></tr>"
+ "<tr><td><img src='images/+paddle.png'></td>"
+ "<td>Increases the width of your paddle.</td></tr>"
+ "<tr><td><img src='images/-paddle.png'></td>"
+ "<td>Decreases the width of your paddle.</td></tr>"
+ "<tr><td><img src='images/+speed.png'></td>"
+ "<td>Increases the speed of your paddle. Good for getting to the ball quicker - but be careful, if your paddle gets too fast you will lose a lot of control over its position, and therefore the ball's direction!</td></tr>"
+ "<tr><td><img src='images/-speed.png'></td>"
+ "<td>Decreases the speed of your paddle.</td></tr>"
+ "<tr><td><img src='images/+ball.png'></td>"
+ "<td>Increases the speed of the ball. This is a bit of a double-edged sword. It makes it harder to reach the ball in time - but a few of these can really help you complete the level quicker.</td></tr>"
+ "<tr><td><img src='images/-ball.png'></td>"
+ "<td>Decreases the speed of the ball.</td></tr>"
+ "</tbody></table>"
});
}
gameLoop();