##########################################################################
# This file is part of Vacuum Magic
# Copyright (C) 2008 by UPi <upi at sourceforge.net>
##########################################################################

use strict;
use Carp;

our (@Cos, $Difficulty, $DifficultySetting, $Game, $PhysicalScreenHeight, $PhysicalScreenWidth, $ScreenHeight, $ScreenWidth, @Sin, %Sprites);


@BossRegistry = qw(
  ShootingRange BatsNest HorrorMoon Asteroids TrialByFire RoboWitch
  CrossWind MotherCloud PangZeroBoss RedDragon WipeOut PapaKoules
  UpsideDown BlueDragon Gravity Bomber
);


##########################################################################
# GAME OBJECT CLASSES -- BOSSES
##########################################################################


package BossLifeIndicator;
package Boss;
package BossExplosion;
package MotherCloud;
package HorrorMoon;
package TrialByFire;
package RedDragon;
package BlueDragon;
package BlueDragonPullEffect;
package CrossWind;
package Gravity;
package GravityPulledGuy;
package GravityPulledBall;
package UpsideDown;
package WipeOut;
package RoboWitch;
package Debris;
package Bomber;
package LaserShot;
package PapaKoules;


##########################################################################
package BossLifeIndicator;
##########################################################################

@BossLifeIndicator::ISA = qw(GameObject);

sub new {
  my ($class, $boss) = @_;
  
  my $self = new GameObject();
  %$self = ( %$self,
    boss => $boss,
    dir => -1,
  );
  bless $self, $class;
  $self->ResetPosition();
  return $self;
}

sub Delete {
  my $self = shift;
  
  delete $self->{boss};
  $self->SUPER::Delete();
}

sub ResetPosition {
  my ($self) = @_;
  $self->{x} = $ScreenWidth - 70;
  $self->{y} = $ScreenHeight - 70;
}

sub Draw {
  my ($self) = @_;
  my ($boss, $x, $y);
  
  $boss = $self->{boss};
  $x = $self->{x};
  $y = $self->{y};
  ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE);
  foreach (1 .. $boss->{hitpoint}) {
    $::Sprites{bossvitality}->[($Game->{anim} / 3 + $x * 2) % 46]->Blit($x, $y, 50, 50);
    $x += 50 * $self->{dir};
  }
  ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE_MINUS_SRC_ALPHA);
}


##########################################################################
package Boss;
##########################################################################

@Boss::ISA = qw(Enemy);

sub new {
  my $class = shift;
  
  my $self = new Enemy(@_);
  %$self = ( %$self,
    'hit' => 0,
    'dies' => 0,
    'hitpoint' => 0,
    'isboss' => 1,
    'lifeIndicator' => new BossLifeIndicator($self),
  );
  bless $self, $class;
}

sub Delete {
  my ($self) = @_;
  
  $self->{lifeIndicator}->Delete();
  delete $self->{lifeIndicator};
  $self->SUPER::Delete();
}

sub EnforceBounds {}

sub AwardScoreForHitByProjectile {
  my ($self, $projectile, $guy) = @_;
  my ($score);
  
  $score = $self->{score};
  $self->{score} = 0;   # Don't give the player score for killing the boss just yet
  $self->SUPER::AwardScoreForHitByProjectile($projectile, $guy);
  $self->{score} = $score;
}

sub OnHitByProjectile {
  my ($self, $projectile) = @_;
  
  $self->OnDamaged(1, $projectile);
}

sub CaughtInExplosion {
  my ($self, $projectile) = @_;
  
  $self->OnDamaged(1, $projectile);
}

sub CheckHitByFireShield {}

sub BlownByWind {}

sub OnDamaged {
  my ($self, $damage, $projectile) = @_;

  return  if $self->{hitpoint} <= 0;
  &EnemyExplosion::Create($self)  if $projectile;
  $self->{hitpoint} -= $damage;
  $self->OnPushedByProjectile($projectile)  if $projectile;
  if ($self->{hitpoint} <= 0) {
    $self->OnKilled();
    $Game->PlaySound('bossdies');
  } else {
    $self->{hit} = 5;
    $Game->PlaySound('bosshit');
  }
  $Game->ExpireTimeEffect();
}

sub OnPushedByProjectile {
  my ($self, $projectile) = @_;
  $self->{speedX} = 3 * ($projectile->{speedX} > 0 ? 1 : -1);
}

sub OnKilled {
  my ($self) = @_;
  
  return  if $self->{dies};
  $self->{dies} = 1;
  new MegaExplosion($self);
  $self->GiveScoreToPlayers();
}

sub GiveScoreToPlayers {
  my ($self) = @_;
  my ($player);
  
  foreach $player (@::Players) {
    if ($player->{number} >= $::NumGuys) {
      last;
    }
    if ($player->{lives} >= 0) {
      $player->GiveScore($self->{score});
    }
  }
}


##########################################################################
package MotherCloud;
##########################################################################

@MotherCloud::ISA = qw(Boss);
$MotherCloud::LevelName = ::T('Stage Boss: Mother Cloud');
$MotherCloud::LevelDifficulty = 5;

sub new {
  my $class = shift;
  
  my $self = new Boss(@_);
  %$self = ( %$self,
    'w' => 192,
    'h' => 96,
    'collisionw' => 192 * 2 / 3,
    'collisionh' => 96 * 2 / 3,
    'x' => $ScreenWidth + 100,
    'y' => $ScreenHeight / 2,
    'speedX' => 0,
    'speedY' => 0,
    'score' => 50000,
    'acceleration' => 0.02,
    'hitpoint' => 5,
    'spawnDelay' => 500,
    'hit' => 0,
    'dies' => 0,
  );
  bless $self, $class;
  $self->SetupCollisions();
  $self;
}

sub EnemyAdvance {
  my $self = shift;
  my ($targetX, $targetY);
  
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  
  if ($self->{dies}) {
    $self->{w} -= 2;
    $self->Delete()  if $self->{w} <= 16;
    $self->{h} -= 1;
    return;
  }
  
  $targetX = $ScreenWidth - $self->{w} - 20;
  $targetY = $ScreenHeight / 2 + ($ScreenHeight / 2 - $self->{h}) * $Cos[ $Game->{anim} * 1.5 % 800 ];
  if ($self->{x} + $self->{speedX} * abs($self->{speedX}) / $self->{acceleration} / 2 < $targetX) {
    $self->{speedX} += $self->{acceleration};
  } else {
    $self->{speedX} -= $self->{acceleration};
  }
  if ($self->{y} + $self->{speedY} * abs($self->{speedY}) / $self->{acceleration} / 2 < $targetY) {
    $self->{speedY} += $self->{acceleration};
  } else {
    $self->{speedY} -= $self->{acceleration};
  }
  
  $self->CheckGuyCollisions();
  -- $self->{hit}  if $self->{hit} > 0;
  if (--$self->{spawnDelay} < 0) {
    $self->{spawnDelay} = int(100 / &::GetDifficultyMultiplier());
    $self->SpawnNewbornCloud();
  } elsif ($self->{spawnDelay} == 20) {
    $Game->PlaySound('bossspit');
    $self->SpawnNewbornCloud();
  }
}

sub SpawnNewbornCloud {
  my ($self) = @_;
    
  my $newbornCloud = new CloudKill;
  $newbornCloud->{x} = $self->{x} + $self->{w} / 2;
  $newbornCloud->{y} = $self->{y} + 24;
  $newbornCloud->{score} = 1000;
}

sub OnDamaged {
  my ($self, $damage, $projectile) = @_;
  
  $self->SUPER::OnDamaged($damage, $projectile);
  $self->{spawnDelay} += 200;
}

sub Draw {
  my $self = shift;
  
  if ($self->{hit}) {
    ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE);
  }
#  ::glEnable(::GL_COLOR_LOGIC_OP);
#  ::glLogicOp(::GL_AND_INVERTED);
  $::Sprites{mothercloud}->[$Game->{anim} / 30 % 3]->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h});
#  ::glDisable(::GL_COLOR_LOGIC_OP);
  if ($self->{hit}) {
    ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE_MINUS_SRC_ALPHA);
  }
}


##########################################################################
package HorrorMoon;
##########################################################################

@HorrorMoon::ISA = qw(Boss);
$HorrorMoon::LevelName = ::T('Stage Boss: Full Metal Moon');
$HorrorMoon::LevelDifficulty = 7;

sub new {
  my $class = shift;
  
  my $self = new Boss(@_);
  %$self = ( %$self,
    'w' => 112 * 2,
    'h' => 96 * 2,
    'collisionw' => 112 * 1.5,
    'collisionh' => 96 * 1.5,
    'x' => $ScreenWidth,
    'y' => $ScreenHeight / 2,
    'speedX' => 0,
    'speedY' => 0,
    'score' => 100000,
    'acceleration' => 0.04,
    'hitpoint' => 5,
    'hit' => 0,
    'dies' => 0,
    'chargeDelay' => 1000,
    'spawnDelay' => 500,
    'fireDelay' => 100,
    'difficulty' => 8,
    rotate => 0,
  );
  bless $self, $class;
  $self->SetupCollisions();
  $self->{targetX} = $ScreenWidth - $self->{w} - 20;
  $self;
}

sub EnemyAdvance {
  my $self = shift;
  
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  $self->{speedY} -= 0.07;
  
  if ($self->{dies}) {
    $self->{rotate} += 2 * ($self->{speedX} > 0 ? -1 : 1);
    $self->Delete()  if $self->{y} < -$self->{h};
    new BossExplosion($self)  if $Game->{anim} % 5 == 0;
    return;
  }
  
  if ($self->{x} + $self->{speedX} * abs($self->{speedX}) / $self->{acceleration} / 2 < $self->{targetX}) {
    $self->{speedX} += $self->{acceleration};
  } else {
    $self->{speedX} -= $self->{acceleration};
  }
  if ($self->{y} < -12) {
    $self->{speedY} = 7.4;
  }
  -- $self->{hit}  if $self->{hit} > 0;
  if (--$self->{chargeDelay} < 0 ) {
    if ($self->{chargeDelay} == -1) {
      $self->{targetX} = 60;
    }
    if ($self->{chargeDelay} == -300) {
      $self->{targetX} = $ScreenWidth - $self->{w} - 20;
    }
    if ($self->{chargeDelay} == -450) {
      $self->{chargeDelay} = 900;
    }
  } else {
    if (--$self->{spawnDelay} <= 0) {
      if ($self->{spawnDelay} == -60 || $self->{spawnDelay} == -80 || $self->{spawnDelay} == -100) {
        $self->SpawnBouncyMoon();
        $Game->PlaySound('bossspit')  if $self->{spawnDelay} == -60;
      } elsif ($self->{spawnDelay} == -160) {
        $self->{spawnDelay} = 720;
      }
    }
    if (--$self->{fireDelay} == 0) {
      $self->Fire();
    }
    if ($self->{fireDelay} == -70) {
      $self->Fire();
      $self->{fireDelay} = 230;
    }
  }
  $self->CheckGuyCollisions();
}


