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

use strict;
use Carp;

our (@Cos, $Difficulty, $DifficultySetting, %Events, $Game, %GameEvents, @GameObjects, %Keys, $Level, $NumGuys, $ScreenHeight, $ScreenWidth, @Sin, %Sprites, %Textures);

##########################################################################
# GAME OBJECT CLASSES
##########################################################################

package GameObject;
package InvisibleGameObject;
package TextGameObject;
package GameLevelIndicator;
package BonusText;
package Guy;
package Slurp;
package Kickback;
package SpitParticles;
package InvincibilityBubble;
package PoppedInvincibilityBubble;
package FireShield;
package FireShieldPiece;
package PlayerDeathEffect;
package Bump;
package SlurpingEffect;
package EatenEffect;
package Enemy;
package CompoundCollision;
package BonusBox;
package BonusItem;
package TimeEffect;
package EnemyExplosion;
package EnemyPop;
package NullExplosion;
package MegaExplosion;
package Ball;
package SuperBall;
package Comet;
package CometHit;
package Bee;
package PangGuy;
package Harpoon;
package Witch;
package Fireball;
package Dougar;
package CloudKill;
package ReturningCloudKill;
package Bat;
package FireBat;
package BouncyMoon;
package FlameElemental;
package FlameThrower;
package GhostSwarm;
package Ghost;
package Spider;
package Web;
package IceDart;
package IceParticles;
package IceCube;
package SeekerBot;
package Bullseye;


package main;

sub DumpObjects {
  print STDERR &GetObjectDump();
}

sub GetObjectDump {
  my ($o, $result);
  foreach $o (@GameObjects) {
    $result .=  "$o: " . ref($o) . "\n";
    foreach (sort keys %$o) {
      $result .= "\t$_\t$o->{$_}\n";
    }
  }
  return $result;
}



=comment

== Using enemies as projectiles ==

Certain enemies (e.g. Balls, Bees and Dougars) can be used to kill other 
enemies (e.g. Witches) when the player spits them. I will explain how
this works, because it is more complex than other GameObject interactions:

* When the Slurp object advances in spitting mode (SpittingAdvance),
  it calls the slurpedObject->UseAsProjectile method.

* UseAsProjectile does nothing, unless the enemy can be used as a projectile.

* If the enemy can be used as a projectile, UseAsProjectile calls
  CheckHitByProjectile for every GameObject.

* CheckHitByProjectile can do many things:
  * Nothing, if the GameObject does not interact with projectiles (e.g. Balls)
  * Slurps will "steal" the projectile when they are not slurping something else.
  * Guys will be killed by projectiles (players can kill each other this way.)
  * Enemies that can be killed by projecties will
    * Call their own OnHitByProjectile (which will kill the Enemy)
    * Call the projectile->UseAsProjectileExplode (which will kill the projectile)

=cut



##########################################################################
package GameObject;
##########################################################################

sub new {
  my $class = shift;
  my $self = {
    'speedX' => 0,
    'speedY' => 0,
    'x' => -5000,
    'y' => -5000,
    'w' => 10,
    'h' => 10,
    @_,
  };
  bless $self, $class;
  push @GameObjects, $self;
  return $self;
}

sub Delete {
  my $self = shift;
  my ($i);

  for ($i = 0; $i < scalar @GameObjects; ++$i) {
    if ($GameObjects[$i] eq $self) {
      splice @GameObjects, $i, 1;
      last;
    }
  }
  $self->{deleted} = 1;
  if ($self->{ondeleted}) {
    my ($sub, @ondeleted) = @{$self->{ondeleted}};
    $sub->(@ondeleted, $self);
  }
}

=comment
SetOnDeleted takes a sub ref and a number of optional additional parameters.
The ref will be called when this GameObject is deleted with the given parameters.
A reference to this game object will be appended to the parameter list.
=cut

sub SetOnDeleted {
  my $self = shift;
  $self->{ondeleted} = \@_;
}

sub GetName {
  my $self = shift;
  my $result = ref($self);
  $result =~ s/([a-z])([A-Z])/$1 $2/;
  return $result;
}

sub Advance {
  my $self = shift;
  
  $self->{advance}->($self) if $self->{advance};
}

sub Draw {
  my ($self) = @_;

  if ($self->{draw}) {
    $self->{draw}->($self);
    return;
  }
  ::glDisable(::GL_TEXTURE_2D);
  ::glColor( 1, 0, 0);
  ::glBegin(::GL_QUADS);
    ::glVertex($self->{x}, $self->{y});
    ::glVertex($self->{x}, $self->{y} + $self->{h});
    ::glVertex($self->{x} + $self->{w}, $self->{y} + $self->{h});
    ::glVertex($self->{x} + $self->{w}, $self->{y});
  ::glEnd();
  ::glColor( 1, 1, 1);
  ::glEnable(::GL_TEXTURE_2D);
}

sub SetupCollisions {
  my ($self) = @_;
  
  $self->{collisionw} = ($self->{collisionw} or $self->{w});
  $self->{collisionh} = ($self->{collisionh} or $self->{h});
  $self->{collisionmarginw1} = ( $self->{w} - $self->{collisionw} ) / 2;
  $self->{collisionmarginw2} = $self->{collisionmarginw1} + $self->{collisionw};
  $self->{collisionmarginh1} = ( $self->{h} - $self->{collisionh} ) / 2;
  $self->{collisionmarginh2} = $self->{collisionmarginh1} + $self->{collisionh};
  $self->{centerx} = $self->{w} / 2;
  $self->{centery} = $self->{h} / 2;
}

sub SetWidth {
  my ($self, $width) = @_;
  $self->{w} = $width;
}

sub Collisions {
  my ($self, $other) = @_;
  
  # Bounding box detection
  if (defined $self->{collisionmarginw1} and defined $other->{collisionmarginw1}) {
    return 0 if $self->{x} + $self->{collisionmarginw1} >= $other->{x} + $other->{collisionmarginw2};
    return 0 if $other->{x} + $other->{collisionmarginw1} >= $self->{x} + $self->{collisionmarginw2};
    return 0 if $self->{y} + $self->{collisionmarginh1} >= $other->{y} + $other->{collisionmarginh2};
    return 0 if $other->{y} + $other->{collisionmarginh1} >= $self->{y} + $self->{collisionmarginh2};
    return 1;
  }
  if (defined $self->{collisionmarginw1}) {
    return 0 if $self->{x} + $self->{collisionmarginw1} >= $other->{x} + $other->{w};
    return 0 if $other->{x} >= $self->{x} + $self->{collisionmarginw2};
    return 0 if $self->{y} + $self->{collisionmarginh1} >= $other->{y} + $other->{h};
    return 0 if $other->{y} >= $self->{y} + $self->{collisionmarginh2};
    return 1;
  }
  if (defined $other->{collisionmarginw1}) {
    return 0 if $self->{x} >= $other->{x} + $other->{collisionmarginw2};
    return 0 if $other->{x} + $other->{collisionmarginw1} >= $self->{x} + $self->{w};
    return 0 if $self->{y} >= $other->{y} + $other->{collisionmarginh2};
    return 0 if $other->{y} + $other->{collisionmarginh1} >= $self->{y} + $self->{h};
    return 1;
  }
  
  return 0 if $self->{x} >= $other->{x} + $other->{w};
  return 0 if $other->{x} >= $self->{x} + $self->{w};
  return 0 if $self->{y} >= $other->{y} + $other->{h};
  return 0 if $other->{y} >= $self->{y} + $self->{h};
  return 1;
}

sub EnforceBounds {
  my $self = shift;
  
  if ($self->{x} < 0) {
    $self->{x} = 0;
  }
  if ($self->{x} > $ScreenWidth - $self->{w}) {
    $self->{x} = $ScreenWidth - $self->{w};
  }
  if ($self->{y} < 0) {
    $self->{y} = 0;
  }
  if ($self->{y} > $ScreenHeight - $self->{h}) {
    $self->{y} = $ScreenHeight - $self->{h};
  }
}

sub CheckHitByProjectile {
  return 0;
}

sub CheckHitByFireShield {
  my $self = shift;
  return $self->CheckHitByProjectile(@_);
}

sub CaughtInExplosion {}

sub BlownByWind {}

sub ApproachCoordinates {
  my ($self, $x, $y) = @_;

  if ($self->{x} + $self->{speedX} * abs($self->{speedX}/$self->{acceleration}) / 2 < $x) {
    $self->{speedX} += $self->{acceleration};
  } else {
    $self->{speedX} -= $self->{acceleration};
  }
  if ($self->{y} + $self->{speedY} * abs($self->{speedY}/$self->{acceleration}) / 2 < $y) {
    $self->{speedY} += $self->{acceleration};
  } else {
    $self->{speedY} -= $self->{acceleration};
  }
}

sub CapSpeed {
  my ($self, $maxSpeed2) = @_;
  my ($speed2, $speedMultiplier);
  
  $speed2 = $self->{speedX} * $self->{speedX} + $self->{speedY} * $self->{speedY};
  return  unless $speed2 > $maxSpeed2;
  $speedMultiplier = $maxSpeed2 / $speed2;
  $self->{speedX} *= $speedMultiplier;
  $self->{speedY} *= $speedMultiplier;
}


##########################################################################
package NoCollisions;
##########################################################################

=comment
Inherit from NoCollisions if a GameObject doesnt interact with others.
See also: InvisibleGameObject
=cut

sub Draw {}
sub Collisions { return 0; }


##########################################################################
package InvisibleGameObject;
##########################################################################

=comment
Inherit from InvisibleGameObject when creating a GameObject that has no physical body.
=cut

sub Draw {}
sub Collisions { return 0; }


##########################################################################
package TextGameObject;
##########################################################################

@TextGameObject::ISA = qw(NoCollisions GameObject);

# Params: text font x y
sub new {
  my $class = shift;
  my ($self);

  $self = new GameObject(@_);
  bless $self, $class;
}

sub Center {
  my $self = shift;
  
  $self->{x} = int($ScreenWidth - $self->{font}->TextWidth($self->{text})) / 2;
}

sub Draw {
  my $self = shift;

  $self->{font}->pre_output();
  $self->{font}->output($self->{x}, $self->{y}, $self->{text});
  $self->{font}->post_output();
}


##########################################################################
package GameLevelIndicator;
##########################################################################

@GameLevelIndicator::ISA = qw(NoCollisions GameObject);

# params: level bonus bonusText force
sub new {
  my ($class, %params) = @_;
  my ($self, $width, $height, $time, $bonusText, $bw);

  return  if ref($Game) eq 'SilentPlaybackGame' && !$params{force};  # Nothing displayed in silent playback mode
  $self = new GameObject(%params);
  $width = $::GlossyFont->TextWidth($params{level});
  $height = $::GlossyFont->char_height;
  $time = 300;
  if ($params{bonus}) {
    $bonusText = ::Ts("Time bonus: %s", $params{bonus});
    $bw = $::ScoreFont->TextWidth($bonusText);
  } elsif ($params{bonusText}) {
    $bonusText = $params{bonusText};
    $bw = $::ScoreFont->TextWidth($bonusText);
    $time += 200;
  } else {
    $bonusText = $bw = 0;
  }

  %{$self} = ( %{$self},
    'x' => ($ScreenWidth - $width) / 2,
    'y' => ($ScreenHeight - $height) / 2,
    'bx' => ($ScreenWidth - $bw) / 2,
    'w' => $width,
    'h' => $height,
    'anim' => 0,
    'bonus' => $bonusText,
    'time' => $time,
  );
  bless $self, $class;
}

sub Advance {
  my $self = shift;
  $self->Delete()  if (++$self->{anim} >= $self->{time});
}

sub Draw {
  my $self = shift;
  
  my $alpha = 100;
  $alpha = $self->{anim}  if $self->{anim} < 100;
  $alpha = $self->{time} - $self->{anim}  if $self->{anim} > $self->{time} - 100;
  $::GlossyFont->pre_output;
  $::GlossyFont->output( $self->{x}, $self->{y}, $self->{level}, [1,1,1], $alpha/100 );
  $::ScoreFont->output( $self->{bx}, $self->{y} - $self->{h}, $self->{bonus}, [1,1,1], $alpha/100 )  if $self->{bonus};
  $::GlossyFont->post_output;
  ::glColor(1,1,1,1);
}


##########################################################################
package BonusText;
##########################################################################

@BonusText::ISA = qw(NoCollisions GameObject);
use vars qw(%Texts $Texture);

#params: enemy, text
sub new {
  my ($class, %params) = @_;
  my ($self, $enemy, $text);

  $enemy = delete $params{enemy};
  $text = delete $params{text};
  $self = new GameObject(%params);
  %{$self} = ( %{$self},
    'x' => $enemy->{x} + $enemy->{w} / 2,
    'y' => $enemy->{y} + $enemy->{h},
    'anim' => 0,
  );
  ($self->{text1}, $self->{text2}) = $text =~ /([^|]+)\|?(.*)/;
  die "BonusText: unregistered text $self->{text1}"  unless $Texts{$self->{text1}};
  die "BonusText: unregistered text $self->{text2}"  unless $Texts{$self->{text2}};
  bless $self, $class;
}

sub Advance {
  my $self = shift;
  
  $self->Delete()  if (++$self->{anim} >= 120);
}

sub Draw {
  my $self = shift;
  my ($scale1, $scale2);
  
  $scale1 = $self->{anim} / 40;
  $scale1 = 1 if $scale1 > 1;
  $scale1 = 1 - ($self->{anim}-90) / 30  if $self->{anim} > 90;
  $scale1 *= 0.75;
  ::glLoadIdentity();
  ::glTranslate($self->{x}, $self->{y}, 0);
  ::glScale($scale1, $scale1, 1);
  &_DrawText($self->{text1});
  ::glLoadIdentity();

  return  unless $self->{text2};
  $scale2 = ($self->{anim} - 25) / 40;
  return  if $scale2 <= 0;
  $scale2 = 1 if $scale2 > 1;
  $scale2 *= 0.75;
  $scale2 = $scale1  if $self->{anim} > 90;
  ::glLoadIdentity();
  ::glTranslate($self->{x}, $self->{y} - 40, 0);
  ::glScale($scale2, $scale2, 1);
  &_DrawText($self->{text2});
  ::glLoadIdentity();
}

sub _DrawText {
  my ($text) = @_;
  my ($textureX, $textureW, $textureY, $textureH);

  $textureX = $Texts{$text}->{textureX};
  $textureW = $Texts{$text}->{textureW};
  $textureY = $Texts{$text}->{textureY};
  $textureH = $Texts{$text}->{textureH};
  $Texture->Blit(-$textureW / 2, -$textureH/2, $textureW, $textureH, $textureX, $textureY, $textureW, $textureH);
}

sub RegisterBonusText {
  my ($text) = @_;
  
  die "BonusText::RegisterBonusText: Texture already created"  if $Texture;
  while ($text =~ /([^|]+)/g) {
    $Texts{$1} = 1;
  }
}

sub CreateBonusTexture {
  my ($text, $textWidth, $x, $y, $totalWidth, $totalHeight, $textureWidth, $textureHeight, $surface);
  die "BonusText::CreateBonusText: Texture already created"  if $Texture;
  
  $totalWidth = $x = $y = 0;
  $totalHeight = 64;
  foreach $text (keys %Texts) {
    $textWidth = $::GlossyFont->TextWidth($text);
    if ($x + $textWidth > 1024) {
      $totalHeight += 64;
      $y += 64;
      $x = 0;
    }
    $Texts{$text} = { 
      textureX => $x,
      textureY => $y,
      textureW => $textWidth,
      textureH => 64,
    };
    $x += $textWidth + 10;
    $totalWidth = $x  if $totalWidth < $x;
  }
  $textureWidth = 64;
  while ($textureWidth < $totalWidth) { $textureWidth <<= 1; }
  $textureHeight = 64;
  while ($textureHeight < $totalHeight) { $textureHeight <<= 1; }
  print STDERR "CreateBonusTexture: Total width: $totalWidth; Texture width: $textureWidth;\n";
  
  $surface = new SDL::Surface( -w => $textureWidth, -h => $textureHeight, -d => 32, -flags=>::SDL_SWSURFACE(), -name => '' );
  foreach $text (keys %Texts) {
    $::GlossyFont->RenderToSurface($Texts{$text}->{textureX}, $Texts{$text}->{textureY}, $text, $surface);
  }
  $Texture = new Texture('bonustext');
  $Texture->LoadFromSurface($surface);
}


##########################################################################
package Guy;
##########################################################################

@Guy::ISA = qw(GameObject);
use vars qw(@Guys);

#params: player
sub new {
  my ($class, %params) = @_;
  my ($self, $number, $player);

  $player = $params{player};
  $self = new GameObject(%params);
  $number = $player->{number};

  %{$self} = ( %{$self},
    'number' => $number,
    'x' => 100 * $number,
    'y' => 128,
    'w' => 96,
    'h' => 64,
    'collisionw' => 60,
    'collisionh' => 30,
    'speedY' => 0,
    'speedX' => 0,
    'speed' => 3 + (2 - $DifficultySetting) / 2.5,
    'tilt' => 0,
    'dir' => 1,
    'state' => 'idle',
    'killed' => 0,
    'invincible' => 0,
    'color' => $player->{color},
  );
  bless $self, $class;
  $self->SetupCollisions();
  push @Guys, $self;
  $player->{guy} = $self;
  return $self;
}

sub Delete {
  my $self = shift;
  
  return  if $self->{deleted};
  $self->{slurp}->Delete()  if $self->{slurp};
  $self->{invincibilityBubble}->Delete()  if $self->{invincibilityBubble};
  $self->SUPER::Delete;
  ::RemoveFromList @Guys, $self;
  $self->{player}->{guy} = undef;
  $self->{slurp} = undef;
  $self->{invincibilityBubble} = undef;
  $self->{fireshield}->Delete()  if $self->{fireshield};
}

sub ChooseRandomGuy {
  return undef  unless @Guys;
  return $Guys[0]  if 1 == scalar(@Guys);
  my $rand = $Game->Rand(scalar @Guys);
  return $Guys[$rand];
}

sub Spawn {
  my $self = shift;

warn "Guy::Spawn $self $self->{number} @Guys";
  $self->{x} = ($ScreenWidth / $NumGuys) * $self->{number} + 20;
  $self->{y} = -$self->{h};
  $self->{speedY} = 3;
  $self->{speedX} = 0;
  $self->{dir} = 1;
  $self->{state} = 'spawning';
  $self->{slurp} = new Slurp($self);
  $self->{invincible} = 300;
  $self->GiveInvincibility()  if $DifficultySetting == 0 && !($self->{invincibilityBubble});
}

sub Leave {
  my ($self) = @_;
  
  ::RemoveFromList @Guys, $self;
  &LeavingGuy::InitLeavingGuy($self);
warn "Guy::Leave $self $self->{number} @Guys";
}

sub GiveFireShield {
  my ($self) = @_;
  
  if ($self->{fireshield}) {
    $self->{fireshield}->Recharge();
  } else {
    new FireShield(guy => $self);
  }
}

