setiastrosuitepro 1.6.1.post1__py3-none-any.whl → 1.6.4__py3-none-any.whl

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.

Potentially problematic release.


This version of setiastrosuitepro might be problematic. Click here for more details.

Files changed (139) hide show
  1. setiastro/images/Background_startup.jpg +0 -0
  2. setiastro/images/rotatearbitrary.png +0 -0
  3. setiastro/qml/ResourceMonitor.qml +126 -0
  4. setiastro/saspro/__main__.py +162 -25
  5. setiastro/saspro/_generated/build_info.py +2 -1
  6. setiastro/saspro/abe.py +62 -11
  7. setiastro/saspro/aberration_ai.py +3 -3
  8. setiastro/saspro/add_stars.py +5 -2
  9. setiastro/saspro/astrobin_exporter.py +3 -0
  10. setiastro/saspro/astrospike_python.py +3 -1
  11. setiastro/saspro/autostretch.py +4 -2
  12. setiastro/saspro/backgroundneutral.py +60 -9
  13. setiastro/saspro/batch_convert.py +3 -0
  14. setiastro/saspro/batch_renamer.py +3 -0
  15. setiastro/saspro/blemish_blaster.py +3 -0
  16. setiastro/saspro/blink_comparator_pro.py +474 -251
  17. setiastro/saspro/cheat_sheet.py +50 -15
  18. setiastro/saspro/clahe.py +27 -1
  19. setiastro/saspro/comet_stacking.py +103 -38
  20. setiastro/saspro/convo.py +3 -0
  21. setiastro/saspro/copyastro.py +3 -0
  22. setiastro/saspro/cosmicclarity.py +70 -45
  23. setiastro/saspro/crop_dialog_pro.py +28 -1
  24. setiastro/saspro/curve_editor_pro.py +18 -0
  25. setiastro/saspro/debayer.py +3 -0
  26. setiastro/saspro/doc_manager.py +40 -17
  27. setiastro/saspro/fitsmodifier.py +3 -0
  28. setiastro/saspro/frequency_separation.py +8 -2
  29. setiastro/saspro/function_bundle.py +18 -16
  30. setiastro/saspro/generate_translations.py +715 -1
  31. setiastro/saspro/ghs_dialog_pro.py +3 -0
  32. setiastro/saspro/graxpert.py +3 -0
  33. setiastro/saspro/gui/main_window.py +364 -92
  34. setiastro/saspro/gui/mixins/dock_mixin.py +119 -7
  35. setiastro/saspro/gui/mixins/file_mixin.py +7 -0
  36. setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
  37. setiastro/saspro/gui/mixins/menu_mixin.py +29 -0
  38. setiastro/saspro/gui/mixins/toolbar_mixin.py +33 -10
  39. setiastro/saspro/gui/statistics_dialog.py +47 -0
  40. setiastro/saspro/halobgon.py +29 -3
  41. setiastro/saspro/histogram.py +3 -0
  42. setiastro/saspro/history_explorer.py +2 -0
  43. setiastro/saspro/i18n.py +22 -10
  44. setiastro/saspro/image_combine.py +3 -0
  45. setiastro/saspro/image_peeker_pro.py +3 -0
  46. setiastro/saspro/imageops/stretch.py +5 -13
  47. setiastro/saspro/isophote.py +3 -0
  48. setiastro/saspro/legacy/numba_utils.py +64 -47
  49. setiastro/saspro/linear_fit.py +3 -0
  50. setiastro/saspro/live_stacking.py +13 -2
  51. setiastro/saspro/mask_creation.py +3 -0
  52. setiastro/saspro/mfdeconv.py +5 -0
  53. setiastro/saspro/morphology.py +30 -5
  54. setiastro/saspro/multiscale_decomp.py +713 -256
  55. setiastro/saspro/nbtorgb_stars.py +12 -2
  56. setiastro/saspro/numba_utils.py +148 -47
  57. setiastro/saspro/ops/scripts.py +77 -17
  58. setiastro/saspro/ops/settings.py +1 -43
  59. setiastro/saspro/perfect_palette_picker.py +1 -0
  60. setiastro/saspro/pixelmath.py +6 -2
  61. setiastro/saspro/plate_solver.py +1 -0
  62. setiastro/saspro/remove_green.py +18 -1
  63. setiastro/saspro/remove_stars.py +136 -162
  64. setiastro/saspro/remove_stars_preset.py +55 -13
  65. setiastro/saspro/resources.py +36 -10
  66. setiastro/saspro/rgb_combination.py +1 -0
  67. setiastro/saspro/rgbalign.py +4 -4
  68. setiastro/saspro/save_options.py +1 -0
  69. setiastro/saspro/selective_color.py +79 -20
  70. setiastro/saspro/sfcc.py +50 -8
  71. setiastro/saspro/shortcuts.py +94 -21
  72. setiastro/saspro/signature_insert.py +3 -0
  73. setiastro/saspro/stacking_suite.py +924 -446
  74. setiastro/saspro/star_alignment.py +291 -331
  75. setiastro/saspro/star_spikes.py +116 -32
  76. setiastro/saspro/star_stretch.py +38 -1
  77. setiastro/saspro/stat_stretch.py +35 -3
  78. setiastro/saspro/status_log_dock.py +1 -1
  79. setiastro/saspro/subwindow.py +63 -2
  80. setiastro/saspro/supernovaasteroidhunter.py +3 -0
  81. setiastro/saspro/swap_manager.py +77 -42
  82. setiastro/saspro/translations/all_source_strings.json +4726 -0
  83. setiastro/saspro/translations/ar_translations.py +4096 -0
  84. setiastro/saspro/translations/de_translations.py +441 -446
  85. setiastro/saspro/translations/es_translations.py +278 -32
  86. setiastro/saspro/translations/fr_translations.py +280 -32
  87. setiastro/saspro/translations/hi_translations.py +3803 -0
  88. setiastro/saspro/translations/integrate_translations.py +38 -1
  89. setiastro/saspro/translations/it_translations.py +1211 -145
  90. setiastro/saspro/translations/ja_translations.py +556 -307
  91. setiastro/saspro/translations/pt_translations.py +3316 -3322
  92. setiastro/saspro/translations/ru_translations.py +3082 -0
  93. setiastro/saspro/translations/saspro_ar.qm +0 -0
  94. setiastro/saspro/translations/saspro_ar.ts +16019 -0
  95. setiastro/saspro/translations/saspro_de.qm +0 -0
  96. setiastro/saspro/translations/saspro_de.ts +14428 -133
  97. setiastro/saspro/translations/saspro_es.qm +0 -0
  98. setiastro/saspro/translations/saspro_es.ts +11503 -7821
  99. setiastro/saspro/translations/saspro_fr.qm +0 -0
  100. setiastro/saspro/translations/saspro_fr.ts +11168 -7812
  101. setiastro/saspro/translations/saspro_hi.qm +0 -0
  102. setiastro/saspro/translations/saspro_hi.ts +14855 -0
  103. setiastro/saspro/translations/saspro_it.qm +0 -0
  104. setiastro/saspro/translations/saspro_it.ts +14347 -7821
  105. setiastro/saspro/translations/saspro_ja.qm +0 -0
  106. setiastro/saspro/translations/saspro_ja.ts +14860 -137
  107. setiastro/saspro/translations/saspro_pt.qm +0 -0
  108. setiastro/saspro/translations/saspro_pt.ts +14904 -137
  109. setiastro/saspro/translations/saspro_ru.qm +0 -0
  110. setiastro/saspro/translations/saspro_ru.ts +11835 -0
  111. setiastro/saspro/translations/saspro_sw.qm +0 -0
  112. setiastro/saspro/translations/saspro_sw.ts +15237 -0
  113. setiastro/saspro/translations/saspro_uk.qm +0 -0
  114. setiastro/saspro/translations/saspro_uk.ts +15248 -0
  115. setiastro/saspro/translations/saspro_zh.qm +0 -0
  116. setiastro/saspro/translations/saspro_zh.ts +10581 -7812
  117. setiastro/saspro/translations/sw_translations.py +3897 -0
  118. setiastro/saspro/translations/uk_translations.py +3929 -0
  119. setiastro/saspro/translations/zh_translations.py +283 -32
  120. setiastro/saspro/versioning.py +36 -5
  121. setiastro/saspro/view_bundle.py +20 -17
  122. setiastro/saspro/wavescale_hdr.py +22 -1
  123. setiastro/saspro/wavescalede.py +23 -1
  124. setiastro/saspro/whitebalance.py +39 -3
  125. setiastro/saspro/widgets/minigame/game.js +991 -0
  126. setiastro/saspro/widgets/minigame/index.html +53 -0
  127. setiastro/saspro/widgets/minigame/style.css +241 -0
  128. setiastro/saspro/widgets/resource_monitor.py +263 -0
  129. setiastro/saspro/widgets/spinboxes.py +18 -0
  130. setiastro/saspro/widgets/wavelet_utils.py +52 -20
  131. setiastro/saspro/wimi.py +100 -80
  132. setiastro/saspro/wims.py +33 -33
  133. setiastro/saspro/window_shelf.py +2 -2
  134. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/METADATA +15 -4
  135. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/RECORD +139 -115
  136. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/WHEEL +0 -0
  137. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/entry_points.txt +0 -0
  138. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/LICENSE +0 -0
  139. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/license.txt +0 -0
