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.
- package/.vscode/launch.json +15 -0
- package/README.MD +31 -36
- package/games/breakout.html +297 -0
- package/games/pong.html +40 -33
- 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 +1 -1
- package/punter.js +38 -19
- 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/interface-spec.js +30 -0
- 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
|
@@ -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
|
-
|
|
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
|
|
|
@@ -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. **
|
|
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',
|
|
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.
|
|
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
|
|
@@ -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
|
|
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
|
|
|
@@ -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(
|
|
71
|
-
var PADDLE_W = px(14);
|
|
72
|
-
var PADDLE_H = px(90);
|
|
73
|
-
var
|
|
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/
|
|
100
|
-
ball: '../images/pong/
|
|
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',
|
|
112
|
-
aiSprite = punter.createSprite({ id: 'ai',
|
|
113
|
-
ballSprite = punter.createSprite({ id: 'ball',
|
|
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.
|
|
130
|
-
if (punter.keys['ArrowDown'] || punter.keys['s']) playerSprite.
|
|
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.
|
|
143
|
-
if (ballSprite.y > aiCenterY + 10) aiSprite.
|
|
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.
|
|
148
|
-
ballSprite.
|
|
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) {
|
|
180
|
-
|
|
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