littlejsengine 1.11.17 → 1.12.4

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 (62) hide show
  1. package/dist/littlejs.d.ts +112 -43
  2. package/dist/littlejs.esm.js +586 -500
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +2993 -122
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +2992 -119
  7. package/examples/box2d/game.js +70 -39
  8. package/examples/box2d/gameObjects.js +81 -77
  9. package/examples/box2d/index.html +2 -7
  10. package/examples/box2d/scenes.js +80 -90
  11. package/examples/breakout/game.js +63 -28
  12. package/examples/breakout/gameObjects.js +45 -47
  13. package/examples/breakout/index.html +1 -5
  14. package/examples/breakoutTutorial/game.js +36 -29
  15. package/examples/breakoutTutorial/index.html +1 -3
  16. package/examples/empty/game.js +5 -2
  17. package/examples/empty/index.html +1 -3
  18. package/examples/htmlMenu/game.js +24 -15
  19. package/examples/htmlMenu/index.html +5 -7
  20. package/examples/index.html +2 -3
  21. package/examples/module/game.js +32 -34
  22. package/examples/module/index.html +1 -1
  23. package/examples/particles/index.html +27 -22
  24. package/examples/platformer/game.js +71 -45
  25. package/examples/platformer/gameCharacter.js +42 -34
  26. package/examples/platformer/gameEffects.js +82 -75
  27. package/examples/platformer/gameLevel.js +67 -38
  28. package/examples/platformer/{gameLevelData.js → gameLevelData.json} +1 -11
  29. package/examples/platformer/gameObjects.js +70 -64
  30. package/examples/platformer/gamePlayer.js +12 -7
  31. package/examples/platformer/index.html +1 -9
  32. package/examples/puzzle/game.js +59 -40
  33. package/examples/puzzle/index.html +1 -3
  34. package/examples/shorts/base.html +2 -2
  35. package/examples/shorts/particles.js +1 -1
  36. package/examples/shorts/platformer.js +1 -1
  37. package/examples/shorts/tileLayer.js +5 -6
  38. package/examples/shorts/tiles.png +0 -0
  39. package/examples/starter/game.js +9 -12
  40. package/examples/starter/index.html +13 -13
  41. package/examples/stress/index.html +44 -44
  42. package/examples/typescript/build.js +17 -18
  43. package/examples/typescript/game.js +32 -34
  44. package/examples/typescript/game.ts +32 -34
  45. package/examples/typescript/index.html +1 -1
  46. package/examples/uiSystem/game.js +34 -31
  47. package/examples/uiSystem/index.html +1 -5
  48. package/package.json +1 -1
  49. package/plugins/box2d.js +6 -2
  50. package/plugins/pluginExport.js +1 -2
  51. package/reference.md +29 -27
  52. package/src/engine.js +1 -1
  53. package/src/engineBuild.js +13 -6
  54. package/src/engineDebug.js +1 -3
  55. package/src/engineExport.js +2 -6
  56. package/src/engineInput.js +3 -1
  57. package/src/engineObject.js +18 -14
  58. package/src/engineSettings.js +6 -6
  59. package/src/engineTileLayer.js +179 -96
  60. package/examples/typescript/build/dist/littlejs.esm.js +0 -4834
  61. package/examples/typescript/build/examples/typescript/build.js +0 -24
  62. package/examples/typescript/build/examples/typescript/game.js +0 -102
@@ -158,7 +158,10 @@ class EngineObject
158
158
  this.velocity.x *= this.damping;
159
159
  this.velocity.y *= this.damping;
160
160
  if (this.mass) // don't apply gravity to static objects
161
- this.velocity.y += gravity * this.gravityScale;
161
+ {
162
+ this.velocity.x += gravity.x * this.gravityScale;
163
+ this.velocity.y += gravity.y * this.gravityScale;
164
+ }
162
165
  this.pos.x += this.velocity.x;
163
166
  this.pos.y += this.velocity.y;
164
167
  this.angle += this.angleVelocity *= this.angleDamping;