sub GiveExtraSpeed {
  my ($self) = @_;
  
  $self->{speed} = 4.5;
  $self->{xQueue} = [ ($self->{x}) x 30 ];
  $self->{yQueue} = [ ($self->{y}) x 30 ];
}

sub Advance {
  my ($self) = @_;
  
  if ($self->{speed} > 4) {
    pop @{$self->{xQueue}};
    unshift @{$self->{xQueue}}, $self->{x};
    pop @{$self->{yQueue}};
    unshift @{$self->{yQueue}}, $self->{y};
  }
  
  if ($self->{state} eq 'spawning') {
    $self->{slurp}->{w} = -100;
    $self->{speedY} = 4;
    $self->{tilt} = -100;
    $self->{state} = 'idle'  if $self->{y} > 0;
  } else {
    $self->HandleFireKey();
    $self->HandleDirectionKeys();
    $self->EnforceBounds();
  }
  --$self->{invincible} if $self->{invincible} > 0;
  
  if ($self->{rocking}) {
    $self->HandleRocking();
  } else {
    $self->{rocking} = 50  if $Level->{timeLeft} < 0;
  }
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};

  unless ($self->{collision}) {
    foreach (@Guys) {
      next if $_ eq $self;
      next if $_->{collision};
      if ($_->Collisions($self)) {
        $self->OnCollisionWithOtherGuy($_);
      }
    }
  }
}

sub HandleDirectionKeys {
  my $self = shift;
  my $keys = $self->{player}->{keys};
  my $rocking = $self->{rocking} ? 0.75 : 1;
  my $speed = $self->{speed};
  
  if ($self->{collision}) {
    --$self->{collision};
    $keys = [0,0,0,0,0];
  }
  
  if ( $Keys{$keys->[3]} ) {
    $self->{speedY} = -$speed * $rocking;
    $self->{tilt} += 4;  $self->{tilt} = 100 if $self->{tilt} > 100;
  } elsif ( $Keys{$keys->[2]} ) {
    $self->{speedY} = $speed * $rocking;
    $self->{tilt} -= 4;  $self->{tilt} = -100 if $self->{tilt} < -100;
  } else {
    $self->{speedY} -= 0.125 if $self->{speedY} > 0;
    $self->{speedY} += 0.125 if $self->{speedY} < 0;
    $self->{tilt} = - $self->{speedY} * 33;
    $self->{y} += $Sin[(9 * $Game->{anim} + 128 * $self->{number}) % 800] / 2;  # /
  }
  if ( $Keys{$keys->[0]} ) {
    $self->{speedX} = -$speed * $rocking;
  } elsif ( $Keys{$keys->[1]} ) {
    $self->{speedX} = $speed * $rocking;
  } else {
    $self->{speedX} -= 0.125 if $self->{speedX} > 0;
    $self->{speedX} += 0.125 if $self->{speedX} < 0;
  }
}

sub HandleFireKey {
  my ($self) = @_;
  my ($fireKey);
  
  $fireKey = $self->{player}->{keys}->[4];
  if ($self->{fireKeyPressed}) {
    return  if $Keys{$fireKey};
    $self->{fireKeyPressed} = 0;
    $self->{slurp}->Spit()  if $self->{slurp}->{state} eq 'slurping';
    return;
  }
  if ($Events{$fireKey}) {
    if ($self->{slurp}->{state} eq 'active') {
      $self->{dir} = $self->{dir} ? 0 : 1;
    } else {
      $self->{fireKeyPressed} = 1;
    }
  }
}

sub HandleRocking {
  my ($self) = @_;
  my ($r);
  
  $r = --$self->{rocking};
  new Bump($self)  unless $r % 5;
  $r = $r % 12;
  if ($r == 0) { $self->{x} += 4; }
  elsif ($r == 3)  { $self->{y} -= 4; }
  elsif ($r == 6)  { $self->{x} -= 4; }
  elsif ($r == 9)  { $self->{y} += 4; }
}

sub OnCollisionWithOtherGuy {
  my ($self, $other) = @_;
  my ($deltax, $deltay, $length);
  
  $deltax = $self->{x} - $other->{x};
  $deltay = $self->{y} - $other->{y};
  $length = sqrt($deltax*$deltax + $deltay*$deltay);
  $deltax = $length = 1  unless $length;
  
  $deltax /= $length / 3;
  $deltay /= $length / 3;
  $self->{speedX} = $deltax;
  $self->{speedY} = $deltay;
  $self->{collision} = 10;
  $self->{rocking} = 50;
  $other->{speedX} = -$deltax;
  $other->{speedY} = -$deltay;
  $other->{collision} = 10;
  $other->{rocking} = 50;
  
  new Bump($self);
  new Bump($self);
  new Bump($other);
  new Bump($other);
  $Game->PlaySound('playercollision');
}

sub Draw {
  my ($self) = @_;
  my ($texture, @guy, @wing);

  $self->DrawPoisonSwirl()  if $self->{slurp}->{poison};
  return  if $self->{invincible} and not($self->{invincible} / 5 % 3);
  $texture = $Textures{'player'};
  @guy = @wing = ($self->{x}, $self->{y} + 4, $self->{w}, $self->{h});

  if ($self->{tilt} < -90) {
    push @guy, 48, 64 + 32 * (int($Game->{anim} / 10) % 2);
    push @wing, 192, 64;
    $wing[1] += 3 * 2;
  } elsif ($self->{tilt} < -60) {
    push @guy, 48, 32;
    push @wing, 192, 32;
    $wing[1] += 2 * 2;
  } elsif ($self->{tilt} < -30) {
    push @guy, 48, 0;
    push @wing, 192, 0;
    $wing[1] += 2 * 2;
  } elsif ($self->{icecube}) {    # Use for {icecube}
    push @guy, 96, 32;
    push @wing, 96, 64;
  } elsif ($self->{tilt} <  30) {
    push @guy, 96, 32 * (int($Game->{anim} / 10) % 2);
    push @wing, 96, 64;
  } elsif ($self->{tilt} <  60) {
    push @guy, 0, 0;
    push @wing, 144, 0;
    $wing[1] += 1 * 2;
  } elsif ($self->{tilt} <  90) {
    push @guy, 0, 32;
    push @wing, 144, 32;
    $wing[1] += 2 * 2;
  } else {
    push @guy, 0, 64 + 32 * ( int($Game->{anim} / 10) % 2);
    push @wing, 144, 64;
    $wing[1] += 3 * 2;
  }
  push @guy, 48, 32;
  push @wing, 48, 32;
  
  unless ($self->{dir}) {
    $guy[6] = -$guy[6];
    $wing[6] = -$wing[6];
  }
  
  if ($self->{speed} > 4) {
    my $x = $self->{x};
    my $y = $self->{y};
    my @guy2 = @guy;
    my @wing2 = @wing;
    foreach (8, 16, 24) {
      $guy2[0] = $guy[0] - $x + $self->{xQueue}->[$_];
      $guy2[1] = $guy[1] - $y + $self->{yQueue}->[$_];
      $wing2[0] = $wing[0] - $x + $self->{xQueue}->[$_];
      $wing2[1] = $wing[1] - $y + $self->{yQueue}->[$_];
      ::glColor(1, 1, 1, (32 - $_) / 32);
      $texture->Blit(@guy2);
      ::glColor(@{$self->{color}}[0..2], (32 - $_) / 32);
      $texture->Blit(@wing2);
    }
    ::glColor(1,1,1,1);
  }

  ::glColor(1,0.5,0.5,1)  if $self->{slurp}->{poison};
  $texture->Blit(@guy);
  unless ($self->{speed} > 4 and not $Game->{anim} / 5 % 5) {
    ::glColor(@{$self->{color}});
  }
  $texture->Blit(@wing);
  ::glColor(1,1,1,1);
}

sub DrawPoisonSwirl {
  my ($self) = @_;
  my ($size, $alpha);
  
  $size = abs($self->{slurp}->{w} + 50) * 2;
  $alpha = (230 - $size) / 150;
  return  if $alpha < 0;
  &::glColor(1, 0, 0, $alpha);
  $Sprites{swirl}->RotoBlit($self->{x} + $self->{w} / 2 - $size, $self->{y} + $self->{h} / 2 - $size, $size*2, $size*2, $Game->{anim} * 4);
  &::glColor(1, 1, 1, 1);
}

sub Poisoned {
  my ($self) = @_;
  
  $self->Die('force');
}

sub Die {
  my ($self, $force) = @_;
  my ($i, $x0, $y0, $xr, $yr, $xv, $yv);
  
  return  if $self->{deleted};  # already killed
  if ($force) {
    $self->LoseInvincibility();
    $self->{invincible} = 0;    # Invincibility doesn't protect against poison
  } else {
    return  if $self->{invincible};
    return $self->LoseInvincibility()  if $self->{invincibilityBubble};
  }
  
  $x0 = $self->{x} + $self->{w} / 2;
  $y0 = $self->{y} + $self->{h} / 2;
  new PlayerDeathEffect($self->{color}, $x0, $y0, $i);
  $GameEvents{playerkilled}->{$self->{number}} = 1;
  $self->Delete();
  $Game->PlaySound('killed');
}

sub CheckHitByProjectile {
  my ($self, $projectile, $otherGuy) = @_;
  
  if ($projectile->Collisions($self)) {
    $self->Die();
    $projectile->UseAsProjectileExplode();
    return 1;
  }
  return 0;
}

sub CheckHitByFireShield {}

sub BlownByWind {
  my ($self, $speedX, $speedY) = @_;
  
  $self->{x} += $speedX;
  $self->{y} += $speedY;
  $self->{slurp}->OnGuyBlownByWind();
}

sub GiveInvincibility {
  my ($self) = @_;
  
  return $self->GiveFireShield()  if $self->{invincibilityBubble};
  $self->{invincibilityBubble} = new InvincibilityBubble($self);
}

sub LoseInvincibility {
  my ($self) = @_;
  
  my $bubble = $self->{invincibilityBubble};
  return  unless $bubble;
  $bubble->Pop();
  $self->{invincibilityBubble} = undef;
  $self->{invincible} = 200;
  $self->{icecube}->Melt()  if $self->{icecube};
}


##########################################################################
package LeavingGuy;
##########################################################################

@LeavingGuy::ISA = qw(NoCollisions Guy);

sub InitLeavingGuy {
  my ($self) = @_;
  
  bless $self;
  $self->{fakeplayer} = { keys => [ qw(l_no l_no l_no l_yes l_no) ] };
  $self->{slurp}->Delete();
  $self->{slurp} = undef;
}

sub Spawn {
  my ($self) = @_;
  
  bless $self, 'Guy';
  push @Guy::Guys, $self;
  $self->Spawn();
}

sub Advance {
  my ($self) = @_;
  my ($player);
  
  return  if $self->{y} < -500;
  $Keys{l_yes} = 1;
  
  $player = $self->{player};
  $self->{player} = $self->{fakeplayer};
  $self->SUPER::Advance;
  $self->{player} = $player;
}

sub EnforceBounds {}

sub Draw {
  &Guy::Draw(@_);
}

sub OnCollisionWithOtherGuy {}


##########################################################################
package Slurp;
##########################################################################

@Slurp::ISA = qw(NoCollisions GameObject);
use vars qw(@Slurps);

sub new {
  my ($class, $guy) = @_;
  my ($self);
  
  $self = new GameObject;

  %{$self} = ( %{$self},
    'guy' => $guy,
    'slurpedObject' => undef,
    'poison' => 0,
    'x' => $guy->{x},
    'y' => $guy->{y},
    'w' => 96,
    'h' => 32,
    #'collisionw' => 80,  Do not specify -- {w} is subject to change
    'baseWidth' => 96,
    'power' => 2,
    'spitPower' => 5,
    'spitRange' => 300,
    'dir' => $guy->{dir},
    'state' => 'active',
  );
  bless $self, $class;
  push @Slurps, $self;
  # $self->SetupCollisions();  Do not use -- base on w and h
  return $self;
}

sub Delete {
  my $self = shift;

  $self->SUPER::Delete();
  ::RemoveFromList @Slurps, $self;
  $self->RestoreSlurpedObject()  if $self->{slurpedObject};
  # This line causes problems for Perl 5.6 under windows:
  # delete $self->{guy};  # Help the garbage collector out
}

sub GiveLargeWidth {
  my ($self) = @_;
  
  $self->{baseWidth} = 144;
  $self->{h} = 48
}

sub GiveExtraSlurpPower {
  my ($self) = @_;
  
  $self->{power} = 4;
}

sub GiveExtraSpitPower {
  my ($self) = @_;
  
  $self->{spitPower} = 10;
  $self->{spitRange} = 500;
}

sub Advance {
  my ($self) = @_;
  my ($guy);
  
  $guy = $self->{guy};
  $self->{x} = $guy->{x} + ($guy->{dir} ? $guy->{w} : -$self->{w});
  $self->{y} = $guy->{y} + 38 - $self->{h} / 2;
  $self->{dir} = $guy->{dir};
  
  if ($self->{state} eq 'slurping') {
    $self->SlurpingAdvance();
  } elsif ($self->{state} eq 'spitting') {
    $self->SpittingAdvance();
  } else {
    $self->{w} += 3  if $self->{w} < $self->{baseWidth};
  }
}

sub SlurpingAdvance {
  my ($self) = @_;
  my ($slurpedObject, $slurpX, $slurpY, $dir, $power);
  
  $slurpedObject = $self->{slurpedObject};
  $slurpX = $self->{slurpedObjectX};
  $slurpY = $self->{slurpedObjectY};
  $dir = $self->{dir};
  $slurpedObject->{x} = $dir ? $self->{x} + $slurpX : $self->{guy}->{x} + $slurpX;
  $slurpedObject->{y} = $self->{y} + $slurpY;
  $power = $self->{power};
  $power = 1  if $self->{poison};
  $power = 0.75  if $self->{guy}->{fireKeyPressed};
  
  if (1 == $dir) {
    $slurpX -= $power;
    return $self->Eat()  if $slurpX <= - ($self->{guy}->{w} + $slurpedObject->{w}) / 2;
    $self->{w} = $slurpX + $slurpedObject->{w};
    if ($slurpedObject->{w} > 16) {
      $slurpX += $power / 2;
      $slurpedObject->{w} -= $power / 2;
    }
  } else {
    $slurpX += $power;
    return $self->Eat()  if $slurpX > ($self->{guy}->{w} - $slurpedObject->{w}) / 2;
    $self->{w} = -$slurpX;
    if ($slurpedObject->{w} > 16) {
      $slurpedObject->{w} -= $power / 2;
    }
  }
  
  --$slurpedObject->{h} if $slurpedObject->{h} > 16; 
  if ($slurpY > ($self->{h} - $slurpedObject->{h}) / 2) {
    $slurpY -= 1;
  } else {
    $slurpY += 1;
  }
  $self->{slurpedObjectX} = $slurpX;
  $self->{slurpedObjectY} = $slurpY;
}

sub SpittingAdvance {
  my ($self) = @_;
  my ($slurpedObject, $x);
  
  $slurpedObject = $self->{slurpedObject};
  &::ConfessWithDump()  unless $slurpedObject;
  if ($slurpedObject->can('FreeFall')) {
    $slurpedObject->FreeFall();
  } else {
    if ($self->{dir}) {
      $slurpedObject->{x} += $self->{spitPower};
    } else {
      $slurpedObject->{x} -= $self->{spitPower};
    }
  }
  $slurpedObject->SetWidth($slurpedObject->{w}+1)  if $slurpedObject->{w} < $self->{slurpedObjectW};
  ++$slurpedObject->{h}  if $slurpedObject->{h} < $self->{slurpedObjectH};
  $slurpedObject->UseAsProjectile($self->{guy});
  return  unless $self->{slurpedObject};
  $self->{w} = -1;
  $self->{spitDistance} += $self->{spitPower};
  if ($self->{spitDistance} > $self->{spitRange} || $slurpedObject->{deleted}) {
    return $self->RestoreSlurpedObject();
  }
  $x = $slurpedObject->{x};
  $slurpedObject->EnforceBounds();
  if ($x != $slurpedObject->{x} or $slurpedObject->{x} < -$slurpedObject->{w} or $slurpedObject->{x} > $ScreenWidth) {
    return $self->RestoreSlurpedObject();
  }
}

sub RestoreSlurpedObject {
  my ($self) = @_;
  
  my $slurpedObject = $self->{slurpedObject};
  $self->{slurpedObject} = undef;
  $self->{state} = 'active';
  $self->{poison} = 0;
  $slurpedObject->{w} = $self->{slurpedObjectW};
  $slurpedObject->{h} = $self->{slurpedObjectH};
  $slurpedObject->{speedX} = $self->{slurpedObjectSpeedX};
  $slurpedObject->{speedY} = $self->{slurpedObjectSpeedY};
  $slurpedObject->Spat( ($self->{dir} == 1 ? $self->{spitPower} : -$self->{spitPower}), 0 );
  $slurpedObject->SetupCollisions();
}

sub Draw {
  my ($self) = @_;
  my ($texture, $dir, $tw, $flash);

  return   if $self->{w} <= 0;
  $texture = $Textures{'slurp'};
  $dir = $self->{dir};
  $tw = 128 * $self->{w} / $self->{baseWidth};
  $flash = $self->{power} > 2;
  
  ::glColor(0.5,1,0.2,1)  if $flash;
  $texture->Blit( $self->{x}, $self->{y}, $self->{w}, $self->{h},
    0, 42 * (int($Game->{anim} / 10) % 3), $dir ? $tw : -$tw, 42 );
  ::glColor(1,1,1,1)  if $flash;
}

sub Slurp {
  my ($self, $slurpedObject) = @_;
  
  $Game->PlaySound('slurp');
  $slurpedObject->{slurped} = 1;
  $self->{slurpedObject} = $slurpedObject;
  $self->{slurpedObjectX} = $self->{dir} ? $slurpedObject->{x} - $self->{x} : $slurpedObject->{x} - $self->{guy}->{x};
  $self->{slurpedObjectY} = $slurpedObject->{y} - $self->{y};
  $self->{slurpedObjectW} = $slurpedObject->{w};
  $self->{slurpedObjectH} = $slurpedObject->{h};
  $self->{slurpedObjectSpeedX} = $slurpedObject->{speedX};
  $self->{slurpedObjectSpeedY} = $slurpedObject->{speedY};
  $self->{state} = 'slurping';
  $self->{poison} = $slurpedObject->{poison};
  new SlurpingEffect($slurpedObject);
  if ($self->{poison}) {
    if ($self->{dir}) {
      $self->{slurpedObjectX} = 40  if $self->{slurpedObjectX} < 40
    } else {
      $self->{slurpedObjectX} = -40  if $self->{slurpedObjectX} > -40
    }
  }
}

