| 1 | // vtest build: !openbsd |
| 2 | import gg |
| 3 | import math |
| 4 | import rand |
| 5 | import sokol.audio |
| 6 | import os.asset |
| 7 | import sokol.sgl |
| 8 | |
| 9 | const designed_width = 600 |
| 10 | const designed_height = 800 |
| 11 | const brick_width = 53 |
| 12 | const brick_height = 20 |
| 13 | const bevel_size = int(brick_height * 0.18) |
| 14 | const highlight_color = gg.rgba(255, 255, 255, 65) |
| 15 | const shade_color = gg.rgba(0, 0, 0, 65) |
| 16 | |
| 17 | struct Brick { |
| 18 | mut: |
| 19 | x f32 |
| 20 | y f32 |
| 21 | w f32 = brick_width |
| 22 | h f32 = brick_height |
| 23 | c gg.Color |
| 24 | value int |
| 25 | alive bool = true |
| 26 | } |
| 27 | |
| 28 | struct Game { |
| 29 | mut: |
| 30 | width int = designed_width |
| 31 | height int = designed_height |
| 32 | ball_x f32 |
| 33 | ball_y f32 |
| 34 | ball_r f32 = 10.0 |
| 35 | ball_dx f32 = 4 |
| 36 | ball_dy f32 = -4 |
| 37 | paddle_x f32 = 250 |
| 38 | paddle_w f32 = 100 |
| 39 | paddle_h f32 = 20 |
| 40 | paddle_dx f32 = 8 |
| 41 | bricks []Brick |
| 42 | nbricks int |
| 43 | npaddles int = 10 |
| 44 | npoints int |
| 45 | nlevels int = 1 |
| 46 | nevent int |
| 47 | sound SoundManager |
| 48 | ctx &gg.Context = unsafe { nil } |
| 49 | } |
| 50 | |
| 51 | fn Game.new() &Game { |
| 52 | mut g := &Game{} |
| 53 | g.ball_x, g.ball_y = f32(g.width) / 2, f32(g.height) - g.paddle_h |
| 54 | g.init_bricks() |
| 55 | return g |
| 56 | } |
| 57 | |
| 58 | enum SoundKind { |
| 59 | paddle |
| 60 | brick |
| 61 | wall |
| 62 | lose_ball |
| 63 | } |
| 64 | |
| 65 | struct SoundManager { |
| 66 | mut: |
| 67 | sounds [4][]f32 // TODO: using map[SoundKind][]f32 here breaks emscripten; use map after the fix |
| 68 | initialised bool |
| 69 | } |
| 70 | |
| 71 | fn (mut sm SoundManager) init() { |
| 72 | all_kinds := [SoundKind.paddle, .brick, .wall, .lose_ball]! |
| 73 | sample_rate := f32(audio.sample_rate()) |
| 74 | duration, volume := 0.09, f32(.25) |
| 75 | nframes := int(sample_rate * duration) |
| 76 | for i in 0 .. nframes { |
| 77 | t := f32(i) / sample_rate |
| 78 | sm.sounds[int(SoundKind.paddle)] << volume * math.sinf(t * 936.0 * 2 * math.pi) |
| 79 | sm.sounds[int(SoundKind.brick)] << volume * math.sinf(t * 432.0 * 2 * math.pi) |
| 80 | sm.sounds[int(SoundKind.wall)] << volume * math.sinf(t * 174.0 * 2 * math.pi) |
| 81 | sm.sounds[int(SoundKind.lose_ball)] << math.sinf(t * 123.0 * 2 * math.pi) |
| 82 | } |
| 83 | border_samples := 2000 |
| 84 | for k, s := f32(0), 0; s <= border_samples; k, s = k + 1.0 / f32(border_samples), s + 1 { |
| 85 | rk := f32(1) - k |
| 86 | rs := nframes - border_samples - 1 + s |
| 87 | for kind in all_kinds { |
| 88 | sm.sounds[int(kind)][s] *= k |
| 89 | sm.sounds[int(kind)][rs] *= rk |
| 90 | } |
| 91 | } |
| 92 | sm.initialised = true |
| 93 | } |
| 94 | |
| 95 | fn (mut g Game) play(k SoundKind) { |
| 96 | if g.sound.initialised { |
| 97 | s := g.sound.sounds[int(k)] |
| 98 | audio.push(s.data, s.len) |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | fn (mut g Game) init_bricks() { |
| 103 | yoffset, xoffset := f32(50 + rand.intn(100) or { 0 }), f32(0 + rand.intn(50) or { 0 }) |
| 104 | g.bricks.clear() |
| 105 | g.nbricks = 0 |
| 106 | for row in 0 .. 10 { |
| 107 | for col in 0 .. 10 { |
| 108 | g.bricks << Brick{ |
| 109 | x: col * (brick_width + 1) + xoffset |
| 110 | y: row * (brick_height + 1) + yoffset |
| 111 | c: gg.rgb(0x40 | rand.u8(), 0x40 | rand.u8(), 0x40 | rand.u8()) |
| 112 | value: 10 - row |
| 113 | } |
| 114 | g.nbricks++ |
| 115 | } |
| 116 | } |
| 117 | for _ in 0 .. 5 + rand.intn(10) or { 0 } { |
| 118 | i := rand.intn(g.bricks.len - 1) or { 0 } |
| 119 | if g.bricks[i].alive { |
| 120 | g.bricks[i].alive = false |
| 121 | g.nbricks-- |
| 122 | } |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | fn (mut g Game) draw() { |
| 127 | ws := gg.window_size() |
| 128 | g.ctx.begin() |
| 129 | sgl.push_matrix() |
| 130 | sgl.scale(f32(ws.width) / f32(designed_width), f32(ws.height) / f32(designed_height), 0) |
| 131 | |
| 132 | g.draw_paddle() |
| 133 | g.draw_ball() |
| 134 | for brick in g.bricks { |
| 135 | if brick.alive { |
| 136 | g.draw_brick(brick) |
| 137 | } |
| 138 | } |
| 139 | label1 := 'Level: ${g.nlevels:02} Points: ${g.npoints:06}' |
| 140 | label2 := 'Bricks: ${g.nbricks:03} Paddles: ${g.npaddles:02}' |
| 141 | g.ctx.draw_text(5, 3, label1, size: 24, color: gg.rgb(255, 255, 255)) |
| 142 | g.ctx.draw_text(320, 3, label2, size: 24, color: gg.rgb(255, 255, 255)) |
| 143 | |
| 144 | sgl.pop_matrix() |
| 145 | g.ctx.end() |
| 146 | } |
| 147 | |
| 148 | fn (g &Game) draw_ball() { |
| 149 | g.ctx.draw_circle_filled(g.ball_x, g.ball_y, g.ball_r, gg.red) |
| 150 | mut ball_r_less := g.ball_r |
| 151 | for _ in 0 .. 3 { |
| 152 | ball_r_less *= 0.8 |
| 153 | g.ctx.draw_circle_filled(g.ball_x - g.ball_r + ball_r_less, g.ball_y - g.ball_r + |
| 154 | ball_r_less, ball_r_less, highlight_color) |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | fn (g &Game) draw_paddle() { |
| 159 | roffset, rradius := -5, 18 |
| 160 | g.ctx.draw_circle_filled(g.paddle_x - roffset, g.height, rradius, gg.blue) |
| 161 | g.ctx.draw_circle_filled(g.paddle_x + g.paddle_w + roffset, g.height, rradius, gg.blue) |
| 162 | g.ctx.draw_rect_filled(g.paddle_x, g.height - g.paddle_h + 2, g.paddle_w, g.paddle_h, gg.blue) |
| 163 | g.ctx.draw_rect_filled(g.paddle_x, g.height - g.paddle_h + 2, g.paddle_w, bevel_size, |
| 164 | highlight_color) |
| 165 | } |
| 166 | |
| 167 | fn (g &Game) draw_brick(brick Brick) { |
| 168 | g.ctx.draw_rect_filled(brick.x, brick.y, brick.w, brick.h, brick.c) |
| 169 | g.ctx.draw_rect_filled(brick.x, brick.y, brick.w, bevel_size, highlight_color) |
| 170 | g.ctx.draw_rect_filled(brick.x, brick.y, bevel_size, brick.h - bevel_size, highlight_color) |
| 171 | g.ctx.draw_rect_filled(brick.x + brick.w - bevel_size, brick.y, bevel_size, |
| 172 | brick.h - bevel_size, shade_color) |
| 173 | g.ctx.draw_rect_filled(brick.x, brick.y + brick.h - bevel_size, brick.w, bevel_size, |
| 174 | shade_color) |
| 175 | } |
| 176 | |
| 177 | fn (mut g Game) game_over() { |
| 178 | g.init_bricks() |
| 179 | g.npoints, g.nlevels, g.npaddles = 0, 1, 5 |
| 180 | } |
| 181 | |
| 182 | fn (mut g Game) goto_next_level() { |
| 183 | g.init_bricks() |
| 184 | g.npaddles++ |
| 185 | g.nlevels++ |
| 186 | } |
| 187 | |
| 188 | fn (mut g Game) move(k f32) { |
| 189 | if k < 0 { |
| 190 | if g.paddle_x <= 0 { |
| 191 | return |
| 192 | } |
| 193 | } else if k > 0 { |
| 194 | if g.paddle_x >= g.width - g.paddle_w { |
| 195 | return |
| 196 | } |
| 197 | } |
| 198 | g.paddle_x += k * g.paddle_dx |
| 199 | } |
| 200 | |
| 201 | fn (mut g Game) update() { |
| 202 | if g.ctx.pressed_keys[gg.KeyCode.left] { |
| 203 | g.move(-1.0) |
| 204 | } |
| 205 | if g.ctx.pressed_keys[gg.KeyCode.right] { |
| 206 | g.move(1.0) |
| 207 | } |
| 208 | // |
| 209 | g.ball_x, g.ball_y = g.ball_x + g.ball_dx, g.ball_y + g.ball_dy |
| 210 | // Wall collisions |
| 211 | if g.ball_x < g.ball_r || g.ball_x > g.width - g.ball_r { |
| 212 | g.ball_dx *= -1 |
| 213 | g.play(.wall) |
| 214 | } |
| 215 | if g.ball_y < g.ball_r { |
| 216 | g.ball_dy *= -1 |
| 217 | g.play(.wall) |
| 218 | } |
| 219 | if g.ball_y > g.height { |
| 220 | g.ball_x, g.ball_y = g.paddle_x + g.paddle_w / 2, f32(g.height) - g.paddle_h |
| 221 | g.ball_dy = -4 |
| 222 | g.npaddles-- |
| 223 | g.play(.lose_ball) |
| 224 | if g.npaddles <= 0 { |
| 225 | g.game_over() |
| 226 | } |
| 227 | } |
| 228 | // Paddle collision |
| 229 | is_ball_on_paddle_y := g.ball_y + g.ball_r > g.height - g.paddle_h |
| 230 | && g.ball_y < g.height - g.ball_r |
| 231 | is_ball_on_paddle_x := g.ball_x > g.paddle_x - 10 && g.ball_x < g.paddle_x + g.paddle_w + 10 |
| 232 | if is_ball_on_paddle_y && is_ball_on_paddle_x { |
| 233 | g.play(.paddle) |
| 234 | g.ball_dy = -math.abs(g.ball_dy) |
| 235 | x_in_paddle := g.ball_x - g.paddle_x |
| 236 | rmargin := 10 |
| 237 | if x_in_paddle < rmargin || x_in_paddle + rmargin > g.paddle_w { |
| 238 | g.ball_dx *= -1 |
| 239 | } else if !(x_in_paddle > 40 && x_in_paddle < 60) { |
| 240 | r := 10 * (-0.5 + rand.f32()) |
| 241 | g.ball_dx += r |
| 242 | g.ball_dx = f32(int_min(int_max(-80, int(g.ball_dx * 10)), 80)) / 10 |
| 243 | } |
| 244 | } |
| 245 | // Brick collisions |
| 246 | for mut brick in g.bricks { |
| 247 | if brick.alive && g.ball_y - g.ball_r < brick.y + brick.h && g.ball_y + g.ball_r > brick.y |
| 248 | && g.ball_x + g.ball_r > brick.x && g.ball_x - g.ball_r < brick.x + brick.w { |
| 249 | g.play(.brick) |
| 250 | brick.alive = false |
| 251 | g.nbricks-- |
| 252 | g.npoints += brick.value |
| 253 | g.ball_dy *= -1 |
| 254 | if g.nbricks == 0 { |
| 255 | g.goto_next_level() |
| 256 | } |
| 257 | } |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | fn (mut g Game) touch_event(touch_point gg.TouchPoint) { |
| 262 | ws := gg.window_size() |
| 263 | tx := touch_point.pos_x |
| 264 | if tx <= f32(ws.width) * 0.5 { |
| 265 | g.move(-1.0) |
| 266 | } else { |
| 267 | g.move(1.0) |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | @[if wasm32_emscripten] |
| 272 | fn (mut g Game) handle_event() { |
| 273 | if g.nevent > 0 { |
| 274 | return |
| 275 | } |
| 276 | // the audio has to be started when the wasm canvas has received user |
| 277 | // interaction, unlike on desktop platforms |
| 278 | audio.setup(buffer_frames: 1024) |
| 279 | g.sound.init() |
| 280 | g.nevent++ |
| 281 | } |
| 282 | |
| 283 | fn main() { |
| 284 | mut g := Game.new() |
| 285 | mut fpath := asset.get_path('../assets', 'fonts/RobotoMono-Regular.ttf') |
| 286 | $if !wasm32_emscripten { |
| 287 | audio.setup(buffer_frames: 512) // too small values lead to cracking sounds or no sound at all on macos |
| 288 | g.sound.init() |
| 289 | fpath = '' |
| 290 | } |
| 291 | g.ctx = gg.new_context( |
| 292 | width: g.width |
| 293 | height: g.height |
| 294 | window_title: 'V Breakout' |
| 295 | frame_fn: fn (mut g Game) { |
| 296 | dt := g.ctx.timer.elapsed().milliseconds() |
| 297 | if dt > 15 { |
| 298 | g.update() |
| 299 | g.ctx.timer.restart() |
| 300 | } |
| 301 | g.draw() |
| 302 | } |
| 303 | click_fn: fn (x f32, y f32, btn gg.MouseButton, mut g Game) { |
| 304 | g.handle_event() |
| 305 | } |
| 306 | event_fn: fn (e &gg.Event, mut g Game) { |
| 307 | g.handle_event() |
| 308 | if e.typ == .touches_began || e.typ == .touches_moved { |
| 309 | if e.num_touches > 0 { |
| 310 | touch_point := e.touches[0] |
| 311 | g.touch_event(touch_point) |
| 312 | } |
| 313 | } |
| 314 | } |
| 315 | keydown_fn: fn (key gg.KeyCode, _ gg.Modifier, mut g Game) { |
| 316 | g.handle_event() |
| 317 | match key { |
| 318 | .r { |
| 319 | g.game_over() |
| 320 | } |
| 321 | .escape { |
| 322 | exit(0) |
| 323 | } |
| 324 | else {} |
| 325 | } |
| 326 | } |
| 327 | user_data: g |
| 328 | font_path: fpath |
| 329 | ) |
| 330 | g.ctx.run() |
| 331 | } |
| 332 | |