sub Fire {
  my ($self) = shift;
  my ($x, $y, $i);
  
  $x = $self->{x} + $self->{w} * 53 / 112;
  $y = $self->{y} + $self->{h} * 75 / 96;
  for ($i = -1.5; $i <= 1.5; ++$i ) {
    &Fireball::CreateFromSpeed($self, $x, $y, -$Cos[$i*35] * 7, $Sin[$i*35] * 7 + $self->{speedY});
  }
}

sub SpawnBouncyMoon {
  my ($self) = shift;
  
  my $bouncyMoon = new BouncyMoon;
  $bouncyMoon->{x} = $self->{x} + $self->{w} * 42 / 112;
  $bouncyMoon->{y} = $self->{y} + $self->{h} * 20 / 96;
  $bouncyMoon->{speedY} = $self->{speedY};
  $bouncyMoon->{score} = 1000;
}

sub Draw {
  my $self = shift;
  my ($phase, $spawnDelay);
  
  #    0      -20     -40           -120    -140      -160
  #    11111112222222223333333333333222222221111111111
  $phase = 0;
  $spawnDelay = $self->{spawnDelay};
  if ($spawnDelay < 0) {
    if ($spawnDelay > - 20) { $phase = 1 }
    elsif ($spawnDelay > - 40) { $phase = 2 }
    elsif ($spawnDelay < - 140) { $phase = 1 }
    elsif ($spawnDelay < - 120) { $phase = 2 }
    else { $phase = 3; }
  }
  
  if ($self->{hit}) {
    ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE);
  }
  if ($self->{rotate}) {
    $::Sprites{horrormoon}->[$phase]->RotoBlit($self->{x}, $self->{y}, $self->{w}, $self->{h}, $self->{rotate});
  } else {
    $::Sprites{horrormoon}->[$phase]->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h});
  }
  if ($self->{hit}) {
    ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE_MINUS_SRC_ALPHA);
  }
}

sub OnKilled {
  my ($self) = @_;
  
  $self->SUPER::OnKilled();
  $self->{speedX} = $self->{speedX} > 0 ? 1 : -1;
  $self->{speedY} = 4;
}


##########################################################################
package TrialByFire;
##########################################################################

@TrialByFire::ISA = qw(InvisibleGameObject Boss);
$TrialByFire::LevelName = ::T('Trial By Fire');
$TrialByFire::LevelDifficulty = 5;

sub new {
  my $class = shift;
  
  my $self = new Boss(@_);
  %$self = ( %$self,
    'score' => 50000,
    'hitpoint' => 4,
    'hit' => 0,
    'dies' => 0,
    'stages' => [\&InitStageOne, \&InitStageTwo, \&InitStageThree],
    'elementals' => [],
    'lava' => new Lava(),
  );
  bless $self, $class;
  $self->AdvanceToNextStage();
  $self;
}

sub EnemyAdvance {
}

sub AdvanceToNextStage {
  my $self = shift;
  my ($stages, $nextStage);
  
  $stages = $self->{stages};
  --$self->{hitpoint};
  if (0 == scalar(@$stages)) {
    $self->GiveScoreToPlayers();
    $self->{lava}->Leave();
    $self->Delete();
    return;
  } else {
    $nextStage = shift @$stages;
    $nextStage->($self);
    foreach (@{$self->{elementals}}) {
      $_->SetOnDeleted(\&OnElementalDeleted, $self);
      $_->{isboss} = 1;
    }
  }
}

sub OnElementalDeleted {
  my ($self, $elemental) = @_;
  
  ::RemoveFromList(@{$self->{elementals}}, $elemental);
  if (scalar(@{$self->{elementals}}) == 0) {
    # All the elementals in the last stage are dead.
    $self->AdvanceToNextStage();
  } else {
    foreach (@{$self->{elementals}}) {
      $_->{targetDelay} = 10  if $_->{targetDelay} > 100;
    }
  }
}

sub InitStageOne {
  my $self = shift;
  
  push @{$self->{elementals}}, new FlameElemental;
}

sub InitStageTwo {
  my $self = shift;
  my ($elemental1, $elemental2);
  
  $elemental1 = new FlameElemental();
  $elemental2 = new FlameElemental();
  $elemental2->{x} = -80;
  push @{$self->{elementals}}, $elemental1, $elemental2;
}

sub InitStageThree {
  my $self = shift;
  my ($elemental1, $elemental2);
  
  $elemental1 = new FlameElemental();
  $elemental2 = new FlameElemental();
  $elemental2->{x} = -80;
  push @{$self->{elementals}}, $elemental1, $elemental2;
  $elemental1 = new FlameElemental();
  $elemental1->{speedX} = -3;
  $elemental1->{y} = 30;
  $elemental1->{targetDelay} = 1000;
  $elemental2 = new FlameElemental();
  $elemental2->{x} = -80;
  $elemental2->{speedX} = 3;
  $elemental2->{y} = 30;
  $elemental2->{targetDelay} = 1000;
  $elemental2->{dir} = 1;
  push @{$self->{elementals}}, $elemental1, $elemental2;
}


##########################################################################
package RedDragon;
##########################################################################

@RedDragon::ISA = qw(Boss);
$RedDragon::LevelName = ::T('Stage Boss: Red Dragon');
$RedDragon::LevelDifficulty = 6;

sub new {
  my $class = shift;
  
  my $self = new Boss(@_);
  %$self = ( %$self,
    'w' => 96*2,
    'h' => 64*2,
    'collisionw' => 96*2,
    'collisionh' => 64*2,
    'x' => $ScreenWidth + 20,
    'y' => $ScreenHeight / 2,
    'speedX' => 0,
    'speedY' => 0,
    'score' => 100000,
    'acceleration' => 0.1,
    'hitpoint' => 5,
    'hit' => 0,
    'dies' => 0,
    'chargeDelay' => 1000,
    'attackType' => '',
    'attack' => 0,
    'dir' => -1,
    'anim' => 0,
#     'target' => new GameObject,
  );
  bless $self, $class;
  $self->SetupCollisions();
  $self;
}

sub EnemyAdvance {
  my $self = shift;
  
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  -- $self->{hit}  if $self->{hit} > 0;
  ++$self->{anim};
  
  if ($self->{dies}) {
    $self->{attack} += 0.5;
    if ($self->{attack} >= 50) {
      print STDERR "$self->{attack}:$self->{y} ";
      print STDERR "Death by attack\n";
      $self->Delete();
    }
    $self->{speedY} -= 0.05;
    if ($self->{y} < - $self->{h}) {
      print STDERR "$self->{attack}:$self->{y} ";
      print STDERR "Death by fall\n";
      $self->Delete();
    }
    return;
  }
  $self->DragonMovement();
  $self->DragonAttack();
  $self->CheckGuyCollisions();
}

sub DragonMovement {
  my $self = shift;
  my ($targetX, $targetY);
  
  if (--$self->{chargeDelay} > 0) {
    $targetX = $ScreenWidth - $self->{w} - abs($Cos[$self->{anim} * 1.9 % 800]) * 120;
    $self->{targetY} = $targetY = ($ScreenHeight - $self->{h}) / 2 -  + ($ScreenHeight / 2 - $self->{h}) * $Sin[ $self->{anim} * 1.9 % 800 ];
  } else {
    $targetX = $ScreenWidth - $self->{w} - abs($Sin[$self->{chargeDelay} * 2 % 800]) * 500 - 200;
    $targetY = $self->{targetY};
    $self->{chargeDelay} = 1150  if $self->{chargeDelay} < - 200;
  }
  $self->{target}->{x} = $targetX;
  $self->{target}->{y} = $targetY;
  if ($self->{x} + $self->{speedX} * abs($self->{speedX}) / $self->{acceleration} / 2 < $targetX) {
    $self->{speedX} += $self->{acceleration};
  } else {
    $self->{speedX} -= $self->{acceleration};
  }
  if ($self->{y} + $self->{speedY} * abs($self->{speedY}) / $self->{acceleration} / 2 < $targetY) {
    $self->{speedY} += $self->{acceleration};
  } else {
    $self->{speedY} -= $self->{acceleration};
  }
}

sub DragonAttack {
  my $self = shift;
  my ($flame);
  
  if ($self->{attackType} eq 'attack1') {
    ++$self->{attack};
    if ($self->{attack} >= 20 and $self->{attack} <= 70 and $self->{attack} % 15 == 0) {
      $flame = new FlameThrower($self);
      $flame->{y} = $self->{y} - 20;
      $flame->{speedX} = ($flame->{speedX} + $self->{speedX}) / 2;
      $flame = new FlameThrower($self);
      $flame->{y} = $self->{y} - 20;
      $flame->{x} = $flame->{x} - 30;
      $flame->{speedX} = ($flame->{speedX} + $self->{speedX}) / 2 - 0.8;
      $flame->{speedY} += $self->{speedY} / 2 + 0.1;
      $flame = new FlameThrower($self);
      $flame->{y} = $self->{y} - 20;
      $flame->{x} = $flame->{x} - 60;
      $flame->{speedX} = ($flame->{speedX} + $self->{speedX}) / 2 - 1.6;
      $flame->{speedY} += $self->{speedY} / 2 + 0.2;
    }
    if ($self->{attack} >= 120) {
      $self->{attackType} = '';
    }
  }
  elsif ($self->{anim} % 60 == 0) {
    $self->{attackType} = 'attack1';
    $self->{attack} = 0;
  }
}

sub Draw {
  my $self = shift;
  my ($sequence, $index);
  
  if ($self->{attackType}) {
    $sequence = $::Sprites{"reddragon_$self->{attackType}"};
    $index = $self->{attack} / 10;
  } else {
    $sequence = $::Sprites{reddragon_fly};
    $index = $self->{anim} / 10;
  }
  
  if ($self->{hit}) {
    ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE);
  }
  $sequence->[$index % @$sequence]->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h});
  if ($self->{hit}) {
    ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE_MINUS_SRC_ALPHA);
  }
}

sub OnKilled {
  my ($self) = @_;
  
  $self->SUPER::OnKilled();
  $self->{attackType} = 'die1';
  $self->{attack} = 0;
  $self->{speedX} /= 2;
}


##########################################################################
package BlueDragon;
##########################################################################

@BlueDragon::ISA = qw(Boss);
$BlueDragon::LevelName = ::T('Stage Boss: Blue Dragon');
$BlueDragon::LevelDifficulty = 5;

sub new {
  my $class = shift;
  
  my $self = new Boss(@_);
  %$self = ( %$self,
    'w' => 96*1.5,
    'h' => 64*1.5,
    'collisionw' => 96*1.5,
    'collisionh' => 64*1.5,
    'x' => $ScreenWidth + 50,
    'y' => 50,
    'speedX' => -1,
    'speedY' => 0,
    'score' => 100000,
    'acceleration' => 0.07,
    'hitpoint' => 5,
    'hit' => 0,
    'dies' => 0,
    'movementType' => 'wait',
    'movementDelay' => 200,
    'attackType' => '',
    'attack' => 0,
    'dir' => -1,
    'anim' => 0,
#     'target' => new GameObject,
  );
  bless $self, $class;
  $self->SetupCollisions();
  $self;
}

