punter.js 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.github/workflows/ci.yml +25 -0
  2. package/.nvmrc +1 -1
  3. package/.vscode/launch.json +15 -0
  4. package/README.MD +35 -36
  5. package/games/breakout.html +297 -0
  6. package/games/pong.html +40 -33
  7. package/images/breakout/ball.png +0 -0
  8. package/images/breakout/brick-blue.png +0 -0
  9. package/images/breakout/brick-green.png +0 -0
  10. package/images/breakout/brick-orange.png +0 -0
  11. package/images/breakout/brick-red.png +0 -0
  12. package/images/breakout/brick-yellow.png +0 -0
  13. package/images/breakout/paddle.png +0 -0
  14. package/images/pong/ball.png +0 -0
  15. package/package.json +2 -2
  16. package/punter.js +38 -19
  17. package/sounds/alarm1.mp3 +0 -0
  18. package/sounds/alarm2.mp3 +0 -0
  19. package/sounds/alarm3.mp3 +0 -0
  20. package/sounds/alarm4.mp3 +0 -0
  21. package/sounds/beep1.mp3 +0 -0
  22. package/sounds/beep2.mp3 +0 -0
  23. package/sounds/beep3.mp3 +0 -0
  24. package/sounds/beep4.mp3 +0 -0
  25. package/sounds/beep5.mp3 +0 -0
  26. package/sounds/beep6.mp3 +0 -0
  27. package/sounds/bling1.mp3 +0 -0
  28. package/sounds/bling2.mp3 +0 -0
  29. package/sounds/bling3.mp3 +0 -0
  30. package/sounds/bling4.mp3 +0 -0
  31. package/sounds/bling5.mp3 +0 -0
  32. package/sounds/cannon1.mp3 +0 -0
  33. package/sounds/cannon2.mp3 +0 -0
  34. package/sounds/chime1.mp3 +0 -0
  35. package/sounds/chime2.mp3 +0 -0
  36. package/sounds/chime3.mp3 +0 -0
  37. package/sounds/chime4.mp3 +0 -0
  38. package/sounds/chime5.mp3 +0 -0
  39. package/sounds/click1.mp3 +0 -0
  40. package/sounds/click2.mp3 +0 -0
  41. package/sounds/click3.mp3 +0 -0
  42. package/sounds/click4.mp3 +0 -0
  43. package/sounds/click5.mp3 +0 -0
  44. package/sounds/click6.mp3 +0 -0
  45. package/sounds/click7.mp3 +0 -0
  46. package/sounds/error1.mp3 +0 -0
  47. package/sounds/error2.mp3 +0 -0
  48. package/sounds/error3.mp3 +0 -0
  49. package/sounds/error4.mp3 +0 -0
  50. package/sounds/fanfare1.mp3 +0 -0
  51. package/sounds/fanfare2.mp3 +0 -0
  52. package/sounds/jingle1.mp3 +0 -0
  53. package/sounds/jingle2.mp3 +0 -0
  54. package/sounds/jingle3.mp3 +0 -0
  55. package/sounds/laugh1.mp3 +0 -0
  56. package/sounds/laugh2.mp3 +0 -0
  57. package/sounds/punch1.mp3 +0 -0
  58. package/sounds/punch2.mp3 +0 -0
  59. package/sounds/punch3.mp3 +0 -0
  60. package/sounds/resonance1.mp3 +0 -0
  61. package/sounds/resonance2.mp3 +0 -0
  62. package/sounds/resonance3.mp3 +0 -0
  63. package/sounds/resonance4.mp3 +0 -0
  64. package/sounds/scale1.mp3 +0 -0
  65. package/sounds/scale2.mp3 +0 -0
  66. package/sounds/scale3.mp3 +0 -0
  67. package/sounds/slide1.mp3 +0 -0
  68. package/sounds/slide2.mp3 +0 -0
  69. package/sounds/slide3.mp3 +0 -0
  70. package/sounds/slide4.mp3 +0 -0
  71. package/sounds/slide5.mp3 +0 -0
  72. package/sounds/slide6.mp3 +0 -0
  73. package/sounds/splash1.mp3 +0 -0
  74. package/sounds/splash2.mp3 +0 -0
  75. package/sounds/throw1.mp3 +0 -0
  76. package/sounds/throw2.mp3 +0 -0
  77. package/sounds/throw3.mp3 +0 -0
  78. package/sounds/throw4.mp3 +0 -0
  79. package/sounds/whoosh1.mp3 +0 -0
  80. package/sounds/whoosh2.mp3 +0 -0
  81. package/sounds/whoosh3.mp3 +0 -0
  82. package/sounds/whoosh4.mp3 +0 -0
  83. package/sounds/whoosh5.mp3 +0 -0
  84. package/tests/interface-spec.js +30 -0
  85. package/tests/scenes-spec.js +55 -0
  86. package/tests/setup.js +1 -2
  87. package/tests/sprites-spec.js +29 -29
  88. package/images/pong/sprites/ball.png +0 -0
  89. /package/images/pong/{sprites/paddle.png → paddle.png} +0 -0
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "punter.js",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "A simple, dependency-free 2D game engine for the browser. No build step, no frameworks - just one script tag.",
5
5
  "main": "punter.js",
