bebechien commited on
Commit
7ebea6e
·
verified ·
1 Parent(s): fd2f84d

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +805 -19
index.html CHANGED
@@ -1,19 +1,805 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Nova Blossom</title>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.7.77/Tone.js"></script>
8
+ <style>
9
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
10
+
11
+ body {
12
+ margin: 0;
13
+ padding: 0;
14
+ display: flex;
15
+ justify-content: center;
16
+ align-items: center;
17
+ height: 100vh;
18
+ background-color: #0c0a1a;
19
+ font-family: 'Inter', sans-serif;
20
+ color: #e0e0ff;
21
+ overflow: hidden;
22
+ }
23
+
24
+ #game-container {
25
+ display: flex;
26
+ justify-content: center;
27
+ align-items: center;
28
+ width: 100%;
29
+ height: 100%;
30
+ }
31
+
32
+ #ui-wrapper {
33
+ display: flex;
34
+ justify-content: center;
35
+ max-width: 1000px;
36
+ width: 100%;
37
+ aspect-ratio: 4 / 3;
38
+ max-height: 95vh;
39
+ position: relative;
40
+ }
41
+
42
+ #game-canvas {
43
+ background-color: #121020;
44
+ border-left: 1px solid #4a4a8a;
45
+ border-right: 1px solid #4a4a8a;
46
+ width: 60%;
47
+ height: 100%;
48
+ }
49
+
50
+ .ui-panel {
51
+ width: 20%;
52
+ padding: 20px;
53
+ box-sizing: border-box;
54
+ background-color: #1a182f;
55
+ display: flex;
56
+ flex-direction: column;
57
+ justify-content: space-between;
58
+ }
59
+
60
+ .ui-panel h2 {
61
+ color: #ff79c6;
62
+ text-align: center;
63
+ margin-top: 0;
64
+ border-bottom: 1px solid #4a4a8a;
65
+ padding-bottom: 10px;
66
+ }
67
+
68
+ .info-box {
69
+ margin-bottom: 20px;
70
+ }
71
+
72
+ .info-box p {
73
+ margin: 5px 0;
74
+ }
75
+
76
+ .info-box .label {
77
+ color: #8be9fd;
78
+ font-size: 0.9em;
79
+ }
80
+
81
+ .info-box .value {
82
+ color: #f1fa8c;
83
+ font-size: 1.5em;
84
+ font-weight: bold;
85
+ text-shadow: 0 0 5px #f1fa8c;
86
+ }
87
+
88
+ #blossom-meter-container {
89
+ width: 100%;
90
+ height: 25px;
91
+ background-color: #282a36;
92
+ border: 1px solid #4a4a8a;
93
+ border-radius: 5px;
94
+ overflow: hidden;
95
+ margin-top: auto;
96
+ }
97
+
98
+ #blossom-meter-fill {
99
+ width: 0%;
100
+ height: 100%;
101
+ background: linear-gradient(90deg, #ff79c6, #ff5555);
102
+ transition: width 0.1s linear;
103
+ }
104
+
105
+ .game-overlay {
106
+ position: absolute;
107
+ top: 0;
108
+ left: 0;
109
+ width: 100%;
110
+ height: 100%;
111
+ background-color: rgba(12, 10, 26, 0.8);
112
+ display: flex;
113
+ flex-direction: column;
114
+ justify-content: center;
115
+ align-items: center;
116
+ text-align: center;
117
+ z-index: 10;
118
+ }
119
+
120
+ .game-overlay h1 {
121
+ font-size: 3em;
122
+ color: #ff79c6;
123
+ margin-bottom: 10px;
124
+ }
125
+
126
+ .game-overlay p {
127
+ font-size: 1.2em;
128
+ max-width: 60%;
129
+ margin-bottom: 20px;
130
+ }
131
+
132
+ .game-overlay button {
133
+ padding: 15px 30px;
134
+ font-size: 1.2em;
135
+ background-color: #bd93f9;
136
+ color: #1a182f;
137
+ border: none;
138
+ border-radius: 8px;
139
+ cursor: pointer;
140
+ font-weight: bold;
141
+ transition: background-color 0.3s, transform 0.2s;
142
+ box-shadow: 0 4px 15px rgba(189, 147, 249, 0.4);
143
+ }
144
+
145
+ .game-overlay button:hover {
146
+ background-color: #ff79c6;
147
+ transform: scale(1.05);
148
+ }
149
+
150
+ .controls {
151
+ margin-top: 30px;
152
+ background: rgba(40, 42, 54, 0.7);
153
+ padding: 15px;
154
+ border-radius: 10px;
155
+ }
156
+
157
+ .controls p {
158
+ margin: 5px 0;
159
+ font-size: 1em;
160
+ }
161
+
162
+ .controls strong {
163
+ color: #8be9fd;
164
+ }
165
+ </style>
166
+ </head>
167
+ <body>
168
+
169
+ <div id="game-container">
170
+ <div id="ui-wrapper">
171
+ <!-- Left UI Panel -->
172
+ <div class="ui-panel">
173
+ <div>
174
+ <h2>STATUS</h2>
175
+ <div class="info-box">
176
+ <p class="label">LIVES</p>
177
+ <p class="value" id="lives-display">3</p>
178
+ </div>
179
+ <div class="info-box">
180
+ <p class="label">BLOSSOM BURSTS</p>
181
+ <p class="value" id="bombs-display">1</p>
182
+ </div>
183
+ </div>
184
+ <div>
185
+ <p class="label">BLOSSOM METER</p>
186
+ <div id="blossom-meter-container">
187
+ <div id="blossom-meter-fill"></div>
188
+ </div>
189
+ </div>
190
+ </div>
191
+
192
+ <!-- Game Canvas -->
193
+ <canvas id="game-canvas"></canvas>
194
+
195
+ <!-- Right UI Panel -->
196
+ <div class="ui-panel">
197
+ <div>
198
+ <h2>SCORE</h2>
199
+ <div class="info-box">
200
+ <p class="label">HIGH SCORE</p>
201
+ <p class="value" id="highscore-display">0</p>
202
+ </div>
203
+ <div class="info-box">
204
+ <p class="label">CURRENT SCORE</p>
205
+ <p class="value" id="score-display">0</p>
206
+ </div>
207
+ <div class="info-box">
208
+ <p class="label">GRAZE</p>
209
+ <p class="value" id="graze-display">0</p>
210
+ </div>
211
+ </div>
212
+ </div>
213
+
214
+ <!-- Game Overlays -->
215
+ <div class="game-overlay" id="start-screen">
216
+ <h1>Nova Blossom</h1>
217
+ <p>Weave through celestial curtains of bullets and turn danger into power. Graze enemy fire to charge your Blossom Burst and unleash your true potential.</p>
218
+ <button id="start-button">Start Game</button>
219
+ <div class="controls">
220
+ <p><strong>Arrow Keys:</strong> Move</p>
221
+ <p><strong>Shift:</strong> Focus (Slow Movement / Laser Shot)</p>
222
+ <p><strong>X:</strong> Use Blossom Burst (Bomb)</p>
223
+ </div>
224
+ </div>
225
+ <div class="game-overlay" id="game-over-screen" style="display: none;">
226
+ <h1>Game Over</h1>
227
+ <p id="final-score">Your score: 0</p>
228
+ <button id="restart-button">Play Again</button>
229
+ </div>
230
+ </div>
231
+ </div>
232
+
233
+ <script>
234
+ document.addEventListener('DOMContentLoaded', () => {
235
+ const canvas = document.getElementById('game-canvas');
236
+ const ctx = canvas.getContext('2d');
237
+
238
+ const scoreDisplay = document.getElementById('score-display');
239
+ const highscoreDisplay = document.getElementById('highscore-display');
240
+ const livesDisplay = document.getElementById('lives-display');
241
+ const bombsDisplay = document.getElementById('bombs-display');
242
+ const grazeDisplay = document.getElementById('graze-display');
243
+ const blossomMeterFill = document.getElementById('blossom-meter-fill');
244
+ const startScreen = document.getElementById('start-screen');
245
+ const gameOverScreen = document.getElementById('game-over-screen');
246
+ const finalScoreDisplay = document.getElementById('final-score');
247
+ const startButton = document.getElementById('start-button');
248
+ const restartButton = document.getElementById('restart-button');
249
+
250
+ let canvasWidth, canvasHeight;
251
+ let player, keys = {}, playerBullets = [], enemyBullets = [], enemies = [], particles = [], scoreItems = [];
252
+ let score = 0, highScore = localStorage.getItem('novaBlossomHighScore') || 0;
253
+ let grazeCount = 0;
254
+ let lives = 3;
255
+ let bombs = 3;
256
+ let blossomMeter = 0;
257
+ const blossomMeterMax = 500;
258
+ let frameCount = 0;
259
+ let isGameOver = true;
260
+ let audioInitialized = false;
261
+
262
+ // --- Audio Synthesis with Tone.js ---
263
+ let synths = {};
264
+ function initAudio() {
265
+ if (audioInitialized) return;
266
+ synths.playerShoot = new Tone.PolySynth(Tone.Synth, {
267
+ oscillator: { type: "triangle" },
268
+ envelope: { attack: 0.005, decay: 0.1, sustain: 0, release: 0.1 },
269
+ volume: -20
270
+ }).toDestination();
271
+ synths.playerLaser = new Tone.NoiseSynth({
272
+ noise: { type: 'pink' },
273
+ envelope: { attack: 0.01, decay: 0.05, sustain: 0, release: 0.1 },
274
+ volume: -15
275
+ }).toDestination();
276
+ synths.graze = new Tone.Synth({
277
+ oscillator: { type: "sine" },
278
+ envelope: { attack: 0.001, decay: 0.1, sustain: 0, release: 0.1 },
279
+ volume: -10
280
+ }).toDestination();
281
+ synths.enemyHit = new Tone.Synth({
282
+ oscillator: { type: "square" },
283
+ envelope: { attack: 0.01, decay: 0.2, sustain: 0, release: 0.2 },
284
+ volume: -15
285
+ }).toDestination();
286
+ synths.itemCollect = new Tone.Synth({
287
+ oscillator: { type: "sine" },
288
+ envelope: { attack: 0.001, decay: 0.2, sustain: 0, release: 0.2 },
289
+ volume: -15
290
+ }).toDestination();
291
+ synths.playerHit = new Tone.NoiseSynth({
292
+ noise: { type: 'white' },
293
+ envelope: { attack: 0.01, decay: 0.3, sustain: 0, release: 0.3 },
294
+ volume: -5
295
+ }).toDestination();
296
+ synths.blossomBurst = new Tone.MembraneSynth({
297
+ pitchDecay: 0.01,
298
+ octaves: 10,
299
+ oscillator: { type: "sine" },
300
+ envelope: { attack: 0.001, decay: 0.8, sustain: 0.01, release: 1.4, attackCurve: "exponential" },
301
+ volume: -2
302
+ }).toDestination();
303
+ audioInitialized = true;
304
+ }
305
+
306
+ function resizeCanvas() {
307
+ const wrapper = document.getElementById('ui-wrapper');
308
+ canvas.width = canvas.offsetWidth;
309
+ canvas.height = canvas.offsetHeight;
310
+ canvasWidth = canvas.width;
311
+ canvasHeight = canvas.height;
312
+ }
313
+
314
+ window.addEventListener('resize', resizeCanvas);
315
+
316
+ function setupGame() {
317
+ resizeCanvas();
318
+ isGameOver = false;
319
+ score = 0;
320
+ grazeCount = 0;
321
+ lives = 3;
322
+ bombs = 1;
323
+ blossomMeter = 0;
324
+ frameCount = 0;
325
+ player = new Player();
326
+ playerBullets = [];
327
+ enemyBullets = [];
328
+ enemies = [];
329
+ particles = [];
330
+ scoreItems = [];
331
+ updateUI();
332
+ gameOverScreen.style.display = 'none';
333
+ gameLoop();
334
+ }
335
+
336
+ // --- Event Listeners ---
337
+ startButton.addEventListener('click', () => {
338
+ Tone.start();
339
+ initAudio();
340
+ startScreen.style.display = 'none';
341
+ setupGame();
342
+ });
343
+
344
+ restartButton.addEventListener('click', () => {
345
+ setupGame();
346
+ });
347
+
348
+ window.addEventListener('keydown', e => keys[e.code] = true);
349
+ window.addEventListener('keyup', e => {
350
+ keys[e.code] = false;
351
+ // Blossom Burst on key press (not hold)
352
+ if (e.code === 'KeyX' && !isGameOver) {
353
+ player.useBomb();
354
+ }
355
+ });
356
+
357
+ // --- Game Classes ---
358
+ class Player {
359
+ constructor() {
360
+ this.x = canvasWidth / 2;
361
+ this.y = canvasHeight - 50;
362
+ this.width = 30;
363
+ this.height = 40;
364
+ this.speed = 5;
365
+ this.focusedSpeed = 2.5;
366
+ this.hitboxRadius = 3;
367
+ this.grazeRadius = 30;
368
+ this.shootCooldown = 0;
369
+ this.invincible = false;
370
+ this.invincibilityTimer = 0;
371
+ }
372
+
373
+ update() {
374
+ const currentSpeed = keys['ShiftLeft'] || keys['ShiftRight'] ? this.focusedSpeed : this.speed;
375
+
376
+ if (keys['ArrowUp']) this.y -= currentSpeed;
377
+ if (keys['ArrowDown']) this.y += currentSpeed;
378
+ if (keys['ArrowLeft']) this.x -= currentSpeed;
379
+ if (keys['ArrowRight']) this.x += currentSpeed;
380
+
381
+ this.x = Math.max(this.width / 2, Math.min(canvasWidth - this.width / 2, this.x));
382
+ this.y = Math.max(this.height / 2, Math.min(canvasHeight - this.height / 2, this.y));
383
+
384
+ if (this.shootCooldown > 0) this.shootCooldown--;
385
+ this.shoot();
386
+
387
+ if (this.invincibilityTimer > 0) {
388
+ this.invincibilityTimer--;
389
+ if (this.invincibilityTimer <= 0) {
390
+ this.invincible = false;
391
+ }
392
+ }
393
+ }
394
+
395
+ draw() {
396
+ ctx.save();
397
+ // Invincibility flash
398
+ if (this.invincible) {
399
+ ctx.globalAlpha = Math.abs(Math.sin(this.invincibilityTimer * 0.2));
400
+ }
401
+
402
+ // Ship Body
403
+ ctx.fillStyle = '#bd93f9';
404
+ ctx.beginPath();
405
+ ctx.moveTo(this.x, this.y - this.height / 2);
406
+ ctx.lineTo(this.x - this.width / 2, this.y + this.height / 2);
407
+ ctx.lineTo(this.x + this.width / 2, this.y + this.height / 2);
408
+ ctx.closePath();
409
+ ctx.fill();
410
+ ctx.strokeStyle = '#f1fa8c';
411
+ ctx.lineWidth = 2;
412
+ ctx.stroke();
413
+
414
+ // Engine glow
415
+ ctx.fillStyle = '#ff79c6';
416
+ ctx.fillRect(this.x - 5, this.y + this.height/2, 10, 5);
417
+
418
+ // Focused state indicator and hitbox
419
+ if (keys['ShiftLeft'] || keys['ShiftRight']) {
420
+ ctx.fillStyle = 'rgba(255, 121, 198, 0.3)';
421
+ ctx.beginPath();
422
+ ctx.arc(this.x, this.y, this.grazeRadius, 0, Math.PI * 2);
423
+ ctx.fill();
424
+
425
+ ctx.fillStyle = '#f1fa8c';
426
+ ctx.beginPath();
427
+ ctx.arc(this.x, this.y, this.hitboxRadius + 2, 0, Math.PI * 2);
428
+ ctx.fill();
429
+ }
430
+
431
+ // Hitbox center
432
+ ctx.fillStyle = '#ff5555';
433
+ ctx.beginPath();
434
+ ctx.arc(this.x, this.y, this.hitboxRadius, 0, Math.PI * 2);
435
+ ctx.fill();
436
+ ctx.restore();
437
+ }
438
+
439
+ shoot() {
440
+ if (this.shootCooldown <= 0) {
441
+ const isFocused = keys['ShiftLeft'] || keys['ShiftRight'];
442
+ if (isFocused) {
443
+ playerBullets.push(new PlayerBullet(this.x, this.y, 0, -15, 10, '#8be9fd'));
444
+ playerBullets.push(new PlayerBullet(this.x - 7, this.y, 0, -15, 8, '#8be9fd'));
445
+ playerBullets.push(new PlayerBullet(this.x + 7, this.y, 0, -15, 8, '#8be9fd'));
446
+ this.shootCooldown = 4;
447
+ synths.playerLaser?.triggerAttackRelease("32n", Tone.now());
448
+ } else {
449
+ playerBullets.push(new PlayerBullet(this.x - 10, this.y, -0.5, -12, 5, '#50fa7b'));
450
+ playerBullets.push(new PlayerBullet(this.x + 10, this.y, 0.5, -12, 5, '#50fa7b'));
451
+ playerBullets.push(new PlayerBullet(this.x, this.y - 10, 0, -12, 5, '#50fa7b'));
452
+ this.shootCooldown = 6;
453
+ synths.playerShoot?.triggerAttackRelease(["C5", "E5"], "16n", Tone.now());
454
+ }
455
+ }
456
+ }
457
+
458
+ takeHit() {
459
+ if (this.invincible) return;
460
+
461
+ synths.playerHit?.triggerAttackRelease("8n", Tone.now());
462
+ lives--;
463
+ grazeCount = 0; // Reset graze on hit
464
+ if (lives <= 0) {
465
+ gameOver();
466
+ } else {
467
+ this.invincible = true;
468
+ this.invincibilityTimer = 120; // 2 seconds of invincibility
469
+ for (let i = 0; i < 50; i++) {
470
+ particles.push(new Particle(this.x, this.y, Math.random() * 4 + 2, '#ff5555', 60));
471
+ }
472
+ }
473
+ updateUI();
474
+ }
475
+
476
+ useBomb() {
477
+ if (bombs > 0) {
478
+ synths.blossomBurst?.triggerAttackRelease("C2", "1s", Tone.now());
479
+ bombs--;
480
+ this.invincible = true;
481
+ this.invincibilityTimer = 180; // 3 seconds invincibility
482
+
483
+ enemyBullets.forEach(b => {
484
+ scoreItems.push(new ScoreItem(b.x, b.y, 50));
485
+ for (let i = 0; i < 2; i++) {
486
+ particles.push(new Particle(b.x, b.y, Math.random() * 2 + 1, '#ff79c6', 30));
487
+ }
488
+ });
489
+ enemyBullets = [];
490
+
491
+ enemies.forEach(e => e.health -= 50);
492
+
493
+ // Visual effect
494
+ particles.push(new Particle(this.x, this.y, canvasWidth, 'rgba(255, 121, 198, 0.5)', 30, true));
495
+
496
+ updateUI();
497
+ }
498
+ }
499
+ }
500
+
501
+ class Bullet {
502
+ constructor(x, y, vx, vy, radius, color) {
503
+ this.x = x;
504
+ this.y = y;
505
+ this.vx = vx;
506
+ this.vy = vy;
507
+ this.radius = radius;
508
+ this.color = color;
509
+ this.grazed = false;
510
+ }
511
+
512
+ update() {
513
+ this.x += this.vx;
514
+ this.y += this.vy;
515
+ }
516
+
517
+ draw() {
518
+ ctx.beginPath();
519
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
520
+ ctx.fillStyle = this.color;
521
+ ctx.fill();
522
+ ctx.strokeStyle = 'white';
523
+ ctx.lineWidth = 1;
524
+ ctx.stroke();
525
+ }
526
+ }
527
+
528
+ class PlayerBullet extends Bullet {
529
+ constructor(x, y, vx, vy, damage, color) {
530
+ super(x, y, vx, vy, 5, color);
531
+ this.damage = damage;
532
+ }
533
+ }
534
+
535
+ class Enemy {
536
+ constructor(x, y, health, type) {
537
+ this.x = x;
538
+ this.y = y;
539
+ this.health = health;
540
+ this.type = type;
541
+ this.width = 40;
542
+ this.height = 40;
543
+ this.shootTimer = Math.random() * 60;
544
+ }
545
+
546
+ update() {
547
+ this.y += 1.5;
548
+ this.shootTimer--;
549
+ if (this.shootTimer <= 0) {
550
+ this.shoot();
551
+ }
552
+ }
553
+
554
+ draw() {
555
+ ctx.fillStyle = '#ff5555';
556
+ ctx.beginPath();
557
+ ctx.moveTo(this.x, this.y + this.height/2);
558
+ ctx.lineTo(this.x - this.width/2, this.y - this.height/2);
559
+ ctx.lineTo(this.x + this.width/2, this.y - this.height/2);
560
+ ctx.closePath();
561
+ ctx.fill();
562
+ ctx.strokeStyle = '#f8f8f2';
563
+ ctx.stroke();
564
+ }
565
+
566
+ shoot() {
567
+ if (this.type === 1) { // Simple aimed shot
568
+ const angle = Math.atan2(player.y - this.y, player.x - this.x);
569
+ const speed = 3;
570
+ enemyBullets.push(new Bullet(this.x, this.y, Math.cos(angle) * speed, Math.sin(angle) * speed, 5, '#f1fa8c'));
571
+ this.shootTimer = 80;
572
+ } else if (this.type === 2) { // Spiral pattern
573
+ const bulletsInSpiral = 8;
574
+ const speed = 2.5;
575
+ for (let i = 0; i < bulletsInSpiral; i++) {
576
+ const angle = (frameCount * 0.1 + (i / bulletsInSpiral) * Math.PI * 2);
577
+ enemyBullets.push(new Bullet(this.x, this.y, Math.cos(angle) * speed, Math.sin(angle) * speed, 4, '#ffb86c'));
578
+ }
579
+ this.shootTimer = 20;
580
+ }
581
+ }
582
+
583
+ takeDamage(amount, timeOffset = 0) {
584
+ this.health -= amount;
585
+ synths.enemyHit?.triggerAttackRelease("G2", "8n", Tone.now() + timeOffset);
586
+ for (let i = 0; i < 3; i++) {
587
+ particles.push(new Particle(this.x + (Math.random() - 0.5) * this.width, this.y + (Math.random() - 0.5) * this.height, Math.random() * 2 + 1, '#8be9fd', 20));
588
+ }
589
+ }
590
+ }
591
+
592
+ class Particle {
593
+ constructor(x, y, radius, color, lifespan, isCircle=false) {
594
+ this.x = x;
595
+ this.y = y;
596
+ this.radius = isCircle ? 0 : radius;
597
+ this.maxRadius = radius;
598
+ this.color = color;
599
+ this.lifespan = lifespan;
600
+ this.isCircle = isCircle;
601
+ if (!isCircle) {
602
+ this.vx = (Math.random() - 0.5) * 5;
603
+ this.vy = (Math.random() - 0.5) * 5;
604
+ }
605
+ }
606
+
607
+ update() {
608
+ this.lifespan--;
609
+ if(this.isCircle) {
610
+ this.radius += this.maxRadius / 30;
611
+ } else {
612
+ this.x += this.vx;
613
+ this.y += this.vy;
614
+ this.vx *= 0.95;
615
+ this.vy *= 0.95;
616
+ }
617
+ }
618
+
619
+ draw() {
620
+ ctx.save();
621
+ ctx.globalAlpha = this.lifespan / 60;
622
+ ctx.beginPath();
623
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
624
+ ctx.fillStyle = this.color;
625
+ ctx.fill();
626
+ ctx.restore();
627
+ }
628
+ }
629
+
630
+ class ScoreItem {
631
+ constructor(x, y, value) {
632
+ this.x = x;
633
+ this.y = y;
634
+ this.value = value;
635
+ this.radius = 5;
636
+ this.vy = 1;
637
+ }
638
+
639
+ update() {
640
+ // Move towards player if close
641
+ const dist = Math.hypot(this.x - player.x, this.y - player.y);
642
+ if (dist < 100) {
643
+ const angle = Math.atan2(player.y - this.y, player.x - this.x);
644
+ this.x += Math.cos(angle) * 5;
645
+ this.y += Math.sin(angle) * 5;
646
+ } else {
647
+ this.y += this.vy;
648
+ this.vy += 0.1;
649
+ }
650
+ }
651
+
652
+ draw() {
653
+ ctx.save();
654
+ ctx.beginPath();
655
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
656
+ ctx.fillStyle = '#f1fa8c';
657
+ ctx.fill();
658
+ ctx.shadowColor = '#f1fa8c';
659
+ ctx.shadowBlur = 10;
660
+ ctx.restore();
661
+ }
662
+ }
663
+
664
+ // --- Game Logic Functions ---
665
+ function spawnEnemies() {
666
+ if (frameCount % 120 === 0) {
667
+ enemies.push(new Enemy(Math.random() * canvasWidth, -20, 50, 1));
668
+ }
669
+ if (frameCount > 300 && frameCount % 240 === 0) {
670
+ enemies.push(new Enemy(canvasWidth / 2, -20, 150, 2));
671
+ }
672
+ }
673
+
674
+ function handleCollisions() {
675
+ // Player bullets vs enemies
676
+ let enemyHitSoundOffset = 0;
677
+ for (let i = playerBullets.length - 1; i >= 0; i--) {
678
+ for (let j = enemies.length - 1; j >= 0; j--) {
679
+ const pb = playerBullets[i];
680
+ const e = enemies[j];
681
+ if (pb.x > e.x - e.width / 2 && pb.x < e.x + e.width / 2 &&
682
+ pb.y > e.y - e.height / 2 && pb.y < e.y + e.height / 2) {
683
+ e.takeDamage(pb.damage, enemyHitSoundOffset);
684
+ enemyHitSoundOffset += 0.01;
685
+ playerBullets.splice(i, 1);
686
+
687
+ if (e.health <= 0) {
688
+ score += 500;
689
+ for (let k = 0; k < 20; k++) particles.push(new Particle(e.x, e.y, Math.random() * 3 + 1, '#ff5555', 60));
690
+ for (let k = 0; k < 3; k++) scoreItems.push(new ScoreItem(e.x + (Math.random() - 0.5) * 20, e.y + (Math.random() - 0.5) * 20, 100));
691
+ enemies.splice(j, 1);
692
+ updateUI();
693
+ }
694
+
695
+ break;
696
+ }
697
+ }
698
+ }
699
+
700
+ if (player.invincible) return;
701
+
702
+ // Enemy bullets vs player
703
+ let grazeSoundOffset = 0;
704
+ for (let i = enemyBullets.length - 1; i >= 0; i--) {
705
+ const b = enemyBullets[i];
706
+ const distHit = Math.hypot(player.x - b.x, player.y - b.y);
707
+
708
+ // Collision
709
+ if (distHit < player.hitboxRadius + b.radius) {
710
+ player.takeHit();
711
+ enemyBullets.splice(i, 1);
712
+ break;
713
+ }
714
+
715
+ // Graze
716
+ if (!b.grazed && distHit < player.grazeRadius + b.radius) {
717
+ b.grazed = true;
718
+ grazeCount++;
719
+ blossomMeter += 5;
720
+ if (blossomMeter >= blossomMeterMax) {
721
+ blossomMeter = blossomMeterMax;
722
+ if(bombs < 5) bombs++; // Max 5 bombs
723
+ blossomMeter = 0;
724
+ }
725
+ synths.graze?.triggerAttackRelease("C6", "16n", Tone.now() + grazeSoundOffset);
726
+ grazeSoundOffset += 0.01;
727
+ updateUI();
728
+ }
729
+ }
730
+ }
731
+
732
+ function handleScoreItems() {
733
+ let itemsCollected = 0;
734
+ let uiNeedsUpdate = false;
735
+ for (let i = scoreItems.length - 1; i >= 0; i--) {
736
+ const item = scoreItems[i];
737
+ const dist = Math.hypot(player.x - item.x, player.y - item.y);
738
+ if (dist < player.grazeRadius) {
739
+ score += item.value * (1 + Math.floor(grazeCount/100));
740
+ scoreItems.splice(i, 1);
741
+ synths.itemCollect?.triggerAttackRelease("A5", "16n", Tone.now() + itemsCollected * 0.02);
742
+ itemsCollected++;
743
+ uiNeedsUpdate = true;
744
+ }
745
+ }
746
+ if (uiNeedsUpdate) {
747
+ updateUI();
748
+ }
749
+ }
750
+
751
+ function updateUI() {
752
+ scoreDisplay.textContent = score;
753
+ highscoreDisplay.textContent = highScore;
754
+ livesDisplay.textContent = lives;
755
+ bombsDisplay.textContent = bombs;
756
+ grazeDisplay.textContent = grazeCount;
757
+ blossomMeterFill.style.width = `${(blossomMeter / blossomMeterMax) * 100}%`;
758
+ }
759
+
760
+ function gameOver() {
761
+ isGameOver = true;
762
+ if (score > highScore) {
763
+ highScore = score;
764
+ localStorage.setItem('novaBlossomHighScore', highScore);
765
+ }
766
+ finalScoreDisplay.textContent = `Your score: ${score}`;
767
+ gameOverScreen.style.display = 'flex';
768
+ }
769
+
770
+ // --- Main Game Loop ---
771
+ function gameLoop() {
772
+ if (isGameOver) return;
773
+
774
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
775
+
776
+ frameCount++;
777
+ spawnEnemies();
778
+
779
+ // Update and draw entities
780
+ player.update();
781
+ player.draw();
782
+
783
+ [particles, playerBullets, enemyBullets, enemies, scoreItems].forEach(arr => {
784
+ arr.forEach(item => {
785
+ item.update();
786
+ item.draw();
787
+ });
788
+ });
789
+
790
+ handleCollisions();
791
+ handleScoreItems();
792
+
793
+ // Clean up off-screen entities
794
+ playerBullets = playerBullets.filter(b => b.y > -10);
795
+ enemyBullets = enemyBullets.filter(b => b.x > -10 && b.x < canvasWidth + 10 && b.y > -10 && b.y < canvasHeight + 10);
796
+ enemies = enemies.filter(e => e.y < canvasHeight + e.height);
797
+ particles = particles.filter(p => p.lifespan > 0);
798
+ scoreItems = scoreItems.filter(item => item.y < canvasHeight + 10);
799
+
800
+ requestAnimationFrame(gameLoop);
801
+ }
802
+ });
803
+ </script>
804
+ </body>
805
+ </html>