sub Spit {
  my ($self) = @_;
  
  new SpitParticles(slurp => $self);
  $self->{state} = 'spitting';
  $self->{spitDistance} = 0;
  $self->{poison} = 0;
  $self->{slurpedObject}->{speedX} = $self->{spitPower} * ($self->{dir} ? 1 : -1);
  $Game->PlaySound($self->{spitPower} > 5 ? 'superspit' : 'spit');
  if ($self->{spitPower} > 5) {
    new Kickback( guy => $self->{guy}, speedX => $self->{dir} ? -1 : +1, speedY => 0);
  }
}

sub Eat {
  my ($self) = @_;
  
  $self->{state} = 'active';
  $self->{slurpedObject}->Eaten($self->{guy});
  $self->{slurpedObject} = undef;
  new EatenEffect($self->{guy});
  $Game->PlaySound('gulp');
}

sub CheckHitByProjectile {
  my ($self, $projectile, $otherGuy) = @_;
  
  return 0  if $self->{state} ne 'active';
  if ($self->Collisions($projectile)) {
    $otherGuy->{slurp}->RestoreSlurpedObject();
    return  if $projectile->{deleted};  # e.g. bomb exploded
    $self->Slurp($projectile);
    return 1;
  }
  return 0;
}

sub OnGuyBlownByWind {
  my ($self) = @_;
  
  $self->RestoreSlurpedObject()  if $self->{slurpedObject};
  $self->{w} = 0;
}


##########################################################################
package Kickback;
##########################################################################

@Kickback::ISA = qw(NoCollisions GameObject);

# params: guy, speedX, speedY, power
sub new {
  my ($class, %params) = @_;
  
  my $self = new GameObject(
    power => 30 + 20 * &::GetDifficultyMultiplier(),
    %params,
  );
  bless $self, $class;
}

sub Advance {
  my ($self) = @_;
  my ($power, $guy);
  
  return $self->Delete  if $guy->{deleted};
  $power = --$self->{power};
  $guy = $self->{guy};
  return $self->Delete()  if $power <= 0;
  $guy->{x} += $power / 5 * $self->{speedX};
  $guy->{y} += $power / 5 * $self->{speedY};
}


##########################################################################
package SpitParticles;
##########################################################################

@SpitParticles::ISA = qw(NoCollisions GameObject);

# params: slurp
sub new {
  my ($class, %params) = @_;
  my ($slurp, $guy, $dir, $x, $y, $w, $h, $particles, $self);
  
  $slurp = $params{slurp};
  $guy = $slurp->{guy};
  $dir = $guy->{dir} > 0 ? 1 : -1;
  $x = $guy->{x} + ($guy->{dir} > 0 ? $guy->{w} : 0) - 8;
  $y = $slurp->{y} + $slurp->{h} / 2 - 8;
  $particles = [ map { {x=>$x, y=>$y, speedX => $dir * (rand(3) + 2), speedY => rand(2)-0.5} } (0 .. 3) ];
  $self = new GameObject(
    particles => $particles,
    anim => 50,
  );
  bless $self, $class;
}

sub Advance {
  my ($self) = @_;
  
  $self->{anim} -= 1;
  return $self->Delete()  if $self->{anim} <= 0;
  foreach my $particle (@{$self->{particles}}) {
    $particle->{speedY} -= 0.07;
    $particle->{x} += $particle->{speedX};
    $particle->{y} += $particle->{speedY};
  }
}

sub Draw {
  my ($self) = @_;
  
  ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE);
  ::glColor(0.8, 1, 0.8, $self->{anim}/100);
  foreach my $particle (@{$self->{particles}}) {
    $Textures{particle}->Blit($particle->{x}, $particle->{y}, 16, 16, 0, 0, -1, -1);
  }
  ::glColor(1,1,1,1);
  ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE_MINUS_SRC_ALPHA);
}


##########################################################################
package InvincibilityBubble;
##########################################################################

@InvincibilityBubble::ISA = qw(NoCollisions GameObject);
our (@AnimationPhases);
@AnimationPhases = (0,1,2,3,4,3,2,1,0,1,2,3,4,3,2,1,0,1,5,6,7,8,4,3,2,1);

sub new {
  my ($class, $guy) = @_;
  my ($self);
  
  $self = new GameObject(
    'guy' => $guy,
    'x' => $guy->{x},
    'y' => $guy->{y},
    'w' => 96,
    'h' => 96,
  );
  bless $self, $class;
  return $self;
}

sub Delete {
  my $self = shift;

  $self->SUPER::Delete();
  $self->{guy} = undef;  # Help the garbage collector out
}

sub Pop {
  my $self = shift;
  
  $self->{poppedAnim} = 0;
  bless $self, 'PoppedInvincibilityBubble';
  $Game->PlaySound('harpoonhit');  # TODO find a more suitable sound
}

sub Advance {
  my ($self) = @_;
  my ($guy);
  
  $guy = $self->{guy};
  $self->{x} = $guy->{x};
  $self->{y} = $guy->{y} - 8;
}

sub Draw {
  my ($self) = @_;
  my ($anim, @phases);
  
  $anim = $Game->{anim};
  $Textures{'bonus'}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h},
    $AnimationPhases[$anim / 6 % scalar(@AnimationPhases)] * 48, 0, 48, 48);
}



##########################################################################
package PoppedInvincibilityBubble;
##########################################################################

@PoppedInvincibilityBubble::ISA = qw(NoCollisions InvincibilityBubble);

sub Advance {
  my $self = shift;
  if (++$self->{poppedAnim} >= 24) {
    $self->Delete();
  }
}

sub Draw {
  my $self = shift;
  $Textures{'bonus'}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h},
    48 * int($self->{poppedAnim} / 6), 48, 48, 48);
}



##########################################################################
package FireShield;
##########################################################################

@FireShield::ISA = qw(GameObject);

#params: guy
sub new {
  my ($class, %params) = @_;
  my ($self, $guy);
  
  $guy = $params{guy} or Carp::confess "No guy, no FireShield";
  $self = new GameObject(
    radius => 65,
    %params,
  );
  bless $self, $class;
  $self->Recharge();
  foreach (1..2) {
    $self->{"piece$_"} = new FireShieldPiece(
      fireshield => $self,
    );
  }
  $self->{collisionPieces} = [ $self->{piece1}, $self->{piece2} ];
  $guy->{fireshield} = $self;
  return $self;
}

sub Delete {
  my $self = shift;

  $self->SUPER::Delete();
  $self->{piece1}->Delete();
  $self->{piece2}->Delete();
  delete $self->{guy}->{fireshield};
  $self->{guy} = undef;  # Help the garbage collector out
}

sub Recharge {
  my ($self) = @_;
  
  $self->{delay} += 2000 / &::GetDifficultyMultiplier();  # +20s
}

sub Advance {
  my ($self) = @_;
  my ($guy, $angle, $anim);
  
  return $self->Delete if --$self->{delay} <= 0;
  $guy = $self->{guy};
  $anim = -$self->{delay};
  $angle = -$anim * 12 % 800;
  
  $self->{x} = $guy->{x} + $guy->{centerx} - 16;
  $self->{y} = $guy->{y} + $guy->{centery} - 16;
  $self->{piece1}->{x} = $self->{x} + $Sin[$angle] * $self->{radius};
  $self->{piece1}->{y} = $self->{y} + $Cos[$angle] * $self->{radius};
  $self->{piece1}->{speedX} = $Cos[$angle] * $self->{radius};
  $self->{piece1}->{angle} = 100 - $angle * 360 / 800;
  $self->{piece2}->{x} = $self->{x} - $Sin[$angle] * $self->{radius};
  $self->{piece2}->{y} = $self->{y} - $Cos[$angle] * $self->{radius};
  $self->{piece2}->{speedX} = -$Cos[$angle] * $self->{radius};
  $self->{piece2}->{angle} = 280 - $angle * 360 / 800;
  $self->CheckEnemyCollisions();
}

sub CheckEnemyCollisions {
  my ($self) = @_;
  my ($piece1, $piece2, $guy, @gameObjects, $enemy);
  
  $piece1 = $self->{piece1};
  $piece2 = $self->{piece2};
  $guy = $self->{guy};
  @gameObjects = @GameObjects;
  foreach $enemy (@gameObjects) {
    next  if $enemy->isa('Guy');
    $enemy->CheckHitByFireShield($piece1, $guy);
    $enemy->CheckHitByFireShield($piece2, $guy);
  }
}

sub Draw {
  my ($self) = @_;
  my ($anim, $angle, $alpha, $piece);
  
  $alpha = $self->{delay};
  ::glColor(1,1,1,$alpha/256)  if $alpha < 256;
  foreach $piece ($self->{piece1}, $self->{piece2}) {
    $Sprites{flameshield}->RotoBlit($piece->{x}, $piece->{y}, $piece->{w}, $piece->{h}, $piece->{angle});
  }
  ::glColor(1,1,1,1)  if $alpha < 256;
}


##########################################################################
package FireShieldPiece;
##########################################################################

@FireShieldPiece::ISA = qw(GameObject);

# params: fireshield
sub new {
  my ($class, %params) = @_;
  my $self = new GameObject(
    score => 0,
    w => 28,
    h => 28,
    collisionw => 40,
    collisionh => 40,
    angle => 0,
    %params,
  );
  $self->{guy} = $self->{fireshield}->{guy};
  bless $self, $class;
  $self->SetupCollisions();
  return $self;
}

sub Advance {}
sub Draw {}
sub UseAsProjectileExplode {}

sub CheckHitByProjectile {
  my ($self, $projectile, $guy) = @_;
  
  return 0  if $guy eq $self->{guy};
  return 0  unless $projectile->Collisions($self);
  $projectile->UseAsProjectileExplode();
  return 1;
}


##########################################################################
package PlayerDeathEffect;
##########################################################################

@PlayerDeathEffect::ISA = qw(GameObject);

sub new {
  my ($class, $color, $x0, $y0) = @_;
  my ($self, $size);
  
  $size = 32;
  $self = new GameObject;
  %{$self} = ( %{$self},
    'x0' => $x0 - $size/2,
    'y0' => $y0 - $size/2,
    'length' => 10,
    'speed' => 3,
    'w' => $size,
    'h' => $size,
    'color' => $color,
    'dir' => 0,
  );
  bless $self, $class;
  return $self;
}

sub Advance {
  my $self = shift;
  
  if (($self->{length} += $self->{speed}) > 800) {
    return $self->Delete();
  }
  $self->{speed} += 0.1;
  $self->{dir} += $self->{speed};
  while ($self->{dir} > 40) {
    $self->{dir} -= 40;
  }
}

sub Draw {
  my ($self) = @_;
  my ($i, $x, $y);
  
  ::glColor(@{$self->{color}});
  for ($i = $self->{dir}; $i < 800; $i += 40) {
    $x = $self->{x0} + $Sin[$i] * $self->{length};
    $y = $self->{y0} + $Cos[$i] * $self->{length};
    $Textures{particle}->Blit( $x, $y, 32, 32, 0, 0, -1, -1);
  }
  ::glColor(1,1,1,1);
}


##########################################################################
package Bump;
##########################################################################

@Bump::ISA = qw(GameObject);

sub new {
  my ($class, $guy) = @_;
  my ($self, $dir, $speedX, $speedY);

  $self = new GameObject;
  $dir = $guy->{dir};
  $speedX = $guy->{speedX};
  $speedY = $guy->{speedY};
  %{$self} = ( %{$self},
    'x' => $guy->{x} + $guy->{w} - rand($guy->{w} / 2),
    'y' => $guy->{y} + rand($guy->{h} - 16),
    'w' => 16,
    'h' => 16,
    'speedX' => -2,
    'speedY' => 0, # $speedY,
    'type' => int(rand(2)),
    'ttl' => 0,
  );
  bless $self, $class;
  return $self;
}

sub Advance {
  my $self = shift;
  
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  if (++$self->{ttl} >= 40) {
    $self->Delete();
  }
}

sub Draw {
  my $self = shift;
  
  $Textures{player}->Blit( $self->{x}, $self->{y}, $self->{w}, $self->{h},
    96 + int($self->{ttl} / 8) * 16, 96 + $self->{type} * 16, 16, 16);
}


##########################################################################
package SlurpingEffect;
##########################################################################

@SlurpingEffect::ISA = qw(GameObject);

sub new {
  my ($class, $enemy) = @_;
  my ($self, $width, $height);

  $self = new GameObject;
  $width = 39;
  $height = 39;
  %{$self} = ( %{$self},
    enemy => $enemy,
    x => $enemy->{x} + ($enemy->{w} - $width) / 2,
    y => $enemy->{y} + ($enemy->{h} - $height) / 2,
    w => $width,
    h => $height,
    anim => 0,
  );
  bless $self, $class;
  return $self;
}

sub Advance {
  my $self = shift;
  
  if (++$self->{anim} >= 4*6) {
    $self->Delete();
  }
}

sub Draw {
  my $self = shift;
  my ($enemy);
  
  $enemy = $self->{enemy}  or return;
  ::glColor(1,0,0,1)  if $enemy->{poison};
  $Sprites{slurped}->[$self->{anim}/6]->Blit( $self->{x}, $self->{y},  $self->{w}, $self->{h} );
  ::glColor(1,1,1,1)  if $enemy->{poison};
}


##########################################################################
package EatenEffect;
##########################################################################

@EatenEffect::ISA = qw(GameObject);

sub new {
  my ($class, $guy) = @_;
  my ($self, $width, $height);

  $self = new GameObject;
  $width = 128 * 1.2;
  $height = 80 * 1.2;
  %{$self} = ( %{$self},
    guy => $guy,
    w => $width,
    h => $height,
    anim => 0,
  );
  bless $self, $class;
  return $self;
}

sub Advance {
  my $self = shift;
  
  if (++$self->{anim} >= 7*6) {
    $self->Delete();
  }
}

sub Draw {
  my $self = shift;
  my ($guy);
  
  $guy = $self->{guy} or return;
  $Sprites{eaten}->[$self->{anim}/6]->Blit( 
    $guy->{x} + ($guy->{w} - $self->{w}) / 2,
    $guy->{y} + ($guy->{h} - $self->{h}) / 2,
    $self->{w}, $self->{h}
  );
}


##########################################################################
package Enemy;
##########################################################################

=comment
* Difficulty parameter for each enemy (0-20), makes them faster or meaner the closer they get to 20.
  0 is very tame, 10 is hard. Values over 10 are not meant to be fair (punishment for overtime).
=cut

@Enemy::ISA = qw(GameObject);

sub new {
  my $class = shift;
  my $difficulty = shift;
  
  my $self = new GameObject(
    difficulty => $difficulty || $Difficulty || 5,
    score => 0,
    @_
  );
  return $self;
}

sub Advance {
  my $self = shift;
  
  return  if $self->{slurped};
  $self->EnemyAdvance()  unless $Game->{enemyStop};
}

sub FindClosestGuy {
  my ($self) = @_;
  my ($x, $y, $closestGuyDistance, $guy, $guyDistance, $closestGuy);

  return $Guy::Guys[0]  if scalar(@Guy::Guys) == 1;
  return undef  unless @Guy::Guys;
  $x = $self->{x};
  $y = $self->{y};
  $closestGuyDistance = 5000*5000;
  foreach $guy (@Guy::Guys) {
    $guyDistance = ($guy->{x}-$x) * ($guy->{x}-$x) + ($guy->{y}-$y) * ($guy->{y}-$y);
    if ($guyDistance < $closestGuyDistance) {
      $closestGuyDistance = $guyDistance;
      $closestGuy = $guy;
    }
  }
  return $closestGuy;
}

sub CheckSlurpCollisions {
  my $self = shift;
  my ($slurp);

  foreach $slurp (@Slurp::Slurps) {
    next  unless $slurp->{state} eq 'active' and $slurp->{w} > 10;
    if ($self->Collisions($slurp)) {
      $slurp->Slurp($self);
      return;
    }
  }
}

sub CheckGuyCollisions {
  my $self = shift;
  
  foreach my $guy (@Guy::Guys) {
    if ($self->Collisions($guy)) {
      $self->OnGuyCollisions($guy);
    }
  }
}

sub OnGuyCollisions {
  my ($self, $guy) = @_;
  
  $guy->Die();
}

sub Spat {
  my ($self, $recommendedSpeedX, $recommendedSpeedY) = @_;
  
  $self->{slurped} = 0;
  $self->{speedX} = $recommendedSpeedX;
  $self->{speedY} = $recommendedSpeedY;
}

sub Eaten {
  my ($self, $guy) = @_;
  
  if ($self->{poison}) {
    $guy->Poisoned();
  } else {
    $guy->{player}->GiveScore($self->{score});
  }
  if (ref($self) eq 'Ball') {
    ++$guy->{player}->{ballsVacuumed};
  } else {
    ++$guy->{player}->{enemiesVacuumed};
  }
  $self->Delete();
}

sub OnPlayerRespawn {
  my $self = shift;
  
  return  if $self->{slurped} || $self->{isboss};
  &EnemyExplosion::Create($self);
  $self->Delete();
}

sub UseAsProjectile {}

sub UseAsProjectileExplode {
  # OK, so I couldn't find a good name for this method.
  # It is invoked when this object is used as a projectile AND the projectile hits.
  my ($self) = @_;
  
  $self->{speedX} = -$self->{speedX};
  &EnemyExplosion::Create($self);
  $self->Delete();
}

sub CheckHitByProjectile {
  my ($self, $projectile, $guy) = @_;
  
  return 0  if $self->{slurped};
  return 0  unless $self->Collisions($projectile);
  
  ++$guy->{player}->{enemiesShot};
  $self->OnHitByProjectile($projectile);
  $projectile->UseAsProjectileExplode();
  $self->AwardScoreForHitByProjectile($projectile, $guy);
  return 1;
}

sub AwardScoreForHitByProjectile {
  my ($self, $projectile, $guy) = @_;
  my ($scoreMultiplicator);
  
  $scoreMultiplicator = 1;
  $scoreMultiplicator *= 2  if $self->{poison};
  $scoreMultiplicator *= 2  if $projectile->{poison};
  $guy->{player}->GiveScore( ($self->{score} + $projectile->{score}) * $scoreMultiplicator );
  if ($scoreMultiplicator > 1) {
    new BonusText(enemy=>$self, text=>$scoreMultiplicator > 2 ? ::T('4x|Score') : ::T('Double|Score'));
  }
  if ($scoreMultiplicator > 2) {
    my $bonusBall = new SuperBall;
    $bonusBall->{x} = $self->{x} + ($self->{w} - $bonusBall->{w}) / 2;
    $bonusBall->{y} = $self->{y} + ($self->{h} - $bonusBall->{h}) / 2;
  }
}

&BonusText::RegisterBonusText(::T('4x|Score'));
&BonusText::RegisterBonusText(::T('Double|Score'));

sub OnHitByProjectile {
  my ($self, $projectile) = @_;
  
  $self->{speedX} = $projectile->{speedX}  if $projectile;
  &EnemyExplosion::Create($self);
  $self->Delete();
}

sub CaughtInExplosion {
  my ($self, $projectile) = @_;
  
  &EnemyExplosion::Create($self);
  $self->Delete();
}

sub BlownByWind {
  my ($self, $speedX, $speedY) = @_;
  
  $self->{x} += $speedX;
  $self->{y} += $speedY;
}

