punter.js 1.1.0 → 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 (86) hide show
  1. package/.vscode/launch.json +15 -0
  2. package/README.MD +31 -36
  3. package/games/breakout.html +297 -0
  4. package/games/pong.html +40 -33
  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 +1 -1
  14. package/punter.js +38 -19
  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/interface-spec.js +30 -0
  83. package/tests/scenes-spec.js +55 -0
  84. package/tests/sprites-spec.js +29 -29
  85. package/images/pong/sprites/ball.png +0 -0
  86. /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
@@ -47,15 +47,14 @@ 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
 
@@ -63,7 +62,7 @@ Every punter.js game follows the same three-step pattern:
63
62
 
64
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
@@ -76,7 +75,7 @@ punter.setup({
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,16 +83,10 @@ 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
92
  ## Setup
@@ -123,7 +116,7 @@ Option | Required | Description
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.images`. 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
 
@@ -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>
package/games/pong.html CHANGED
@@ -20,6 +20,8 @@
20
20
 
21
21
  canvas {
22
22
  display: block;
23
+ cursor: none;
24
+ background: #1b2125;
23
25
  }
24
26
  </style>
25
27
  <script src="../punter.js"></script>
@@ -67,10 +69,11 @@
67
69
  // =========================================================
68
70
  // GAME SETTINGS
69
71
  // =========================================================
70
- var MARGIN = px(40); // gap between paddle and screen edge
71
- var PADDLE_W = px(14);
72
- var PADDLE_H = px(90);
73
- var BALL_SIZE = px(14);
72
+ var MARGIN = px(20); // gap between paddle and screen edge
73
+ var PADDLE_W = px(14); // width of paddle
74
+ var PADDLE_H = px(90); // height of paddle
75
+ var AI_PADDLE_H = px(90);
76
+ var BALL_SIZE = px(24);
74
77
  var PADDLE_SPEED = px(7);
75
78
  var AI_SPEED = px(5);
76
79
  var BALL_SPEED = px(5);
@@ -96,8 +99,12 @@
96
99
  punter.setup({
97
100
  canvas: '#game',
98
101
  images: {
99
- paddle: '../images/pong/sprites/paddle.png',
100
- ball: '../images/pong/sprites/ball.png'
102
+ paddle: '../images/pong/paddle.png',
103
+ ball: '../images/pong/ball.png'
104
+ },
105
+ sounds: {
106
+ bounce: '../sounds/beep6.mp3',
107
+ die: '../sounds/error2.mp3'
101
108
  }
102
109
  });
103
110
 
@@ -106,11 +113,13 @@
106
113
  // =========================================================
107
114
  punter.scene('play', function () {
108
115
 
116
+ score = { player: 0, ai: 0 };
117
+
109
118
  // Create the three sprites punter.js will draw each frame.
110
119
  // w/h set the draw size; the image is scaled to fit automatically.
111
- playerSprite = punter.createSprite({ id: 'player', key: 'paddle', x: 0, y: 0, w: PADDLE_W, h: PADDLE_H });
112
- aiSprite = punter.createSprite({ id: 'ai', key: 'paddle', x: 0, y: 0, w: PADDLE_W, h: PADDLE_H });
113
- ballSprite = punter.createSprite({ id: 'ball', key: 'ball', x: 0, y: 0, w: BALL_SIZE, h: BALL_SIZE });
120
+ playerSprite = punter.createSprite({ id: 'player', image: 'paddle', x: 0, y: 0, w: PADDLE_W, h: PADDLE_H });
121
+ aiSprite = punter.createSprite({ id: 'ai', image: 'paddle', x: 0, y: 0, w: PADDLE_W, h: AI_PADDLE_H });
122
+ ballSprite = punter.createSprite({ id: 'ball', image: 'ball', x: 0, y: 0, w: BALL_SIZE, h: BALL_SIZE });
114
123
 
115
124
  // Place everything in the correct starting position
116
125
  resetPositions();
@@ -126,8 +135,8 @@
126
135
  aiSprite.x = w - MARGIN - PADDLE_W;
127
136
 
128
137
  // --- Player: keyboard ---
129
- if (punter.keys['ArrowUp'] || punter.keys['w']) playerSprite.y -= PADDLE_SPEED;
130
- if (punter.keys['ArrowDown'] || punter.keys['s']) playerSprite.y += PADDLE_SPEED;
138
+ if (punter.keys['ArrowUp'] || punter.keys['w']) playerSprite.moveY(-PADDLE_SPEED);
139
+ if (punter.keys['ArrowDown'] || punter.keys['s']) playerSprite.moveY(PADDLE_SPEED);
131
140
 
132
141
  // --- Player: tap / click ---
133
142
  if (punter.mouse.clicked) {
@@ -139,13 +148,13 @@
139
148
 
140
149
  // --- AI: follow the ball ---
141
150
  var aiCenterY = aiSprite.y + PADDLE_H / 2;
142
- if (ballSprite.y < aiCenterY - 10) aiSprite.y -= AI_SPEED;
143
- if (ballSprite.y > aiCenterY + 10) aiSprite.y += AI_SPEED;
151
+ if (ballSprite.y < aiCenterY - 10) aiSprite.moveY(-AI_SPEED);
152
+ if (ballSprite.y > aiCenterY + 10) aiSprite.moveY(AI_SPEED);
144
153
  aiSprite.y = clamp(aiSprite.y, 0, h - PADDLE_H);
145
154
 
146
155
  // --- Move the ball ---
147
- ballSprite.x += ballVel.x;
148
- ballSprite.y += ballVel.y;
156
+ ballSprite.moveX(ballVel.x);
157
+ ballSprite.moveY(ballVel.y);
149
158
 
150
159
  // Bounce off top and bottom walls
151
160
  if (ballSprite.y <= 0) { ballVel.y = Math.abs(ballVel.y); ballSprite.y = 0; }
@@ -159,6 +168,8 @@
159
168
  // Where on the paddle the ball hit: 0 = top edge, 1 = bottom edge, 0.5 = centre (flat return)
160
169
  var playerHit = (ballSprite.y + BALL_SIZE / 2 - playerSprite.y) / PADDLE_H;
161
170
  ballVel.y = (playerHit - 0.5) * BALL_SPEED * 2;
171
+
172
+ punter.playSound('bounce');
162
173
  }
163
174
 
164
175
  // --- Collision: ball hits AI paddle ---
@@ -169,6 +180,8 @@
169
180
  // Where on the paddle the ball hit: 0 = top edge, 1 = bottom edge, 0.5 = centre (flat return)
170
181
  var aiHit = (ballSprite.y + BALL_SIZE / 2 - aiSprite.y) / PADDLE_H;
171
182
  ballVel.y = (aiHit - 0.5) * BALL_SPEED * 2;
183
+
184
+ punter.playSound('bounce');
172
185
  }
173
186
 
174
187
  // Cap ball speed so the game stays playable
@@ -176,8 +189,17 @@
176
189
  if (ballVel.x < -MAX_BALL_SPEED) ballVel.x = -MAX_BALL_SPEED;
177
190
 
178
191
  // --- Scoring ---
179
- if (ballSprite.x + BALL_SIZE < 0) { score.ai++; resetBall(SERVE_RIGHT); }
180
- if (ballSprite.x > w) { score.player++; resetBall(SERVE_LEFT); }
192
+ if (ballSprite.x + BALL_SIZE < 0) {
193
+ score.ai++;
194
+ punter.playSound('die');
195
+ resetBall(SERVE_RIGHT);
196
+ }
197
+
198
+ if (ballSprite.x > w) {
199
+ score.player++;
200
+ punter.playSound('die');
201
+ resetBall(SERVE_LEFT);
202
+ }
181
203
  });
182
204
 
183
205
  // -------------------------------------------------------
@@ -188,10 +210,6 @@
188
210
  var w = punter.width;
189
211
  var h = punter.height;
190
212
 
191
- // Background
192
- ctx.fillStyle = COLOR_BG;
193
- ctx.fillRect(0, 0, w, h);
194
-
195
213
  // Dashed centre line
196
214
  ctx.setLineDash([px(8), px(10)]);
197
215
  ctx.strokeStyle = COLOR_NET;
@@ -202,11 +220,6 @@
202
220
  ctx.stroke();
203
221
  ctx.setLineDash([]);
204
222
 
205
- // Sprites — punter.js scales and draws the PNG images
206
- playerSprite.draw(ctx);
207
- aiSprite.draw(ctx);
208
- ballSprite.draw(ctx);
209
-
210
223
  // Scores
211
224
  ctx.fillStyle = COLOR_SCORE;
212
225
  ctx.font = 'bold ' + px(48) + 'px monospace';
@@ -260,13 +273,7 @@
260
273
  resetPositions();
261
274
  });
262
275
 
263
- // =========================================================
264
- // START — go to the play scene once everything is loaded
265
- // =========================================================
266
- punter.on('ready', function () {
267
- score = { player: 0, ai: 0 };
268
- punter.go('play');
269
- });
276
+ punter.go('play');
270
277
 
271
278
  </script>
272
279
  </body>
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "punter.js",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "A simple, dependency-free 2D game engine for the browser. No build step, no frameworks - just one script tag.",
5
5
  "main": "punter.js",
6
6
  "directories": {