punter.js 1.1.0 → 1.2.1

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 (89) hide show
  1. package/.github/workflows/ci.yml +25 -0
  2. package/.nvmrc +1 -1
  3. package/.vscode/launch.json +15 -0
  4. package/README.MD +35 -36
  5. package/games/breakout.html +297 -0
  6. package/games/pong.html +40 -33
  7. package/images/breakout/ball.png +0 -0
  8. package/images/breakout/brick-blue.png +0 -0
  9. package/images/breakout/brick-green.png +0 -0
  10. package/images/breakout/brick-orange.png +0 -0
  11. package/images/breakout/brick-red.png +0 -0
  12. package/images/breakout/brick-yellow.png +0 -0
  13. package/images/breakout/paddle.png +0 -0
  14. package/images/pong/ball.png +0 -0
  15. package/package.json +2 -2
  16. package/punter.js +38 -19
  17. package/sounds/alarm1.mp3 +0 -0
  18. package/sounds/alarm2.mp3 +0 -0
  19. package/sounds/alarm3.mp3 +0 -0
  20. package/sounds/alarm4.mp3 +0 -0
  21. package/sounds/beep1.mp3 +0 -0
  22. package/sounds/beep2.mp3 +0 -0
  23. package/sounds/beep3.mp3 +0 -0
  24. package/sounds/beep4.mp3 +0 -0
  25. package/sounds/beep5.mp3 +0 -0
  26. package/sounds/beep6.mp3 +0 -0
  27. package/sounds/bling1.mp3 +0 -0
  28. package/sounds/bling2.mp3 +0 -0
  29. package/sounds/bling3.mp3 +0 -0
  30. package/sounds/bling4.mp3 +0 -0
  31. package/sounds/bling5.mp3 +0 -0
  32. package/sounds/cannon1.mp3 +0 -0
  33. package/sounds/cannon2.mp3 +0 -0
  34. package/sounds/chime1.mp3 +0 -0
  35. package/sounds/chime2.mp3 +0 -0
  36. package/sounds/chime3.mp3 +0 -0
  37. package/sounds/chime4.mp3 +0 -0
  38. package/sounds/chime5.mp3 +0 -0
  39. package/sounds/click1.mp3 +0 -0
  40. package/sounds/click2.mp3 +0 -0
  41. package/sounds/click3.mp3 +0 -0
  42. package/sounds/click4.mp3 +0 -0
  43. package/sounds/click5.mp3 +0 -0
  44. package/sounds/click6.mp3 +0 -0
  45. package/sounds/click7.mp3 +0 -0
  46. package/sounds/error1.mp3 +0 -0
  47. package/sounds/error2.mp3 +0 -0
  48. package/sounds/error3.mp3 +0 -0
  49. package/sounds/error4.mp3 +0 -0
  50. package/sounds/fanfare1.mp3 +0 -0
  51. package/sounds/fanfare2.mp3 +0 -0
  52. package/sounds/jingle1.mp3 +0 -0
  53. package/sounds/jingle2.mp3 +0 -0
  54. package/sounds/jingle3.mp3 +0 -0
  55. package/sounds/laugh1.mp3 +0 -0
  56. package/sounds/laugh2.mp3 +0 -0
  57. package/sounds/punch1.mp3 +0 -0
  58. package/sounds/punch2.mp3 +0 -0
  59. package/sounds/punch3.mp3 +0 -0
  60. package/sounds/resonance1.mp3 +0 -0
  61. package/sounds/resonance2.mp3 +0 -0
  62. package/sounds/resonance3.mp3 +0 -0
  63. package/sounds/resonance4.mp3 +0 -0
  64. package/sounds/scale1.mp3 +0 -0
  65. package/sounds/scale2.mp3 +0 -0
  66. package/sounds/scale3.mp3 +0 -0
  67. package/sounds/slide1.mp3 +0 -0
  68. package/sounds/slide2.mp3 +0 -0
  69. package/sounds/slide3.mp3 +0 -0
  70. package/sounds/slide4.mp3 +0 -0
  71. package/sounds/slide5.mp3 +0 -0
  72. package/sounds/slide6.mp3 +0 -0
  73. package/sounds/splash1.mp3 +0 -0
  74. package/sounds/splash2.mp3 +0 -0
  75. package/sounds/throw1.mp3 +0 -0
  76. package/sounds/throw2.mp3 +0 -0
  77. package/sounds/throw3.mp3 +0 -0
  78. package/sounds/throw4.mp3 +0 -0
  79. package/sounds/whoosh1.mp3 +0 -0
  80. package/sounds/whoosh2.mp3 +0 -0
  81. package/sounds/whoosh3.mp3 +0 -0
  82. package/sounds/whoosh4.mp3 +0 -0
  83. package/sounds/whoosh5.mp3 +0 -0
  84. package/tests/interface-spec.js +30 -0
  85. package/tests/scenes-spec.js +55 -0
  86. package/tests/setup.js +1 -2
  87. package/tests/sprites-spec.js +29 -29
  88. package/images/pong/sprites/ball.png +0 -0
  89. /package/images/pong/{sprites/paddle.png → paddle.png} +0 -0
