package game import rl "vendor:raylib" Projectile :: struct { active: bool, position: Vec2, target_slot: int, damage: int, speed: f32, mode: Projectile_Mode, direction: Vec2, splash_radius: f32, } Projectile_Mode :: enum { Homing, // chases target Ballistic, // flies in a fixed direction } acquire_projectile :: proc(world: ^World) -> ^Projectile { for i in 0 ..< MAX_PROJECTILES { if !world.projectiles[i].active { return &world.projectiles[i] } } return nil } enemy_still_on_map :: proc(world: ^World, slot: int) -> (^Enemy, bool) { if slot < 0 || slot >= MAX_ENEMIES do return nil, false e := &world.enemies[slot] if !e.active do return nil, false if len(world.path) == 0 do return nil, false if e.path_index >= len(world.path) do return nil, false return e, true } spawn_projectile :: proc(world: ^World, from: Vec2, target_slot: int, damage: int) { p := acquire_projectile(world) if p == nil do return p.mode = .Homing p^ = Projectile { active = true, position = from, target_slot = target_slot, damage = damage, speed = PROJECTILE_SPEED, splash_radius = 0, } } spawn_ballistic :: proc(world: ^World, from, dir: Vec2, damage: int, splash: f32) { p := acquire_projectile(world) if p == nil do return p^ = Projectile { active = true, position = from, direction = dir, mode = .Ballistic, damage = damage, speed = CANNON_PROJECTILES_SPEED, splash_radius = splash, } } hit_enemy_at :: proc(world: ^World, pos: Vec2, damage: int) -> bool { for i in 0 ..< MAX_ENEMIES { e := &world.enemies[i] if !e.active do continue if distance(pos, e.position) <= PROJECTILE_HIT_RADIUS { apply_tower_hit(world, e, damage) return true } } return false } update_projectiles :: proc(world: ^World, dt: f32) { for i in 0 ..< MAX_PROJECTILES { p := &world.projectiles[i] if !p.active do continue switch p.mode { case .Homing: update_homing_projectile(world, p, dt) case .Ballistic: update_ballistic_projectile(world, p, dt) } } } update_homing_projectile :: proc(world: ^World, p: ^Projectile, dt: f32) { target, ok := enemy_still_on_map(world, p.target_slot) if !ok { p.active = false return } move_toward(&p.position, target.position, p.speed, dt) if distance(p.position, target.position) <= PROJECTILE_HIT_RADIUS { p.active = false apply_tower_hit(world, target, p.damage) } } update_ballistic_projectile :: proc(world: ^World, p: ^Projectile, dt: f32) { step := p.speed * dt p.position.x += p.direction.x * step p.position.y += p.direction.y * step if apply_splash_damage(world, p.position, p.damage, p.splash_radius) { p.active = false return } if p.position.x < 0 || p.position.y < 0 || p.position.x > MAP_W * TILE_SIZE || p.position.y > MAP_H * TILE_SIZE { p.active = false } } render_projectiles :: proc(world: ^World) { for i in 0 ..< MAX_PROJECTILES { p := world.projectiles[i] if !p.active do continue color := rl.GOLD radius: f32 = 4 if p.mode == .Ballistic { color = rl.GRAY radius = 6 } rl.DrawCircle(i32(p.position.x), i32(p.position.y), radius, color) } } apply_splash_damage :: proc(world: ^World, center: Vec2, damage: int, radius: f32) -> bool { if radius <= 0 do return hit_enemy_at(world, center, damage) hit_any := false for i in 0 ..< MAX_ENEMIES { e := &world.enemies[i] if !e.active do continue if distance(center, e.position) <= radius { apply_tower_hit(world, e, damage) hit_any = true } } return hit_any }