punter.js 1.0.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.
@@ -0,0 +1,260 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Pong</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ html, body { width: 100%; height: 100%; background: #1e2230; overflow: hidden; }
10
+ canvas { display: block; }
11
+ </style>
12
+ <script src="../punter.js"></script>
13
+ </head>
14
+ <body>
15
+ <canvas id="game"></canvas>
16
+ <script>
17
+
18
+ // =========================================================
19
+ // COLORS (Orca Scan brand palette)
20
+ // =========================================================
21
+ var COLOR_BG = '#1b2125';
22
+ var COLOR_NET = '#2c3033';
23
+ var COLOR_SCORE = '#f5c518';
24
+
25
+ // =========================================================
26
+ // HELPERS
27
+ // =========================================================
28
+
29
+ var DPR = punter.dpr;
30
+
31
+ /**
32
+ * Converts a design-pixel value to real screen pixels.
33
+ * On a retina display (DPR=2), px(14) becomes 28 — keeping sizes
34
+ * physically consistent across all screens.
35
+ * @param {number} n - value in design pixels
36
+ * @returns {number}
37
+ */
38
+ function px(n) {
39
+ return n * DPR;
40
+ }
41
+
42
+ /**
43
+ * Keeps a number between a minimum and maximum value.
44
+ * e.g. clamp(110, 0, 100) returns 100.
45
+ * @param {number} value
46
+ * @param {number} min
47
+ * @param {number} max
48
+ * @returns {number}
49
+ */
50
+ function clamp(value, min, max) {
51
+ return Math.max(min, Math.min(value, max));
52
+ }
53
+
54
+ // =========================================================
55
+ // GAME SETTINGS
56
+ // =========================================================
57
+ var MARGIN = px(40); // gap between paddle and screen edge
58
+ var PADDLE_W = px(14);
59
+ var PADDLE_H = px(90);
60
+ var BALL_SIZE = px(14);
61
+ var PADDLE_SPEED = px(7);
62
+ var AI_SPEED = px(5);
63
+ var BALL_SPEED = px(5);
64
+ var MAX_BALL_SPEED = px(14);
65
+
66
+ // Named serve directions — used when resetting the ball after a point
67
+ var SERVE_LEFT = -1; // ball moves left (toward the AI)
68
+ var SERVE_RIGHT = 1; // ball moves right (toward the player)
69
+
70
+ // =========================================================
71
+ // GAME STATE
72
+ // =========================================================
73
+ var ballVel = { x: BALL_SPEED, y: BALL_SPEED * 0.6 }; // ball direction + speed
74
+ var score = { player: 0, ai: 0 };
75
+
76
+ // Sprite references — set when the scene starts
77
+ var playerSprite, aiSprite, ballSprite;
78
+
79
+ // =========================================================
80
+ // SETUP
81
+ // Load the two sprite images before the game starts.
82
+ // =========================================================
83
+ punter.init({
84
+ canvas: '#game',
85
+ sprites: {
86
+ paddle: '../images/pong/sprites/paddle.png',
87
+ ball: '../images/pong/sprites/ball.png'
88
+ }
89
+ });
90
+
91
+ // =========================================================
92
+ // SCENE: play
93
+ // =========================================================
94
+ punter.scene('play', function () {
95
+
96
+ // Create the three sprites punter.js will draw each frame.
97
+ // w/h set the draw size; the image is scaled to fit automatically.
98
+ playerSprite = punter.createSprite({ id: 'player', key: 'paddle', x: 0, y: 0, w: PADDLE_W, h: PADDLE_H });
99
+ aiSprite = punter.createSprite({ id: 'ai', key: 'paddle', x: 0, y: 0, w: PADDLE_W, h: PADDLE_H });
100
+ ballSprite = punter.createSprite({ id: 'ball', key: 'ball', x: 0, y: 0, w: BALL_SIZE, h: BALL_SIZE });
101
+
102
+ // Place everything in the correct starting position
103
+ resetPositions();
104
+
105
+ // -------------------------------------------------------
106
+ // UPDATE — game logic, runs ~60 times per second
107
+ // -------------------------------------------------------
108
+ punter.on('update', function () {
109
+ var w = punter.width;
110
+ var h = punter.height;
111
+
112
+ // Keep AI paddle pinned to the right edge (handles resize too)
113
+ aiSprite.x = w - MARGIN - PADDLE_W;
114
+
115
+ // --- Player: keyboard ---
116
+ if (punter.keys['ArrowUp'] || punter.keys['w']) playerSprite.y -= PADDLE_SPEED;
117
+ if (punter.keys['ArrowDown'] || punter.keys['s']) playerSprite.y += PADDLE_SPEED;
118
+
119
+ // --- Player: tap / click ---
120
+ if (punter.mouse.clicked) {
121
+ playerSprite.y = punter.mouse.y - PADDLE_H / 2;
122
+ }
123
+
124
+ // Keep the player paddle inside the screen
125
+ playerSprite.y = clamp(playerSprite.y, 0, h - PADDLE_H);
126
+
127
+ // --- AI: follow the ball ---
128
+ var aiCenterY = aiSprite.y + PADDLE_H / 2;
129
+ if (ballSprite.y < aiCenterY - 10) aiSprite.y -= AI_SPEED;
130
+ if (ballSprite.y > aiCenterY + 10) aiSprite.y += AI_SPEED;
131
+ aiSprite.y = clamp(aiSprite.y, 0, h - PADDLE_H);
132
+
133
+ // --- Move the ball ---
134
+ ballSprite.x += ballVel.x;
135
+ ballSprite.y += ballVel.y;
136
+
137
+ // Bounce off top and bottom walls
138
+ if (ballSprite.y <= 0) { ballVel.y = Math.abs(ballVel.y); ballSprite.y = 0; }
139
+ if (ballSprite.y >= h - BALL_SIZE) { ballVel.y = -Math.abs(ballVel.y); ballSprite.y = h - BALL_SIZE; }
140
+
141
+ // --- Collision: ball hits player paddle ---
142
+ if (ballVel.x < 0 && ballSprite.isCollidingWith(playerSprite)) {
143
+
144
+ ballVel.x = Math.abs(ballVel.x) * 1.05;
145
+ ballSprite.x = playerSprite.x + PADDLE_W;
146
+ // Where on the paddle the ball hit: 0 = top edge, 1 = bottom edge, 0.5 = centre (flat return)
147
+ var playerHit = (ballSprite.y + BALL_SIZE / 2 - playerSprite.y) / PADDLE_H;
148
+ ballVel.y = (playerHit - 0.5) * BALL_SPEED * 2;
149
+ }
150
+
151
+ // --- Collision: ball hits AI paddle ---
152
+ if (ballVel.x > 0 && ballSprite.isCollidingWith(aiSprite)) {
153
+
154
+ ballVel.x = -Math.abs(ballVel.x) * 1.05;
155
+ ballSprite.x = aiSprite.x - BALL_SIZE;
156
+ // Where on the paddle the ball hit: 0 = top edge, 1 = bottom edge, 0.5 = centre (flat return)
157
+ var aiHit = (ballSprite.y + BALL_SIZE / 2 - aiSprite.y) / PADDLE_H;
158
+ ballVel.y = (aiHit - 0.5) * BALL_SPEED * 2;
159
+ }
160
+
161
+ // Cap ball speed so the game stays playable
162
+ if (ballVel.x > MAX_BALL_SPEED) ballVel.x = MAX_BALL_SPEED;
163
+ if (ballVel.x < -MAX_BALL_SPEED) ballVel.x = -MAX_BALL_SPEED;
164
+
165
+ // --- Scoring ---
166
+ if (ballSprite.x + BALL_SIZE < 0) { score.ai++; resetBall(SERVE_RIGHT); }
167
+ if (ballSprite.x > w) { score.player++; resetBall(SERVE_LEFT); }
168
+ });
169
+
170
+ // -------------------------------------------------------
171
+ // DRAW — render each frame
172
+ // -------------------------------------------------------
173
+ punter.on('draw', function () {
174
+ var ctx = this; // punter passes the canvas 2D context as 'this'
175
+ var w = punter.width;
176
+ var h = punter.height;
177
+
178
+ // Background
179
+ ctx.fillStyle = COLOR_BG;
180
+ ctx.fillRect(0, 0, w, h);
181
+
182
+ // Dashed centre line
183
+ ctx.setLineDash([px(8), px(10)]);
184
+ ctx.strokeStyle = COLOR_NET;
185
+ ctx.lineWidth = px(2);
186
+ ctx.beginPath();
187
+ ctx.moveTo(w / 2, 0);
188
+ ctx.lineTo(w / 2, h);
189
+ ctx.stroke();
190
+ ctx.setLineDash([]);
191
+
192
+ // Sprites — punter.js scales and draws the PNG images
193
+ playerSprite.draw(ctx);
194
+ aiSprite.draw(ctx);
195
+ ballSprite.draw(ctx);
196
+
197
+ // Scores
198
+ ctx.fillStyle = COLOR_SCORE;
199
+ ctx.font = 'bold ' + px(48) + 'px monospace';
200
+ ctx.textAlign = 'center';
201
+ ctx.textBaseline = 'top';
202
+ ctx.fillText(score.player, w / 2 - px(70), px(20));
203
+ ctx.fillText(score.ai, w / 2 + px(70), px(20));
204
+
205
+ // Controls hint
206
+ ctx.fillStyle = COLOR_NET;
207
+ ctx.font = px(14) + 'px monospace';
208
+ ctx.textBaseline = 'bottom';
209
+ ctx.fillText('Arrow keys or W / S · tap to move', w / 2, h - px(12));
210
+ });
211
+ });
212
+
213
+ /**
214
+ * Centres both paddles and resets the ball.
215
+ * Called when the scene starts and on every window resize.
216
+ */
217
+ function resetPositions() {
218
+ // Sprites aren't created until the ready event fires and the scene runs.
219
+ // The engine's resize can fire before that, so bail out early if not ready.
220
+ if (!playerSprite || !aiSprite || !ballSprite) return;
221
+
222
+ var w = punter.width;
223
+ var h = punter.height;
224
+
225
+ playerSprite.x = MARGIN;
226
+ playerSprite.y = h / 2 - PADDLE_H / 2;
227
+
228
+ aiSprite.x = w - MARGIN - PADDLE_W;
229
+ aiSprite.y = h / 2 - PADDLE_H / 2;
230
+
231
+ resetBall(SERVE_LEFT);
232
+ }
233
+
234
+ /**
235
+ * Puts the ball back in the centre with a fresh serve.
236
+ * @param {number} direction - SERVE_LEFT (toward AI) or SERVE_RIGHT (toward player)
237
+ */
238
+ function resetBall(direction) {
239
+ ballSprite.x = punter.width / 2 - BALL_SIZE / 2;
240
+ ballSprite.y = punter.height / 2 - BALL_SIZE / 2;
241
+ ballVel.x = BALL_SPEED * direction;
242
+ ballVel.y = (Math.random() - 0.5) * BALL_SPEED * 1.2;
243
+ }
244
+
245
+ // Re-centre everything when the browser window resizes
246
+ punter.on('resize', function () {
247
+ resetPositions();
248
+ });
249
+
250
+ // =========================================================
251
+ // START — go to the play scene once everything is loaded
252
+ // =========================================================
253
+ punter.on('ready', function () {
254
+ score = { player: 0, ai: 0 };
255
+ punter.go('play');
256
+ });
257
+
258
+ </script>
259
+ </body>
260
+ </html>
Binary file
Binary file
Binary file
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "punter.js",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight, dependency-free JavaScript 2D game engine for the browser. Drop in a script tag and start building.",
5
+ "main": "punter.js",
6
+ "directories": {
7
+ "test": "tests"
8
+ },
9
+ "scripts": {
10
+ "start": "npx serve .",
11
+ "test": ". \"$NVM_DIR/nvm.sh\" && nvm use && NODE_ENV=test TZ=UTC node_modules/.bin/jasmine --config=tests/jasmine/config.json"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/orca-scan/punter.js.git"
16
+ },
17
+ "keywords": [],
18
+ "author": "",
19
+ "license": "ISC",
20
+ "bugs": {
21
+ "url": "https://github.com/orca-scan/punter.js/issues"
22
+ },
23
+ "homepage": "https://github.com/orca-scan/punter.js#readme",
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "devDependencies": {
28
+ "jasmine": "^5.13.0",
29
+ "jasmine-console-reporter": "^3.1.0",
30
+ "jasmine-xml-reporter": "^1.2.1",
31
+ "puppeteer": "^25.3.0"
32
+ }
33
+ }