@@ -173,9 +176,9 @@ class EngineObject
173
176
  if (this.groundObject)
174
177
  {
175
178
  // apply friction in local space of ground object
176
- const groundSpeed = this.groundObject != this && this.groundObject.velocity ?
177
- this.groundObject.velocity.x : 0;
178
- this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * this.friction;
179
+ const friction = max(this.friction, this.groundObject.friction);
180
+ const groundSpeed = this.groundObject.velocity ? this.groundObject.velocity.x : 0;
181
+ this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * friction;
179
182
  this.groundObject = undefined;
180
183
  //debugOverlay && debugPhysics && debugPoint(this.pos.subtract(vec2(0,this.size.y/2)), '#0f0');
181
184
  }
@@ -217,7 +220,7 @@ class EngineObject
217
220
 
218
221
  // check for collision
219
222
  const sizeBoth = this.size.add(o.size);
220
- const smallStepUp = (oldPos.y - o.pos.y)*2 > sizeBoth.y + gravity; // prefer to push up if small delta
223
+ const smallStepUp = (oldPos.y - o.pos.y)*2 > sizeBoth.y + gravity.y; // prefer to push up if small delta
221
224
  const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
222
225
  const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
223
226
  const elasticity = max(this.elasticity, o.elasticity);
@@ -279,19 +282,21 @@ class EngineObject
279
282
  if (this.collideTiles)
