littlejsengine 1.14.9 → 1.14.11

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.
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.14.9';
36
+ const engineVersion = '1.14.11';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -122,12 +122,12 @@ function engineAddPlugin(updateFunction, renderFunction)
122
122
  * @example
123
123
  * // Basic engine startup
124
124
  * engineInit(
125
- * () => { console.log('Game initialized!'); }, // gameInit
126
- * () => { updateGameLogic(); }, // gameUpdate
127
- * () => { updateUI(); }, // gameUpdatePost
128
- * () => { drawBackground(); }, // gameRender
129
- * () => { drawHUD(); }, // gameRenderPost
130
- * ['tiles.png', 'tilesLevel.png'] // images to load
125
+ * () => { LOG('Game initialized!'); }, // gameInit
126
+ * () => { updateGameLogic(); }, // gameUpdate
127
+ * () => { updateUI(); }, // gameUpdatePost
128
+ * () => { drawBackground(); }, // gameRender
129
+ * () => { drawHUD(); }, // gameRenderPost
130
+ * ['tiles.png', 'tilesLevel.png'] // images to load
131
131
  * );
132
132
  * @memberof Engine */
133
133
  async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
@@ -173,16 +173,20 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
173
173
  if (!debugSpeedUp)
174
174
  frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
175
175
 
176
+ let wasUpdated = false;
176
177
  if (paused)
177
178
  {
179
+ // update everything except the game and objects
180
+ wasUpdated = true;
178
181
  updateCanvas();
182
+ inputUpdate();
183
+ pluginUpdateList.forEach(f=>f());
179
184
 
180
185
  // update object transforms even when paused
181
186
  for (const o of engineObjects)
182
187
  o.parent || o.updateTransforms();
183
188
 
184
- inputUpdate();
185
- pluginUpdateList.forEach(f=>f());
189
+ // do post update
186
190
  debugUpdate();
187
191
  gameUpdatePost();
188
192
  inputUpdatePost();
@@ -199,12 +203,13 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
199
203
  }
200
204
 
201
205
  // update multiple frames if necessary in case of slow framerate
