littlejsengine 1.9.6 → 1.9.8

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.
@@ -76,6 +76,20 @@ function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
76
76
  debugPrimitives.push({pos, size:vec2(size), color, time:new Timer(time), angle, fill});
77
77
  }
78
78
 
79
+ /** Draw a debug poly in world space
80
+ * @param {Vector2} pos
81
+ * @param {Array} points
82
+ * @param {String} [color]
83
+ * @param {Number} [time]
84
+ * @param {Number} [angle]
85
+ * @param {Boolean} [fill]
86
+ * @memberof Debug */
87
+ function debugPoly(pos, points, color='#fff', time=0, angle=0, fill=false)
88
+ {
89
+ ASSERT(typeof color == 'string', 'pass in css color strings');
90
+ debugPrimitives.push({pos, points, color, time:new Timer(time), angle, fill});
91
+ }
92
+
79
93
  /** Draw a debug circle in world space
80
94
  * @param {Vector2} pos
81
95
  * @param {Number} [radius]
@@ -85,7 +99,7 @@ function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
85
99
  * @memberof Debug */
86
100
  function debugCircle(pos, radius=0, color='#fff', time=0, fill=false)
87
101
  {
88
- ASSERT(typeof color == 'string', 'pass in css color strings');
102
+ ASSERT(typeof color == 'string', 'pass in css color strings');
89
103
  debugPrimitives.push({pos, size:radius, color, time:new Timer(time), angle:0, fill});
90
104
  }
91
105
 
@@ -95,7 +109,11 @@ function debugCircle(pos, radius=0, color='#fff', time=0, fill=false)
95
109
  * @param {Number} [time]
96
110
  * @param {Number} [angle]
97
111
  * @memberof Debug */
98
- function debugPoint(pos, color, time, angle) {debugRect(pos, undefined, color, time, angle);}
112
+ function debugPoint(pos, color, time, angle)
113
+ {
114
+ ASSERT(typeof color == 'string', 'pass in css color strings');
115
+ debugRect(pos, undefined, color, time, angle);
116
+ }
99
117
 
100
118
  /** Draw a debug line in world space
101
119
  * @param {Vector2} posA
@@ -106,19 +124,20 @@ function debugPoint(pos, color, time, angle) {debugRect(pos, undefined, color, t
106
124
  * @memberof Debug */
107
125
  function debugLine(posA, posB, color, thickness=.1, time)
108
126
  {
127
+ ASSERT(typeof color == 'string', 'pass in css color strings');
109
128
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
110
129
  const size = vec2(thickness, halfDelta.length()*2);
111
130
  debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), true);
112
131
  }
113
132
 
114
- /** Draw a debug axis aligned bounding box in world space
133
+ /** Draw a debug combined axis aligned bounding box in world space
115
134
  * @param {Vector2} pA - position A
116
135
  * @param {Vector2} sA - size A
117
136
  * @param {Vector2} pB - position B
118
137
  * @param {Vector2} sB - size B
119
138
  * @param {String} [color]
120
139
  * @memberof Debug */
121
- function debugAABB(pA, sA, pB, sB, color)
140
+ function debugOverlap(pA, sA, pB, sB, color)
122
141
  {
123
142
  const minPos = vec2(min(pA.x - sA.x/2, pB.x - sB.x/2), min(pA.y - sA.y/2, pB.y - sB.y/2));
124
143
  const maxPos = vec2(max(pA.x + sA.x/2, pB.x + sB.x/2), max(pA.y + sA.y/2, pB.y + sB.y/2));
@@ -136,7 +155,7 @@ function debugAABB(pA, sA, pB, sB, color)
136
155
  * @memberof Debug */
137
156
  function debugText(text, pos, size=1, color='#fff', time=0, angle=0, font='monospace')
138
157
  {
139
- ASSERT(typeof color == 'string', 'pass in css color strings');
158
+ ASSERT(typeof color == 'string', 'pass in css color strings');
140
159
  debugPrimitives.push({text, pos, size, color, time:new Timer(time), angle, font});
141
160
  }
142
161
 
@@ -235,7 +254,7 @@ function debugRender()
235
254
  const stickScale = 1;
236
255
  const buttonScale = .2;
237
256
  const centerPos = cameraPos;
238
- const sticks = stickData[i];
257
+ const sticks = gamepadStickData[i];
239
258
  for (let j = sticks.length; j--;)
240
259
  {
241
260
  const drawPos = centerPos.add(vec2(j*stickScale*2, i*stickScale*3));
@@ -255,6 +274,7 @@ function debugRender()
255
274
  }
256
275
  }
