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.
Files changed (87) hide show
  1. package/.vscode/launch.json +15 -0
  2. package/README.MD +41 -46
  3. package/games/breakout.html +297 -0
  4. package/games/pong.html +59 -39
  5. package/images/breakout/ball.png +0 -0
  6. package/images/breakout/brick-blue.png +0 -0
  7. package/images/breakout/brick-green.png +0 -0
  8. package/images/breakout/brick-orange.png +0 -0
  9. package/images/breakout/brick-red.png +0 -0
  10. package/images/breakout/brick-yellow.png +0 -0
  11. package/images/breakout/paddle.png +0 -0
  12. package/images/pong/ball.png +0 -0
  13. package/package.json +3 -2
  14. package/punter.js +54 -35
  15. package/sounds/alarm1.mp3 +0 -0
  16. package/sounds/alarm2.mp3 +0 -0
  17. package/sounds/alarm3.mp3 +0 -0
  18. package/sounds/alarm4.mp3 +0 -0
  19. package/sounds/beep1.mp3 +0 -0
  20. package/sounds/beep2.mp3 +0 -0
  21. package/sounds/beep3.mp3 +0 -0
  22. package/sounds/beep4.mp3 +0 -0
  23. package/sounds/beep5.mp3 +0 -0
  24. package/sounds/beep6.mp3 +0 -0
  25. package/sounds/bling1.mp3 +0 -0
  26. package/sounds/bling2.mp3 +0 -0
  27. package/sounds/bling3.mp3 +0 -0
  28. package/sounds/bling4.mp3 +0 -0
  29. package/sounds/bling5.mp3 +0 -0
  30. package/sounds/cannon1.mp3 +0 -0
  31. package/sounds/cannon2.mp3 +0 -0
  32. package/sounds/chime1.mp3 +0 -0
  33. package/sounds/chime2.mp3 +0 -0
  34. package/sounds/chime3.mp3 +0 -0
  35. package/sounds/chime4.mp3 +0 -0
  36. package/sounds/chime5.mp3 +0 -0
  37. package/sounds/click1.mp3 +0 -0
  38. package/sounds/click2.mp3 +0 -0
  39. package/sounds/click3.mp3 +0 -0
  40. package/sounds/click4.mp3 +0 -0
  41. package/sounds/click5.mp3 +0 -0
  42. package/sounds/click6.mp3 +0 -0
  43. package/sounds/click7.mp3 +0 -0
  44. package/sounds/error1.mp3 +0 -0
  45. package/sounds/error2.mp3 +0 -0
  46. package/sounds/error3.mp3 +0 -0
  47. package/sounds/error4.mp3 +0 -0
  48. package/sounds/fanfare1.mp3 +0 -0
  49. package/sounds/fanfare2.mp3 +0 -0
  50. package/sounds/jingle1.mp3 +0 -0
  51. package/sounds/jingle2.mp3 +0 -0
  52. package/sounds/jingle3.mp3 +0 -0
  53. package/sounds/laugh1.mp3 +0 -0
  54. package/sounds/laugh2.mp3 +0 -0
  55. package/sounds/punch1.mp3 +0 -0
  56. package/sounds/punch2.mp3 +0 -0
  57. package/sounds/punch3.mp3 +0 -0
  58. package/sounds/resonance1.mp3 +0 -0
  59. package/sounds/resonance2.mp3 +0 -0
  60. package/sounds/resonance3.mp3 +0 -0
  61. package/sounds/resonance4.mp3 +0 -0
  62. package/sounds/scale1.mp3 +0 -0
  63. package/sounds/scale2.mp3 +0 -0
  64. package/sounds/scale3.mp3 +0 -0
  65. package/sounds/slide1.mp3 +0 -0
  66. package/sounds/slide2.mp3 +0 -0
  67. package/sounds/slide3.mp3 +0 -0
  68. package/sounds/slide4.mp3 +0 -0
  69. package/sounds/slide5.mp3 +0 -0
  70. package/sounds/slide6.mp3 +0 -0
  71. package/sounds/splash1.mp3 +0 -0
  72. package/sounds/splash2.mp3 +0 -0
  73. package/sounds/throw1.mp3 +0 -0
  74. package/sounds/throw2.mp3 +0 -0
  75. package/sounds/throw3.mp3 +0 -0
  76. package/sounds/throw4.mp3 +0 -0
  77. package/sounds/whoosh1.mp3 +0 -0
  78. package/sounds/whoosh2.mp3 +0 -0
  79. package/sounds/whoosh3.mp3 +0 -0
  80. package/sounds/whoosh4.mp3 +0 -0
  81. package/sounds/whoosh5.mp3 +0 -0
  82. package/tests/fixtures/index.html +2 -2
  83. package/tests/interface-spec.js +31 -1
  84. package/tests/scenes-spec.js +55 -0
  85. package/tests/sprites-spec.js +29 -29
  86. package/images/pong/sprites/ball.png +0 -0
  87. /package/images/pong/{sprites/paddle.png → paddle.png} +0 -0
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -20,9 +20,9 @@
20
20
  window.__ready = true;
