|
15 | 15 | ;; Game Constants |
16 | 16 | ;; ============================================================================ |
17 | 17 |
|
| 18 | +;; ============================================================================ |
| 19 | +;; Audio System (Web Audio API) |
| 20 | +;; ============================================================================ |
| 21 | + |
| 22 | +(def audio-context |
| 23 | + "Web Audio API context for sound generation" |
| 24 | + (when (exists? js/AudioContext) |
| 25 | + (js/AudioContext.))) |
| 26 | + |
| 27 | +(defn play-tone |
| 28 | + "Plays a tone at the specified frequency for the given duration" |
| 29 | + [& {:keys [frequency duration volume] |
| 30 | + :or {frequency 440 duration 0.2 volume 0.3}}] |
| 31 | + (when audio-context |
| 32 | + (try |
| 33 | + (let [oscillator (.createOscillator audio-context) |
| 34 | + gain-node (.createGain audio-context)] |
| 35 | + (.connect oscillator gain-node) |
| 36 | + (.connect gain-node (.-destination audio-context)) |
| 37 | + (set! (.-value (.-frequency oscillator)) frequency) |
| 38 | + (set! (.-value (.-gain gain-node)) volume) |
| 39 | + (.start oscillator) |
| 40 | + (.stop oscillator (+ (.-currentTime audio-context) duration))) |
| 41 | + (catch js/Error e |
| 42 | + (js/console.error "Audio error:" e))))) |
| 43 | + |
| 44 | +(defn play-laser-sound |
| 45 | + "Plays a laser shooting sound" |
| 46 | + [] |
| 47 | + (play-tone :frequency 800 :duration 0.1 :volume 0.2)) |
| 48 | + |
| 49 | +(defn play-explosion-sound |
| 50 | + "Plays an explosion sound for asteroids and UFOs" |
| 51 | + [] |
| 52 | + (play-tone :frequency 100 :duration 0.2 :volume 0.3)) |
| 53 | + |
| 54 | +(defn play-thrust-sound |
| 55 | + "Plays a ship thrust/engine sound" |
| 56 | + [] |
| 57 | + (play-tone :frequency 150 :duration 0.08 :volume 0.15)) |
| 58 | + |
| 59 | +(defn play-hyperspace-sound |
| 60 | + "Plays a hyperspace jump sound with descending frequencies" |
| 61 | + [] |
| 62 | + ;; Descending frequencies for warp effect |
| 63 | + (doseq [[idx freq] (map-indexed vector [880 660 440 220])] |
| 64 | + (js/setTimeout |
| 65 | + #(play-tone :frequency freq :duration 0.1 :volume 0.25) |
| 66 | + (* idx 50)))) |
| 67 | + |
| 68 | +(defn play-hit-sound |
| 69 | + "Plays a sound when the ship is hit" |
| 70 | + [] |
| 71 | + (play-tone :frequency 150 :duration 0.3 :volume 0.4)) |
| 72 | + |
| 73 | +(defn play-level-complete-sound |
| 74 | + "Plays a victory sound for completing a level" |
| 75 | + [] |
| 76 | + ;; Ascending notes for victory |
| 77 | + (doseq [[idx freq] (map-indexed vector [523 659 784 1047])] |
| 78 | + (js/setTimeout |
| 79 | + #(play-tone :frequency freq :duration 0.2 :volume 0.25) |
| 80 | + (* idx 100)))) |
| 81 | + |
18 | 82 | (def canvas-width 800) |
19 | 83 | (def canvas-height 600) |
20 | 84 | (def ship-size 10) |
|
215 | 279 | (defn fire-bullet! |
216 | 280 | "Fires a bullet from the ship" |
217 | 281 | [] |
| 282 | + (play-laser-sound) ; Play laser sound |
218 | 283 | (let [{:keys [x y angle]} (:ship @game-state) |
219 | 284 | bullet-vx (* bullet-speed (Math/cos (- angle (/ Math/PI 2)))) |
220 | 285 | bullet-vy (* bullet-speed (Math/sin (- angle (/ Math/PI 2))))] |
|
250 | 315 | "Hyperspace jump with risk" |
251 | 316 | [] |
252 | 317 | (when (<= (:hyperspace-cooldown @game-state) 0) |
| 318 | + (play-hyperspace-sound) ; Play hyperspace sound |
253 | 319 | (let [new-x (rand-int canvas-width) |
254 | 320 | new-y (rand-int canvas-height) |
255 | 321 | died? (< (rand) 0.1)] |
|
309 | 375 | (swap! game-state update :ufos conj (create-ufo)) |
310 | 376 | (swap! game-state assoc :ufo-timer (+ 900 (rand-int 900)))) |
311 | 377 |
|
312 | | - ;; Update ship |
| 378 | +;; Update ship |
313 | 379 | (swap! game-state update :ship |
314 | 380 | (fn [s] |
315 | 381 | (let [new-vx (if (:thrusting s) |
|
328 | 394 | (update :y #(wrap-position :value (+ % new-vy) :max-val canvas-height)) |
329 | 395 | (update :invulnerable #(max 0 (dec %))))))) |
330 | 396 |
|
| 397 | + ;; Play thrust sound (throttled to every 8 frames) |
| 398 | + (when (and (:thrusting ship) |
| 399 | + (= (mod (:frame-count @game-state) 8) 0)) |
| 400 | + (play-thrust-sound)) |
| 401 | + |
331 | 402 | ;; Update bullets |
332 | 403 | (swap! game-state update :bullets |
333 | 404 | (fn [bs] |
|
398 | 469 | :count 5 |
399 | 470 | :color "#FFFFFF")))) |
400 | 471 |
|
401 | | - ;; Apply all collision effects at once |
| 472 | +;; Apply all collision effects at once |
402 | 473 | (when (seq @hit-bullets) |
| 474 | + (play-explosion-sound) ; Play explosion sound for asteroid destruction |
403 | 475 | (swap! game-state update :bullets #(vec (remove (fn [b] (contains? @hit-bullets b)) %))) |
404 | 476 | (swap! game-state update :asteroids #(vec (remove (fn [a] (contains? @hit-asteroids a)) %))) |
405 | 477 | ;; Only add new asteroids if we're under the limit |
|
435 | 507 | :color "#FF00FF")))) |
436 | 508 |
|
437 | 509 | (when (seq @hit-bullets) |
| 510 | + (play-explosion-sound) ; Play explosion sound for UFO destruction |
438 | 511 | (swap! game-state update :bullets #(vec (remove (fn [b] (contains? @hit-bullets b)) %))) |
439 | 512 | (swap! game-state update :ufos #(vec (remove (fn [u] (contains? @hit-ufos u)) %))) |
440 | 513 | (swap! game-state update :score + @score-added) |
|
445 | 518 | (doseq [asteroid asteroids] |
446 | 519 | (when (check-collision :obj1 ship :obj2 asteroid |
447 | 520 | :radius1 ship-size :radius2 (:size asteroid)) |
| 521 | + (play-hit-sound) ; Play hit sound when ship is hit |
448 | 522 | (swap! game-state update :lives dec) |
449 | 523 | (swap! game-state update :particles |
450 | 524 | #(vec (concat % (create-particles :x (:x ship) |
|
462 | 536 | :when (:from-ufo bullet)] |
463 | 537 | (when (check-collision :obj1 ship :obj2 bullet |
464 | 538 | :radius1 ship-size :radius2 3) |
| 539 | + (play-hit-sound) ; Play hit sound when ship is hit by UFO bullet |
465 | 540 | (swap! game-state update :bullets #(vec (remove (fn [b] (= b bullet)) %))) |
466 | 541 | (swap! game-state update :lives dec) |
467 | 542 | (swap! game-state update :particles |
|
474 | 549 | (swap! game-state assoc :game-status :game-over) |
475 | 550 | (swap! game-state update :high-score max (:score @game-state)))))) |
476 | 551 |
|
477 | | - ;; Check level complete |
| 552 | +;; Check level complete |
478 | 553 | (when (empty? asteroids) |
| 554 | + (play-level-complete-sound) ; Play victory sound |
479 | 555 | (swap! game-state update :level inc) |
480 | 556 | (init-level! :level (:level @game-state)))))) |
481 | 557 |
|
|
0 commit comments