257
276
 
277
+ let debugObject;
258
278
  if (debugOverlay)
259
279
  {
260
280
  const saveContext = mainContext;
@@ -265,11 +285,13 @@ function debugRender()
265
285
  debugRect(cameraPos, cameraSize.subtract(vec2(.1)), '#f008');
266
286
 
267
287
  // mouse pick
268
- let bestDistance = Infinity, bestObject;
288
+ let bestDistance = Infinity;
269
289
  for (const o of engineObjects)
270
290
  {
271
291
  if (o.canvas || o.destroyed)
272
292
  continue;
293
+
294
+ o.renderDebugInfo();
273
295
  if (!o.size.x || !o.size.y)
274
296
  continue;
275
297
 
@@ -277,33 +299,15 @@ function debugRender()
277
299
  if (distance < bestDistance)
278
300
  {
279
301
  bestDistance = distance;
280
- bestObject = o;
302
+ debugObject = o;
281
303
  }
282
-
283
- // show object info
284
- const size = vec2(max(o.size.x, .2), max(o.size.y, .2));
285
- const color1 = new Color(o.collideTiles?1:0, o.collideSolidObjects?1:0, o.isSolid?1:0, o.parent?.2:.5);
286
- const color2 = o.parent ? new Color(1,1,1,.5) : new Color(0,0,0,.8);
287
- drawRect(o.pos, size, color1, o.angle, false);
288
- drawRect(o.pos, size.scale(.8), color2, o.angle, false);
289
- o.parent && drawLine(o.pos, o.parent.pos, .1, new Color(0,0,1,.5), false);
290
- }
291
-
292
- if (bestObject)
293
- {
294
- const raycastHitPos = tileCollisionRaycast(bestObject.pos, mousePos);
295
- raycastHitPos && drawRect(raycastHitPos.floor().add(vec2(.5)), vec2(1), new Color(0,1,1,.3));
296
- drawRect(mousePos.floor().add(vec2(.5)), vec2(1), new Color(0,0,1,.5), 0, false);
297
- drawLine(mousePos, bestObject.pos, .1, raycastHitPos ? new Color(1,0,0,.5) : new Color(0,1,0,.5), false);
298
-
299
- const debugText = 'mouse pos = ' + mousePos +
300
- '\nmouse collision = ' + getTileCollisionData(mousePos) +
301
- '\n\n--- object info ---\n' +
302
- bestObject.toString();
303
- drawTextScreen(debugText, mousePosScreen, 24, new Color, .05, undefined, 'center', 'monospace');
304
304
  }
305
305
 
306
- glCopyToContext(mainContext = saveContext);
306
+ if (tileCollisionSize.x > 0 && tileCollisionSize.y > 0)
307
+ drawRect(mousePos.floor().add(vec2(.5)), vec2(1), rgb(0,0,1,.5), 0, false);
308
+ mainContext = saveContext;
309
+
310
+ //glCopyToContext(mainContext = saveContext);
307
311
  }
308
312
 
309
313
  {
@@ -318,6 +322,7 @@ function debugRender()
318
322
  const pos = worldToScreen(p.pos);
319
323
  overlayContext.translate(pos.x|0, pos.y|0);
320
324
  overlayContext.rotate(p.angle);
325
+ overlayContext.scale(1, -1);
321
326
  overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
322
327
 
323
328
  if (p.text != undefined)
@@ -327,7 +332,20 @@ function debugRender()
327
332
  overlayContext.textBaseline = 'middle';
328
333
  overlayContext.fillText(p.text, 0, 0);
329
334
  }
