package game import "core:math" import "core:math/rand" import rl "vendor:raylib" SPEED_INC :: 30 EFFECTS_SPEED :: 300 INITIAL_SPEED :: 200 MAX_SPEED :: 550 init_ball :: proc() -> Entity { return Entity{ type = .Ball, active = true, pos = rl.Vector2{(VIEWPORT_W / 2), (VIEWPORT_H / 2)}, speed = INITIAL_SPEED, max_speed = MAX_SPEED, vel = rl.Vector2{0,0}, shape = .Circle, size = rl.Vector2{8, 0}, tween_duration = 0.65, } } serve_ball :: proc(ball: ^Entity, serve_dir: u8) { dir_x: f32 = 0.0 // Randomize initial direction: -1.0 or 1.0 if serve_dir == 0 do dir_x = f32(rand.float32() > 0.5 ? 1 : -1) if serve_dir == 1 do dir_x = 1.0 if serve_dir == 2 do dir_x = -1.0 dir_y := rand.choice([]f32{-0.1, 0.1}) // Slightly angled launch ball.vel = rl.Vector2Normalize({dir_x, dir_y}) * ball.speed } reset_ball :: proc(ball: ^Entity) { ball.pos = rl.Vector2{(VIEWPORT_W / 2), (VIEWPORT_H / 2)} ball.vel = rl.Vector2{0,0} ball.speed = INITIAL_SPEED ball.trail_positions = {} ball.trail_count = 0 } update_ball :: proc(ball: ^Entity, level: ^Level, dt: f32) { // squish animation check if ball.tween_elapsed < ball.tween_duration { ball.tween_elapsed += dt } // first check the level timer are we waiting to serve? if level.serve_timer > 0 { level.serve_timer -= dt if level.serve_timer <= 0 { serve_ball(ball, level.serve_dir) } return } // Update trail history buffer update_ball_trail(ball, dt) radius := ball.size.x field_bounds := level.field_bounds // paddle collisions detection (player then enemy) if check_ball_paddle_collision(ball, level.player) { level.player.color = rl.Color{255,255,255,255} ball.tween_elapsed = 0.0 resolve_collision(ball, level.player, true) if ball.speed >= EFFECTS_SPEED { spawn_max_speed_sparks(&level.particles, level.player, ball.pos.y, true) } } else if check_ball_paddle_collision(ball, level.opponent) { level.opponent.color = rl.Color{255,255,255,255} ball.tween_elapsed = 0.0 resolve_collision(ball, level.opponent, false) if ball.speed >= EFFECTS_SPEED { spawn_max_speed_sparks(&level.particles, level.opponent, ball.pos.y, false) } } else { level.opponent.color = rl.Color{255,255,255,0} level.player.color = rl.Color{255,255,255,0} } // bounds collision detection if ball.pos.y >= (field_bounds.y + field_bounds.height) - ball.size.x { ball.vel.y = -ball.vel.y } if ball.pos.y <= field_bounds.y + ball.size.x { ball.vel.y = -ball.vel.y } if ball.pos.x >= (field_bounds.x + field_bounds.width) - ball.size.x { increment_score(&level.score_player, level.score_win, true, ball) reset_ball(ball) level.serve_timer = level.wait_time level.serve_dir = 1 } if ball.pos.x <= field_bounds.x + ball.size.x { increment_score(&level.score_enemy, level.score_win, false, ball) reset_ball(ball) level.serve_timer = level.wait_time level.serve_dir = 2 } ball.pos += ball.vel * dt } check_ball_paddle_collision :: proc(ball: ^Entity, paddle: Entity) -> bool { paddle_rec := rl.Rectangle{ x = paddle.pos.x, y = paddle.pos.y, width = paddle.size.x, height = paddle.size.y, } collided := rl.CheckCollisionCircleRec(ball.pos, ball.size.x, paddle_rec) if collided do emit_event(Ball_Collision_Data{speed = ball.speed}) return collided } increment_score :: proc(score_side: ^int, max_score: int, player: bool, ball: ^Entity) { emit_event(.Score_Change) score_side^ += 1 if score_side^ == max_score { emit_event(Game_Over_Data{player}) } } increment_speed :: proc(ball: ^Entity) { if ball.speed <= ball.max_speed { ball.speed += SPEED_INC ball.vel = rl.Vector2Normalize({ball.vel.x, ball.vel.y}) * ball.speed } } resolve_collision :: proc(ball: ^Entity, paddle: Entity, is_player: bool) { ball_center_y := ball.pos.y paddle_center_y := paddle.pos.y + (paddle.size.y / 2.0) local_y := ball_center_y - paddle_center_y half_height := paddle.size.y / 2.0 // check if we are in top or bottom half of paddle hit_t := clamp(local_y / half_height, -1.0, 1.0) // direction is dependant of if its the player dir_x: f32 = is_player ? 1.0 : -1.0 // steer the angle by a value using its hit t value dir_y := hit_t * 1.3 new_dir := rl.Vector2Normalize({dir_x, dir_y}) ball.vel = new_dir * ball.speed increment_speed(ball) // Resolve overlap if is_player { ball.pos.x = paddle.pos.x + paddle.size.x + ball.size.x } else { ball.pos.x = paddle.pos.x - ball.size.x } } update_ball_trail :: proc(ball: ^Entity, dt: f32) { if ball.speed < EFFECTS_SPEED { ball.trail_count = 0 return } ball.trail_timer += dt // get pos every 20ms sample_rate := f32(0.02) if ball.trail_timer >= sample_rate { ball.trail_timer = 0.0 // shift old pos back for i := MAX_TRAILS - 1; i > 0; i -= 1 { ball.trail_positions[i] = ball.trail_positions[i - 1] } // store new pos ball.trail_positions[0] = ball.pos if ball.trail_count < MAX_TRAILS { ball.trail_count += 1 } } } spawn_max_speed_sparks :: proc(particles: ^[dynamic]Particle, paddle: Entity, ball_y: f32, is_player: bool) { count := 12 impact_x := is_player ? (paddle.pos.x + paddle.size.x) : paddle.pos.x impact_y := clamp(ball_y, paddle.pos.y, paddle.pos.y + paddle.size.y) impact_pos := rl.Vector2{impact_x, impact_y} for _ in 0 ..< count { // Direct sparks outward from the paddle face base_angle := is_player ? f32(0.0) : rl.PI // 0 rad = right, PI rad = left spread := (f32(rand.float32()) - 0.5) * 1.2 angle := base_angle + spread speed := f32(rand.float32() * 180.0 + 100.0) append(particles, Particle{ pos = impact_pos, vel = {math.cos(angle) * speed, math.sin(angle) * speed}, color = rl.SKYBLUE, life = 0.25, max_life = 0.25, size = f32(rand.float32() * 2.0 + 1.5), }) } } render_ball_trail :: proc(ball: Entity) { if ball.trail_count == 0 do return base_radius := ball.size.x for i in 0 ..< ball.trail_count { pos := ball.trail_positions[i] // Farther back in history = smaller and fainter progress := f32(i + 1) / f32(ball.trail_count) // 0.0 -> 1.0 radius := base_radius * (1.0 - (progress * 0.6)) // Shrinks slightly alpha := u8(180.0 * (1.0 - progress)) // Fades out trail_color := rl.Color{rl.MAROON.r, rl.MAROON.g, rl.MAROON.b, alpha} rl.DrawCircleV(pos, radius, trail_color) } } render_ball :: proc(ball: Entity, field_bounds: rl.Rectangle) { render_ball_trail(ball) radius_x := ball.size.x radius_y := ball.size.x // if animation active if ball.tween_elapsed < ball.tween_duration { radius_x = rl.EaseBackOut( ball.tween_elapsed, 5.0, ball.size.x - 5.0, ball.tween_duration ) radius_y = rl.EaseBackOut( ball.tween_elapsed, 12.0, ball.size.x - 12.0, ball.tween_duration ) } rl.DrawEllipse(i32(ball.pos.x), i32(ball.pos.y), radius_x, radius_y, rl.MAROON) }