21
21
  });
22
22
 
23
- punter.init({
23
+ punter.setup({
24
24
  canvas: '#game',
25
- sprites: { hero: heroImg },
25
+ images: { hero: heroImg },
26
26
  sounds: { beep: '/tests/fixtures/silence.wav' }
27
27
  });
28
28
  </script>
@@ -19,7 +19,7 @@ describe('Interface', function () {
19
19
  it('exposes the expected methods', async function () {
20
20
  var result = await page.evaluate(function () {
21
21
  return {
22
- init: typeof punter.init,
22
+ setup: typeof punter.setup,
23
23
  scene: typeof punter.scene,
24
24
  go: typeof punter.go,
25
25
  on: typeof punter.on,
@@ -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
  });
@@ -24,7 +24,7 @@ describe('Sprites', function () {
24
24
 
25
25
  it('creates a sprite with the given id, x and y', async function () {
26
26
  var result = await page.evaluate(function () {
27
- var s = punter.createSprite({ id: 's1', key: 'hero', x: 10, y: 20 });
27
+ var s = punter.createSprite({ id: 's1', image: 'hero', x: 10, y: 20 });
28
28
  return { id: s.id, x: s.x, y: s.y };
29
29
  });
30
30
  expect(result.id).toBe('s1');
@@ -34,7 +34,7 @@ describe('Sprites', function () {
34
34
 
35
35
  it('defaults width and height from the image dimensions', async function () {
36
36
  var result = await page.evaluate(function () {
37
- var s = punter.createSprite({ id: 's1', key: 'hero', x: 0, y: 0 });
37
+ var s = punter.createSprite({ id: 's1', image: 'hero', x: 0, y: 0 });
38
38
  return { w: s.w, h: s.h };
39
39
  });
40
40
  // 1x1 pixel image → both default to 1
@@ -45,8 +45,8 @@ describe('Sprites', function () {
45
45
  it('throws when creating a sprite with a duplicate id', async function () {
46
46
  var threw = await page.evaluate(function () {
47
47
  try {
48
- punter.createSprite({ id: 's1', key: 'hero', x: 0, y: 0 });
49
- punter.createSprite({ id: 's1', key: 'hero', x: 0, y: 0 });
48
+ punter.createSprite({ id: 's1', image: 'hero', x: 0, y: 0 });
49
+ punter.createSprite({ id: 's1', image: 'hero', x: 0, y: 0 });
50
50
  return false;
51
51
  } catch (e) { return true; }
52
52
  });
@@ -56,7 +56,7 @@ describe('Sprites', function () {
56
56
  it('throws when the image key has not been loaded', async function () {
57
57
  var threw = await page.evaluate(function () {
58
58
  try {
59
- punter.createSprite({ id: 's1', key: 'missing', x: 0, y: 0 });
59
+ punter.createSprite({ id: 's1', image: 'missing', x: 0, y: 0 });
60
60
  return false;
61
61
  } catch (e) { return true; }
62
62
  });
@@ -67,7 +67,7 @@ describe('Sprites', function () {
67
67
  var result = await page.evaluate(function () {
68
68
  var errors = [];
69
69
  try { punter.createSprite(); } catch (e) { errors.push('no-opts'); }
70
- try { punter.createSprite({ id: 's1', key: 'hero' }); } catch (e) { errors.push('no-x'); }
70
+ try { punter.createSprite({ id: 's1', image: 'hero' }); } catch (e) { errors.push('no-x'); }
71
71
  return errors;
72
72
  });
73
73
  expect(result).toContain('no-opts');
@@ -78,7 +78,7 @@ describe('Sprites', function () {
78
78
 
79
79
  it('getSprite returns the sprite by id', async function () {
80
80
  var result = await page.evaluate(function () {
81
- punter.createSprite({ id: 's1', key: 'hero', x: 0, y: 0 });
81
+ punter.createSprite({ id: 's1', image: 'hero', x: 0, y: 0 });
82
82
  var found = punter.getSprite('s1');
83
83
  return found ? found.id : null;
84
84
  });
@@ -96,7 +96,7 @@ describe('Sprites', function () {
96
96
 
97
97
  it('moveX changes x position', async function () {
98
98
  var result = await page.evaluate(function () {
99
- var s = punter.createSprite({ id: 's1', key: 'hero', x: 50, y: 0 });
99
+ var s = punter.createSprite({ id: 's1', image: 'hero', x: 50, y: 0 });
100
100
  s.moveX(10);
101
101
  var after1 = s.x;
102
102
  s.moveX(-25);
@@ -108,7 +108,7 @@ describe('Sprites', function () {
108
108
 
109
109
  it('moveY changes y position', async function () {
110
110
  var result = await page.evaluate(function () {
111
- var s = punter.createSprite({ id: 's1', key: 'hero', x: 0, y: 50 });
111
+ var s = punter.createSprite({ id: 's1', image: 'hero', x: 0, y: 50 });
112
112
  s.moveY(15);
113
113
  return s.y;
114
114
  });
@@ -117,7 +117,7 @@ describe('Sprites', function () {
117
117
 
118
118
  it('centerX positions the sprite horizontally in the middle', async function () {
119
119
  var result = await page.evaluate(function () {
120
- var s = punter.createSprite({ id: 's1', key: 'hero', x: 0, y: 0 });
120
+ var s = punter.createSprite({ id: 's1', image: 'hero', x: 0, y: 0 });
121
121
  s.centerX();
122
122
  return { x: s.x, expected: Math.floor((punter.width - s.w) / 2) };
123
123
  });
@@ -126,7 +126,7 @@ describe('Sprites', function () {
126
126
 
127
127
  it('centerY positions the sprite vertically in the middle', async function () {
128
128
  var result = await page.evaluate(function () {
129
- var s = punter.createSprite({ id: 's1', key: 'hero', x: 0, y: 0 });
129
+ var s = punter.createSprite({ id: 's1', image: 'hero', x: 0, y: 0 });
130
130
  s.centerY();
131
131
  return { y: s.y, expected: Math.floor((punter.height - s.h) / 2) };
132
132
  });
@@ -137,7 +137,7 @@ describe('Sprites', function () {
137
137
 
138
138
  it('bounce oscillates the sprite y around its initial position', async function () {
139
139
  var result = await page.evaluate(function () {
140
- var s = punter.createSprite({ id: 's1', key: 'hero', x: 0, y: 100 });
140
+ var s = punter.createSprite({ id: 's1', image: 'hero', x: 0, y: 100 });
141
141
  var initial = s.initialY;
142
142
  s.bounce(8, 10);
143
143
  var firstY = s.y;
@@ -152,7 +152,7 @@ describe('Sprites', function () {
152
152
 
153
153
  it('visible is true when the sprite is on screen', async function () {
154
154
  var result = await page.evaluate(function () {
155
- var s = punter.createSprite({ id: 's1', key: 'hero', x: 0, y: 0 });
155
+ var s = punter.createSprite({ id: 's1', image: 'hero', x: 0, y: 0 });
156
156
  return s.visible;
157
157
  });
158
158
  expect(result).toBe(true);
@@ -160,7 +160,7 @@ describe('Sprites', function () {
160
160
 
161
161
  it('visible is false when the sprite is fully off the left edge', async function () {
162
162
  var result = await page.evaluate(function () {
163
- var s = punter.createSprite({ id: 's1', key: 'hero', x: 0, y: 0 });
163
+ var s = punter.createSprite({ id: 's1', image: 'hero', x: 0, y: 0 });
164
164
  s.moveX(-(s.w + 1));
165
165
  return s.visible;
166
166
  });
@@ -171,7 +171,7 @@ describe('Sprites', function () {
171
171
 
172
172
  it('destroy marks the sprite as destroyed and removes it from the registry', async function () {
173
173
  var result = await page.evaluate(function () {
174
- var s = punter.createSprite({ id: 's1', key: 'hero', x: 0, y: 0 });
174
+ var s = punter.createSprite({ id: 's1', image: 'hero', x: 0, y: 0 });
175
175
  s.destroy();
176
176
  return { destroyed: s.destroyed, found: punter.getSprite('s1') };
177
177
  });
@@ -181,25 +181,25 @@ describe('Sprites', function () {
181
181
 
182
182
  // --- animation ---
183
183
 
184
- it('getFrameKey returns the key string for a non-animated sprite', async function () {
184
+ it('getFrameImage returns the image string for a non-animated sprite', async function () {
185
185
  var result = await page.evaluate(function () {
186
- var s = punter.createSprite({ id: 's1', key: 'hero', x: 0, y: 0 });
187
- return s.getFrameKey();
186
+ var s = punter.createSprite({ id: 's1', image: 'hero', x: 0, y: 0 });
187
+ return s.getFrameImage();
188
188
  });
189
189
  expect(result).toBe('hero');
190
190
  });
191
191
 
192
- it('getFrameKey cycles through keys for an animated sprite', async function () {
192
+ it('getFrameImage cycles through images for an animated sprite', async function () {
193
193
  var result = await page.evaluate(function () {
194
- var s = punter.createSprite({ id: 's1', key: ['hero', 'hero'], x: 0, y: 0 });
195
- return s.getFrameKey();
194
+ var s = punter.createSprite({ id: 's1', image: ['hero', 'hero'], x: 0, y: 0 });
195
+ return s.getFrameImage();
196
196
  });
197
197
  expect(result).toBe('hero');
198
198
  });
199
199
 
200
200
  it('animate advances the frame index after the delay elapses', async function () {
201
201
  var result = await page.evaluate(function () {
202
- var s = punter.createSprite({ id: 's1', key: ['hero', 'hero'], x: 0, y: 0 });
202
+ var s = punter.createSprite({ id: 's1', image: ['hero', 'hero'], x: 0, y: 0 });
203
203
  var initial = s._frameIndex;
204
204
  s.animate(0);
205
205
  var afterFirst = s._frameIndex;
@@ -215,8 +215,8 @@ describe('Sprites', function () {
215
215
 
216
216
  it('isCollidingWith returns true when bounding boxes overlap', async function () {
217
217
  var result = await page.evaluate(function () {
218
- var s1 = punter.createSprite({ id: 's1', key: 'hero', x: 0, y: 0 });
219
- var s2 = punter.createSprite({ id: 's2', key: 'hero', x: 0, y: 0 });
218
+ var s1 = punter.createSprite({ id: 's1', image: 'hero', x: 0, y: 0 });
219
+ var s2 = punter.createSprite({ id: 's2', image: 'hero', x: 0, y: 0 });
220
220
  s1.bounds = { x: 0, y: 0, w: 100, h: 100 };
221
221
  s2.bounds = { x: 50, y: 50, w: 100, h: 100 };
222
222
  return s1.isCollidingWith(s2);
@@ -226,8 +226,8 @@ describe('Sprites', function () {
226
226
 
227
227
  it('isCollidingWith returns false when bounding boxes do not overlap', async function () {
228
228
  var result = await page.evaluate(function () {
229
- var s1 = punter.createSprite({ id: 's1', key: 'hero', x: 0, y: 0 });
230
- var s2 = punter.createSprite({ id: 's2', key: 'hero', x: 0, y: 0 });
229
+ var s1 = punter.createSprite({ id: 's1', image: 'hero', x: 0, y: 0 });
230
+ var s2 = punter.createSprite({ id: 's2', image: 'hero', x: 0, y: 0 });
231
231
  s1.bounds = { x: 0, y: 0, w: 100, h: 100 };
232
232
  s2.bounds = { x: 200, y: 200, w: 100, h: 100 };
233
233
  return s1.isCollidingWith(s2);
@@ -237,8 +237,8 @@ describe('Sprites', function () {
237
237
 
238
238
  it('isCollidingWith returns false when either sprite has no bounds', async function () {
239
239
  var result = await page.evaluate(function () {
240
- var s1 = punter.createSprite({ id: 's1', key: 'hero', x: 0, y: 0 });
241
- var s2 = punter.createSprite({ id: 's2', key: 'hero', x: 0, y: 0 });
240
+ var s1 = punter.createSprite({ id: 's1', image: 'hero', x: 0, y: 0 });
241
+ var s2 = punter.createSprite({ id: 's2', image: 'hero', x: 0, y: 0 });
242
242
  return s1.isCollidingWith(s2);
243
243
  });
244
244
  expect(result).toBe(false);
@@ -248,7 +248,7 @@ describe('Sprites', function () {
248
248
 
249
249
  it('seen flag starts as false and can be set', async function () {
250
250
  var result = await page.evaluate(function () {
251
- var s = punter.createSprite({ id: 's1', key: 'hero', x: 0, y: 0 });
251
+ var s = punter.createSprite({ id: 's1', image: 'hero', x: 0, y: 0 });
252
252
  var initial = s.seen;
253
253
  s.seen = true;
254
254
  return { initial: initial, afterSet: s.seen };
Binary file
File without changes