330
- else if (p.size == 0 || p.size.x === 0 && p.size.y === 0 )
335
+ else if (p.points != undefined)
336
+ {
337
+ // poly
338
+ overlayContext.beginPath();
339
+ for (const point of p.points)
340
+ {
341
+ const p2 = point.scale(cameraScale).floor();
342
+ overlayContext.lineTo(p2.x, p2.y);
343
+ }
344
+ overlayContext.closePath();
345
+ p.fill && overlayContext.fill();
346
+ overlayContext.stroke();
347
+ }
348
+ else if (p.size == 0 || p.size.x === 0 && p.size.y === 0)
331
349
  {
332
350
  // point
333
351
  overlayContext.fillRect(-pointSize/2, -1, pointSize, 3);
@@ -336,7 +354,8 @@ function debugRender()
336
354
  else if (p.size.x != undefined)
337
355
  {
338
356
  // rect
339
- const w = p.size.x*cameraScale|0, h = p.size.y*cameraScale|0;
357
+ const s = p.size.scale(cameraScale).floor();
358
+ const w = s.x, h = s.y;
340
359
  p.fill && overlayContext.fillRect(-w/2|0, -h/2|0, w, h);
341
360
  overlayContext.strokeRect(-w/2|0, -h/2|0, w, h);
342
361
  }
@@ -355,6 +374,22 @@ function debugRender()
355
374
  // remove expired primitives
356
375
  debugPrimitives = debugPrimitives.filter(r=>r.time<0);
357
376
  }
377
+
378
+ if (debugObject)
379
+ {
380
+ const saveContext = mainContext;
381
+ mainContext = overlayContext;
382
+ const raycastHitPos = tileCollisionRaycast(debugObject.pos, mousePos);
383
+ raycastHitPos && drawRect(raycastHitPos.floor().add(vec2(.5)), vec2(1), rgb(0,1,1,.3));
384
+ drawLine(mousePos, debugObject.pos, .1, raycastHitPos ? rgb(1,0,0,.5) : rgb(0,1,0,.5), false);
385
+
386
+ const debugText = 'mouse pos = ' + mousePos +
387
+ '\nmouse collision = ' + getTileCollisionData(mousePos) +
388
+ '\n\n--- object info ---\n' +
389
+ debugObject.toString();
390
+ drawTextScreen(debugText, mousePosScreen, 24, rgb(), .05, undefined, 'center', 'monospace');
391
+ mainContext = saveContext;
392
+ }
358
393
 
359
394
  {
360
395
  // draw debug overlay
package/src/engineDraw.js CHANGED
@@ -320,7 +320,7 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, conte
320
320
  context.save();
321
321
  context.translate(pos.x+.5, pos.y+.5);
322
322
  context.rotate(angle);
323
- context.scale(mirror ? -size.x : size.x, size.y);
323
+ context.scale(mirror ? -size.x : size.x, -size.y);
324
324
  drawFunction(context);
325
325
  context.restore();
326
326
  }