sub DrawFires {
  my ($self, $fires) = @_;

  foreach (@{$self->{fire}}) {
    $Sprites{firetrail}->[$Game->{anim} / 4 % 4]->Blit( $self->{x} + $_->[0], $self->{y} + $_->[1], 96, 32 );
  }
}


##########################################################################
package CompoundCollision;
##########################################################################

# CompoundCollision can be inherited by any enemy.
# If inherited, a number of {collisionPieces} must be defined.
# The {collisionPieces} should have {collisionmarginw1} .. {collisionmarginh2}

sub Collisions {
  my ($self, $other) = @_;
  
  foreach (@{$self->{collisionPieces}}) {
    $_->{x} = $self->{x};
    $_->{y} = $self->{y};
    return 1  if $other->Collisions($_);
  }
  return 0;
}


##########################################################################
package CircleCollision;
##########################################################################

# CircleCollision can be inherited by any enemy.
# If inherited, the collision detection for the enemy will be done as if it was a sphere

sub SetupCollisions {
  my ($self) = @_;
  
  &GameObject::SetupCollisions($self);
  $self->{radiusx} = ($self->{collisionw} or $self->{w}) / 2;
  $self->{radiusy} = ($self->{collisionh} or $self->{h}) / 2;
}

sub Collisions {
  my ($self, $other) = @_;

  # Circle vs rectangle collision

  my ($centerX, $centerY, $boxAxisX, $boxAxisY, $boxCenterX, $boxCenterY, $distSquare, $distance);
  $boxAxisX = ($other->{collisionw} or $other->{w}) / 2;
  $boxAxisY = ($other->{collisionh} or $other->{h}) / 2;
  $boxCenterX = $other->{x} + $other->{w} / 2;
  $boxCenterY = $other->{y} + $other->{h} / 2;
  $centerX = $self->{x} + $self->{radiusx};
  $centerY = $self->{y} + $self->{radiusy};

  # Translate coordinates to the box center
  $centerX = abs( $centerX - $boxCenterX );
  $centerY = abs( $centerY - $boxCenterY );

  if ($centerX < $boxAxisX) {
    return $centerY < $boxAxisY + $self->{radiusy};
  }
  if ($centerY < $boxAxisY) {
    return $centerX < $boxAxisX + $self->{radiusx};
  }
  $distSquare = ($centerX-$boxAxisX) * ($centerX-$boxAxisX);
  $distSquare+= ($centerY-$boxAxisY) * ($centerY-$boxAxisY);
  return $distSquare < $self->{radiusx} * $self->{radiusy};
}


##########################################################################
package BonusBox;
##########################################################################

@BonusBox::ISA = qw(Enemy);

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

  my $self = new Enemy;
  %$self = (%$self,
    anim => 0,
    w => 64,
    h => 40,
    score => 3000,
  );
  bless $self, $class;
}

sub EnemyAdvance {
  my ($self) = @_;
  my ($anim, $x);
  
  $anim = ++$self->{anim};
  $anim *= 0.75;
  $x = 1 - ($anim / 450);
  $x = $x * $x * $x * 400 + 500;
  $self->{x} = $x + $Sin[$anim*3 % 800] * 150;
  $self->{y} = 400 - $Cos[$anim*6 % 800] * 80;
  $self->Delete()  if $self->{x} < -100;
}

sub Draw {
  my ($self) = @_;
  
  $Sprites{wings}->[$self->{anim} / 8 % 4]->Blit($self->{x} -8, $self->{y}+10, -32, 32);
  $Sprites{bonusbox}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h});
  $Sprites{wings}->[$self->{anim} / 8 % 4]->Blit($self->{x}+52, $self->{y}+10, 32, 32);
}

sub OnPlayerRespawn {}

sub OnHitByProjectile {
  my ($self, $projectile) = @_;
  
  $self->CreateBonusItem();
  $self->SUPER::OnHitByProjectile($projectile);
}

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

sub CreateBonusItem {
  my ($self) = @_;
  
  new BonusItem($self);
  $self->{opened} = 1;
}

sub CheckHitByFireShield {}


##########################################################################
package BonusItem;
##########################################################################

@BonusItem::ISA = qw(Enemy);
use vars qw(@Items @BonusTexts);
@Items = qw( spitpower slurppower largeslurp speed life 50000 timeeffect fireshield );
@BonusTexts = ( ::T('Spit|Power Up'), ::T('Slurp|Power Up'), ::T('Slurp|Size Up'),
  ::T('Speed|Up'), ::T('Bonus|Life'), ::T('50000|Points'), ::T('Bullet|Time'),
  ::T('Fire|Shield') );
foreach (@BonusTexts) {
  &BonusText::RegisterBonusText($_);
}

sub new {
  my ($class, $bonusBox) = @_;
  my ($self, $rand);
  
  $rand = $Game->Rand(scalar @Items);
  $self = new Enemy;
  %$self = (%$self,
    anim => 0,
    w => 32,
    h => 32,
    score => 5000,
    x => $bonusBox->{x} + ($bonusBox->{w} - 32) / 2,
    y => $bonusBox->{y} + ($bonusBox->{h} - 32) / 2,
    bonus => $Items[$rand],
    bonusText => $BonusTexts[$rand],
    sprite => $Sprites{bonusitem}->{$Items[$rand]},
  );
  bless $self, $class;
}

sub EnemyAdvance {
  my ($self) = @_;
  my ($anim, $x);
  
  $self->{x} -= 1;
  $self->{y} += $Cos[$Game->{anim} * 5 % 800] * 2;
  return $self->Delete()  if $self->{x} < -100;
  $self->CheckSlurpCollisions();
}

sub Eaten {
  my ($self, $guy) = @_;
  my ($bonus, $sound);
  
  new BonusText(enemy=>$self, text=>$self->{bonusText});
  $sound = 'bonusitem';
  $bonus = $self->{bonus};
  if ('largeslurp' eq $bonus) {
    $guy->{slurp}->GiveLargeWidth();
  } elsif ('slurppower' eq $bonus) {
    $guy->{slurp}->GiveExtraSlurpPower();
  } elsif ('spitpower' eq $bonus) {
    $guy->{slurp}->GiveExtraSpitPower();
  } elsif ('speed' eq $bonus) {
    $guy->GiveExtraSpeed();
  } elsif ('life' eq $bonus) {
    $sound = 'bonuslife';
    ++$guy->{player}->{lives};
  } elsif ('50000' eq $bonus) {
    $guy->{player}->GiveScore(50000);
  } elsif ('fireshield' eq $bonus) {
    $guy->GiveFireShield();
  } elsif ('timeeffect' eq $bonus) {
    new TimeEffect();
  } else {
    $guy->GiveInvincibility();
  }
  $Game->PlaySound($sound);
  $self->SUPER::Eaten($guy);
}

sub Draw {
  my ($self) = @_;
  
  $self->{sprite}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h});
}

sub OnPlayerRespawn {}

sub CheckHitByProjectile {
  return 0;
}

sub CaughtInExplosion {}


# ========================================================================
package TimeEffect;
# ========================================================================

@TimeEffect::ISA = qw(InvisibleGameObject GameObject);

# params: duration, lowPoint
sub new {
  my ($class, %params) = @_;
  my ($self);
  
  $self = new GameObject(
    duration => 2000 / &::GetDifficultyMultiplier(),
    lowPoint => 33.25,
    speed => 100,
    distanceError => 0,
    %params,
  );
  bless $self, $class;
}

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

sub Advance {
  my ($self) = @_;
  my ($speed, $duration, $lowPoint, $nextValue);
  
  $duration = --$self->{duration};
  return $self->Delete()  if $duration < 0;
  $speed = $self->{speed};
  $lowPoint = $self->{lowPoint};
  if (100 - $duration / 4 < $lowPoint) {
    $speed -= 0.25  if $speed > $lowPoint;
  } else {
    $speed = 100 - $duration / 4;
  }
  $self->{speed} = $speed;
  
  $self->{distanceError} += $speed;
  if ($self->{distanceError} > 0) {
    $self->{distanceError} -= 100;
    $Game->SetTimeEffect($self, 0);
  } else {
    $Game->SetTimeEffect($self, 1);
  }
}

sub Expire {
  my ($self) = @_;
  my ($endDuration);
  
  $endDuration = (100 - $self->{lowPoint}) * 4;
  $self->{duration} = $endDuration  if $self->{duration} > $endDuration;
}


##########################################################################
package EnemyExplosion;
##########################################################################

@EnemyExplosion::ISA = qw(GameObject);
our (@TX, @TY, @TW);
@TX = (  0,  32,  80, 128, 176, 240,   0,  64, 144, 224 );
@TY = (128, 128, 128, 128, 128, 128, 176, 176, 176, 176 );
@TW = ( 32,  48,  48,  48,  64,  64,  64,  80,  80,  80 );

sub Create {
  my ($enemy) = @_;
  
  if ($enemy->{explosionClass}) {
    eval "new $enemy->{explosionClass}(enemy => \$enemy)"; die $@ if $@;
  } else {
    new EnemyExplosion(enemy => $enemy);
  }
}

# params: enemy
sub new {
  my ($class, %params) = @_;
  my ($self, $dir, $enemy);
  
  $enemy = $params{enemy} or Carp::confess;
  $dir = $enemy->{speedX} > 0 ? 1 : 0;
  $self = new GameObject(
    anim => 0,
    dir => $dir,
    x => $enemy->{x} + ($dir ? 0 : $enemy->{w}),
    y => $enemy->{y} + $enemy->{h} / 2,
    %params,
  );
  bless $self, $class;
  $self->PlaySound();
  return $self;
}

sub PlaySound {
  $Game->PlaySound('enemydeath');
}

sub Advance {
  my $self = shift;
  if ($self->{dir}) {
    $self->{x} += 0.5;
  } else {
    $self->{x} -= 0.5;
  }
  if (++$self->{anim} >= 60) {
    $self->Delete();
  }
}

sub Draw {
  my $self = shift;
  my ($anim, $tw, $x, $y, $zoom);
  
  $zoom = 1.5;
  $anim = int($self->{anim} / 6);
  $tw = $TW[$anim];
  $y = $self->{y} - 24 * $zoom;
  if ($self->{dir}) {
    $Textures{'enemy'}->Blit( $self->{x}, $y, $tw * $zoom, 48 * $zoom,
      $TX[$anim], $TY[$anim], -$tw, 48 );
  } else {
    $x = $self->{x} - $tw * $zoom;
    $Textures{'enemy'}->Blit( $x, $y, $tw * $zoom, 48 * $zoom,
      $TX[$anim], $TY[$anim], $tw, 48 );
  }
}


##########################################################################
package EnemyPop;
##########################################################################

@EnemyPop::ISA = qw(EnemyExplosion);

# params: enemy
sub new {
  my ($class, %params) = @_;
  
  my $enemy = $params{enemy} or Carp::confess;
  my $self = new EnemyExplosion(
    x => $enemy->{x},
    y => $enemy->{y},
    w => $enemy->{w},
    h => $enemy->{h},
    %params
  );
  bless $self, $class;
}

sub Advance {
  my ($self) = @_;
  
  $self->SUPER::Advance();
  if ($self->{anim} >= 24) {
    $self->Delete();
  }
}

sub Draw {
  my ($self) = @_;
  my ($phase);
  
  $phase = int($self->{anim} / 6);
  $Sprites{pangpop}->[$phase]->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h});
}


##########################################################################
package NullExplosion;
##########################################################################

sub new {}


##########################################################################
package MegaExplosion;
##########################################################################

@MegaExplosion::ISA = qw(GameObject);
our (@TX, @TY, @TW, @TH);
@TX = (432,   0,  80, 144, 224, 320,   0,  96 );
@TY = (  0,  96,  96,  96,  64,  64, 160, 160 );
@TW = ( 48,  80,  64,  80,  96,  96,  96,  96 );
@TH = ( 48,  64,  64,  64,  96,  96,  96,  96 );

sub new {
  my ($class, $enemy) = @_;
  my ($self, $dir);
  
  $self = new GameObject(
    'anim' => 0,
    'x' => $enemy->{x} + $enemy->{w} / 2,
    'y' => $enemy->{y} + $enemy->{h} / 2,
  );
  bless $self, $class;
}

sub Advance {
  my $self = shift;
  if (++$self->{anim} >= 72) {
    $self->Delete();
  }
}

sub Draw {
  my $self = shift;
  my ($anim, $tw, $th, $zoom);
  
  $zoom = 4;
  $anim = int($self->{anim} / 8);
  if ($anim == 0) {
    ::glDisable(::GL_TEXTURE_2D);
    ::glBegin(::GL_QUADS);
      ::glVertex(0,0); ::glVertex($ScreenWidth, 0);
      ::glVertex($ScreenWidth, $ScreenHeight); ::glVertex(0, $ScreenHeight);
    ::glEnd();
    ::glEnable(::GL_TEXTURE_2D);
    return;
  }
  --$anim;
  $tw = $TW[$anim];
  $th = $TH[$anim];
  $Textures{'bonus'}->Blit( $self->{x} - $tw * $zoom / 2, $self->{y} - $th * $zoom / 2, $tw * $zoom, $th * $zoom,
    $TX[$anim], $TY[$anim], $tw, $th );
}


##########################################################################
package SafeBall;
##########################################################################

@SafeBall::ISA = qw(Enemy);

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

  $self = new Enemy(@_);
  %{$self} = ( %{$self},
    'x' => $Game->Rand($ScreenWidth-16),
    'y' => $Game->Rand($ScreenHeight/10) + $ScreenHeight,
    'w' => 16,
    'h' => 16,
    'dir' => $Game->Rand(300) + 250,
    'rotation' => 0,
    'rotation2' => 0,
    'rotationDelay' => 100,
    'speed' => $self->{difficulty} * 0.30 + 1.8,
    'state' => 'entering',
    'poison' => 0,
    'score' => 1000,
  );
  $self->SetupCollisions();
  bless $self, $class;
  return $self;
}

sub Leave {
  my $self = shift;
  $self->{state} = 'leaving';
}

sub GetName {
  return 'Food';
}

sub EnemyAdvance {
  my $self = shift;
  
  return $self->LeavingAdvance()  if $self->{state} eq 'leaving';
  $self->BallMovement();
  $self->EnforceBounds();
  $self->CheckSlurpCollisions();
}

sub BallMovement {
  my ($self) = @_;
  
  if (--$self->{rotationDelay} < 0) {
    $self->ChangeRotation();
  }
  $self->{dir} += $self->{rotation} + $self->{rotation2};
  $self->{speedX} = $self->{speed} * $Sin[$self->{dir} % 800];
  $self->{speedY} = $self->{speed} * $Cos[$self->{dir} % 800];
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
}

sub ChangeRotation {
  my ($self) = @_;

  $self->{rotation} = ($Game->Rand(200) - 100) * $self->{difficulty} / 250;
  $self->{rotationDelay} = int(300 / (abs($self->{rotation})+1) + $Game->Rand(1500 / ($self->{difficulty}+2)));
}

sub EnforceBounds {
  my $self = shift;
  
  if ($self->{x} < 0) {
    $self->{x} = 0;
    $self->{dir} = -$self->{dir};
    $self->{rotationDelay} -= 100;
  } elsif ($self->{x} + $self->{w} > $ScreenWidth) {
    $self->{x} = $ScreenWidth - $self->{w};
    $self->{dir} = -$self->{dir};
    $self->{rotationDelay} -= 100;
  }
  if ($self->{y} < 0) {
    $self->{y} = 0;
    $self->{dir} = 400 - $self->{dir};
    $self->{rotationDelay} -= 100;
  } elsif ($self->{y} + $self->{h} > $ScreenHeight) {
    if ($self->{state} eq 'entering') {
      $self->{y} -= 1;
    } else {
      $self->{y} = $ScreenHeight - $self->{h};
      $self->{dir} = 400 - $self->{dir}  if $self->{speedY} > 0;
      $self->{rotationDelay} -= 100;
    }
  } else {
    $self->{state} = '';
  }
}

sub LeavingAdvance {
  my $self = shift;
  
  $self->{speedY} -= 0.05;
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  if ($self->{x} < 0) {
    $self->{x} = 0;
    $self->{speedX} = -$self->{speedX};
  } elsif ($self->{x} + $self->{w} > $ScreenWidth) {
    $self->{x} = $ScreenWidth - $self->{w};
    $self->{speedX} = -$self->{speedX};
  }
  if ($self->{y} < -$self->{h}) {
    return $self->Delete();
  } elsif ($self->{y} + $self->{h} > $ScreenHeight) {
    $self->{y} = $ScreenHeight - $self->{h};
    $self->{speedY} = -$self->{speedY};
  }
}

sub Draw {
  my ($self) = @_;

  ::glColor(@{$self->{color}});
  $Textures{'ball'}->Blit($self->{x}, $self->{y});
  ::glColor(1,1,1,1);
}

sub OnPlayerRespawn {}

sub UseAsProjectile {
  my ($self, $guy) = @_;
  my (@gameObjects, $gameObject);
  
  @gameObjects = @GameObjects;
  foreach $gameObject (@gameObjects) {
    next  if $self eq $gameObject;
    next  if $guy eq $gameObject;
    return 1  if $gameObject->CheckHitByProjectile($self, $guy);
  }
  return 0;
}

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

sub CheckHitByProjectile {
  return 0;
}

sub CaughtInExplosion {}



##########################################################################
package Ball;
##########################################################################

our ($Balls);
$Balls = 0;

@Ball::ISA = qw(SafeBall);

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

  ++$Balls;
  $self = new SafeBall(@_);
  %$self = (%$self,
    w => 32,
    h => 32,
    ($Game->Rand(2)
      ? ( foodSprite => $Sprites{food}, poisonSprite => $Sprites{poison} ) 
      : ( foodSprite => $Sprites{food2}, poisonSprite => $Sprites{poison2} ) 
    ),
  );
  bless $self, $class;
  return $self;
}

sub Delete {
  my $self = shift;
  
  return  if $self->{deleted};
  --$Balls  unless $self->{state} eq 'leaving';
&::ConfessWithDump("Ball double-deleted at $Game->{anim}:\n----\n$self->{deleteMsg}\n----\n")  if $self->{deleted};
$self->{deleteMsg} = "At time $Game->{anim}: " . Carp::longmess;
  $self->SUPER::Delete();
}

sub Leave {
  my $self = shift;
  
  return  if $self->{state} eq 'leaving' || $self->{deleted};
  --$Balls;
  $self->SUPER::Leave();
}

sub ChangeRotation {
  my ($self) = @_;
  
  $self->SUPER::ChangeRotation();
  $self->{poison} = ($Game->Rand(28) + 2) < $self->{difficulty} ? 1 : 0;
}

sub Draw {
  my ($self) = @_;
  $self->{$self->{poison} ? 'poisonSprite' : 'foodSprite'}->RotoBlit($self->{x}, $self->{y}, $self->{w}, $self->{h}, $Game->{anim} * 3);
}


