littlejsengine 1.18.2 → 1.18.7

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 (172) hide show
  1. package/COPYRIGHT.txt +38 -0
  2. package/FAQ.md +633 -0
  3. package/LICENSE +5 -27
  4. package/README.md +19 -1
  5. package/dist/littlejs.d.ts +550 -57
  6. package/dist/littlejs.esm.js +2208 -492
  7. package/dist/littlejs.esm.min.js +1 -1
  8. package/dist/littlejs.js +2165 -480
  9. package/dist/littlejs.min.js +1 -1
  10. package/dist/littlejs.release.js +2166 -481
  11. package/package.json +8 -1
  12. package/plugins/box2d.js +36 -9
  13. package/{src/engineMedals.js → plugins/medalSystem.js} +248 -196
  14. package/plugins/pathFinder.js +758 -0
  15. package/plugins/pluginExport.js +26 -0
  16. package/plugins/tween.js +509 -0
  17. package/plugins/tweenSystem.js +509 -0
  18. package/plugins/uiSystem.js +151 -12
  19. package/src/engine.js +9 -145
  20. package/src/engineAudio.js +8 -5
  21. package/src/engineBuild.mjs +4 -1
  22. package/src/engineDebug.js +1 -1
  23. package/src/engineDraw.js +154 -5
  24. package/src/engineExport.js +17 -12
  25. package/src/engineInput.js +27 -1
  26. package/src/engineLogo.js +146 -0
  27. package/src/engineMath.js +10 -3
  28. package/src/engineObject.js +11 -5
  29. package/src/engineParticles.js +6 -3
  30. package/src/engineRelease.js +1 -1
  31. package/src/engineSettings.js +17 -47
  32. package/src/engineUtilities.js +7 -2
  33. package/src/engineWebGL.js +31 -8
  34. package/src/jsconfig.json +3 -1
  35. package/.claude/settings.local.json +0 -7
  36. package/.github/workflows/test.yml +0 -17
  37. package/AI.md +0 -172
  38. package/CLAUDE.md +0 -1
  39. package/examples/box2d/game.js +0 -192
  40. package/examples/box2d/gameObjects.js +0 -564
  41. package/examples/box2d/index.html +0 -9
  42. package/examples/box2d/scenes.js +0 -194
  43. package/examples/box2d/tiles.png +0 -0
  44. package/examples/breakout/game.js +0 -175
  45. package/examples/breakout/gameObjects.js +0 -147
  46. package/examples/breakout/index.html +0 -8
  47. package/examples/breakout/tiles.png +0 -0
  48. package/examples/breakoutTutorial/README.md +0 -521
  49. package/examples/breakoutTutorial/game.js +0 -191
  50. package/examples/breakoutTutorial/images/1.png +0 -0
  51. package/examples/breakoutTutorial/images/10.png +0 -0
  52. package/examples/breakoutTutorial/images/11.png +0 -0
  53. package/examples/breakoutTutorial/images/2.png +0 -0
  54. package/examples/breakoutTutorial/images/3.png +0 -0
  55. package/examples/breakoutTutorial/images/4.png +0 -0
  56. package/examples/breakoutTutorial/images/5.png +0 -0
  57. package/examples/breakoutTutorial/images/6.png +0 -0
  58. package/examples/breakoutTutorial/images/7.png +0 -0
  59. package/examples/breakoutTutorial/images/8.png +0 -0
  60. package/examples/breakoutTutorial/images/9.png +0 -0
  61. package/examples/breakoutTutorial/index.html +0 -8
  62. package/examples/electron/build.mjs +0 -127
  63. package/examples/electron/electron.js +0 -35
  64. package/examples/electron/game.js +0 -54
  65. package/examples/electron/index.html +0 -13
  66. package/examples/electron/package.json +0 -5
  67. package/examples/electron/tiles.png +0 -0
  68. package/examples/empty/game.js +0 -51
  69. package/examples/empty/index.html +0 -5
  70. package/examples/empty/tiles.png +0 -0
  71. package/examples/favicon.png +0 -0
  72. package/examples/games.jpg +0 -0
  73. package/examples/htmlMenu/game.js +0 -82
  74. package/examples/htmlMenu/index.html +0 -45
  75. package/examples/htmlMenu/tiles.png +0 -0
  76. package/examples/index.html +0 -800
  77. package/examples/logo.png +0 -0
  78. package/examples/logo2.png +0 -0
  79. package/examples/module/build.mjs +0 -124
  80. package/examples/module/game.js +0 -132
  81. package/examples/module/index.html +0 -10
  82. package/examples/module/tiles.png +0 -0
  83. package/examples/particles/index.html +0 -426
  84. package/examples/particles/tiles.png +0 -0
  85. package/examples/platformer/data/gameLevelData.tmx +0 -143
  86. package/examples/platformer/data/gameLevelData.tsx +0 -4
  87. package/examples/platformer/game.js +0 -147
  88. package/examples/platformer/gameCharacter.js +0 -309
  89. package/examples/platformer/gameEffects.js +0 -278
  90. package/examples/platformer/gameLevel.js +0 -205
  91. package/examples/platformer/gameLevelData.json +0 -171
  92. package/examples/platformer/gameObjects.js +0 -378
  93. package/examples/platformer/gamePlayer.js +0 -35
  94. package/examples/platformer/index.html +0 -9
  95. package/examples/platformer/tiles.png +0 -0
  96. package/examples/platformer/tilesLevel.png +0 -0
  97. package/examples/puzzle/game.js +0 -331
  98. package/examples/puzzle/index.html +0 -8
  99. package/examples/puzzle/tiles.png +0 -0
  100. package/examples/screenshot.jpg +0 -0
  101. package/examples/shorts/animation.js +0 -18
  102. package/examples/shorts/base.html +0 -54
  103. package/examples/shorts/blending.js +0 -14
  104. package/examples/shorts/box2d.js +0 -51
  105. package/examples/shorts/box2dCar.js +0 -59
  106. package/examples/shorts/box2dPool.js +0 -107
  107. package/examples/shorts/box2dTileLayer.js +0 -48
  108. package/examples/shorts/cameraDrag.js +0 -22
  109. package/examples/shorts/clock.js +0 -21
  110. package/examples/shorts/colors.js +0 -25
  111. package/examples/shorts/debugDraw.js +0 -37
  112. package/examples/shorts/empty.js +0 -30
  113. package/examples/shorts/flappyGame.js +0 -55
  114. package/examples/shorts/fontImage.js +0 -17
  115. package/examples/shorts/fps.js +0 -90
  116. package/examples/shorts/helloWorld.js +0 -11
  117. package/examples/shorts/hillGlideGame.js +0 -63
  118. package/examples/shorts/input.js +0 -64
  119. package/examples/shorts/landerGame.js +0 -57
  120. package/examples/shorts/maze.js +0 -48
  121. package/examples/shorts/medals.js +0 -51
  122. package/examples/shorts/music.js +0 -78
  123. package/examples/shorts/musicPlayer.js +0 -137
  124. package/examples/shorts/nineSlice.js +0 -40
  125. package/examples/shorts/parallax.js +0 -72
  126. package/examples/shorts/particles.js +0 -28
  127. package/examples/shorts/piano.js +0 -44
  128. package/examples/shorts/platformer.js +0 -45
  129. package/examples/shorts/pongGame.js +0 -41
  130. package/examples/shorts/postProcess.js +0 -62
  131. package/examples/shorts/sequencer.js +0 -124
  132. package/examples/shorts/shader.js +0 -29
  133. package/examples/shorts/shapes.js +0 -21
  134. package/examples/shorts/slidingPuzzle.js +0 -52
  135. package/examples/shorts/song.mp3 +0 -0
  136. package/examples/shorts/sound.js +0 -36
  137. package/examples/shorts/spaceGame.js +0 -58
  138. package/examples/shorts/spriteAtlas.js +0 -31
  139. package/examples/shorts/starfield.js +0 -15
  140. package/examples/shorts/texture.js +0 -16
  141. package/examples/shorts/tileLayer.js +0 -48
  142. package/examples/shorts/tileRaycast.js +0 -39
  143. package/examples/shorts/tiles.png +0 -0
  144. package/examples/shorts/tiltedView.js +0 -63
  145. package/examples/shorts/timers.js +0 -53
  146. package/examples/shorts/topDown.js +0 -38
  147. package/examples/shorts/uiSystem.js +0 -61
  148. package/examples/shorts/video.webm +0 -0
  149. package/examples/shorts/videoPlayer.js +0 -34
  150. package/examples/starter/build.bat +0 -7
  151. package/examples/starter/build.mjs +0 -126
  152. package/examples/starter/game.js +0 -137
  153. package/examples/starter/index.html +0 -35
  154. package/examples/starter/tiles.png +0 -0
  155. package/examples/stress/index.html +0 -173
  156. package/examples/style.css +0 -150
  157. package/examples/typescript/build.mjs +0 -60
  158. package/examples/typescript/game.js +0 -100
  159. package/examples/typescript/game.ts +0 -132
  160. package/examples/typescript/index.html +0 -10
  161. package/examples/typescript/tiles.png +0 -0
  162. package/examples/typescript/tsconfig.json +0 -17
  163. package/examples/uiSystem/game.js +0 -139
  164. package/examples/uiSystem/index.html +0 -10
  165. package/examples/uiSystem/tiles.png +0 -0
  166. package/jsconfig.json +0 -12
  167. package/plugins/desktop.ini +0 -2
  168. package/reference.md +0 -447
  169. package/test/math.test.mjs +0 -774
  170. package/test/setup.mjs +0 -22
  171. package/test/smoke.test.mjs +0 -258
  172. package/test/util.test.mjs +0 -80
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.18.2",
3
+ "version": "1.18.7",
4
4
  "description": "LittleJS - Tiny and Fast HTML5 Game Engine",