@@ -29,13 +29,16 @@ export {
29
29
  // Debug
30
30
  ASSERT,
31
31
  debugRect,
32
+ debugPoly,
32
33
  debugCircle,
33
34
  debugPoint,
34
35
  debugLine,
35
- debugAABB,
36
+ debugOverlap,
36
37
  debugText,
37
38
  debugClear,
38
39
  debugSaveCanvas,
40
+ debugSaveText,
41
+ debugSaveDataURL,
39
42
 
40
43
  // Settings
41
44
  cameraPos,
@@ -178,6 +181,7 @@ export {
178
181
  FontImage,
179
182
  isFullscreen,
180
183
  toggleFullscreen,
184
+ getCameraSize,
181
185
 
182
186
  // WebGL
183
187
  glCanvas,
@@ -186,7 +190,6 @@ export {
186
190
  glCompileShader,
187
191
  glCreateProgram,
188
192
  glCreateTexture,
189
- glInitPostProcess,
190
193
 
191
194
  // Input
192
195
  keyIsDown,
@@ -245,7 +248,5 @@ export {
245
248
  medals,
246
249
  medalsPreventUnlock,
247
250
  medalsInit,
248
- newgroundsInit,
249
251
  Medal,
250
- Newgrounds,
251
252
  };
@@ -9,9 +9,9 @@
9
9
  'use strict';
10
10
 
11
11
  /** List of all medals
12
- * @type {Array}
12
+ * @type {Object}
13
13
  * @memberof Medals */
14
- const medals = [];
14
+ const medals = {};
15
15
 
16
16
  // Engine internal variables not exposed to documentation
17
17
  let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
@@ -27,9 +27,45 @@ function medalsInit(saveName)
27
27
  {
28
28
  // check if medals are unlocked
29
29
  medalsSaveName = saveName;
30
- debugMedals || medals.forEach(medal=> medal.unlocked = (localStorage[medal.storageKey()] | 0));
30
+ if (!debugMedals)
31
+ medalsForEach(medal=> medal.unlocked = (localStorage[medal.storageKey()] | 0));
32
+
33
+ // engine automatically renders medals
34
+ addPluginRender(function()
35
+ {
36
+ if (!medalsDisplayQueue.length)
37
+ return;
38
+
39
+ // update first medal in queue
40
+ const medal = medalsDisplayQueue[0];
41
+ const time = timeReal - medalsDisplayTimeLast;
42
+ if (!medalsDisplayTimeLast)
43
+ medalsDisplayTimeLast = timeReal;
44
+ else if (time > medalDisplayTime)
45
+ {
46
+ medalsDisplayTimeLast = 0;
47
+ medalsDisplayQueue.shift();
48
+ }
49
+ else
50
+ {
51
+ // slide on/off medals
52
+ const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
53
+ const hidePercent =
54
+ time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
55
+ time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
56
+ medal.render(hidePercent);
57
+ }
58
+ });
31
59
  }
32
60
 
61
+ /** Calls a function for each medal
62
+ * @param {Function} callback
63
+ * @memberof Medals */
64
+ function medalsForEach(callback)
65
+ { Object.values(medals).forEach(medal=>callback(medal)); }
66
+
67
+ ///////////////////////////////////////////////////////////////////////////////
68
+
33
69
  /**
34
70
  * Medal - Tracks an unlockable medal
35
71
  * @example
@@ -74,7 +110,6 @@ class Medal
74
110
  ASSERT(medalsSaveName, 'save name must be set');
75
111
  localStorage[this.storageKey()] = this.unlocked = 1;
76
112
  medalsDisplayQueue.push(this);
77
- newgrounds && newgrounds.unlockMedal(this.id);
78
113
  }
79
114
 
80
115
  /** Render a medal
@@ -122,171 +157,4 @@ class Medal
122
157
 
123
158
  // Get local storage key used by the medal
124
159
  storageKey() { return medalsSaveName + '_' + this.id; }
125
- }
126
-
127
- // engine automatically renders medals
128
- function medalsRender()
129
- {
130
- if (!medalsDisplayQueue.length)
131
- return;
132
-
133
- // update first medal in queue
134
- const medal = medalsDisplayQueue[0];
135
- const time = timeReal - medalsDisplayTimeLast;
136
- if (!medalsDisplayTimeLast)
137
- medalsDisplayTimeLast = timeReal;
138
- else if (time > medalDisplayTime)
139
- {
140
- medalsDisplayTimeLast = 0;
141
- medalsDisplayQueue.shift();
142
- }
143
- else
144
- {
145
- // slide on/off medals
146
- const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
147
- const hidePercent =
148
- time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
149
- time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
150
- medal.render(hidePercent);
151
- }
152
- }
153
-
154
- ///////////////////////////////////////////////////////////////////////////////
155
-
156
- // global Newgrounds object
157
- let newgrounds;
158
-
159
- /** This can used to enable Newgrounds functionality
160
- * @param {Number} app_id - The newgrounds App ID
161
- * @param {String} [cipher] - The encryption Key (AES-128/Base64)
162
- * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher
163
- * @memberof Medals */
164
- function newgroundsInit(app_id, cipher, cryptoJS)
165
- { newgrounds = new Newgrounds(app_id, cipher, cryptoJS); }
166
-
167
- /**
168
- * Newgrounds API wrapper object
169
- * @example
170
- * // create a newgrounds object, replace the app id with your own
171
- * const app_id = '53123:1ZuSTQ9l';
172
- * newgrounds = new Newgrounds(app_id);
173
- */
174
- class Newgrounds
175
- {
176
- /** Create a newgrounds object
177
- * @param {Number} app_id - The newgrounds App ID
178
- * @param {String} [cipher] - The encryption Key (AES-128/Base64)
179
- * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher */
180
- constructor(app_id, cipher, cryptoJS)
181
- {
182
- ASSERT(!newgrounds && app_id>0, 'there can only be one newgrounds object');
183
- ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
184
-
185
- this.app_id = app_id;
186
- this.cipher = cipher;
187
- this.cryptoJS = cryptoJS;
188
- this.host = location ? location.hostname : '';
189
-
190
- // get session id from url search params
191
- const url = new URL(location.href);
192
- this.session_id = url.searchParams.get('ngio_session_id');
193
-
194
- if (!this.session_id)
195
- return; // only use newgrounds when logged in
196
-
197
- // get medals
198
- const medalsResult = this.call('Medal.getList');
199
- this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
200
- debugMedals && console.log(this.medals);
201
- for (const newgroundsMedal of this.medals)
202
- {
203
- const medal = medals[newgroundsMedal['id']];
204
- if (medal)
205
- {
206
- // copy newgrounds medal data
207
- medal.image = new Image;
208
- medal.image.src = newgroundsMedal['icon'];
209
- medal.name = newgroundsMedal['name'];
210
- medal.description = newgroundsMedal['description'];
211
- medal.unlocked = newgroundsMedal['unlocked'];
212
- medal.difficulty = newgroundsMedal['difficulty'];
213
- medal.value = newgroundsMedal['value'];
214
-
215
- if (medal.value)
216
- medal.description = medal.description + ' (' + medal.value + ')';
217
- }
218
- }
219
-
220
- // get scoreboards
221
- const scoreboardResult = this.call('ScoreBoard.getBoards');
222
- this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
223
- debugMedals && console.log(this.scoreboards);
224
-
225
- const keepAliveMS = 5 * 60 * 1e3;
226
- setInterval(()=>this.call('Gateway.ping', 0, true), keepAliveMS);
227
- }
228
-
229
- /** Send message to unlock a medal by id
230
- * @param {Number} id - The medal id */
231
- unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, true); }
232
-
233
- /** Send message to post score
234
- * @param {Number} id - The scoreboard id
235
- * @param {Number} value - The score value */
236
- postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, true); }
237
-
238
- /** Get scores from a scoreboard
239
- * @param {Number} id - The scoreboard id
240
- * @param {String} [user] - A user's id or name
241
- * @param {Number} [social] - If true, only social scores will be loaded
242
- * @param {Number} [skip] - Number of scores to skip before start
243
- * @param {Number} [limit] - Number of scores to include in the list
244
- * @return {Object} - The response JSON object
245
- */
246
- getScores(id, user, social=0, skip=0, limit=10)
247
- { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
248
-
249
- /** Send message to log a view */
250
- logView() { return this.call('App.logView', {'host':this.host}, true); }
251
-
252
- /** Send a message to call a component of the Newgrounds API
253
- * @param {String} component - Name of the component
254
- * @param {Object} [parameters] - Parameters to use for call
255
- * @param {Boolean} [async] - If true, don't wait for response before continuing
256
- * @return {Object} - The response JSON object
257
- */
258
- call(component, parameters, async=false)
259
- {
260
- const call = {'component':component, 'parameters':parameters};
261
- if (this.cipher)
262
- {
263
- // encrypt using AES-128 Base64 with cryptoJS
264
- const cryptoJS = this.cryptoJS;
265
- const aesKey = cryptoJS['enc']['Base64']['parse'](this.cipher);
266
- const iv = cryptoJS['lib']['WordArray']['random'](16);
267
- const encrypted = cryptoJS['AES']['encrypt'](JSON.stringify(call), aesKey, {'iv':iv});
268
- call['secure'] = cryptoJS['enc']['Base64']['stringify'](iv.concat(encrypted['ciphertext']));
269
- call['parameters'] = 0;
270
- }
271
-
272
- // build the input object
273
- const input =
274
- {
275
- 'app_id': this.app_id,
276
- 'session_id': this.session_id,
277
- 'call': call
278
- };
279
-
280
- // build post data
281
- const formData = new FormData();
282
- formData.append('input', JSON.stringify(input));
283
-
284
- // send post data
285
- const xmlHttp = new XMLHttpRequest();
286
- const url = 'https://newgrounds.io/gateway_v3.php';
287
- xmlHttp.open('POST', url, !debugMedals && async);
288
- xmlHttp.send(formData);
289
- debugMedals && console.log(xmlHttp.responseText);
290
- return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
291
- }
292
160
  }