sub Delete {
  my ($self) = @_;
  
  $self->{target}->Delete()  if $self->{target}->{w};
  $self->SUPER::Delete();
}

sub EnemyAdvance {
  my $self = shift;
  
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  -- $self->{hit}  if $self->{hit} > 0;
  ++$self->{anim};
  
  if ($self->{dies}) {
    $self->{attack} += 0.5;
    if ($self->{attack} >= 50) {
      return $self->Delete();
    }
    $self->{speedY} -= 0.05;
    if ($self->{y} < - $self->{h}) {
      return $self->Delete();
    }
    return;
  }
  $self->DragonMovement();
  $self->DragonAttack();
  $self->CheckGuyCollisions();
}

sub DragonMovement {
  my $self = shift;
  my ($targetX, $targetY, $movementType);
  
  $movementType = $self->{movementType};
  if ($self->{waitForAttack}) {
    $targetX = $self->{target}->{x};
    $targetY = $self->{target}->{y};
    if (not $self->{anim} % 60) {
      $self->{attackType} = $self->{waitForAttack};
      $self->{waitForAttack} = '';
    }
  } elsif ($self->{attackType}) {
    $targetX = $self->{target}->{x};
    $targetY = $self->{target}->{y};
  } elsif ($movementType eq 'wait') {
    $targetX = $self->{dir} > 0 ? 10 : $ScreenWidth - 150;
    $targetY = 50;
    $self->TimeForAttack()  if $self->{movementDelay} == 10;
    if (--$self->{movementDelay} < 0) {
      $self->{movementType} = 'rise';
      $targetY = $ScreenHeight - 200 + $Game->Rand(300);
    }
  } elsif ($movementType eq 'rise') {
    $targetX = $self->{target}->{x};
    $targetY = $self->{target}->{y};
    if ($self->{y} >= $targetY - 50) {
      $self->TimeForAttack();
      $self->{movementType} = 'swoop';
      $self->{swoopY} = $self->{y} - 400;
    }
  } elsif ($movementType eq 'swoop') {
    $targetX = $self->{dir} > 0 ? $ScreenWidth + 150 : -300;
    $targetY = $self->{swoopY};
    $self->{speedY} -= ($self->{y} - $self->{swoopY}) / 6000;
    if (abs($self->{x} - $targetX) < 100) {
      $self->{movementType} = 'wait';
      $self->{movementDelay} = 150;
      $self->{dir} = -$self->{dir};
    }
  }
  
  $self->{target}->{x} = $targetX;
  $self->{target}->{y} = $targetY;
  if ($self->{x} + $self->{speedX} * abs($self->{speedX}) / $self->{acceleration} / 2 < $targetX) {
    $self->{speedX} += $self->{acceleration};
  } else {
    $self->{speedX} -= $self->{acceleration};
  }
  if ($self->{y} + $self->{speedY} * abs($self->{speedY}) / $self->{acceleration} / 2 < $targetY) {
    $self->{speedY} += $self->{acceleration};
  } else {
    $self->{speedY} -= $self->{acceleration};
  }
}

sub TimeForAttack {
  my ($self) = @_;
  
  return  if $self->{attackType};
  return  if $self->{target}->{y} >= $ScreenHeight - 50;
  $self->{waitForAttack} = 'attack2';
  $self->{attack} = 0;
}

sub DragonAttack {
  my ($self) = @_;
  my ($attack);
  
  if ($self->{attackType} eq 'attack2') {
    $attack = ++$self->{attack};
    if ($attack == 5) {
      new BlueDragonPullEffect($self);
    } elsif ($attack >= 120) {
      $self->{attackType} = '';
    }
  }
  
}

sub Draw {
  my $self = shift;
  my ($sequence, $index);
  
  if ($self->{attackType}) {
    $sequence = $::Sprites{"bluedragon_$self->{attackType}"};
    $index = $self->{attack} / 10;
  } else {
    $sequence = $::Sprites{bluedragon_fly};
    $index = $self->{anim} / 10;
  }
  
  if ($self->{hit}) {
    ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE);
  }
  $sequence->[$index % @$sequence]->Blit($self->{x}, $self->{y}, -$self->{w} * $self->{dir}, $self->{h});
  if ($self->{hit}) {
    ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE_MINUS_SRC_ALPHA);
  }
}

sub OnKilled {
  my ($self) = @_;
  
  $self->SUPER::OnKilled();
  $self->{attackType} = 'die1';
  $self->{attack} = 0;
  $self->{speedX} /= 2;
}


package BlueDragonPullEffect;

@BlueDragonPullEffect::ISA = qw(GameObject);

sub new {
  my ($class, $boss) = @_;
  my ($self, $dir);
  
  $dir = $boss->{dir};
  $self = new GameObject;
  %$self = ( %$self,
    'boss' => $boss,
    'dir' => $dir,
    'x' => $dir > 0 ? $boss->{x} + $boss->{w} : $boss->{x} - 500,
    'y' => $boss->{y} - $boss->{h} * 1.5,
    'w' => 500,
    'h' => $boss->{h} * 4,
    'collisionw' => 500,
    'collisionh' => $boss->{h} * 4,
    'anim' => 0,
    'speed' => 0,
    'sparks' => [],
  );
  bless $self, $class;
  $self->SetupCollisions();
  $self->{ymiddle} = $self->{y} + $self->{h} / 2;
  $self->{pulledObjects} = [ grep { $_->Collisions($self) } @::GameObjects ];
  return $self;
}

sub Advance {
  my ($self) = @_;
  my ($anim, $speed, $speedX);
  
  $anim = ++$self->{anim};
  if ($anim > 160) {
    return $self->Delete();
  } elsif ( $anim < 80 ) {
    $speed = $self->{speed} += 0.1;
    $self->AddSpark()  unless $anim % 4;
  } else {
    $speed = $self->{speed} -= 0.1;
  }
  $speedX = $speed * -$self->{dir};
  foreach (@{$self->{pulledObjects}}) {
    next  if $_->{deleted};
    $_->BlownByWind($speedX, $speed * ($self->{ymiddle} - $_->{y}) / 50);
  }
  $self->AdvanceSparks();
}

sub AddSpark {
  my ($self) = @_;
  
  push @{$self->{sparks}}, {
    x => $self->{x} + int(rand($self->{w})),
    y => $self->{y} + int(rand($self->{h})),
    speed => 0,
  };
}

sub AdvanceSparks {
  my ($self) = @_;
  
  foreach (@{$self->{sparks}}) {
    $_->{x} -= $_->{speed} * $self->{dir};
    $_->{speed} += 0.1
  }
}

sub Draw {
  my ($self) = @_;
  my ($anim);
  
  foreach (@{$self->{sparks}}) {
    $anim = $_->{speed};
    next  if $anim >= 4;
    $::Sprites{bluedragon_attack2_info_hit}->[$anim]->Blit($_->{x}, $_->{y}, 2, 2);
  }
}



##########################################################################
package CrossWind;
##########################################################################

@CrossWind::ISA = qw(InvisibleGameObject Boss);
$CrossWind::LevelName = ::T('Area Trial: Cross Wind');
$CrossWind::LevelDifficulty = 5;

sub new {
  my $class = shift;
  
  my $self = new Boss(@_);
  %$self = ( %$self,
    'score' => 50000,
    'hitpoint' => 5,
    'cloudsToSpawn' => 20,
    'cloudsSpawned' => 0,
    'spawnDelay' => 0,
    'maxCloudsOut' => 12 * &::GetDifficultyMultiplier(),
    'cloudsOut' => 0,
    'cloudsDeleted' => 0,
  );
  bless $self, $class;
  $self;
}

sub EnemyAdvance {
  my ($self) = @_;
  
  if (--$self->{spawnDelay} < 0) {
    if ($self->{cloudsSpawned} >= $self->{cloudsToSpawn}) {
      $self->{spawnDelay} = 100000;
      return;
    }
    if ($self->{cloudsOut} >= $self->{maxCloudsOut}) {
      $self->{spawnDelay} = 100;
      return;
    }
    new ReturningCloudKill($self, $self->{difficulty});
    $self->{difficulty} += 0.1;
    ++$self->{cloudsSpawned};
    ++$self->{cloudsOut};
    $self->{spawnDelay} = 50;
  }
}

sub OnCloudDeleted {
  my ($self) = @_;
  my ($hitpoint);
  
  ++$self->{cloudsDeleted};
  --$self->{cloudsOut};
  $hitpoint = 5 * ($self->{cloudsToSpawn} - $self->{cloudsDeleted}) / $self->{cloudsToSpawn};
  $self->{hitpoint} = ($hitpoint == int($hitpoint)) ? $hitpoint : int($hitpoint + 1);
  if ($self->{cloudsDeleted} == $self->{cloudsToSpawn}) {
    $self->GiveScoreToPlayers();
    $self->Delete();
  }
}



##########################################################################
package Gravity;
##########################################################################

@Gravity::ISA = qw(InvisibleGameObject Boss);
$Gravity::LevelName = ::T('Area Trial: Gravity Well');
$Gravity::LevelDifficulty = 5;

sub new {
  my $class = shift;
  
  my $self = new Boss(@_);
  %$self = ( %$self,
    'score' => 50000,
    'hitpoint' => 5,
    'acceleration' => 0.15 * &::GetDifficultyMultiplier(),
    'cometDelay' => 15,
    'cometInterval' => 40,
    'ballDelay' => 50,
    'ballsToSpawn' => 15,
    'ballsSpawned' => 0,
    'maxBalls' => 7,
    'lava' => new Lava(),
  );
  bless $self, $class;
  $self->SpawnBall();
  $self;
}

sub EnemyAdvance {
  my ($self) = @_;
  
  if (--$self->{cometDelay} <= 0) {
    my $comet = new Comet;
    $comet->{isboss} = 1;
    $self->{cometDelay} = $self->{cometInterval} / &::GetDifficultyMultiplier();
  }
  if (--$self->{ballDelay} <= 0) {
    $self->SpawnBall();
    $self->{ballDelay} = 50;
    my $hitpoint = 5 * ($self->{ballsToSpawn} - $self->{ballsSpawned} + $Ball::Balls) / $self->{ballsToSpawn};
    $self->{hitpoint} = ($hitpoint == int($hitpoint)) ? $hitpoint : int($hitpoint + 1);
  }
  foreach (@Guy::Guys) {
    &GravityPulledGuy::InitGravity($_, $self)  unless $_->{isGravityGuy};
  }
  if ($Ball::Balls == 0 && $self->{ballsSpawned} >= $self->{ballsToSpawn}) {
    $self->{lava}->Leave();
    foreach (@Guy::Guys) {
      $_->EndGravity();
    }
    $self->Delete();
  }
}

