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.
- package/.github/workflows/ci.yml +25 -0
- package/.nvmrc +1 -1
- package/.vscode/launch.json +15 -0
- package/README.MD +35 -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 +2 -2
- 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/setup.js +1 -2
- 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,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
|
-
|
|
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
|
+
[](https://github.com/orca-scan/punter.js/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/punter.js)
|
|
5
|
+
[](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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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. **
|
|
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',
|
|
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.
|
|
94
|
-
punter.
|
|
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
|
|
147
|
-
punter.go(
|
|
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
|
-
//
|
|
156
|
-
|
|
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:
|
|
171
|
-
|
|
172
|
-
x:
|
|
173
|
-
y:
|
|
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
|
-
###
|
|
176
|
+
### Options
|
|
178
177
|
|
|
179
178
|
Option | Default | Description
|
|
180
179
|
-----------------|------------|----------------------------------------------------------------------------------------------------
|
|
181
180
|
`id` | - | **Required.** Unique name for this sprite.
|
|
182
|
-
`
|
|
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
|
-
###
|
|
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
|
-
###
|
|
204
|
+
### Methods
|
|
206
205
|
|
|
207
206
|
#### `sprite.draw(ctx)`
|
|
208
|
-
Draws the sprite.
|
|
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 `
|
|
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:
|
|
243
|
-
|
|
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(
|
|
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
|