##########################################################################
package PoisonBall;
##########################################################################

@PoisonBall::ISA = qw(Ball);

sub ChangeRotation {
  my ($self) = @_;
  
  &SafeBall::ChangeRotation($self);
  $self->{poison} = $Game->Rand(28) < 10;
}



##########################################################################
package SuperBall;
##########################################################################

@SuperBall::ISA = qw(SafeBall);

# The superball gives the player invincibility (for one hit only; doesn't protect
# against poison). Using the SuperBall as a projectile creates a large explosion,
# killing every enemy within a radius.

our ($SuperBallColor, $SuperBallFlash);
$SuperBallColor = [0, 1, 0, 1];
$SuperBallFlash = [1, 1, 1, 1];

sub new {
  my $class = shift;
  my ($self);
  
  $self = new SafeBall;
  $self->{score} = 3000,
  bless $self, $class;
  return $self;
}

sub GetName {
  return 'SuperBall';
}

sub Draw {
  my $self = shift;
  
  if ( $Game->{anim} / 5 % 3 ) {
    $self->{color} = $SuperBallColor;
    $self->SUPER::Draw();
  } else {
    ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE);
    $self->{color} = $SuperBallFlash;
    $self->SUPER::Draw();
    ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE_MINUS_SRC_ALPHA);
  }
}

sub Eaten {
  my ($self, $guy) = @_;
  
  $self->SUPER::Eaten($guy);
  $guy->GiveInvincibility();
  $Game->PlaySound('protection');
}

sub UseAsProjectileExplode {
  my ($self) = @_;
  my (@gameObjects, $x, $y, $dx, $dy, $obj);
  
  $self->Delete();
  new MegaExplosion($self);
  $x = $self->{x};
  $y = $self->{y};
  @gameObjects = @GameObjects;
  foreach $obj (@gameObjects) {
    $dx = $obj->{x} + $obj->{w} / 2 - $x;
    $dy = $obj->{y} + $obj->{h} / 2 - $y;
    if ($dx*$dx + $dy*$dy < 400 * 400) {
      $obj->CaughtInExplosion($self);
    }
  }
  #TODO Award score!
}


##########################################################################
package Comet;
##########################################################################

@Comet::ISA = qw(Enemy);

sub new {
  my $class = shift;
  
  my $self = new Enemy(@_);
  %{$self} = ( %{$self},
    'x' => int($Game->Rand($ScreenWidth) - 40) + $ScreenHeight / 7.5,
    'y' => $ScreenHeight,
    'w' => 24,
    'collisionw' => 12,
    'collisionh' => 40,
    'h' => 64,
    'speedX' => -1,
    'speedY' => -2.5 - $self->{difficulty} / 5,
    'acceleration' => -$self->{difficulty} / 800,
    'score' => 0,
    'explosionClass' => 'CometHit',
  );
  bless $self, $class;
  $self->SetupCollisions();
  $self;
}

sub EnemyAdvance {
  my $self = shift;
  
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  $self->{speedY} += $self->{acceleration};
  $self->Delete()  if $self->{y} < -$self->{h};
  $self->CheckGuyCollisions();
}

sub OnGuyCollisions {
  my ($self, $guy) = @_;
  
  $guy->Die();
  new CometHit(enemy => $self);
  $self->Delete();
}

sub OnPlayerRespawn {
  my $self = shift;
  
  return  if $self->{isboss};
  new CometHit(enemy => $self);
  $self->Delete();
}

sub Draw {
  my $self = shift;
  $Textures{enemy}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h},
    ($Game->{anim} / 4) % 4 * 16, 0, 16, 32 );
}

sub CheckHitByProjectile {
  return 0;
}

sub CheckHitByFireShield {
  my $self = shift;
  $self->Enemy::CheckHitByProjectile(@_);
}

sub CaughtInExplosion {
  my $self = shift;
  $self->Delete();
}


##########################################################################
package CometHit;
##########################################################################

@CometHit::ISA = qw(NoCollisions GameObject);

# params: enemy, size
sub new {
  my ($class, %params) = @_;
  my ($enemy, $size);
  
  $enemy = $params{enemy} || &::ConfessWithDump();
  $size = $params{size} || 40;
  my $self = new GameObject(
    x => $enemy->{x} - ($size - $enemy->{w}) / 2,
    y => $enemy->{y} - 8,
    w => $size,
    h => $size,
    anim => 0,
    speedY => 0.5,
    %params,
  );
  $Game->PlaySound('comethit');
  bless $self, $class;
}

sub Advance {
  my $self = shift;
  
  if (++$self->{anim} >= 7*4) {
    return $self->Delete();
  }
  $self->{y} += $self->{speedY};
}

sub Draw {
  my $self = shift;
  
  return  if $self->{anim} <= 0;
  ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE);
  $Textures{enemy}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h},
    int($self->{anim} / 4) * 32 + 224, 32, 32, 32 );
  ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE_MINUS_SRC_ALPHA);
}

##########################################################################
package BossExplosion;
##########################################################################

@BossExplosion::ISA = qw(CometHit);

sub new {
  my ($class, $enemy, $size) = @_;
  
  $size = 60  unless $size;
  my $self = new CometHit(
    enemy => $enemy,
    size => $size,
    x => $enemy->{x} + rand($enemy->{w} - $size),
    y => $enemy->{y} + rand($enemy->{h} - $size),
  );
  bless $self, $class;
}


##########################################################################
package Bee;
##########################################################################

@Bee::ISA = qw(Enemy);

sub new {
  my $class = shift;
  
  my $self = new Enemy(@_);
  %$self = ( %$self,
    'x' => $ScreenWidth + 20,
    'y' => $Game->Rand($ScreenHeight-100) + 50,
    'w' => 64,
    'h' => 32,
    'collisionw' => 48,
    'collisionh' => 32,
    'targetDelay' => 0,
    'poisonDelay' => 0,
    'acceleration' => 0.02 + $self->{difficulty} * 0.005,  # 0.09 .. 0.07 ..  0.12
    'score' => 2000,
    'dir' => -1,
  );
  bless $self, $class;
  $self->{targetX} = $self->{x};
  $self->{targetY} = $self->{y};
  $self->SetupCollisions();
  $self;
}

sub ChooseNewTarget {
  my ($self) = @_;
  
  $self->{targetX} = $Game->Rand($ScreenWidth - $self->{w} - 100) + 50;
  $self->{targetY} = $Game->Rand($ScreenHeight - $self->{h} - 100) + 50;
  $self->{targetX} = ($self->{targetX} + $ScreenWidth) / 2  if $self->{x} >= $ScreenWidth;
  $self->{targetX} /= 2  if $self->{x} <= 0;
  $self->{targetDelay} = $Game->Rand(6000 / ($self->{difficulty}+10)) + 3000 / ($self->{difficulty} + 10);
  $self->{dir} = $self->{x} < $self->{targetX} ? 1 : -1;
}

sub EnemyAdvance {
  my $self = shift;
  
  if ($self->{speedX}*abs($self->{speedX}/$self->{acceleration})/2 + $self->{x} < $self->{targetX}) {
    $self->{speedX} += $self->{acceleration};
  } else {
    $self->{speedX} -= $self->{acceleration};
  }
  if ($self->{speedY}*abs($self->{speedY}/$self->{acceleration})/2 + $self->{y} < $self->{targetY}) {
    $self->{speedY} += $self->{acceleration};
  } else {
    $self->{speedY} -= $self->{acceleration};
  }
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY} + $Sin[(14 * $Game->{anim} + 283 * $self->{targetX}) % 800] / 2;
  $self->ChooseNewTarget()  if --$self->{targetDelay} <= 0;
  $self->CheckGuyCollisions();
  $self->CheckSlurpCollisions();
  if (--$self->{poisonDelay} < 0 and $self->{difficulty} > 2) {
    $self->{poison} = !$self->{poison};
    $self->{poisonDelay} = ($self->{poison} ? $Game->Rand(20 * $self->{difficulty}) : $Game->Rand(500)) + 100;
  }
}

sub EnforceBounds {}

sub OnGuyCollisions {
  my ($self, $guy) = @_;
  
  $guy->Die();
  $self->{speedX} = -$self->{speedX};
  $self->{speedY} = -$self->{speedY};
  $self->ChooseNewTarget();
}

sub Draw {
  my $self = shift;
  
  my $anim = $Game->{anim} + 189 * $self->{targetX};
  ::glColor(1,0,0,1)  if $self->{poison};
  $Textures{enemy}->Blit($self->{x}, $self->{y}+$self->{h}/2, $self->{w}, $self->{h},
    (0,32,64,64,32,0)[$anim / 3 % 6], 32, 32 * $self->{dir}, 16 );
  $Textures{enemy}->Blit($self->{x}, $self->{y}-$self->{h}/2, $self->{w}, $self->{h},
    (0,32,64,32,0,0,0,0,0)[$anim / 15 % 8], 48, 32 * $self->{dir}, 16 );
  ::glColor(1,1,1,1);
}

sub UseAsProjectile {
  my ($self, $guy) = @_;
  
  $self->{collisionmarginw1} = 0;
  $self->{collisionmarginh1} = 0;
  $self->{collisionmarginw2} = $self->{w};
  $self->{collisionmarginh2} = $self->{h};
  &SafeBall::UseAsProjectile($self, $guy);
}



##########################################################################
package PangGuy;
##########################################################################

@PangGuy::ISA = qw(Enemy);


sub new {
  my ($class, $direction, $difficulty) = @_;
  
  my $self = new Enemy($difficulty);
  %$self = ( %$self,
    x => ($direction > 0 ? -64 : $ScreenWidth),
    y => 0,
    w => 64,
    h => 64,
    direction => ($direction > 0 ? 1 : -1),
    collisionw => 32,
    collisionh => 48,
    delay => 0,
    speedY => 0,
    harpoons => int($self->{difficulty} / 3) + 1,
    score => 5000,
  );
  bless $self, $class;
  $self->SetupCollisions();
  $self->SetSpeed();
  $self;
}

sub SetSpeed {
  my $self = shift;
  $self->{speedX} = 2 + $self->{difficulty} / 5;
  $self->{speedX} *= $self->{direction};
}

sub EnforceBounds {}

sub Spat {
  my ($self, $recommendedSpeedX, $recommendedSpeedY) = @_;
  
  $self->SUPER::Spat($recommendedSpeedX, $self->{freeSpeedY});
  $self->{direction} = $recommendedSpeedX > 0 ? 1 : -1;
  $self->SetSpeed();
}

sub UseAsProjectile {
  return &Bee::UseAsProjectile(@_);
}

sub EnemyAdvance {
  my $self = shift;
  
  $self->CheckSlurpCollisions();
  return $self->FreeFall() if $self->{y};
  return $self->FiringAdvance()  if $self->{delay} > 0;
  $self->{x} += $self->{speedX};
  if ($self->{x} > $ScreenWidth or $self->{x} < -$self->{w}) {
    $self->Delete();
  }
  $self->Fire()  if $self->{harpoons} > 0;
}

sub FiringAdvance {
  my $self = shift;
  
  --$self->{delay};
  return  if $self->{delay} > 0;
  if ($self->{firing} ==1) {
    $self->{firing} = 2;
    new Harpoon($self, $self->{difficulty});
    $self->{delay} = 20 + 100 / ($self->{difficulty} + 2);
    --$self->{harpoons};
  } else {
    $self->{delay} = 0;
    $self->{firing} = 0;
  }
}

sub Fire {
  my $self = shift;
  
  foreach my $guy (@Guy::Guys) {
    # my $pys = $guy->{speedY};
    # next if $pys == 4;
    # my $py0 = $guy->{y} + $guy->{h} / 2;
    my $pxt = $guy->{x}; # + 0 * $py0 * $guy->{speedX} / (4 - $pys);
    if (abs($pxt - $self->{x}) < 15) {
      $self->{firing} = 1;
      $self->{delay} = 20 + 100 / ($self->{difficulty} + 2);
    }
  }
}

sub FreeFall {
  my $self = shift;
  
  $self->{x} += $self->{speedX};
  $self->{direction} = $self->{speedX} > 0 ? 1 : -1;
  $self->{freeSpeedY} = $self->{speedY};
  if ($self->{y} < 0) {
    $self->{y} += 3;
    return;
  } elsif ($self->{y} > 0) {
    $self->{speedY} -= 0.1;
    $self->{y} += $self->{speedY};
    if ($self->{y} <= 0) {
      $self->{y} = 0;
      $self->{speedY} = 0;
    }
    return;
  }
}

sub Draw {
  my $self = shift;
  
  if ( $self->{slurped} or $self->{y} ) {
    $Sprites{pangguy_fall}->Blit($self->{x}, $self->{y}, $self->{w} * $self->{direction}, $self->{h});
  } elsif ($self->{firing}) {
    $Sprites{pangguy_fire}->Blit($self->{x} + $self->{w} / 8, $self->{y}, $self->{w} *3/4 * $self->{direction}, $self->{h});
  } else {
    $Sprites{pangguy_walk}->[$Game->{anim} / 10 % 4]->Blit($self->{x}, $self->{y}, $self->{w} * $self->{direction}, $self->{h},);
  }
}


##########################################################################
package Harpoon;
##########################################################################

@Harpoon::ISA = qw(Enemy);


sub new {
  my ($class, $pangguy, $difficulty) = @_;
  
  my $self = new Enemy($difficulty);
  %$self = ( %$self,
    'x' => $pangguy->{x} + $pangguy->{w}/2 - 10,
    'y' => 0,
    'w' => 20,
    'h' => 0,
    'speed' => 3 + $self->{difficulty} / 4,
    'score' => 0,
  );
  $Game->PlaySound('harpoon');
  bless $self, $class;
  $self;
}

sub EnemyAdvance {
  my $self = shift;
  
  $self->{h} += $self->{speed};
  if ($self->{h} >= $ScreenHeight) {
    $self->Delete();
  }
  $self->CheckGuyCollisions();
  if ($PangZeroBoss::Boss) {
    $self->CheckPangBallCollisions($PangZeroBoss::Boss->{balls});
  }
}

sub OnGuyCollisions {
  my ($self, $guy) = @_;
  
  $Game->PlaySound('harpoonhit');
  $guy->Die();
  $self->Delete();
}

sub CheckPangBallCollisions {
  my ($self, $pangballs, $ball) = @_;
  
  foreach $ball (@$pangballs) {
    if ($ball->Collisions($self)) {
      $Game->PlaySound('harpoonhit');
      $ball->OnHitByProjectile();
      $self->Delete();
      return;
    }
  }
}

sub OnPlayerRespawn {
  my $self = shift;
  $self->Delete();
}

sub Draw {
  my $self = shift;
  my ($y, $ty, $th);
  
  $y  = $self->{h} + 16;
  $ty = 32;
  
  while ($y > 0) {
    $th = 160 - $ty;
    $th = $y  if $th > $y;
    $y -= $th;
    $Textures{enemy}->Blit($self->{x}, $y, $self->{w}, $th,
      448 + 20 * ($Game->{anim} / 7 % 3), $ty, 20, $th);
    $ty = 64;
  }
}

sub OnHitByProjectile {
  my $self = shift;
  
  $self->Delete();
  $Game->PlaySound('harpoonhit');
}

sub CaughtInExplosion {}


##########################################################################
package Witch;
##########################################################################

@Witch::ISA = qw(Enemy);


sub new {
  my ($class, $dir, $difficulty) = @_;
  my ($self, $speed);
  
  $self = new Enemy($difficulty);
  $dir = $dir || ($Game->Rand(100) < 50 ? -1 : 1);
  $speed = (2.5 + $self->{difficulty} / 4) * $dir;
  %$self = ( %$self,
    'x' => $dir > 0 ? -96 : $ScreenWidth,
    'y' => $ScreenHeight - 100 - $Game->Rand(100),
    'w' => 96,
    'h' => 64,
    'dir' => $dir,
    'collisionw' => 70,
    'collisionh' => 48,
    'speedX' => $speed,
    'maxSpeed' => $speed,
    'fireDelay' => 30,
    'score' => 10000,
  );
  bless $self, $class;
  $self->SetupCollisions();
  $Game->PlaySound('witch');
  $self;
}

sub EnforceBounds {}

sub UseAsProjectile {
  return &Bee::UseAsProjectile(@_);
}

sub EnemyAdvance {
  my $self = shift;
  
  my $dir = $self->{dir};
  if ($dir > 0) {
    $self->{speedX} += 0.1  if $self->{speedX} < $self->{maxSpeed};
  } else {
    $self->{speedX} -= 0.1  if $self->{speedX} > $self->{maxSpeed};
  }
  $self->{x} += $self->{speedX};
  $self->{y} += $Sin[(5 * $Game->{anim}) % scalar @Sin];
  if ( ($self->{x} < - $self->{w} and $dir < 0)
    or ($self->{x} > $ScreenWidth and $dir > 0)
  ) {
    $self->Delete();
  }
  if (--$self->{fireDelay} < 0) {
    $self->Fire();
    $self->{fireDelay} = 150 / ($self->{difficulty}+4) + 15;
  }
  $self->CheckGuyCollisions();
  $self->CheckSlurpCollisions();
}

sub Fire {
  new Fireball(@_);
}

sub Draw {
  my $self = shift;
  
  my $dir = $self->{dir};
  $Textures{enemy}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h},
    304 + (0, 48, 96, 48)[$Game->{anim} / 10 % 4], 64, 48 * $dir, 32 );
}


##########################################################################
package Fireball;
##########################################################################

@Fireball::ISA = qw(Enemy);


sub new {
  my ($class, $enemy) = @_;
  
  my $self = new Enemy($enemy->{difficulty});
  %$self = ( %$self,
    'x' => $enemy->{x} + $enemy->{w}/2 - 8,
    'y' => $enemy->{y} + $enemy->{h}/2 - 8,
    'w' => 32,
    'h' => 32,
    'collisionw' => 16,
    'collisionw' => 16,
    'speed' => 2 + $self->{difficulty} / 5,  # 2.2 .. 4 .. 6
    'explosionClass' => 'CometHit',
  );
  bless $self, $class;
  $self->SetupCollisions();
  $self->SelectTarget();
  $self;
}

sub CreateToTarget {
  my ($enemy, $x, $y, $toX, $toY) = @_;
  my ($distance, $speedX, $speedY, $speed);
  
  $speed = 2 + $enemy->{difficulty} / 5,  # 2.2 .. 4 .. 6
  $speedX = $toX - $x;
  $speedY = $toY - $y;
  $distance = sqrt( ($speedX) * ($speedX) + ($speedY) * ($speedY) );
  $speedX = $distance = 1  unless $distance;
  
  my $self = new Enemy($enemy->{difficulty});
  %$self = ( %$self,
    'x' => $x,
    'y' => $y,
    'w' => 32,
    'h' => 32,
    'collisionw' => 16,
    'collisionw' => 16,
    'speedX' => $speedX / $distance * $speed,
    'speedY' => $speedY / $distance * $speed,
  );
  bless $self;
  $self->SetupCollisions();
  $self;
}