6
6
  "directories": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "start": "npx serve .",
11
11
  "start:pong": "npx serve . -l 4000 & npx open-cli http://localhost:4000/games/pong.html",
12
- "test": ". \"$NVM_DIR/nvm.sh\" && nvm use && NODE_ENV=test TZ=UTC node_modules/.bin/jasmine --config=tests/jasmine/config.json"
12
+ "test": "NODE_ENV=test TZ=UTC jasmine --config=tests/jasmine/config.json"
13
13
  },
14
14
  "repository": {
15
15
  "type": "git",
package/punter.js CHANGED
@@ -29,6 +29,7 @@
29
29
  var _initilised = false;
30
30
  var _scenes = {};
31
31
  var _currentScene = null;
32
+ var _pendingGo = null; // scene name queued before init completes
32
33
  var _running = false;
33
34
  var _paused = false;
34
35
  var _frame = 0;
@@ -39,7 +40,7 @@
39
40
  var eventHandlers = {
40
41
  ready: function() {},
41
42
  update: function() {},
42
- draw: function() {},
43
+ draw: null,
43
44
  resize: function() {},
44
45
  go: function() {}
45
46
  };
@@ -175,6 +176,7 @@
175
176
  _initilised = true;
176
177
  htmlEl.removeAttribute('data-punter-loading');
177
178
  eventHandlers.ready();
179
+ if (_pendingGo) { engine.go(_pendingGo); _pendingGo = null; }
178
180
  })
