@guinetik/gcanvas 1.0.2 → 1.0.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 (217) hide show
  1. package/dist/gcanvas.es.js +25656 -0
  2. package/dist/gcanvas.es.min.js +1 -0
  3. package/dist/gcanvas.umd.js +1 -0
  4. package/dist/gcanvas.umd.min.js +1 -0
  5. package/package.json +23 -6
  6. package/src/game/objects/index.js +1 -0
  7. package/src/game/objects/spritesheet.js +260 -0
  8. package/src/game/ui/theme.js +6 -0
  9. package/src/io/keys.js +9 -1
  10. package/src/math/boolean.js +481 -0
  11. package/src/math/index.js +1 -0
  12. package/.github/workflows/release.yaml +0 -70
  13. package/.jshintrc +0 -4
  14. package/.vscode/settings.json +0 -22
  15. package/CLAUDE.md +0 -310
  16. package/blackhole.jpg +0 -0
  17. package/demo.png +0 -0
  18. package/demos/CNAME +0 -1
  19. package/demos/animations.html +0 -31
  20. package/demos/basic.html +0 -38
  21. package/demos/baskara.html +0 -31
  22. package/demos/bezier.html +0 -35
  23. package/demos/beziersignature.html +0 -29
  24. package/demos/blackhole.html +0 -28
  25. package/demos/blob.html +0 -35
  26. package/demos/coordinates.html +0 -698
  27. package/demos/cube3d.html +0 -23
  28. package/demos/demos.css +0 -303
  29. package/demos/dino.html +0 -42
  30. package/demos/easing.html +0 -28
  31. package/demos/events.html +0 -195
  32. package/demos/fluent.html +0 -647
  33. package/demos/fluid-simple.html +0 -22
  34. package/demos/fluid.html +0 -37
  35. package/demos/fractals.html +0 -36
  36. package/demos/gameobjects.html +0 -626
  37. package/demos/genart.html +0 -26
  38. package/demos/gendream.html +0 -26
  39. package/demos/group.html +0 -36
  40. package/demos/home.html +0 -587
  41. package/demos/index.html +0 -376
  42. package/demos/isometric.html +0 -34
  43. package/demos/js/animations.js +0 -452
  44. package/demos/js/basic.js +0 -204
  45. package/demos/js/baskara.js +0 -751
  46. package/demos/js/bezier.js +0 -692
  47. package/demos/js/beziersignature.js +0 -241
  48. package/demos/js/blackhole/accretiondisk.obj.js +0 -379
  49. package/demos/js/blackhole/blackhole.obj.js +0 -318
  50. package/demos/js/blackhole/index.js +0 -409
  51. package/demos/js/blackhole/particle.js +0 -56
  52. package/demos/js/blackhole/starfield.obj.js +0 -218
  53. package/demos/js/blob.js +0 -2276
  54. package/demos/js/coordinates.js +0 -840
  55. package/demos/js/cube3d.js +0 -789
  56. package/demos/js/dino.js +0 -1420
  57. package/demos/js/easing.js +0 -477
  58. package/demos/js/fluent.js +0 -183
  59. package/demos/js/fluid-simple.js +0 -253
  60. package/demos/js/fluid.js +0 -527
  61. package/demos/js/fractals.js +0 -931
  62. package/demos/js/fractalworker.js +0 -93
  63. package/demos/js/gameobjects.js +0 -176
  64. package/demos/js/genart.js +0 -268
  65. package/demos/js/gendream.js +0 -209
  66. package/demos/js/group.js +0 -140
  67. package/demos/js/info-toggle.js +0 -25
  68. package/demos/js/isometric.js +0 -863
  69. package/demos/js/kerr.js +0 -1556
  70. package/demos/js/lavalamp.js +0 -590
  71. package/demos/js/layout.js +0 -354
  72. package/demos/js/mondrian.js +0 -285
  73. package/demos/js/opacity.js +0 -275
  74. package/demos/js/painter.js +0 -484
  75. package/demos/js/particles-showcase.js +0 -514
  76. package/demos/js/particles.js +0 -299
  77. package/demos/js/patterns.js +0 -397
  78. package/demos/js/penrose/artifact.js +0 -69
  79. package/demos/js/penrose/blackhole.js +0 -121
  80. package/demos/js/penrose/constants.js +0 -73
  81. package/demos/js/penrose/game.js +0 -943
  82. package/demos/js/penrose/lore.js +0 -278
  83. package/demos/js/penrose/penrosescene.js +0 -892
  84. package/demos/js/penrose/ship.js +0 -216
  85. package/demos/js/penrose/sounds.js +0 -211
  86. package/demos/js/penrose/voidparticle.js +0 -55
  87. package/demos/js/penrose/voidscene.js +0 -258
  88. package/demos/js/penrose/voidship.js +0 -144
  89. package/demos/js/penrose/wormhole.js +0 -46
  90. package/demos/js/pipeline.js +0 -555
  91. package/demos/js/plane3d.js +0 -256
  92. package/demos/js/platformer.js +0 -1579
  93. package/demos/js/scene.js +0 -304
  94. package/demos/js/scenes.js +0 -320
  95. package/demos/js/schrodinger.js +0 -410
  96. package/demos/js/schwarzschild.js +0 -1023
  97. package/demos/js/shapes.js +0 -628
  98. package/demos/js/space/alien.js +0 -171
  99. package/demos/js/space/boom.js +0 -98
  100. package/demos/js/space/boss.js +0 -353
  101. package/demos/js/space/buff.js +0 -73
  102. package/demos/js/space/bullet.js +0 -102
  103. package/demos/js/space/constants.js +0 -85
  104. package/demos/js/space/game.js +0 -1884
  105. package/demos/js/space/hud.js +0 -112
  106. package/demos/js/space/laserbeam.js +0 -179
  107. package/demos/js/space/lightning.js +0 -277
  108. package/demos/js/space/minion.js +0 -192
  109. package/demos/js/space/missile.js +0 -212
  110. package/demos/js/space/player.js +0 -430
  111. package/demos/js/space/powerup.js +0 -90
  112. package/demos/js/space/starfield.js +0 -58
  113. package/demos/js/space/starpower.js +0 -90
  114. package/demos/js/spacetime.js +0 -559
  115. package/demos/js/sphere3d.js +0 -229
  116. package/demos/js/sprite.js +0 -473
  117. package/demos/js/svgtween.js +0 -204
  118. package/demos/js/tde/accretiondisk.js +0 -471
  119. package/demos/js/tde/blackhole.js +0 -219
  120. package/demos/js/tde/blackholescene.js +0 -209
  121. package/demos/js/tde/config.js +0 -59
  122. package/demos/js/tde/index.js +0 -820
  123. package/demos/js/tde/jets.js +0 -290
  124. package/demos/js/tde/lensedstarfield.js +0 -154
  125. package/demos/js/tde/tdestar.js +0 -297
  126. package/demos/js/tde/tidalstream.js +0 -372
  127. package/demos/js/tde_old/blackhole.obj.js +0 -354
  128. package/demos/js/tde_old/debris.obj.js +0 -791
  129. package/demos/js/tde_old/flare.obj.js +0 -239
  130. package/demos/js/tde_old/index.js +0 -448
  131. package/demos/js/tde_old/star.obj.js +0 -812
  132. package/demos/js/tiles.js +0 -312
  133. package/demos/js/tweendemo.js +0 -79
  134. package/demos/js/visibility.js +0 -102
  135. package/demos/kerr.html +0 -28
  136. package/demos/lavalamp.html +0 -27
  137. package/demos/layouts.html +0 -37
  138. package/demos/logo.svg +0 -4
  139. package/demos/loop.html +0 -84
  140. package/demos/mondrian.html +0 -32
  141. package/demos/og_image.png +0 -0
  142. package/demos/opacity.html +0 -36
  143. package/demos/painter.html +0 -39
  144. package/demos/particles-showcase.html +0 -28
  145. package/demos/particles.html +0 -24
  146. package/demos/patterns.html +0 -33
  147. package/demos/penrose-game.html +0 -31
  148. package/demos/pipeline.html +0 -737
  149. package/demos/plane3d.html +0 -24
  150. package/demos/platformer.html +0 -43
  151. package/demos/scene.html +0 -33
  152. package/demos/scenes.html +0 -96
  153. package/demos/schrodinger.html +0 -27
  154. package/demos/schwarzschild.html +0 -27
  155. package/demos/shapes.html +0 -16
  156. package/demos/space.html +0 -85
  157. package/demos/spacetime.html +0 -27
  158. package/demos/sphere3d.html +0 -24
  159. package/demos/sprite.html +0 -18
  160. package/demos/svgtween.html +0 -29
  161. package/demos/tde.html +0 -28
  162. package/demos/tiles.html +0 -28
  163. package/demos/transforms.html +0 -400
  164. package/demos/tween.html +0 -45
  165. package/demos/visibility.html +0 -33
  166. package/docs/README.md +0 -230
  167. package/docs/api/FluidSystem.md +0 -173
  168. package/docs/concepts/architecture-overview.md +0 -204
  169. package/docs/concepts/coordinate-system.md +0 -384
  170. package/docs/concepts/lifecycle.md +0 -255
  171. package/docs/concepts/rendering-pipeline.md +0 -279
  172. package/docs/concepts/shapes-vs-gameobjects.md +0 -187
  173. package/docs/concepts/tde-zorder.md +0 -106
  174. package/docs/concepts/two-layer-architecture.md +0 -229
  175. package/docs/fluid-dynamics.md +0 -99
  176. package/docs/getting-started/first-game.md +0 -354
  177. package/docs/getting-started/hello-world.md +0 -269
  178. package/docs/getting-started/installation.md +0 -175
  179. package/docs/modules/collision/README.md +0 -453
  180. package/docs/modules/fluent/README.md +0 -1075
  181. package/docs/modules/game/README.md +0 -303
  182. package/docs/modules/isometric-camera.md +0 -210
  183. package/docs/modules/isometric.md +0 -275
  184. package/docs/modules/painter/README.md +0 -328
  185. package/docs/modules/particle/README.md +0 -559
  186. package/docs/modules/shapes/README.md +0 -221
  187. package/docs/modules/shapes/base/euclidian.md +0 -123
  188. package/docs/modules/shapes/base/geometry2d.md +0 -204
  189. package/docs/modules/shapes/base/renderable.md +0 -215
  190. package/docs/modules/shapes/base/shape.md +0 -262
  191. package/docs/modules/shapes/base/transformable.md +0 -243
  192. package/docs/modules/shapes/hierarchy.md +0 -218
  193. package/docs/modules/state/README.md +0 -577
  194. package/docs/modules/util/README.md +0 -99
  195. package/docs/modules/util/camera3d.md +0 -412
  196. package/docs/modules/util/scene3d.md +0 -395
  197. package/index.html +0 -17
  198. package/jsdoc.json +0 -50
  199. package/scripts/build-demo.js +0 -69
  200. package/scripts/bundle4llm.js +0 -276
  201. package/scripts/clearconsole.js +0 -48
  202. package/test/math/orbital.test.js +0 -61
  203. package/test/math/tensor.test.js +0 -114
  204. package/test/particle/emitter.test.js +0 -204
  205. package/test/particle/particle-system.test.js +0 -310
  206. package/test/particle/particle.test.js +0 -116
  207. package/test/particle/updaters.test.js +0 -386
  208. package/test/setup.js +0 -120
  209. package/test/shapes/euclidian.test.js +0 -44
  210. package/test/shapes/geometry.test.js +0 -86
  211. package/test/shapes/group.test.js +0 -86
  212. package/test/shapes/rectangle.test.js +0 -64
  213. package/test/shapes/transform.test.js +0 -379
  214. package/test/util/camera3d.test.js +0 -428
  215. package/test/util/scene3d.test.js +0 -352
  216. package/vite.config.js +0 -50
  217. package/vitest.config.js +0 -13