sub CreateFromSpeed {
  my ($enemy, $x, $y, $speedX, $speedY) = @_;
  my ($speed, $distance);
  
  $speed = 2 + $enemy->{difficulty} / 5,  # 2.2 .. 4 .. 6
  $distance = sqrt( ($speedX) * ($speedX) + ($speedY) * ($speedY) );
  $speedX = $distance = 1  unless $distance;
  
  my $self = new Enemy($enemy->{difficulty});
  %$self = ( %$self,
    'x' => $x,
    'y' => $y,
    'w' => 32,
    'h' => 32,
    'collisionw' => 16,
    'collisionh' => 16,
    'speedX' => $speedX / $distance * $speed,
    'speedY' => $speedY / $distance * $speed,
  );
  bless $self;
  $self->SetupCollisions();
  $self;
}

sub SelectTarget {
  my $self = shift;
  my ($x, $y, $gx, $gy, $guy, $distance, $shortestDistance, $closestGuy);
  
  $shortestDistance = 10000000;
  $x = $self->{x};
  $y = $self->{y};
  foreach my $guy ( @Guy::Guys ) {
    $gx = $guy->{x} + $guy->{w} / 2;
    $gy = $guy->{y} + $guy->{h} / 2;
    $distance = ($gx - $x) * ($gx - $x) + ($gy - $y) * ($gy - $y);
    if ($distance < $shortestDistance) {
      $closestGuy = $guy;
      $shortestDistance = $distance;
      $self->{speedX} = $gx - $x;
      $self->{speedY} = $gy - $y;
    }
  }
  $self->Delete  unless $closestGuy;
  $self->{speedX} = $shortestDistance = 1  unless $shortestDistance;
  $distance = sqrt($shortestDistance);
  $self->{speedX} = $self->{speedX} / $distance * $self->{speed};
  $self->{speedY} = $self->{speedY} / $distance * $self->{speed};
}

sub EnemyAdvance {
  my $self = shift;
  
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  if ($self->{y} >= $ScreenHeight
    or $self->{x} >= $ScreenWidth
    or $self->{y} < -$self->{h}
    or $self->{x} < -$self->{w}
  ) {
    $self->Delete();
  }
  $self->CheckGuyCollisions();
}

sub OnGuyCollisions {
  my ($self, $guy) = @_;
  
  $guy->Die();
  $self->Delete();
}

sub OnPlayerRespawn {
  my $self = shift;
  
  $self->Delete();
}

sub Draw {
  my $self = shift;
  
  $Textures{enemy}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h},
    64 + ($Game->{anim} / 4 % 4) * 16, 16, 16, 16);
}

sub CheckHitByProjectile {
  return 0;
}

sub CheckHitByFireShield {
  my $self = shift;
  $self->Enemy::CheckHitByProjectile(@_);
}

sub CaughtInExplosion {}



##########################################################################
package Dougar;
##########################################################################

@Dougar::ISA = qw(Enemy);


sub new {
  my ($class, $anim, $orientation, $difficulty) = @_;
  
  $anim = $anim || 0,
  my $self = new Enemy($difficulty);
  %$self = ( %$self,
    'w' => 48,
    'h' => 48,
    'collisionw' => 30,
    'collisionh' => 30,
    'anim' => 90 - $anim * 10,
    'orientation' => $orientation ? 1 : -1,
    'speed' => 0.5 + $self->{difficulty} / 15, #   0.56 .. 1.16 .. 1.83
    'score' => 1000,
#   'explosionClass' => 'CometHit',
  );
  bless $self, $class;
  $self->SetupCollisions();
  $self;
}

sub EnemyAdvance {
  my $self = shift;
  
  if ($self->{spat}) {
    $self->{x} += $self->{speedX};
    $self->{y} += $self->{speedY};
    $self->Delete()  if $self->{x} > $ScreenWidth  || $self->{x} < -$self->{w};
    return;
  }
  
  my $anim = $self->{anim} += $self->{speed};
  $self->{x} = $Sin[$anim * 3 % 800] * 200 + 800 -  $self->{anim} * 1.5;
  $self->{y} = $Cos[$anim * 3 % 800] * 150 * $self->{orientation} + 300;
  $self->CheckGuyCollisions();
  $self->CheckSlurpCollisions();
  $self->Delete()  if ($self->{x} < -50);
}

sub Spat {
  my ($self, $recommendedSpeedX, $recommendedSpeedY) = @_;
  
  $self->SUPER::Spat($recommendedSpeedX, $recommendedSpeedY);
  $self->{spat} = 1;
}

sub EnforceBounds {}

sub OnGuyCollisions {
  my ($self, $guy) = @_;
  
  $guy->Die();
  $self->Delete();
  new CometHit(enemy => $self);
}

sub Draw {
  my $self = shift;
  
  $Textures{enemy}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h},
    96 + ($Game->{anim} / 6 % 4) * 32, 32, 32, 32);
}

sub UseAsProjectile {
  &Bee::UseAsProjectile(@_);
}


##########################################################################
package CloudKill;
##########################################################################

@CloudKill::ISA = qw(Enemy);

sub new {
  my $class = shift;
  
  my $self = new Enemy(@_);
  %$self = ( %$self,
    'w' => 0,
    'h' => 16,
    'x' => $Game->Rand($ScreenWidth / 2) + $ScreenWidth / 2 + 100,
    'y' => $Game->Rand($ScreenHeight - 90) + 0,
    'dir' => -1,
    'maxSpeed' => 2.5 + $self->{difficulty} / 8,
    'score' => 5000,
    'anim' => 0,
  );
  bless $self, $class;
  $self->SetWidth(20);
  $self->{accel} = $self->{maxSpeed} / 50;
  $self;
}

sub Collisions {
  my $self = shift;
  
  return 0  if $self->{dies};
  return $self->SUPER::Collisions(@_);
}

sub SetWidth {
  my ($self, $width) = @_;
  
  $self->{w} = $width;
  $self->{h} = $width * 24 / 64;
  $self->{collisionw} = $width * 3 / 4;
  $self->{collisionh} = $width / 4;
  $self->SetupCollisions();
}

sub EnemyAdvance {
  my ($self) = @_;
  my ($dir);
  
  $self->{x} += $self->{speedX};
  ++$self->{anim};
  if ($self->{dies}) {
    $self->SetWidth($self->{w} - 2);
    $self->Delete()  if $self->{w} <= 16;
  } else {
    $dir = $self->{dir};
    $self->{speedX} += $self->{accel} * $dir  if $self->{speedX} * $dir <= $self->{maxSpeed};
    $self->{y} += $Sin[(8 * $self->{anim}) % scalar @Sin] * $self->{difficulty} / 4;
    $self->SetWidth($self->{w} + 1)  if $self->{w} < 96;
    $self->CheckGuyCollisions()  if $self->{w} > 80;
    $self->CheckSlurpCollisions();
  }
  $self->CloudOutOfBounds();
}

sub CloudOutOfBounds {
  my ($self) = @_;
  
  $self->Delete()  if $self->{x} < -100;
}

sub EnforceBounds {}

sub OnPlayerRespawn {
  my $self = shift;
  $self->{dies} = 1;
}

sub Draw {
  my $self = shift;
  my ($tx, $ty, $w);
  
  $w = $self->{w};
  if ($w >= 90) {
    $tx = 128 + ($Game->{anim} / 30 % 3)*64;
  } elsif ($w > 90) {
    $tx = 448;
  } elsif ($w > 50) {
    $tx = 384;
  } else {
    $tx = 320;
  }
  $Textures{enemy}->Blit($self->{x}, $self->{y}, $w, $self->{h},
    $tx, 0, -64 * $self->{dir}, 24);
}

sub OnHitByProjectile {
  my $self = shift;
  
  &EnemyExplosion::Create($self);
  $self->{dies} = 1;
  $self->{speedX} = 1;
}

sub CaughtInExplosion {
  my $self = shift;
  
  &EnemyExplosion::Create($self);
  $self->{dies} = 1;
  $self->{speedX} = 1;
}


##########################################################################
package ReturningCloudKill;
##########################################################################

@ReturningCloudKill::ISA = qw(CloudKill);

sub new {
  my $class = shift;
  my $boss = shift;
  
  my $self = new CloudKill(@_);
  %$self = ( %$self,
    'score' => 1000,
    'boss' => $boss,
    'isboss' => 1,
  );
  bless $self, $class;
}

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

sub CloudOutOfBounds {
  my ($self) = @_;
  
  if ($self->{x} < -100 and $self->{dir} < 0) {
    $self->{dir} = 1;
    $self->{speedX} = 0;
    $self->{y} = $Game->Rand($ScreenHeight) - 10;
  }
  if ($self->{x} > $ScreenWidth and $self->{dir} > 0) {
    $self->{dir} = -1;
    $self->{speedX} = 0;
    $self->{y} = $Game->Rand($ScreenHeight) - 10;
  }
}

sub EnforceBounds {}

sub OnPlayerRespawn {}


##########################################################################
package Bat;
##########################################################################

@Bat::ISA = qw(Enemy);

sub new {
  my $class = shift;
  my $self = new Enemy(@_);
  
  %$self = (%$self,
    'x' => $ScreenWidth + 80,
    'y' => $Game->Rand($ScreenWidth - 50),
    'w' => 72,
    'h' => 48,
    'collisionw' => 40,
    'collisionh' => 28,
    'direction' => $Game->Rand(200) + 500,
    'directionDelay' => 100,
    'speed' => 0.5 + ($self->{difficulty} + 5) / 5,   # 1.7 .. 3.5 .. 5.5
    'score' => 3000,
  );
  $self->{newDirection} = $self->{direction};
  bless $self, $class;
  $self->SetupCollisions();
  return $self;
}

sub ChooseDirection {
  my ($self) = @_;
  
  $self->{newDirection} = $self->{direction} + $Game->Rand(400) - 200;
  $self->{directionDelay} = 100;
}

sub EnforceBounds {}

sub UseAsProjectile {
  return &Bee::UseAsProjectile(@_);
}

sub EnemyAdvance {
  my ($self) = @_;
  
  $self->ChooseDirection()  if --$self->{directionDelay} <= 0;
  
  if ($self->{direction} < $self->{newDirection}) {
    $self->{direction} += 5;
  } else {
    $self->{direction} -= 5;
  }
  
  $self->{speedX} = $Sin[$self->{direction} % 800] * $self->{speed};
  $self->{speedY} = $Cos[$self->{direction} % 800] * $self->{speed};
  if ($self->{x} < 0) {
    $self->{speedX} = abs($self->{speedX}) + 1;
    $self->{direction} = 200;
  } elsif ($self->{x} + $self->{w} > $ScreenWidth) {
    $self->{speedX} = -abs($self->{speedX}) - 1;
    $self->{direction} = 600;
  }
  if ($self->{y} < 0) {
    $self->{speedY} = abs($self->{speedY}) + 1;
    $self->{direction} = 0;
  } elsif ($self->{y} + $self->{h} > $ScreenHeight) {
    $self->{speedY} = -abs($self->{speedY}) - 1;
    $self->{direction} = 400;
    }
  
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  
  $self->CheckGuyCollisions();
  $self->CheckSlurpCollisions();
}

sub OnGuyCollisions {
  my ($self, $guy) = @_;
  
  $guy->Die();
  $self->Delete();
  &EnemyExplosion::Create($self);
}

sub Draw {
  my $self = shift;
  
  $Textures{enemy}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h},
    304 + 48 * ($Game->{anim} / 10 % 3), 96, 48, 32 );
}


##########################################################################
package FireBat;
##########################################################################

@FireBat::ISA = qw(Bat);

sub new {
  my $class = shift;
  my $self = new Bat(@_);
  
  %$self = (%$self,
    'fireDelay' => 200,
    'score' => 10000,
    'speed' => $self->{speed} * 0.75,
    'w' => 96,
    'h' => 64,
  );
  bless $self, $class;
  return $self;
}


sub EnemyAdvance {
  my ($self) = @_;
  
  $self->SUPER::EnemyAdvance();
  if (--$self->{fireDelay} < 0) {
    new Fireball($self);
    $self->{fireDelay} = 500 / ($self->{difficulty}+4) + 50;   #  150 .. 85 .. 70
  }
}

sub OnGuyCollisions {
  my ($self, $guy) = @_;
  
  $guy->Die();
  $self->Delete();
  &EnemyExplosion::Create($self);
}

sub Draw {
  my $self = shift;
  
  $Textures{enemy}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h},
    352 + 48 * ($Game->{anim} / 10 % 3), 352, 48, 32 );
}


##########################################################################
package BouncyMoon;
##########################################################################

@BouncyMoon::ISA = qw(Enemy);

sub new {
  my $class = shift;
  my $self = new Enemy(@_);
  my ($speedY, $speedMax);
  
  $speedY = $self->{difficulty} / 2 + 5;
  $speedMax = 10.49;  # sqrt( 2 * 0.1 * ($ScreenHeight-50) );
  $speedY = $speedMax  if $speedY > $speedMax;
  %$self = (%$self,
    'x' => $ScreenWidth + 80,
    'y' => 0,
    'w' => 72,
    'h' => 48,
    'collisionw' => 40,
    'collisionh' => 28,
    'speedX' => -3,
    'speedY' => $speedY,
    'speed0' => $speedY,
    'score' => 3000,
  );
  bless $self, $class;
  $self->SetupCollisions();
  return $self;
}

sub EnemyAdvance {
  my ($self) = @_;
  
  $self->{speedY} -= 0.1;
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  if ($self->{y} < 0) {
    $self->{y} = 0;
    $self->{speedY} = $self->{speed0};
  }
  if ($self->{x} < -$self->{w} or $self->{x} > $ScreenWidth + 100) {
    $self->Delete();
  }
  $self->CheckGuyCollisions();
  $self->CheckSlurpCollisions();
}

sub OnGuyCollisions {
  my ($self, $guy) = @_;
  
  $guy->Die();
  $self->Delete();
  &EnemyExplosion::Create($self);
}

sub UseAsProjectile {
  &Bee::UseAsProjectile(@_);
}

sub Draw {
  my $self = shift;
  
  $Textures{enemy}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h},
    464, 396, $self->{speedX} > 0 ? - 48 : 48, 36 );
}


##########################################################################
package FlameElemental;
##########################################################################

@FlameElemental::ISA = qw(Enemy);

sub new {
  my $class = shift;
  my $self = new Enemy(@_);
  
  %$self = (%$self,
    'x' => $ScreenWidth + 80,
    'y' => $Game->Rand($ScreenWidth - 50),
    'w' => 64,
    'h' => 64,
    'collisionw' => 40,
    'collisionh' => 40,
    'target' => undef,
    'targetDelay' => 1,
    'acceleration' => 0.01 + $self->{difficulty} * 0.003,
    'dir' => -1,
    'score' => 10000,
  );
  bless $self, $class;
  $self->SetupCollisions();
  return $self;
}

sub ChooseTarget {
  my ($self) = @_;
  my ($target);
  
#   print STDERR "Choosing target.\n";
  $target = &Guy::ChooseRandomGuy();
  unless ($target) {
    $self->{target} = undef;
    $self->{targetDelay} = 100;
    return;
  }
  $self->{target} = $target;
  $self->{targetDelay} = 0;
}

sub EnforceBounds {}

sub UseAsProjectile {
  return &Bee::UseAsProjectile(@_);
}

sub EnemyAdvance {
  my ($self) = @_;
  my ($target, $targetX, $targetY);
  
  $target = $self->{target};
  if ($target and $target->{deleted}) {
    $self->{target} = 0;
    $target = undef;
    $self->{targetDelay} = 100;
  }
  unless ($target) {
    $self->{y} += $Sin[(5 * $Game->{anim}) % 800] / 2 + $self->{speedY};
    $self->{x} += $Cos[(6 * $Game->{anim}) % 800] / 3 + $self->{speedX};
    $self->ChooseTarget()  if $self->{targetDelay}-- <= 0;
    if ($self->{speedX} > 0) {
      $self->{speedX} -= $self->{acceleration};
    } else {
      $self->{speedX} += $self->{acceleration};
    }
    if ($self->{speedY} > 0) {
      $self->{speedY} -= $self->{acceleration};
    } else {
      $self->{speedY} += $self->{acceleration};
    }
  } else {
    $targetX = $target->{x} + ($self->{x} > $target->{x} ? 250 + $target->{w} : -250 - $self->{w});
    $targetY = $target->{y} - 60;
    $targetY /=2  if $targetY < 0;
    if ($self->{speedX}*abs($self->{speedX}/$self->{acceleration})/2 + $self->{x} < $targetX) {
      $self->{speedX} += $self->{acceleration};
    } else {
      $self->{speedX} -= $self->{acceleration};
    }
    if ($self->{speedY}*abs($self->{speedY}/$self->{acceleration})/2 + $self->{y} < $targetY) {
      $self->{speedY} += $self->{acceleration};
    } else {
      $self->{speedY} -= $self->{acceleration};
    }
    $self->{x} += $self->{speedX};
    $self->{y} += $self->{speedY}; #  + $Sin[14 * $Game->{anim} % 800] / 2;
  }
  
  my ($x1, $y1, $x2, $y2);
  $y1 = $self->{y};
  $y2 = $y1 + 60;
  $x1 = $self->{x} + ($self->{dir} > 0 ? $self->{w} + 30 : - 330);
  $x2 = $x1 + 300;
  
  unless ($self->{firing}) {
    $self->{dir} = $target->{x} > $self-> {x} ? 1 : -1  if $target;
    foreach (@Guy::Guys) {
      if ($_->{x} < $x2  and  $_->{y} < $y2  and
        $_->{x} + $_->{w} > $x1  and  $_->{y} + $_->{h} > $y1)
      {
        $self->Fire();
      }
    }
  } else {
    $self->FiringAdvance();
  }
  $self->CheckGuyCollisions();
  $self->CheckSlurpCollisions();
}

sub Fire {
  my $self = shift;
  
  $self->{firing} = 1;
}

sub FiringAdvance {
  my $self = shift;
  
  ++$self->{firing};
  if ($self->{firing} >= 100 and ($self->{firing} % 15) == 0) {
    new FlameThrower($self);
  }
  if ($self->{firing} >= 200) {
    $self->{firing} = 0;
    $self->{targetDelay} = 100;
  }
}

sub OnGuyCollisions {
  my ($self, $guy) = @_;
  
  $guy->Die();
  $self->Delete();
  &EnemyExplosion::Create($self);
}

sub Draw {
  my $self = shift;
  
  if ($self->{firing}) {
    my $anim = int($self->{firing} / 25);
    $anim = $anim % 4 + 4  if $anim > 8;
    $Textures{enemy}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h},
      128 + 32 * $anim, 320, $self->{dir} > 0 ? -32 : 32, 32 );
  } else {
    $Textures{enemy}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h},
      32 * ($Game->{anim} / 16 % 4), 320, $self->{dir} > 0 ? -32 : 32, 32 );
  }
}



##########################################################################
package FlameThrower;
##########################################################################

@FlameThrower::ISA = qw(Enemy);