@@ -85,6 +85,8 @@ class EngineObject
85
85
  this.spawnTime = time;
86
86
  /** @property {Array} - List of children of this object */
87
87
  this.children = [];
88
+ /** @property {Boolean} - Limit object speed using linear or circular math */
89
+ this.clampSpeedLinear = true;
88
90
 
89
91
  // parent child system
90
92
  /** @property {EngineObject} - Parent of object if in local space */
@@ -133,8 +135,21 @@ class EngineObject
133
135
  return;
134
136
 
135
137
  // limit max speed to prevent missing collisions
136
- this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
137
- this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
138
+ if (this.clampSpeedLinear)
139
+ {
140
+ this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
141
+ this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
142
+ }
143
+ else
144
+ {
145
+ const length2 = this.velocity.lengthSquared();
146
+ if (length2 > objectMaxSpeed*objectMaxSpeed)
147
+ {
148
+ const s = objectMaxSpeed / length2**.5;
149
+ this.velocity.x *= s;
150
+ this.velocity.y *= s;
151
+ }
152
+ }
138
153
 
139
154
  // apply physics
140
155
  const oldPos = this.pos.copy();
@@ -190,7 +205,7 @@ class EngineObject
190
205
  if (o.mass) // push away if not fixed
191
206
  o.velocity = o.velocity.subtract(velocity);