sub SpawnBall {
  my ($self) = @_;
  
  return  if $Ball::Balls >= $self->{maxBalls};
  return  if $self->{ballsSpawned} >= $self->{ballsToSpawn};
  new GravityPulledBall($self->{acceleration} / 5);
  $self->{cometInterval} -= 1;
  $self->{acceleration} += 0.01 * &::GetDifficultyMultiplier();
  ++$self->{ballsSpawned};
}


##########################################################################
package Lava;
##########################################################################

@Lava::ISA = qw(Enemy);
use vars qw(@LavaPhaseH @LavaPhaseY);
@LavaPhaseH = (30,40,40,50,40,40,40,40,40,40,40,40);

sub new {
  my ($class) = @_;
  
  my $self = new Enemy();
  %$self = (%$self,
    x => 0,
    y => -80,
    w => $ScreenWidth,
    h => 50,
    leaving => 0,
    isboss => 1,
  );
  my ($i, $h);
  for ($i = $h = 0; $i < scalar(@LavaPhaseH); ++$i) {
    $LavaPhaseY[$i] = $h;
    $h += $LavaPhaseH[$i];
  }
  bless $self, $class;
}

sub Leave {
  my ($self) = @_;
  
  $self->{leaving} = 1;
}

sub Advance {
  my ($self) = @_;
  
  if ($self->{leaving}) {
    $self->{y} -= 0.25;
    $self->Delete()  if $self->{y} < -80;
    return;
  } elsif ($self->{y} < -30) {
    $self->{y} += 0.25;
  }
  $self->CheckGuyCollisions();
}

sub Draw {
  my ($self) = @_;
  my ($anim, $lavaH, $lavaY);
  
  $anim = $Game->{anim} / 8 % 12;
  $lavaH = $LavaPhaseH[$anim];
  $lavaY = $LavaPhaseY[$anim];
  
  ::glColor(1,1,1,0.5);
  $::Textures{lava}->Blit($self->{x}, $self->{y}+30, $self->{w}, $lavaH, 0, $lavaY, $ScreenWidth, $lavaH);
  ::glColor(1,1,1);
}

sub CheckHitByProjectile { 0 }


##########################################################################
package GravityPulledGuy;
##########################################################################

@GravityPulledGuy::ISA = qw( Guy );

sub InitGravity {
  my ($guy, $gravity) = @_;
  
  $guy->{isGravityPulledGuy} = 1;
  $guy->{gravity} = $gravity;
  bless $guy;
}

sub EndGravity {
  my ($self) = @_;
  
  delete $self->{isGravityPulledGuy};
  delete $self->{gravity};
  bless $self, 'Guy';
}

sub HandleDirectionKeys {
  my ($self) = @_;
  my ($speedY, $keys, $rocking);
  
  $speedY = $self->{speedY};
  $keys = $self->{player}->{keys};
  $rocking = $self->{rocking} ? 0.75 : 1;
  $self->SUPER::HandleDirectionKeys();
  
  if ( $::Keys{$keys->[3]} ) {
    $self->{speedY} = $speedY - 0.5 * $rocking;
  } elsif ( $::Keys{$keys->[2]} ) {
    $self->{speedY} = $speedY + 0.5 * $rocking;
  } else {
    $self->{speedY} = $speedY;
  }
  $self->{speedY} -= $self->{gravity}->{acceleration} / 2  if $self->{state} ne 'spawning';
  $self->{speedY} = -6  if $self->{speedY} < -6;
  $self->{speedY} = 2  if $self->{speedY} > 2;
}


##########################################################################
package GravityPulledBall;
##########################################################################

@GravityPulledBall::ISA = qw(Ball);

sub new {
  my ($class, $acceleration) = @_;
  my ($self, $bounceHeight);

  $self = new Ball;
  # bounceHeight = reboundSpeed * reboundSpeed / acceleration / 2
  # reboundSpeed = sqrt ( bounceHeight * acceleration * 2 )
  $bounceHeight = (100 + $Game->Rand(200)) / &::GetDifficultyMultiplier();  
  
  %$self = ( %$self,
    'acceleration' => $acceleration,
    'reboundSpeed' => sqrt($bounceHeight * $acceleration * 2),
    'speedX' => $Ball::Balls % 2 ? 2.5 : -2.5,
  );
  bless $self, $class;
}

sub EnemyAdvance {
  my $self = shift;
  
  return $self->LeavingAdvance()  if $self->{state} eq 'leaving';
  $self->{x} += $self->{speedX};
  $self->{speedY} -= $self->{acceleration};
  $self->{y} += $self->{speedY};
  if ($self->{x} < 0) {
    $self->{speedX} = abs($self->{speedX});
    $self->{x} = 0;
  } elsif ($self->{x} > $ScreenWidth - $self->{w}) {
    $self->{speedX} = -abs($self->{speedX});
    $self->{x} = $ScreenWidth - $self->{w};
  } elsif ($self->{y} < 0) {
    $self->{speedY} = $self->{reboundSpeed};
    $self->{y} = 0;
  }
  $self->CheckSlurpCollisions();
}

sub Spat {
  my ($self, $recommendedSpeedX, $recommendedSpeedY) = @_;
  my ($speedX);
  
  $speedX = abs($self->{speedX});
  $self->SUPER::Spat($recommendedSpeedX > 0 ? $speedX : -$speedX, 0);
}


##########################################################################
package BatsNest;
##########################################################################

@BatsNest::ISA = qw(InvisibleGameObject Boss);
$BatsNest::LevelName = ::T("Area Trial: Bat's Nest");
$BatsNest::LevelDifficulty = 5;

sub new {
  my $class = shift;
  
  my $self = new Boss(@_);
  %$self = ( %$self,
    'score' => 50000,
    'hitpoint' => 5,
    'batsToSpawn' => 20,
    'batsToGo'    => 20,
    'spawnDelay'  => 0,
    'maxBatsOut'  => 7,
    'batsOut'     => 0,
  );
  bless $self, $class;
  $self;
}

sub EnemyAdvance {
  my ($self) = @_;
  
  return  if (--$self->{spawnDelay} >= 0);
  if ($self->{batsToGo} == 0) {
    $self->{spawnDelay} = 100;
    return;
  }
  $self->SpawnBat();
  $self->{spawnDelay} = 130 / &::GetDifficultyMultiplier();
}

sub SpawnBat {
  my ($self) = @_;
  
  my $bat;
  return  if $self->{maxBatsOut} <= $self->{batsOut};
  if ($self->{batsToGo} <= 2) {
    return if $self->{batsOut} > 5;
    $bat = new FireBat($self->{difficulty});
  } else {
    $bat = new Bat($self->{difficulty});
  }
  $bat->{isboss} = 1;
  $bat->SetOnDeleted(\&OnBatDeleted, $self);
  $self->{difficulty} += 0.1;
  --$self->{batsToGo};
  ++$self->{batsOut};
}

sub OnBatDeleted {
  my ($self, $bat) = @_;
  
  --$self->{batsOut};
  my $hitpoint = 5 * ($self->{batsToGo} + $self->{batsOut}) / $self->{batsToSpawn};
  $self->{hitpoint} = ($hitpoint == int($hitpoint)) ? $hitpoint : int($hitpoint + 1);
  if ($self->{batsOut} == 0 and $self->{batsToGo} == 0) {
    $self->GiveScoreToPlayers();
    $self->Delete();
  }
}

sub OnPlayerKilled {
  my ($self) = @_;
  
  $self->{spawnDelay} += 200;
}


##########################################################################
package Asteroids;
##########################################################################

@Asteroids::ISA = qw(InvisibleGameObject Boss);
$Asteroids::LevelName = ::T("Area Trial: Asteroids");
$Asteroids::LevelDifficulty = 3;

@Asteroids::Levels = (
# difficulty sizes        spawners
  [   2,  [ 96         ], '' ],
  [   3,  [ 96, 48     ], '' ],
  [   2,  [ 96, 96     ], '' ],
  [   3,  [ 96, 48, 32 ], '' ],
  [   2,  [ 96         ], 'BatAndBeeSpawner,200,8' ],
  [   3,  [ 96, 96     ], 'BatAndBeeSpawner,200,10' ],
  [ 3.5,  [ 96, 96     ], 'EasySpawner,200,8' ],
  [ 4.0,  [ 96, 96, 48 ], 'EasySpawner,200,10' ],
  [ 4.5,  [ 96, 96, 48 ], 'FullEasySpawner,200,10' ],
  [ 5.0,  [ 96, 96, 96 ], 'FullHardSpawner,200,10' ],
);

sub new {
  my $class = shift;
  my ($self, $level);
  
  $self = new Boss(@_);
  %$self = ( %$self,
    score => 50000,
    hitpoint => 5,
    maxAsteroids => 1,
    asteroidsLeft => 1,
    spawnDelay => 100,
  );
  bless $self, $class;
  
  $level = $::Level->{asteroidsLevel};
  $level = 1  unless defined($level);
  $self->SetLevel($level);
  $self->CalculateAsteroidTotal();
  $::Level->{timeLeft} += 3000;
  $self;
}

sub SetLevel {
  my ($self, $level) = @_;
  my ($levelDesc, $difficulty, $sizes, $spawners, $size, $asteroid);
  
  $levelDesc = $Asteroids::Levels[$level];
  Carp::confess  unless $levelDesc;
  ($difficulty, $sizes, $spawners) = @$levelDesc;
  warn "Setting asteroid level: l=$level d=$difficulty s=@$sizes sp=$spawners";
  $Difficulty = $::Level->{difficulty} = $difficulty;
  foreach $size (@$sizes) {
    $asteroid = new Asteroid($ScreenWidth - $size/2, $ScreenHeight / 2, $size);
    $asteroid->{isboss} = 1;
    $asteroid->SetOnDeleted(\&OnAsteroidDestroyed, $self);
  }
  $::Level->MakeSpawners($spawners)  if $spawners;
}

sub CalculateAsteroidTotal {
  my ($self) = @_;
  my ($total, $asteroid, $size, $value);
  
  $total = 0;
  foreach $asteroid (@::GameObjects) {
    next  unless $asteroid->isa('Asteroid');
    $value = 1;
    $size = $asteroid->{w};
    while ($size > 15) {
      $total += $value;
      $value *= 2;
      $size /= 2;
      warn "CalculateAsteroidTotal $size $value $total";
    }
  }
  $self->{maxAsteroids} = $self->{asteroidsLeft} = $total;
}

sub EnemyAdvance {}

sub OnAsteroidDestroyed {
  my ($self) = @_;
  
  --$self->{asteroidsLeft};
  my $hitpoint = 5 * $self->{asteroidsLeft} / $self->{maxAsteroids};
  $self->{hitpoint} = ($hitpoint == int($hitpoint)) ? $hitpoint : int($hitpoint + 1);
  if ($self->{asteroidsLeft} <= 0) {
    $self->GiveScoreToPlayers();
    $self->Delete();
  }
}


##########################################################################
package PangZeroBoss;
##########################################################################

@PangZeroBoss::ISA = qw(InvisibleGameObject Boss);
$PangZeroBoss::LevelName = ::T("Area Trial: Pang Zero");
$PangZeroBoss::LevelDifficulty = 3;
$PangZeroBoss::Boss = undef;