5
5
  "main": "dist/littlejs.esm.js",
6
6
  "types": "dist/littlejs.d.ts",
@@ -29,6 +29,13 @@
29
29
  "url": "https://github.com/KilledByAPixel/LittleJS/issues"
30
30
  },
31
31
  "homepage": "https://github.com/KilledByAPixel/LittleJS",
32
+ "files": [
33
+ "dist/",
34
+ "src/",
35
+ "plugins/",
36
+ "COPYRIGHT.txt",
37
+ "FAQ.md"
38
+ ],
32
39
  "scripts": {
33
40
  "build": "node src/engineBuild.mjs",
34
41
  "test": "node --test --import ./test/setup.mjs \"test/**/*.test.mjs\""
package/plugins/box2d.js CHANGED
@@ -401,7 +401,8 @@ class Box2dObject extends EngineObject
401
401
  {
402
402
  this.pos = pos;
403
403
  this.angle = angle;
404
- this.body.SetTransform(box2d.vec2dTo(pos), angle);
404
+ // box2d uses reverse angle
405
+ this.body.SetTransform(box2d.vec2dTo(pos), -angle);
405
406
  }
406
407
 
407
408
  /** Sets the position
@@ -412,7 +413,7 @@ class Box2dObject extends EngineObject
412
413
  /** Sets the angle
413
414
  * @param {number} angle */
414
415
  setAngle(angle)
415
- { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
416
+ { this.setTransform(box2d.vec2From(this.body.GetPosition()), angle); }
416
417
 
417
418
  /** Sets the linear velocity
418
419
  * @param {Vector2} velocity */
@@ -524,14 +525,29 @@ class Box2dObject extends EngineObject
524
525
  this.body.ApplyForce(box2d.vec2dTo(force), box2d.vec2dTo(pos));
525
526
  }
526
527
 
527
- /** Apply acceleration to this object
528
+ /** Apply acceleration to this object (changes velocity by acceleration,
529
+ * mass-independent — matches EngineObject.applyAcceleration semantics).
530
+ * Use applyImpulse if you want the mass-dependent velocity change
531
+ * Δv = impulse / mass, or applyForce for a Newton-style sustained force.
528
532
  * @param {Vector2} acceleration
529
533
  * @param {Vector2} [pos] */
530
534
  applyAcceleration(acceleration, pos)
531
- {
535
+ {
536
+ pos ||= this.getCenterOfMass();
537
+ this.setAwake();
538
+ const impulse = acceleration.scale(this.getMass());
539
+ this.body.ApplyLinearImpulse(box2d.vec2dTo(impulse), box2d.vec2dTo(pos));
540
+ }
541
+
542
+ /** Apply an instantaneous linear impulse. Changes velocity immediately by
543
+ * impulse / mass (so heavier bodies move less for the same impulse).
544
+ * @param {Vector2} impulse
545
+ * @param {Vector2} [pos] */
546
+ applyImpulse(impulse, pos)
547
+ {
532
548
  pos ||= this.getCenterOfMass();
533
549
  this.setAwake();
534
- this.body.ApplyLinearImpulse(box2d.vec2dTo(acceleration), box2d.vec2dTo(pos));
550
+ this.body.ApplyLinearImpulse(box2d.vec2dTo(impulse), box2d.vec2dTo(pos));
535
551
  }
536
552
 
537
553
  /** Apply torque to this object
@@ -541,13 +557,23 @@ class Box2dObject extends EngineObject
541
557
  this.setAwake();
542
558
  this.body.ApplyTorque(torque);
543
559
  }
544
-
545
- /** Apply angular acceleration to this object
560
+
561
+ /** Apply angular acceleration to this object (changes angular velocity by
562
+ * acceleration, mass-independent — matches EngineObject semantics).
546
563
  * @param {number} acceleration */
547
564
  applyAngularAcceleration(acceleration)
548
565
  {
549
566
  this.setAwake();
550
- this.body.ApplyAngularImpulse(acceleration);
567
+ this.body.ApplyAngularImpulse(acceleration * this.getInertia());
568
+ }
569
+
570
+ /** Apply an instantaneous angular impulse. Changes angular velocity by
571
+ * impulse / inertia immediately.
572
+ * @param {number} impulse */
573
+ applyAngularImpulse(impulse)
574
+ {
575
+ this.setAwake();
576
+ this.body.ApplyAngularImpulse(impulse);
551
577
  }
552
578
 
553
579
  ///////////////////////////////////////////////////////////////////////////////
@@ -793,7 +819,7 @@ class Box2dJoint
793
819
 
794
820
  /** Check if the connected bodies should collide
795
821
  * @return {boolean} */
796
- getCollideConnected() { return this.box2dJoint.getCollideConnected();}
822
+ getCollideConnected() { return this.box2dJoint.GetCollideConnected();}
797
823
 
798
824
  /** Check if either connected body is active
799
825
  * @return {boolean} */
@@ -1957,6 +1983,7 @@ async function box2dInit()
1957
1983
  {
1958
1984
  if (o.body)
1959
1985
  {
1986
+ // box2d uses reverse angle
1960
1987
  o.pos = box2d.vec2From(o.body.GetPosition());
1961
1988
  o.angle = -o.body.GetAngle();
1962
1989
  }
@@ -1,196 +1,248 @@
1
- /**
2
- * LittleJS Medal System
3
- * - Achievement/trophy system for games
4
- * - Medal class with name, description, icon, and unlock tracking
5
- * - Automatic saving to local storage
6
- * - Visual display queue with slide-in notifications
7
- * - Newgrounds API integration for online achievements
8
- * - Debug mode to unlock/reset medals during development
9
- * @namespace Medals
10
- */
11
-
12
- 'use strict';
13
-
14
- /** List of all medals
15
- * @type {Object}
16
- * @memberof Medals */
17
- const medals = {};
18
-
19
- // Engine internal variables not exposed to documentation
20
- let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
21
-
22
- ///////////////////////////////////////////////////////////////////////////////
23
-
24
- /** Initialize medals with a save name used for storage
25
- * - Call this after creating all medals
26
- * - Checks if medals are unlocked
27
- * @param {string} saveName
28
- * @memberof Medals */
29
- function medalsInit(saveName)
30
- {
31
- // check if medals are unlocked
32
- medalsSaveName = saveName;
33
- if (!debugMedals)
34
- medalsForEach(medal=> medal.unlocked = !!localStorage[medal.storageKey()]);
35
-
36
- // engine automatically renders medals
37
- engineAddPlugin(undefined, medalsRender);
38
-
39
- // plugin functions
40
- function medalsRender()
41
- {
42
- if (!medalsDisplayQueue.length) return;
43
-
44
- // update first medal in queue
45
- const medal = medalsDisplayQueue[0];
46
- const time = timeReal - medalsDisplayTimeLast;
47
- if (!medalsDisplayTimeLast)
48
- medalsDisplayTimeLast = timeReal;
49
- else if (time > medalDisplayTime)
50
- {
51
- medalsDisplayTimeLast = 0;
52
- medalsDisplayQueue.shift();
53
- }
54
- else
55
- {
56
- // slide on/off medals
57
- const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
58
- const hidePercent =
59
- time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
60
- time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
61
- medal.render(hidePercent);
62
- }
63
- }
64
- }
65
-
66
- /**
67
- * @callback MedalCallbackFunction - Function that processes a medal
68
- * @param {Medal} medal
69
- * @memberof Medals
70
- */
71
-
72
- /** Calls a function for each medal
73
- * @param {MedalCallbackFunction} callback
74
- * @memberof Medals */
75
- function medalsForEach(callback)
76
- { Object.values(medals).forEach(medal=>callback(medal)); }
77
-
78
- ///////////////////////////////////////////////////////////////////////////////
79
-
80
- /**
81
- * Medal - Tracks an unlockable medal
82
- * @memberof Medals
83
- * @example
84
- * // create a medal
85
- * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
86
- *
87
- * // initialize medals
88
- * medalsInit('Example Game');
89
- *
90
- * // unlock the medal
91
- * medal_example.unlock();
92
- */
93
- class Medal
94
- {
95
- /** Create a medal object and adds it to the list of medals
96
- * @param {number} id - The unique identifier of the medal
97
- * @param {string} name - Name of the medal
98
- * @param {string} [description] - Description of the medal
99
- * @param {string} [icon] - Icon for the medal
100
- * @param {string} [src] - Image location for the medal
101
- */
102
- constructor(id, name, description='', icon='🏆', src)
103
- {
104
- ASSERT(id >= 0 && !medals[id]);
105
-
106
- /** @property {number} - The unique identifier of the medal */
107
- this.id = id;
108
-
109
- /** @property {string} - Name of the medal */
110
- this.name = name;
111
-
112
- /** @property {string} - Description of the medal */
113
- this.description = description;
114
-
115
- /** @property {string} - Icon for the medal */
116
- this.icon = icon;
117
-
118
- /** @property {boolean} - Is the medal unlocked? */
119
- this.unlocked = false;
120
-
121
- // load the source image if provided
122
- if (src)
123
- (this.image = new Image).src = src;
124
-
125
- // add this to list of medals
126
- medals[id] = this;
127
- }
128
-
129
- /** Unlocks a medal if not already unlocked */
130
- unlock()
131
- {
132
- if (medalsPreventUnlock || this.unlocked) return;
133
-
134
- // save the medal
135
- ASSERT(medalsSaveName, 'save name must be set');
136
- localStorage[this.storageKey()] = this.unlocked = true;
137
- medalsDisplayQueue.push(this);
138
- }
139
-
140
- /** Render a medal
141
- * @param {number} [hidePercent] - How much to slide the medal off screen
142
- */
143
- render(hidePercent=0)
144
- {
145
- const context = mainContext;
146
- const width = min(medalDisplaySize.x, mainCanvas.width);
147
- const height = medalDisplaySize.y;
148
- const x = mainCanvas.width - width;
149
- const y = -height*hidePercent;
150
- const backgroundColor = hsl(0,0,.9);
151
-
152
- // draw containing rect and clip to that region
153
- context.save();
154
- context.beginPath();
155
- context.fillStyle = backgroundColor.toString();
156
- context.strokeStyle = BLACK.toString();
157
- context.lineWidth = 3;
158
- context.rect(x, y, width, height);
159
- context.fill();
160
- context.stroke();
161
- context.clip();
162
-
163
- // draw the icon
164
- const gap = vec2(.1, .05).scale(height);
165
- const medalDisplayIconSize = height - 2*gap.x;
166
- this.renderIcon(vec2(x + gap.x + medalDisplayIconSize/2, y + height/2), medalDisplayIconSize);
167
-
168
- // draw the name
169
- const nameSize = height*.5;
170
- const descriptionSize = height*.3;
171
- const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
172
- const textWidth = width - medalDisplayIconSize - 3*gap.x;
173
- drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
174
-
175
- // draw the description
176
- pos.y = y + height - gap.y*2 - descriptionSize/2;
177
- drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
178
- context.restore();
179
- }
180
-
181
- /** Render the icon for a medal
182
- * @param {Vector2} pos - Screen space position
183
- * @param {number} size - Screen space size
184
- */
185
- renderIcon(pos, size)
186
- {
187
- // draw the image or icon
188
- if (this.image)
189
- mainContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
190
- else
191
- drawTextScreen(this.icon, pos, size*.7, BLACK);
192
- }
193
-
194
- // Get local storage key used by the medal
195
- storageKey() { return medalsSaveName + '_' + this.id; }
196
- }
1
+ /**
2
+ * LittleJS Medal System
3
+ * - Achievement/trophy system for games
4
+ * - Medal class with name, description, icon, and unlock tracking
5
+ * - Automatic saving to local storage
6
+ * - Visual display queue with slide-in notifications
7
+ * - Newgrounds API integration for online achievements
8
+ * - Debug mode to unlock/reset medals during development
9
+ * @namespace Medals
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ let debugMedals = false;
15
+
16
+ ///////////////////////////////////////////////////////////////////////////////
17
+ // Medals settings
18
+
19
+ /** How long to show medals for in seconds
20
+ * @type {number}
21
+ * @default
22
+ * @memberof Settings */
23
+ let medalDisplayTime = 5;
24
+
25
+ /** How quickly to slide on/off medals in seconds
26
+ * @type {number}
27
+ * @default
28
+ * @memberof Settings */
29
+ let medalDisplaySlideTime = .5;
30
+
31
+ /** Size of medal display
32
+ * @type {Vector2}
33
+ * @default Vector2(640,80)
34
+ * @memberof Settings */
35
+ let medalDisplaySize = vec2(640, 80);
36
+
37
+ /** Set to stop medals from being unlockable (like if cheats are enabled)
38
+ * @type {boolean}
39
+ * @default
40
+ * @memberof Settings */
41
+ let medalsPreventUnlock = false;
42
+
43
+ /** List of all medals
44
+ * @type {Object}
45
+ * @memberof Medals */
46
+ const medals = {};
47
+
48
+ // Engine internal variables not exposed to documentation
49
+ let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
50
+
51
+ ///////////////////////////////////////////////////////////////////////////////
52
+
53
+ /** Initialize medals with a save name used for storage
54
+ * - Call this after creating all medals
55
+ * - Checks if medals are unlocked
56
+ * @param {string} saveName
57
+ * @memberof Medals */
58
+ function medalsInit(saveName)
59
+ {
60
+ // check if medals are unlocked
61
+ medalsSaveName = saveName;
62
+ if (!debugMedals)
63
+ medalsForEach(medal=> medal.unlocked = !!localStorage[medal.storageKey()]);
64
+
65
+ // engine automatically renders medals
66
+ engineAddPlugin(undefined, medalsRender);
67
+
68
+ // plugin functions
69
+ function medalsRender()
70
+ {
71
+ if (!medalsDisplayQueue.length) return;
72
+
73
+ // update first medal in queue
74
+ const medal = medalsDisplayQueue[0];
75
+ const time = timeReal - medalsDisplayTimeLast;
76
+ if (!medalsDisplayTimeLast)
77
+ medalsDisplayTimeLast = timeReal;
78
+ else if (time > medalDisplayTime)
79
+ {
80
+ medalsDisplayTimeLast = 0;
81
+ medalsDisplayQueue.shift();
82
+ }
83
+ else
84
+ {
85
+ // slide on/off medals
86
+ const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
87
+ const hidePercent =
88
+ time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
89
+ time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
90
+ medal.render(hidePercent);
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * @callback MedalCallbackFunction - Function that processes a medal
97
+ * @param {Medal} medal
98
+ * @memberof Medals
99
+ */
100
+
101
+ /** Calls a function for each medal
102
+ * @param {MedalCallbackFunction} callback
103
+ * @memberof Medals */
104
+ function medalsForEach(callback)
105
+ { Object.values(medals).forEach(medal=>callback(medal)); }
106
+
107
+ ///////////////////////////////////////////////////////////////////////////////
108
+
109
+ /**
110
+ * Medal - Tracks an unlockable medal
111
+ * @memberof Medals
112
+ * @example
113
+ * // create a medal
114
+ * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
115
+ *
116
+ * // initialize medals
117
+ * medalsInit('Example Game');
118
+ *
119
+ * // unlock the medal
120
+ * medal_example.unlock();
121
+ */
122
+ class Medal
123
+ {
124
+ /** Create a medal object and adds it to the list of medals
125
+ * @param {number} id - The unique identifier of the medal
126
+ * @param {string} name - Name of the medal
127
+ * @param {string} [description] - Description of the medal
128
+ * @param {string} [icon] - Icon for the medal
129
+ * @param {string} [src] - Image location for the medal
130
+ */
131
+ constructor(id, name, description='', icon='🏆', src)
132
+ {
133
+ ASSERT(id >= 0 && !medals[id]);
134
+
135
+ /** @property {number} - The unique identifier of the medal */
136
+ this.id = id;
137
+
138
+ /** @property {string} - Name of the medal */
139
+ this.name = name;
140
+
141
+ /** @property {string} - Description of the medal */
142
+ this.description = description;
143
+
144
+ /** @property {string} - Icon for the medal */
145
+ this.icon = icon;
146
+
147
+ /** @property {boolean} - Is the medal unlocked? */
148
+ this.unlocked = false;
149
+
150
+ // load the source image if provided
151
+ if (src)
152
+ (this.image = new Image).src = src;
153
+
154
+ // add this to list of medals
155
+ medals[id] = this;
156
+ }
157
+
158
+ /** Unlocks a medal if not already unlocked */
159
+ unlock()
160
+ {
161
+ if (medalsPreventUnlock || this.unlocked) return;
162
+
163
+ // save the medal
164
+ ASSERT(medalsSaveName, 'save name must be set');
165
+ localStorage[this.storageKey()] = this.unlocked = true;
166
+ medalsDisplayQueue.push(this);
167
+ }
168
+
169
+ /** Render a medal
170
+ * @param {number} [hidePercent] - How much to slide the medal off screen
171
+ */
172
+ render(hidePercent=0)
173
+ {
174
+ const context = mainContext;
175
+ const width = min(medalDisplaySize.x, mainCanvas.width);
176
+ const height = medalDisplaySize.y;
177
+ const x = mainCanvas.width - width;
178
+ const y = -height*hidePercent;
179
+ const backgroundColor = hsl(0,0,.9);
180
+
181
+ // draw containing rect and clip to that region
182
+ context.save();
183
+ context.beginPath();
184
+ context.fillStyle = backgroundColor.toString();
185
+ context.strokeStyle = BLACK.toString();
186
+ context.lineWidth = 3;
187
+ context.rect(x, y, width, height);
188
+ context.fill();
189
+ context.stroke();
190
+ context.clip();
191
+
192
+ // draw the icon
193
+ const gap = vec2(.1, .05).scale(height);
194
+ const medalDisplayIconSize = height - 2*gap.x;
195
+ this.renderIcon(vec2(x + gap.x + medalDisplayIconSize/2, y + height/2), medalDisplayIconSize);
196
+
197
+ // draw the name
198
+ const nameSize = height*.5;
199
+ const descriptionSize = height*.3;
200
+ const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
201
+ const textWidth = width - medalDisplayIconSize - 3*gap.x;
202
+ drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
203
+
204
+ // draw the description
205
+ pos.y = y + height - gap.y*2 - descriptionSize/2;
206
+ drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
207
+ context.restore();
208
+ }
209
+
210
+ /** Render the icon for a medal
211
+ * @param {Vector2} pos - Screen space position
212
+ * @param {number} size - Screen space size
213
+ */
214
+ renderIcon(pos, size)
215
+ {
216
+ // draw the image or icon
217
+ if (this.image)
218
+ mainContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
219
+ else
220
+ drawTextScreen(this.icon, pos, size*.7, BLACK);
221
+ }
222
+
223
+ // Get local storage key used by the medal
224
+ storageKey() { return medalsSaveName + '_' + this.id; }
225
+ }
226
+
227
+ ///////////////////////////////////////////////////////////////////////////////
228
+ // Medals setting setters
229
+
230
+ /** Set how long to show medals for in seconds
231
+ * @param {number} time
232
+ * @memberof Settings */
233
+ function setMedalDisplayTime(time) { medalDisplayTime = time; }
234
+
235
+ /** Set how quickly to slide on/off medals in seconds
236
+ * @param {number} time
237
+ * @memberof Settings */
238
+ function setMedalDisplaySlideTime(time) { medalDisplaySlideTime = time; }
239
+
240
+ /** Set size of medal display
241
+ * @param {Vector2} size
242
+ * @memberof Settings */
243
+ function setMedalDisplaySize(size) { medalDisplaySize = size.copy(); }
244
+
245
+ /** Set to stop medals from being unlockable
246
+ * @param {boolean} preventUnlock
247
+ * @memberof Settings */
248
+ function setMedalsPreventUnlock(preventUnlock) { medalsPreventUnlock = preventUnlock; }