192
207
 
193
- debugOverlay && debugPhysics && debugAABB(this.pos, this.size, o.pos, o.size, '#f00');
208
+ debugOverlay && debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
194
209
  continue;
195
210
  }
196
211
 
@@ -252,7 +267,7 @@ class EngineObject
252
267
  else // bounce if other object is fixed
253
268
  this.velocity.x *= -elasticity;
254
269
  }
255
- debugOverlay && debugPhysics && debugAABB(this.pos, this.size, o.pos, o.size, '#f0f');
270
+ debugOverlay && debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f');
256
271
  }
257
272
  }
258
273
  if (this.collideTiles)
@@ -313,6 +328,22 @@ class EngineObject
313
328
  for (const child of this.children)
314
329
  child.destroy(child.parent = 0);
315
330
  }
331
+
332
+ /** Convert from local space to world space
333
+ * @param {Vector2} pos - local space point */
334
+ localToWorld(pos) { return this.pos.add(pos.rotate(-this.angle)); }
335
+
336
+ /** Convert from world space to local space
337
+ * @param {Vector2} pos - world space point */
338
+ worldToLocal(pos) { return pos.subtract(this.pos).rotate(this.angle); }
339
+
340
+ /** Convert from local space to world space for a vector (rotation only)
341
+ * @param {Vector2} vec - local space vector */
342
+ localToWorldVector(vec) { return vec.rotate(this.angle); }
343
+
344
+ /** Convert from world space to local space for a vector (rotation only)
345
+ * @param {Vector2} vec - world space vector */
346
+ worldToLocalVector(vec) { return vec.rotate(-this.angle); }
316
347
 