202
- for (;frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
206
+ for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
203
207
  {
204
208
  // increment frame and update time
205
209
  time = frame++ / frameRate;
206
210
 
207
211
  // update game and objects
212
+ wasUpdated = true;
208
213
  updateCanvas();
209
214
  inputUpdate();
210
215
  gameUpdate();
@@ -215,7 +220,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
215
220
  debugUpdate();
216
221
  gameUpdatePost();
217
222
  inputUpdatePost();
218
-
219
223
  if (debugVideoCaptureIsActive())
220
224
  renderFrame();
221
225
  }
@@ -232,6 +236,10 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
232
236
  {
233
237
  if (headlessMode) return;
234
238
 
239
+ // canvas must be updated before rendering
240
+ if (!wasUpdated)
241
+ updateCanvas();
242
+
235
243
  // render sort then render while removing destroyed objects
236
244
  enginePreRender();
237
245
  gameRender();
@@ -391,7 +399,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
391
399
  promises.push(new Promise(resolve =>
392
400
  {
393
401
  let t = 0;
394
- console.log(`${engineName} Engine v${engineVersion}`);
402
+ LOG(`${engineName} Engine v${engineVersion}`);
395
403
  updateSplash();
396
404
  function updateSplash()
397
405
  {
@@ -690,6 +698,7 @@ const debugMedals = 0;
690
698
 
691
699
  // debug commands are automatically removed from the final build
692
700
  function ASSERT (){}
701
+ function LOG (){}
693
702
  function debugInit (){}
694
703
  function debugUpdate (){}
695
704
  function debugRender (){}
@@ -927,7 +936,12 @@ function wave(frequency=1, amplitude=1, t=time, offset=0)
927
936
  * @param {number} t - time in seconds
928
937
  * @return {string}
929
938
  * @memberof Utilities */
930
- function formatTime(t) { return (t/60|0) + ':' + (t%60<10?'0':'') + (t%60|0); }
939
+ function formatTime(t)
940
+ {
941
+ const sign = t < 0 ? '-' : '';
942
+ t = abs(t)|0;
943
+ return sign + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
944
+ }
931
945
 
932
946
  /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
933
947
  * @param {string} url - URL of JSON file
@@ -1263,11 +1277,7 @@ class Vector2
1263
1277
  direction()
1264
1278
  { return abs(this.x) > abs(this.y) ? this.x < 0 ? 3 : 1 : this.y < 0 ? 2 : 0; }
1265
1279
 
1266
- /** Returns a copy of this vector that has been inverted
1267
- * @return {Vector2} */
1268
- invert() { return new Vector2(this.y, -this.x); }
1269
-
1270
- /** Returns a copy of this vector absolute values
1280
+ /** Returns a copy of this vector with absolute values
1271
1281
  * @return {Vector2} */
1272
1282
  abs() { return new Vector2(abs(this.x), abs(this.y)); }
1273
1283
 
@@ -1309,13 +1319,10 @@ class Vector2
1309
1319
  toString(digits=3)
1310
1320
  {
1311
1321
  ASSERT_NUMBER_VALID(digits);
1312
- if (debug)
1313
- {
1314
- if (this.isValid())
1315
- return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`;
1316
- else
1317
- return `(${this.x}, ${this.y})`;
1318
- }
1322
+ if (this.isValid())
1323
+ return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`;
1324
+ else
1325
+ return `(${this.x}, ${this.y})`;
1319
1326
  }
1320
1327
 
1321
1328
  /** Checks if this is a valid vector
@@ -1704,7 +1711,7 @@ class Timer
1704
1711
 
1705
1712
  /** Returns this timer expressed as a string
1706
1713
  * @return {string} */
1707
- toString() { if (debug) { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }}
1714
+ toString() { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
1708
1715
 
1709
1716
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1710
1717
  * @return {number} */
@@ -1932,6 +1939,12 @@ let touchGamepadEnable = false;
1932
1939
  * @memberof Settings */
1933
1940
  let touchGamepadAnalog = true;
1934
1941
 
1942
+ /** Number of buttons on touch gamepad
1943
+ * @type {number}
1944
+ * @default
1945
+ * @memberof Settings */
1946
+ let touchGamepadButtonCount = 4;
1947
+
1935
1948
  /** Size of virtual gamepad for touch devices in pixels
1936
1949
  * @type {number}
1937
1950
  * @default
@@ -3962,7 +3975,7 @@ function inputInit()
3962
3975
  function onMouseLeave()
3963
3976
  {
3964
3977
  // set mouse position and delta when leaving canvas
3965
- mousePosScreen = vec2(Infinity);
3978
+ mousePosScreen = vec2(-1);
3966
3979
  mouseDeltaScreen = vec2(0);
3967
3980
  }
3968
3981
  }
@@ -4017,10 +4030,12 @@ function gamepadsUpdate()
4017
4030
  const data = inputData[1] || (inputData[1] = []);
4018
4031
  for (let i=10; i--;)
4019
4032
  {
4020
- const j = i === 3 ? 2 : i === 2 ? 3 : i; // fix button locations
4021
- const wasDown = gamepadIsDown(j,0);
4022
- data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
4033
+ const wasDown = gamepadIsDown(i,0);
4034
+ data[i] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
4023
4035
  }
4036
+
4037
+ // disable normal gamepads when touch gamepad is active
4038
+ return;
4024
4039
  }
4025
4040
 
4026
4041
  // return if gamepads are disabled or not supported
@@ -4095,6 +4110,15 @@ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
4095
4110
  // touch gamepad internal variables
4096
4111
  let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
4097
4112
 
4113
+ function touchGamepadButtonCenter()
4114
+ {
4115
+ // draw right face buttons
4116
+ let center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
4117
+ if (touchGamepadButtonCount <= 2)
4118
+ center.x += touchGamepadSize/2;
4119
+ return center;
4120
+ }
4121
+
4098
4122
  // enable touch input mouse passthrough
4099
4123
  function touchInputInit()
4100
4124
  {
@@ -4142,8 +4166,8 @@ function touchInputInit()
4142
4166
  // set was touching
4143
4167
  wasTouching = touching;
4144
4168
 
4145
- // prevent default handling like copy and magnifier lens
4146
- if (inputPreventDefault && document.hasFocus()) // allow document to get focus
4169
+ // prevent default handling like copy, magnifier lens, and scrolling
4170
+ if (inputPreventDefault && document.hasFocus() && e.cancelable)
4147
4171
  e.preventDefault();
4148
4172
 
4149
4173
  // must return true so the document will get focus
@@ -4172,25 +4196,36 @@ function touchInputInit()
4172
4196
 
4173
4197
  // get center of left and right sides
4174
4198
  const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
4175
- const buttonCenter = mainCanvasSize.subtract(vec2(touchGamepadSize, touchGamepadSize));
4199
+ const buttonCenter = touchGamepadButtonCenter();
4176
4200
  const startCenter = mainCanvasSize.scale(.5);
4177
4201
 
4178
4202
  // check each touch point
4179
4203
  for (const touch of e.touches)
4180
4204
  {
4181
4205
  const touchPos = mouseEventToScreen(vec2(touch.clientX, touch.clientY));
4182
- if (touchPos.distance(stickCenter) < touchGamepadSize)
4206
+ if (stickCenter.distance(touchPos) < touchGamepadSize)
4183
4207
  {
4184
4208
  // virtual analog stick
4185
4209
  touchGamepadStick = touchPos.subtract(stickCenter).scale(2/touchGamepadSize).clampLength();
4186
4210
  }
4187
- else if (touchPos.distance(buttonCenter) < touchGamepadSize)
4211
+ else if (buttonCenter.distance(touchPos) < touchGamepadSize)
4188
4212
  {
4189
4213
  // virtual face buttons
4190
- const button = touchPos.subtract(buttonCenter).direction();
4191
- touchGamepadButtons[button] = 1;
4214
+ let button = buttonCenter.subtract(touchPos).direction();
4215
+ button = mod(button+2, 4);
4216
+ if (touchGamepadButtonCount === 1)
4217
+ button = 0;
4218
+ else if (touchGamepadButtonCount === 2)
4219
+ {
4220
+ const delta = buttonCenter.subtract(touchPos);
4221
+ button = -delta.x < delta.y ? 1 : 0;
4222
+ }
4223
+ // fix button locations (swap 2 and 3 to match gamepad layout)
4224
+ button = button === 3 ? 2 : button === 2 ? 3 : button;
4225
+ if (button < touchGamepadButtonCount)
4226
+ touchGamepadButtons[button] = 1;
4192
4227
  }
4193
- else if (touchPos.distance(startCenter) < touchGamepadSize && !wasTouching)
4228
+ else if (startCenter.distance(touchPos) < touchGamepadSize && !wasTouching)
4194
4229
  {
4195
4230
  // virtual start button in center
4196
4231
  touchGamepadButtons[9] = 1;
@@ -4221,11 +4256,10 @@ function touchGamepadRender()
4221
4256
  // draw left analog stick
4222
4257
  context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
4223
4258
  context.beginPath();
4224
-
4225
- const leftCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
4259
+ const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
4226
4260
  if (touchGamepadAnalog) // draw circle shaped gamepad
4227
4261
  {
4228
- context.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
4262
+ context.arc(stickCenter.x, stickCenter.y, touchGamepadSize/2, 0, 9);
4229
4263
  context.fill();
4230
4264
  context.stroke();
4231
4265
  }
@@ -4234,21 +4268,27 @@ function touchGamepadRender()
4234
4268
  for (let i=10; i--;)
4235
4269
  {
4236
4270
  const angle = i*PI/4;
4237
- context.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
4238
- i%2 && context.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
4271
+ context.arc(stickCenter.x, stickCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
4272
+ i%2 && context.arc(stickCenter.x, stickCenter.y, touchGamepadSize*.33, angle, angle);
4239
4273
  i===1 && context.fill();
4240
4274
  }
4241
4275
  context.stroke();
4242
4276
  }
4243
4277
 
4244
4278
  // draw right face buttons
4245
- const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
4246
- for (let i=4; i--;)
4247
- {
4248
- const pos = rightCenter.add(vec2().setDirection(i, touchGamepadSize/2));
4249
- context.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
4279
+ const buttonCenter = touchGamepadButtonCenter();
4280
+ const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
4281
+ for (let i=0; i<touchGamepadButtonCount; i++)
4282
+ {
4283
+ let j = mod(i-1, 4);
4284
+ let button = touchGamepadButtonCount > 2 ?
4285
+ j : min(j, touchGamepadButtonCount-1);
4286
+ // fix button locations (swap 2 and 3 to match gamepad layout)
4287
+ button = button === 3 ? 2 : button === 2 ? 3 : button;
4288
+ const pos = buttonCenter.add(vec2().setDirection(j, touchGamepadSize/2));
4289
+ context.fillStyle = touchGamepadButtons[button] ? '#fff' : '#000';
4250
4290
  context.beginPath();
4251
- context.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
4291
+ context.arc(pos.x, pos.y, buttonSize, 0,9);
4252
4292
  context.fill();
4253
4293
  context.stroke();
4254
4294
  }
@@ -5038,6 +5078,12 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
5038
5078
  const tileLayer = new TileCollisionLayer(vec2(), levelSize, tileInfo, layerRenderOrder);
5039
5079
  tileLayers[layerIndex] = tileLayer;
5040
5080
 
5081
+ // apply layer color
5082
+ const layerColor = dataLayer.color || WHITE;
5083
+ if (dataLayer.tintcolor)
5084
+ layerColor.setHex(dataLayer.tintcolor);
5085
+ ASSERT(isColor(layerColor), 'layer color is not a color');
5086
+
5041
5087
  for (let x=levelSize.x; x--;)
5042
5088
  for (let y=levelSize.y; y--;)
5043
5089
  {
@@ -5045,7 +5091,7 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
5045
5091
  const data = dataLayer.data[x + y*levelSize.x];
5046
5092
  if (data)
5047
5093
  {
5048
- const layerData = new TileLayerData(data-1);
5094
+ const layerData = new TileLayerData(data-1, 0, false, layerColor);
5049
5095
  tileLayer.setData(pos, layerData);
5050
5096
 
5051
5097
  // set collision for top layer
@@ -6939,7 +6985,7 @@ class NewgroundsPlugin
6939
6985
  // get medals
6940
6986
  const medalsResult = this.call('Medal.getList');
6941
6987
  this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
6942
- debugMedals && console.log(this.medals);
6988
+ debugMedals && LOG(this.medals);
6943
6989
  for (const newgroundsMedal of this.medals)
6944
6990
  {
6945
6991
  const medal = medals[newgroundsMedal['id']];
@@ -6962,7 +7008,7 @@ class NewgroundsPlugin
6962
7008
  // get scoreboards
6963
7009
  const scoreboardResult = this.call('ScoreBoard.getBoards');
6964
7010
  this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
6965
- debugMedals && console.log(this.scoreboards);
7011
+ debugMedals && LOG(this.scoreboards);
6966
7012
 
6967
7013
  // keep the session alive with a ping every minute
6968
7014
  const keepAliveMS = 60 * 1e3;
@@ -7031,10 +7077,10 @@ class NewgroundsPlugin
7031
7077
  try { xmlHttp.send(formData); }
7032
7078
  catch(e)
7033
7079
  {
7034
- debugMedals && console.log('newgrounds call failed', e);
7080
+ debugMedals && LOG('newgrounds call failed', e);
7035
7081
  return;
7036
7082
  }
7037
- debugMedals && console.log(xmlHttp.responseText);
7083
+ debugMedals && LOG(xmlHttp.responseText);
7038
7084
  return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
7039
7085
  }
7040
7086
  }
@@ -7604,7 +7650,7 @@ class UIObject
7604
7650
  const mouseDown = mouseIsDown(0);
7605
7651
  const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
7606
7652
  if (!uiSystem.hoverObject)
7607
- if (mousePress || !mouseDown || isActive)
7653
+ if (mousePress || isActive || (!mouseDown && !isTouchDevice))
7608
7654
  {
7609
7655
  const size = this.size.add(vec2(isTouchDevice && this.extraTouchSize || 0));
7610
7656
  if (isOverlapping(this.pos, size, mousePosScreen))
@@ -9631,7 +9677,7 @@ async function box2dInit()
9631
9677
  }
9632
9678
  function box2dRender()
9633
9679
  {
9634
- if (box2dDebug || debugPhysics && debugOverlay)
9680
+ if (box2dDebug || debugPhysics)
9635
9681
  box2d.world.DrawDebugData();
9636
9682
  }
9637
9683
 
@@ -53,7 +53,7 @@ Theme:
53
53
 
54
54
  class ExampleInfo
55
55
  {
56
- constructor(name, filename, description='', largeExample=false)
56
+ constructor(name, filename, description='', largeExample=false, tags='')
57
57
  {
58
58
  if (filename && !largeExample)
59
59
  filename = 'shorts/' + filename;
@@ -61,62 +61,66 @@ class ExampleInfo
61
61
  this.filename = filename;
62
62
  this.description = description;
63
63
  this.largeExample = largeExample;
64
+ this.tags = tags;
64
65
  this.isHeading = !filename;
65
66
  this.text = this.name;
66
67
  if (this.description)
67
68
  this.text += ' - ' + this.description;
69
+ this.selectText = this.text;
70
+ if (this.tags)
71
+ this.selectText += ' (' + this.tags + ')';
68
72
  }
69
73
  }
70
74
 
71
75
  const exampleList =
72
76
  [
73
77
  new ExampleInfo('--- SHORT EXAMPLES ---'),
74
- new ExampleInfo('Hello World', 'helloWorld.js', 'Minimal starter example'),
75
- new ExampleInfo('Texture', 'texture.js', 'Texture display and manipulation'),
76
- new ExampleInfo('Animation', 'animation.js', 'Sprite animation system'),
77
- new ExampleInfo('Shapes', 'shapes.js', 'Draw geometric shapes and primitives'),
78
- new ExampleInfo('Colors', 'colors.js', 'Color manipulation and HSL'),
79
- new ExampleInfo('Starfield', 'starfield.js', 'Animated parallax starfield'),
80
- new ExampleInfo('Sprite Atlas', 'spriteAtlas.js', 'Sprite atlas and tile rendering'),
81
- new ExampleInfo('Blending', 'blending.js', 'Additive blending and transparency'),
82
- new ExampleInfo('Tile Layer', 'tileLayer.js', 'Tile layer rendering system'),
83
- new ExampleInfo('Particles', 'particles.js', 'Particle system'),
84
- new ExampleInfo('Timers', 'timers.js', 'Timer objects and UI'),
85
- new ExampleInfo('Sound Effects', 'sound.js', 'ZzFX sound effect generator'),
86
- new ExampleInfo('Music', 'music.js', 'Music playback and controls'),
87
- new ExampleInfo('Font Image', 'systemFont.js', 'Bitmap font system with built-in system font'),
88
- new ExampleInfo('Parallax', 'parallax.js', 'Parallax scrolling mountains'),
89
- new ExampleInfo('Maze Generator', 'maze.js', 'Procedural maze generation'),
90
- new ExampleInfo('Medals', 'medals.js', 'Achievement system'),
91
- new ExampleInfo('Piano', 'piano.js', 'Interactive piano keyboard'),
92
- new ExampleInfo('Clock', 'clock.js', 'Animated analog clock'),
78
+ new ExampleInfo('Hello World', 'helloWorld.js', 'Minimal starter example', false, 'beginner, gradient, text, tiles'),
79
+ new ExampleInfo('Texture', 'texture.js', 'Texture display and manipulation', false, 'sprites, loading, tiles'),
80
+ new ExampleInfo('Animation', 'animation.js', 'Sprite animation system', false, 'frames, loop, tiles, sprites'),
81
+ new ExampleInfo('Shapes', 'shapes.js', 'Draw geometric shapes and primitives', false, 'circle, ellipses, rectangles, polygon, lines'),
82
+ new ExampleInfo('Colors', 'colors.js', 'Color manipulation and HSL', false, 'hue, saturation, blending, rectangles'),
83
+ new ExampleInfo('Starfield', 'starfield.js', 'Animated parallax starfield', false, 'space, movement, depth, rectangles'),
84
+ new ExampleInfo('Sprite Atlas', 'spriteAtlas.js', 'Sprite atlas and tile rendering', false, 'sheet, frames, tiles'),
85
+ new ExampleInfo('Blending', 'blending.js', 'Additive blending and transparency', false, 'alpha, color, tiles, smooth'),
86
+ new ExampleInfo('Tile Layer', 'tileLayer.js', 'Tile layer rendering system', false, 'level, map, grid, particles'),
87
+ new ExampleInfo('Particles', 'particles.js', 'Particle system', false, 'effects, emitter, physics, fire, smoke'),
88
+ new ExampleInfo('Timers', 'timers.js', 'Timer objects and UI', false, 'delay, interval, slider'),
89
+ new ExampleInfo('Sound Effects', 'sound.js', 'ZzFX sound effect generator', false, 'audio, volume, ui'),
90
+ new ExampleInfo('Music', 'music.js', 'Music playback and controls', false, 'audio, streaming, volume, ui'),
91
+ new ExampleInfo('Font Image', 'systemFont.js', 'Bitmap font system with built-in system font', false, 'text, characters'),
92
+ new ExampleInfo('Parallax', 'parallax.js', 'Parallax scrolling mountains', false, 'generative, canvas, background'),
93
+ new ExampleInfo('Maze Generator', 'maze.js', 'Procedural maze generation', false, 'generative, level, tiles, map, grid'),
94
+ new ExampleInfo('Medals', 'medals.js', 'Achievement system', false, 'unlock, progress, newgrounds'),
95
+ new ExampleInfo('Piano', 'piano.js', 'Interactive piano keyboard', false, 'music, notes, ui, instrument'),
96
+ new ExampleInfo('Clock', 'clock.js', 'Animated analog clock', false, 'time, rotation, lines, rectangles'),
93
97
  new ExampleInfo('--- MINI GAMES ---'),
94
- new ExampleInfo('Pong Game', 'pongGame.js', 'Classic paddle ball bouncing'),
95
- new ExampleInfo('Platformer Game', 'platformer.js', 'Jump and run side view'),
96
- new ExampleInfo('Top Down Game', 'topDown.js', 'Top-down style camera'),
97
- new ExampleInfo('Tilted View Game', 'tiltedView.js', 'Pseudo-3D oblique view'),
98
- new ExampleInfo('Flappy Game', 'flappyGame.js', 'Flappy bird style game'),
99
- new ExampleInfo('Lander Game', 'landerGame.js', 'Lunar lander style game'),
100
- new ExampleInfo('Hill Glide Game', 'hillGlideGame.js', 'Tiny wings style sliding game'),
101
- new ExampleInfo('Sliding Puzzle', 'slidingPuzzle.js', '15 tile sliding puzzle'),
102
- new ExampleInfo('Space Game', 'spaceGame.js', 'Spaceship shooter with parallax'),
103
- new ExampleInfo('Raycasting 3D Game', 'raycasting.js', 'Wolfenstein-style raycasting'),
98
+ new ExampleInfo('Pong Game', 'pongGame.js', 'Classic paddle ball bouncing', false, 'objects, collision'),
99
+ new ExampleInfo('Platformer Game', 'platformer.js', 'Jump and run side view', false, 'objects, gravity, level, tiles, camera'),
100
+ new ExampleInfo('Top Down Game', 'topDown.js', 'Top-down style camera', false, 'objects, movement, exploration'),
101
+ new ExampleInfo('Tilted View Game', 'tiltedView.js', 'Pseudo-3D oblique view', false, 'isometric, depth'),
102
+ new ExampleInfo('Flappy Game', 'flappyGame.js', 'Flappy bird style game', false, 'objects, obstacles'),
103
+ new ExampleInfo('Lander Game', 'landerGame.js', 'Lunar lander style game', false, 'objects, physics'),
104
+ new ExampleInfo('Hill Glide Game', 'hillGlideGame.js', 'Tiny wings style sliding game', false, 'objects, physics, speed'),
105
+ new ExampleInfo('Sliding Puzzle', 'slidingPuzzle.js', '15 tile sliding puzzle', false, 'objects, numbers, ui'),
106
+ new ExampleInfo('Space Game', 'spaceGame.js', 'Spaceship shooter with parallax', false, 'objects, weapons, stars, camera, rotation'),
107
+ new ExampleInfo('Raycasting 3D Game', 'raycasting.js', 'Pseudo 3D raycasting demo', false, '3D, maze, camera'),
104
108
  new ExampleInfo('--- PLUGINS ---'),
105
- new ExampleInfo('Box2d Demo', 'box2d.js', 'Box2D physics plugin'),
106
- new ExampleInfo('Box2d Car', 'box2dCar.js', 'Drivable car with Box2D physics'),
107
- new ExampleInfo('Post Processing', 'postProcess.js', 'Shader effects and filters'),
108
- new ExampleInfo('UI System Plugin', 'uiSystem.js', 'Buttons, sliders, and checkboxes'),
109
- new ExampleInfo('Nine Slice', 'nineSlice.js', 'Scalable UI panels'),
109
+ new ExampleInfo('Box2d Demo', 'box2d.js', 'Box2D physics plugin', false, 'objects, mouse'),
110
+ new ExampleInfo('Box2d Car', 'box2dCar.js', 'Drivable car with Box2D physics', false, 'objects, vehicle, suspension, wheels'),
111
+ new ExampleInfo('Post Processing', 'postProcess.js', 'Shader effects and filters', false, 'webgl, visual, effect'),
112
+ new ExampleInfo('UI System Plugin', 'uiSystem.js', 'Buttons, sliders, and checkboxes', false, 'objects, widgets, interactive'),
113
+ new ExampleInfo('Nine Slice', 'nineSlice.js', 'Scalable UI panels', false, 'three slice, stretch, corners, text, tiles'),
110
114
  new ExampleInfo('--- FULL EXAMPLES ---'),
111
- new ExampleInfo('Starter', 'starter', 'Clean project template', true),
112
- new ExampleInfo('Breakout Tutorial', 'breakoutTutorial', 'Step-by-step breakout tutorial', true),
113
- new ExampleInfo('Breakout Game', 'breakout', 'Complete breakout game', true),
114
- new ExampleInfo('Platforming Game', 'platformer', 'Full platformer with levels', true),
115
- new ExampleInfo('Puzzle Game', 'puzzle', 'Match-3 puzzle game', true),
116
- new ExampleInfo('Stress Test', 'stress', 'Performance and music test', true),
117
- new ExampleInfo('Box2D Plugin', 'box2d', 'Full Box2D physics demo', true),
118
- new ExampleInfo('HTML Menus', 'htmlMenu', 'HTML UI integration', true),
119
- new ExampleInfo('UI System Plugin', 'uiSystem', 'Complete UI system demo', true),
115
+ new ExampleInfo('Starter', 'starter', 'Clean project template', true, 'base, empty, particles'),
116
+ new ExampleInfo('Breakout Tutorial', 'breakoutTutorial', 'Step-by-step breakout game tutorial', true, 'objects, physics, score'),
117
+ new ExampleInfo('Breakout Game', 'breakout', 'Complete breakout game', true, 'objects, physics, score'),
118
+ new ExampleInfo('Platforming Game', 'platformer', 'Platformer with level loading', true, 'jump, world, tiles, pixel art, sprites'),
119
+ new ExampleInfo('Puzzle Game', 'puzzle', 'Match 3 style puzzle game', true, 'match, swap, sprites'),
120
+ new ExampleInfo('Stress Test', 'stress', 'Performance and music test', true, 'optimization, tiles, sprites'),
121
+ new ExampleInfo('Box2D Plugin', 'box2d', 'Full Box2D physics demo', true, 'objects, bodies, joints'),
122
+ new ExampleInfo('HTML Menus', 'htmlMenu', 'HTML UI integration', true, 'web, browser, overlay, button, slider, textbox'),
123
+ new ExampleInfo('UI System Plugin', 'uiSystem', 'Complete UI system demo', true, 'menu, overlay, button, slider, checkbox'),
120
124
  ];
121
125
 
122
126
  ///////////////////////////////////////////////////////////////////////////////
@@ -313,9 +317,10 @@ function filterExamples(reset=0)
313
317
  if (!example.filename)
314
318
  continue;
315
319
 
316
- // Check if search term matches name or description
320
+ // Check if search term matches name, description, or tags
317
321
  if (example.name.toLowerCase().includes(searchTerm) ||
318
- example.description.toLowerCase().includes(searchTerm))
322
+ example.description.toLowerCase().includes(searchTerm) ||
323
+ example.tags.toLowerCase().includes(searchTerm))
319
324
  matchingIndices.push(i);
320
325
  }
321
326
 
@@ -345,7 +350,7 @@ function filterExamples(reset=0)
345
350
  else if (matchingIndices.includes(i))
346
351
  {
347
352
  // add matching example
348
- const o = new Option(example.text);
353
+ const o = new Option(example.selectText);
349
354
  o.originalIndex = i;
350
355
  selectExample.add(o);
351
356
  }
@@ -22,10 +22,9 @@ const {vec2, hsl, Timer} = LJS;
22
22
  export class Character extends GameObjects.GameObject
23
23
  {
24
24
  constructor(pos)
25
- {
25
+ {
26
26
  super(pos, vec2(.6,.95));
27
27
 
28
- this.weapon = new GameObjects.Weapon(pos, this);
29
28
  this.lastPos = pos;
30
29
  this.groundTimer = new Timer;
31
30
  this.jumpTimer = new Timer;
@@ -41,6 +40,11 @@ export class Character extends GameObjects.GameObject
41
40
  this.health = 1;
42
41
  this.climbingLadder = false;
43
42
  this.setCollision(true,false);
43
+
44
+ // attach weapon
45
+ this.weapon = new GameObjects.Weapon(pos);
46
+ this.weapon.renderOrder = this.renderOrder + 1;
47
+ this.addChild(this.weapon, vec2(this.size.x,0));
44
48
  }
45
49
 
46
50
  update()
@@ -50,7 +50,7 @@ function loadLevelData()
50
50
  {
51
51
  // load the level data
52
52
  const tileMapData = Game.gameLevelData;
53
- tileLayers = LJS.tileCollisionLoad(tileMapData, tile(0,16,1));
53
+ tileLayers = LJS.tileCollisionLoad(tileMapData, tile(0,16,1), 0, 1);
54
54
  levelSize = tileLayers[0].size;
55
55
  playerStartPos = vec2(0, levelSize.y); // default player start
56
56
  foregroundTileLayer = tileLayers[0];
@@ -101,10 +101,14 @@ export class Coin extends LJS.EngineObject
101
101
 
102
102
  render()
103
103
  {
104
+ // side of coin
105
+ LJS.drawTile(this.pos, vec2(.1, .6), 0, this.color);
106
+
104
107
  // make it appear to spin
105
108
  const t = LJS.time+this.pos.x/4+this.pos.y/4;
106
- LJS.drawTile(this.pos, vec2(.1, .6), 0, this.color); // edge of coin
107
- LJS.drawTile(this.pos, vec2(.5+.5*Math.sin(t*2*LJS.PI), 1), this.tileInfo, this.color);
109
+ const spinSize = vec2(.5+.5*Math.sin(t*2*LJS.PI), 1);
110
+ if (spinSize.x > .1)
111
+ LJS.drawTile(this.pos, spinSize, this.tileInfo, this.color);
108
112
  }
109
113
 
110
114
  update()
@@ -227,7 +231,7 @@ export class Grenade extends GameObject
227
231
 
228
232
  export class Weapon extends LJS.EngineObject
229
233
  {
230
- constructor(pos, parent)
234
+ constructor(pos)
231
235
  {
232
236
  super(pos, vec2(.6), Game.spriteAtlas.gun);
233
237
 
@@ -238,10 +242,8 @@ export class Weapon extends LJS.EngineObject
238
242
  this.damage = 1;
239
243
 
240
244
  // prepare to fire
241
- this.renderOrder = parent.renderOrder + 1;
242
245
  this.fireTimeBuffer = this.localAngle = 0;
243
246
  this.recoilTimer = new Timer;
244
- parent.addChild(this, vec2(.6,0));
245
247
 
246
248
  // shell effect
247
249
  this.addChild(this.shellEmitter = new LJS.ParticleEmitter(
@@ -1,4 +1,4 @@
1
- // use smoother textures
1
+ // use smooth blending
2
2
  setCanvasPixelated(false);
3
3
  setTilesPixelated(false);
4
4
 
@@ -4,14 +4,38 @@ class Player extends EngineObject
4
4
  {
5
5
  super(pos, vec2(1,2), tile(8), 0, RED);
6
6
  this.setCollision(); // make object collide
7
+ this.angleDamping = .95;
7
8
  }
8
9
 
9
10
  update()
10
11
  {
11
12
  super.update();
13
+
14
+ // space ship controls
15
+ const moveInput = keyDirection();
16
+ this.applyAngularAcceleration(moveInput.x * .002);
17
+ this.applyAcceleration(vec2().setAngle(this.angle, moveInput.y*.002));
18
+ }
19
+
20
+ // explode when it collides
21
+ collideWithObject()
22
+ {
23
+ // don't explode if player is almost still and not tilted
24
+ if (abs(this.angle) < .2)
25
+ if (this.velocity.length() < .05)
26
+ return true;
12
27
 
13
- // apply movement controls
14
- this.applyAcceleration(keyDirection().scale(.002));
28
+ this.destroy();
29
+ new ParticleEmitter(
30
+ this.pos, 0, // pos, angle
31
+ 1, .1, 300, 3.14, // emitSize, emitTime, emitRate, emitCone
32
+ 0, // tileInfo
33
+ rgb(1,.5,.1), rgb(1,.1,.1), // colorStartA, colorStartB
34
+ rgb(1,.5,.1,0), rgb(1,.1,.1,0), // colorEndA, colorEndB
35
+ .7, .8, .2, .2, .05, // time, sizeStart, sizeEnd, speed, angleSpeed
36
+ .9, 1, -.2, 3.14, .05, // damp, angleDamp, gravity, particleCone, fade
37
+ 1, 0, 1 // randomness, collide, additive
38
+ );
15
39
  }
16
40
  }
17
41