@PangZeroBoss::Levels = (
# difficulty sizes        spawners
  [   2,  [ 32, 32     ], 'PangGuySpawner,400,4'],
  [   2,  [ 64, 64     ], 'PangGuySpawner,400,4'],
  [   2,  [ 96, 64     ], 'PangGuySpawner,400,4, BatAndBeeSpawner,500,5'],
  [   3,  [ 96, 96, 32 ], 'PangGuySpawner,400,4, BatAndBeeSpawner,500,5'],
  [   3,  [128         ], 'BatAndBeeSpawner,200,8' ],
  [ 2.8,  [ 96, 32     ], 'PangGuySpawner,400,5, FullEasySpawner,200,5, BatAndBeeSpawner,500,5'],
  [ 3.5,  [ 96, 96     ], 'PangGuySpawner,400,5, FullEasySpawner,200,5, BatAndBeeSpawner,500,5' ],
  [ 4.0,  [ 96, 96, 32 ], 'PangGuySpawner,400,5, EasySpawner,200,10' ],
  [ 4.5,  [128, 64     ], 'FullModerateSpawner,200,10' ],
  [ 5.0,  [128, 96     ], 'FullHardSpawner,200,10' ],
);

sub new {
  my $class = shift;
  my ($self, $level);
  
  $self = new Boss(@_);
  %$self = ( %$self,
    score => 50000,
    hitpoint => 5,
    maxAsteroids => 1,
    asteroidsLeft => 1,
    spawnDelay => 100,
    balls => [],
  );
  bless $self, $class;
  $PangZeroBoss::Boss = $self;
  
  $level = $::Level->{pangzeroLevel};
  $level = 5  unless defined($level);
  $self->SetLevel($level);
  $self->CalculateBallTotal();
  $::Level->{timeLeft} += 6000;
  $self;
}

sub SetLevel {
  my ($self, $level) = @_;
  my ($levelDesc, $difficulty, $sizes, $spawners, $size);
  
  $levelDesc = $PangZeroBoss::Levels[$level];
  Carp::confess  unless $levelDesc;
  ($difficulty, $sizes, $spawners) = @$levelDesc;
  warn "Setting pangzero level: l=$level d=$difficulty s=@$sizes sp=$spawners";
  $Difficulty = $::Level->{difficulty} = $difficulty;
  foreach $size (@$sizes) {
    new PangBall($size)->{isboss} = 1;
  }
  $::Level->MakeSpawners($spawners)  if $spawners;
}

sub CalculateBallTotal {
  my ($self) = @_;
  my ($total, $pangball, $size);
  
  $total = 0;
  foreach $pangball (@{$self->{balls}}) {
    $size = $pangball->{size};
    if ($size <= 16) { $total += 1; }
    elsif ($size <= 32) { $total += 3; }
    elsif ($size <= 64) { $total += 7; }
    elsif ($size <= 96) { $total += 15; }
    else { $total += 31; }
  }
  $self->{maxBalls} = $self->{ballsLeft} = $total;
}

sub Delete {
  my ($self) = @_;
  
  $PangZeroBoss::Boss = undef;
  $self->SUPER::Delete();
}

sub EnemyAdvance {}

sub OnBallCreated {
  my ($self, $pangball) = @_;
  
  push @{$self->{balls}}, $pangball;
}

sub OnBallDestroyed {
  my ($self, $pangball) = @_;
  
  ::RemoveFromList @{$self->{balls}}, $pangball;
  --$self->{ballsLeft};
  my $hitpoint = 5 * $self->{ballsLeft} / $self->{maxBalls};
  $self->{hitpoint} = ($hitpoint == int($hitpoint)) ? $hitpoint : int($hitpoint + 1);
  if ($self->{ballsLeft} <= 0) {
    $self->GiveScoreToPlayers();
    $self->Delete();
  }
}


##########################################################################
package UpsideDown;
##########################################################################

@UpsideDown::ISA = qw(InvisibleGameObject Boss);
$UpsideDown::LevelName = ::T('Area Trial: Upside Down');
$UpsideDown::LevelDifficulty = 4;

sub new {
  my $class = shift;
  
  my $self = new Boss(@_);
  %$self = ( %$self,
    'score' => 50000,
    'hitpoint' => 5,
    'cometDelay' => 100000,
    'cometInterval' => 140,
    'ballDelay' => 0,
    'ballsToSpawn' => 12,
    'ballsSpawned' => 0,
    'maxBalls' => 7,
    'anim' => 200,
  );
  bless $self, $class;
  $Game->SetSpecialProjection($self);
  $self->{lifeIndicator}->{x} = 20;
  $self->{lifeIndicator}->{y} = 20;
  $self->{lifeIndicator}->{dir} = 1;
  return $self;
}

sub Delete {
  my ($self) = @_;
  
  $Game->SetSpecialProjection(undef);
  $self->SUPER::Delete();
}

sub EnemyAdvance {
  my ($self) = @_;
  
  if (--$self->{cometDelay} <= 0) {
    my $comet = new Comet;
    $comet->{isboss} = 1;
    $self->{cometDelay} = $self->{cometInterval};
  }
  if (--$self->{ballDelay} <= 0) {
    $self->SpawnBall();
    $self->{ballDelay} = 50;
    my $hitpoint = 5 * ($self->{ballsToSpawn} - $self->{ballsSpawned} + $Ball::Balls) / $self->{ballsToSpawn};
    $self->{hitpoint} = ($hitpoint == int($hitpoint)) ? $hitpoint : int($hitpoint + 1);
  }
  --$self->{anim}  if $self->{anim};
  if ($Ball::Balls == 0 && $self->{ballsSpawned} >= $self->{ballsToSpawn}) {
    if (0 == $self->{anim}) {
      $self->{cometInterval} = 100000;
      $self->{anim} = 200;
    }
    if (1 == $self->{anim}) {
      return $self->Delete();
    }
  }
}

sub SpawnBall {
  my ($self) = @_;
  my ($enemy);
  
  return  if $Ball::Balls >= $self->{maxBalls};
  return  if $self->{ballsSpawned} >= $self->{ballsToSpawn};
  new Ball($self->{difficulty});
  $self->{cometInterval} -= 1;
  ++$self->{ballsSpawned};
  $Difficulty = $self->{difficulty} += 0.1;
  if ($self->{ballsSpawned} == 8) {
    foreach (1..3) {
      $enemy = new Bee;
      $enemy->{isboss} = 1;
    }
  }
  if ($self->{ballsSpawned} == 12) {
    foreach (1..3) {
      $enemy = new Bat;
      $enemy->{isboss} = 1;
    }
  }
  if ($self->{ballsSpawned} == 12) {
    $self->{cometDelay} = 0;
  }
}

sub SpecialProjection {
  my ($self) = @_;
  my ($anim, $x1, $y1, $x2, $y2, $clipx, $clipy, $clipw, $cliph, $ow, $oh);
  
  $anim = $self->{anim};
  $anim = 200 - $anim  if $Ball::Balls;
  if ($anim >= 200) {
    $x1 = $ScreenWidth;
    $y1 = $ScreenHeight;
  } elsif ($anim >= 100) {
    $x1 = (-$Cos[($anim - 100) * 4] * $ScreenWidth + $ScreenWidth) / 2;
    $y1 = $ScreenHeight;
  } else {
    $x1 = 0;
    $y1 = (-($Cos[$anim * 4]) * $ScreenHeight + $ScreenHeight) / 2;
  }
  $x1 = int($x1);
  $y1 = int($y1);
  $x2 = $ScreenWidth - $x1;
  $y2 = $ScreenHeight - $y1;
  
  $clipx = $x1 < $x2 ? $x1 : $x2;
  $clipy = $y1 < $y2 ? $y1 : $y2;
  $clipw = abs($x1-$x2) || 1;
  $cliph = abs($y1-$y2) || 1;
  
#   print STDERR "$anim ($x1 $y1 $x2 $y2) ($clipx $clipy $clipw $cliph) ";
  
  $ow = int( $ScreenWidth * $ScreenWidth / ( ($x2 - $x1) || 1) / 2 );
  $oh = int( $ScreenHeight * $ScreenHeight / ( ($y2 - $y1) || 1) / 2 );
  $x1 = $ScreenWidth / 2 - $ow;
  $x2 = $ScreenWidth / 2 + $ow;
  $y1 = $ScreenHeight / 2 - $oh;
  $y2 = $ScreenHeight / 2 + $oh;
#   print STDERR "($x1 $y1 $x2 $y2)\n";
  
  ::glMatrixMode(::GL_PROJECTION);
  ::glLoadIdentity();
  ::glEnable(::GL_SCISSOR_TEST);
  ::glOrtho($x1, $x2, $y1, $y2, -1000, 1000);
  ::glScissor($clipx * $PhysicalScreenWidth / $ScreenWidth, $clipy * $PhysicalScreenHeight / $ScreenHeight, $clipw * $PhysicalScreenWidth / $ScreenWidth, $cliph * $PhysicalScreenHeight / $ScreenHeight);
  ::glMatrixMode(::GL_MODELVIEW);
  return $anim;
}


##########################################################################
package WipeOut;
##########################################################################

@WipeOut::ISA = qw(InvisibleGameObject Boss);
$WipeOut::LevelName = ::T('Area Trial: Wipe Out');
$WipeOut::LevelDifficulty = 4;

sub new {
  my $class = shift;
  
  my $self = new Boss(@_);
  %$self = ( %$self,
    'score' => 50000,
    'hitpoint' => 5,
    'cometDelay' => 100000,
    'cometInterval' => 140,
    'ballDelay' => 0,
    'ballsToSpawn' => 12,
    'ballsSpawned' => 0,
    'maxBalls' => 7,
    'screenSize' => 100,
    'requestedScreenSize' => 90,
  );
  bless $self, $class;
  $self->MakePreadvanceAction();
  $Game->SetSpecialProjection($self);
  return $self;
}

sub Delete {
  my ($self) = @_;
  
  $Game->RemoveAction($self->{preAdvanceAction});
  $ScreenWidth = 800;
  $ScreenHeight = 600;
  $Game->SetSpecialProjection(undef);
  $self->SUPER::Delete();
}

sub OnPlayerRespawn {
  my $self = shift;
  $self->{requestedScreenSize} += 15;
  $self->{requestedScreenSize} = 100  if $self->{requestedScreenSize} > 100;
  $self->SUPER::OnPlayerRespawn(@_);
}

sub AdjustScreenSize {
  my ($self) = @_;
  
  my $requestedScreenSize = $self->{requestedScreenSize};
  $requestedScreenSize = 100  if $requestedScreenSize > 100;
  if ($self->{screenSize} < $requestedScreenSize) {
    $self->{screenSize} += 0.5;
  } else {
    $self->{screenSize} -= 0.25;
    $self->{screenSize} = $requestedScreenSize  if $self->{screenSize} < $requestedScreenSize;
  }
}