@@ -0,0 +1,991 @@
1
+ /**
2
+ * Neon Invaders: Hyperdrive
3
+ * Core Game Engine
4
+ */
5
+
6
+ const canvas = document.getElementById('gameCanvas');
7
+ const ctx = canvas.getContext('2d');
8
+
9
+ // --- CONSTANTS ---
10
+ const GAME_WIDTH = 800;
11
+ const GAME_HEIGHT = 600;
12
+ const FPS = 48; // Reduced to 80% speed (Original 60)
13
+ const DT = 1 / FPS;
14
+
15
+ const COLORS = {
16
+ PLAYER: '#0ff',
17
+ PLAYER_BULLET: '#0ff',
18
+ ENEMY_BASIC: '#f0f',
19
+ ENEMY_FAST: '#0f0',
20
+ ENEMY_HEAVY: '#f00',
21
+ ENEMY_BOSS: '#fff',
22
+ ENEMY_BULLET: '#f00',
23
+ PARTICLE_EXPLOSION: '#fa0',
24
+ PARTICLE_THRUST: '#0ff',
25
+ POWERUP_HP: '#f00',
26
+ POWERUP_HP: '#f00',
27
+ POWERUP_FIRE: '#0ff',
28
+ POWERUP_SHIELD: '#00ffff' // Cyan
29
+ };
30
+
31
+ const KEYS = {
32
+ LEFT: 'ArrowLeft',
33
+ RIGHT: 'ArrowRight',
34
+ UP: 'ArrowUp',
35
+ DOWN: 'ArrowDown',
36
+ SHOOT: ' '
37
+ };
38
+
39
+ // --- GLOBAL STATE ---
40
+ let gameState = 'MENU'; // MENU, PLAYING, GAMEOVER, VICTORY, LEVEL_SELECT
41
+ let lastTime = 0;
42
+ let score = 0;
43
+ let level = 1;
44
+
45
+ // --- INPUT HANDLER ---
46
+ const Input = {
47
+ keys: {},
48
+ init() {
49
+ window.addEventListener('keydown', e => this.keys[e.key] = true);
50
+ window.addEventListener('keyup', e => this.keys[e.key] = false);
51
+ },
52
+ isDown(key) { return this.keys[key]; }
53
+ };
54
+ Input.init();
55
+
56
+ // --- UTILS ---
57
+ const dist = (x1, y1, x2, y2) => Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
58
+ const rectIntersect = (r1, r2) => !(r2.left > r1.right || r2.right < r1.left || r2.top > r1.bottom || r2.bottom < r1.top);
59
+ const rand = (min, max) => Math.random() * (max - min) + min;
60
+
61
+ // --- AUDIO (Placeholder for synth) ---
62
+ const AudioSys = {
63
+ ctx: new (window.AudioContext || window.webkitAudioContext)(),
64
+ playTone(freq, type, duration) {
65
+ if (this.ctx.state === 'suspended') this.ctx.resume();
66
+ const osc = this.ctx.createOscillator();
67
+ const gain = this.ctx.createGain();
68
+ osc.type = type;
69
+ osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
70
+ gain.gain.setValueAtTime(0.1, this.ctx.currentTime);
71
+ gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration);
72
+ osc.connect(gain);
73
+ gain.connect(this.ctx.destination);
74
+ osc.start();
75
+ osc.stop(this.ctx.currentTime + duration);
76
+ },
77
+ shoot() { this.playTone(400, 'square', 0.1); },
78
+ explosion() { this.playTone(100, 'sawtooth', 0.3); },
79
+ hit() { this.playTone(200, 'sawtooth', 0.1); }
80
+ };
81
+
82
+ // --- CLASSES ---
83
+
84
+ class Starfield {
85
+ constructor() {
86
+ this.stars = [];
87
+ this.speed = 2; // Base speed
88
+ for (let i = 0; i < 100; i++) this.addStar(true);
89
+ }
90
+
91
+ addStar(randomY = false) {
92
+ this.stars.push({
93
+ x: rand(0, GAME_WIDTH),
94
+ y: randomY ? rand(0, GAME_HEIGHT) : -10,
95
+ z: rand(0.5, 2), // Depth factor for parallax
96
+ size: rand(0.5, 2)
97
+ });
98
+ }
99
+
100
+ update() {
101
+ // Speed locked (User Request: "same as level 1")
102
+ const currentSpeed = this.speed;
103
+ this.stars.forEach(s => {
104
+ s.y += currentSpeed * s.z;
105
+ if (s.y > GAME_HEIGHT) {
106
+ s.y = -10;
107
+ s.x = rand(0, GAME_WIDTH);
108
+ }
109
+ });
110
+ }
111
+
112
+ draw(ctx) {
113
+ // Change Star color based on level - More saturated/distinct
114
+ const colors = ['#ffffff', '#00ff00', '#ffff00', '#ff00ff', '#ff0000', '#00ffff', '#00ff00', '#ffff00', '#ff00ff', '#ff0000'];
115
+ ctx.fillStyle = colors[(level - 1) % colors.length] || '#fff';
116
+
117
+ this.stars.forEach(s => {
118
+ ctx.globalAlpha = s.z / 2;
119
+ ctx.beginPath();
120
+ ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
121
+ ctx.fill();
122
+ });
123
+ ctx.globalAlpha = 1;
124
+ }
125
+ }
126
+
127
+ class Particle {
128
+ constructor(x, y, color, speed, life) {
129
+ this.x = x;
130
+ this.y = y;
131
+ this.color = color;
132
+ this.life = life;
133
+ this.maxLife = life;
134
+ const angle = rand(0, Math.PI * 2);
135
+ this.vx = Math.cos(angle) * speed;
136
+ this.vy = Math.sin(angle) * speed;
137
+ this.size = rand(1, 3);
138
+ }
139
+ update() {
140
+ this.x += this.vx;
141
+ this.y += this.vy;
142
+ this.life--;
143
+ this.size *= 0.95;
144
+ }
145
+ draw(ctx) {
146
+ ctx.fillStyle = this.color;
147
+ ctx.globalAlpha = this.life / this.maxLife;
148
+ ctx.beginPath();
149
+ ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
150
+ ctx.fill();
151
+ ctx.globalAlpha = 1;
152
+ }
153
+ }
154
+
155
+ class Powerup {
156
+ constructor(type) {
157
+ this.type = type; // 'hp' or 'fire'
158
+ this.x = rand(50, GAME_WIDTH - 50);
159
+ this.y = -30;
160
+ this.width = 30;
161
+ this.height = 30;
162
+ this.speed = 2;
163
+ this.active = true;
164
+ this.speed = 2;
165
+ this.active = true;
166
+ this.color = type === 'hp' ? COLORS.POWERUP_HP : (type === 'fire' ? COLORS.POWERUP_FIRE : COLORS.POWERUP_SHIELD);
167
+ }
168
+
169
+ update() {
170
+ this.y += this.speed;
171
+ if (this.y > GAME_HEIGHT + 30) this.active = false;
172
+ }
173
+
174
+ draw(ctx) {
175
+ ctx.fillStyle = this.color;
176
+ ctx.shadowBlur = 15;
177
+ ctx.shadowColor = this.color;
178
+ ctx.beginPath();
179
+ if (this.type === 'hp') {
180
+ // Heart shape approximation or Cross
181
+ ctx.fillRect(this.x - 10, this.y - 4, 20, 8);
182
+ ctx.fillRect(this.x - 4, this.y - 10, 8, 20);
183
+ } else {
184
+ // Lightning bolt / Energy
185
+ ctx.moveTo(this.x, this.y - 15);
186
+ ctx.lineTo(this.x + 10, this.y);
187
+ ctx.lineTo(this.x - 5, this.y);
188
+ ctx.lineTo(this.x + 5, this.y + 15);
189
+ ctx.lineTo(this.x - 10, this.y);
190
+ ctx.lineTo(this.x + 5, this.y);
191
+ ctx.closePath();
192
+ ctx.fill();
193
+ }
194
+ ctx.shadowBlur = 0;
195
+ ctx.fill();
196
+ }
197
+
198
+ getBounds() {
199
+ return { left: this.x - 15, right: this.x + 15, top: this.y - 15, bottom: this.y + 15 };
200
+ }
201
+ }
202
+
203
+ class Bullet {
204
+ constructor(x, y, vy, isPlayer) {
205
+ this.x = x;
206
+ this.y = y;
207
+ this.vy = vy;
208
+ this.isPlayer = isPlayer;
209
+ this.width = 4;
210
+ this.height = 10;
211
+ this.active = true;
212
+ }
213
+
214
+ update() {
215
+ this.y += this.vy;
216
+ if (this.y < -50 || this.y > GAME_HEIGHT + 50) this.active = false;
217
+ }
218
+
219
+ draw(ctx) {
220
+ ctx.fillStyle = this.isPlayer ? COLORS.PLAYER_BULLET : COLORS.ENEMY_BULLET;
221
+ ctx.shadowBlur = 5;
222
+ ctx.shadowColor = ctx.fillStyle;
223
+ ctx.fillRect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height);
224
+ ctx.shadowBlur = 0;
225
+ }
226
+
227
+ getBounds() {
228
+ return { left: this.x - 2, right: this.x + 2, top: this.y - 5, bottom: this.y + 5 };
229
+ }
230
+ }
231
+
232
+ class Player {
233
+ constructor() {
234
+ this.width = 40;
235
+ this.height = 40;
236
+ this.x = GAME_WIDTH / 2;
237
+ this.y = GAME_HEIGHT - 100;
238
+ this.speed = 5;
239
+ this.hp = 100;
240
+ this.maxHp = 100;
241
+ this.cooldown = 0;
242
+ this.fireRate = 10;
243
+ this.fireRate = 10;
244
+ this.fireRateTimer = null; // Track timeout ID
245
+ this.dead = false;
246
+ this.shield = 0; // Shield strength (0 or 1)
247
+ }
248
+
249
+ update() {
250
+ if (this.dead) return;
251
+
252
+ // Movement
253
+ let dx = 0;
254
+ let dy = 0;
255
+ if (Input.isDown(KEYS.LEFT)) dx -= this.speed;
256
+ if (Input.isDown(KEYS.RIGHT)) dx += this.speed;
257
+ if (Input.isDown(KEYS.UP)) dy -= this.speed;
258
+ if (Input.isDown(KEYS.DOWN)) dy += this.speed;
259
+
260
+ this.x += dx;
261
+ this.y += dy;
262
+
263
+ // Clamp
264
+ this.x = Math.max(this.width / 2, Math.min(GAME_WIDTH - this.width / 2, this.x));
265
+ this.y = Math.max(this.height / 2, Math.min(GAME_HEIGHT - this.height / 2, this.y));
266
+
267
+ // Shoot
268
+ if (this.cooldown > 0) this.cooldown--;
269
+ if (Input.isDown(KEYS.SHOOT) && this.cooldown <= 0) {
270
+ Game.bullets.push(new Bullet(this.x, this.y - 20, -10, true));
271
+ if (this.fireRate < 10) { // Double shot for fast fire
272
+ setTimeout(() => Game.bullets.push(new Bullet(this.x, this.y - 20, -10, true)), 100);
273
+ }
274
+ AudioSys.shoot();
275
+ this.cooldown = this.fireRate;
276
+ }
277
+
278
+ // Thruster particles
279
+ if (Math.random() < 0.5) {
280
+ Game.particles.push(new Particle(this.x + rand(-5, 5), this.y + 20, COLORS.PARTICLE_THRUST, 1, 20));
281
+ }
282
+ }
283
+
284
+ draw(ctx) {
285
+ if (this.dead) return;
286
+ ctx.strokeStyle = COLORS.PLAYER;
287
+ ctx.lineWidth = 2;
288
+ ctx.shadowColor = COLORS.PLAYER;
289
+ ctx.shadowBlur = 10;
290
+
291
+ // Draw Ship (Triangle shape)
292
+ ctx.beginPath();
293
+ ctx.moveTo(this.x, this.y - 20); // Nose
294
+ ctx.lineTo(this.x + 20, this.y + 20); // Right Wing
295
+ ctx.lineTo(this.x, this.y + 10); // Center Engine
296
+ ctx.lineTo(this.x - 20, this.y + 20); // Left Wing
297
+ ctx.closePath();
298
+ ctx.stroke();
299
+
300
+ ctx.fillStyle = COLORS.PLAYER;
301
+ ctx.fill();
302
+
303
+ ctx.shadowBlur = 0;
304
+ }
305
+
306
+ hit(damage) {
307
+ if (this.shield > 0) {
308
+ this.shield--; // Absorb hit
309
+ AudioSys.hit(); // Maybe different sound?
310
+ return;
311
+ }
312
+ this.hp -= damage;
313
+ AudioSys.hit();
314
+ updateHUD();
315
+ if (this.hp <= 0) {
316
+ this.hp = 0;
317
+ this.dead = true;
318
+ Game.createExplosion(this.x, this.y, 50, COLORS.PLAYER);
319
+ setTimeout(() => Game.gameOver(), 1000);
320
+ }
321
+ }
322
+
323
+ getBounds() {
324
+ return { left: this.x - 15, right: this.x + 15, top: this.y - 15, bottom: this.y + 15 };
325
+ }
326
+ }
327
+
328
+ class Enemy {
329
+ constructor(type) {
330
+ this.type = type;
331
+ this.width = 30;
332
+ this.height = 30;
333
+ this.x = rand(50, GAME_WIDTH - 50);
334
+ this.y = -50;
335
+ this.active = true;
336
+ this.timer = Math.floor(rand(0, 100)); // Randomize start timer so they don't sync
337
+ this.lastShot = 0; // frame count
338
+
339
+
340
+ // Define stats based on type
341
+ switch (type) {
342
+ case 'basic':
343
+ this.hp = 2; // User requested 2 hits
344
+ this.score = 100;
345
+ this.speed = 1.5;
346
+ this.color = COLORS.ENEMY_BASIC;
347
+ this.shootInterval = 3 * 60; // 3 seconds (was 5)
348
+ break;
349
+ case 'fast':
350
+ this.hp = 1;
351
+ this.score = 50; // User requested 50
352
+ this.speed = 3;
353
+ this.width = 20; this.height = 20;
354
+ this.color = COLORS.ENEMY_FAST;
355
+ this.shootInterval = 0; // Doesn't shoot usually
356
+ break;
357
+ case 'heavy':
358
+ this.hp = 5; // Reduced from 10 to 5 (User Request)
359
+ this.speed = 0.8;
360
+ this.score = 250; // User requested 250
361
+ this.width = 50; this.height = 50;
362
+ this.color = COLORS.ENEMY_HEAVY;
363
+ this.shootInterval = 3 * 60; // 3 seconds
364
+ break;
365
+ }
366
+ }
367
+
368
+ update() {
369
+ this.y += this.speed;
370
+ this.timer++;
371
+
372
+ // Shooting logic replacement
373
+ // "I nemici non sparano se non arrivano ad un certo punto" -> Check if y > 50 (fully on screen)
374
+ if (this.shootInterval > 0 && this.timer % this.shootInterval === 0 && this.y > 50 && this.y < GAME_HEIGHT - 50) {
375
+ Game.bullets.push(new Bullet(this.x, this.y + this.height / 2, 5, false));
376
+ }
377
+
378
+ // Basic AI movement
379
+ if (this.type === 'fast') {
380
+ this.x += Math.sin(this.timer * 0.1) * 2;
381
+ }
382
+
383
+ // Random erratic shots for basic rarely? No, strict interval now.
384
+ // Removing old random shooting logic.
385
+
386
+ // Constrain X
387
+ if (this.x < 0) this.x = 0;
388
+ if (this.x > GAME_WIDTH) this.x = GAME_WIDTH;
389
+ }
390
+
391
+ draw(ctx) {
392
+ ctx.strokeStyle = this.color;
393
+ ctx.lineWidth = 2;
394
+ ctx.shadowColor = this.color;
395
+ ctx.shadowBlur = 10;
396
+
397
+ ctx.beginPath();
398
+ if (this.type === 'basic') {
399
+ ctx.rect(this.x - 15, this.y - 15, 30, 30);
400
+ } else if (this.type === 'fast') {
401
+ ctx.moveTo(this.x, this.y + 10);
402
+ ctx.lineTo(this.x + 10, this.y - 10);
403
+ ctx.lineTo(this.x - 10, this.y - 10);
404
+ ctx.closePath();
405
+ } else if (this.type === 'heavy') {
406
+ ctx.arc(this.x, this.y, 25, 0, Math.PI * 2);
407
+ }
408
+ ctx.stroke();
409
+ ctx.shadowBlur = 0;
410
+ }
411
+
412
+ hit(damage) {
413
+ if (this.y < 0) return; // Cannot hit off-screen enemies
414
+ this.hp -= damage;
415
+ AudioSys.hit();
416
+ if (this.hp <= 0) {
417
+ this.active = false;
418
+ score += this.score;
419
+ Game.createExplosion(this.x, this.y, 20, this.color);
420
+ updateHUD();
421
+ Game.checkLevelProgress();
422
+ }
423
+ }
424
+
425
+ getBounds() {
426
+ let w = this.width / 2;
427
+ let h = this.height / 2;
428
+ return { left: this.x - w, right: this.x + w, top: this.y - h, bottom: this.y + h };
429
+ }
430
+ }
431
+
432
+ class Boss {
433
+ constructor() {
434
+ this.width = 120;
435
+ this.height = 80;
436
+ this.x = GAME_WIDTH / 2;
437
+ this.y = -100;
438
+ this.active = true;
439
+ this.hp = 500;
440
+ this.maxHp = 500;
441
+ this.speed = 2;
442
+ this.timer = 0;
443
+ this.dir = 1;
444
+ this.color = COLORS.ENEMY_BOSS;
445
+ this.score = 5000;
446
+ }
447
+
448
+ update() {
449
+ this.timer++;
450
+
451
+ // Entrance
452
+ if (this.y < 100) {
453
+ this.y += 1;
454
+ } else {
455
+ // Horizontal Movement
456
+ this.x += this.speed * this.dir;
457
+ if (this.x > GAME_WIDTH - 80 || this.x < 80) this.dir *= -1;
458
+ }
459
+
460
+ // Attacks
461
+ // 1. Triple Shot (Front)
462
+ if (this.timer % 60 === 0) {
463
+ Game.bullets.push(new Bullet(this.x, this.y + 40, 5, false));
464
+ Game.bullets.push(new Bullet(this.x - 30, this.y + 30, 5, false));
465
+ Game.bullets.push(new Bullet(this.x + 30, this.y + 30, 5, false));
466
+ }
467
+
468
+ // 2. Spread Shot (every 3s)
469
+ if (this.timer % 180 === 0) {
470
+ for (let i = -2; i <= 2; i++) {
471
+ // Fake angle by setting vx
472
+ let b = new Bullet(this.x, this.y + 40, 4, false);
473
+ b.x += i * 15; // Offset start
474
+ b.width = 8;
475
+ Game.bullets.push(b);
476
+ }
477
+ }
478
+ }
479
+
480
+ draw(ctx) {
481
+ ctx.fillStyle = this.color;
482
+ ctx.shadowColor = this.color;
483
+ ctx.shadowBlur = 20;
484
+
485
+ // Custom shape for Mothership
486
+ ctx.beginPath();
487
+ ctx.moveTo(this.x, this.y + 40);
488
+ ctx.lineTo(this.x + 60, this.y - 20);
489
+ ctx.lineTo(this.x + 30, this.y - 40);
490
+ ctx.lineTo(this.x - 30, this.y - 40);
491
+ ctx.lineTo(this.x - 60, this.y - 20);
492
+ ctx.closePath();
493
+ ctx.fill();
494
+
495
+ // Reactor Core
496
+ ctx.fillStyle = '#ff0000';
497
+ ctx.beginPath();
498
+ ctx.arc(this.x, this.y, 15, 0, Math.PI * 2);
499
+ ctx.fill();
500
+
501
+ ctx.shadowBlur = 0;
502
+
503
+ // HP Bar (Above Boss)
504
+ ctx.fillStyle = '#555';
505
+ ctx.fillRect(this.x - 50, this.y - 60, 100, 10);
506
+ ctx.fillStyle = '#f00';
507
+ ctx.fillRect(this.x - 50, this.y - 60, 100 * (this.hp / this.maxHp), 10);
508
+ }
509
+
510
+ hit(damage) {
511
+ this.hp -= damage;
512
+ AudioSys.hit();
513
+ if (this.hp <= 0) {
514
+ this.active = false;
515
+ score += this.score;
516
+ Game.createExplosion(this.x, this.y, 100, '#fff');
517
+ updateHUD();
518
+ Game.victory(); // Win game after boss
519
+ }
520
+ }
521
+
522
+ getBounds() {
523
+ return { left: this.x - 50, right: this.x + 50, top: this.y - 30, bottom: this.y + 30 };
524
+ }
525
+ }
526
+
527
+ // --- GAME MANAGER ---
528
+ const Game = {
529
+ player: null,
530
+ bullets: [],
531
+ enemies: [],
532
+ particles: [],
533
+ powerups: [],
534
+ starfield: null,
535
+ waveTimer: 0,
536
+ enemiesToSpawn: 0,
537
+ spawnTimer: 0,
538
+ powerupQueue: [],
539
+ starfield: null,
540
+ waveTimer: 0,
541
+ enemiesToSpawn: 0,
542
+ spawnTimer: 0,
543
+
544
+ init() {
545
+ canvas.width = GAME_WIDTH;
546
+ canvas.height = GAME_HEIGHT;
547
+ this.starfield = new Starfield();
548
+ this.setupUI();
549
+ requestAnimationFrame(loop);
550
+ },
551
+
552
+ shake: 0,
553
+ triggerShake(amount) {
554
+ this.shake = amount;
555
+ },
556
+
557
+ startLevel(startLevel) {
558
+ level = startLevel;
559
+ if (!this.player) this.player = new Player();
560
+ this.bullets = [];
561
+ this.enemies = [];
562
+ this.particles = [];
563
+ this.powerups = [];
564
+ this.enemiesToSpawn = 10 * level;
565
+ this.waveTimer = 0;
566
+
567
+ // Initialize Counts (Base: 1 Fire, 1 HP)
568
+ let hpCount = 1;
569
+ let utilCount = 1;
570
+
571
+ if (level >= 3) hpCount++;
572
+ if (level >= 6) { hpCount += 2; utilCount += 2; } // L6-8: 4 HP, 3 Util
573
+ if (level >= 9) { hpCount += 1; utilCount += 1; } // L9-10: 5 HP, 4 Util
574
+
575
+ // 1. Utility
576
+ this.powerupQueue = [];
577
+ let timeOffset = 180 + rand(100, 300);
578
+ this.powerupQueue.push({ type: 'fire', time: timeOffset });
579
+
580
+ if (level >= 2) {
581
+ for (let i = 0; i < utilCount - 1; i++) {
582
+ timeOffset += rand(150, 400); // Faster intervals
583
+ this.powerupQueue.push({ type: 'shield', time: timeOffset });
584
+ }
585
+ }
586
+
587
+ // 2. HP
588
+ for (let i = 0; i < hpCount; i++) {
589
+ timeOffset += rand(150, 400); // Faster intervals
590
+ this.powerupQueue.push({ type: 'hp', time: timeOffset });
591
+ }
592
+ },
593
+
594
+ start(startLevel = 1) {
595
+ level = startLevel;
596
+ score = 0;
597
+ this.player = new Player();
598
+ this.bullets = [];
599
+ this.enemies = [];
600
+ this.particles = [];
601
+ this.powerups = [];
602
+ this.enemiesToSpawn = 10 * level; // Simple scaling
603
+
604
+ // Schedule powerups (frame count)
605
+ this.powerupQueue = [];
606
+ let timeOffset = rand(300, 600); // Start spawning after 5-10s
607
+
608
+ // Initialize Counts (Base: 1 Fire, 1 HP)
609
+ let hpCount = 1;
610
+ let utilCount = 1;
611
+
612
+ if (level >= 3) hpCount++;
613
+ if (level >= 6) { hpCount += 2; utilCount += 2; } // L6-8: 4 HP, 3 Util
614
+ if (level >= 9) { hpCount += 1; utilCount += 1; } // L9-10: 5 HP, 4 Util
615
+
616
+ // 1. Utility Powerups (Fire / Shield)
617
+ // Fire always 1
618
+ this.powerupQueue.push({ type: 'fire', time: timeOffset });
619
+
620
+ // Remaining Utility slots used for Shield (if Level >= 2)
621
+ if (level >= 2) {
622
+ // We used 1 for Fire, so loop remaining utilCount - 1
623
+ for (let i = 0; i < utilCount - 1; i++) {
624
+ timeOffset += rand(150, 400); // Faster intervals
625
+ this.powerupQueue.push({ type: 'shield', time: timeOffset });
626
+ }
627
+ }
628
+
629
+ // 2. HP Powerups
630
+ for (let i = 0; i < hpCount; i++) {
631
+ timeOffset += rand(150, 400); // Faster intervals
632
+ this.powerupQueue.push({ type: 'hp', time: timeOffset });
633
+ }
634
+
635
+ gameState = 'PLAYING';
636
+
637
+ document.getElementById('main-menu').classList.add('hidden');
638
+ document.getElementById('level-select').classList.add('hidden');
639
+ document.getElementById('game-over').classList.add('hidden');
640
+ document.getElementById('victory').classList.add('hidden');
641
+ document.getElementById('hud').classList.remove('hidden');
642
+ updateHUD();
643
+ },
644
+
645
+ setupUI() {
646
+ // Main Menu
647
+ document.getElementById('btn-start').onclick = () => this.start(1);
648
+ document.getElementById('btn-levels').onclick = () => {
649
+ document.getElementById('main-menu').classList.add('hidden');
650
+ document.getElementById('level-select').classList.remove('hidden');
651
+ this.renderLevelGrid();
652
+ };
653
+ // Level Select
654
+ document.getElementById('btn-back').onclick = () => {
655
+ document.getElementById('level-select').classList.add('hidden');
656
+ document.getElementById('main-menu').classList.remove('hidden');
657
+ };
658
+ // Game Over
659
+ document.getElementById('btn-retry').onclick = () => this.start(level);
660
+ document.getElementById('btn-menu').onclick = () => this.showMenu();
661
+ // Victory
662
+ document.getElementById('btn-menu-win').onclick = () => this.showMenu();
663
+ },
664
+
665
+ renderLevelGrid() {
666
+ const grid = document.getElementById('level-grid');
667
+ grid.innerHTML = '';
668
+ for (let i = 1; i <= 10; i++) {
669
+ const btn = document.createElement('button');
670
+ btn.className = 'level-btn';
671
+ btn.innerText = `LEVEL ${i}`;
672
+ btn.onclick = () => this.start(i);
673
+ grid.appendChild(btn);
674
+ }
675
+ },
676
+
677
+ showMenu() {
678
+ gameState = 'MENU';
679
+ document.getElementById('game-over').classList.add('hidden');
680
+ document.getElementById('victory').classList.add('hidden');
681
+ document.getElementById('hud').classList.add('hidden');
682
+ document.getElementById('main-menu').classList.remove('hidden');
683
+ },
684
+
685
+ gameOver() {
686
+ gameState = 'GAMEOVER';
687
+ document.getElementById('final-score').innerText = score;
688
+ document.getElementById('game-over').classList.remove('hidden');
689
+ },
690
+
691
+ victory() {
692
+ gameState = 'VICTORY';
693
+ document.getElementById('victory-score').innerText = score;
694
+ document.getElementById('victory').classList.remove('hidden');
695
+ },
696
+
697
+ spawnPowerup(type) {
698
+ this.powerups.push(new Powerup(type));
699
+ },
700
+
701
+ showLevelTransition() {
702
+ // Warp Effect Removed
703
+ // Text overlay
704
+ const t = document.createElement('div');
705
+ t.innerText = `LEVEL ${level}`;
706
+ t.innerText += `\nGET READY`;
707
+ t.style.textAlign = 'center';
708
+ t.className = 'level-transition';
709
+ t.style.position = 'absolute';
710
+ t.style.top = '50%';
711
+ t.style.left = '50%';
712
+ t.style.transform = 'translate(-50%, -50%)';
713
+ t.style.fontSize = '4rem';
714
+ t.style.color = '#fff';
715
+ t.style.textShadow = `0 0 20px ${this.getLevelColor()}`;
716
+ t.style.fontWeight = 'bold';
717
+ t.style.zIndex = '100';
718
+ t.style.animation = 'fadeUp 2s forwards';
719
+ document.body.appendChild(t);
720
+
721
+ const originalSpeed = this.starfield.speed;
722
+ // User requested removing warp speed effect
723
+ // this.starfield.speed = 20;
724
+ setTimeout(() => {
725
+ t.remove();
726
+ // this.starfield.speed = originalSpeed;
727
+ }, 2000);
728
+ },
729
+
730
+ getLevelColor() {
731
+ // Enhanced colors for text shadow
732
+ const colors = ['#00ffff', '#00ff00', '#ffff00', '#ff00ff', '#ff0000', '#00ffff', '#00ff00', '#ffff00', '#ff00ff', '#ff0000'];
733
+ return colors[(level - 1) % colors.length];
734
+ },
735
+
736
+ createExplosion(x, y, count, color) {
737
+ AudioSys.explosion();
738
+ this.triggerShake(count / 2);
739
+ for (let i = 0; i < count; i++) {
740
+ this.particles.push(new Particle(x, y, color, rand(1, 4), rand(20, 40)));
741
+ }
742
+ },
743
+
744
+ spawnEnemy() {
745
+ if (this.enemiesToSpawn <= 0) return;
746
+
747
+ // Spawn logic based on level
748
+ let type = 'basic';
749
+ const r = Math.random();
750
+
751
+ if (level === 2) {
752
+ if (r < 0.2) type = 'fast';
753
+ } else if (level >= 3) {
754
+ if (r < 0.3) type = 'fast';
755
+ if (r > 0.9) type = 'heavy';
756
+ } else if (level === 5 && this.enemiesToSpawn === 1) {
757
+ // Boss Spawn (Mid-point)
758
+ this.enemies.push(new Boss());
759
+ this.enemiesToSpawn--;
760
+ return;
761
+ } else if (level >= 6 && level < 10) {
762
+ // Harder scaling for 6-9
763
+ if (r < 0.4) type = 'fast';
764
+ if (r > 0.8) type = 'heavy';
765
+ } else if (level === 10 && this.enemiesToSpawn === 1) {
766
+ // Final Boss
767
+ let boss = new Boss();
768
+ boss.hp = 1000; // Double HP
769
+ boss.maxHp = 1000;
770
+ boss.color = '#fff'; // White Boss
771
+ this.enemies.push(boss);
772
+ this.enemiesToSpawn--;
773
+ return;
774
+ }
775
+
776
+ this.enemies.push(new Enemy(type));
777
+ this.enemiesToSpawn--;
778
+ },
779
+
780
+ checkLevelProgress() {
781
+ if (this.enemiesToSpawn === 0 && this.enemies.filter(e => e.active).length === 0) {
782
+ // Next Level
783
+ if (level === 10) {
784
+ this.victory();
785
+ } else {
786
+ this.nextLevel();
787
+ }
788
+ }
789
+ },
790
+
791
+ nextLevel() {
792
+ level++;
793
+ this.enemiesToSpawn = 10 * level + 5;
794
+ this.waveTimer = 0;
795
+
796
+ // Initialize Counts (Base: 1 Fire, 1 HP)
797
+ let hpCount = 1;
798
+ let utilCount = 1;
799
+
800
+ if (level >= 3) hpCount++;
801
+ if (level >= 6) { hpCount += 2; utilCount += 2; } // L6-8: 4 HP, 3 Util
802
+ if (level >= 9) { hpCount += 1; utilCount += 1; } // L9-10: 5 HP, 4 Util
803
+
804
+ // 1. Utility
805
+ this.powerupQueue = [];
806
+ let timeOffset = 180 + rand(100, 300);
807
+ this.powerupQueue.push({ type: 'fire', time: timeOffset });
808
+
809
+ if (level >= 2) {
810
+ for (let i = 0; i < utilCount - 1; i++) {
811
+ timeOffset += rand(150, 400); // Faster intervals
812
+ this.powerupQueue.push({ type: 'shield', time: timeOffset });
813
+ }
814
+ }
815
+
816
+ // 2. HP
817
+ for (let i = 0; i < hpCount; i++) {
818
+ timeOffset += rand(150, 400); // Faster intervals
819
+ this.powerupQueue.push({ type: 'hp', time: timeOffset });
820
+ }
821
+
822
+
823
+
824
+ this.showLevelTransition();
825
+ updateHUD();
826
+ },
827
+
828
+ update() {
829
+ if (gameState !== 'PLAYING') {
830
+ this.starfield.update(); // Keep stars moving in menu
831
+ return;
832
+ }
833
+
834
+ this.starfield.update();
835
+ this.player.update();
836
+
837
+ // Spawning
838
+ this.spawnTimer++;
839
+
840
+ // Delay spawning at start of level (3 seconds = 180 frames) or if enemiesToSpawn is empty
841
+ if (this.waveTimer >= 180) {
842
+ // Faster spawn rate: Cap at Level 5
843
+ // Level 1: 90 - 8 = 82 frames.
844
+ // Level 5: 90 - 40 = 50 frames.
845
+ // Level 10: Still 50 frames (using Math.min)
846
+ const effectiveLevel = Math.min(level, 5);
847
+ if (this.spawnTimer > 90 - (effectiveLevel * 8)) {
848
+ this.spawnEnemy();
849
+ this.spawnTimer = 0;
850
+ }
851
+ }
852
+
853
+ // Powerup Spawning
854
+ this.waveTimer++;
855
+ for (let i = this.powerupQueue.length - 1; i >= 0; i--) {
856
+ if (this.waveTimer >= this.powerupQueue[i].time) {
857
+ this.spawnPowerup(this.powerupQueue[i].type);
858
+ this.powerupQueue.splice(i, 1);
859
+ }
860
+ }
861
+
862
+ // Entities
863
+ this.bullets = this.bullets.filter(b => b.active);
864
+ this.bullets.forEach(b => b.update());
865
+
866
+ this.powerups = this.powerups.filter(p => p.active);
867
+ this.powerups.forEach(p => p.update());
868
+
869
+ this.enemies = this.enemies.filter(e => e.active);
870
+ this.enemies.forEach(e => e.update());
871
+
872
+ this.particles = this.particles.filter(p => p.life > 0);
873
+ this.particles.forEach(p => p.update());
874
+
875
+ // Collisions
876
+ // Player Bullets -> Enemies
877
+ this.bullets.filter(b => b.isPlayer).forEach(b => {
878
+ this.enemies.forEach(e => {
879
+ if (rectIntersect(b.getBounds(), e.getBounds()) && e.y > 0) { // Check visibility
880
+ b.active = false;
881
+ e.hit(Game.player.fireRate < 10 ? 2 : 1); // Double damage if fast fire? Or just more bullets. Keep damage 1 but fire faster.
882
+ Game.createExplosion(b.x, b.y, 5, b.isPlayer ? COLORS.PLAYER_BULLET : COLORS.ENEMY_BULLET);
883
+ }
884
+ });
885
+ });
886
+
887
+ // Player -> Powerups
888
+ this.powerups.forEach(p => {
889
+ if (rectIntersect(p.getBounds(), this.player.getBounds())) {
890
+ p.active = false;
891
+ if (p.type === 'hp') {
892
+ this.player.hp = this.player.maxHp; // Full Restore
893
+ updateHUD();
894
+ } else if (p.type === 'fire') {
895
+ this.player.fireRate = 5;
896
+ // Clear existing timer if any to extend duration instead of cutting it short
897
+ if (this.player.fireRateTimer) clearTimeout(this.player.fireRateTimer);
898
+ this.player.fireRateTimer = setTimeout(() => {
899
+ if (this.player) {
900
+ this.player.fireRate = 10;
901
+ this.player.fireRateTimer = null;
902
+ }
903
+ }, 5000);
904
+ } else if (p.type === 'shield') {
905
+ this.player.shield = 1;
906
+ }
907
+ }
908
+ });
909
+
910
+ // Enemy Bullets -> Player
911
+ this.bullets.filter(b => !b.isPlayer).forEach(b => {
912
+ if (rectIntersect(b.getBounds(), this.player.getBounds())) {
913
+ b.active = false;
914
+ this.player.hit(20);
915
+ }
916
+ });
917
+
918
+ // Enemies -> Player (Crash) or Escaped
919
+ this.enemies.forEach(e => {
920
+ if (e.y > GAME_HEIGHT + 30) {
921
+ e.active = false;
922
+ // Penalty for letting enemy pass
923
+ this.player.hit(10);
924
+ Game.checkLevelProgress();
925
+ } else if (rectIntersect(e.getBounds(), this.player.getBounds())) {
926
+ e.hit(100); // Enemy dies
927
+ this.player.hit(30); // Player takes damage
928
+ }
929
+ });
930
+ },
931
+
932
+ draw() {
933
+ // Clear - Background Color changes significantly per level
934
+ // Clear - Background Color changes significantly per level
935
+ // More visible changes: Dark Blue, Dark Green, Dark Olive, Dark Purple, Dark Red
936
+ const bgColors = ['#000022', '#002200', '#222200', '#220022', '#220000', '#000022', '#002200', '#222200', '#220022', '#220000'];
937
+ ctx.fillStyle = bgColors[(level - 1) % bgColors.length];
938
+ ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
939
+
940
+ ctx.save();
941
+ if (this.shake > 0) {
942
+ const dx = Math.random() * this.shake - this.shake / 2;
943
+ const dy = Math.random() * this.shake - this.shake / 2;
944
+ ctx.translate(dx, dy);
945
+ this.shake *= 0.9;
946
+ if (this.shake < 0.5) this.shake = 0;
947
+ }
948
+
949
+ // Stars
950
+ this.starfield.draw(ctx);
951
+
952
+ if (gameState === 'PLAYING') {
953
+ this.player.draw(ctx);
954
+ this.powerups.forEach(p => p.draw(ctx));
955
+ this.enemies.forEach(e => e.draw(ctx));
956
+ this.bullets.forEach(b => b.draw(ctx));
957
+ this.particles.forEach(p => p.draw(ctx));
958
+ }
959
+ ctx.restore();
960
+ }
961
+ };
962
+
963
+ function updateHUD() {
964
+ document.getElementById('score').innerText = score;
965
+ document.getElementById('level').innerText = level;
966
+ if (Game.player) {
967
+ const pct = (Game.player.hp / Game.player.maxHp) * 100;
968
+ document.getElementById('hp-fill').style.width = `${Math.max(0, pct)}%`;
969
+ }
970
+ }
971
+
972
+ // Game Loop
973
+ let lastTimeMs = 0;
974
+ const FRAME_INTERVAL = 1000 / FPS;
975
+
976
+ function loop(timestamp) {
977
+ requestAnimationFrame(loop);
978
+
979
+ if (!lastTimeMs) lastTimeMs = timestamp;
980
+ const elapsed = timestamp - lastTimeMs;
981
+
982
+ if (elapsed > FRAME_INTERVAL) {
983
+ lastTimeMs = timestamp - (elapsed % FRAME_INTERVAL);
984
+ Game.update();
985
+ Game.draw();
986
+ }
987
+ }
988
+
989
+ // Start
990
+ Game.init();
991
+ window.Game = Game; // Expose for debugging