sub new {
  my ($class, $enemy) = @_;
  
  my $self = new Enemy($enemy->{difficulty});
  %$self = ( %$self,
    'x' => $enemy->{x} + ($enemy->{dir} > 0 ? $enemy->{w}-24 : -$self->{w}+4),
    'y' => $enemy->{y} + $enemy->{h}/2 - 18,
    'w' => 64,
    'h' => 64,
    'collisionw' => 32,
    'collisionh' => 32,
    'harmless' => 0,
    'speedY' => 0,
    'speedX' => 3.5 * $enemy->{dir},
    'anim' => 0,
    'score' => 0,
    'explosionClass' => 'CometHit',
  );
  bless $self, $class;
  $self->SetupCollisions();
  $self;
}

sub EnemyAdvance {
  my $self = shift;
  
  ++$self->{anim};
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  $self->{speedY} += 0.01;
  if ($self->{anim} >= 120) {
    $self->Delete();
  }
  $self->CheckGuyCollisions()  unless $self->{harmless};
}

sub OnGuyCollisions {
  my ($self, $guy) = @_;
  
  $guy->Die();
  $self->Delete();
}

sub OnPlayerRespawn {
  my $self = shift;
  
  $self->{harmless} = 1;
}

sub Draw {
  my $self = shift;
  
  my ($anim) = int($self->{anim} / 8);
  if ($anim < 4) {
    $Textures{enemy}->Blit($self->{x} + $self->{w} / 4, $self->{y}, $self->{w} / 2, $self->{h},
      $anim * 16, 0, 16, 32);
  } else {
    $Textures{enemy}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h},
      ($anim - 4) * 32, 352, 32, 32);
  }
}

sub CheckHitByProjectile {
  return 0;
}

sub CheckHitByFireShield {
  my $self = shift;
  $self->Enemy::CheckHitByProjectile(@_);
}

sub CaughtInExplosion {}


##########################################################################
package GhostSwarm;
##########################################################################

@GhostSwarm::ISA = qw(InvisibleGameObject GameObject);

sub new {
  my ($class, $difficulty) = @_;
  my $self = new GameObject();
  $self->{difficulty} = $difficulty = $difficulty || $Difficulty;
  
  %$self = (%$self,
    x => $ScreenWidth + 100,
    y => $Game->Rand($ScreenHeight - 200) + 100,
    w => 30,
    h => 30,
    score => 0,
    speedX => -2,
    speedY => 0,
    targetX => $Game->Rand($ScreenWidth / 2) + 200,
    targetY => $Game->Rand($ScreenHeight - 200) + 100,
    acceleration => $difficulty / 500 + 0.01,   #  0.01 .. 0.03 .. 0.05
  );
  bless $self, $class;
  my $numGhosts = int($difficulty / 2.2) + 3;
  foreach (1..$numGhosts) {
    new Ghost($self, $_ / 5, $numGhosts);  #/
  }
  return $self;
}

sub Advance {
  my ($self) = @_;
  
  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} + $self->{speedY} * abs($self->{speedY}) / $self->{acceleration} / 2 < $self->{targetY}) {
    $self->{speedY} += $self->{acceleration};
  } else {
    $self->{speedY} -= $self->{acceleration};
  }

  if (abs($self->{speedX}) + abs($self->{speedY}) < 1) {
    $self->Delete();
  }

  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
}


##########################################################################
package Ghost;
##########################################################################

@Ghost::ISA = qw(Enemy);

sub new {
  my ($class, $swarm, $angle, $difficulty) = @_;
  
  my $self = new Enemy($difficulty);
  unless ($swarm) {
    $difficulty = $self->{difficulty};
    $swarm = { deleted => 1, x => $ScreenWidth, speedX => 0, speedY => 0 };
    $angle = 0;
  }
  
  %$self = (%$self,
    x => $swarm->{x},
    y => $swarm->{y},
    w => 70,
    h => 64,
    collisionw => 48,
    collisionh => 32,
    score => 3000,
    speedX => $swarm->{speedX},
    speedY => $swarm->{speedY},
    acceleration => $difficulty / 200 + 0.04,   #  0.04 .. 0.09 .. 0.14
    rotation => $difficulty / 4 + 3,   # 3.25 .. 5.5 .. 8
    swarm => $swarm,
    angle => int($angle * 800),
  );
  bless $self, $class;
  $self->SetupCollisions();
  return $self;
}

sub EnemyAdvance {
  my ($self) = @_;
  my ($swarm);
  
  $swarm = $self->{swarm};
  if ($swarm and $swarm->{deleted}) {
    $swarm = undef;
    delete $self->{swarm};
  }
  unless ($swarm) {
    $self->{speedX} -= $self->{acceleration};
    $self->Delete()  if $self->{x} < -100;
  } else {
    $self->{targetX} = $swarm->{x} + $Sin[($Game->{anim} * $self->{rotation} + $self->{angle}) % 800] * 100;
    $self->{targetY} = $swarm->{y} + $Cos[($Game->{anim} * $self->{rotation} + $self->{angle}) % 800] * 100;
    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} + $self->{speedY} * abs($self->{speedY}) / $self->{acceleration} / 2 < $self->{targetY}) {
      $self->{speedY} += $self->{acceleration};
    } else {
      $self->{speedY} -= $self->{acceleration};
    }
  }
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  $self->CheckGuyCollisions();
  $self->CheckSlurpCollisions();
}

sub Draw {
  my ($self) = @_;

  $Textures{enemy}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h},
    ($Game->{anim} + $self->{angle}) / 6 % 4 * 80, 496, 70, 64);
}


##########################################################################
package SeekerBot;
##########################################################################

@SeekerBot::ISA = qw(Enemy);

=comment
SeekerBot phases:

* Prepare: Moves to lower-left / upper right of player
* Attack: Charges directly at player
* Wait: Slows down after rebound / miss
=cut


sub new {
  my ($class, $difficulty, %params) = @_;
  
  my $self = new Enemy( $difficulty,
    x => $ScreenWidth,
    y => $Game->Rand($ScreenHeight - 36),
    w => 36,
    h => 36,
    collisionw => 24,
    collisionh => 24,
    speedX => $Game->Rand(60) / 10 - 3,
    speedY => 0,
    score => 2000,
    delay => -1,
    %params,
  );
  $self->{acceleration} = 0.02 + $self->{difficulty} * 0.005;
  bless $self, $class;
  $self->SetupCollisions();
  $self->PrepareAttack();
  $self;
}

sub PrepareAttack {
  my ($self) = @_;
  my ($target);
  
  $target = &Guy::ChooseRandomGuy();
  unless ($target) {
    $self->{delay} = 100 + $Sin[$self->{y} * 1387 % 800] * 20;
    $self->{advance} = \&DelayAdvance;
    $self->{speedX} += $Sin[$self->{x} * 1387 % 800];
    $self->{speedY} += $Cos[$self->{x} * 1387 % 800];
    return;
  }
  $self->{targetGuy} = $target;
  $self->{targetOffsetY} = $target->{y} > $self->{y} ? -200 : 200;
  $self->{targetOffsetY} += $Game->Rand(40) - 20;
  $self->{targetOffsetX} = $Game->Rand(80) - 40;
  $self->{advance} = \&PrepareAdvance;
  $self->{attack} = 0;
}

sub PrepareAdvance {
  my ($self) = @_;
  my ($targetGuy, $targetX, $targetY);
  
  $targetGuy = $self->{targetGuy};
  if (not($targetGuy) or $targetGuy->{killed}) {
    $self->{delay} = 100;
    $self->{advance} = \&DelayAdvance;
    return;
  }
  $targetX = $targetGuy->{x} + $self->{targetOffsetX};
  $targetY = $targetGuy->{y} + $self->{targetOffsetY};
  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};
  }
  if ( abs($self->{x} - $targetX) < 50 and abs($self->{y} - $targetY) < 10 ) {
    $self->{targetOffsetX} = ($targetGuy->{w} - $self->{w}) / 2;
    $self->{targetOffsetY} = ($targetGuy->{h} - $self->{h}) / 2;
    $self->{advance} = \&AttackAdvance;
    $self->{attack} = 1;
  }
}

sub DelayAdvance {
  my ($self) = @_;
  
  $self->{speedX} += ($self->{speedX} > 0) ? -$self->{acceleration} : $self->{acceleration};
  $self->{speedY} += ($self->{speedY} > 0) ? -$self->{acceleration} : $self->{acceleration};
  if (--$self->{delay} <= 0) {
    $self->PrepareAttack();
  }
}

sub AttackAdvance {
  my ($self) = @_;
  my ($targetGuy, $targetX, $targetY);
  
  $targetGuy = $self->{targetGuy};
  if (not($targetGuy) or $targetGuy->{killed}) {
    $self->{delay} = 100;
    $self->{advance} = \&DelayAdvance;
    $self->{attack} = 0;
    return;
  }
  $targetX = $targetGuy->{x} + $self->{targetOffsetX};
  $targetY = $targetGuy->{y} + $self->{targetOffsetY};
  if ($self->{x} + $self->{speedX} * abs($self->{speedX}) / $self->{acceleration} / 4 < $targetX) {
    $self->{speedX} += $self->{acceleration};
  } else {
    $self->{speedX} -= $self->{acceleration};
  }
  if ($self->{y}  + $self->{speedY} * abs($self->{speedY}) / $self->{acceleration} / 4 < $targetY) {
    $self->{speedY} += $self->{acceleration};
  } else {
    $self->{speedY} -= $self->{acceleration};
  }
  if (abs($self->{y} - $targetY) < 10) {
    $self->{advance} = \&DelayAdvance;
    $self->{delay} = 10;
    $self->{attack} = 0;
  }
}

sub EscapeAdvance {
  my ($self) = @_;
  
  $self->{speedX} += $self->{acceleration};
  $self->Delete()  if $self->{x} < -100 || $self->{x} > $ScreenWidth;
}

sub EnemyAdvance {
  my $self = shift;
  my ($targetGuy, $targetX, $targetY);
  
  $self->{advance}->($self);
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  $self->CheckGuyCollisions();
  $self->CheckSlurpCollisions();
}

sub Escape {
  my ($self) = @_;
  $self->{acceleration} = $self->{acceleration} * -1  if $self->{x} < $ScreenWidth / 2;
  $self->{advance} = \&EscapeAdvance;
  $self->{attack} = 0;
}

sub OnGuyCollisions {
  my ($self, $guy) = @_;
  
  $guy->Die();
  $self->Delete();
  &EnemyExplosion::Create($self);
}

sub UseAsProjectile {
  &Bee::UseAsProjectile(@_);
}

sub Draw {
  my ($self) = @_;

  $Sprites{seeker_move}->[$Game->{anim} / 4 % 4]->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h});
  return  if $self->{slurped};
  if ($self->{attack}) {
    if ($Game->{anim}/5 % 8 == 0) {
      &DrawFlare($self->{x}-16, $self->{y}+2, 1,0,0);
    } elsif ($Game->{anim}/5 % 8 == 4) {
      &DrawFlare($self->{x}+20, $self->{y}+2, 1,0,0);
    }
  } else {
    if ($Game->{anim}/5 % 16 == 0) {
      &DrawFlare($self->{x}-16, $self->{y}+2, 0,1,0);
    } elsif ($Game->{anim}/5 % 16 == 4) {
      &DrawFlare($self->{x}+20, $self->{y}+2, 0,1,0);
    }
  }
}

sub DrawFlare {
  my ($x, $y, @color) = @_;
  
  ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE);
  ::glColor(@color);
  $Sprites{flare}->Blit($x, $y, 32, 32);
  ::glColor(1,1,1,1);
  ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE_MINUS_SRC_ALPHA);
}


##########################################################################
package Bullseye;
##########################################################################

@Bullseye::ISA = qw(CircleCollision SafeBall);

sub new {
  my ($class, $size, $hover, $speed) = @_;
  my ($self, $x, $y);

  $self = new SafeBall();
  $size = $size || 128;
  $hover = $hover || 0;
  $speed = $speed || 0;
  $x = $Game->Rand($ScreenWidth - $size - 100) + 50;
  $y = $Game->Rand($ScreenHeight - $size - 100) + 50;
  
  %{$self} = ( %{$self},
    x => ($x > ($ScreenWidth - $size) / 2 ? $ScreenWidth : -$size),
    y => $y,
    targetX => $x,
    targetY => $y,
    w => $size,
    h => $size,
    collisionw => $size,
    collisionh => $size,
    speedX => 0,
    speedY => 0,
    acceleration => 0.05,
    speed => $speed,
    hover => $hover,
    score => 1000,
    explosionClass => 'BullseyeExplosion',
  );
  bless $self, $class;
  $self->SetupCollisions();
  $self;
}

sub EnemyAdvance {
  my ($self) = @_;
  
  if ($self->{state} eq 'entering') {
    $self->ApproachCoordinates($self->{targetX}, $self->{targetY});
    $self->{state} = ''  if abs($self->{x} - $self->{targetX}) < 2;
    $self->{x} += $self->{speedX};
    $self->{y} += $self->{speedY};
  } else {
    $self->BallMovement();
    $self->EnforceBounds();
  }
  $self->{y} += $Cos[($Game->{anim} * 8 + $self->{targetY} * 4237) % 800] * $self->{hover};
}

sub Draw {
  my ($self) = @_;
  
  $Sprites{bullseye}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h});
}

sub CheckSlurpCollisions {}

sub CaughtInExplosion {
  my $self = shift;
  $self->Enemy::CaughtInExplosion(@_);
}

sub CheckHitByProjectile {
  my $self = shift;
  $self->Enemy::CheckHitByProjectile(@_);
}

sub CheckHitByFireShield {}

sub OnPlayerRespawn {
  my $self = shift;
  $self->Enemy::OnPlayerRespawn(@_);
}


##########################################################################
package BullseyeExplosion;
##########################################################################

@BullseyeExplosion::ISA = qw(SafeBall);

# params: enemy
sub new {
  my ($class, %params) = @_;
  my ($enemy, $self);
  
  $enemy = $params{enemy};
  $self = new GameObject(
    x => $enemy->{x},
    y => $enemy->{y},
    w => $enemy->{w},
    h => $enemy->{h},
    speedX => $enemy->{speedX},
    delay => 60,
    %params,
  );
  bless $self, $class;
}

sub Collisions {}

sub Advance {
  my ($self) = @_;
  
  $self->{x} += $self->{speedX};
  return $self->Delete()  if --$self->{delay} <= 0;
}

sub Draw {
  my ($self) = @_;
  my ($w, $delay);
  
  $delay = $self->{delay};
  $w = $::Sin[$delay * 23 % 400] * $self->{w};
  ::glColor(1,1,1,$delay / 60);
  $Sprites{bullseye}->Blit($self->{x} - $w / 2, $self->{y}, $w, $self->{h});
  ::glColor(1,1,1,1);
}


##########################################################################
package Asteroid;
##########################################################################

@Asteroid::ISA = qw(Enemy);

sub new {
  my ($class, $x, $y, $size, $difficulty) = @_;
  my ($self, $objectSize, $collisionSize, $dir, $speed);

  $self = new Enemy($difficulty);
  $objectSize = $size;
  $collisionSize = $size * 2 / 3;
  $dir = $Game->Rand(400) + 100;
  $dir += 200  if $dir > 300;      # 100 .. 300,  500 .. 700
  $speed = $self->{difficulty} * 0.50 + 0.5;  # 1.0 .. 5.5
  
  %{$self} = ( %{$self},
    x => $x,
    y => $y,
    w => $objectSize,
    h => $objectSize,
    collisionw => $collisionSize,
    collisionh => $collisionSize,
    speedX => $Sin[$dir] * $speed,
    speedY => $Cos[$dir] * $speed,
    score => 1000,
  );
  if ($self->{w} > 30) { $class = 'BigAsteroid'; }
  warn $self->{difficulty};
  bless $self, $class;
  $self->SetupCollisions();
  $self;
}

sub EnemyAdvance {
  my ($self) = @_;
  
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  $self->{x} -= $ScreenWidth  if $self->{x} > $ScreenWidth;
  $self->{x} += $ScreenWidth  if $self->{x} < 0;
  $self->{y} -= $ScreenHeight  if $self->{y} > $ScreenHeight;
  $self->{y} += $ScreenHeight  if $self->{y} < 0;
  $self->CheckGuyCollisions();
  $self->CheckSlurpCollisions();
}

sub Draw {
  my ($self) = @_;
  
  ::glColor( 1, 0, 1);
  $Sprites{rock1}->[$Game->{anim} / 10 % 8]->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h});
  if ($self->{x} + $self->{w} > $ScreenWidth) {
    $Sprites{rock1}->[$Game->{anim} / 10 % 8]->Blit($self->{x}-$ScreenWidth, $self->{y}, $self->{w}, $self->{h});
    if ($self->{y} + $self->{h} > $ScreenHeight) {
      $Sprites{rock1}->[$Game->{anim} / 10 % 8]->Blit($self->{x}, $self->{y}-$ScreenHeight, $self->{w}, $self->{h});
      $Sprites{rock1}->[$Game->{anim} / 10 % 8]->Blit($self->{x}-$ScreenWidth, $self->{y}-$ScreenHeight, $self->{w}, $self->{h});
    }
  } else {
    if ($self->{y} + $self->{h} > $ScreenHeight) {
      $Sprites{rock1}->[$Game->{anim} / 10 % 8]->Blit($self->{x}, $self->{y}-$ScreenHeight, $self->{w}, $self->{h});
    }
  }
  ::glColor( 1, 1, 1);
}

sub OnHitByProjectile {
  my ($self, $projectile) = @_;
  
  $self->{speedX} = $projectile->{speedX};
  &EnemyExplosion::Create($self);
  if ($self->{x} + $self->{w} > $ScreenWidth) {
    $self->{x} -= $ScreenWidth; &EnemyExplosion::Create($self);
    if ($self->{y} + $self->{h} > $ScreenHeight) {
      $self->{y} -= $ScreenHeight; &EnemyExplosion::Create($self);
      $self->{x} += $ScreenWidth; &EnemyExplosion::Create($self);
    }
  } else {
    if ($self->{y} + $self->{h} > $ScreenHeight) {
      $self->{y} -= $ScreenHeight; &EnemyExplosion::Create($self);
    }
  }
  $self->Delete();
}

sub OnGuyCollisions {
  my ($self, $guy) = @_;
  
  return  if $guy->{invincible};
  $self->SUPER::OnGuyCollisions($guy);
  $self->OnHitByProjectile($guy);
}

sub Collisions {
  my ($self, $other) = @_;
  my ($clone);
  
  if ($self->{x} + $self->{w} < $ScreenWidth and $self->{y} + $self->{h} < $ScreenHeight) {
    return $self->SUPER::Collisions($other);
  }
  $clone = { x => $self->{x}, y => $self->{y},
    collisionmarginw1 => $self->{collisionmarginw1}, collisionmarginh1 => $self->{collisionmarginh1},
    collisionmarginw2 => $self->{collisionmarginw2}, collisionmarginh2 => $self->{collisionmarginh2},
    centerx => $self->{centerx}, centery => $self->{centery},
  };
  bless $clone, 'GameObject';
  return 1  if $other->Collisions($clone);
  $clone->{x} -= $ScreenWidth;
  return 1  if $other->Collisions($clone);
  $clone->{y} -= $ScreenHeight;
  return 1  if $other->Collisions($clone);
  $clone->{x} += $ScreenWidth;
  return $other->Collisions($clone);
}