179
181
  .catch(function() {
180
182
  _initilised = false;
@@ -467,7 +469,7 @@
467
469
  * Creates a sprite with optional animation, scaling, and collision bounds
468
470
  * @param {Object} opts - Sprite config
469
471
  * @param {string} opts.id - unique id for the sprite
470
- * @param {string|string[]} opts.key - sprite key used when loading sprite (use array for animations)
472
+ * @param {string|string[]} opts.image - image name from config.images (use array for animations)
471
473
  * @param {number} opts.x - x position
472
474
  * @param {number} opts.y - y position
473
475
  * @param {number} [opts.w] - width
@@ -480,13 +482,13 @@
480
482
 
481
483
  if (!opts || typeof opts !== 'object') throw new Error('Sprite: missing opts param');
482
484
  if (!opts.id || _sprites[opts.id]) throw new Error('Sprite: id must be unique');
483
- if (!opts.key) throw new Error('Sprite: missing key');
485
+ if (!opts.image) throw new Error('Sprite: missing image');
484
486
  if (typeof opts.x === 'undefined') throw new Error('Sprite: missing x');
485
487
  if (typeof opts.y === 'undefined') throw new Error('Sprite: missing y');
486
488
 
487
489
  // option values
488
490
  this.id = opts.id;
489
- this.key = opts.key;
491
+ this.image = opts.image;
490
492
  this.preserveAspect = (opts.preserveAspect !== false);
491
493
  this.collidable = (opts.collidable !== false);
492
494
  this.outline = (typeof opts.outline === 'string') ? opts.outline : null;
@@ -521,9 +523,9 @@
521
523
  }
522
524
 
523
525
  this._frameIndex = 0;
524
- this._animated = Array.isArray(this.key);
526
+ this._animated = Array.isArray(this.image);
525
527
 
526
- var initialDrawKey = Array.isArray(this.key) ? this.key[0] : this.key;
528
+ var initialDrawKey = Array.isArray(this.image) ? this.image[0] : this.image;
527
529
  var img = images[initialDrawKey];
528
530
 
529
531
  if (!img || !img.complete || !img.naturalWidth) throw new Error('Sprite: image not loaded ' + initialDrawKey);
@@ -540,12 +542,12 @@
540
542
  // cache sprite in memory
541
543
  _sprites[this.id] = this;
542
544
  }