sub EnemyAdvance {
  my ($self) = @_;
  
  if (--$self->{cometDelay} <= 0) {
    my $comet = new Comet;
    $comet->{isboss} = 1;
    $self->{cometDelay} = $self->{cometInterval};
  }
  if (--$self->{ballDelay} <= 0) {
    $self->SpawnBall();
    $self->{ballDelay} = 50;
    my $hitpoint = 5 * ($self->{ballsToSpawn} - $self->{ballsSpawned} + $Ball::Balls) / $self->{ballsToSpawn};
    $self->{hitpoint} = ($hitpoint == int($hitpoint)) ? $hitpoint : int($hitpoint + 1);
  }
  
  if ($Ball::Balls == 0 && $self->{ballsSpawned} >= $self->{ballsToSpawn}) {
    $self->{requestedScreenSize} = 100;
    return $self->Delete()  if $self->{screenSize} == 100;
  } else {
    $self->{requestedScreenSize} -= 0.01  if $self->{requestedScreenSize} > 15;
  }
  $self->AdjustScreenSize();
}

sub OnBallDestroyed {
  my ($enemy);
  
  if ($Game->Rand(2) < 1) {
    $enemy = new Bat;
  } else {
    $enemy = new Bee;
  }
  $enemy->{isboss} = 1;
}

sub SpawnBall {
  my ($self) = @_;
  my ($ball, $enemy);
  
  return  if $Ball::Balls >= $self->{maxBalls};
  return  if $self->{ballsSpawned} >= $self->{ballsToSpawn};
  $ball = new Ball($self->{difficulty});
  $ball->SetOnDeleted(\&OnBallDestroyed);
  $self->{cometInterval} -= 1;
  ++$self->{ballsSpawned};
  $Difficulty = $self->{difficulty} += 0.1;
  if ($self->{ballsSpawned} == 8) {
    foreach (1..3) {
      $enemy = new Bee;
      $enemy->{isboss} = 1;
    }
  } elsif ($self->{ballsSpawned} == 12) {
    foreach (1..3) {
      $enemy = new Bat;
      $enemy->{isboss} = 1;
    }
    $self->{cometDelay} = 0;
  }
}

sub MakePreadvanceAction {
  my ($self) = @_;
  $self->{preAdvanceAction} = {
    boss => $self,
    advance => \&OnPreadvanceAction,
  };
  $Game->AddPreadvanceAction($self->{preAdvanceAction});
}

sub OnPreadvanceAction {
  my ($preAdvanceAction) = @_;
  my ($self, $screenSize);
  
  $self = $preAdvanceAction->{boss};
  $screenSize = abs($self->{screenSize});
  $ScreenWidth = 800 * $screenSize /  100;
  $ScreenHeight = 600 * $screenSize / 100;
  $self->{lifeIndicator}->ResetPosition();
}

sub SpecialProjection {
  my ($self) = @_;
  my ($screenSize, $clipx, $clipy, $clipw, $cliph);
  
  $screenSize = abs($self->{screenSize});
  $ScreenWidth = 800 * $screenSize /  100;
  $ScreenHeight = 600 * $screenSize / 100;
  
  $clipx = (800 - $ScreenWidth) / 2;
  $clipy = (600 - $ScreenHeight) / 2;
  $clipw = $ScreenWidth;
  $cliph = $ScreenHeight;
  
  ::glMatrixMode(::GL_PROJECTION);
  ::glLoadIdentity();
  ::glOrtho(-$clipx, 800-$clipx, -$clipy, 600-$clipy, -1000, 1000);
  ::glEnable(::GL_SCISSOR_TEST);
  ::glScissor($clipx * $PhysicalScreenWidth / 800, $clipy * $PhysicalScreenHeight / 600, $clipw * $PhysicalScreenWidth / 800, $cliph * $PhysicalScreenHeight / 600);
  ::glMatrixMode(::GL_MODELVIEW);
}


##########################################################################
package ShootingRange;
##########################################################################

@ShootingRange::ISA = qw(InvisibleGameObject Boss);
$ShootingRange::LevelName = ::T('Area Trial: Shooting Range');
$ShootingRange::LevelDifficulty = 3;

@ShootingRange::Waves = (
  [ 4, 4, 5, 5 ],
  [ 6, 6, 7, 7 ],
  [ (8) x (4 * &::GetDifficultyMultiplier()) ],
);

sub new {
  my $class = shift;
  
  my $self = new Boss(@_);
  %$self = ( %$self,
    score => 50000,
    hitpoint => 4,
    bullseyes => 0,
    waves => [ @ShootingRange::Waves ],
    timeBonus => [ 0, 30 / &::GetDifficultyMultiplier(), 60 / &::GetDifficultyMultiplier() ],
    spawners => ['', 'EasySpawner,100,6', 'BatAndBeeSpawner,100,6' ],
  );
  bless $self, $class;
  $self->SpawnNextWave();
  return $self;
}

sub EnemyAdvance {
  while ($Ball::Balls < 5) {
    new Ball($Difficulty)->{score} = 0;
  }
}

sub SpawnNextWave {
  my ($self) = @_;
  my ($wave, $bullseyeLevel, @bullseyeParams, $bullseye, $timeBonus);
  
  $self->{hitpoint}--;
  $wave = shift @{$self->{waves}};
  unless ($wave) {
    $self->GiveScoreToPlayers();
    $self->Delete();
    return;
  }
  
  $Difficulty += 0.5;
  $timeBonus = shift @{$self->{timeBonus}};
  $::Level->{timeLeft} += $timeBonus * 100;
  $::Level->MakeSpawners(shift @{$self->{spawners}});
  foreach $bullseyeLevel (@$wave) {
    (undef, undef, @bullseyeParams) = @{$TargetPracticeLevel::Levels[$bullseyeLevel-1]};
    $bullseye = new Bullseye(@bullseyeParams);
    $bullseye->{score} = 1000;
    $bullseye->{isboss} = 1;
    $bullseye->SetOnDeleted(\&OnBullseyeDeleted, $self);
    ++$self->{bullseyes};
  }
}

sub OnBullseyeDeleted {
  my ($self) = @_;
  
  if (--$self->{bullseyes} <= 0) {
    $self->SpawnNextWave();
  }
}


##########################################################################
package RoboWitch;
##########################################################################

@RoboWitch::ISA = qw(CompoundCollision Boss);
$RoboWitch::LevelName = ::T('Area Boss: RoboWitch');
$RoboWitch::LevelDifficulty = 5;

sub new {
  my $class = shift;
  
  my $self = new Boss(@_);
  %$self = ( %$self,
    'score' => 100000,
    'hitpoint' => 3,
    'acceleration' => 0.04,
    'fireDelay' => 100,
    'fireInterval' => 55,
    'x' => $ScreenWidth + 50,
    'y' => 500,
    'w' => 112 * 2,
    'h' => 64 * 2,
    collisionPieces => [{ 'collisionmarginw1' => 16*2, 'collisionmarginh1' => 24*2, 'collisionmarginw2' =>  48*2, 'collisionmarginh2' => 56*2 },
                        { 'collisionmarginw1' => 48*2, 'collisionmarginh1' =>  0*2, 'collisionmarginw2' => 112*2, 'collisionmarginh2' => 48*2 } ],
    phase => 0,
    anim => 0,
  );
  bless $self, $class;
  return $self;
}

sub EnemyAdvance {
  my ($self) = @_;
  my ($targetX, $targetY);
  
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  ++$self->{anim};
  if ($self->{dies}) {
    $self->{speedY} -= 0.05;
    if ($self->{y} <= -$self->{h}) {
      $self->Delete();
    }
    $self->{witch}->[0] += $self->{witchSpeedX};
    $self->{witch}->[1] += $self->{witchSpeedY};
    return;
  }
  if (--$self->{fireDelay} <= 0) {
    $self->{fireDelay} = $self->{fireInterval} / &::GetDifficultyMultiplier();
    new Fireball($self);
  }
  
  $targetX = 500;
  $targetY = 400 + $Sin[(6 * $Game->{anim}) % 800] * 30;
  $self->{y} += $Sin[(6 * $Game->{anim}) % 800] / 2;
  $self->{target}->{x} = $targetX;
  $self->{target}->{y} = $targetY;
  if ($self->{x} + $self->{speedX} * abs($self->{speedX}) / $self->{acceleration} / 2 < $targetX) {
    $self->{speedX} += $self->{acceleration};
  } else {
    $self->{speedX} -= $self->{acceleration};
  }
  if ($self->{y} + $self->{speedY} * abs($self->{speedY}) / $self->{acceleration} / 2 < $targetY) {
    $self->{speedY} += $self->{acceleration};
  } else {
    $self->{speedY} -= $self->{acceleration};
  }
  $self->CheckGuyCollisions();
}

sub OnDamaged {
  my ($self, $damage, $projectile) = @_;
  
  $self->SUPER::OnDamaged($damage, $projectile);
  $self->{fireDelay} = 100;
  $self->SpawnBats()  if $self->{hitpoint} > 0;
  if ($self->{hitpoint} <= 0) {
    $self->{witchSpeedX} = -2;
    $self->{witchSpeedY} = -1;
  } elsif ($self->{hitpoint} <= 1 and $self->{phase} < 2) {
    $self->{phase} = 2;
    $self->{witch} = [32*2, 8*2];
    $self->{fireInterval} = 45;
    $self->{fire} = [ [-32*2,0*2], [-40*2,32*2], [16*2,8*2] ];
    $self->{w} = 64 * 2;
    $self->{h} = 48 * 2;
    $self->{x} += 40 * 2;
    $self->{collisionPieces} = [{ 'collisionmarginw1' =>  0, 'collisionmarginh1' => 0, 'collisionmarginw2' =>  64*2, 'collisionmarginh2' => 48*2 }];
    foreach (1..15) { new Debris($self); }
  } elsif ($self->{hitpoint} <= 2 and $self->{phase} < 1) {
    $self->{phase} = 1;
    $self->{witch} = [64*2, 8*2];
    $self->{fireInterval} = 50;
    $self->{fire} = [ [8*2,40*2], [-24*2,24*2], [18*2,2*2] ];
    foreach (1..15) { new Debris($self); }
  }
}

sub SpawnBats {
  my ($self) = @_;
  my ($numBats, $direction, $angle, $bat, $x, $y);
  
  $numBats = $DifficultySetting + 3;
  print STDERR "OnDamaged->SpawnBats $numBats\n";
  $angle = int(800 / $numBats);
  $direction = 0;
  $x = $self->{x} + $self->{w} / 2;
  $y = $self->{y} + $self->{h} / 2;
  foreach (1 .. $numBats) {
    $bat = new Bat();
    $bat->{x} = $x;
    $bat->{y} = $y;
    $bat->{direction} = $direction;
    $bat->{isboss} = 1;
    $direction += $angle;
  }
}