280
283
  {
281
284
  // check collision against tiles
282
- if (tileCollisionTest(this.pos, this.size, this))
285
+ const hitLayer = tileCollisionTest(this.pos, this.size, this)
286
+ if (hitLayer)
283
287
  {
284
288
  // if already was stuck in collision, don't do anything
285
289
  // this should not happen unless something starts in collision
286
290
  if (!tileCollisionTest(oldPos, this.size, this))
287
291
  {
288
292
  // test which side we bounced off (or both if a corner)
289
- const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
290
- const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
291
- if (isBlockedY || !isBlockedX)
293
+ const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
294
+ const blockedLayerX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
295
+ if (blockedLayerY || !blockedLayerX)
292
296
  {
293
297
  // bounce velocity
294
- this.velocity.y *= -this.elasticity;
298
+ const elasticity = max(this.elasticity, hitLayer.elasticity);
299
+ this.velocity.y *= -elasticity;
295
300
 
296
301
  if (wasMovingDown)
297
302
  {
@@ -300,9 +305,8 @@ class EngineObject
300
305
  const epsilon = .0001;
301
306
  this.pos.y = (oldPos.y-this.size.y/2|0)+this.size.y/2+epsilon;
302
307
 
303
- // set ground object to self for tile collision
304
- // TODO: rework system so tile collision is its own object
305
- this.groundObject = this;
308
+ // set ground object for tile collision
309
+ this.groundObject = hitLayer;
306
310
  }
307
311
  else
308
312
  {
@@ -311,7 +315,7 @@ class EngineObject
311
315
  this.groundObject = undefined;
312
316
  }
313
317
  }
314
- if (isBlockedX)
318
+ if (blockedLayerX)
315
319
  {
316
320
  // move to previous position and bounce
317
321
  this.pos.x = oldPos.x;
@@ -37,7 +37,7 @@ let canvasMaxSize = vec2(1920, 1080);
37
37
  * @memberof Settings */
38
38
  let canvasFixedSize = vec2();
39
39
 
40
- /** Use nearest neighbor scaling algorithm for canvas for more pixelated look
40
+ /** Use nearest neighbor canvas scaling for more pixelated look
41
41
  * - Must be set before startup to take effect
42
42
  * - If enabled sets css image-rendering:pixelated
43
43
  * @type {boolean}
@@ -147,11 +147,11 @@ let objectDefaultFriction = .8;
147
147
  * @memberof Settings */
148
148
  let objectMaxSpeed = 1;
149
149
 
150
- /** How much gravity to apply to objects along the Y axis, negative is down
151
- * @type {number}
150
+ /** How much gravity to apply to objects, negative Y is down
151
+ * @type {Vector2}
152
152
  * @default
153
153
  * @memberof Settings */
154
- let gravity = 0;
154
+ let gravity = vec2();
155
155
 
156
156
  /** Scales emit rate of particles, useful for low graphics mode (0 disables particle emitters)
157
157
  * @type {number}
@@ -377,8 +377,8 @@ function setObjectDefaultFriction(friction) { objectDefaultFriction = friction;
377
377
  * @memberof Settings */
378
378
  function setObjectMaxSpeed(speed) { objectMaxSpeed = speed; }
379
379
 
380
- /** Set how much gravity to apply to objects along the Y axis
381
- * @param {number} newGravity
380
+ /** Set how much gravity to apply to objects
381
+ * @param {Vector2} newGravity
382
382
  * @memberof Settings */
383
383
  function setGravity(newGravity) { gravity = newGravity; }
384
384
 
@@ -2,75 +2,47 @@
2
2
  * LittleJS Tile Layer System
3
3
  * - Caches arrays of tiles to off screen canvas for fast rendering
4
4
  * - Unlimited numbers of layers, allocates canvases as needed
5
- * - Interfaces with EngineObject for collision
6
- * - Collision layer is separate from visible layers
7
- * - It is recommended to have a visible layer that matches the collision
8
5
  * - Tile layers can be drawn to using their context with canvas2d
9
6
  * - Drawn directly to the main canvas without using WebGL
7
+ * - Tile layers can also have collision with EngineObjects
10
8
  * @namespace TileCollision
11
9
  */
12
10
 
13
11
  'use strict';
14
12
 
15
- /** The tile collision layer grid, use setTileCollisionData and getTileCollisionData to access
16
- * @type {Array<number>}
17
- * @memberof TileCollision */
18
- let tileCollision = [];
19
-
20
- /** Size of the tile collision layer 2d grid
21
- * @type {Vector2}
22
- * @memberof TileCollision */
23
- let tileCollisionSize = vec2();
13
+ ///////////////////////////////////////////////////////////////////////////////
14
+ // Tile Layer System
24
15
 
25
- /** Clear and initialize tile collision
26
- * @param {Vector2} size - width and height of tile collision 2d grid
16
+ /** Keep track of all tile layers with collision
17
+ * @type {Array<TileCollisionLayer>}
27
18
  * @memberof TileCollision */
28
- function initTileCollision(size)
29
- {
30
- tileCollisionSize = size;
31
- tileCollision = [];
32
- for (let i=tileCollision.length = tileCollisionSize.area(); i--;)
33
- tileCollision[i] = 0;
34
- }
35
-
36
- /** Set tile collision data for a given cell in the grid
37
- * @param {Vector2} pos
38
- * @param {number} [data]
39
- * @memberof TileCollision */
40
- function setTileCollisionData(pos, data=0)
41
- {
42
- pos.arrayCheck(tileCollisionSize) && (tileCollision[(pos.y|0)*tileCollisionSize.x+pos.x|0] = data);
43
- }
19
+ let tileCollisionLayers = [];
44
20
 
45
21
  /** Get tile collision data for a given cell in the grid
46
- * @param {Vector2} pos
47
- * @return {number}
48
- * @memberof TileCollision */
22
+ * @param {Vector2} pos
23
+ * @return {number}
24
+ * @memberof TileCollision */
49
25
  function getTileCollisionData(pos)
50
26
  {
51
- return pos.arrayCheck(tileCollisionSize) ? tileCollision[(pos.y|0)*tileCollisionSize.x+pos.x|0] : 0;
27
+ // check all tile collision layers
28
+ for (const layer of tileCollisionLayers)
29
+ if (pos.arrayCheck(layer.size))
30
+ return layer.getCollisionData(pos);
31
+ return 0;
52
32
  }
53
33
 
54
- /** Check if collision with another object should occur
34
+ /** Check if a tile layer collides with another object
55
35
  * @param {Vector2} pos
56
36
  * @param {Vector2} [size=(0,0)]
57
37
  * @param {EngineObject} [object]
58
- * @return {boolean}
38
+ * @return {TileCollisionLayer}
59
39
  * @memberof TileCollision */
60
40
  function tileCollisionTest(pos, size=vec2(), object)
61
41
  {
62
- const minX = max(pos.x - size.x/2|0, 0);
63
- const minY = max(pos.y - size.y/2|0, 0);
64
- const maxX = min(pos.x + size.x/2, tileCollisionSize.x);
65
- const maxY = min(pos.y + size.y/2, tileCollisionSize.y);
66
- for (let y = minY; y < maxY; ++y)
67
- for (let x = minX; x < maxX; ++x)
68
- {
69
- const tileData = tileCollision[y*tileCollisionSize.x+x];
70
- if (tileData && (!object || object.collideWithTile(tileData, vec2(x, y))))
71
- return true;
72
- }
73
- return false;
42
+ // check all tile collision layers
43
+ for (const layer of tileCollisionLayers)
44
+ if (layer.collisionTest(pos, size, object))
45
+ return layer;
74
46
  }
75
47
 
76
48
  /** Return the center of first tile hit, undefined if nothing was hit.
@@ -82,49 +54,17 @@ function tileCollisionTest(pos, size=vec2(), object)
82
54
  * @memberof TileCollision */
83
55
  function tileCollisionRaycast(posStart, posEnd, object)
84
56
  {
85
- // test if a ray collides with tiles from start to end
86
- // todo: a way to get the exact hit point, it must still be inside the hit tile
87
- const delta = posEnd.subtract(posStart);
88
- const totalLength = delta.length();
89
- const normalizedDelta = delta.normalize();
90
- const unit = vec2(abs(1/normalizedDelta.x), abs(1/normalizedDelta.y));
91
- const flooredPosStart = posStart.floor();
92
-
93
- // setup iteration variables
94
- let pos = flooredPosStart;
95
- let xi = unit.x * (delta.x < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1);
96
- let yi = unit.y * (delta.y < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1);
97
-
98
- while (true)
57
+ // check all tile collision layers
58
+ for (const layer of tileCollisionLayers)
99
59
  {
100
- // check for tile collision
101
- const tileData = getTileCollisionData(pos);
102
- if (tileData && (!object || object.collideWithTile(tileData, pos)))
103
- {
104
- debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
105
- debugRaycast && debugPoint(pos.add(vec2(.5)), '#ff0');
106
- return pos.add(vec2(.5));
107
- }
108
-
109
- // check if past the end
110
- if (xi > totalLength && yi > totalLength)
111
- break;
112
-
113
- // get coordinates of the next tile to check
114
- if (xi > yi)
115
- pos.y += sign(delta.y), yi += unit.y;
116
- else
117
- pos.x += sign(delta.x), xi += unit.x;
60
+ const hitPos = layer.collisionRaycast(posStart, posEnd, object)
61
+ if (hitPos)
62
+ return hitPos;
118
63
  }
119
-
120
- debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
121
64
  }
122
65
 
123
- ///////////////////////////////////////////////////////////////////////////////
124
- // Tile Layer Rendering System
125
-
126
66
  /**
127
- * Tile layer data object stores info about how to render a tile
67
+ * Tile layer data object stores info about how to draw a tile
128
68
  * @example
129
69
  * // create tile layer data with tile index 0 and random orientation and color
130
70
  * const tileIndex = 0;
@@ -156,28 +96,27 @@ class TileLayerData
156
96
  clear() { this.tile = this.direction = 0; this.mirror = false; this.color = new Color; }
157
97
  }
158
98
 
99
+ ///////////////////////////////////////////////////////////////////////////////
159
100
  /**
160
101
  * Tile Layer - cached rendering system for tile layers
161
102
  * - Each Tile layer is rendered to an off screen canvas
162
103
  * - To allow dynamic modifications, layers are rendered using canvas 2d
163
104
  * - Some devices like mobile phones are limited to 4k texture resolution
164
- * - So with 16x16 tiles this limits layers to 256x256 on mobile devices
105
+ * - For with 16x16 tiles this limits layers to 256x256 on mobile devices
165
106
  * @extends EngineObject
166
107
  * @example
167
- * // create tile collision and visible tile layer
168
- * initTileCollision(vec2(200,100));
169
- * const tileLayer = new TileLayer();
108
+ * const tileLayer = new TileLayer(vec2(), vec2(200,100));
170
109
  */
171
110
  class TileLayer extends EngineObject
172
111
  {
173
112
  /** Create a tile layer object
174
- * @param {Vector2} [position=(0,0)] - World space position
175
- * @param {Vector2} [size=tileCollisionSize] - World space size
176
- * @param {TileInfo} [tileInfo] - Tile info for layer
177
- * @param {Vector2} [scale=(1,1)] - How much to scale this layer when rendered
178
- * @param {number} [renderOrder] - Objects are sorted by renderOrder
113
+ * @param {Vector2} [position=(0,0)] - World space position
114
+ * @param {Vector2} [size=(1,1)] - World space size
115
+ * @param {TileInfo} [tileInfo] - Tile info for layer
116
+ * @param {Vector2} [scale=(1,1)] - How much to scale this layer when rendered
117
+ * @param {number} [renderOrder] - Objects are sorted by renderOrder
179
118
  */
180
- constructor(position, size=tileCollisionSize, tileInfo=tile(), scale=vec2(1), renderOrder=0)
119
+ constructor(position, size, tileInfo=tile(), scale=vec2(1), renderOrder=0)
181
120
  {
182
121
  super(position, size, tileInfo, 0, undefined, renderOrder);
183
122
 
@@ -189,6 +128,10 @@ class TileLayer extends EngineObject
189
128
  this.scale = scale;
190
129
  /** @property {boolean} - If true this layer will render to overlay canvas and appear above all objects */
191
130
  this.isOverlay = false;
131
+ // set no friction by default, applied friction is max of both objects
132
+ this.friction = 0;
133
+ // set no elasticity by default, applied elasticity is max of both objects
134
+ this.elasticity = 0;
192
135
 
193
136
  // init tile data
194
137
  this.data = [];
@@ -385,4 +328,144 @@ class TileLayer extends EngineObject
385
328
  * @param {number} [angle=0] */
386
329
  drawRect(pos, size, color, angle)
387
330
  { this.drawTile(pos, size, undefined, color, angle); }
331
+ }
332
+
333
+ ///////////////////////////////////////////////////////////////////////////////
334
+ /**
335
+ * Tile Collision Layer - a tile layer with collision
336
+ * - adds collision data and functions to TileLayer
337
+ * - there can be multiple tile collision layers
338
+ * - tile collison layers should not overlap each other
339
+ * @extends TileLayer
340
+ */
341
+ class TileCollisionLayer extends TileLayer
342
+ {
343
+ /** Create a tile layer object
344
+ * @param {Vector2} [position=(0,0)] - World space position
345
+ * @param {Vector2} [size=(0,0)] - World space size
346
+ * @param {TileInfo} [tileInfo] - Tile info for layer
347
+ * @param {number} [renderOrder] - Objects are sorted by renderOrder
348
+ */
349
+ constructor(position, size, tileInfo=tile(), renderOrder=0)
350
+ {
351
+ const scale = vec2(1); // collision layers are not scaled
352
+ super(position, size.floor(), tileInfo, scale, renderOrder);
353
+
354
+ /** @property {Array<number>} - The tile collision grid */
355
+ this.collisionData = [];
356
+ this.initCollision(this.size);
357
+
358
+ // keep track of all collision layers
359
+ tileCollisionLayers.push(this);
360
+ }
361
+
362
+ /** Destroy this collision layer */
363
+ destroy()
364
+ {
365
+ if (this.destroyed)
366
+ return;
367
+
368
+ // remove from collision layers array and destroy
369
+ const index = tileCollisionLayers.indexOf(this);
370
+ ASSERT(index >= 0, 'tile collision layer not found in array');
371
+ tileCollisionLayers.splice(index, 1);
372
+ super.destroy();
373
+ }
374
+
375
+ /** Clear and initialize tile collision to new size
376
+ * @param {Vector2} size - width and height of tile collision 2d grid */
377
+ initCollision(size)
378
+ {
379
+ this.size = size.floor();
380
+ this.collisionData = [];
381
+ this.collisionData.length = size.area();
382
+ this.collisionData.fill(0);
383
+ }
384
+
385
+ /** Set tile collision data for a given cell in the grid
386
+ * @param {Vector2} pos
387
+ * @param {number} [data] */
388
+ setCollisionData(pos, data=1)
389
+ {
390
+ const i = (pos.y|0)*this.size.x + pos.x|0;
391
+ pos.arrayCheck(this.size) && (this.collisionData[i] = data);
392
+ }
393
+
394
+ /** Get tile collision data for a given cell in the grid
395
+ * @param {Vector2} pos
396
+ * @return {number} */
397
+ getCollisionData(pos)
398
+ {
399
+ const i = (pos.y|0)*this.size.x + pos.x|0;
400
+ return pos.arrayCheck(this.size) ? this.collisionData[i] : 0;
401
+ }
402
+
403
+ /** Check if collision with another object should occur
404
+ * @param {Vector2} pos
405
+ * @param {Vector2} [size=(0,0)]
406
+ * @param {EngineObject} [object]
407
+ * @return {boolean} */
408
+ collisionTest(pos, size=vec2(), object)
409
+ {
410
+ const minX = max(pos.x - size.x/2|0, 0);
411
+ const minY = max(pos.y - size.y/2|0, 0);
412
+ const maxX = min(pos.x + size.x/2, this.size.x);
413
+ const maxY = min(pos.y + size.y/2, this.size.y);
414
+ for (let y = minY; y < maxY; ++y)
415
+ for (let x = minX; x < maxX; ++x)
416
+ {
417
+ // check if the object should collide with this tile
418
+ const tileData = this.collisionData[y*this.size.x+x];
419
+ if (tileData && (!object || object.collideWithTile(tileData, vec2(x, y))))
420
+ return true;
421
+ }
422
+ return false;
423
+ }
424
+
425
+ /** Return the center of first tile hit, undefined if nothing was hit.
426
+ * This does not return the exact intersection, but the center of the tile hit.
427
+ * @param {Vector2} posStart
428
+ * @param {Vector2} posEnd
429
+ * @param {EngineObject} [object]
430
+ * @return {Vector2} */
431
+ collisionRaycast(posStart, posEnd, object)
432
+ {
433
+ // test if a ray collides with tiles from start to end
434
+ // todo: a way to get the exact hit point, it must still be inside the hit tile
435
+ const delta = posEnd.subtract(posStart);
436
+ const totalLength = delta.length();
437
+ const normalizedDelta = delta.normalize();
438
+ const unit = vec2(abs(1/normalizedDelta.x), abs(1/normalizedDelta.y));
439
+ const flooredPosStart = posStart.floor();
440
+
441
+ // setup iteration variables
442
+ let pos = flooredPosStart;
443
+ let xi = unit.x * (delta.x < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1);
444
+ let yi = unit.y * (delta.y < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1);
445
+
446
+ // use line drawing algorithm to test for collisions
447
+ while (true)
448
+ {
449
+ // check for tile collision
450
+ const tileData = this.getCollisionData(pos);
451
+ if (tileData && (!object || object.collideWithTile(tileData, pos)))
452
+ {
453
+ debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
454
+ debugRaycast && debugPoint(pos.add(vec2(.5)), '#ff0');
455
+ return pos.add(vec2(.5));
456
+ }
457
+
458
+ // check if past the end
459
+ if (xi > totalLength && yi > totalLength)
460
+ break;
461
+
462
+ // get coordinates of next tile to check
463
+ if (xi > yi)
464
+ pos.y += sign(delta.y), yi += unit.y;
465
+ else
466
+ pos.x += sign(delta.x), xi += unit.x;
467
+ }
468
+
469
+ debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
470
+ }
388
471
  }