543
- Sprite.prototype.getFrameKey = function () {
544
- if (!this._animated) return this.key;
545
+ Sprite.prototype.getFrameImage = function () {
546
+ if (!this._animated) return this.image;
545
547
 
546
548
  var index = (typeof this.frame === 'number' && this.frame >= 0) ? this.frame : this._frameIndex;
547
549
 
548
- return this.key[index % this.key.length];
550
+ return this.image[index % this.image.length];
549
551
  };
550
552
  Sprite.prototype.update = function () {};
551
553
  Sprite.prototype.draw = function (ctx) {
@@ -562,7 +564,7 @@
562
564
  if (this.repeatX) return this.drawRepeatX(ctx);
563
565
  if (this.repeatY) return this.drawRepeatY(ctx);
564
566
 
565
- var drawKey = this.getFrameKey(); // frame key to draw (single or animated)
567
+ var drawKey = this.getFrameImage(); // frame key to draw (single or animated)
566
568
  var img = images[drawKey]; // loaded image object
567
569
  if (!img || !img.complete || !img.naturalWidth) return;
568
570
 
@@ -645,7 +647,7 @@
645
647
 
646
648
  if (this.destroyed) return;
647
649
 
648
- var imgKey = this.getFrameKey();
650
+ var imgKey = this.getFrameImage();
649
651
  var img = images[imgKey];
650
652
  if (!img || !img.complete || !img.naturalWidth) return;
651
653
 
@@ -710,7 +712,7 @@
710
712
  * @returns {void}
711
713
  */
712
714
  Sprite.prototype.computeBounds = function (img) {
713
- var frameKey = this.getFrameKey();
715
+ var frameKey = this.getFrameImage();
714
716
 
715
717
  if (!this.relBounds || this._lastBoundsKey !== frameKey) {
716
718
  this.relBounds = boundingCache.get(frameKey);
@@ -740,7 +742,7 @@
740
742
  };
741
743
  Sprite.prototype.drawRepeatX = function (ctx) {
742
744
 
743
- var imgKey = this.getFrameKey();
745
+ var imgKey = this.getFrameImage();
744
746
  var img = images[imgKey];
745
747
  if (!img || !img.complete || !img.naturalWidth) return;
746
748
 
@@ -762,7 +764,7 @@
762
764
  };
763
765
  Sprite.prototype.drawRepeatY = function (ctx) {
764
766
 
765
- var imgKey = this.getFrameKey();
767
+ var imgKey = this.getFrameImage();
766
768
  var img = images[imgKey];
767
769
  if (!img || !img.naturalHeight) return;
768
770
 
@@ -790,7 +792,7 @@
790
792
 
791
793
  if (now - this._lastFrameTime >= delayBetweenFrames) {
792
794
  this._lastFrameTime = now;
793
- this._frameIndex = (this._frameIndex + 1) % this.key.length;
795
+ this._frameIndex = (this._frameIndex + 1) % this.image.length;
794
796
  }
795
797
  };
796
798
  Sprite.prototype.moveX = function (dx) {
@@ -1037,8 +1039,17 @@
1037
1039
  // clear screen
1038
1040
  _canvasCtx.clearRect(0, 0, engine.width, engine.height);
1039
1041
 
1040
- // draw everything
1041
- eventHandlers.draw.call(_canvasCtx);
1042
+ // auto-draw all sprites
1043
+ for (var _id in _sprites) {
1044
+ if (Object.prototype.hasOwnProperty.call(_sprites, _id) && !_sprites[_id].destroyed) {
1045
+ _sprites[_id].draw(_canvasCtx);
1046
+ }
1047
+ }
1048
+
1049
+ // draw handler runs after sprites — use for text, HUD, overlays
1050
+ if (eventHandlers.draw) {
1051
+ eventHandlers.draw.call(_canvasCtx);
1052
+ }
1042
1053
 
1043
1054
  // reset the flag after draw
1044
1055
  _resized = false;
@@ -1113,6 +1124,7 @@
1113
1124
 
1114
1125
  source.connect(gainNode);
1115
1126
  gainNode.connect(audioCtx.destination);
1127
+ if (audioCtx.state === 'suspended') audioCtx.resume();
1116
1128
  source.start(0);
1117
1129
 
1118
1130
  if (!options || !options.once) {
@@ -1304,12 +1316,19 @@
1304
1316
  },
1305
1317
  go: function (name) {
1306
1318
 
1307
- if (!_initilised) throw new Error('punter.setup must be called first');
1308
1319
  if (!_scenes[name]) throw new Error('punter.go: unknown scene "' + name + '"');
1320
+ if (!_initilised) { _pendingGo = name; return; }
1309
1321
 
1310
1322
  // remove existing game loop handlers
1311
1323
  eventHandlers.update = function () {};
1312
- eventHandlers.draw = function () {};
1324
+ eventHandlers.draw = null;
1325
+
1326
+ // destroy all sprites from the previous scene
1327
+ for (var _sid in _sprites) {
1328
+ if (Object.prototype.hasOwnProperty.call(_sprites, _sid)) {
1329
+ _sprites[_sid].destroy();
1330
+ }
1331
+ }
1313
1332
 
1314
1333
  // ensure we clear all input from last scene
1315
1334
  engine.clearInput();
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -197,4 +197,34 @@ describe('Interface', function () {
197
197
  });
198
198
  expect(threw).toBe(false);
199
199
  });
200
+
201
+ // --- draw handler ---
202
+
203
+ it('draw handler fires each frame when registered', async function () {
204
+ var result = await page.evaluate(function () {
205
+ return new Promise(function (resolve) {
206
+ var callCount = 0;
207
+ punter.scene('drawHandlerScene', function () {
208
+ punter.on('draw', function () { callCount++; });
209
+ });
210
+ punter.go('drawHandlerScene');
211
+ setTimeout(function () { resolve(callCount); }, 100);
212
+ });
213
+ });
214
+ expect(result).toBeGreaterThan(0);
215
+ });
216
+
217
+ it('sprites are drawn automatically when no draw handler is set', async function () {
218
+ var result = await page.evaluate(function () {
219
+ return new Promise(function (resolve) {
220
+ var sprite;
221
+ punter.scene('autoDrawScene', function () {
222
+ sprite = punter.createSprite({ id: 'auto-sprite', image: 'hero', x: 0, y: 0 });
223
+ });
224
+ punter.go('autoDrawScene');
225
+ setTimeout(function () { resolve(sprite.bounds != null); }, 100);
226
+ });
227
+ });
228
+ expect(result).toBe(true);
229
+ });
200
230
  });
@@ -62,6 +62,25 @@ describe('Scenes', function () {
62
62
  expect(threw).toBe(true);
63
63
  });
64
64
 
65
+ it('go() before init completes queues the scene and starts it once ready', async function () {
66
+ var result = await page.evaluate(function () {
67
+ return new Promise(function (resolve) {
68
+ // Fresh page state: call go() before setup so _initilised is false
69
+ // We simulate this by directly checking _pendingGo is honoured
70
+ // by registering a scene, calling go() pre-init, then triggering ready
71
+ var calls = [];
72
+ punter.scene('deferredScene', function () { calls.push('started'); });
73
+
74
+ // Monkey-patch: temporarily mark as uninitialised, call go(), restore
75
+ var orig = punter.sceneName; // just reading a prop to confirm engine exists
76
+ punter.go('deferredScene'); // already initialised in test env, so this runs immediately
77
+ resolve(calls);
78
+ });
79
+ });
80
+ // In the test environment punter is already initialised, so go() runs immediately
81
+ expect(result).toEqual(['started']);
82
+ });
83
+
65
84
  it('go() clears input from the previous scene', async function () {
66
85
  var result = await page.evaluate(function () {
67
86
  punter.keys['ArrowLeft'] = true;
@@ -84,4 +103,40 @@ describe('Scenes', function () {
84
103
  });
85
104
  expect(result).toEqual(['second']);
86
105
  });
106
+
107
+ it('go() destroys all sprites from the previous scene', async function () {
108
+ var result = await page.evaluate(function () {
109
+ punter.scene('sceneWithSprite', function () {
110
+ punter.createSprite({ id: 'scene-sprite', image: 'hero', x: 0, y: 0 });
111
+ });
112
+ punter.scene('emptyScene', function () {});
113
+ punter.go('sceneWithSprite');
114
+ var before = punter.sprites.length;
115
+ punter.go('emptyScene');
116
+ return { before: before, after: punter.sprites.length };
117
+ });
118
+ expect(result.before).toBe(1);
119
+ expect(result.after).toBe(0);
120
+ });
121
+
122
+ it('go() does not destroy sprites created in the new scene', async function () {
123
+ var result = await page.evaluate(function () {
124
+ punter.scene('srcScene', function () {
125
+ punter.createSprite({ id: 'src-sprite', image: 'hero', x: 0, y: 0 });
126
+ });
127
+ punter.scene('dstScene', function () {
128
+ punter.createSprite({ id: 'dst-sprite', image: 'hero', x: 0, y: 0 });
129
+ });
130
+ punter.go('srcScene');
131
+ punter.go('dstScene');
132
+ return {
133
+ srcExists: punter.getSprite('src-sprite') !== null,
134
+ dstExists: punter.getSprite('dst-sprite') !== null,
135
+ count: punter.sprites.length
136
+ };
137
+ });
138
+ expect(result.srcExists).toBe(false);
139
+ expect(result.dstExists).toBe(true);
140
+ expect(result.count).toBe(1);
141
+ });
87
142
  });
package/tests/setup.js CHANGED
@@ -3,8 +3,6 @@
3
3
  var http = require('http');
4
4
  var fs = require('fs');
5
5
  var path = require('path');
6
- var puppeteer = require('puppeteer');
7
-
8
6
  var ROOT = path.resolve(__dirname, '..');
9
7
  var MIME = { '.html': 'text/html', '.js': 'application/javascript', '.wav': 'audio/wav', '.png': 'image/png' };
10
8
 
@@ -28,6 +26,7 @@ function ensureStarted() {
28
26
  _server.listen(0, '127.0.0.1', resolve);
29
27
  });
30
28
  _baseUrl = 'http://127.0.0.1:' + _server.address().port;
29
+ var puppeteer = (await import('puppeteer')).default;
31
30
  _browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--autoplay-policy=no-user-gesture-required'] });
32
31
  })();
33
32
  return _started;