sub Draw {
  my ($self) = @_;
  
  $::Sprites{robowitch}->[$self->{phase}]->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h});
  if ($self->{witch}) {
    $::Sprites{robowitch}->[3]->Blit($self->{x} + $self->{witch}->[0], $self->{y} + $self->{witch}->[1], 32*2, 32*2);
  }
  $self->DrawFires($self->{fire})  if $self->{fire};
}

package Debris;
@Debris::ISA = qw(GameObject);

sub new {
  my ($class, $enemy) = @_;
  my ($self, $size);
  
  $self = new GameObject;
  $size = 32;
  %$self = (%$self,
    x => $enemy->{x} + rand($enemy->{w} - $size),
    y => $enemy->{y} + rand($enemy->{h} - $size),
    w => $size,
    h => $size,
    speedX => rand(4) - 3,
    speedY => rand(2) + 1,
  );
  bless $self, $class
}

sub Advance  {
  my ($self) = @_;
  
  $self->{speedY} -= 0.07;
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  $self->Delete()  if $self->{y} < $self->{h};
}

sub Draw {
  my ($self) = @_;
  
  $::Sprites{rubble}->[$Game->{anim} / 4 % 4]->Blit( $self->{x}, $self->{y}, $self->{w}, $self->{h} );
}


##########################################################################
package Koules;
##########################################################################

@Koules::ISA = qw(Enemy);
use vars qw(@Koules);

sub new {
  my ($class, $size) = @_;

  my $self = new Enemy();
  %$self = (%$self,
    x => $ScreenWidth,
    y => $Game->Rand($ScreenHeight - 16),
    w => $size,
    h => $size,
    size => $size,
    speed => 2.5,
    score => 3000,
    speedX => 0,
    speedY => 0,
    acceleration => $self->{difficulty} / 200 + 0.04,   #  0.04 .. 0.09 .. 0.14
    maxspeed => $self->{difficulty} * 0.15 + 2.5,  # 2.5 .. 4 ..5.5
    lifetime => 1300 * &::GetDifficultyMultiplier() + $Game->Rand(300),
  );
  $self->{maxspeed2} = $self->{maxspeed} * $self->{maxspeed};
  bless $self, $class;
  push @Koules, $self;
  return $self;
}

sub Delete {
  my ($self) = @_;
  
  $self->SUPER::Delete();
  ::RemoveFromList @Koules, $self;
}

sub EnemyAdvance {
  my ($self) = @_;
  my ($x, $y, $closestGuy, $closestGuyDistance, $guy, $guyDistance, $targetX, $targetY, $other, $found);
  
  $closestGuy = $self->FindClosestGuy();
  unless ($self->{isboss}) {
    if (--$self->{lifetime} < 0) {
      new EnemyPop(enemy => $self);
      return $self->Delete();
    }
  }
  if ($closestGuy) {
    $targetX = $closestGuy->{x} - ($self->{w} - $closestGuy->{w}) / 2;
    $targetY = $closestGuy->{y} - ($self->{h} - $closestGuy->{h}) / 2;
  } else {
    $targetX = ($ScreenWidth - $self->{w}) / 2;
    $targetY = ($ScreenHeight - $self->{h}) / 2;
  }
  $self->ApproachCoordinates($targetX, $targetY);
  $self->CapSpeed($self->{maxspeed2});
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  if ($self->{collisions}) {
    delete $self->{collisions}  unless --$self->{collisions};
  } else {
    $self->CheckGuyCollisions();
    foreach $other (@Koules) {
      if ($found) {
        next  if $other->{collisions};
        if ($self->Collisions($other)) {
          $self->OnCollisionsWithOther($other);
        }
      } elsif ($other eq $self) {
        $found  = 1;
      }
    }
  }
}

sub OnCollisionsWithOther {
  my ($self, $other) = @_;
  my ($dx, $dy, $dlen);
  
  print STDERR "OnCollisionsWithOther $self $other\n";
  $dx = $self->{x} - $other->{x};
  $dy = $self->{y} - $other->{y};
  $dx = 1  unless $dx || $dy;
  $dlen = sqrt($dx * $dx + $dy * $dy);
  $dx /= $dlen;
  $dy /= $dlen;
  $self->{x} += $dx * $self->{w} / 2;
  $self->{y} += $dy * $self->{h} / 2;
  $other->{x} -= $dx * $self->{w} / 2;
  $other->{y} -= $dy * $self->{h} / 2;
  new Kickback(guy => $self, speedX => $dx/2, speedY => $dy/2, power => $self->{size});
  new Kickback(guy => $other, speedX => -$dx/2, speedY => -$dy/2, power => $self->{size});
  $self->{collisions} = $other->{collisions} = 3;
}

sub OnGuyCollisions {
  my ($self, $guy) = @_;
  my ($dx, $dy, $dlen);
  
  print STDERR "OnGuyCollisions $self $guy\n";
  $dx = $self->{x} + $self->{w}/2 - ($guy->{x} + $guy->{w} / 2);
  $dy = $self->{y} + $self->{h}/2 - ($guy->{y} + $guy->{h} / 2);
  $dx = 1  unless $dx || $dy;
  $dlen = sqrt($dx * $dx + $dy * $dy);
  $dx /= $dlen;
  $dy /= $dlen;
  new Kickback(guy => $self, speedX => $dx/2, speedY => $dy/2, power => 50);
  new Kickback(guy => $guy, speedX => -$dx/2, speedY => -$dy/2, power => $self->{size});
  $guy->{rocking} = 50;
  $Game->PlaySound('playercollision');
  $self->{collisions} = 5;
}

sub Draw {
  my ($self) = @_;
  my ($a, $a2);
  
  $a = $Cos[$Game->{anim} * 23 % 800] * $self->{w} / 8;
  $a2 = $a * 2;
  $Sprites{koules}->Blit($self->{x}+$a, $self->{y}-$a, $self->{w}-$a2, $self->{h}+$a2);
}

sub CheckHitByFireShield {}


##########################################################################
package Bomber;
##########################################################################

@Bomber::ISA = qw(CompoundCollision Boss);
$Bomber::LevelName = ::T('Area Boss: Bomber');
$Bomber::LevelDifficulty = 5;

=comment
Bomber attacks:
* Floating + Sweeping laser
* Falling + laser
* Releasing bombs
* Releasing seeker bots
=cut

sub new {
  my $class = shift;
  
  my $self = new Boss(@_);
  %$self = ( %$self,
    score => 100000,
    hitpoint => int(7 * &::GetDifficultyMultiplier()),
    acceleration => 0.05,
    fireDelay => 200 / &::GetDifficultyMultiplier(),
    fireInterval => 200,
    x => $ScreenWidth + 50,
    y => 100,
    w => 256 * 1,
    h => 50 * 1.5,
    collisionPieces => [{ collisionmarginw1 => 15, collisionmarginh1 => 34*1.5, collisionmarginw2 => 108, collisionmarginh2 => 56*1.5 },
                        { collisionmarginw1 => 107, collisionmarginh1 => 22*1.5, collisionmarginw2 => 172, collisionmarginh2 => 48*1.5 },
                        { collisionmarginw1 => 171, collisionmarginh1 =>  6*1.5, collisionmarginw2 => 256, collisionmarginh2 => 36*1.5 } ],
    tilt => 0,
    anim => 0,
    movement => 'right',
    delay => 0,
    bombDelay => 1000,
    target => { x => $ScreenWidth + 50, y => 100},
  );
  bless $self, $class;
  return $self;
}

sub EnemyAdvance {
  my ($self) = @_;
  
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  $self->{speedY} -= 0.01;
  ++$self->{anim};
  if ($self->{dies}) {
    $self->{speedY} -= 0.05;
    $self->{tilt} -= 1;
    if ($self->{y} <= -$self->{h}) {
      $self->Delete();
    }
    new BossExplosion($self)  if $Game->{anim} % 5 == 0;
    return;
  }

  $self->FireLasers();
  $self->UpdateTarget();
  $self->ChaseTarget();
  $self->CheckGuyCollisions();
}

sub UpdateTarget {
  my ($self) = @_;
  my ($movement);

  $movement = $self->{movement};
  
  if ($movement eq 'left') {
    $self->{target}->{y} = 100 + $Sin[(3 * $self->{anim}) % 800] * 30;
    if ( !$self->{spawnedSeekers} && $self->{x} < 600 ) {
      $self->{spawnedSeekers} = 1;
      $self->SpawnSeekerBots();
    }
    if ( ($self->{target}->{x} -= 3) < 20) {
      $self->{movement} = 'waitLeft';
      $self->{delay} = 200;
    }
  } elsif ($movement eq 'waitLeft') {
    $self->{target}->{y} = 100 + $Sin[(3 * $self->{anim}) % 800] * 30;
    if (--$self->{delay} <= 0) {
      $self->{movement} = 'patrolLeft';
      $self->{acceleration} = 0.07;
      $self->{patrolBomb} = 1;
    }
  } elsif ($movement eq 'patrolLeft') {
    $self->{target}->{y} = 300 - $Cos[(2.5 * $self->{delay}) % 800] * 200;
    if ($self->{y} > 450 && $self->{patrolBomb}) {
      $self->PatrolBomb();
    }
    $self->{patrolBomb} = 1  if $self->{y} < 300;
    if (++$self->{delay} >= 2000 / 2.5) {
      $self->{movement} = 'right';
      $self->{acceleration} = 0.05;
      $self->{target}->{x} = $ScreenWidth + 200;
      $self->{target}->{y} = 400;
      $self->{delay} = 300;
      $self->{bombDelay} = $Game->Rand(20) + 10;
      $self->{numBombs} = $DifficultySetting + 1;
      foreach (@{$self->{seekerBots}}) {
        $_->Escape()  unless $_->{deleted};
      }
      delete $self->{seekerBots};
    }
  } elsif ($movement eq 'right') {
    $self->ThrowBombs();
    if (--$self->{delay} < 0) {
      $self->{movement} = 'left';
      $self->{spawnedSeekers} = 0;
      $self->{y} = $Game->Rand(400) + 200;
    }
  }
}

sub FireLasers {
  my ($self) = @_;
  my ($fireDelay, $numShots, $interval);

  return  if $self->{x} > 500 && $self->{fireDelay} > 50;
  $fireDelay = --$self->{fireDelay};
  return  if $fireDelay > 50;
  if ($fireDelay < 0) {
    $fireDelay = $self->{fireInterval};
    $self->{fireDelay} = $Game->Rand($fireDelay / 4 ) + $fireDelay * 3 / 4;
    return;
  }
  $numShots = $DifficultySetting + 2;  # 3, 4, 5, 6
  $interval = int(50 / $numShots);
  if ($fireDelay % $interval == 0) {
    new LaserShot(x => $self->{x} + $self->{w} - 32, y => $self->{y}, dir => $fireDelay * 2 + 140);
  }
}

sub PatrolBomb {
  my ($self) = @_;
  
  foreach (@Guy::Guys) {
    if ($_->{x} < 200 and $_->{y} < 200) {
      new FallingBomb(x => $self->{x} + 150, y => $self->{y} + 28 * 1.5, speedX => -0.2, isboss => 1, range => 300);
      new FallingBomb(x => $self->{x} + 150, y => $self->{y} + 28 * 1.5, speedX => 0.2, isboss => 1, range => 300);
      $self->{patrolBomb} = 0;
      return;
    }
  }
}