sub UseAsProjectile {
  &Bee::UseAsProjectile(@_);
}


##########################################################################
package BigAsteroid;
##########################################################################

@BigAsteroid::ISA = qw(Asteroid);


sub CheckSlurpCollisions {}

sub OnHitByProjectile {
  my ($self, $projectile) = @_;
  my ($asteroid);
  
  foreach (0..1) {
    $asteroid = new Asteroid($self->{x}, $self->{y}, $self->{w}/2, $self->{difficulty} + 0.6 + $_ * 0.4);
    $asteroid->{isboss} = $self->{isboss};
    $asteroid->{ondeleted} = $self->{ondeleted};
    $asteroid->{speedX} = $projectile->{speedX} > 0 ? abs($asteroid->{speedX}) : -abs($asteroid->{speedX});
  }
  $self->SUPER::OnHitByProjectile($projectile);
}

sub CaughtInExplosion {}

sub CheckHitByFireShield {}


##########################################################################
package PangBall;
##########################################################################

@PangBall::ISA = qw(CircleCollision Enemy);
use vars qw($Gravity);
$Gravity = 0.05;

sub new {
  my ($class, $size, $difficulty) = @_;
  my ($self, $speedX, $bounceY); 

  $self = new Enemy($difficulty);
  $speedX = $self->{difficulty} * 0.40 + 0.5;  # 1.0 .. 4.5
  # Bounce height = $bounceY * $bounceY / $Gravity / 2;
  # $bounceY = sqrt($bounceHeight * $Gravity * 2)
  $bounceY = 6.87 + ($size - 128) * 0.014;  # 6.87 .. 6;
  
  %{$self} = ( %{$self},
    size => $size,
    x => $Game->Rand($ScreenWidth - $size - 200) + 100,
    y => $ScreenHeight,
    w => $size * 1.25,
    h => $size * 1.20,
    collisionw => $size,
    collisionh => $size,
    speedX => $Game->Rand(2) ? -$speedX : $speedX,
    speedY => 0,
    speed => $self->{difficulty} * 0.1 + 0.6,  # 0.7 .. 1.6
    bounceY => $bounceY,
    score => 2000,
    explosionClass => 'EnemyPop',
  );
  if ($self->{w} > 30) { $class = 'BigPangBall'; }
  bless $self, $class;
  $self->SetupCollisions();
  $PangZeroBoss::Boss->OnBallCreated($self)  if $PangZeroBoss::Boss;
  $self;
}

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

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

sub EnemyAdvance {
  my ($self) = @_;
  
  $self->{speedY} -= $Gravity * $self->{speed};
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY} * $self->{speed};
  if ($self->{y} < 0) {
    $self->{y} = 0;
    $self->{speedY} = $self->{bounceY};
  }
  if ($self->{x} < 0) {
    $self->{x} = 0;
    $self->{speedX} = abs($self->{speedX});
  }
  if ($self->{x} > $ScreenWidth - $self->{w}) {
    $self->{x} = $ScreenWidth - $self->{w};
    $self->{speedX} = -abs($self->{speedX});
  }
  $self->CheckSlurpCollisions();
}

sub Draw {
  my ($self) = @_;
  
  $Sprites{pangball}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h});
}

sub OnGuyCollisions {
  my ($self, $guy) = @_;
  
  return  if $guy->{invincible};
  $self->SUPER::OnGuyCollisions($guy);
  $self->OnHitByProjectile($guy);
}

sub UseAsProjectile {
  &Bee::UseAsProjectile(@_);
}


##########################################################################
package BigPangBall;
##########################################################################

@BigPangBall::ISA = qw(PangBall);


sub EnemyAdvance {
  my ($self) = @_;
  
  $self->SUPER::EnemyAdvance();
  $self->CheckGuyCollisions()  unless $self->{deleted};
}

sub CheckSlurpCollisions {}

sub OnHitByProjectile {
  my ($self, $projectile) = @_;
  my ($pangball, $size);
  
  $size = $self->{size};
  if ($size > 127) { $size = 96; }
  elsif ($size > 95) { $size = 64; }
  elsif ($size > 63) { $size = 32; }
  else { $size = 16; }
  
  foreach (0..1) {
    $pangball = new PangBall($size, $self->{difficulty});
    $pangball->{isboss} = 1  if $self->{isboss};
    $pangball->{x} = $self->{x} + ($_ ? $self->{w} - $pangball->{w} : 0);
    $pangball->{y} = $self->{y} + ($self->{h} - $pangball->{h}) / 2;
    $pangball->{speedX} = abs($self->{speedX}) * ($_ ? 1 : -1);
    $pangball->{speedY} = 3;
  }
  $self->SUPER::OnHitByProjectile($projectile);
}

sub CaughtInExplosion {}

sub CheckHitByFireShield {}


##########################################################################
package Spider;
##########################################################################

@Spider::ISA = qw(Enemy);

sub new {
  my $class = shift;

  my $self = new Enemy(@_);
  %{$self} = ( %{$self},
    x => $Game->Rand($ScreenWidth - 200 - 36) + 100,
    y => $ScreenHeight,
    w => 36,
    h => 32,
    collisionw => 36 * 0.8,
    collisionh => 32 * 0.8,
    score => 3000,
    phase => 0,
  );
  bless $self, $class;
  $self->SetupCollisions();
  $self->{web} = new Web(spider => $self);
  $self;
}

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

sub EnemyAdvance {
  my ($self) = @_;
  my ($phase);
  
  $phase = $self->{phase};
  if ($phase == 0) {
    $self->{y} -= 2;
    $self->{phase} = 1  if $self->{y} < 400;
  } elsif ($phase == 1) {
    $self->{y} += 2;
    return $self->Delete()  if $self->{y} > $ScreenHeight;
  } elsif ($phase == -1) {
    $self->{speedY} -= 0.7;
    $self->{x} += $self->{speedX};
    $self->{y} += $self->{speedY};
    $self->{rotation} += 10;
    return $self->Delete()  if $self->{y} < -$self->{h};
  }
  
  $self->CheckGuyCollisions();
  $self->CheckSlurpCollisions();
}

sub UseAsProjectile {
  return &Bee::UseAsProjectile(@_);
}

sub Spat {
  my ($self, $recommendedSpeedX, $recommendedSpeedY) = @_;
  
  $self->SUPER::Spat($recommendedSpeedX, $recommendedSpeedY);
  $self->{phase} = -1;
}

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


##########################################################################
package Web;
##########################################################################

@Web::ISA = qw(Enemy);

# params: spider
sub new {
  my ($class, %params) = @_;

  my $self = new Enemy();
  my $spider = $params{spider} or Carp::confess('No spider, no web');
  warn "New web, spider = $spider";
  %{$self} = ( %{$self},
    x1 => $spider->{x} + $spider->{w} / 2,
    y1 => $ScreenHeight,
    x2 => $spider->{x} + $spider->{w} / 2,
    y2 => $spider->{y} + $spider->{h},
    spider => $spider,
    score => 0,
  );
  bless $self, $class;
  $self->SetupCollisions();
  $self;
}

sub EnemyAdvance {
  my ($self) = @_;
  my ($spider);
  
  $spider = $self->{spider};
  $self->{x2} = $spider->{x} + $spider->{w} / 2;
  $self->{y2} = $spider->{y} + $spider->{h};
}

sub Draw {
  my ($self) = @_;
  
  my $weby = $ScreenHeight - 32;
  $weby = $self->{y2}  if $self->{y2} > $weby;
  $Sprites{spiderweb}->Blit($self->{x1}-28, $weby, 64, 32);
  return  unless $weby == $ScreenHeight - 32;
  ::glDisable(::GL_TEXTURE_2D);
  ::glColor(1, 1, 1, 0.5);
  ::glBegin(::GL_LINES);
    ::glVertex($self->{x1}, $self->{y1} - 32);
    ::glVertex($self->{x2}, $self->{y2});
  ::glEnd();
  ::glColor(1,1,1,1);
  ::glEnable(::GL_TEXTURE_2D);
}


##########################################################################
package IceDart;
##########################################################################

@IceDart::ISA = qw(Enemy);

sub new {
  my $class = shift;
  
  my $self = new Enemy(@_);
  %{$self} = ( %{$self},
    'x' => int($Game->Rand($ScreenWidth) - 40) + 80,
    'y' => $ScreenHeight,
    'w' => 25,
    'h' => 64,
    'collisionw' => 18,
    'collisionh' => 50,
    'speedX' => -1,
    'speedY' => -3.5 - $self->{difficulty} / 4,
    'acceleration' => -$self->{difficulty} / 600,
    'score' => 0,
    'dies' => 0,
    'explosionClass' => 'IceExplosion',
  );
  bless $self, $class;
  $self->SetupCollisions();
  $self;
}

sub EnemyAdvance {
  my $self = shift;
  
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  $self->{speedY} += $self->{acceleration};
  $self->Delete()  if $self->{y} < -$self->{h};
  if ($self->{dies}) {
    $self->{dies} -= 3;
    $self->Delete()  if $self->{dies} < 5;
  } else {
    $self->CheckGuyCollisions();
  }
}

sub OnGuyCollisions {
  my ($self, $guy) = @_;
  
  return  if $guy->{invincible};
  if ($guy->{icecube}) {
    $guy->{icecube}->{delay} = 500;
  } else {
    new IceCube(guy => $guy);
  }
  new IceParticles(guy => $guy);
  $self->Delete();
}

sub OnPlayerRespawn {
  my $self = shift;
  
  return  if $self->{isboss};
  $self->{dies} = 100;
}

sub Draw {
  my $self = shift;
  my $alpha = $self->{dies} ? $self->{dies} / 100 : 1;
  
  ::glColor(1,1,1,$alpha);
  $Sprites{icedart}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h});
  ::glColor(1,1,1,1);
}

sub CheckHitByProjectile {
  return 0;
}

sub CheckHitByFireShield {
  my $self = shift;
  $self->Enemy::CheckHitByProjectile(@_);
}

sub CaughtInExplosion {
  my $self = shift;
  $self->{dies} = 100;
}


##########################################################################
package IceParticles;
##########################################################################

@IceParticles::ISA = qw(NoCollisions GameObject);

# params: guy numParticles
sub new {
  my ($class, %params) = @_;
  my ($guy, $x, $y, $w, $h, $particles, $self, $numParticles);
  
  $guy = $params{guy} or Carp::confess;
  $x = $guy->{x};
  $y = $guy->{y};
  $w = $guy->{w} - 32;
  $h = $guy->{h} - 32;
  $numParticles = $params{numParticles} || 20;
  $particles = [ map { {x=>$x + rand($w), y=>$y + rand($h), speedX => ($_-10)/5, speedY => rand(4)-0.5} } (1 .. $numParticles) ];
  $self = new GameObject(
    particles => $particles,
    anim => 100,
  );
  bless $self, $class;
}

sub Advance {
  my ($self) = @_;
  
  return $self->Delete()  if --$self->{anim} <= 0;
  foreach my $particle (@{$self->{particles}}) {
    $particle->{speedY} -= 0.07;
    $particle->{x} += $particle->{speedX};
    $particle->{y} += $particle->{speedY};
  }
}

sub Draw {
  my ($self) = @_;
  
  ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE);
  ::glColor(0.09, 0.73, 1, $self->{anim}/100);
  foreach my $particle (@{$self->{particles}}) {
    $Textures{particle}->Blit($particle->{x}, $particle->{y}, 32, 32, 0, 0, -1, -1);
  }
  ::glColor(1,1,1,1);
  ::glBlendFunc(::GL_SRC_ALPHA,::GL_ONE_MINUS_SRC_ALPHA);
}


##########################################################################
package IceExplosion;
##########################################################################
@IceExplosion::ISA = qw(IceParticles);

sub new {
  my ($class, %params) = @_;
  return new IceParticles(%params, guy => $params{enemy}, numParticles => 5);
}

##########################################################################
package IceCube;
##########################################################################

@IceCube::ISA = qw(NoCollisions GameObject);

# params: guy
sub new {
  my ($class, %params) = @_;
  
  my $guy = $params{guy}  or Carp::confess('No guy, no icecube');
  my $self = new GameObject(%params,
    w => 112,
    h => 96,
    x0 => $guy->{x} - 8,
    y0 => $guy->{y} - 16,
    delay => 500,
    dir => $guy->{dir},
  );
  $guy->{icecube} = $self;
  bless $self, $class;
}

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

sub Melt {
  my ($self) = @_;
  
  new IceParticles(guy => $self->{guy});
  $self->Delete();
}

sub Advance {
  my ($self) = @_;
  
  my $guy = $self->{guy};
  return $self->Delete()  if $guy->{deleted};
  $self->{x} = $self->{x0};
  $self->{y} = $self->{y0};
  if (--$self->{delay} > 50) {
    $guy->{x} = $self->{x} + 8;
    $guy->{y} = $self->{y} + 16;
    $guy->{tilt} = 1;
    $guy->{dir} = $self->{dir};
  } else {
    $self->{x} = $self->{x0} = $guy->{x} - 8;
    $self->{y} = $self->{y0} = $guy->{y} - 16;
    $guy->{tilt} = 0;
    return $self->Delete()  if $self->{delay} <= 0;
  }
  $guy->{slurp}->OnGuyBlownByWind();
}

sub Draw {
  my ($self) = @_;
  my $alpha = $self->{delay};
  if ($alpha > 100) {
    $alpha = $alpha * 0.00125 + 0.375;
  } else {
    $alpha = $alpha / 200;
  }
  
  ::glColor(1,1,1,$alpha);
  $Sprites{icecube}->Blit($self->{x}, $self->{y}, $self->{w}, $self->{h});
  ::glColor(1,1,1,1);
}


##########################################################################
package Bomb;
##########################################################################

@Bomb::ISA = qw(Enemy);

sub new {
  my ($class, $difficulty, %params) = @_;
  my $self = new Enemy( $difficulty,
    x => $ScreenWidth,
    y => $ScreenHeight,
    w => 48,
    h => 48,
    explosionX => $Game->Rand(400) + 200,
    explosionY => $Game->Rand(300) + 150,
    range => 350,
    targetX => 0,
    targetY => 0,
    targetRadius => 600,
    targetAngle => 200,
    speedX => 0,
    speedY => 0,
    collisionw => 32,
    collisionh => 32,
    score => 3000,
    %params,
  );
  bless $self, $class;
  $self->SetupCollisions();
  return $self;
}

sub EnemyAdvance {
  my ($self) = @_;
  
  $self->{targetAngle} -= ($self->{targetRadius} / 200 + 3) * ($self->{difficulty} / 10 + 0.4);
  $self->{targetRadius} -= 1;
  $self->{targetX} = $self->{explosionX} + $self->{targetRadius} * $Sin[$self->{targetAngle} % 800];
  $self->{targetY} = $self->{explosionY} + $self->{targetRadius} * $Cos[$self->{targetAngle} % 800];
  $self->{x} = $self->{targetX};
  $self->{y} = $self->{targetY};
  return $self->Explode()  if $self->{targetRadius} <= $self->{difficulty} * 6;
  $self->CheckSlurpCollisions();
}

sub Explode {
  my ($self) = @_;
  my ($guy, @guysToKill, $x, $y, $dx, $dy, $range, $i, $radius, $angle);
  
  new MegaExplosion($self);
  &SuperBall::UseAsProjectileExplode($self);
  
  $range = $self->{range};
  $x = $self->{x} - $self->{w} / 2;
  $y = $self->{y} - $self->{h} / 2;
  foreach $guy (@Guy::Guys) {
    next  if $guy->{fireshield};
    $dx = $guy->{x} + $guy->{w} / 2 - $x;
    $dy = $guy->{y} + $guy->{h} / 2 - $y;
    if ($dx*$dx + $dy*$dy < $range * $range) {
      push @guysToKill, $guy;
    }
  }
  foreach $guy (@guysToKill) {
    $guy->Die();
  }
  $Game->PlaySound('bomb');
  $self->Delete();
  for (my $i = int($range * $range / 2000); $i > 0; --$i) {
    $angle = int(rand(800));
    $radius = int(rand($range - 20)) + 20;
    new CometHit(
      enemy => $self,
      x => $radius * $Cos[$angle] + $x,
      y => $radius * $Sin[$angle] + $y,
      anim => -$i,
    );
  }
}

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

sub UseAsProjectileExplode {
  my ($self) = @_;
  $self->Explode();
}

sub CaughtInExplosion {
  my ($self) = @_;
  $self->Explode();
}

sub Spat {
  my ($self) = @_;
  $self->Explode();
}

sub Draw {
  my ($self) = @_;
  
  my $offset = 0;
  my $size = $self->{w};
  if ($self->{targetRadius} < 150 and ($Game->{anim} / 10 % 2) and not $self->{slurped}) {
    $offset = 8;
    $size -= 16;
  }
  $Sprites{bomb}->Blit($self->{x}+$offset, $self->{y}+$offset, $size, $size);
  $Sprites{bombspark}->[$Game->{anim} / 6 % 3]->Blit(
    $self->{x} + $self->{w}/2 - 6, $self->{y} + $self->{h} - 8, 16, 16);
}


##########################################################################
package FallingBomb;
##########################################################################

@FallingBomb::ISA = qw(Bomb);

#params: x, y, speedX,
sub new {
  my ($class, %params) = @_;
  my $self = new Bomb($Difficulty,
    speedY => 0,
    gravity => 0.1,
    score => 3000,
    %params,
  );
  foreach (sort keys %$self) { print "$_=$self->{$_}:" }; print "\n";
  bless $self, $class;
}

sub EnemyAdvance {
  my ($self) = @_;
  
  $self->{speedY} -= $self->{gravity};
  $self->{targetRadius} = $self->{y} += $self->{speedY};
  $self->{x} += $self->{speedX};
  return $self->Explode()  if $self->{targetRadius} <= $self->{difficulty} * 6;
  $self->CheckGuyCollisions();
  $self->CheckSlurpCollisions();
}

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

sub OnGuyCollisions {
  my ($self, $guy) = @_;
  
  $self->Explode();
}




# Eaten: 3230200 attack1.info.hit -> 4ph 7sc narancs
#        4230106.attack1.info.hit -> ugyanez kék
#        4230116.attack1.info.hit -> 3ph 8sc narancs
#        4230117.attack1.info.hit -> ugyanez kék
#        4240000.attack1.info.hit -> 3ph 5pt
#        8140702.attack1.info.hit -> 6ph 7pt
#        8140703.attack1.info.hit

# 9300038 -> lapított kőr
# 9300056 -> ugyanez méreg?
# 9300045 -> kis kőrös
# 9300046 -> ugyanez méreg?


1;
