# ============================================================
# RAIDEN : 縦スクロールシューティング（雷電風）
#   ← ↑ ↓ → : 自機を8方向移動（弾は上方向へ自動連射）
#   敵を撃破。敵本体・敵弾に当たると 1 ミス（残機は左下）。残機 0 で GAME OVER
#   GAME OVER 後は Space でリスタート。背景は流れる星
# ============================================================

IMAGE = Image.load("sprite.png")

# 当たり判定サイズ。描画サイズとは独立の固定値
class Entity < Sprite
  NATIVE_WIDTH = 75
  NATIVE_HEIGHT = 83
  def width  = NATIVE_WIDTH * scale_x
  def height = NATIVE_HEIGHT * scale_y
end

class Bullet < Entity
  # center_x, center_y は中心位置で受ける
  def setup(center_x:, center_y:, velocity_x:, velocity_y:, scale:, z:)
    self.image = IMAGE
    self.scale_x = self.scale_y = scale
    self.z = z
    self.x = center_x - width / 2.0
    self.y = center_y - height / 2.0
    @velocity_x = velocity_x
    @velocity_y = velocity_y
  end

  def update
    self.x += @velocity_x
    self.y += @velocity_y
    vanish if offscreen?
  end
end

class Player < Entity
  SPEED = 5
  COOLDOWN = 6
  INVINCIBLE = 90

  def setup(**)
    self.image = IMAGE
    self.scale_x = self.scale_y = 0.45
    self.z = 10
    recenter
    @cooldown = 0
    @invincibility = 0
  end

  def update
    self.x += input.x * SPEED
    self.y += input.y * SPEED
    self.x = x.clamp(0, scene.width - width)
    self.y = y.clamp(0, scene.height - height)
    @cooldown -= 1 if @cooldown > 0
    if @invincibility > 0
      @invincibility -= 1
      self.alpha = (@invincibility / 4) % 2 == 0 ? 90 : 255
    else
      self.alpha = 255
    end
  end

  def fire
    return nil if @cooldown > 0
    @cooldown = COOLDOWN
    scene.spawn(Bullet, center_x: center_x, center_y: y, velocity_x: 0, velocity_y: -12, scale: 0.18, z: 6)
  end

  def hurt
    return false if @invincibility > 0
    @invincibility = INVINCIBLE
    recenter
    true
  end

  private

  def recenter
    self.x = (scene.width - width) / 2.0
    self.y = scene.height - height - 20
  end
end

class Enemy < Entity
  def setup(x:, kind:)
    self.image = IMAGE
    self.scale_x = self.scale_y = 0.40
    self.x = x
    self.y = -50
    self.z = 0
    @kind = kind
    @initial_x = x
    @frame_count = 0
    @velocity_y = 2.4
  end

  def update
    @frame_count += 1
    case @kind
    when :sine
      self.y += @velocity_y
      self.x  = @initial_x + Math.sin(@frame_count * 0.07) * 50
    when :dive
      self.y += @velocity_y * 1.7
    else
      self.y += @velocity_y
    end
    vanish if y > scene.height + 40
  end

  def shooter? = @kind == :sine
end

class Star < Entity
  def setup(**)
    self.image = IMAGE
    self.z = -20
    reposition(rand(scene.height))
  end

  def update
    self.y += @velocity_y
    reposition(-height) if y > scene.height
  end

  private

  def reposition(start_y)
    scale = 0.03 + rand * 0.05
    self.scale_x = self.scale_y = scale
    self.x = rand(scene.width)
    self.y = start_y
    self.alpha = 70 + (scale * 1600).to_i
    @velocity_y = 1.4 + scale * 45
  end
end

class PlayScene < Scene
  LIVES = 3

  def setup(**)
    @player  = spawn(Player)
    @stars   = Array.new(16) { spawn(Star) }
    @enemies = []
    @shots   = []
    @bombs   = []
    @lives   = LIVES
    @frame_count = 0
    @spawn_wait  = 0
    @score       = 0
    setup_hud
    window.bgcolor = [6, 8, 16]
  end

  def update
    @frame_count += 1
    Sprite.update(@stars)
    @player.update
    shot = @player.fire
    @shots << shot if shot

    @spawn_wait -= 1
    if @spawn_wait <= 0
      @spawn_wait = [38 - @frame_count / 240, 12].max
      kind = [:straight, :sine, :dive, :straight].sample
      @enemies << spawn(Enemy, x: 60 + rand(width - 120), kind: kind)
    end

    Sprite.update(@enemies, @shots, @bombs)

    if @frame_count % 45 == 0
      shooter = @enemies.reject(&:vanished?).select(&:shooter?).sample
      @bombs << aimed_bomb(shooter) if shooter
    end

    collide

    Sprite.clean(@enemies, @shots, @bombs)
    @score_label.text = "SCORE #{@score}"
  end

  def draw
    Sprite.draw(@stars, @shots, @enemies, @bombs, @player,
                @title_label, @score_label, @lives_label, @life_icons.first(@lives))
  end

  private

  def setup_hud
    @title_label = spawn(Label, x: 8, y: 6, text: "RAIDEN", size: 18, color: [200, 220, 245], z: 50, bold: true)
    @score_label = spawn(Label, x: width - 8, y: 8, size: 14, color: [200, 200, 160], z: 50, align: :right)
    @lives_label = spawn(Label, x: 10, y: height - 46, text: "LIVES", size: 11, color: [150, 165, 190], z: 50)
    @life_icons = Array.new(LIVES) do |index|
      icon = spawn(Sprite, x: 10 + index * 22, y: height - 26, image: IMAGE, z: 30)
      icon.scale_x = icon.scale_y = 0.22
      icon
    end
  end

  def collide
    @shots.each do |shot|
      next if shot.vanished?
      @enemies.each do |enemy|
        next if enemy.vanished?
        next unless shot.hit?(enemy)
        shot.vanish
        enemy.vanish
        @score += 100
      end
    end

    hit = @enemies.any? { |enemy| !enemy.vanished? && enemy.hit?(@player) } ||
          @bombs.any?   { |bomb| !bomb.vanished? && bomb.hit?(@player) }
    return unless hit && @player.hurt
    @lives -= 1
    return if @lives > 0
    window.bgcolor = [40, 10, 12]
    world.push_scene(OverScene)
  end

  def aimed_bomb(enemy)
    delta_x = @player.center_x - enemy.center_x
    delta_y = @player.center_y - enemy.center_y
    distance = Math.sqrt(delta_x * delta_x + delta_y * delta_y)
    distance = 1.0 if distance < 1.0
    speed = 4.2
    spawn(Bullet, center_x: enemy.center_x, center_y: enemy.center_y,
                  velocity_x: delta_x / distance * speed, velocity_y: delta_y / distance * speed,
                  scale: 0.13, z: 2)
  end
end

class OverScene < Scene
  # 下層の update を止め、draw は通す
  def transparent = { update: false }

  def setup(**)
    @over_label = spawn(Label, x: width / 2, y: 196, text: "GAME OVER",
                        size: 40, color: [235, 110, 100], z: 60, align: :center, bold: true)
    @restart_label = spawn(Label, x: width / 2, y: 248, text: "Press SPACE to restart",
                           size: 16, color: [230, 200, 200], z: 60, align: :center)
  end

  def update
    world.set_scene(PlayScene) if input.key_push?(Input::K_SPACE)
  end

  def draw
    Sprite.draw(@over_label, @restart_label)
  end
end

window = Window.new
world  = World.new(window)
window.world = world
world.push_scene(PlayScene)
window.run