sub ThrowBombs {
  my ($self) = @_;
  my ($bombDelay);
  
  $bombDelay = --$self->{bombDelay};
  if ($bombDelay <= 0) {
    if ($self->{x} > 600) {
      return $self->{bombDelay} = 1000;
    }
    new FallingBomb(x => $self->{x} + 150, y => $self->{y} + 28 * 1.5, speedX => $self->{speedX}/2, isboss => 1, range => 300);
    if (--$self->{numBombs} <= 0) {
      return $self->{bombDelay} = 1000;
    }
    $self->{bombDelay} = $Game->Rand(20) + 50 / $self->{numBombs};
  }
}

sub SpawnSeekerBots {
  my ($self) = @_;
  my ($numSeekers, $angle, $bot);
  
  $numSeekers = int(5 * &::GetDifficultyMultiplier()) - 2; # 1, 2, 3, 4
  $angle = 800 / $numSeekers + $Game->Rand(800);
  foreach (1 .. $numSeekers) {
    $bot = new SeekerBot( $Difficulty,
      x => $self->{x} + 150,
      y => $self->{y} + 50*1.5,
      speedX => $self->{speedX} + $Sin[$angle % 800] * 3,
      speedY => $self->{speedY} + $Cos[$angle % 800] * 3,
      isboss => 1,
    );
    push @{$self->{seekerBots}}, $bot;
    $angle += 800 / $numSeekers;
  }
}

sub ChaseTarget {
  my ($self) = @_;
  my ($tilt, $targetX, $targetY);
  
  $tilt = $self->{tilt};
  $targetX = $self->{target}->{x};
  $targetY = $self->{target}->{y};
  if ($self->{x} + $self->{speedX} * abs($self->{speedX}) / $self->{acceleration} / 2 < $targetX) {
    $self->{speedX} += $self->{acceleration};
  } else {
    $self->{speedX} -= $self->{acceleration};
  }
  if ($self->{y} + $self->{speedY} * abs($self->{speedY}) / $self->{acceleration} / 2 < $targetY) {
    $self->{speedY} += $self->{acceleration};
    $tilt += 3  if $tilt <= 100;
    $tilt += 2  if $tilt < 0;
  } else {
    $tilt -= 3  if $tilt >= -100;
    $tilt -= 2  if $tilt > 0;
    $self->{speedY} -= $self->{acceleration};
  }
  $self->{tilt} = $tilt;
}

sub OnDamaged {
  my ($self, $damage, $projectile) = @_;
  
  $self->SUPER::OnDamaged($damage, $projectile);
  $self->{fireDelay} = 100;
  if ($self->{hitpoint} >= 1) {
    foreach (1..15) { new Debris($self); }
  }
  if ($self->{hitpoint} == 3) {
    $self->{fire} = [ [146-94, 34*1.5-32] ];
  } elsif ($self->{hitpoint} == 2) {
    $self->{fire} = [ [146-94, 34*1.5-32], [238-94, 10*1.5-32] ];
  } elsif ($self->{hitpoint} == 1) {
    $self->{fire} = [ [146-94, 34*1.5-32], [238-94, 10*1.5-32], [217-94, 26*1.5-32] ];
  }
}

sub OnKilled {
  my ($self) = @_;
  
  $self->SUPER::OnKilled();
  delete $self->{seekerBots};
}

sub CaughtInExplosion {
  my ($self, $projectile) = @_;
  
  return if $projectile->isa('Bomb');
  $self->SUPER::CaughtInExplosion();
}

sub Draw {
  my ($self) = @_;
  my ($tilt);
  
  $tilt = POSIX::floor(($self->{tilt} + 90) * (6.0 / 180.0) + 0.5);   # Map -90 .. 90  to 0 .. 6
  $tilt = 0  if $tilt < 0;
  $tilt = 6  if $tilt > 6;
  $::Sprites{bomber}->[$tilt]->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h});
  $self->DrawFires($self->{fire})  if $self->{fire};
  $self->DrawLaserWarning()  if $self->{fireDelay} < 100;
}

sub DrawLaserWarning {
  my ($self) = @_;
  my ($size, $alpha);
  
  $size = abs($self->{fireDelay} + 15) * 1.5;  # 170 .. 30
  $alpha = (170 - $size) / 170;
  return  if $alpha < 0;
  &::glColor(0.0, 1, 0.0, $alpha);
  $Sprites{swirl}->RotoBlit($self->{x} + $self->{w} - $size, $self->{y} - $size, $size*2, $size*2, $Game->{anim} * 4);
  &::glColor(1, 1, 1, 1);
}


##########################################################################
package LaserShot;
##########################################################################

@LaserShot::ISA = qw(Enemy);

# params: x, y, dir, speed
sub new {
  my ($class, %params) = @_;
  my ($self, $dir, $speed);
  
  $dir = $params{dir} || 0;
  $speed = $params{speed} || 6 + $Difficulty / 4;  # 6 .. 8.5 .. 11
  $self = new Enemy($Difficulty,
    speedX => $Sin[$dir % 800] * $speed,
    speedY => $Cos[$dir % 800] * $speed,
    w => 64,
    h => 32,
    collisionw => 24,
    collisionh => 24,
    isboss => 1,
    %params,
    speed => $speed,
    dir => 270 - $dir * 360 / 800,
    score => 0,
  );
  bless $self, $class;
  $self->SetupCollisions();
  $Game->PlaySound('laser');
  return $self;
}

sub EnemyAdvance {
  my ($self) = @_;
  my ($x, $y);
  
  $x = $self->{x} += $self->{speedX};
  $y = $self->{y} += $self->{speedY};
  return $self->Delete()  if $x < -64 || $x > $ScreenWidth || $y < -64 || $y > $ScreenHeight;
  $self->CheckGuyCollisions();
}

sub Draw {
  my ($self) = @_;
  
  ::glColor(0,1,0);
  $Sprites{laser}->RotoBlit($self->{x}, $self->{y}, $self->{w}, $self->{h}, $self->{dir});
  ::glColor(1,1,1);
}

sub OnHitByProjectile {
  my ($self) = @_;
  $self->Delete();
}

sub CaughtInExplosion {
}


##########################################################################
package PapaKoules;
##########################################################################

@PapaKoules::ISA = qw(CircleCollision Boss);
$PapaKoules::LevelName = ::T('Area Boss: Papa Koules');
$PapaKoules::LevelDifficulty = 4;

sub new {
  my $class = shift;
  my ($self, $size); 

  $self = new Boss(@_);
  $size = 256;
  
  %{$self} = ( %{$self},
    size => $size,
    x => $ScreenWidth + $size,
    y => $ScreenHeight,
    w => $size,
    h => $size,
    collisionw => $size,
    collisionh => $size,
    speedX => 0,
    speedY => 0,
    score => 100000,
    animAmplitude => 0,
    animPhase => 0,
    hitpoint => 5,
    maxKoules => 6 * &::GetDifficultyMultiplier(),
    koulesSpawned => 0,
    gravity => 0.045 * &::GetDifficultyMultiplier(),
    pause => 0,
  );
  bless $self, $class;
  $self->SetupCollisions();
  $self->Drop('first');
  $self;
}

sub OnKoulesKilled {
  my ($self, $koules) = @_;
  --$self->{koulesSpawned};
}

sub Drop {
  my ($self, $first) = @_;
  if ($first) {
    $self->{x} = $ScreenWidth;
  } else {
    $self->{x} = $Game->Rand($ScreenWidth - $self->{w});
  }
  $self->{speedX} = 3 * &::GetDifficultyMultiplier() * ($self->{x} + $self->{w} / 2 > $ScreenWidth / 2 ? -1 : 1);
  $self->{speedY} = -0.5;
  $self->{y} = $ScreenHeight + 30;
  $self->{animAmplitude} = 0;
  $self->{pause} = 100 / &::GetDifficultyMultiplier()  if $self->{spawnedChild};
  $self->{spawnedChild} = 0;
}

sub Bounce {
  my ($self) = @_;
  
  if ($self->{x} < $ScreenWidth && $self->{x} > -$self->{w} && $self->{koulesSpawned} < $self->{maxKoules}) {
    $self->{spawnedChild} = 1;
    my $koulesSize = $Game->Rand(32) + 32;
    my $koules = new Koules($koulesSize);
    $koules->{x} = $self->{x} + ($self->{w} - $koulesSize) / 2;
    $koules->{y} = 0;
    $koules->SetOnDeleted(\&OnKoulesKilled, $self);
    ++$self->{koulesSpawned};
    $Game->PlaySound('bossspit');
  }
  $self->{y} = 0;
  $self->{speedY} = sqrt( $ScreenHeight * $self->{gravity} * 2.1 );
  $self->{animPhase} = 0;
  $self->{animAmplitude} = 30;
}

sub EnemyAdvance {
  my ($self) = @_;
  
  if ($self->{pause} > 0) {
    return --$self->{pause};
  }
  $self->{animPhase} += 23;
  $self->{animAmplitude} -= 0.15  if $self->{animAmplitude} > 1;
  
  $self->{speedY} -= $self->{gravity};
  $self->{y} += $self->{speedY};
  $self->{x} += $self->{speedX};
  if ($self->{dies}) {
    return $self->Delete  if $self->{y} < -$self->{h};
    new BossExplosion($self)  if $Game->{anim} % 5 == 0;
    $self->{rotate} += 4;
    return;
  }
  if ($self->{y} < 0) {
    $self->Bounce();
  } elsif ($self->{y} > $ScreenHeight && $self->{speedY} > 0 || $self->{x} < -$self->{w} * 2 || $self->{x} > $ScreenWidth + $self->{w}) {
    $self->Drop();
  }
  $self->CheckGuyCollisions();
  while ($Ball::Balls < 8) {
    my $ball = new Ball;
    $ball->{score} = 0;
  }
}

sub OnDamaged {
  my ($self, $damage, $projectile) = @_;
  
  $self->SUPER::OnDamaged($damage, $projectile);
  if ($projectile) {
    $self->{speedY} = 3  if $self->{speedY} < 3;
  }
  $self->{animAmplitude} = 35;
}

sub Draw {
  my ($self) = @_;
  my ($a, $a2);
  
  $a = -$Sin[$self->{animPhase} % 800] * $self->{animAmplitude};
  $a2 = $a * 2;
  if (exists $self->{rotate}) {
    $Sprites{papakoules}->RotoBlit($self->{x}+$a, $self->{y}-$a, $self->{w}-$a2, $self->{h}+$a2, $self->{rotate});
  } else {
    $Sprites{papakoules}->Blit($self->{x}+$a, $self->{y}-$a, $self->{w}-$a2, $self->{h}+$a2);
  }
}

sub OnKilled {
  my ($self) = @_;
  $self->SUPER::OnKilled();
  $self->{speedX} /= 4;
}


1;
