-
Notifications
You must be signed in to change notification settings - Fork 10
/
level.cljs
187 lines (158 loc) · 6.21 KB
/
level.cljs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
(ns breakout.engine.level
(:require-macros [cljs.core.async.macros :refer [go]])
(:require [cljs.core.async :refer [<!]]
[reagent.core :refer [atom]]
[breakout.engine.input :as input]
[breakout.levels.data :as levels])
(:import goog.math.Rect))
;; --- constants
(def tile-size 16)
(def board {:width 320 :height 416})
(def paddle-y (- (:height board) (* 3 tile-size)))
(def ball-size {:width tile-size :height tile-size})
(def paddle-size {:width 48 :height 16})
(def starting-ball-pos {:x (* 2 tile-size) :y (* 15 tile-size)})
(def base-vel (/ 120 1000)) ; 120 pixels per second
(def starting-ball-vel {:x base-vel :y base-vel})
(def total-countdown-duration 3500)
;; --- state, there's a lot of it (this is a game after all)
(def phase (atom nil))
(def running (atom nil))
(def last-ts (atom nil))
(def next-scene! (atom nil))
(def score (atom nil))
(def lives (atom nil))
(def level (atom nil))
(def bricks (atom nil))
(def paddle-pos (atom {:x 0 :y paddle-y}))
(def ball-pos (atom starting-ball-pos))
(def ball-vel (atom starting-ball-vel))
(def countdown-duration (atom total-countdown-duration))
;; --- collision related
;; these functions detect collision and are called from update-state! :gameplay
(def walls [(Rect. 0 0 tile-size (:height board))
(Rect. (- (:width board) tile-size) 0 tile-size (:height board))])
(def ceiling (Rect. 0 0 (:width board) tile-size))
(defn pos->rect [pos size]
(Rect. (:x pos) (:y pos) (:width size) (:height size)))
(defn get-collided-brick [ball-rect bricks]
(first
(filter #(.intersects (:rect %) ball-rect) bricks)))
(defn- rect-collided-with [src-rect rects]
(some #(.intersects % src-rect) rects))
(defn- get-collision [pos bricks walls ceiling paddle-pos]
(let [ball-rect (pos->rect pos ball-size)
collided-brick (get-collided-brick ball-rect bricks)]
(or
(and collided-brick [:brick collided-brick])
(and (rect-collided-with ball-rect walls) [:wall])
(and (rect-collided-with ball-rect [ceiling]) [:ceiling])
(and (rect-collided-with ball-rect [(pos->rect paddle-pos paddle-size)]) [:paddle])
[:none])))
(defn- move-ball [delta pos vel]
(let [pos (update-in pos [:x] + (* delta (:x vel)))]
(update-in pos [:y] + (* delta (:y vel)))))
(defn- beyond-board? [pos]
(>= (:y pos) (:height board)))
(defn- flip! [vel-atom key]
(swap! vel-atom assoc key (- (key @vel-atom))))
(defn- get-center-x [pos size]
(+ (/ (:width size) 2) (:x pos)))
;; TODO this is too primitive, in many situations it
;; does not pick the correct direction to flip
(defn- get-flip-direction [ball-pos brick]
(let [cbx (get-center-x ball-pos ball-size)
left (get-in brick [:pos :x])
right (+ left (:width brick))]
(if (and (> cbx left) (< cbx right))
:y
:x)))
;; determines the x velocity for the ball based on where
;; on the paddle the ball struck. The closer to the center
;; of the paddle, the closer to zero the x velocity
(defn- get-x-vel-from-paddle-bounce [ball-pos paddle-pos]
(let [half-paddle (/ (:width paddle-size) 2)
cbx (get-center-x ball-pos ball-size)
cpx (get-center-x paddle-pos paddle-size)
distance (- cbx cpx)
ratio (/ distance half-paddle)]
(* 1.5 base-vel ratio)))
(defn- setup-next-level! [level]
(let [brick-data (levels/get-level-data level)]
(when brick-data
;; small hack -- by delaying the brick data, it allows
;; React's CSSTransitionGroup to kick in, causing the bricks
;; to appear on the board with a CSS animation
(.setTimeout js/window #(reset! bricks brick-data) 100)
(reset! phase :countdown))))
;; --- state initialization
;; called whenever a phase transition within gameplay happens
(defmulti init-phase! identity)
(defmethod init-phase! :countdown [_]
(reset! ball-pos starting-ball-pos)
(reset! countdown-duration total-countdown-duration))
(defmethod init-phase! :gameplay [_]
(reset! ball-pos starting-ball-pos)
(reset! ball-vel starting-ball-vel))
(add-watch phase :phase
(fn [_ _ _ new-phase]
(when new-phase
(init-phase! new-phase))))
;; --- updating phases
;; called once per frame to update the current phase
(defmulti update-phase! (fn [delta phase] phase))
(defmethod update-phase! :countdown [delta _]
(swap! countdown-duration - delta)
(when (<= @countdown-duration 0)
(reset! phase :gameplay)))
(defmethod update-phase! :gameplay [delta _]
(let [old-pos @ball-pos
new-pos (move-ball delta old-pos @ball-vel)
pad-pos @paddle-pos
[collided-type collided-object] (get-collision new-pos @bricks walls ceiling pad-pos)]
(case collided-type
:wall (flip! ball-vel :x)
:ceiling (flip! ball-vel :y)
:paddle (do
(flip! ball-vel :y)
(swap! ball-vel assoc :x (get-x-vel-from-paddle-bounce new-pos pad-pos)))
:brick (do
(flip! ball-vel (get-flip-direction new-pos collided-object))
(swap! score + 100)
(swap! bricks disj collided-object)
(when (zero? (count @bricks))
(let [next-level (swap! level inc)]
(when-not (setup-next-level! next-level)
(@next-scene! :win)))))
:none (if (beyond-board? new-pos)
(let [remaining-lives (swap! lives dec)]
(if (<= remaining-lives 0)
(@next-scene! :game-over)
(reset! phase :countdown)))
(reset! ball-pos new-pos)))))
(defn- update! [ts]
(when @running
(let [delta (- ts (or @last-ts ts))]
(reset! last-ts ts)
(update-phase! delta @phase))
(. js/window (requestAnimationFrame update!))))
(defn- listen-to-input-moves []
(go
(while @running
(let [input-x (<! input/movement)]
(swap! paddle-pos assoc :x input-x)))))
(defn- init! []
(reset! lives 3)
(reset! score 0)
(reset! level 0)
(reset! bricks #{})
(setup-next-level! 0)
(reset! last-ts nil)
(reset! running true))
(defn start! [set-next-scene!]
(reset! next-scene! set-next-scene!)
(init!)
(listen-to-input-moves)
(. js/window (requestAnimationFrame update!)))
(defn stop! []
(reset! running false))