|
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) |
253 | | - (swap! game-state assoc-in [:ship :x] (rand-int canvas-width)) |
254 | | - (swap! game-state assoc-in [:ship :y] (rand-int canvas-height)) |
255 | | - (swap! game-state assoc-in [:ship :vx] 0) |
256 | | - (swap! game-state assoc-in [:ship :vy] 0) |
257 | | - (swap! game-state assoc :hyperspace-cooldown hyperspace-cooldown) |
258 | | - ;; 10% chance of explosion (risky!) |
259 | | - (when (< (rand) 0.1) |
260 | | - (swap! game-state update-in [:lives] dec) |
261 | | - (swap! game-state update :particles |
262 | | - #(vec (concat % (create-particles :x (:x (:ship @game-state)) |
263 | | - :y (:y (:ship @game-state)) |
264 | | - :count 12 |
265 | | - :color "#FFFFFF"))))))) |
| 318 | + (play-hyperspace-sound) ; Play hyperspace sound |
| 319 | + (let [new-x (rand-int canvas-width) |
| 320 | + new-y (rand-int canvas-height) |
| 321 | + died? (< (rand) 0.1)] |
| 322 | + (swap! game-state |
| 323 | + (fn [state] |
| 324 | + (-> state |
| 325 | + ;; Teleport ship |
| 326 | + (assoc-in [:ship :x] new-x) |
| 327 | + (assoc-in [:ship :y] new-y) |
| 328 | + (assoc-in [:ship :vx] 0) |
| 329 | + (assoc-in [:ship :vy] 0) |
| 330 | + (assoc :hyperspace-cooldown hyperspace-cooldown) |
| 331 | + ;; Conditionally handle death |
| 332 | + (#(if died? |
| 333 | + (-> % |
| 334 | + (update-in [:lives] dec) |
| 335 | + (update :particles |
| 336 | + (fn [particles] |
| 337 | + (vec (concat particles |
| 338 | + (create-particles |
| 339 | + :x new-x |
| 340 | + :y new-y |
| 341 | + :count 12 |
| 342 | + :color "#FFFFFF")))))) |
| 343 | + %)))))))) |
266 | 344 |
|
267 | 345 | ;; ============================================================================ |
268 | 346 | ;; Collision Detection |
|
297 | 375 | (swap! game-state update :ufos conj (create-ufo)) |
298 | 376 | (swap! game-state assoc :ufo-timer (+ 900 (rand-int 900)))) |
299 | 377 |
|
300 | | - ;; Update ship |
| 378 | +;; Update ship |
301 | 379 | (swap! game-state update :ship |
302 | 380 | (fn [s] |
303 | 381 | (let [new-vx (if (:thrusting s) |
|
316 | 394 | (update :y #(wrap-position :value (+ % new-vy) :max-val canvas-height)) |
317 | 395 | (update :invulnerable #(max 0 (dec %))))))) |
318 | 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 | + |
319 | 402 | ;; Update bullets |
320 | 403 | (swap! game-state update :bullets |
321 | 404 | (fn [bs] |
|
386 | 469 | :count 5 |
387 | 470 | :color "#FFFFFF")))) |
388 | 471 |
|
389 | | - ;; Apply all collision effects at once |
| 472 | +;; Apply all collision effects at once |
390 | 473 | (when (seq @hit-bullets) |
| 474 | + (play-explosion-sound) ; Play explosion sound for asteroid destruction |
391 | 475 | (swap! game-state update :bullets #(vec (remove (fn [b] (contains? @hit-bullets b)) %))) |
392 | 476 | (swap! game-state update :asteroids #(vec (remove (fn [a] (contains? @hit-asteroids a)) %))) |
393 | 477 | ;; Only add new asteroids if we're under the limit |
|
423 | 507 | :color "#FF00FF")))) |
424 | 508 |
|
425 | 509 | (when (seq @hit-bullets) |
| 510 | + (play-explosion-sound) ; Play explosion sound for UFO destruction |
426 | 511 | (swap! game-state update :bullets #(vec (remove (fn [b] (contains? @hit-bullets b)) %))) |
427 | 512 | (swap! game-state update :ufos #(vec (remove (fn [u] (contains? @hit-ufos u)) %))) |
428 | 513 | (swap! game-state update :score + @score-added) |
|
433 | 518 | (doseq [asteroid asteroids] |
434 | 519 | (when (check-collision :obj1 ship :obj2 asteroid |
435 | 520 | :radius1 ship-size :radius2 (:size asteroid)) |
| 521 | + (play-hit-sound) ; Play hit sound when ship is hit |
436 | 522 | (swap! game-state update :lives dec) |
437 | 523 | (swap! game-state update :particles |
438 | 524 | #(vec (concat % (create-particles :x (:x ship) |
|
450 | 536 | :when (:from-ufo bullet)] |
451 | 537 | (when (check-collision :obj1 ship :obj2 bullet |
452 | 538 | :radius1 ship-size :radius2 3) |
| 539 | + (play-hit-sound) ; Play hit sound when ship is hit by UFO bullet |
453 | 540 | (swap! game-state update :bullets #(vec (remove (fn [b] (= b bullet)) %))) |
454 | 541 | (swap! game-state update :lives dec) |
455 | 542 | (swap! game-state update :particles |
|
462 | 549 | (swap! game-state assoc :game-status :game-over) |
463 | 550 | (swap! game-state update :high-score max (:score @game-state)))))) |
464 | 551 |
|
465 | | - ;; Check level complete |
| 552 | +;; Check level complete |
466 | 553 | (when (empty? asteroids) |
| 554 | + (play-level-complete-sound) ; Play victory sound |
467 | 555 | (swap! game-state update :level inc) |
468 | 556 | (init-level! :level (:level @game-state)))))) |
469 | 557 |
|
|
0 commit comments