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.
- package/.vscode/launch.json +15 -0
- package/README.MD +41 -46
- package/games/breakout.html +297 -0
- package/games/pong.html +59 -39
- package/images/breakout/ball.png +0 -0
- package/images/breakout/brick-blue.png +0 -0
- package/images/breakout/brick-green.png +0 -0
- package/images/breakout/brick-orange.png +0 -0
- package/images/breakout/brick-red.png +0 -0
- package/images/breakout/brick-yellow.png +0 -0
- package/images/breakout/paddle.png +0 -0
- package/images/pong/ball.png +0 -0
- package/package.json +3 -2
- package/punter.js +54 -35
- package/sounds/alarm1.mp3 +0 -0
- package/sounds/alarm2.mp3 +0 -0
- package/sounds/alarm3.mp3 +0 -0
- package/sounds/alarm4.mp3 +0 -0
- package/sounds/beep1.mp3 +0 -0
- package/sounds/beep2.mp3 +0 -0
- package/sounds/beep3.mp3 +0 -0
- package/sounds/beep4.mp3 +0 -0
- package/sounds/beep5.mp3 +0 -0
- package/sounds/beep6.mp3 +0 -0
- package/sounds/bling1.mp3 +0 -0
- package/sounds/bling2.mp3 +0 -0
- package/sounds/bling3.mp3 +0 -0
- package/sounds/bling4.mp3 +0 -0
- package/sounds/bling5.mp3 +0 -0
- package/sounds/cannon1.mp3 +0 -0
- package/sounds/cannon2.mp3 +0 -0
- package/sounds/chime1.mp3 +0 -0
- package/sounds/chime2.mp3 +0 -0
- package/sounds/chime3.mp3 +0 -0
- package/sounds/chime4.mp3 +0 -0
- package/sounds/chime5.mp3 +0 -0
- package/sounds/click1.mp3 +0 -0
- package/sounds/click2.mp3 +0 -0
- package/sounds/click3.mp3 +0 -0
- package/sounds/click4.mp3 +0 -0
- package/sounds/click5.mp3 +0 -0
- package/sounds/click6.mp3 +0 -0
- package/sounds/click7.mp3 +0 -0
- package/sounds/error1.mp3 +0 -0
- package/sounds/error2.mp3 +0 -0
- package/sounds/error3.mp3 +0 -0
- package/sounds/error4.mp3 +0 -0
- package/sounds/fanfare1.mp3 +0 -0
- package/sounds/fanfare2.mp3 +0 -0
- package/sounds/jingle1.mp3 +0 -0
- package/sounds/jingle2.mp3 +0 -0
- package/sounds/jingle3.mp3 +0 -0
- package/sounds/laugh1.mp3 +0 -0
- package/sounds/laugh2.mp3 +0 -0
- package/sounds/punch1.mp3 +0 -0
- package/sounds/punch2.mp3 +0 -0
- package/sounds/punch3.mp3 +0 -0
- package/sounds/resonance1.mp3 +0 -0
- package/sounds/resonance2.mp3 +0 -0
- package/sounds/resonance3.mp3 +0 -0
- package/sounds/resonance4.mp3 +0 -0
- package/sounds/scale1.mp3 +0 -0
- package/sounds/scale2.mp3 +0 -0
- package/sounds/scale3.mp3 +0 -0
- package/sounds/slide1.mp3 +0 -0
- package/sounds/slide2.mp3 +0 -0
- package/sounds/slide3.mp3 +0 -0
- package/sounds/slide4.mp3 +0 -0
- package/sounds/slide5.mp3 +0 -0
- package/sounds/slide6.mp3 +0 -0
- package/sounds/splash1.mp3 +0 -0
- package/sounds/splash2.mp3 +0 -0
- package/sounds/throw1.mp3 +0 -0
- package/sounds/throw2.mp3 +0 -0
- package/sounds/throw3.mp3 +0 -0
- package/sounds/throw4.mp3 +0 -0
- package/sounds/whoosh1.mp3 +0 -0
- package/sounds/whoosh2.mp3 +0 -0
- package/sounds/whoosh3.mp3 +0 -0
- package/sounds/whoosh4.mp3 +0 -0
- package/sounds/whoosh5.mp3 +0 -0
- package/tests/fixtures/index.html +2 -2
- package/tests/interface-spec.js +31 -1
- package/tests/scenes-spec.js +55 -0
- package/tests/sprites-spec.js +29 -29
- package/images/pong/sprites/ball.png +0 -0
- /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
|
-
- [
|
|
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
|
-
|
|
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
|
-
|
|
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. **
|
|
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. **
|
|
65
|
+
3. **go** - call `punter.go()` to start
|
|
67
66
|
|
|
68
67
|
```js
|
|
69
68
|
// 1. Load assets
|
|
70
|
-
punter.
|
|
69
|
+
punter.setup({
|
|
71
70
|
canvas: '#game',
|
|
72
|
-
|
|
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',
|
|
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.
|
|
94
|
-
punter.
|
|
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
|
|
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.
|
|
97
|
+
punter.setup({
|
|
105
98
|
canvas: '#game', // CSS selector or HTMLCanvasElement
|
|
106
99
|
debug: false, // true = show FPS and bounding boxes
|
|
107
|
-
|
|
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
|
-
`
|
|
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
|
|
147
|
-
punter.go(
|
|
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
|
-
//
|
|
156
|
-
|
|
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:
|
|
171
|
-
|
|
172
|
-
x:
|
|
173
|
-
y:
|
|
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
|
-
###
|
|
172
|
+
### Options
|
|
178
173
|
|
|
179
174
|
Option | Default | Description
|
|
180
175
|
-----------------|------------|----------------------------------------------------------------------------------------------------
|
|
181
176
|
`id` | - | **Required.** Unique name for this sprite.
|
|
182
|
-
`
|
|
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
|
-
###
|
|
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
|
-
###
|
|
200
|
+
### Methods
|
|
206
201
|
|
|
207
202
|
#### `sprite.draw(ctx)`
|
|
208
|
-
Draws the sprite.
|
|
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 `
|
|
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:
|
|
243
|
-
|
|
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.
|
|
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.
|
|
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>
|