package/demos/js/dino.js DELETED
@@ -1,1420 +0,0 @@
1
- import {
2
- Game,
3
- GameObject,
4
- Text,
5
- Rectangle,
6
- Circle,
7
- Keys,
8
- FPSCounter,
9
- PlatformerScene,
10
- Collision,
11
- Painter,
12
- Position,
13
- Group,
14
- Synth,
15
- Sprite,
16
- } from "../../src/index.js";
17
-
18
- // ==================== Configuration ====================
19
- const CONFIG = {
20
- // Theme - Vercel meets Terminal (default)
21
- theme: {
22
- background: "#000000",
23
- primary: "#00ff00", // Terminal green
24
- secondary: "#0a0a0a",
25
- accent: "#00cc00",
26
- text: "#ffffff",
27
- textDim: "#666666",
28
- ground: "#1a1a1a",
29
- groundLine: "#00ff00",
30
- obstacle: "#00ff00",
31
- dino: "#00ff00",
32
- },
33
-
34
- // Theme - 80s Outrun (unlocks at 1000 points)
35
- outrunTheme: {
36
- background: "#1a1a2e", // Deep purple-blue
37
- primary: "#ff6b9d", // Hot pink
38
- secondary: "#c44569",
39
- accent: "#f8b500", // Orange/gold
40
- text: "#ffffff",
41
- textDim: "#9d65c9", // Purple
42
- ground: "#16213e",
43
- groundLine: "#ff6b9d",
44
- obstacle: "#f8b500", // Orange palm trees
45
- dino: "#00d9ff", // Cyan dino
46
- sun: "#ff6b9d", // Sun gradient colors
47
- sunGlow: "#f8b500",
48
- },
49
-
50
- // Level transition thresholds
51
- outrunStartScore: 2000, // Enter outrun at 2k
52
- outrunDuration: 1000, // Stay in outrun for 1k points
53
- levelCycle: 3000, // Full cycle length (2k + 1k)
54
-
55
- // Game settings
56
- gravity: 2800,
57
- jumpVelocity: -750,
58
-
59
- // Player (Dino)
60
- dinoScale: 1.0,
61
-
62
- // Ground - centered layout
63
- groundHeight: 2,
64
-
65
- // Obstacles (Cacti / Palm Trees)
66
- cactusWidth: 15,
67
- cactusMinHeight: 30,
68
- cactusMaxHeight: 50,
69
- cactusSpawnMinInterval: 1.0,
70
- cactusSpawnMaxInterval: 2.2,
71
-
72
- // Scrolling
73
- scrollSpeed: 350,
74
- scrollAcceleration: 8,
75
- maxScrollSpeed: 800,
76
-
77
- // Sound
78
- soundEnabled: true,
79
- masterVolume: 0.3,
80
- };
81
-
82
- // ==================== Sound Effects ====================
83
- /**
84
- * SFX - Retro 8-bit style sound effects for the dino game
85
- */
86
- class SFX {
87
- static initialized = false;
88
-
89
- static init() {
90
- if (this.initialized || !CONFIG.soundEnabled) return;
91
-
92
- Synth.init({ masterVolume: CONFIG.masterVolume });
93
- this.initialized = true;
94
- }
95
-
96
- static async resume() {
97
- if (!this.initialized) return;
98
- await Synth.resume();
99
- }
100
-
101
- /**
102
- * Jump sound - Quick upward sweep
103
- */
104
- static jump() {
105
- if (!this.initialized) return;
106
-
107
- // Retro jump: quick upward frequency sweep
108
- Synth.osc.sweep(150, 400, 0.15, {
109
- type: "square",
110
- volume: 0.25,
111
- });
112
- }
113
-
114
- /**
115
- * Score milestone sound - Quick double beep
116
- */
117
- static milestone() {
118
- if (!this.initialized) return;
119
-
120
- const now = Synth.now;
121
-
122
- // Two quick beeps
123
- Synth.osc.tone(880, 0.08, {
124
- type: "square",
125
- volume: 0.2,
126
- attack: 0.01,
127
- decay: 0.02,
128
- sustain: 0.5,
129
- release: 0.05,
130
- startTime: now,
131
- });
132
-
133
- Synth.osc.tone(1100, 0.1, {
134
- type: "square",
135
- volume: 0.2,
136
- attack: 0.01,
137
- decay: 0.02,
138
- sustain: 0.5,
139
- release: 0.05,
140
- startTime: now + 0.1,
141
- });
142
- }
143
-
144
- /**
145
- * Game over sound - Sad descending tone
146
- */
147
- static gameOver() {
148
- if (!this.initialized) return;
149
-
150
- const now = Synth.now;
151
-
152
- // Descending arpeggio
153
- const notes = [440, 349, 293, 220];
154
- notes.forEach((freq, i) => {
155
- Synth.osc.tone(freq, 0.2, {
156
- type: "square",
157
- volume: 0.25,
158
- attack: 0.01,
159
- decay: 0.05,
160
- sustain: 0.6,
161
- release: 0.15,
162
- startTime: now + i * 0.12,
163
- });
164
- });
165
-
166
- // Final low buzz
167
- Synth.osc.tone(110, 0.4, {
168
- type: "sawtooth",
169
- volume: 0.15,
170
- attack: 0.05,
171
- decay: 0.1,
172
- sustain: 0.3,
173
- release: 0.25,
174
- startTime: now + 0.5,
175
- });
176
- }
177
-
178
- /**
179
- * Start game sound - Ascending beep
180
- */
181
- static start() {
182
- if (!this.initialized) return;
183
-
184
- Synth.osc.sweep(200, 600, 0.2, {
185
- type: "square",
186
- volume: 0.2,
187
- });
188
- }
189
-
190
- /**
191
- * Theme transition sound - Synth flourish
192
- */
193
- static themeTransition(toOutrun = true) {
194
- if (!this.initialized) return;
195
-
196
- const now = Synth.now;
197
-
198
- if (toOutrun) {
199
- // Ascending arpeggio for entering outrun mode
200
- const notes = [330, 440, 550, 660, 880];
201
- notes.forEach((freq, i) => {
202
- Synth.osc.tone(freq, 0.15, {
203
- type: "sawtooth",
204
- volume: 0.2,
205
- attack: 0.01,
206
- decay: 0.03,
207
- sustain: 0.7,
208
- release: 0.1,
209
- startTime: now + i * 0.08,
210
- });
211
- });
212
- } else {
213
- // Descending for returning to normal
214
- const notes = [660, 550, 440, 330];
215
- notes.forEach((freq, i) => {
216
- Synth.osc.tone(freq, 0.12, {
217
- type: "square",
218
- volume: 0.15,
219
- attack: 0.01,
220
- decay: 0.02,
221
- sustain: 0.6,
222
- release: 0.08,
223
- startTime: now + i * 0.06,
224
- });
225
- });
226
- }
227
- }
228
- }
229
-
230
- // ==================== Dino Shape Factory ====================
231
- /**
232
- * Creates pixelated T-Rex dinosaur frames using rectangles
233
- * Supports different leg positions for walking animation
234
- */
235
- class DinoShapeFactory {
236
- /**
237
- * Creates a dino frame with specified leg position
238
- * @param {Object} options
239
- * @param {string} [options.color] - Dino color
240
- * @param {number} [options.scale] - Scale factor
241
- * @param {string} [options.legPose] - 'stand' | 'left' | 'right'
242
- * @returns {Group} A Group containing the dino shape
243
- */
244
- static createFrame(options = {}) {
245
- const color = options.color || CONFIG.theme.dino;
246
- const scale = options.scale || 1;
247
- const legPose = options.legPose || 'stand';
248
- const px = 3 * scale;
249
-
250
- const group = new Group({});
251
-
252
- // Body pixels (shared across all frames) - tail handled separately
253
- const bodyPixels = [
254
- // Head (top)
255
- { x: 6, y: 0, w: 7, h: 1 },
256
- { x: 5, y: 1, w: 9, h: 1 },
257
- { x: 5, y: 2, w: 9, h: 1 },
258
- // Eye (gap)
259
- { x: 5, y: 3, w: 4, h: 1 },
260
- { x: 11, y: 3, w: 3, h: 1 },
261
- // Mouth area
262
- { x: 5, y: 4, w: 9, h: 1 },
263
- { x: 3, y: 5, w: 11, h: 1 },
264
- { x: 2, y: 6, w: 8, h: 1 },
265
- // Neck
266
- { x: 2, y: 7, w: 5, h: 1 },
267
- { x: 1, y: 8, w: 5, h: 1 },
268
- // Body
269
- { x: 0, y: 9, w: 6, h: 1 },
270
- { x: 0, y: 10, w: 6, h: 1 },
271
- { x: 0, y: 11, w: 6, h: 1 },
272
- // Tiny T-Rex arms (high on chest, classic style)
273
- { x: 5, y: 7, w: 2, h: 1 }, // Upper arm
274
- { x: 6, y: 8, w: 2, h: 1 }, // Forearm
275
- // Lower body (connects to legs)
276
- { x: 0, y: 12, w: 6, h: 1 },
277
- { x: 0, y: 13, w: 6, h: 1 },
278
- ];
279
-
280
- // Tail pixels based on pose
281
- let tailPixels = [];
282
- if (legPose === 'jump') {
283
- // Tail up during jump
284
- tailPixels = [
285
- { x: -3, y: 9, w: 3, h: 1 },
286
- { x: -4, y: 8, w: 4, h: 1 },
287
- { x: -5, y: 7, w: 4, h: 1 },
288
- { x: -6, y: 6, w: 3, h: 1 },
289
- ];
290
- } else {
291
- // Normal tail position
292
- tailPixels = [
293
- { x: -3, y: 10, w: 3, h: 1 },
294
- { x: -4, y: 11, w: 4, h: 1 },
295
- { x: -5, y: 12, w: 5, h: 1 },
296
- { x: -5, y: 13, w: 6, h: 1 },
297
- ];
298
- }
299
-
300
- // Leg pixels based on pose
301
- let legPixels = [];
302
-
303
- switch (legPose) {
304
- case 'left':
305
- // Left leg forward, right leg back
306
- legPixels = [
307
- // Left leg (forward, extended)
308
- { x: 0, y: 14, w: 2, h: 1 },
309
- { x: -1, y: 15, w: 2, h: 1 },
310
- { x: -2, y: 16, w: 3, h: 1 },
311
- // Right leg (back, lifted)
312
- { x: 4, y: 14, w: 2, h: 1 },
313
- { x: 5, y: 15, w: 2, h: 1 },
314
- ];
315
- break;
316
-
317
- case 'right':
318
- // Right leg forward, left leg back
319
- legPixels = [
320
- // Left leg (back, lifted)
321
- { x: 0, y: 14, w: 2, h: 1 },
322
- { x: -1, y: 15, w: 2, h: 1 },
323
- // Right leg (forward, extended)
324
- { x: 4, y: 14, w: 2, h: 1 },
325
- { x: 5, y: 15, w: 2, h: 1 },
326
- { x: 6, y: 16, w: 3, h: 1 },
327
- ];
328
- break;
329
-
330
- case 'jump':
331
- // Legs tucked together during jump
332
- legPixels = [
333
- { x: 1, y: 14, w: 4, h: 1 },
334
- { x: 2, y: 15, w: 3, h: 1 },
335
- { x: 3, y: 16, w: 2, h: 1 },
336
- ];
337
- break;
338
-
339
- case 'stand':
340
- default:
341
- // Both legs down (standing/idle)
342
- legPixels = [
343
- { x: 0, y: 14, w: 2, h: 1 },
344
- { x: 4, y: 14, w: 2, h: 1 },
345
- { x: 0, y: 15, w: 2, h: 1 },
346
- { x: 4, y: 15, w: 2, h: 1 },
347
- { x: -1, y: 16, w: 3, h: 1 },
348
- { x: 3, y: 16, w: 3, h: 1 },
349
- ];
350
- break;
351
- }
352
-
353
- const allPixels = [...bodyPixels, ...tailPixels, ...legPixels];
354
-
355
- // Center offset
356
- const offsetX = -3 * px;
357
- const offsetY = -8 * px;
358
-
359
- allPixels.forEach(p => {
360
- const rect = new Rectangle({
361
- x: p.x * px + offsetX + (p.w * px) / 2,
362
- y: p.y * px + offsetY + (p.h * px) / 2,
363
- width: p.w * px,
364
- height: p.h * px,
365
- color: color,
366
- });
367
- group.add(rect);
368
- });
369
-
370
- return group;
371
- }
372
-
373
- /**
374
- * Creates all walking animation frames
375
- * @param {Object} options
376
- * @returns {Group[]} Array of frame groups
377
- */
378
- static createWalkFrames(options = {}) {
379
- return [
380
- this.createFrame({ ...options, legPose: 'left' }),
381
- this.createFrame({ ...options, legPose: 'stand' }),
382
- this.createFrame({ ...options, legPose: 'right' }),
383
- this.createFrame({ ...options, legPose: 'stand' }),
384
- ];
385
- }
386
-
387
- /**
388
- * Creates the idle frame
389
- * @param {Object} options
390
- * @returns {Group}
391
- */
392
- static createIdleFrame(options = {}) {
393
- return this.createFrame({ ...options, legPose: 'stand' });
394
- }
395
-
396
- /**
397
- * Creates the jump frame with tucked legs and raised tail
398
- * @param {Object} options
399
- * @returns {Group}
400
- */
401
- static createJumpFrame(options = {}) {
402
- return this.createFrame({ ...options, legPose: 'jump' });
403
- }
404
- }
405
-
406
- // ==================== Dino (Player) ====================
407
- /**
408
- * Dino - The player character as an animated Sprite
409
- */
410
- class Dino extends Sprite {
411
- constructor(game, options = {}) {
412
- super(game, {
413
- ...options,
414
- frameRate: 10,
415
- loop: true,
416
- });
417
-
418
- this.width = 50;
419
- this.height = 55;
420
- this.vx = 0;
421
- this.vy = 0;
422
- this._grounded = true;
423
- this._isRunning = false; // Track if game has started
424
- this._currentTheme = CONFIG.theme;
425
- this._targetRotation = 0;
426
- this._rotationSpeed = 8; // How fast to lerp rotation
427
-
428
- this.buildAnimations(CONFIG.theme);
429
-
430
- // Start with idle animation (legs don't move before game starts)
431
- this.stopAnimation('idle');
432
- }
433
-
434
- buildAnimations(theme) {
435
- // Clear existing animations
436
- this._animations.clear();
437
-
438
- const frameOptions = {
439
- color: theme.dino,
440
- scale: CONFIG.dinoScale,
441
- };
442
-
443
- // Add animations
444
- this.addAnimation('walk', DinoShapeFactory.createWalkFrames(frameOptions), {
445
- frameRate: 12,
446
- });
447
- this.addAnimation('idle', [DinoShapeFactory.createIdleFrame(frameOptions)], {
448
- loop: false,
449
- });
450
- this.addAnimation('jump', [DinoShapeFactory.createJumpFrame(frameOptions)], {
451
- loop: false,
452
- });
453
- }
454
-
455
- setTheme(theme) {
456
- if (this._currentTheme === theme) return;
457
- this._currentTheme = theme;
458
- const currentAnim = this.currentAnimationName;
459
- this.buildAnimations(theme);
460
- if (currentAnim) {
461
- this.playAnimation(currentAnim);
462
- }
463
- }
464
-
465
- startRunning() {
466
- this._isRunning = true;
467
- this.playAnimation('walk');
468
- }
469
-
470
- stopRunning() {
471
- this._isRunning = false;
472
- this.stopAnimation('idle');
473
- }
474
-
475
- update(dt) {
476
- super.update(dt);
477
-
478
- // Don't animate if game hasn't started
479
- if (!this._isRunning) return;
480
-
481
- // Switch animations based on state
482
- if (!this._grounded) {
483
- if (this.currentAnimationName !== 'jump') {
484
- this.playAnimation('jump');
485
- }
486
- // Target tilt when jumping (upward)
487
- this._targetRotation = -0.3;
488
- } else {
489
- if (this.currentAnimationName !== 'walk') {
490
- this.playAnimation('walk');
491
- }
492
- // Reset rotation when grounded
493
- this._targetRotation = 0;
494
- }
495
-
496
- // Smoothly animate rotation towards target
497
- const rotationDiff = this._targetRotation - this.rotation;
498
- this.rotation += rotationDiff * this._rotationSpeed * dt;
499
- }
500
-
501
- getBounds() {
502
- // Shrink hitbox for fairness
503
- const shrink = 8;
504
- return {
505
- x: this.x - this.width / 2 + shrink,
506
- y: this.y - this.height / 2 + shrink,
507
- width: this.width - shrink * 2,
508
- height: this.height - shrink * 2,
509
- };
510
- }
511
- }
512
-
513
- // ==================== Obstacle (Cactus / Palm Tree) ====================
514
- /**
515
- * Obstacle - A pixel-art style obstacle (cactus or palm tree)
516
- */
517
- class Obstacle extends GameObject {
518
- constructor(game, options = {}) {
519
- super(game, options);
520
- this.width = CONFIG.cactusWidth;
521
- this.height = options.height || CONFIG.cactusMinHeight +
522
- Math.random() * (CONFIG.cactusMaxHeight - CONFIG.cactusMinHeight);
523
- this.isPalmTree = options.isPalmTree || false;
524
- this.theme = options.theme || CONFIG.theme;
525
-
526
- this.group = new Group({});
527
- this.build();
528
- }
529
-
530
- build() {
531
- this.group.clear();
532
-
533
- if (this.isPalmTree) {
534
- this.buildPalmTree();
535
- } else {
536
- this.buildCactus();
537
- }
538
- }
539
-
540
- buildCactus() {
541
- const c = this.theme.obstacle;
542
- const w = this.width;
543
- const h = this.height;
544
-
545
- // Main stem
546
- this.group.add(new Rectangle({
547
- x: 0,
548
- y: 0,
549
- width: w * 0.6,
550
- height: h,
551
- color: c,
552
- }));
553
-
554
- // Left arm (if tall enough)
555
- if (h > 35) {
556
- this.group.add(new Rectangle({
557
- x: -w * 0.5,
558
- y: -h * 0.2,
559
- width: w * 0.4,
560
- height: h * 0.3,
561
- color: c,
562
- }));
563
- this.group.add(new Rectangle({
564
- x: -w * 0.5,
565
- y: -h * 0.35,
566
- width: w * 0.4,
567
- height: w * 0.4,
568
- color: c,
569
- }));
570
- }
571
-
572
- // Right arm (if even taller)
573
- if (h > 45) {
574
- this.group.add(new Rectangle({
575
- x: w * 0.5,
576
- y: -h * 0.1,
577
- width: w * 0.4,
578
- height: h * 0.25,
579
- color: c,
580
- }));
581
- this.group.add(new Rectangle({
582
- x: w * 0.5,
583
- y: -h * 0.27,
584
- width: w * 0.4,
585
- height: w * 0.4,
586
- color: c,
587
- }));
588
- }
589
- }
590
-
591
- buildPalmTree() {
592
- const trunkColor = "#8B4513"; // Brown trunk
593
- const leafColor = this.theme.obstacle; // Orange/gold leaves
594
- const w = this.width;
595
- const h = this.height * 1.5; // Palm trees are taller
596
-
597
- // Trunk (slightly curved look with segments)
598
- const trunkWidth = w * 0.4;
599
- const segments = 4;
600
- for (let i = 0; i < segments; i++) {
601
- const segY = (i / segments) * h - h * 0.3;
602
- const segH = h / segments + 2;
603
- this.group.add(new Rectangle({
604
- x: (i % 2 === 0 ? 1 : -1) * 1, // Slight wobble
605
- y: segY,
606
- width: trunkWidth,
607
- height: segH,
608
- color: trunkColor,
609
- }));
610
- }
611
-
612
- // Palm fronds (leaves) - radiating from top
613
- const leafLength = w * 2.5;
614
- const leafWidth = w * 0.3;
615
- const topY = -h * 0.5 - 5;
616
-
617
- // Left fronds
618
- this.group.add(new Rectangle({
619
- x: -leafLength * 0.4,
620
- y: topY - 5,
621
- width: leafLength,
622
- height: leafWidth,
623
- color: leafColor,
624
- }));
625
- this.group.add(new Rectangle({
626
- x: -leafLength * 0.3,
627
- y: topY - 12,
628
- width: leafLength * 0.8,
629
- height: leafWidth,
630
- color: leafColor,
631
- }));
632
-
633
- // Right fronds
634
- this.group.add(new Rectangle({
635
- x: leafLength * 0.4,
636
- y: topY - 5,
637
- width: leafLength,
638
- height: leafWidth,
639
- color: leafColor,
640
- }));
641
- this.group.add(new Rectangle({
642
- x: leafLength * 0.3,
643
- y: topY - 12,
644
- width: leafLength * 0.8,
645
- height: leafWidth,
646
- color: leafColor,
647
- }));
648
-
649
- // Center/top fronds
650
- this.group.add(new Rectangle({
651
- x: 0,
652
- y: topY - 18,
653
- width: leafLength * 0.6,
654
- height: leafWidth,
655
- color: leafColor,
656
- }));
657
-
658
- // Coconuts (small circles near top)
659
- const coconutColor = "#654321";
660
- this.group.add(new Circle({
661
- x: -3,
662
- y: topY + 3,
663
- radius: 4,
664
- color: coconutColor,
665
- }));
666
- this.group.add(new Circle({
667
- x: 4,
668
- y: topY + 5,
669
- radius: 4,
670
- color: coconutColor,
671
- }));
672
- }
673
-
674
- draw() {
675
- super.draw();
676
- this.group.render();
677
- }
678
-
679
- getBounds() {
680
- // Use consistent hitbox regardless of visual style
681
- return {
682
- x: this.x - this.width / 2,
683
- y: this.y - this.height / 2,
684
- width: this.width,
685
- height: this.height,
686
- };
687
- }
688
- }
689
-
690
- // Alias for backwards compatibility
691
- const Cactus = Obstacle;
692
-
693
- // ==================== Ground ====================
694
- /**
695
- * Ground - Terminal-style ground with scrolling texture
696
- */
697
- class Ground extends GameObject {
698
- constructor(game, options = {}) {
699
- super(game, options);
700
- this.groundWidth = options.width || game.width * 3;
701
- this.scrollOffset = 0;
702
- this._theme = CONFIG.theme;
703
-
704
- // Main ground line
705
- this.line = new Rectangle({
706
- width: this.groundWidth,
707
- height: CONFIG.groundHeight,
708
- color: this._theme.groundLine,
709
- });
710
-
711
- // Generate ground texture marks (will scroll)
712
- this.marks = [];
713
- const markSpacing = 15;
714
- const numMarks = Math.ceil(this.groundWidth / markSpacing) + 10;
715
- for (let i = 0; i < numMarks; i++) {
716
- // Different mark types for variety
717
- const type = Math.random();
718
- if (type < 0.3) {
719
- // Small dash
720
- this.marks.push({
721
- baseX: i * markSpacing,
722
- y: 4 + Math.random() * 3,
723
- width: 2 + Math.random() * 4,
724
- height: 1,
725
- });
726
- } else if (type < 0.5) {
727
- // Tall tick
728
- this.marks.push({
729
- baseX: i * markSpacing,
730
- y: 3,
731
- width: 1,
732
- height: 3 + Math.random() * 4,
733
- });
734
- } else if (type < 0.6) {
735
- // Double dash
736
- this.marks.push({
737
- baseX: i * markSpacing,
738
- y: 4,
739
- width: 6 + Math.random() * 8,
740
- height: 1,
741
- });
742
- this.marks.push({
743
- baseX: i * markSpacing + 2,
744
- y: 7,
745
- width: 4 + Math.random() * 4,
746
- height: 1,
747
- });
748
- }
749
- }
750
- this.markCycleWidth = numMarks * markSpacing;
751
- }
752
-
753
- setTheme(theme) {
754
- this._theme = theme;
755
- this.line.color = theme.groundLine;
756
- }
757
-
758
- setScrollOffset(offset) {
759
- this.scrollOffset = offset % this.markCycleWidth;
760
- }
761
-
762
- draw() {
763
- super.draw();
764
- this.line.render();
765
-
766
- // Draw scrolling ground texture
767
- Painter.useCtx((ctx) => {
768
- ctx.fillStyle = this._theme.textDim;
769
- const startX = -this.groundWidth / 2;
770
- this.marks.forEach(m => {
771
- // Calculate scrolled position
772
- let x = startX + m.baseX - this.scrollOffset;
773
- // Wrap around
774
- while (x < startX - 20) x += this.markCycleWidth;
775
- while (x > startX + this.groundWidth) x -= this.markCycleWidth;
776
- ctx.fillRect(x, m.y, m.width, m.height);
777
- });
778
- });
779
- }
780
- }
781
-
782
- // ==================== Sky Layer ====================
783
- /**
784
- * SkyLayer - Parallax sky with Tron clouds or Outrun sun
785
- */
786
- class SkyLayer extends GameObject {
787
- constructor(game, options = {}) {
788
- super(game, options);
789
- this.cloudWidth = game.width * 2;
790
- this.scrollOffset = 0;
791
- this._theme = CONFIG.theme;
792
- this._isOutrunMode = false;
793
-
794
- // Generate cloud grid lines
795
- this.gridLines = [];
796
- this.glowLines = [];
797
-
798
- // Horizontal scan lines (main cloud layer)
799
- const numLines = 8;
800
- for (let i = 0; i < numLines; i++) {
801
- const y = -150 + i * 25 + (Math.random() - 0.5) * 10;
802
- const segments = [];
803
-
804
- // Create broken line segments for cloud effect
805
- let x = 0;
806
- while (x < this.cloudWidth) {
807
- if (Math.random() > 0.4) {
808
- const segWidth = 50 + Math.random() * 150;
809
- const opacity = 0.1 + Math.random() * 0.3;
810
- segments.push({
811
- x: x - this.cloudWidth / 2,
812
- width: segWidth,
813
- opacity,
814
- glow: Math.random() > 0.7,
815
- });
816
- }
817
- x += 30 + Math.random() * 80;
818
- }
819
-
820
- this.gridLines.push({ y, segments });
821
- }
822
-
823
- // Vertical grid lines (sparse, for depth)
824
- const numVerticals = 12;
825
- for (let i = 0; i < numVerticals; i++) {
826
- const x = (i / numVerticals) * this.cloudWidth - this.cloudWidth / 2;
827
- if (Math.random() > 0.5) {
828
- this.glowLines.push({
829
- x,
830
- y1: -180 + Math.random() * 30,
831
- y2: -80 + Math.random() * 40,
832
- opacity: 0.05 + Math.random() * 0.15,
833
- });
834
- }
835
- }
836
- }
837
-
838
- setTheme(theme, isOutrun = false) {
839
- this._theme = theme;
840
- this._isOutrunMode = isOutrun;
841
- }
842
-
843
- setScrollOffset(offset) {
844
- this.scrollOffset = offset % this.cloudWidth;
845
- }
846
-
847
- drawOutrunSun(ctx) {
848
- const sunX = 0;
849
- const sunY = -120;
850
- const sunRadius = 80;
851
-
852
- // Sun glow
853
- const gradient = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, sunRadius * 1.5);
854
- gradient.addColorStop(0, this._theme.sunGlow || '#f8b500');
855
- gradient.addColorStop(0.5, this._theme.sun || '#ff6b9d');
856
- gradient.addColorStop(1, 'transparent');
857
-
858
- ctx.globalAlpha = 0.6;
859
- ctx.fillStyle = gradient;
860
- ctx.beginPath();
861
- ctx.arc(sunX, sunY, sunRadius * 1.5, 0, Math.PI * 2);
862
- ctx.fill();
863
-
864
- // Sun body with horizontal stripes (retrowave style)
865
- ctx.globalAlpha = 1;
866
- const stripeCount = 8;
867
- for (let i = 0; i < stripeCount; i++) {
868
- const stripeY = sunY - sunRadius + (i * 2 + 1) * (sunRadius / stripeCount);
869
- const stripeHeight = sunRadius / stripeCount - 2;
870
-
871
- // Calculate stripe width at this y position (circle intersection)
872
- const dy = Math.abs(stripeY - sunY);
873
- if (dy < sunRadius) {
874
- const stripeHalfWidth = Math.sqrt(sunRadius * sunRadius - dy * dy);
875
-
876
- // Gradient from orange to pink
877
- const t = i / stripeCount;
878
- const r = Math.floor(255 - t * 50);
879
- const g = Math.floor(107 + t * 50);
880
- const b = Math.floor(0 + t * 157);
881
-
882
- ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
883
- ctx.fillRect(sunX - stripeHalfWidth, stripeY, stripeHalfWidth * 2, stripeHeight);
884
- }
885
- }
886
-
887
- // Horizontal lines cutting through sun (retrowave effect)
888
- ctx.strokeStyle = this._theme.background || '#1a1a2e';
889
- ctx.lineWidth = 3;
890
- for (let i = 1; i < 6; i++) {
891
- const lineY = sunY + sunRadius * 0.2 + i * 12;
892
- if (lineY < sunY + sunRadius) {
893
- const dy = Math.abs(lineY - sunY);
894
- const halfWidth = Math.sqrt(sunRadius * sunRadius - dy * dy);
895
- ctx.beginPath();
896
- ctx.moveTo(sunX - halfWidth, lineY);
897
- ctx.lineTo(sunX + halfWidth, lineY);
898
- ctx.stroke();
899
- }
900
- }
901
- }
902
-
903
- draw() {
904
- super.draw();
905
-
906
- Painter.useCtx((ctx) => {
907
- // Draw outrun sun if in outrun mode
908
- if (this._isOutrunMode) {
909
- this.drawOutrunSun(ctx);
910
- }
911
-
912
- const baseX = -this.scrollOffset;
913
-
914
- // Draw vertical grid lines
915
- ctx.strokeStyle = this._theme.primary;
916
- this.glowLines.forEach(line => {
917
- let x = line.x + baseX;
918
- // Wrap
919
- while (x < -this.cloudWidth / 2) x += this.cloudWidth;
920
- while (x > this.cloudWidth / 2) x -= this.cloudWidth;
921
-
922
- ctx.globalAlpha = line.opacity;
923
- ctx.beginPath();
924
- ctx.moveTo(x, line.y1);
925
- ctx.lineTo(x, line.y2);
926
- ctx.lineWidth = 1;
927
- ctx.stroke();
928
- });
929
-
930
- // Draw horizontal cloud lines
931
- this.gridLines.forEach(line => {
932
- line.segments.forEach(seg => {
933
- let x = seg.x + baseX;
934
- // Wrap
935
- while (x < -this.cloudWidth / 2 - 200) x += this.cloudWidth;
936
- while (x > this.cloudWidth / 2 + 200) x -= this.cloudWidth;
937
-
938
- // Glow effect
939
- if (seg.glow) {
940
- ctx.globalAlpha = seg.opacity * 0.3;
941
- ctx.fillStyle = this._theme.primary;
942
- ctx.fillRect(x - 2, line.y - 2, seg.width + 4, 5);
943
- }
944
-
945
- // Main line
946
- ctx.globalAlpha = seg.opacity;
947
- ctx.fillStyle = this._theme.primary;
948
- ctx.fillRect(x, line.y, seg.width, 1);
949
- });
950
- });
951
-
952
- // Add some "data" dots traveling along lines
953
- const time = Date.now() / 1000;
954
- ctx.fillStyle = this._theme.primary;
955
- for (let i = 0; i < 5; i++) {
956
- const lineIdx = i % this.gridLines.length;
957
- const line = this.gridLines[lineIdx];
958
- if (line.segments.length > 0) {
959
- const seg = line.segments[i % line.segments.length];
960
- const dotX = seg.x + baseX + ((time * 50 * (i + 1)) % seg.width);
961
- let x = dotX;
962
- while (x < -this.cloudWidth / 2 - 200) x += this.cloudWidth;
963
- while (x > this.cloudWidth / 2 + 200) x -= this.cloudWidth;
964
-
965
- ctx.globalAlpha = 0.8;
966
- ctx.fillRect(x, line.y - 1, 3, 3);
967
- }
968
- }
969
-
970
- ctx.globalAlpha = 1;
971
- });
972
- }
973
- }
974
-
975
- // Alias for backwards compatibility
976
- const TronClouds = SkyLayer;
977
-
978
- // ==================== AutoScrollScene ====================
979
- /**
980
- * AutoScrollScene - A PlatformerScene for endless runner
981
- * Full screen centered layout
982
- */
983
- class AutoScrollScene extends PlatformerScene {
984
- constructor(game, options = {}) {
985
- super(game, {
986
- ...options,
987
- autoInput: false,
988
- });
989
- this.scrollSpeed = options.scrollSpeed || CONFIG.scrollSpeed;
990
- }
991
-
992
- updateCamera(dt) {
993
- // No camera following in endless runner
994
- }
995
-
996
- getCameraOffset() {
997
- return { x: 0, y: 0 };
998
- }
999
- }
1000
-
1001
- // ==================== DinoGame ====================
1002
- /**
1003
- * DinoGame - Vercel/Terminal aesthetic endless runner
1004
- */
1005
- class DinoGame extends Game {
1006
- constructor(canvas) {
1007
- super(canvas);
1008
- this.backgroundColor = CONFIG.theme.background;
1009
- this.enableFluidSize();
1010
- }
1011
-
1012
- init() {
1013
- super.init();
1014
- SFX.init();
1015
- this.setupGame();
1016
- this.setupInput();
1017
- }
1018
-
1019
- setupGame() {
1020
- // Game state
1021
- this.score = 0;
1022
- this.highScore = parseInt(localStorage.getItem('dinoHighScore') || '0');
1023
- this.scrollSpeed = CONFIG.scrollSpeed;
1024
- this.gameOver = false;
1025
- this.gameStarted = false;
1026
- this.distance = 0;
1027
- this.nextCactusSpawn = this.getRandomSpawnTime();
1028
- this.cacti = [];
1029
-
1030
- // Theme state
1031
- this.isOutrunMode = false;
1032
- this.currentTheme = CONFIG.theme;
1033
- this._lastThemeScore = 0; // Track last theme transition
1034
-
1035
- // Calculate ground Y - centered vertically, slightly below center
1036
- this.groundY = this.height * 0.65;
1037
-
1038
- // Create player at left side of screen
1039
- this.dino = new Dino(this, {
1040
- x: this.width * 0.15,
1041
- y: this.groundY - 27,
1042
- });
1043
-
1044
- // Create platformer scene - full screen at origin
1045
- this.level = new AutoScrollScene(this, {
1046
- player: this.dino,
1047
- gravity: CONFIG.gravity,
1048
- jumpVelocity: CONFIG.jumpVelocity,
1049
- groundY: this.groundY - 27,
1050
- scrollSpeed: this.scrollSpeed,
1051
- x: 0,
1052
- y: 0,
1053
- });
1054
-
1055
- // Add Tron-style clouds (slow parallax)
1056
- this.clouds = new TronClouds(this, {
1057
- x: this.width / 2,
1058
- y: this.groundY - 100,
1059
- });
1060
- this.level.addLayer(this.clouds, { speed: 0.3 });
1061
-
1062
- // Add ground - spans full width
1063
- this.ground = new Ground(this, {
1064
- x: this.width / 2,
1065
- y: this.groundY,
1066
- width: this.width * 3,
1067
- });
1068
- this.level.addLayer(this.ground, { speed: 0 });
1069
-
1070
- // Add player to scene
1071
- this.level.add(this.dino);
1072
-
1073
- this.pipeline.add(this.level);
1074
-
1075
- // UI Elements
1076
- this.createUI();
1077
-
1078
- // FPS counter
1079
- this.pipeline.add(
1080
- new FPSCounter(this, {
1081
- color: CONFIG.theme.textDim,
1082
- anchor: Position.BOTTOM_RIGHT
1083
- })
1084
- );
1085
- }
1086
-
1087
- createUI() {
1088
- // Score display (top right)
1089
- this.scoreText = new Text(this, "00000", {
1090
- font: "bold 24px 'Courier New', monospace",
1091
- color: CONFIG.theme.primary,
1092
- align: "right",
1093
- anchor: Position.BOTTOM_LEFT,
1094
- anchorOffsetX: -30,
1095
- });
1096
- this.pipeline.add(this.scoreText);
1097
-
1098
- // High score (next to score)
1099
- this.highScoreText = new Text(this, this.highScore > 0 ? `HI ${String(this.highScore).padStart(5, "0")}` : "", {
1100
- font: "16px 'Courier New', monospace",
1101
- color: CONFIG.theme.textDim,
1102
- align: "right",
1103
- anchor: Position.TOP_RIGHT,
1104
- anchorOffsetX: -30,
1105
- anchorOffsetY: 55,
1106
- });
1107
- this.pipeline.add(this.highScoreText);
1108
-
1109
- // Start message - centered
1110
- this.startText = new Text(this, "[ PRESS SPACE TO START ]", {
1111
- font: "18px 'Courier New', monospace",
1112
- color: CONFIG.theme.primary,
1113
- align: "center",
1114
- anchor: Position.CENTER,
1115
- });
1116
- this.pipeline.add(this.startText);
1117
-
1118
- // Subtitle
1119
- this.subtitleText = new Text(this, "avoid the obstacles", {
1120
- font: "14px 'Courier New', monospace",
1121
- color: CONFIG.theme.textDim,
1122
- align: "center",
1123
- anchor: Position.CENTER,
1124
- anchorOffsetY: 30,
1125
- });
1126
- this.pipeline.add(this.subtitleText);
1127
-
1128
- // Game over text (hidden)
1129
- this.gameOverText = new Text(this, "GAME OVER", {
1130
- font: "bold 36px 'Courier New', monospace",
1131
- color: CONFIG.theme.primary,
1132
- align: "center",
1133
- anchor: Position.CENTER,
1134
- anchorOffsetY: -30,
1135
- visible: false,
1136
- });
1137
- this.pipeline.add(this.gameOverText);
1138
-
1139
- // Restart instruction (hidden)
1140
- this.restartText = new Text(this, "[ PRESS SPACE TO RESTART ]", {
1141
- font: "16px 'Courier New', monospace",
1142
- color: CONFIG.theme.textDim,
1143
- align: "center",
1144
- anchor: Position.CENTER,
1145
- anchorOffsetY: 20,
1146
- visible: false,
1147
- });
1148
- this.pipeline.add(this.restartText);
1149
-
1150
- // Final score text (hidden)
1151
- this.finalScoreText = new Text(this, "", {
1152
- font: "20px 'Courier New', monospace",
1153
- color: CONFIG.theme.text,
1154
- align: "center",
1155
- anchor: Position.CENTER,
1156
- anchorOffsetY: 60,
1157
- visible: false,
1158
- });
1159
- this.pipeline.add(this.finalScoreText);
1160
- }
1161
-
1162
- setupInput() {
1163
- this.events.on(Keys.SPACE, () => this.handleJump());
1164
- this.events.on(Keys.UP, () => this.handleJump());
1165
- this.events.on(Keys.W, () => this.handleJump());
1166
- }
1167
-
1168
- async handleJump() {
1169
- // Resume audio on first interaction
1170
- await SFX.resume();
1171
-
1172
- if (this.gameOver) {
1173
- this.restartGame();
1174
- return;
1175
- }
1176
-
1177
- if (!this.gameStarted) {
1178
- this.startGame();
1179
- return;
1180
- }
1181
-
1182
- if (this.level.isPlayerGrounded()) {
1183
- this.dino.vy = CONFIG.jumpVelocity;
1184
- this.dino._grounded = false;
1185
- SFX.jump();
1186
- }
1187
- }
1188
-
1189
- startGame() {
1190
- this.gameStarted = true;
1191
- this.startText.visible = false;
1192
- this.subtitleText.visible = false;
1193
- this.dino.startRunning(); // Start walking animation
1194
- SFX.start();
1195
- }
1196
-
1197
- restartGame() {
1198
- // Save high score to localStorage
1199
- if (this.score > this.highScore) {
1200
- this.highScore = this.score;
1201
- localStorage.setItem('dinoHighScore', String(this.highScore));
1202
- }
1203
-
1204
- // Clear and restart
1205
- this.pipeline.clear();
1206
- this.setupGame();
1207
-
1208
- // Auto-start after game over
1209
- this.gameStarted = true;
1210
- this.startText.visible = false;
1211
- this.subtitleText.visible = false;
1212
- this.dino.startRunning(); // Start walking animation
1213
- }
1214
-
1215
- getRandomSpawnTime() {
1216
- return CONFIG.cactusSpawnMinInterval +
1217
- Math.random() * (CONFIG.cactusSpawnMaxInterval - CONFIG.cactusSpawnMinInterval);
1218
- }
1219
-
1220
- /**
1221
- * Transition to a new theme (normal or outrun)
1222
- * @param {boolean} toOutrun - True to switch to outrun mode
1223
- */
1224
- setTheme(toOutrun) {
1225
- if (this.isOutrunMode === toOutrun) return;
1226
-
1227
- this.isOutrunMode = toOutrun;
1228
- this.currentTheme = toOutrun ? CONFIG.outrunTheme : CONFIG.theme;
1229
-
1230
- // Update background
1231
- this.backgroundColor = this.currentTheme.background;
1232
-
1233
- // Update dino
1234
- if (this.dino) {
1235
- this.dino.setTheme(this.currentTheme);
1236
- }
1237
-
1238
- // Update ground
1239
- if (this.ground) {
1240
- this.ground.setTheme(this.currentTheme);
1241
- }
1242
-
1243
- // Update clouds/sky
1244
- if (this.clouds) {
1245
- this.clouds.setTheme(this.currentTheme, toOutrun);
1246
- }
1247
-
1248
- // Update UI colors
1249
- if (this.scoreText) {
1250
- this.scoreText.color = this.currentTheme.primary;
1251
- }
1252
- if (this.highScoreText) {
1253
- this.highScoreText.color = this.currentTheme.textDim;
1254
- }
1255
- if (this.startText) {
1256
- this.startText.color = this.currentTheme.primary;
1257
- }
1258
- if (this.gameOverText) {
1259
- this.gameOverText.color = this.currentTheme.primary;
1260
- }
1261
- if (this.restartText) {
1262
- this.restartText.color = this.currentTheme.textDim;
1263
- }
1264
- if (this.finalScoreText) {
1265
- this.finalScoreText.color = this.currentTheme.text;
1266
- }
1267
-
1268
- // Play transition sound
1269
- SFX.themeTransition(toOutrun);
1270
- }
1271
-
1272
- /**
1273
- * Check and handle theme transitions based on score
1274
- */
1275
- checkThemeTransition() {
1276
- // Calculate position in the level cycle
1277
- const cyclePosition = this.score % CONFIG.levelCycle;
1278
-
1279
- // Within first 1000 points: normal theme
1280
- // From 1000 to 6000: outrun theme
1281
- const shouldBeOutrun = cyclePosition >= CONFIG.outrunStartScore;
1282
-
1283
- // Only transition if state changed
1284
- if (shouldBeOutrun !== this.isOutrunMode) {
1285
- this.setTheme(shouldBeOutrun);
1286
- }
1287
- }
1288
-
1289
- update(dt) {
1290
- if (!this.gameStarted || this.gameOver) {
1291
- // Blinking effect for start text
1292
- if (!this.gameStarted && this.startText) {
1293
- const blink = Math.sin(Date.now() / 500) > 0;
1294
- this.startText.opacity = blink ? 1 : 0.5;
1295
- }
1296
- super.update(dt);
1297
- return;
1298
- }
1299
-
1300
- // Update distance/score
1301
- this.distance += this.scrollSpeed * dt;
1302
- this.score = Math.floor(this.distance / 10);
1303
- this.scoreText.text = String(this.score).padStart(5, "0");
1304
-
1305
- // Check for theme transitions (normal <-> outrun)
1306
- this.checkThemeTransition();
1307
-
1308
- // Update scroll offsets for ground and clouds
1309
- if (this.ground) {
1310
- this.ground.setScrollOffset(this.distance);
1311
- }
1312
- if (this.clouds) {
1313
- this.clouds.setScrollOffset(this.distance * 0.3); // Slower parallax
1314
- }
1315
-
1316
- // Milestone flash effect (every 100 points)
1317
- if (this.score > 0 && this.score % 100 === 0 && !this._lastMilestone) {
1318
- this._lastMilestone = this.score;
1319
- this.scoreText.color = this.currentTheme.text;
1320
- SFX.milestone();
1321
- setTimeout(() => {
1322
- if (this.scoreText) this.scoreText.color = this.currentTheme.primary;
1323
- }, 100);
1324
- } else if (this.score % 100 !== 0) {
1325
- this._lastMilestone = null;
1326
- }
1327
-
1328
- // Increase speed over time
1329
- this.scrollSpeed = Math.min(
1330
- CONFIG.maxScrollSpeed,
1331
- this.scrollSpeed + CONFIG.scrollAcceleration * dt
1332
- );
1333
-
1334
- // Spawn cacti
1335
- this.nextCactusSpawn -= dt;
1336
- if (this.nextCactusSpawn <= 0) {
1337
- this.spawnCactus();
1338
- this.nextCactusSpawn = this.getRandomSpawnTime();
1339
- }
1340
-
1341
- // Update cacti and check collisions
1342
- for (let i = this.cacti.length - 1; i >= 0; i--) {
1343
- const cactus = this.cacti[i];
1344
- cactus.x -= this.scrollSpeed * dt;
1345
-
1346
- // Remove off-screen
1347
- if (cactus.x < -100) {
1348
- this.level.remove(cactus);
1349
- this.cacti.splice(i, 1);
1350
- continue;
1351
- }
1352
-
1353
- // Collision check
1354
- if (Collision.rectRect(this.dino.getBounds(), cactus.getBounds())) {
1355
- this.triggerGameOver();
1356
- break;
1357
- }
1358
- }
1359
-
1360
- super.update(dt);
1361
- }
1362
-
1363
- spawnCactus() {
1364
- const cactusHeight = CONFIG.cactusMinHeight +
1365
- Math.random() * (CONFIG.cactusMaxHeight - CONFIG.cactusMinHeight);
1366
-
1367
- // In outrun mode, spawn palm trees instead of cacti
1368
- const obstacle = new Obstacle(this, {
1369
- x: this.width + 50,
1370
- y: this.groundY - cactusHeight / 2,
1371
- height: cactusHeight,
1372
- isPalmTree: this.isOutrunMode,
1373
- theme: this.currentTheme,
1374
- });
1375
-
1376
- this.cacti.push(obstacle);
1377
- this.level.add(obstacle);
1378
- }
1379
-
1380
- triggerGameOver() {
1381
- this.gameOver = true;
1382
- this.dino.stopRunning();
1383
- this.gameOverText.visible = true;
1384
- this.restartText.visible = true;
1385
- this.finalScoreText.visible = true;
1386
- this.finalScoreText.text = `SCORE: ${this.score}`;
1387
- SFX.gameOver();
1388
-
1389
- // Update high score
1390
- if (this.score > this.highScore) {
1391
- this.highScore = this.score;
1392
- this.highScoreText.text = `HI ${String(this.highScore).padStart(5, "0")}`;
1393
- localStorage.setItem('dinoHighScore', String(this.highScore));
1394
- }
1395
- }
1396
-
1397
- onResize() {
1398
- // Recalculate positions on resize
1399
- if (this.dino) {
1400
- this.groundY = this.height * 0.65;
1401
- this.dino.x = this.width * 0.15;
1402
- this.level.groundY = this.groundY - 27;
1403
- if (this.ground) {
1404
- this.ground.x = this.width / 2;
1405
- this.ground.y = this.groundY;
1406
- }
1407
- if (this.clouds) {
1408
- this.clouds.x = this.width / 2;
1409
- this.clouds.y = this.groundY - 100;
1410
- }
1411
- }
1412
- }
1413
- }
1414
-
1415
- // ==================== Initialize ====================
1416
- window.addEventListener("load", () => {
1417
- const canvas = document.getElementById("game");
1418
- const game = new DinoGame(canvas);
1419
- game.start();
1420
- });