317
348
  /** Called to check if a tile collision should be resolved
318
349
  * @param {Number} tileData - the value of the tile at the position
@@ -399,4 +430,16 @@ class EngineObject
399
430
  return text;
400
431
  }
401
432
  }
433
+
434
+ /** Render debug info for this object */
435
+ renderDebugInfo()
436
+ {
437
+ // show object info for debugging
438
+ const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
439
+ const color1 = rgb(this.collideTiles?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, this.parent?.2:.5);
440
+ const color2 = this.parent ? rgb(1,1,1,.5) : rgb(0,0,0,.8);
441
+ drawRect(this.pos, size, color1, this.angle, false);
442
+ drawRect(this.pos, size.scale(.8), color2, this.angle, false);
443
+ this.parent && drawLine(this.pos, this.parent.pos, .1, rgb(0,0,1,.5), false);
444
+ }
402
445
  }
@@ -274,6 +274,9 @@ class Particle extends EngineObject
274
274
  this.localSpaceEmitter = localSpaceEmitter;
275
275
  /** @property {Function} - Called when particle dies */
276
276
  this.destroyCallback = destroyCallback;
277
+
278
+ // particles use circular clamped speed
279
+ this.clampSpeedLinear = false;
277
280
  }
278
281
 
279
282
  /** Render the particle, automatically called each frame, sorted by renderOrder */
@@ -22,10 +22,13 @@ function debugInit (){}
22
22
  function debugUpdate (){}
23
23
  function debugRender (){}
24
24
  function debugRect (){}
25
+ function debugPoly (){}
25
26
  function debugCircle (){}
26
27
  function debugPoint (){}
27
28
  function debugLine (){}
28
- function debugAABB (){}
29
+ function debugOverlap (){}
29
30
  function debugText (){}
30
31
  function debugClear (){}
31
- function debugSaveCanvas (){}
32
+ function debugSaveCanvas (){}
33
+ function debugSaveText (){}
34
+ function debugSaveDataURL(){}
@@ -26,9 +26,9 @@ let cameraScale = 32;
26
26
 
27
27
  /** The max size of the canvas, centered if window is larger
28
28
  * @type {Vector2}
29
- * @default Vector2(1920,1200)
29
+ * @default Vector2(1920,1080)
30
30
  * @memberof Settings */
31
- let canvasMaxSize = vec2(1920, 1200);
31
+ let canvasMaxSize = vec2(1920, 1080);
32
32
 
33
33
  /** Fixed size of the canvas, if enabled canvas size never changes
34
34
  * - you may also need to set mainCanvasSize if using screen space coords in startup
@@ -417,7 +417,12 @@ function setSoundEnable(enable) { soundEnable = enable; }
417
417
  /** Set volume scale to apply to all sound, music and speech
418
418
  * @param {Number} volume
419
419
  * @memberof Settings */
420
- function setSoundVolume(volume) { soundVolume = volume; }
420
+ function setSoundVolume(volume)
421
+ {
422
+ soundVolume = volume;
423
+ if (soundEnable && !headlessMode && audioGainNode)
424
+ audioGainNode.gain.value = volume; // update gain immediatly
425
+ }
421
426
 
422
427
  /** Set default range where sound no longer plays
423
428
  * @param {Number} range