punter.js 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/.vscode/launch.json +15 -0
  2. package/README.MD +41 -46
  3. package/games/breakout.html +297 -0
  4. package/games/pong.html +59 -39
  5. package/images/breakout/ball.png +0 -0
  6. package/images/breakout/brick-blue.png +0 -0
  7. package/images/breakout/brick-green.png +0 -0
  8. package/images/breakout/brick-orange.png +0 -0
  9. package/images/breakout/brick-red.png +0 -0
  10. package/images/breakout/brick-yellow.png +0 -0
  11. package/images/breakout/paddle.png +0 -0
  12. package/images/pong/ball.png +0 -0
  13. package/package.json +3 -2
  14. package/punter.js +54 -35
  15. package/sounds/alarm1.mp3 +0 -0
  16. package/sounds/alarm2.mp3 +0 -0
  17. package/sounds/alarm3.mp3 +0 -0
  18. package/sounds/alarm4.mp3 +0 -0
  19. package/sounds/beep1.mp3 +0 -0
  20. package/sounds/beep2.mp3 +0 -0
  21. package/sounds/beep3.mp3 +0 -0
  22. package/sounds/beep4.mp3 +0 -0
  23. package/sounds/beep5.mp3 +0 -0
  24. package/sounds/beep6.mp3 +0 -0
  25. package/sounds/bling1.mp3 +0 -0
  26. package/sounds/bling2.mp3 +0 -0
  27. package/sounds/bling3.mp3 +0 -0
  28. package/sounds/bling4.mp3 +0 -0
  29. package/sounds/bling5.mp3 +0 -0
  30. package/sounds/cannon1.mp3 +0 -0
  31. package/sounds/cannon2.mp3 +0 -0
  32. package/sounds/chime1.mp3 +0 -0
  33. package/sounds/chime2.mp3 +0 -0
  34. package/sounds/chime3.mp3 +0 -0
  35. package/sounds/chime4.mp3 +0 -0
  36. package/sounds/chime5.mp3 +0 -0
  37. package/sounds/click1.mp3 +0 -0
  38. package/sounds/click2.mp3 +0 -0
  39. package/sounds/click3.mp3 +0 -0
  40. package/sounds/click4.mp3 +0 -0
  41. package/sounds/click5.mp3 +0 -0
  42. package/sounds/click6.mp3 +0 -0
  43. package/sounds/click7.mp3 +0 -0
  44. package/sounds/error1.mp3 +0 -0
  45. package/sounds/error2.mp3 +0 -0
  46. package/sounds/error3.mp3 +0 -0
  47. package/sounds/error4.mp3 +0 -0
  48. package/sounds/fanfare1.mp3 +0 -0
  49. package/sounds/fanfare2.mp3 +0 -0
  50. package/sounds/jingle1.mp3 +0 -0
  51. package/sounds/jingle2.mp3 +0 -0
  52. package/sounds/jingle3.mp3 +0 -0
  53. package/sounds/laugh1.mp3 +0 -0
  54. package/sounds/laugh2.mp3 +0 -0
  55. package/sounds/punch1.mp3 +0 -0
  56. package/sounds/punch2.mp3 +0 -0
  57. package/sounds/punch3.mp3 +0 -0
  58. package/sounds/resonance1.mp3 +0 -0
  59. package/sounds/resonance2.mp3 +0 -0
  60. package/sounds/resonance3.mp3 +0 -0
  61. package/sounds/resonance4.mp3 +0 -0
  62. package/sounds/scale1.mp3 +0 -0
  63. package/sounds/scale2.mp3 +0 -0
  64. package/sounds/scale3.mp3 +0 -0
  65. package/sounds/slide1.mp3 +0 -0
  66. package/sounds/slide2.mp3 +0 -0
  67. package/sounds/slide3.mp3 +0 -0
  68. package/sounds/slide4.mp3 +0 -0
  69. package/sounds/slide5.mp3 +0 -0
  70. package/sounds/slide6.mp3 +0 -0
  71. package/sounds/splash1.mp3 +0 -0
  72. package/sounds/splash2.mp3 +0 -0
  73. package/sounds/throw1.mp3 +0 -0
  74. package/sounds/throw2.mp3 +0 -0
  75. package/sounds/throw3.mp3 +0 -0
  76. package/sounds/throw4.mp3 +0 -0
  77. package/sounds/whoosh1.mp3 +0 -0
  78. package/sounds/whoosh2.mp3 +0 -0
  79. package/sounds/whoosh3.mp3 +0 -0
  80. package/sounds/whoosh4.mp3 +0 -0
  81. package/sounds/whoosh5.mp3 +0 -0
  82. package/tests/fixtures/index.html +2 -2
  83. package/tests/interface-spec.js +31 -1
  84. package/tests/scenes-spec.js +55 -0
  85. package/tests/sprites-spec.js +29 -29
  86. package/images/pong/sprites/ball.png +0 -0
  87. /package/images/pong/{sprites/paddle.png → paddle.png} +0 -0
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": "0.2.0",
3
+ "configurations": [
4
+ {
5
+ "name": "Start dev server",
6
+ "type": "node",
7
+ "request": "launch",
8
+ "runtimeExecutable": "npx",
9
+ "runtimeArgs": ["serve", ".", "-l", "4000"],
10
+ "cwd": "${workspaceFolder}",
11
+ "console": "integratedTerminal",
12
+ "skipFiles": ["<node_internals>/**"]
13
+ }
14
+ ]
15
+ }
package/README.MD CHANGED
@@ -14,7 +14,7 @@ A simple, dependency-free 2D game engine for the browser. No build step, no fram
14
14
  - [Install](#install)
15
15
  - [Run the Example](#run-the-example)
16
16
  - [How it Works](#how-it-works)
17
- - [punter.init()](#punter-init)
17
+ - [Setup](#setup)
18
18
  - [Scenes](#scenes)
19
19
  - [Events](#events)
20
20
  - [Sprites](#sprites)
@@ -47,36 +47,35 @@ Download `punter.js` and include it with a script tag before your game code:
47
47
 
48
48
  ## Run the Example
49
49
 
50
- The repo includes a Pong game in `games/pong.html`. Open it with a local web server - browsers block local file loading without one.
50
+ **VS Code (recommended):**
51
+ 1. Open this folder in VS Code
52
+ 2. Run → Start Debugging
53
+ 3. Open http://localhost:4000/games/pong.html
51
54
 
52
- ```bash
53
- npm start
54
- ```
55
-
56
- Then open **http://localhost:3000/games/pong.html** in your browser.
57
-
58
- > `npm start` uses `npx serve` - no install step needed beyond having Node.js.
55
+ **Terminal:**
56
+ 1. Run `npm start`
57
+ 2. Open http://localhost:4000/games/pong.html
59
58
 
60
59
  ## How it Works
61
60
 
62
61
  Every punter.js game follows the same three-step pattern:
63
62
 
64
- 1. **init** - specify which canvas to use and which images/sounds to preload
63
+ 1. **setup** - specify which canvas to use and which images/sounds to preload
65
64
  2. **scene** - define a named scene (level, menu, etc.) with update/draw logic
66
- 3. **ready** - switch to a scene once everything has loaded
65
+ 3. **go** - call `punter.go()` to start
67
66
 
68
67
  ```js
69
68
  // 1. Load assets
70
- punter.init({
69
+ punter.setup({
71
70
  canvas: '#game',
72
- sprites: { player: 'images/player.png' },
71
+ images: { player: 'images/player.png' },
73
72
  sounds: { jump: 'sounds/jump.mp3' }
74
73
  });
75
74
 
76
75
  // 2. Define a scene
77
76
  punter.scene('level1', function () {
78
77
 
79
- var player = punter.createSprite({ id: 'player', key: 'player', x: '50%', y: '50%' });
78
+ var player = punter.createSprite({ id: 'player', image: 'player', x: '50%', y: '50%' });
80
79
 
81
80
  punter.on('update', function () {
82
81
  if (punter.keys['ArrowRight']) player.moveX(3);
@@ -84,27 +83,21 @@ punter.scene('level1', function () {
84
83
  if (punter.keys['ArrowUp']) player.moveY(-3);
85
84
  if (punter.keys['ArrowDown']) player.moveY(3);
86
85
  });
87
-
88
- punter.on('draw', function () {
89
- player.draw(this); // 'this' is the canvas 2D context
90
- });
91
86
  });
92
87
 
93
- // 3. Start when ready
94
- punter.on('ready', function () {
95
- punter.go('level1');
96
- });
88
+ // 3. Go! (waits for assets to finish loading automatically)
89
+ punter.go('level1');
97
90
  ```
98
91
 
99
- ## `punter.init()`
92
+ ## Setup
100
93
 
101
94
  Call this once at the start. The `ready` event fires once all assets have loaded.
102
95
 
103
96
  ```js
104
- punter.init({
97
+ punter.setup({
105
98
  canvas: '#game', // CSS selector or HTMLCanvasElement
106
99
  debug: false, // true = show FPS and bounding boxes
107
- sprites: {
100
+ images: {
108
101
  player: 'images/player.png',
109
102
  coin: 'images/coin.png'
110
103
  },
@@ -117,13 +110,13 @@ punter.init({
117
110
  Option | Required | Description
118
111
  ----------|----------|------------------------------------------------
119
112
  `canvas` | Yes | CSS selector (`'#game'`) or `HTMLCanvasElement`
120
- `sprites` | No | Key/path pairs of images to preload
113
+ `images` | No | Key/path pairs of images to preload
121
114
  `sounds` | No | Key/path pairs of audio files to preload
122
115
  `debug` | No | Enables the debug overlay. Default: `false`
123
116
 
124
117
  ## Scenes
125
118
 
126
- Scenes are the different screens in your game - a menu, level 1, game over. Define each with `punter.scene()`, then switch between them with `punter.go()`. Switching scenes automatically clears the previous update/draw handlers.
119
+ Scenes are the different screens in your game - a menu, level 1, game over. Define each with `punter.scene()`, then switch between them with `punter.go()`. Switching scenes automatically clears the previous update/draw handlers and destroys all sprites from the previous scene.
127
120
 
128
121
  ```js
129
122
  punter.scene('menu', function () {
@@ -143,8 +136,8 @@ Register handlers with `punter.on()`. Each event can only have one handler at a
143
136
 
144
137
  ```js
145
138
  punter.on('ready', function () {
146
- // fires once - all assets loaded, safe to create sprites
147
- punter.go('level1');
139
+ // optional - fires once when all assets have loaded
140
+ // not needed for starting a scene; use punter.go() directly instead
148
141
  });
149
142
 
150
143
  punter.on('update', function () {
@@ -152,8 +145,10 @@ punter.on('update', function () {
152
145
  });
153
146
 
154
147
  punter.on('draw', function () {
155
- // fires every frame - 'this' is the canvas 2D context
156
- player.draw(this);
148
+ // optional - 'this' is the canvas 2D context
149
+ // sprites are drawn automatically before this runs, so use it for
150
+ // overlays: score text, HUD, shapes drawn on top of sprites
151
+ // backgrounds should be set via CSS on the canvas element
157
152
  });
158
153
 
159
154
  punter.on('resize', function () {
@@ -163,23 +158,23 @@ punter.on('resize', function () {
163
158
 
164
159
  ## Sprites
165
160
 
166
- A sprite is any image in your game - a character, enemy, coin, background tile. Create one with `punter.createSprite()`:
161
+ A sprite is any image in your game - a character, enemy, coin, background tile. Create one with `punter.createSprite(options)`:
167
162
 
168
163
  ```js
169
164
  var player = punter.createSprite({
170
- id: 'player', // unique name - no two sprites can share one
171
- key: 'player', // matches a key from config.sprites
172
- x: '50%', // position - number (pixels) or percent string
173
- y: '80%'
165
+ id: 'player', // unique name - no two sprites can share one
166
+ image: 'player', // name from config.images
167
+ x: '50%', // position - number (pixels) or percent string
168
+ y: '80%'
174
169
  });
175
170
  ```
176
171
 
177
- ### Creation Options
172
+ ### Options
178
173
 
179
174
  Option | Default | Description
180
175
  -----------------|------------|----------------------------------------------------------------------------------------------------
181
176
  `id` | - | **Required.** Unique name for this sprite.
182
- `key` | - | **Required.** Image key from `config.sprites`. Pass an array (`['frame1','frame2']`) for animation.
177
+ `image` | - | **Required.** Image name from `config.images`. Pass an array (`['frame1','frame2']`) for animation.
183
178
  `x` | - | **Required.** Horizontal position in pixels or `'50%'`.
184
179
  `y` | - | **Required.** Vertical position in pixels or `'50%'`.
185
180
  `w` | auto | Width in pixels or percent. Inferred from the image if omitted.
@@ -191,7 +186,7 @@ Option | Default | Description
191
186
  `clipHeight` | `null` | Limit the drawn height in pixels - useful for health/fill bars.
192
187
  `clipFrom` | `'bottom'` | Which end to clip from: `'top'` or `'bottom'`.
193
188
 
194
- ### Sprite Properties
189
+ ### Properties
195
190
 
196
191
  Property | Description
197
192
  ------------|-----------------------------------------------------------------------------------------
@@ -202,10 +197,10 @@ Property | Description
202
197
  `frame` | Override the current animation frame index. Set to `null` to let `animate()` control it.
203
198
  `destroyed` | `true` after `destroy()` has been called. Read-only.
204
199
 
205
- ### Sprite Methods
200
+ ### Methods
206
201
 
207
202
  #### `sprite.draw(ctx)`
208
- Draws the sprite. Call inside your `draw` handler - `ctx` is `this`.
203
+ Draws the sprite manually. Only needed when using a custom `draw` handler - for example, to control layer order alongside backgrounds or text.
209
204
 
210
205
  ```js
211
206
  punter.on('draw', function () {
@@ -235,12 +230,12 @@ player.center(0, -50); // centred, 50px above the middle
235
230
  Use `sprite.centerX()` or `sprite.centerY()` to centre on one axis only.
236
231
 
237
232
  #### `sprite.animate(delayMs)`
238
- Advance to the next animation frame. Requires `key` to be an array of image keys.
233
+ Advance to the next animation frame. Requires `image` to be an array of image names.
239
234
 
240
235
  ```js
241
236
  var run = punter.createSprite({
242
- id: 'runner',
243
- key: ['run1', 'run2', 'run3'],
237
+ id: 'runner',
238
+ image: ['run1', 'run2', 'run3'],
244
239
  x: 100, y: 200
245
240
  });
246
241
 
@@ -294,7 +289,7 @@ punter.on('update', function () {
294
289
  ## Sound
295
290
 
296
291
  ```js
297
- punter.init({
292
+ punter.setup({
298
293
  sounds: { jump: 'sounds/jump.mp3', music: 'sounds/theme.mp3' }
299
294
  });
300
295
 
@@ -419,7 +414,7 @@ html:not([data-punter-loading]) #loader { display: none; }
419
414
  ## Debug Mode
420
415
 
421
416
  ```js
422
- punter.init({ canvas: '#game', debug: true }); // enable at startup
417
+ punter.setup({ canvas: '#game', debug: true }); // enable at startup
423
418
  punter.debug = true; // or toggle at runtime
424
419
  ```
425
420
 
@@ -0,0 +1,297 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>Breakout</title>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ html, body {
15
+ width: 100%;
16
+ height: 100%;
17
+ background: #1e2230;
18
+ overflow: hidden;
19
+ }
20
+
21
+ canvas {
22
+ display: block;
23
+ cursor: none;
24
+ background: #1b2125;
25
+ }
26
+ </style>
27
+ <script src="../punter.js"></script>
28
+ </head>
29
+ <body>
30
+ <canvas id="game"></canvas>
31
+ <script>
32
+
33
+ // =========================================================
34
+ // COLORS
35
+ // =========================================================
36
+ var COLOR_TEXT = '#f5c518';
37
+ var COLOR_DIM = '#2c3033';
38
+
39
+ // One image name per brick row (matches the images/ folder)
40
+ var BRICK_IMAGES = ['brickRed', 'brickOrange', 'brickYellow', 'brickGreen', 'brickBlue'];
41
+
42
+ // =========================================================
43
+ // HELPERS
44
+ // =========================================================
45
+ var DPR = punter.dpr;
46
+
47
+ function px(n) {
48
+ return n * DPR;
49
+ }
50
+
51
+ function clamp(value, min, max) {
52
+ return Math.max(min, Math.min(value, max));
53
+ }
54
+
55
+ // =========================================================
56
+ // GAME SETTINGS
57
+ // =========================================================
58
+ var PADDLE_W = px(90);
59
+ var PADDLE_H = px(14);
60
+ var BALL_SIZE = px(16);
61
+ var BALL_SPEED = px(6);
62
+ var PADDLE_SPEED = px(8);
63
+ var BRICK_ROWS = 5;
64
+ var BRICK_COLS = 8;
65
+ var BRICK_H = px(22);
66
+ var BRICK_PAD = px(6);
67
+ var PADDLE_OFFSET = px(40); // distance from bottom
68
+
69
+ // =========================================================
70
+ // GAME STATE
71
+ // =========================================================
72
+ var paddleSprite, ballSprite, bricks, ballVel;
73
+ var score, lives, gameOver, won;
74
+
75
+ // =========================================================
76
+ // SETUP
77
+ // =========================================================
78
+ punter.setup({
79
+ canvas: '#game',
80
+ images: {
81
+ ball: '../images/breakout/ball.png',
82
+ paddle: '../images/breakout/paddle.png',
83
+ brickRed: '../images/breakout/brick-red.png',
84
+ brickOrange: '../images/breakout/brick-orange.png',
85
+ brickYellow: '../images/breakout/brick-yellow.png',
86
+ brickGreen: '../images/breakout/brick-green.png',
87
+ brickBlue: '../images/breakout/brick-blue.png'
88
+ },
89
+ sounds: {
90
+ bounce: '../sounds/beep6.mp3',
91
+ brick: '../sounds/click1.mp3',
92
+ die: '../sounds/error2.mp3',
93
+ win: '../sounds/fanfare1.mp3'
94
+ }
95
+ });
96
+
97
+ // =========================================================
98
+ // SCENE: play
99
+ // =========================================================
100
+ punter.scene('play', function () {
101
+
102
+ var w = punter.width;
103
+ var h = punter.height;
104
+
105
+ // Create the paddle and ball sprites
106
+ paddleSprite = punter.createSprite({ id: 'paddle', image: 'paddle', x: w / 2 - PADDLE_W / 2, y: h - PADDLE_OFFSET, w: PADDLE_W, h: PADDLE_H });
107
+ ballSprite = punter.createSprite({ id: 'ball', image: 'ball', x: w / 2, y: h / 2, w: BALL_SIZE, h: BALL_SIZE });
108
+
109
+ // Set up game state for the first time
110
+ bricks = [];
111
+ ballVel = { x: 0, y: 0 };
112
+ score = 0;
113
+ lives = 3;
114
+ gameOver = false;
115
+ won = false;
116
+
117
+ buildBricks();
118
+ resetBall();
119
+
120
+ // -------------------------------------------------------
121
+ // UPDATE — game logic, runs ~60 times per second
122
+ // -------------------------------------------------------
123
+ punter.on('update', function () {
124
+
125
+ // Restart on any input when the game has ended
126
+ if (gameOver || won) {
127
+ if (punter.keys[' '] || punter.mouse.clicked) {
128
+ punter.clearInput();
129
+ resetGame();
130
+ }
131
+ return;
132
+ }
133
+
134
+ var w = punter.width;
135
+ var h = punter.height;
136
+
137
+ // Keep paddle pinned to the correct Y on resize
138
+ paddleSprite.y = h - PADDLE_OFFSET;
139
+
140
+ // Paddle: keyboard
141
+ if (punter.keys['ArrowLeft'] || punter.keys['a']) paddleSprite.moveX(-PADDLE_SPEED);
142
+ if (punter.keys['ArrowRight'] || punter.keys['d']) paddleSprite.moveX(PADDLE_SPEED);
143
+
144
+ // Paddle: tap / click
145
+ if (punter.mouse.clicked) {
146
+ paddleSprite.x = punter.mouse.x - PADDLE_W / 2;
147
+ }
148
+
149
+ paddleSprite.x = clamp(paddleSprite.x, 0, w - PADDLE_W);
150
+
151
+ // Move ball
152
+ ballSprite.moveX(ballVel.x);
153
+ ballSprite.moveY(ballVel.y);
154
+
155
+ // Bounce off side walls and ceiling
156
+ if (ballSprite.x <= 0) { ballSprite.x = 0; ballVel.x = Math.abs(ballVel.x); }
157
+ if (ballSprite.x + BALL_SIZE >= w) { ballSprite.x = w - BALL_SIZE; ballVel.x = -Math.abs(ballVel.x); }
158
+ if (ballSprite.y <= 0) { ballSprite.y = 0; ballVel.y = Math.abs(ballVel.y); }
159
+
160
+ // Ball falls off the bottom — lose a life
161
+ if (ballSprite.y > h) {
162
+ lives--;
163
+ punter.playSound('die');
164
+ if (lives <= 0) { gameOver = true; punter.clearInput(); return; }
165
+ resetBall();
166
+ return;
167
+ }
168
+
169
+ // Ball vs paddle
170
+ if (ballVel.y > 0 && ballSprite.isCollidingWith(paddleSprite)) {
171
+ var hit = (ballSprite.x + BALL_SIZE / 2 - paddleSprite.x) / PADDLE_W;
172
+ ballVel.x = (hit - 0.5) * BALL_SPEED * 2.5;
173
+ ballVel.y = -Math.abs(ballVel.y);
174
+ punter.playSound('bounce');
175
+ }
176
+
177
+ // Ball vs bricks
178
+ for (var i = 0; i < bricks.length; i++) {
179
+ if (ballSprite.isCollidingWith(bricks[i])) {
180
+ bricks[i].destroy();
181
+ bricks.splice(i, 1);
182
+ ballVel.y = -ballVel.y;
183
+ score += 10;
184
+ punter.playSound('brick');
185
+ break; // only hit one brick per frame
186
+ }
187
+ }
188
+
189
+ if (bricks.length === 0) {
190
+ won = true;
191
+ punter.playSound('win');
192
+ punter.clearInput();
193
+ }
194
+ });
195
+
196
+ // -------------------------------------------------------
197
+ // DRAW — runs after sprites are drawn; used for HUD and overlays
198
+ // -------------------------------------------------------
199
+ punter.on('draw', function () {
200
+ var ctx = this; // punter passes the canvas 2D context as 'this'
201
+ var w = punter.width;
202
+ var h = punter.height;
203
+
204
+ // HUD: score and lives
205
+ ctx.fillStyle = COLOR_TEXT;
206
+ ctx.font = 'bold ' + px(18) + 'px monospace';
207
+ ctx.textBaseline = 'top';
208
+ ctx.textAlign = 'left';
209
+ ctx.fillText('Score: ' + score, px(12), px(12));
210
+ ctx.textAlign = 'right';
211
+ ctx.fillText('Lives: ' + lives, w - px(12), px(12));
212
+
213
+ // Controls hint
214
+ ctx.fillStyle = COLOR_DIM;
215
+ ctx.font = px(14) + 'px monospace';
216
+ ctx.textBaseline = 'bottom';
217
+ ctx.textAlign = 'center';
218
+ ctx.fillText('Arrow keys or A / D · tap to move', w / 2, h - px(12));
219
+
220
+ // Game over / win overlay
221
+ if (gameOver || won) {
222
+ ctx.fillStyle = 'rgba(0,0,0,0.55)';
223
+ ctx.fillRect(0, 0, w, h);
224
+ ctx.fillStyle = COLOR_TEXT;
225
+ ctx.font = 'bold ' + px(40) + 'px monospace';
226
+ ctx.textAlign = 'center';
227
+ ctx.textBaseline = 'middle';
228
+ ctx.fillText(won ? 'YOU WIN!' : 'GAME OVER', w / 2, h / 2 - px(24));
229
+ ctx.font = px(18) + 'px monospace';
230
+ ctx.fillText('Press SPACE or tap to restart', w / 2, h / 2 + px(24));
231
+ }
232
+ });
233
+ });
234
+
235
+ /**
236
+ * Creates a fresh grid of brick sprites.
237
+ * Each brick is a sprite the engine draws automatically.
238
+ */
239
+ function buildBricks() {
240
+ var w = punter.width;
241
+ var brickW = (w - BRICK_PAD * (BRICK_COLS + 1)) / BRICK_COLS;
242
+
243
+ for (var r = 0; r < BRICK_ROWS; r++) {
244
+ for (var c = 0; c < BRICK_COLS; c++) {
245
+ bricks.push(punter.createSprite({
246
+ id: 'brick-' + r + '-' + c,
247
+ image: BRICK_IMAGES[r],
248
+ x: BRICK_PAD + c * (brickW + BRICK_PAD),
249
+ y: px(50) + r * (BRICK_H + BRICK_PAD),
250
+ w: brickW,
251
+ h: BRICK_H
252
+ }));
253
+ }
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Puts the ball back in the centre aimed upward.
259
+ */
260
+ function resetBall() {
261
+ ballSprite.x = punter.width / 2 - BALL_SIZE / 2;
262
+ ballSprite.y = punter.height / 2;
263
+ ballVel.x = (Math.random() < 0.5 ? -1 : 1) * BALL_SPEED * 0.6;
264
+ ballVel.y = -BALL_SPEED;
265
+ }
266
+
267
+ /**
268
+ * Destroys any remaining bricks and resets everything for a new game.
269
+ */
270
+ function resetGame() {
271
+ for (var i = 0; i < bricks.length; i++) { bricks[i].destroy(); }
272
+ bricks = [];
273
+
274
+ score = 0;
275
+ lives = 3;
276
+ gameOver = false;
277
+ won = false;
278
+
279
+ paddleSprite.x = punter.width / 2 - PADDLE_W / 2;
280
+ paddleSprite.y = punter.height - PADDLE_OFFSET;
281
+
282
+ buildBricks();
283
+ resetBall();
284
+ }
285
+
286
+ // Reposition paddle on resize
287
+ punter.on('resize', function () {
288
+ if (!paddleSprite) return;
289
+ paddleSprite.x = clamp(paddleSprite.x, 0, punter.width - PADDLE_W);
290
+ paddleSprite.y = punter.height - PADDLE_OFFSET;
291
+ });
292
+
293
+ punter.go('play');
294
+
295
+ </script>
296
+ </body>
297
+ </html>