@@ -0,0 +1,25 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - uses: actions/setup-node@v4
17
+ with:
18
+ node-version: '18'
19
+ cache: npm
20
+
21
+ - name: Install dependencies
22
+ run: npm install
23
+
24
+ - name: Run tests
25
+ run: npm test
package/.nvmrc CHANGED
@@ -1 +1 @@
1
- 22
1
+ 18
@@ -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
@@ -1,5 +1,9 @@
1
1
  # punter.js
2
2
 
3
+ [![Tests](https://github.com/orca-scan/punter.js/actions/workflows/ci.yml/badge.svg)](https://github.com/orca-scan/punter.js/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/punter.js)](https://www.npmjs.com/package/punter.js)
5
+ [![license](https://img.shields.io/github/license/orca-scan/punter.js)](https://github.com/orca-scan/punter.js/blob/master/LICENSE)
6
+
3
7
  <p align="center"><img src="images/punterjs.png" width="290" height="174" /></p>
4
8
 
5
9
  A simple, dependency-free 2D game engine for the browser. No build step, no frameworks - just one script tag.
@@ -47,15 +51,14 @@ Download `punter.js` and include it with a script tag before your game code:
47
51
 
48
52
  ## Run the Example
49
53
 
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.
51
-
52
- ```bash
53
- npm start
54
- ```
55
-
56
- Then open **http://localhost:3000/games/pong.html** in your browser.
54
+ **VS Code (recommended):**
55
+ 1. Open this folder in VS Code
56
+ 2. Run → Start Debugging
57
+ 3. Open http://localhost:4000/games/pong.html
57
58
 
58
- > `npm start` uses `npx serve` - no install step needed beyond having Node.js.
59
+ **Terminal:**
60
+ 1. Run `npm start`
61
+ 2. Open http://localhost:4000/games/pong.html
59
62
 
60
63
  ## How it Works
61
64
 
@@ -63,7 +66,7 @@ Every punter.js game follows the same three-step pattern:
63
66
 
64
67
  1. **setup** - specify which canvas to use and which images/sounds to preload
65
68
  2. **scene** - define a named scene (level, menu, etc.) with update/draw logic
66
- 3. **ready** - switch to a scene once everything has loaded
69
+ 3. **go** - call `punter.go()` to start
67
70
 
68
71
  ```js
69
72
  // 1. Load assets
@@ -76,7 +79,7 @@ punter.setup({
76
79
  // 2. Define a scene
77
80
  punter.scene('level1', function () {
78
81
 
79
- var player = punter.createSprite({ id: 'player', key: 'player', x: '50%', y: '50%' });
82
+ var player = punter.createSprite({ id: 'player', image: 'player', x: '50%', y: '50%' });
80
83
 
81
84
  punter.on('update', function () {
82
85
  if (punter.keys['ArrowRight']) player.moveX(3);
@@ -84,16 +87,10 @@ punter.scene('level1', function () {
84
87
  if (punter.keys['ArrowUp']) player.moveY(-3);
85
88
  if (punter.keys['ArrowDown']) player.moveY(3);
86
89
  });
87
-
88
- punter.on('draw', function () {
89
- player.draw(this); // 'this' is the canvas 2D context
90
- });
91
90
  });
92
91
 
93
- // 3. Start when ready
94
- punter.on('ready', function () {
95
- punter.go('level1');
96
- });
92
+ // 3. Go! (waits for assets to finish loading automatically)
93
+ punter.go('level1');
97
94
  ```
98
95
 
99
96
  ## Setup
@@ -123,7 +120,7 @@ Option | Required | Description
123
120
 
124
121
  ## Scenes
125
122
 
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.
123
+ 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
124
 
128
125
  ```js
129
126
  punter.scene('menu', function () {
@@ -143,8 +140,8 @@ Register handlers with `punter.on()`. Each event can only have one handler at a
143
140
 
144
141
  ```js
145
142
  punter.on('ready', function () {
146
- // fires once - all assets loaded, safe to create sprites
147
- punter.go('level1');
143
+ // optional - fires once when all assets have loaded
144
+ // not needed for starting a scene; use punter.go() directly instead
148
145
  });
149
146
 
150
147
  punter.on('update', function () {
@@ -152,8 +149,10 @@ punter.on('update', function () {
152
149
  });
153
150
 
154
151
  punter.on('draw', function () {
155
- // fires every frame - 'this' is the canvas 2D context
156
- player.draw(this);
152
+ // optional - 'this' is the canvas 2D context
153
+ // sprites are drawn automatically before this runs, so use it for
154
+ // overlays: score text, HUD, shapes drawn on top of sprites
155
+ // backgrounds should be set via CSS on the canvas element
157
156
  });
158
157
 
159
158
  punter.on('resize', function () {
@@ -163,23 +162,23 @@ punter.on('resize', function () {
163
162
 
164
163
  ## Sprites
165
164
 
166
- A sprite is any image in your game - a character, enemy, coin, background tile. Create one with `punter.createSprite()`:
165
+ A sprite is any image in your game - a character, enemy, coin, background tile. Create one with `punter.createSprite(options)`:
167
166
 
168
167
  ```js
169
168
  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%'
169
+ id: 'player', // unique name - no two sprites can share one
170
+ image: 'player', // name from config.images
171
+ x: '50%', // position - number (pixels) or percent string
172
+ y: '80%'
174
173
  });
175
174
  ```
176
175
 
177
- ### Creation Options
176
+ ### Options
178
177
 
179
178
  Option | Default | Description
180
179
  -----------------|------------|----------------------------------------------------------------------------------------------------
181
180
  `id` | - | **Required.** Unique name for this sprite.
182
- `key` | - | **Required.** Image key from `config.images`. Pass an array (`['frame1','frame2']`) for animation.
181
+ `image` | - | **Required.** Image name from `config.images`. Pass an array (`['frame1','frame2']`) for animation.
183
182
  `x` | - | **Required.** Horizontal position in pixels or `'50%'`.
184
183
  `y` | - | **Required.** Vertical position in pixels or `'50%'`.
185
184
  `w` | auto | Width in pixels or percent. Inferred from the image if omitted.
@@ -191,7 +190,7 @@ Option | Default | Description
191
190
  `clipHeight` | `null` | Limit the drawn height in pixels - useful for health/fill bars.
192
191
  `clipFrom` | `'bottom'` | Which end to clip from: `'top'` or `'bottom'`.
193
192
 
194
- ### Sprite Properties
193
+ ### Properties
195
194
 
196
195
  Property | Description
197
196
  ------------|-----------------------------------------------------------------------------------------
@@ -202,10 +201,10 @@ Property | Description
202
201
  `frame` | Override the current animation frame index. Set to `null` to let `animate()` control it.
203
202
  `destroyed` | `true` after `destroy()` has been called. Read-only.
204
203
 
205
- ### Sprite Methods
204
+ ### Methods
206
205
 
207
206
  #### `sprite.draw(ctx)`
208
- Draws the sprite. Call inside your `draw` handler - `ctx` is `this`.
207
+ Draws the sprite manually. Only needed when using a custom `draw` handler - for example, to control layer order alongside backgrounds or text.
209
208
 
210
209
  ```js
211
210
  punter.on('draw', function () {
@@ -235,12 +234,12 @@ player.center(0, -50); // centred, 50px above the middle
235
234
  Use `sprite.centerX()` or `sprite.centerY()` to centre on one axis only.
236
235
 
237
236
  #### `sprite.animate(delayMs)`
238
- Advance to the next animation frame. Requires `key` to be an array of image keys.
237
+ Advance to the next animation frame. Requires `image` to be an array of image names.
239
238
 
240
239
  ```js
241
240
  var run = punter.createSprite({
242
- id: 'runner',
243
- key: ['run1', 'run2', 'run3'],
241
+ id: 'runner',
242
+ image: ['run1', 'run2', 'run3'],
244
243
  x: 100, y: 200
245
244
  });
246
245
 
@@ -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