#!/usr/bin/perl -w


$::Cheat = 0;
$::Version = '0.15';
$::DataDir = '';  # Set it to a path to avoid autodetection (e.g. /opt/pangzero/data)

=comment

##########################################################################
#
# PANG ZERO
# Copyright (C) 2006 by UPi <upi at sourceforge.net>
#
##########################################################################

This program is free software; you can redistribute it and//or modify
it under the terms of the GNU General Public License version 2, as
published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.


##########################################################################
# TODO:
##########################################################################
* Bonus probability is balldesc based.
* Explore display_format and other speed enhancements.
* Tour mode..?
* Graphics and help for machine gun and power wire
* Demo of beating the game at 'normal' difficulty
* Even more forgiving collision detection (?)


=cut


use strict;
use SDL;
use SDL::App;
use SDL::Event;
use SDL::Surface;
use SDL::Timer;
use SDL::Palette;
use SDL::Sound;
use SDL::Mixer;
use SDL::Font;
use Carp;


# SDL objects

use vars qw (
  $App $RotoZoomer $Background $ScoreFont
  %BallSurfaces
  $BorderSurface $WhiteBorderSurface $RedBorderSurface $BonusSurface $LevelIndicatorSurface $LevelIndicatorSurface2
  $WhiteHarpoonSurface
  %Sounds $Mixer
);

# Pang Zero variables and objects

use vars qw (
  $DataDir $ScreenHeight $ScreenWidth $PhysicalScreenWidth $PhysicalScreenHeight $ScreenMargin
  $SoundEnabled $MusicEnabled $FullScreen
  $DeathBallsEnabled $EarthquakeBallsEnabled $WaterBallsEnabled $SeekerBallsEnabled
  @DifficultyLevels $DifficultyLevelIndex $DifficultyLevel
  @GameObjects %GameEvents $GameSpeed $GamePause $Game
  @Players @GuyImageFiles @GuyColors $NumGuys @BallDesc %BallDesc @GameLevels
  %Keys %Events %MenuEvents );

##########################################################################
# GLOBAL CONFIGURATION
##########################################################################

%Sounds = (
  'pop' => 'pop.voc',
  'shoot' => 'shoot.voc',
  'death' => 'meow.voc',
  'level' => 'level.voc',
  'bonuslife' => 'magic.voc',
  'pause' => 'pop3.voc',
  'quake' => 'quake.voc',
);

@DifficultyLevels = (
  { 'name' => 'Easy',     'spawnmultiplier' => 1.2, 'speed' => 0.8, 'harpoons' => 5, 'superball' => 0.8, 'bonusprobability' => 0.2, },
  { 'name' => 'Normal',   'spawnmultiplier' => 1.0, 'speed' => 1.0, 'harpoons' => 3, 'superball' => 1.0, 'bonusprobability' => 0.1, },
  { 'name' => 'Hard',     'spawnmultiplier' => 0.9, 'speed' => 1.2, 'harpoons' => 2, 'superball' => 1.1, 'bonusprobability' => 0.05, },
  { 'name' => 'Nightmare','spawnmultiplier' => 0.8, 'speed' => 1.4, 'harpoons' => 2, 'superball' => 1.5, 'bonusprobability' => 0.02, },
  { 'name' => 'Miki',     'spawnmultiplier' => 0.4, 'speed' => 1.0, 'harpoons' => 3, 'superball' => 1.0, 'bonusprobability' => 0.1, },
);
$DifficultyLevelIndex = 1;

$NumGuys = 2;
@Players = (
  { 'keys'  => [SDLK_LEFT, SDLK_RIGHT, SDLK_UP], }, # blue
  { 'keys'  => [SDLK_a, SDLK_d, SDLK_s], },           # red
  { 'keys'  => [SDLK_j, SDLK_l, SDLK_k], },         # green
  { 'keys'  => [SDLK_KP6, SDLK_KP4, SDLK_KP5], },   # pink
  { 'keys'  => [SDLK_KP6, SDLK_KP4, SDLK_KP5], },   # yellow
  { 'keys'  => [SDLK_KP6, SDLK_KP4, SDLK_KP5], },   # cyan
  { 'keys'  => [SDLK_KP6, SDLK_KP4, SDLK_KP5], },   # gray
  { 'keys'  => [SDLK_KP6, SDLK_KP4, SDLK_KP5], },   # snot
  { 'keys'  => [SDLK_KP6, SDLK_KP4, SDLK_KP5], },   # purple
);
@GuyImageFiles = ( 'guy_pix.png', 'guy_pux.png', 'guy_danigm.png' );
@GuyColors = ( [170, 255, 'blue'], [0, 255, 'red'], [85, 255, 'green'], [212, 255, 'pink'],
               [42, 255, 'yellow'], [128, 255, 'cyan'], [128, 0, 'gray'], [113, 128, 'snot'], [212, 64, 'purple'] );
for (my $i=0; $i<=$#Players; ++$i) {
  $Players[$i]->{number} = $i;
  $Players[$i]->{colorindex} = $i;
  $Players[$i]->{imagefileindex} = $i % scalar(@GuyImageFiles);
}

my (%n0, %n1, %n2, %n3, %n4);
%n0 = ('popIndex' => 0, 'rect' => new SDL::Rect(-x =>   0, -y =>  12, -width => 128, -height => 106));
%n1 = ('popIndex' => 1, 'rect' => new SDL::Rect(-x => 128, -y =>   8, -width =>  96, -height =>  80));
%n2 = ('popIndex' => 2, 'rect' => new SDL::Rect(-x =>   0, -y => 134, -width =>  64, -height =>  53));
%n3 = ('popIndex' => 3, 'rect' => new SDL::Rect(-x => 224, -y =>   2, -width =>  32, -height =>  28));
%n4 = ('popIndex' => 4, 'rect' => new SDL::Rect(-x => 232, -y =>  42, -width =>  16, -height =>  15));

@BallDesc = (
# Normal balls (n0 .. n4)
  { 'name' => 'n0', 'class' => 'Ball', 'score' =>  2000, 'spawndelay' =>   1, 'speedY' => 6.5, %n0, 'surface' => 'ball', },
  { 'name' => 'n1', 'class' => 'Ball', 'score' =>  1000, 'spawndelay' => 0.5, 'speedY' => 5.7, %n1, 'surface' => 'ball', },
  { 'name' => 'n2', 'class' => 'Ball', 'score' =>   800, 'spawndelay' => 0.25, 'speedY' => 5,  %n2, 'surface' => 'ball', },
  { 'name' => 'n3', 'class' => 'Ball', 'score' =>   600, 'spawndelay' => 0.12, 'speedY' => 4,  %n3, 'surface' => 'ball', },
  { 'name' => 'n4', 'class' => 'Ball', 'score' =>   500, 'spawndelay' => 0.05, 'speedY' => 3,  %n4, 'surface' => 'ball', },
# "Bouncy" balls (b0..b2)
  { 'name' => 'b0', 'class' => 'Ball', 'score' =>  1500, 'spawndelay' => 0.5, 'speedY' => 5.7, 'popIndex' => 2,
    'surface' => 'ball', 'rect' => new SDL::Rect(-x => 160, -y => 166, -width => 64, -height => 52) },
  { 'name' => 'b1', 'class' => 'Ball', 'score' =>   750, 'spawndelay' => 0.2, 'speedY' => 5, 'popIndex' => 3,
    'surface' => 'ball', 'rect' => new SDL::Rect(-x => 224, -y => 66, -width => 32, -height => 28) },
  { 'name' => 'b2', 'class' => 'Ball', 'score' =>   500, 'spawndelay' => 0.1, 'speedY' => 4.2, 'popIndex' => 4,
    'surface' => 'ball', 'rect' => new SDL::Rect(-x => 232, -y => 106, -width => 16, -height => 14) },
# Hexas (h0..h2)
  { 'name' => 'h0', 'class' => 'Hexa', 'score' =>  1500, 'spawndelay' => 0.5, 'popIndex' => 5, 'hexa' => 1,
    'surface' => 'hexa', 'rect' => new SDL::Rect(-x => 0, -y => 6, -width => 64, -height => 52),
    'magicrect' => new SDL::Rect(-x => 0, -y => 102, -width => 64, -height => 52), },
  { 'name' => 'h1', 'class' => 'Hexa', 'score' =>  1000, 'spawndelay' => 0.2, 'popIndex' => 6, 'hexa' => 1,
    'surface' => 'hexa', 'rect' => new SDL::Rect(-x => 0, -y => 66, -width => 32, -height => 28),
    'magicrect' => new SDL::Rect(-x => 0, -y => 162, -width => 32, -height => 28), },
  { 'name' => 'h2', 'class' => 'Hexa', 'score' =>   500, 'spawndelay' => 0.1, 'popIndex' => 7, 'hexa' => 1,
    'surface' => 'hexa', 'rect' => new SDL::Rect(-x => 192, -y => 65, -width => 16, -height => 14),
    'magicrect' => new SDL::Rect(-x => 9, -y => 200, -width => 16, -height => 14), },
# Water ball
  { 'name' => 'w1', 'class' => 'WaterBall', 'score' =>  1500, 'spawndelay' => 0.4, 'speedY' => 5.7, %n1, 'surface' => 'blue', },
  { 'name' => 'w2', 'class' => 'WaterBall', 'score' =>  1000, 'spawndelay' => 0.2, 'speedY' => 5,   %n2, 'surface' => 'blue', },
  { 'name' => 'w3', 'class' => 'WaterBall', 'score' =>   800, 'spawndelay' => 0.1, 'speedY' => 4,   %n3, 'surface' => 'blue', },
  { 'name' => 'w4', 'class' => 'WaterBall', 'score' =>   600, 'spawndelay' => 0.05, 'speedY' => 3,  %n4, 'surface' => 'blue', },
# Superball
  { 'name' => 'super0', 'class' => 'SuperBall', 'score' =>  1000, 'spawndelay' => 0.5, 'speedY' => 5.7, %n1, 'surface' => 'green', },
  { 'name' => 'super1', 'class' => 'SuperBall', 'score' =>   800, 'spawndelay' => 0.25, 'speedY' => 5,  %n2, 'surface' => 'green', },
  { 'name' => 'super2', 'class' => 'SuperBall', 'score' =>   600, 'spawndelay' => 0.12, 'speedY' => 4,  %n3, 'surface' => 'green', },
# Death
  { 'name' => 'death', 'class' => 'DeathBall', 'score' => 0, 'spawndelay' => 0.5, 'speedY' => 5, %n2, 'surface' => 'death', },
# Seeker
  { 'name' => 'seeker', 'class' => 'SeekerBall', 'score' => 1200, 'spawndelay' => 0.2, 'speedY' => 5.7, %n2, 'surface' => 'white', },
  { 'name' => 'seeker1', 'class' => 'SeekerBall', 'score' => 1200, 'spawndelay' => 0.1, 'speedY' => 5,  %n3, 'surface' => 'white', },
# Quake
  { 'name' => 'quake', 'class' => 'EarthquakeBall', 'score' =>   1600, 'spawndelay' => 0.7, 'speedY' => 5.7, 'quake' => 5,
    'surface' => 'quake', %n2 },
  { 'name' => 'quake1', 'class' => 'EarthquakeBall', 'score' =>   1200, 'spawndelay' => 0.2, 'speedY' => 5, 'quake' => 3,
    'surface' => 'quake', %n3 },
  { 'name' => 'quake2', 'class' => 'EarthquakeBall', 'score' =>   1000, 'spawndelay' => 0.1, 'speedY' => 4.2, 'quake' => 2,
    'surface' => 'quake', %n4 },

  { 'name' => 'credits1', 'class' => 'Ball', 'speedY' => 6.1, 'nextgen' => 1, 'surface' => 'blue', %n3 },
  { 'name' => 'credits2', 'class' => 'Ball', 'speedY' => 6.1, 'nextgen' => 1, 'surface' => 'green', %n3 },
);
{
  foreach my $ballDesc (@BallDesc) {
    $ballDesc->{width} = $ballDesc->{rect}->width();
    $ballDesc->{height} = $ballDesc->{rect}->height();
    $BallDesc{$ballDesc->{name}} = $ballDesc;
  }
}
$BallDesc{n0}->{nextgen} = $BallDesc{n1};
$BallDesc{n1}->{nextgen} = $BallDesc{n2};
$BallDesc{n2}->{nextgen} = $BallDesc{n3};
$BallDesc{n3}->{nextgen} = $BallDesc{n4};

$BallDesc{b0}->{nextgen} = $BallDesc{b1};
$BallDesc{b1}->{nextgen} = $BallDesc{b2};

$BallDesc{w1}->{nextgen} = $BallDesc{w2};
$BallDesc{w2}->{nextgen} = $BallDesc{w3};
$BallDesc{w3}->{nextgen} = $BallDesc{w4};

$BallDesc{h0}->{nextgen} = $BallDesc{h1};
$BallDesc{h1}->{nextgen} = $BallDesc{h2};

$BallDesc{death}->{nextgen} = $BallDesc{death};
$BallDesc{quake}->{nextgen} = $BallDesc{quake1};
$BallDesc{quake1}->{nextgen} = $BallDesc{quake2};
$BallDesc{seeker}->{nextgen} = $BallDesc{seeker1};

my %BallMixes;
%BallMixes = (
  'easy'   => [ qw(n0 2 n1 20 n2 10 n3 3 n4 2   b0 5 b1 2 b2 1   w1 10  h0 5  h1 3 h2 1           quake 2   seeker 5 ) ],
  'medium' => [ qw(n0 10 n1 20 n2 10 n3 3 n4 2  b0 10 b1 2 b2 1  w1 15  h0 15 h1 5 h2 1  death 2  quake 5   seeker 10) ],
  'bouncy' => [ qw(n0 20 n1 10 n2 5 n3 1 n4 1   b0 30 b1 9 b2 1  w1 10  h0 15 h1 5       death 5  quake 10  seeker 15) ],
  'hard'   => [ qw(n0 20 n1 10 n2 5 n3 1        b0 20 b1 2       w1 20  h0 20 h1 5       death 10 quake 15  seeker 20) ],
  'watery' => [ qw(n0 20 n1 10 n2 5 n3 1 n4 1   b0 10 b1 5       w1 50  h0 15 h1 5       death 5  quake 10  seeker 15) ],
  'hexas'  => [ qw(n0 20 n1 10 n2 5 n3 1        b0 15 b1 2       w1 20  h0 40 h1 15      death 5  quake 10  seeker 15) ],
  'quakes' => [ qw(n0 15 n1 10 n2 5 n3 1        b0 15            w1 15  h0 20 h1 5       death 5  quake 40  seeker 15) ],
);

sub AddLevels {
  my ($num, $balls, $gamespeedStart, $gamespeedEnd, $spawndelayStart, $spawndelayEnd) = @_;
  my ($i, $level);

  for ($i = 0; $i < $num; ++$i) {
    $level = {
      'balls' => $balls,
      'gamespeed' => $gamespeedStart + ($gamespeedEnd - $gamespeedStart) * ($i) / ($num),
      'spawndelay' => $spawndelayStart + ($spawndelayEnd - $spawndelayStart) * ($i) / ($num),
    };
    push @GameLevels, ( $level );
  }
}

&AddLevels(  9, $BallMixes{easy},   0.75, 1.25, 20, 20 ); # 0-9
&AddLevels( 10, $BallMixes{medium}, 0.7 , 1.3 , 20, 15 ); # 1x
&AddLevels( 10, $BallMixes{hard},   0.7 , 1.5 , 15, 15 ); # 2x
&AddLevels( 10, $BallMixes{hexas},  1.0 , 1.5 , 15, 12 ); # 3x
&AddLevels( 10, $BallMixes{watery}, 0.7 , 1.7 , 15, 17 ); # 4x
&AddLevels( 10, $BallMixes{bouncy}, 1.0 , 2.0 , 12, 12 ); # 5x
&AddLevels( 10, $BallMixes{quakes}, 1.5 , 2.2 , 13,  8 ); # 6x
&AddLevels( 10, $BallMixes{hard},   1.0 , 2.2 , 13, 10 ); # 7x
&AddLevels( 10, $BallMixes{hexas},  1.3 , 2.4 , 12,  9 ); # 8x
&AddLevels( 10, $BallMixes{hard},   2.0 , 3.0 , 13, 10 ); # 9x

# Set defaults

$ScreenMargin = 16;
$ScreenWidth =  800 - $ScreenMargin * 2;
$ScreenHeight = 416;
$SoundEnabled = 1;
$MusicEnabled = 1;
$DeathBallsEnabled = 1;
$EarthquakeBallsEnabled = 1;
$WaterBallsEnabled = 1;
$SeekerBallsEnabled = 1;
$FullScreen = 1;
&SetDifficultyLevel(1);


##########################################################################
# CONFIG SAVE/LOAD
##########################################################################

sub TestDataDir {
  return -f "$DataDir/harpoon.png";
}

sub FindDataDir {
  return if $DataDir and &TestDataDir();
  my @guesses = qw( . .. /usr/share/pangzero /usr/share/games/pangzero /usr/local/share/pangzero /opt/pangzero/ /opt/pangzero);
  foreach my $guess (@guesses) {
    $DataDir = $guess;
    return if &TestDataDir();
    $DataDir = "$guess/data";
    return if &TestDataDir();
  }
  die "Couldn't find the data directory. Please set it manually.";
}

sub GetConfigFilename {
  if ($^O eq 'MSWin32') {
    if ($ENV{USERPROFILE}) {
      return "$ENV{USERPROFILE}\\pangzero.cfg";
    }
    return "$DataDir/pangzero.cfg";
  }
  if ($ENV{HOME}) {
    return "$ENV{HOME}/.pangzerorc";
  }
  if (-w $DataDir) {
    return "$DataDir/pangzero.cfg";
  }
  return "/tmp/pangzero.cfg";
}

sub GetConfigVars {
  my @result = qw(NumGuys DifficultyLevelIndex MusicEnabled SoundEnabled FullScreen 
    DeathBallsEnabled EarthquakeBallsEnabled WaterBallsEnabled SeekerBallsEnabled);
  for (my $i=0; $i < scalar @Players; ++$i) {
    for (my $j=0; $j < 3; ++$j) {
      push @result, ("Players[$i]->{keys}->[$j]");
    }
    push @result, ("Players[$i]->{colorindex}");
    push @result, ("Players[$i]->{imagefileindex}");
  }
  return @result;
}

sub SaveConfig {
  my ($filename, $varname, $value);
  $filename = &GetConfigFilename();

  open CONFIG, "> $filename" or return;
  foreach $varname (&GetConfigVars) {
    eval("\$value = \$$varname"); die $@ if $@;
    print CONFIG "$varname = $value\n";
  }
  close CONFIG;
}

sub LoadConfig {
  my ($filename, $text, $varname);

  $text = '';
  $filename = &GetConfigFilename();
  if (open CONFIG, "$filename") {
    read CONFIG, $text, 16384;
    close CONFIG;
  }
  
  foreach $varname (&GetConfigVars) {
    my $pattern = $varname;
    $pattern =~ s/\[/\\[/g;
    if ($text =~ /$pattern = (.+?)$/m) {
      eval( "\$$varname = '$1'" );
    }
  }
  &SetDifficultyLevel($DifficultyLevelIndex);
}

sub SetDifficultyLevel {
  my $difficultyLevelIndex = shift;
  if ($difficultyLevelIndex < 0 or $difficultyLevelIndex > $#DifficultyLevels) {
    $difficultyLevelIndex = $DifficultyLevelIndex;
  }
  $DifficultyLevelIndex = $difficultyLevelIndex;
  $DifficultyLevel = $DifficultyLevels[$difficultyLevelIndex];
}

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

package GameObject;
package Ball;
package Hexa;
package SuperBall;
package DeathBall;
package SeekerBall;
package EarthquakeBall;
package WaterBall;
package Pop;
package GamePause;
package BonusDrop;
package Guy;
package Harpoon;
package MachineGun;
package PowerWire;
package HalfCutter;
package DeadGuy;


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

sub new {
  my ($class) = @_;
  my $self = {
    'rect' => new SDL::Rect( -x => 0, -y => 0, -width => 0, -height => 0 ),
    'speedX' => 0,
    'speedY' => 0,
    'x' => 0,
    'y' => 0,
    'w' => 10,
    'h' => 10,
  };
  bless $self, $class;
}

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;
  $self->Clear();
}

sub Advance {
}

sub Clear {
  my ($self) = @_;
  $::Background->blit($self->{rect}, $::App, $self->{rect});
}

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

  $self->{rect}->x($self->{x} + $::ScreenMargin);
  $self->{rect}->y($self->{y} + $::ScreenMargin);
  $self->{rect}->width($self->{w});
  $self->{rect}->height($self->{h});
}

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

  $self->TransferRect();
  $::App->fill( $self->{rect}, new SDL::Color(-r => 0x80) );
}

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->{y} / 2;
}

sub Collisions {
  my ($self, $other) = @_;
  
  # Bounding box detection
  
  unless ($self->{collisionmarginw1} and $other->{collisionmarginw1}) {
    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;
  }
  
  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;
}

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

@Ball::ISA = qw(GameObject);
$Ball::Gravity = 0.05;
$Ball::MagicBallRect = new SDL::Rect(-x => 232, -y => 138, -width => 16, -height => 15);

for (my $i=0; $i <= $#::BallDesc; ++$i) {
  my $desc = $::BallDesc[$i];
  next if $desc->{hexa};
  $desc->{bounceY} = $desc->{speedY} * $desc->{speedY} / $Ball::Gravity / 2;
}

sub Create {
  my ($description, $x, $y, $dir) = @_;
  my ($retval);

  eval("\$retval = new $description->{class}(\@_);"); die $@ if $@;
  return $retval;
}

sub Spawn {
  my ($description, $x, $dir, $hasBonus) = @_;
  my ($retval);
  
  $x = $::Game->Rand( $::ScreenWidth - $description->{width} ) if $x < 0;
  $retval = &Create( $description, $x, -$description->{height}, $dir );
  $retval->GiveMagic();
  $retval->GiveBonus() if $hasBonus;

  $retval->{spawning} = 1;
  my $surfaceName = 'dark' . $description->{surface};
  $retval->{surface} = $::BallSurfaces{$surfaceName};
  die "No surface: $surfaceName" unless $retval->{surface};
  return $retval;
}

sub new {
  my ($class, $description, $x, $y, $dir) = @_;
  my ($self);

  $self = new GameObject;
  %{$self} = ( %{$self},
    'x' => $x,
    'y' => $y,
    'w' => $description->{width},
    'h' => $description->{height},
    'surface' => $::BallSurfaces{$description->{surface}},
    'hexa' => $description->{hexa} ? 1 : 0,
    'desc' => $description,
    'hasmagic' => 0,        # true if one of the ball's descendants is magic
    'ismagic' => 0,         # true if the ball IS magic
    'spawning' => 0,
    'delay' => 0,
  );
  $self->{speedX} = $dir > 0 ? 1.3 : -1.3;
  $self->SetupCollisions();
  bless $self, $class;
}

sub NormalAdvance {
  my $self = shift;
  
  $self->{speedY} += $Ball::Gravity * $::GameSpeed unless ($self->{hexa});
  $self->{x} += $self->{speedX} * $::GameSpeed;
  $self->{y} += $self->{speedY} * $::GameSpeed;
  if ($self->{y} > $::ScreenHeight - $self->{h}) {
    $self->{y} = $::ScreenHeight - $self->{h};
    if ($self->{hexa}) {
      $self->{speedY} = -abs($self->{speedY});
    } else {
      $self->{speedY} = -$self->{desc}->{speedY};
    }
    $self->Bounce;
  }
  if ($self->{y} < 0) {
    $self->{y} = 0;
    $self->{speedY} = abs($self->{speedY});
  }
  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} );
  }
}

sub SpawningAdvance {
  my $self = shift;

  --$self->{delay};
  if ($self->{delay} < 0) {
    $self->{y} += 16;
    $self->{delay} = 50 / $::GameSpeed;
    if ($self->{y} >= 0) {
      $self->{spawning} = 0;
      $self->{surface} = $::BallSurfaces{$self->{desc}->{surface}},
    }
  }
}

sub Advance {
  my $self = shift;

  unless( $::GamePause > 0 ) {
    if ($self->{spawning}) {
      $self->SpawningAdvance();
    } else {
      $self->NormalAdvance();
    }
  }

  $self->CheckCollisions() unless $::Game->{nocollision} or $self->{spawning};
}

sub Bounce {
}

sub CheckCollisions {
  my $self = shift;
  my ($gameObject);

  foreach $gameObject (@::GameObjects) {
    if ( $gameObject->isa('Harpoon') ) {
      if ($self->Collisions($gameObject)) {
        $self->Pop($gameObject->{guy}, $gameObject->{popEffect});
        $gameObject->Delete();
        last;
      }
    }
    elsif ( ref $gameObject eq 'Guy' ) {
      if ($::GamePause <= 0 and $self->Collisions($gameObject)) {
        $gameObject->Kill();
      }
    }
  }
}

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

  return if $::GamePause > 0 and $::GamePause < 100 and (int($::GamePause / 3) % 4) < 2;
  
  $self->TransferRect();
  if ($self->{ismagic} and int($::Game->{anim}/4) % 2) {
    $self->{surface}->blit( $Ball::MagicBallRect, $::App, $self->{rect} )
  } else {
    $self->{surface}->blit( $self->{desc}->{rect}, $::App, $self->{rect} );
  }
}

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

  # Bounding box detection

  return unless $self->SUPER::Collisions($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->{w} / 2;
  $centerY = $self->{y} + $self->{h} / 2;

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

  if ($centerX < $boxAxisX) {
    return 1 if $centerY < $boxAxisY + $self->{h} / 2;
    return 0;
  }
  if ($centerY < $boxAxisY) {
    return 2 if $centerX < $boxAxisX + $self->{w} / 2;
    return 0;
  }
  $distSquare = ($centerX-$boxAxisX) * ($centerX-$boxAxisX);
  $distSquare+= ($centerY-$boxAxisY) * ($centerY-$boxAxisY);
  return 3 if $distSquare < $self->{h} * $self->{h} / 4;

  return 0;
}

sub Pop {
  my ($self, $guy, $popEffect) = @_;

  Carp::confess "no $popEffect" unless defined $popEffect;
  $::GameEvents{'pop'} = 1;
  $::GameEvents{'magic'} = 1 if ($self->{ismagic});
  $guy->GiveScore($self->{desc}->{score}) if $guy;
  
  goto skipChildren if ($popEffect eq 'meltdown');
  
  if ($self->{desc}->{nextgen}) {
    my @children = $self->SpawnChildren();
    if (scalar @children) {
      $self->AdjustChildren(@children);
      if ($popEffect eq 'HalfCutter') {
        push @::GameObjects, ($self->{speedX} > 0 ? $children[1] : $children[0]);
      } else {
        push @::GameObjects, (@children);
      }
    }
  }
  if ($self->{bonus} and $popEffect ne 'superkill') {
    push @::GameObjects, (new BonusDrop($self));
  }
  $::Game->OnBallPopped();
  
  skipChildren:
  push @::GameObjects, (new Pop($self->{x}, $self->{y}, $self->{desc}->{popIndex}, $self->{surface}));
  $self->Delete();
}

sub SpawnChildren {
  my $self = shift;
  my ($nextgen, $child1, $child2, $x, $y);

  $nextgen = $self->{desc}->{nextgen};
  $x = $self->{x} + $self->{w} / 2;
  $y = $self->{y} + ( $self->{h} - $nextgen->{height} ) / 2;

  $child1 = &Create($nextgen, $self->{x}, $y, 0);
  $child2 = &Create($nextgen, $self->{x} + $self->{w} - $nextgen->{width}, $y, 1);
  return ($child1, $child2);
}

sub AdjustChildren {
  my ($self, $child1, $child2) = @_;
  my ($nextgen, $speedY, $altitude);

  if ($self->{hasmagic}) {
    $child1->GiveMagic();
  }

  $nextgen = $self->{desc}->{nextgen};
  $altitude = $::ScreenHeight - $self->{y} - $self->{h};
  if ($altitude > $nextgen->{bounceY}) {
    $speedY = 1.8;
  } else {
    $speedY = 1.8;
    while ($speedY * $speedY / $Ball::Gravity / 2 + $altitude < $nextgen->{bounceY}) {
      ++$speedY;
    }
  }

  $child1->{speedY} = -$speedY;
  $child2->{speedY} = -$speedY;
}

sub GiveMagic {
  my $self = shift;

  $self->{hasmagic} = 1;
  $self->{ismagic} = 1 unless $self->{desc}->{nextgen};
}

sub GiveBonus {
  my $self = shift;

  $self->{bonus} = 1;
}



##########################################################################
package Hexa;
##########################################################################

@Hexa::ISA = qw(Ball);

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

  $self = new Ball(@_);
  $self->{speedX} = ($::Game->Rand(1.25) + 1.25) * ($self->{speedX} > 0 ? 1 : -1);
  $self->{speedY} = -4 + abs($self->{speedX});

  bless $self, $class;
}

sub Draw {
  my $self = shift;
  my ($rect, $srcx);

  return if $::GamePause > 0 and $::GamePause < 100 and (int($::GamePause / 3) % 4) < 2;
  
  $self->TransferRect();
  if ($self->{ismagic} and int($::Game->{anim} / 3) % 3 == 0) {
    $self->{surface}->blit($self->{desc}->{magicrect}, $::App, $self->{rect});
  } else {
    $rect = $self->{desc}->{rect};
    $srcx = (int($::Game->{anim} / 3) % 3) * $self->{w};
    $rect->x( $rect->x + $srcx );
    $self->{surface}->blit( $rect, $::App, $self->{rect} );
    $rect->x( $rect->x - $srcx );
  }
}

sub AdjustChildren {
  my ($self, $child1, $child2) = @_;
  if ($self->{hasmagic}) {
    $child2->GiveMagic();
  }
}


##########################################################################
package WaterBall;
##########################################################################

@WaterBall::ISA = qw( Ball );

sub Bounce {
  my $self = shift;
  if ($self->{desc}->{nextgen}) {
    $self->{bonus} = 0;
    $self->Pop(undef, '');
  }
}


##########################################################################
package SeekerBall;
##########################################################################

@SeekerBall::ISA = qw( Ball );

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

  $self = new Ball(@_);
  my @guys = grep {ref $_ eq 'Guy'} @::GameObjects;
  $self->{target} = $guys[$::Game->Rand(scalar @guys)];
  $self->{deltaX} = (-$self->{w} + $self->{target}->{w}) / 2;
  die unless $self->{target};

  bless $self, $class;
}

sub NormalAdvance {
  my $self = shift;
  
  my $multiplier = ($self->{y} > $::ScreenHeight - 120) ? 0 : 25;
  unless( $::GamePause > 0 ) {
    if ($self->{x} + $self->{speedX} * $multiplier > $self->{target}->{x} + $self->{deltaX}) {
      $self->{speedX} -= 0.08;
    } else {
      $self->{speedX} += 0.08;
    }
  }
  $self->SUPER::NormalAdvance();
}

sub AdjustChildren {
  my ($self, $child1, $child2) = @_;
  
  $self->SUPER::AdjustChildren($child1, $child2);
  $child1->{speedX} *= 2;
  $child1->{deltaX} -= 30;
  $child1->{target} = $self->{target};
  $child2->{speedX} *= 2;
  $child2->{deltaX} += 30;
  $child2->{target} = $self->{target};
}

sub GiveMagic {
}

sub Draw {
  my $self = shift;
  my ($guySurface, $srcrect, $dstrect);
  
  $self->SUPER::Draw();
  $guySurface = $self->{target}->{player}->{guySurface};
  if ($self->{w} <= 32) {
    $srcrect = new SDL::Rect(-width => 16, -height => 16, -x =>320, -y => 240);
  } else {
    $srcrect = new SDL::Rect(-width => 32, -height => 32, -x =>320, -y => 192);
  }
  $dstrect = new SDL::Rect(
    -x => $self->{x} + $::ScreenMargin + ($self->{w} - $srcrect->width()) / 2, 
    -y => $self->{y} + $::ScreenMargin + ($self->{h} - $srcrect->height()) / 2 + 2);
  $guySurface->blit($srcrect, $::App, $dstrect);
}


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

@SuperBall::ISA = qw(Ball);

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

  $self = new Ball(@_);
  $self->{effect} = 0;   # 0 : superpause;  1 : superkill
  $self->{surface} = $::BallSurfaces{green};

  bless $self, $class;
}

sub Bounce {
  my $self = shift;
  $self->{effect} = 1 - $self->{effect};
  $self->{surface} = $::BallSurfaces{$self->{effect} ? 'gold' : 'green'};
}

sub SpawnChildren {
  return ();
}

sub Pop {
  my $self = shift;
  my ($poppedBy) = @_;

  $self->SUPER::Pop(@_);
  if ($self->{effect} == 0) {
    $::GameEvents{superpause} = 1;
  } else {
    $::GameEvents{superkill} = 1;
    $::GameEvents{superkillguy} = $poppedBy;
  }
}

sub GiveMagic {
}


##########################################################################
package DeathBall;
##########################################################################

@DeathBall::ISA = qw(Ball);

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

  $self = new Ball(@_);
  $self->{expires} = 2000;   # 20sec
  $self->{speedX} *= 0.9;
  bless $self, $class;
}

sub NormalAdvance {
  my $self = shift;

  $self->SUPER::NormalAdvance();
  if (--$self->{expires} < 0) {
    $self->{bonus} = 1 if $self->{hasmagic};
    $self->Pop(undef, 'expire');
  }

}

sub Pop {
  my ($self, $guy, $popEffect) = @_;
  
  $self->{dontspawn} = 1 if $popEffect eq 'expire' or $popEffect eq 'superkill';
  $self->SUPER::Pop($guy, $popEffect);
  if (&CountDeathBalls() > 30) {
    $::GameEvents{'meltdown'} = 1;
  }
}

sub SpawnChildren {
  my $self = shift;

  return if $self->{dontspawn};
  $self->SUPER::SpawnChildren(@_);
}

sub CountDeathBalls {
  my $count = 0;

  foreach my $ball (@::GameObjects) {
    if (ref($ball) eq 'DeathBall') { ++$count; }
  }
  return $count;
}


##########################################################################
package EarthquakeBall;
##########################################################################

@EarthquakeBall::ISA = qw(Ball);

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

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

sub CountEarthquakeBalls {
  my $count = 0;

  foreach my $ball (@::GameObjects) {
    if (ref($ball) eq 'EarthquakeBall') { ++$count; }
  }
  return $count;
}

sub Bounce {
  my $self = shift;

  unless ($::GameEvents{earthquake} and $::GameEvents{earthquake} > $self->{desc}->{quake}) {
    $::GameEvents{earthquake} = [$self->{desc}->{quake}, $self->{x}];
  }
}


##########################################################################
package Pop;
##########################################################################

@Pop::ISA = qw(GameObject);

@Pop::Description = (
  { 'xoffset' => 0, 'yoffset' => 12, 'srcx' => 256, 'srcy' =>   0, 'size' => 128, },
  { 'xoffset' => 0, 'yoffset' =>  8, 'srcx' => 256, 'srcy' => 128, 'size' =>  96, },
  { 'xoffset' => 0, 'yoffset' =>  6, 'srcx' => 256, 'srcy' => 224, 'size' =>  64, },
  { 'xoffset' => 0, 'yoffset' =>  2, 'srcx' => 512, 'srcy' => 224, 'size' =>  32, },
  { 'xoffset' =>-8, 'yoffset' => -8, 'srcx' => 512, 'srcy' => 256, 'size' =>  32, },

  { 'xoffset' => 0, 'yoffset' => -6, 'srcx' =>   0, 'srcy' =>  96, 'size' =>  64, },
  { 'xoffset' => 0, 'yoffset' => -4, 'srcx' =>   0, 'srcy' => 160, 'size' =>  32, },
  { 'xoffset' =>-8, 'yoffset' => -6, 'srcx' =>   0, 'srcy' => 192, 'size' =>  32, },
);

sub new {
  my ($class, $x, $y, $index, $surface) = @_;
  my ($self, $desc);

  $desc = $Pop::Description[$index],
  $self = new GameObject;
  %{$self} = ( %{$self},
    'x' => $x + $desc->{xoffset},
    'y' => $y + $desc->{yoffset},
    'w' => $desc->{size},
    'h' => $desc->{size},
    'desc' => $desc,
    'anim' => 0,
    'surface' => $surface,
  );
  bless $self, $class;
}

sub Advance {
  my $self = shift;

  ++$self->{anim};
  if ($self->{anim} >= 20) {
    $self->Delete();
  }
}

sub Draw {
  my $self = shift;
  my ($phase, $srcrect);

  $self->TransferRect();
  $phase = int($self->{anim} / 5);
  $phase = 3 if $phase > 3;

  $srcrect = new SDL::Rect(
    -x => $self->{desc}->{srcx} + $phase * $self->{w},
    -y => $self->{desc}->{srcy},
    -width => $self->{w},
    -height => $self->{h} );
  $self->{surface}->blit( $srcrect, $::App, $self->{rect} );
}


##########################################################################
package GamePause;
##########################################################################

@GamePause::ISA = qw(GameObject);

sub Show {
  foreach my $gameObject (@::GameObjects) {
    return if (ref $gameObject eq 'GamePause');
  }
  push @::GameObjects, (new GamePause);
}

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

  my $self = new GameObject;
  my ($width);
  $width = &::TextWidth("Time left: 9.999");

  %{$self} = ( %{$self},
    'x' => ($::PhysicalScreenWidth - $width) / 2,
    'y' => 100,
    'w' => $width,
    'h' => 32,
  );
  $self->TransferRect();
  bless $self, $class;
}

sub BringToFront {
  my $self = shift;

  @::GameObjects = grep { $_ ne $self } @::GameObjects;
  push @::GameObjects, ($self);
}

sub Advance {
  my $self = shift;

  if ($::GamePause <= 0) {
    $self->Delete;
    return;
  }
  unless ($::GameObjects[$#::GameObjects] eq $self) {
    $self->BringToFront();
  }
}

sub Draw {
  my $self = shift;

  $::App->print( $self->{rect}->x, $self->{rect}->y, "Time left: " . ($::GamePause / 100) );
}


##########################################################################
package FpsIndicator;
##########################################################################

@FpsIndicator::ISA = qw(GameObject);

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

  my $self = new GameObject;
  my ($width);
  $width = &::TextWidth("999");

  %{$self} = ( %{$self},
    'x' => $::ScreenWidth - $width + $::ScreenMargin,
    'y' => -$::ScreenMargin,
    'w' => $width,
    'h' => 32,
  );
  $self->TransferRect();
  bless $self, $class;
}

sub Draw {
  my $self = shift;

  $::App->print( $self->{rect}->x, $self->{rect}->y, &GameTimer::GetFramesPerSecond() );
}


##########################################################################
package BonusDrop;
##########################################################################

@BonusDrop::ISA = qw(GameObject);
use vars qw(@BonusDesc);

@BonusDesc = (
  { 'weaponClass' => 'MachineGun', 'bonusDelay' => 1500, 'srcRect' => new SDL::Rect(-width=>32, -height=>32, -x=>0 , -y=>64), },
  { 'weaponClass' => 'HalfCutter', 'bonusDelay' => 1000, 'srcRect' => new SDL::Rect(-width=>32, -height=>32, -x=>32, -y=>64), },
  { 'weaponClass' => 'PowerWire',  'bonusDelay' => 3000, 'srcRect' => new SDL::Rect(-width=>32, -height=>32, -x=>32, -y=>96), },
);


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

  $self = new GameObject;

  %{$self} = ( %{$self},
    'x' => $ball->{x} + ($ball->{w} - 32) / 2,
    'y' => $ball->{y} + ($ball->{h} - 32) / 2,
    'w' => 32,
    'h' => 32,
    'speedY' => -3,
    'speedX' => 0,
    'bottomDelay' => 500,
    'desc' => $BonusDesc[int $::Game->Rand(scalar @BonusDesc)],
    'anim' => 0,
  );
  bless $self, $class;
}

sub Advance {
  my $self = shift;

  if ($self->{y} >= $::ScreenHeight - $self->{h}) {
    $self->{y} = $::ScreenHeight - $self->{h};
    if (--$self->{bottomDelay} < 0) {
      $self->Delete();
    }
  } else {
    $self->{speedY} += 0.1;
    $self->{y} += $self->{speedY};
  }

  $self->CheckCollisions() if $self->{speedY} >= 0;
}

sub CheckCollisions {
  my $self = shift;
  my ($guy, @guysTouched);

  foreach $guy (@::GameObjects) {
    next unless ref($guy) eq 'Guy';
    next unless $self->Collisions($guy);
    push @guysTouched, ($guy);
  }
  return unless @guysTouched;
  $self->Collected($guysTouched[$::Game->Rand( scalar @guysTouched )]);
}

sub Collected {
  my ($self, $guy) = @_;

  $guy->{weapon} = $self->{desc}->{weaponClass};
  $guy->{bonusDelay} = $self->{desc}->{bonusDelay};
  $self->Delete();
}

sub Draw {
  my $self = shift;

  return if $self->{bottomDelay} < 100 and (($::Game->{anim} / 4) % 2 < 1);
  $self->TransferRect();
  $::BonusSurface->blit($self->{desc}->{srcRect}, $::App, $self->{rect});
}


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

@Guy::ISA = qw(GameObject);

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

  $self = new GameObject;
  $number = $player->{number};

  %{$self} = ( %{$self},
    'player' => $player,
    'number' => $number,
    'x' => $player->{startX},
    'y' => $::ScreenHeight - 64,
    'w' => 64,
    'h' => 64,
    'collisionw' => '28',
    'collisionh' => '48',
    'delay' => 0,
    'speedY' => 0,
    'speedX' => 0,
    'dir' => $number % 2,
    'state' => 'idle',
    'killed' => 0,
    'harpoons' => 0,
    'invincible' => 0,
    'surface' => $player->{guySurface},
    'whiteSurface' => $player->{whiteGuySurface},
    'weapon' => 'Harpoon',
    'bonusDelay' => 0,
  );
  $self->SetupCollisions();
  bless $self, $class;
}

sub DemoMode {
  my ($self) = shift;
  $self->{state} = 'demo';
  $self->{dir} = 1;
}

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

  if ($self->{harpoons} < $::DifficultyLevel->{harpoons}) {
    ++$self->{harpoons};
    eval("unshift \@::GameObjects, ($self->{weapon}::Create(\$self));");
    $self->{state} = 'shoot';
    $self->{delay} = 7;
    ::PlaySound('shoot');
    return 1;
  }
  return 0;
}

sub AdvanceWhileFlying {
  my $self = shift;

  $self->{speedY} += $Ball::Gravity * 2;
  $self->{y} += $self->{speedY};
  $self->{x} += $self->{dir} > 0 ? 1 : -1;
  if ($self->{x} < -16) {
    $self->{x} = 0; $self->{dir} = 1;
  }
  if ($self->{x} > $::ScreenWidth - $self->{w} + 16) {
    $self->{x} = $::ScreenWidth - $self->{w}; $self->{dir} = 0;
  }
  if ($self->{y} >= $::ScreenHeight - $self->{h}) {
    $self->{state} = 'idle';
    $self->{y} = $::ScreenHeight - $self->{h};
  }
}

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

   return if $self->{killed};
   return if $self->{state} eq 'demo';
  --$self->{invincible};

  if ($self->{bonusDelay} > 0) {
    --$self->{bonusDelay};
    $self->{weapon} = 'Harpoon' if $self->{bonusDelay} <= 0;
  }

  if ($self->{delay} > 0) {
    --$self->{delay};
    return;
  }

  if ($self->{state} eq 'fly') {
    $self->AdvanceWhileFlying();
    return;
  }

  $self->{speedX} = 0;
  $self->{state} = 'idle';
  my $keys = $self->{player}->{keys};

  if ( $::Events{$keys->[2]} ) {
    return if $self->Fire();
  }
  if ( $::Keys{$keys->[0]} ) {
    $self->{speedX} = -3;
    $self->{dir} = 0;
    $self->{state} = 'walk';
  } elsif ( $::Keys{$keys->[1]} ) {
    $self->{speedX} = 3;
    $self->{dir} = 1;
    $self->{state} = 'walk';
  }
  $self->{x} += $self->{speedX};

  if ($self->{x} < -16) {
    $self->{x} = -16;
  }
  if ($self->{x} > $::ScreenWidth - $self->{w} + 16) {
    $self->{x} = $::ScreenWidth - $self->{w} + 16;
  }
}

sub Draw {
  my ($self) = @_;
  my ($surface, $srcrect, $srcx, $srcy, $srcw, $srch);

  return if ($self->{killed});
  $surface = $self->{surface};
  $surface = $self->{whiteSurface} if $self->{invincible} > 0 and (int($self->{invincible} / 2) % 3 == 0);

  $srcw = $srch = 64;
  if ($self->{state} eq 'idle') {
    $srcx = $self->{dir} * 128;
    $srcy = 64;
  } elsif ($self->{state} eq 'walk') {
    $srcx = $self->{dir} * 256 + (int($self->{x} / 50) % 4) * 64;
    $srcy = 0;
  } elsif ($self->{state} eq 'demo') {
    $srcx = $self->{dir} * 256 + (int($::Game->{anim} / 16) % 4) * 64;
    $srcy = 0;
  } elsif ($self->{state} eq 'shoot') {
    $srcx = $self->{dir} * 128 + 64;
    $srcx -= 64 if ($self->{delay} <= 1);
    $srcy = 64;
  } elsif ($self->{state} eq 'fly') {
    $srcx = ($self->{number} % 2) * 192 + ($self->{dir} > 0 ? 0 : 64);
    $srcy = ($self->{number} % 2) * 64 + 128;
  }
  $srcrect = new SDL::Rect( -width => $srcw, -height => $srch, -x => $srcx, -y => $srcy );
  $self->TransferRect();
  $surface->blit($srcrect, $::App, $self->{rect});
}

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

  return if $::Cheat;
  return if $self->{invincible} > 0;
  $self->{justkilled} = 1;
  $::GameEvents{'kill'} = 1;
}

sub Earthquake() {
  my ($self, $amplitude) = @_;

  return if $self->{state} eq 'fly';
  $self->{speedY} = -($amplitude->[0]);
  $self->{dir} = $amplitude->[1] > $self->{x} ? 0 : 1;
  $self->{state} = 'fly';
  $self->{y} -= 3;
}

sub DeleteHarpoons {
  my ($self) = @_;
  my (@gameObjects, $harpoon);

  @gameObjects = @::GameObjects;
  foreach $harpoon (@gameObjects) {
    $harpoon->Delete if ($harpoon->{guy} and $harpoon->{guy} eq $self);
  }
}

sub GiveScore {
  my ($self, $score) = @_;

  my $player = $self->{player};
  $player->{score} += $score;
  if ($player->{score} > $player->{scoreforbonuslife}) {
    ++$player->{lives};
    $player->{scoreforbonuslife} += 200000;
    &::PlaySound('bonuslife');
  }
}


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

@Harpoon::ISA = qw(GameObject);

sub Create {
  return new Harpoon(@_);
}

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

  $self = new GameObject;
  %{$self} = ( %{$self},
    'x' => $guy->{x} + 22,
    'y' => $::ScreenHeight - 32,
    'w' => 18,
    'h' => 32,
    'speedY' => -3,
    'speedX' => 0,
    'guy' => $guy,
    'surface' => $guy->{player}->{harpoonSurface},
    'popEffect' => '',
  );
  bless $self, $class;
}

sub Delete {
  my $self = shift;

  --$self->{guy}->{harpoons};
  $self->SUPER::Delete();
}

sub Advance {
  my $self = shift;

  if ($self->{y} < 0) {
    $self->Delete();
    return;
  }
  $self->{y} += $self->{speedY};
  $self->{h} = $::ScreenHeight - $self->{y};
}

sub GetAnimPhase {
  my $self = shift;

  return (int($::Game->{anim} / 4) % 3) + 1;
}

sub Draw {
  my $self = shift;
  my ($x, $y, $h, $maxh, $dstrect, $srcrect);

  $self->TransferRect();
  $y = $self->{y};
  $dstrect = new SDL::Rect( -w => $self->{w}, -x => $self->{x} + $::ScreenMargin );
  $srcrect = new SDL::Rect( -w => $self->{w},
    -x => (0, 64, 32, 96)[ $self->GetAnimPhase() ], -y => 0 );
  $maxh = 160;

  # The harpoon needs to be drawn from tile pieces.
  # $y iterates from $self->{y} to $::ScreenHeight
  # We draw at most $maxh height tiles at a time.

  while ($y < $::ScreenHeight) {
    $h = $::ScreenHeight - $y;
    $h = $maxh if $h > $maxh;
    $dstrect->y( $y + $::ScreenMargin );
    $dstrect->height( $h );
    $srcrect->height( $h );
    $self->{surface}->blit( $srcrect, $::App, $dstrect );

    # Prepare for next piece
    $y += $h;
    $srcrect->y( 32 );      # First piece starts at 0, rest start at 32
    $maxh = 128;
  }
}


##########################################################################
package MachineGun;
##########################################################################

@MachineGun::ISA = qw(Harpoon);
use vars qw(@SrcRects);

@SrcRects = (
  new SDL::Rect( -x=>0, -y=>160, -w=>32, -h=>32 ),
  new SDL::Rect( -x=>32, -y=>160, -w=>32, -h=>32 ),
  new SDL::Rect( -x=>64, -y=>160, -w=>32, -h=>32 ),
);

sub Create {
  return ( new MachineGun(@_, 0), new MachineGun(@_, 1), new MachineGun(@_, 2) );
}

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

  $self = new GameObject;
  %{$self} = ( %{$self},
    'x' => $guy->{x} + 16,
    'y' => $guy->{y} - 16,
    'w' => 32,
    'h' => 32,
    'index' => $index,
    'speedY' => -9,
    'speedX' => (-2, 0, 2)[$index],
    'guy' => $guy,
    'surface' => $guy->{player}->{harpoonSurface},
    'popEffect' => '',
  );
  bless $self, $class;
}

sub Delete {
  my $self = shift;

  --$self->{guy}->{harpoons} if $self->{index} == 1;
  $self->GameObject::Delete();
}

sub Advance {
  my $self = shift;

  if ($self->{y} < 0
    or $self->{x} < 0
    or $self->{x} > $::ScreenWidth - $self->{w}) {
    $self->Delete();
    return;
  }
  $self->{y} += $self->{speedY};
  $self->{x} += $self->{speedX};
}

sub Draw {
  my $self = shift;

  $self->TransferRect();
  $self->{surface}->blit($SrcRects[$self->{index}], $::App, $self->{rect});
}

##########################################################################
package PowerWire;
##########################################################################

@PowerWire::ISA = qw(Harpoon);

sub Create {
  return new PowerWire(@_);
}

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

  $self = new Harpoon(@_);
  %{$self} = ( %{$self},
    'topdelay' => 200,
  );
  bless $self, $class;
}

sub Advance {
  my $self = shift;

  if ($self->{y} > 0) {
    return $self->SUPER::Advance();
  }
  $self->{y} = 0;
  --$self->{topdelay};
  if ($self->{topdelay} <= 0) {
    $self->Delete();
  }
}

sub GetAnimPhase {
  my $self = shift;

  if ($self->{y} <= 0) {
    return 0;
  }
  return $self->SUPER::GetAnimPhase();
}


##########################################################################
package HalfCutter;
##########################################################################

@HalfCutter::ISA = qw(Harpoon);

sub Create {
  return new HalfCutter(@_);
}

sub new {
  my $class = shift;
  my $self = new Harpoon(@_);
  $self->{popEffect} = 'HalfCutter';
  $self->{originalSurface} = $self->{surface};
  bless $self, $class;
}

sub Advance {
  my $self = shift;

  $self->{surface} = (($::Game->{anim} % 15) < 3) ? $::WhiteHarpoonSurface : $self->{originalSurface};
  $self->SUPER::Advance();
}


##########################################################################
package DeadGuy;
##########################################################################

@DeadGuy::ISA = qw(GameObject);

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

  $self = new GameObject;
  $number = $guy->{number};
  $player = $guy->{player};

  %{$self} = ( %{$self},
    'number' => $number,
    'x' => $guy->{x},
    'y' => $guy->{y},
    'w' => 64,
    'h' => 64,
    'speedY' => -7,
    'surface' => $player->{guySurface},
    'anim' => 0,
    'bounce' => 0,
    'bouncex' => 0,
  );
  $self->{'speedX'} = ($::Game->Rand(2) + 1.5) * (($self->{x} > $::ScreenWidth / 2) ? 1 : -1);
  bless $self, $class;
}

sub Advance {
  my $self = shift;

  $self->{speedY} += 0.1;
  $self->{x} += $self->{speedX};
  $self->{y} += $self->{speedY};
  
  unless ($self->{bouncex}) {
    if ($self->{x} < -16) {
      $self->{x} = -16;
      $self->{speedX} = abs( $self->{speedX} );
      $self->{speedY} = -3 if $self->{speedY} > -3;
      $self->{bouncex} = 1;
    }
    if ($self->{x} > $::ScreenWidth - $self->{w} +16) {
      $self->{x} = $::ScreenWidth - $self->{w} + 16;
      $self->{speedX} = -abs( $self->{speedX} );
      $self->{speedY} = -3 if $self->{speedY} > -3;
      $self->{bouncex} = 1;
    }
  }
  if ($self->{y} > $::ScreenHeight - 64 and not $self->{bounce}) {
    $self->{bounce} = 1;
    $self->{speedY} = -3;
  }

  if ($self->{y} > $::PhysicalScreenHeight) {
    $self->Delete;
  }
  $self->{anim} += $self->{speedX} > 0 ? -1 : +1;
}

sub Draw {
  my $self = shift;
  my ($srcrect);

  $srcrect = new SDL::Rect(
    -x => ($self->{number} % 2) * 192 + ($self->{speedX} > 0 ? 0 : 64),
    -y => ($self->{number} % 2) * 64 + 128,
    -width => 64, -height => 64 );
  $self->TransferRect();
  if ($::RotoZoomer) {
  	my $roto = new SDL::Surface( -name =>'',
        -flags=>::SDL_SWSURFACE(), -width => 64, -height => 64, -depth => 32);
    $self->{surface}->blit( $srcrect, $roto, new SDL::Rect );
    $::RotoZoomer->rotoZoom( $roto, $self->{anim} * 5, 1, 0 );
    $self->{rect}->x( $self->{rect}->x - ($roto->width - 64) / 2 );
    $self->{rect}->y( $self->{rect}->y - ($roto->height - 64) / 2 );
    $roto->blit( 0, $::App, $self->{rect} );
    return;
  } else {
    $self->{surface}->blit( $srcrect, $::App, $self->{rect} );
  }
}


##########################################################################
package Meltdown;
##########################################################################

@Meltdown::ISA = qw(GameObject);

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

  $self = new GameObject;
  $surface = new SDL::Surface( -name => "$::DataDir/meltdown.png" );
  %{$self} = ( %{$self},
    'x' => ($::ScreenWidth - $surface->width) / 2,
    'y' => -$surface->height,
    'w' => $surface->width,
    'h' => $surface->height,
    'speedY' => 0,
    'surface' => $surface,
    'bounce' => 0,
  );
  bless $self, $class;
}

sub Advance {
  my $self = shift;
  $self->{speedY} += 0.1;
  $self->{y} += $self->{speedY};
  if ($self->{bounce} == 0 and $self->{y} > $::ScreenHeight - $self->{h}) {
    $self->{bounce} = 1;
    $self->{speedY} = -5;
    $self->{y} = $::ScreenHeight - $self->{h};
  }
  if ($self->{bounce} and $self->{y} > $::PhysicalScreenHeight) {
    $self->Delete;
  }
}

sub Draw {
  my $self = shift;
  
  $self->TransferRect();
  $self->{surface}->blit( 0, $::App, $self->{rect} );
}


##########################################################################
package MenuItem;
##########################################################################

@MenuItem::ISA = qw(GameObject);
use vars qw($Gravity);
$Gravity = 0.2;

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

  $self = new GameObject;

  %{$self} = ( %{$self},
    'targetX' => $x,
    'targetY' => $y,
    'h' => 42,
    'selected' => 0,
    'filled' => 0,
    'fillcolor' => new SDL::Color(-b=>128),
    'parameter' => 0,
  );
  bless $self, $class;
  $self->SetText($text);
  $self->SetInitialSpeed();
  return $self;
}

sub Center {
  my $self = shift;
  
  $self->{targetX} = ( $::ScreenWidth - $self->{w} ) / 2;
}

sub Show {
  my $self = shift;
  return if $self->{state} eq 'shown';
  $self->SetInitialSpeed();
}

sub Hide {
  my $self = shift;
  $self->SUPER::Clear();
  $self->{state} = 'leaving';
  $self->{speedX} = rand(10) - 5;
}

sub HideAndDelete {
  my $self = shift;
  $self->Hide();
  $self->{deleteAfterHiding} = 1;
}

sub Delete {
  my $self = shift;
  $self->{selected} = $self->{filled} = 0;
  $self->SUPER::Delete();
}

sub ApproachingSpeed {
  my ($position, $speed, $target) = @_;
    
  if ($position + $speed * abs($speed / $Gravity) / 2 > $target) {
    return $speed - $Gravity;
  } else {
    return $speed + $Gravity;
  }
}

sub Advance {
  my $self = shift;
  
  if ('entering' eq $self->{state}) {
    $self->{x} += $self->{speedX};
    $self->{y} += $self->{speedY};
    $self->{speedX} = &ApproachingSpeed($self->{x}, $self->{speedX}, $self->{targetX});
    $self->{speedY} = &ApproachingSpeed($self->{y}, $self->{speedY}, $self->{targetY});
    if ( abs($self->{x} - $self->{targetX}) + abs($self->{y} - $self->{targetY}) < 2 ) {
      $self->{x} = $self->{targetX};
      $self->{y} = $self->{targetY};
      $self->{state} = 'shown';
    }
  } elsif ('leaving' eq $self->{state}) {
    $self->{x} += $self->{speedX};
    $self->{y} += $self->{speedY};
    $self->{speedY} += $Gravity;
    if ($self->{y} > $::PhysicalScreenWidth) {
      $self->{state} = 'hidden';
      $self->Delete() if $self->{deleteAfterHiding}
    }
  }
}

sub Draw {
  my $self = shift;

  return if $self->{state} eq 'hidden';
  $self->TransferRect();
  if ($self->{selected} or $self->{filled}) {
    $::App->fill($self->{rect}, $self->{fillcolor});
  }
  $::App->print($self->{x} + 5 +$::ScreenMargin, $self->{y} + $::ScreenMargin, $self->{text});
}

sub SetInitialSpeed {
  my $self = shift;
  
  $self->{x} = $self->{targetX} + rand(500) - 250;
  $self->{y} = $::PhysicalScreenHeight;
  $self->{speedY} = -sqrt( 2 * $Gravity * ($self->{y} - $self->{targetY}) );
  $self->{speedX} = 0;
  $self->{state} = 'entering';
}

sub InternalSetText {
  my ($self, $text) = @_;
  
  $self->SUPER::Clear();
  $self->{text} = $text;
  $self->{w} = &::TextWidth($text) + 10;
}

sub SetText {
  my ($self, $text) = @_;

  $self->{parameter} = '';
  $self->{basetext} = $text;
  $self->InternalSetText($text);
}

sub SetParameter {
  my ($self, $parameter) = @_;
  
  $self->{parameter} = $parameter;
  $self->InternalSetText($self->{basetext} . ' ' . $parameter);
}

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

  foreach my $item (@::GameObjects) {
    $item->{selected} = 0 if ref $item eq 'MenuItem';
  }
  $self->{selected} = 1;
}



##########################################################################
package GameTimer;
##########################################################################

use vars qw($FirstTick $LastTick $TotalAdvances $LastFpsTick $LastFps $Fps);

sub ResetTimer {
  $FirstTick = $::App->ticks;
  $LastTick = $LastFpsTick = $FirstTick;
  $TotalAdvances = 0;
  $Fps = $LastFps = 0;
}

sub GetAdvances {
  my ($ticks, $advance);

  $ticks = $::App->ticks;
  $advance = int(($ticks - $FirstTick) / 10) - $TotalAdvances;
  $TotalAdvances += $advance;
  
  # Calculate frames per second;
  ++$Fps if $advance > 0;
  if ($ticks - $LastFpsTick > 1000) {
    $LastFps = $Fps;
    $LastFpsTick = $ticks;
    $Fps = 0;
  }
  
  return $advance;
}

sub GetFramesPerSecond {
  return $LastFps;
}


##########################################################################
package Joystick;
##########################################################################

use vars qw(@Joysticks @JoystickButtons);

sub InitJoystick {
  my ($numJoysticks, $joystick, $numButtons, $i);

  $numJoysticks = &SDL::NumJoysticks();
  for ($i = 0; $i < $numJoysticks; ++$i) {
    print STDERR "Found joystick " , $i+1 , ": " , &SDL::JoystickName($i), "\n";
    $joystick = &SDL::JoystickOpen($i);
    next unless $joystick;
    $numButtons = &SDL::JoystickNumButtons($joystick);
    next unless $numButtons;
    push @Joysticks, $joystick;
    push @JoystickButtons, $numButtons;
    print STDERR "Joystick opened, $numButtons buttons.\n";
  }
}

sub ReadJoystick {
  my ($readBothAxes) = @_;
  my ($i, $button, $buttonPressed);

  $i = 0;
  foreach my $joystick (@Joysticks) {
    ++$i;
    my $axis = &SDL::JoystickGetAxis($joystick, 0);
    if ($axis <= -10000) {
      $::Events{"L$i"} = $::MenuEvents{LEFT} = 1 unless $::Keys{"L$i"};
      $::Keys{"L$i"} = 1;
      $::Keys{"R$i"} = 0;
    } elsif ($axis >= 10000) {
      $::Events{"R$i"} = $::MenuEvents{RIGHT} = 1 unless $::Keys{"R$i"};
      $::Keys{"R$i"} = 1;
      $::Keys{"L$i"} = 0;
    } else {
      $::Keys{"L$i"} = 0;
      $::Keys{"R$i"} = 0;
    }
    if ($readBothAxes) {
      $axis = &SDL::JoystickGetAxis($joystick, 1);
      if ($axis <= -10000) {
        $::Events{"U$i"} = $::MenuEvents{UP} = 1 unless $::Keys{"U$i"};
        $::Keys{"U$i"} = 1;
        $::Keys{"D$i"} = 0;
      } elsif ($axis >= 10000) {
        $::Events{"D$i"} = $::MenuEvents{DOWN} = 1 unless $::Keys{"D$i"};
        $::Keys{"D$i"} = 1;
        $::Keys{"U$i"} = 0;
      } else {
        $::Keys{"D$i"} = 0;
        $::Keys{"U$i"} = 0;
      }
    }
    $buttonPressed = 0;
    for ($button = 0; $button < $JoystickButtons[$i-1]; ++$button) {
      if (&SDL::JoystickGetButton($joystick, $button)) {
        $buttonPressed = 1; last;
      }
    }
    if ($buttonPressed and not $::Keys{"B$i"}) {
        $::Events{"B$i"} = $::MenuEvents{BUTTON} = 1;
    }
    $::Keys{"B$i"} = $buttonPressed;
  }
}


##########################################################################
# PALETTE MANIPULATION
##########################################################################

package main;

sub RgbToHsi {
  my ($r, $g, $b) = @_;
  my ($min, $max, $delta, $h, $s, $i);

  if ($r > $g) {
    $max = $r > $b ? $r : $b;
    $min = $g < $b ? $g : $b;
  } else {
    $max = $g > $b ? $g : $b;
    $min = $r < $b ? $r : $b;
  }
  $i = ($min + $max) / 2;
  if ($min == $max) {
    return (0, 0, $i);
  }

  $delta = ($max - $min);
  if ($i < 128) {
    $s = 255 * $delta / ($min + $max);
  } else {
    $s = 255 * $delta / (511 - $min - $max);
  }

  if ($r == $max) {
    $h = ($g - $b) / $delta;
  } elsif ($g == $max) {
    $h = 2 + ($b - $r) / $delta;
  } else {
    $h = 4 + ($r - $g) / $delta;
  }
  $h = $h * 42.5;
  $h += 255 if $h < 0;
  $h -= 255 if $h > 255;

  return ($h, $s, $i);
}

sub HsiToRgb {
  my ($h, $s, $i) = @_;
  my ($m1, $m2);

  if ($s < 1) {
    $i = int($i + 0.5);
    return ($i, $i, $i);
  }

  if ($i < 128) {
    $m2 = ($i * (255 + $s)) / 65025.0;
  } else {
    $m2 = ($i + $s - ($i * $s) / 255.0) / 255.0;
  }
  $m1 = ($i / 127.5) - $m2;

  return (
    &GetHsiValue( $m1, $m2, $h + 85),
    &GetHsiValue( $m1, $m2, $h),
    &GetHsiValue( $m1, $m2, $h - 85)
  );
}

sub GetHsiValue {
  my ($n1, $n2, $hue) = @_;
  my ($value);

  $hue -= 255 if ($hue > 255);
  $hue += 255 if ($hue < 0);
  if ($hue < 42.5) {
    $value = $n1 + ($n2 - $n1) * ($hue / 42.5);
  } elsif ($hue < 127.5) {
    $value = $n2;
  } elsif ($hue < 170) {
    $value = $n1 + ($n2 - $n1) * ((170 - $hue) / 42.5);
  } else {
    $value = $n1;
  }
  return int($value * 255 + 0.5);
}

##########################################################################
# GRAPHICS-RELATED SUBS
##########################################################################

sub LoadSurfaces {
  my ($i);

  foreach $i (qw(ball darkball blue darkblue green darkgreen gold death quake darkquake darkwhite white)) {
    $BallSurfaces{$i} = new SDL::Surface( -name => "$DataDir/ball.png" );
  }
  foreach $i (qw(hexa darkhexa)) {
    $BallSurfaces{$i} = new SDL::Surface( -name => "$DataDir/hexa.png" );
  }
  $BallSurfaces{darkdeath} = $BallSurfaces{death};
  $BorderSurface = new SDL::Surface( -name => "$DataDir/border.png" );
  $RedBorderSurface = new SDL::Surface( -name => "$DataDir/border.png" );
  $WhiteBorderSurface = new SDL::Surface( -name => "$DataDir/border.png" );
  $BonusSurface = new SDL::Surface( -name => "$DataDir/bonus.png" );
  $LevelIndicatorSurface = new SDL::Surface( -name => "$DataDir/level.png" );
  $LevelIndicatorSurface2 = new SDL::Surface( -name => "$DataDir/level_empty.png" );

  my $srcrect = new SDL::Rect(-w=>32, -h=>32);
  $BonusSurface->blit($srcrect, $BallSurfaces{green}, new SDL::Rect(-x=>48, -y=>48));
  $BonusSurface->blit($srcrect, $BallSurfaces{green}, new SDL::Rect(-x=>160, -y=>32));
  $BonusSurface->blit($srcrect, $BallSurfaces{green}, new SDL::Rect(-x=>16, -y=>144));
  $BonusSurface->blit($srcrect, $BallSurfaces{darkgreen}, new SDL::Rect(-x=>48, -y=>48));
  $BonusSurface->blit($srcrect, $BallSurfaces{darkgreen}, new SDL::Rect(-x=>160, -y=>32));
  $BonusSurface->blit($srcrect, $BallSurfaces{darkgreen}, new SDL::Rect(-x=>16, -y=>144));
  $srcrect->x(32);
  $BonusSurface->blit($srcrect, $BallSurfaces{gold}, new SDL::Rect(-x=>48, -y=>48));
  $BonusSurface->blit($srcrect, $BallSurfaces{gold}, new SDL::Rect(-x=>160, -y=>32));
  $BonusSurface->blit($srcrect, $BallSurfaces{gold}, new SDL::Rect(-x=>16, -y=>144));
  $srcrect->y(32);
  $BonusSurface->blit($srcrect, $BallSurfaces{quake}, new SDL::Rect(-x=>16, -y=>144));
  $BonusSurface->blit($srcrect, $BallSurfaces{quake}, new SDL::Rect(-x=>224, -y=>0));
  $BonusSurface->blit($srcrect, $BallSurfaces{darkquake}, new SDL::Rect(-x=>16, -y=>144));
  $BonusSurface->blit($srcrect, $BallSurfaces{darkquake}, new SDL::Rect(-x=>224, -y=>0));
  $srcrect->x(0);
  $BonusSurface->blit($srcrect, $BallSurfaces{death}, new SDL::Rect(-x=>16, -y=>144));

  &AlterPalette( $BallSurfaces{darkball}, sub { 1; }, sub { return $_[1] * 2 / 3, $_[2] * 2 / 3, $_[3] * 2 / 3; } );
  &AlterPalette( $BallSurfaces{quake}, sub { 1; },
    sub { shift @_; my ($h, $s, $i) = &RgbToHsi(@_); return &HsiToRgb( 25, $s, $i * 0.75 + 63 ); } );
  &AlterPalette( $BallSurfaces{darkquake}, sub { 1; },
    sub { shift @_; my ($h, $s, $i) = &RgbToHsi(@_); my ($r, $g, $b) = &HsiToRgb( 25, $s, $i * 0.75 + 63 ); return ($r*2/3, $g*2/3, $b*2/3); } );
  &AlterPalette( $BallSurfaces{darkhexa}, sub { 1; }, sub { return $_[1] * 2 / 3, $_[2] * 2 / 3, $_[3] * 2 / 3; } );
  &AlterPalette( $BallSurfaces{blue}, sub { 1; },
    sub { shift @_; my ($h, $s, $i) = &RgbToHsi(@_); return &HsiToRgb( 170, $s, $i ); } );
  &AlterPalette( $BallSurfaces{darkblue}, sub { 1; },
    sub { shift @_; my ($h, $s, $i) = &RgbToHsi(@_); my ($r,$g,$b) = &HsiToRgb( 170, $s, $i); return ($r*2/3, $g*2/3, $b*2/3); } );
  &AlterPalette( $BallSurfaces{green}, sub { 1; },
    sub { shift @_; my ($h, $s, $i) = &RgbToHsi(@_); return &HsiToRgb( 85, $s, $i ); } );
  &AlterPalette( $BallSurfaces{darkgreen}, sub { 1; },
    sub { shift @_; my ($h, $s, $i) = &RgbToHsi(@_); my ($r,$g,$b) = &HsiToRgb( 85, $s, $i); return ($r*2/3, $g*2/3, $b*2/3); } );
  &AlterPalette( $BallSurfaces{gold}, sub { 1; },
    sub { shift @_; my ($h, $s, $i) = &RgbToHsi(@_); return &HsiToRgb( 35, 255, $i ); } );
  &AlterPalette( $BallSurfaces{death}, sub { 1; },
    sub { my ($i) = int($_[1] + $_[2] + $_[3]) / 6; return ($i, $i, $i) } );
  &AlterPalette( $BallSurfaces{white}, sub { 1; },
    sub { my ($i) = int($_[1] + $_[2] + $_[3]) / 12 + 191; return ($i, $i, $i) } );
  &AlterPalette( $BallSurfaces{darkwhite}, sub { 1; },
    sub { my ($i) = int($_[1] + $_[2] + $_[3]) / 6 + 64; return ($i, $i, $i) } );
  &AlterPalette( $RedBorderSurface, sub { 1; },
    sub { shift @_; my ($h, $s, $i) = &RgbToHsi(@_); return &HsiToRgb( $h + 100, $s, $i * 0.75 + 63); } );
  &AlterPalette( $WhiteBorderSurface, sub { 1; },
    sub { shift @_; my ($h, $s, $i) = &RgbToHsi(@_); return &HsiToRgb( 0, 0, $i*0.25 + 191 ); } );

  &MakeGuySurfaces();
}

sub MakeGuySurface {
  my ($player) = @_;
  my ($guySurfaceFile, $guySurface, $whiteGuySurface, $harpoonSurface);
  
  $guySurfaceFile = $DataDir . '/' . $GuyImageFiles[ $player->{imagefileindex} ];
  $guySurface = new SDL::Surface( -name => ($guySurfaceFile) );
  $whiteGuySurface = new SDL::Surface( -name => ($guySurfaceFile) );
  $harpoonSurface = new SDL::Surface( -name => "$DataDir/harpoon.png" );
  $player->{hue} = $GuyColors[$player->{colorindex}]->[0];
  $player->{saturation} = $GuyColors[$player->{colorindex}]->[1];
  
  &AlterPalette($whiteGuySurface, sub {1;}, sub { return (255, 255, 255); } );
  &AlterPalette( $guySurface, sub { $_[3] > $_[2] and $_[3] > $_[1]; },
    sub {
      shift @_;
      my ($h, $s, $i) = &RgbToHsi(@_);
      return &HsiToRgb($player->{hue}, $player->{saturation}, $i); }
  );
  &AlterPalette( $harpoonSurface, sub { 1; },
    sub {
      shift @_;
      my ($h, $s, $i) = &RgbToHsi(@_);
      return &HsiToRgb($player->{hue}, $player->{saturation} * $s / 256, $i); }
  );
  $player->{guySurface} = $guySurface;
  $player->{whiteGuySurface} = $whiteGuySurface;
  $player->{harpoonSurface} = $harpoonSurface;
}

sub MakeGuySurfaces {
  foreach my $player (@Players) {
    &MakeGuySurface($player);
  }
  
  $WhiteHarpoonSurface = new SDL::Surface( -name => "$DataDir/harpoon.png" );
  &AlterPalette($WhiteHarpoonSurface, sub {1;}, sub { return (255, 255, 255); } );
}

sub AlterPalette {
  my ($surface, $filterSub, $alterSub) = @_;
  my ($r, $g, $b);
  my ($palette, $numColors, $n, $color);

  $palette = $surface->palette();
  $numColors = SDL::PaletteNColors($palette);
  for ($n = 1; $n < $numColors; ++$n) {
    $color = SDL::PaletteColors($palette, $n);
    ($r, $g, $b) = ( SDL::ColorR($color), SDL::ColorG($color), SDL::ColorB($color) );

    next unless $filterSub->($n, $r, $g, $b);
    ($r, $g, $b) = $alterSub->($n, $r, $g, $b);
    $r = $g = $b = 4 if ($r == 0 and $g == 0 and $b == 0);

    SDL::PaletteColors($palette, $n, $r, $g, $b);
  }
  $surface->display_format();
}

sub RenderBorder {
  my ($borderSurface, $targetSurface) = @_;
  my ($dstrect, $srcrect1, $srcrect2, $xpos, $ypos, $x, $y, $width, $height);
  
  $x = $y = 0;
  $width = $ScreenWidth + 2 * $ScreenMargin;
  $height = $ScreenHeight + 2 * $ScreenMargin;

  # Draw top and bottom border

  $dstrect = new SDL::Rect( -width => 32, -height =>16);
  $srcrect1 = new SDL::Rect( -width => 32, -height =>16, -x => 32, -y => 0);
  $srcrect2 = new SDL::Rect( -width => 32, -height =>16, -x => 32, -y => 80);
  for ($xpos = 32; $xpos < $width-32; $xpos += 32) {
    $dstrect->x($xpos + $x);
    $dstrect->y($y);
    $borderSurface->blit($srcrect1, $targetSurface, $dstrect);
    $dstrect->y($y + $height - 16);
    $borderSurface->blit($srcrect2, $targetSurface, $dstrect);
  }

  # Draw left and right border
  $dstrect->width(16);
  $dstrect->height(32);
  $srcrect1 = new SDL::Rect( -width => 16, -height =>32, -x => 0, -y => 32);
  $srcrect2 = new SDL::Rect( -width => 16, -height =>32, -x => 80, -y => 32);
  for ($ypos = 32; $ypos < $height-32; $ypos += 32) {
    $dstrect->x($x);
    $dstrect->y($ypos + $y);
    $borderSurface->blit($srcrect1, $targetSurface, $dstrect);
    $dstrect->x($x + $width - 16);
    $borderSurface->blit($srcrect2, $targetSurface, $dstrect);
  }

  # Draw the borders
  $srcrect1 = new SDL::Rect( -width => 32, -height => 32 );
  $dstrect->width(32);
  $dstrect->x($x);
  $dstrect->y($y);
  $borderSurface->blit($srcrect1, $targetSurface, $dstrect);
  $srcrect1->x(64);
  $dstrect->x($x + $width - 32);
  $borderSurface->blit($srcrect1, $targetSurface, $dstrect);
  $srcrect1->y(64);
  $dstrect->y($y + $height - 32);
  $borderSurface->blit($srcrect1, $targetSurface, $dstrect);
  $srcrect1->x(0);
  $dstrect->x($x);
  $borderSurface->blit($srcrect1, $targetSurface, $dstrect);
}

sub LoadBackground {
  my $filename = shift;
  my ($backgroundImage, $srcrect, $dstrect);
  
  $Background->fill( new SDL::Rect(-width=>$PhysicalScreenWidth, -height=>$PhysicalScreenHeight), new SDL::Color() );
  $backgroundImage = new SDL::Surface(-name => "$DataDir/$filename");
  $dstrect = new SDL::Rect(-x => $ScreenMargin, -y => $ScreenMargin);
  $srcrect = new SDL::Rect(-width => $ScreenWidth, -height => $ScreenHeight);
  if ($ScreenWidth > $backgroundImage->width() or $ScreenHeight > $backgroundImage->height()) {
    if ($::RotoZoomer) {
      my $zoomX = $ScreenWidth / $backgroundImage->width(); $zoomX = 1.0 if $zoomX < 1.0;
      my $zoomY = $ScreenHeight / $backgroundImage->height(); $zoomY = 1.0 if $zoomY < 1.0;
      $backgroundImage = $::RotoZoomer->zoom($backgroundImage, $zoomX, $zoomY, 0);
    }
  }
  $backgroundImage->blit($srcrect, $Background, $dstrect);
  
  &RenderBorder($BorderSurface, $Background);
}

sub TextWidth {
  if (defined(&SDL::App::SDL_TEXTWIDTH)) {
    SDL::App::SDL_TEXTWIDTH(@_);   # perl-sdl-1.x
  } else {
    SDL::SFont::SDL_TEXTWIDTH(@_); # perl-sdl-2.x
  }
}

sub FindVideoMode {
  if ($FullScreen < 2) {
    return (800, 600);
  }
  
  # Find a suitable widescreen mode
  # One native resolution:   1680 x 1050 => 1.6  : 1
  # Which could translate to: 840 x 525  => 1.6  : 1
  # Some adapters have:       848 x 480  => 1.76 : 1
  #                           720 x 480  => 1.5  : 1
  #                           800 x 512  => 1.56 : 1
  # Conclusion: Any resolution where w in [800,900], h > 480 and r in [1.5, 1.8] is good
  
  my ($modes, $mode, @goodModes, $w, $h, $ratio);
  $modes = SDL::ListModes( 0, SDL_FULLSCREEN|SDL_HWSURFACE );
  foreach $mode (@{$modes}) {
    $w = SDL::RectW($mode);
    $h = SDL::RectH($mode);
    $ratio = $w / $h;
    # print sprintf( "%4d x %4d => %0.3f\n", $w, $h, $ratio );
    next if $w < 800 or $w > 900;
    next if $h < 480;
    next if $ratio < 1.5 or $ratio > 1.8; 
    push @goodModes, ( { -w => $w, -h => $h, -score => abs($ratio - 1.6) * 1000 + abs($w - 800) } );
  }
  @goodModes = sort { $a->{-score} <=> $b->{-score} } @goodModes;
  return (800, 600) unless @goodModes;
  foreach $mode (@goodModes) {
    print sprintf( '%d x %d => %0.3f (score %d)', $mode->{-w}, $mode->{-h}, $mode->{-w} / $mode->{-h}, $mode->{-score} ), "\n";
  }
  return ($goodModes[0]->{-w}, $goodModes[0]->{-h});
}


##########################################################################
# SOUNDS
##########################################################################

sub LoadSounds {
  $Mixer = eval { SDL::Mixer->new(-frequency => 22050, -channels => 2, -size => 1024); };
  if ($@) {
    warn $@;
    return 0;
  }

  my ($soundName, $fileName);
  while (($soundName, $fileName) = each %Sounds) {
    $Sounds{$soundName} = new SDL::Sound("$DataDir/$fileName");
  }

  $::music = new SDL::Music("$DataDir/UPiPang.mp3");
  $::music = new SDL::Music("$DataDir/UPiPang.mid") unless $::music and $::music->{-data};
  &SetMusicEnabled($MusicEnabled);
}

sub PlaySound {
  return unless $SoundEnabled;
  my $sound = shift;
  $Mixer and $Sounds{$sound} and $Mixer->play_channel(-1, $Sounds{$sound}, 0);
}

sub SetMusicEnabled {
  return $MusicEnabled = 0 unless $::music and $::music->{-data};
  my $musicEnabled = shift;

  $MusicEnabled = $musicEnabled ? 1 : 0;
  if ( (not $MusicEnabled) and $Mixer->playing_music() ) {
    $Mixer->halt_music();
  }
  if ($MusicEnabled and not $Mixer->playing_music()) {
    $Mixer->play_music($::music, -1);
  }
}



package PlaybackGame;
package RecordGame;
package Game;
package DemoGame;
package Menu;

##########################################################################
package GameBase;
##########################################################################

sub new {
  my ($class) = @_;
  my $self = {
    'abortgame' => 0,
    'anim' => 0,
    'nocollision' => 0,
  };
  $::GameSpeed = 1.0;
  $::GamePause = 0;
  bless $self, $class;
}

sub Exit {
  exit;
}

sub Rand {
  shift;
  return rand($_[0]);
}

sub ResetGame {
  my $self = shift;

  @::GameObjects = ();
  $::GamePause = 0;
  %::GameEvents = ();
}

sub CalculateAdvances {
  my $advance = &GameTimer::GetAdvances();
  while ($advance <= 0) {
    $::App->delay(3); # Wait 3ms = 0.3 game ticks
    $advance = &GameTimer::GetAdvances();
  }
  if ($advance > 5) {
    # print STDERR "advance = $advance!\n";
    $advance = 5;
  }
  return $advance;
}

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

  ++$self->{anim};
  foreach my $gameObject (@::GameObjects) {
    $gameObject->Advance();
  }
}

sub OnBallPopped {
}

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

  my ($gameObject);
  foreach $gameObject (@::GameObjects) {
    $gameObject->Clear();
  }
  $self->DrawScoreBoard();
  foreach $gameObject (@::GameObjects) {
    $gameObject->Draw();
  }
  $::App->sync();
}

sub DrawScoreBoard() {
}

##########################################################################
package Game;
##########################################################################

@Game::ISA = qw( GameBase );

sub new {
  my ($class) = @_;
  my $self = new GameBase;
  %{$self} = (%{$self},
    'spawndelay'     => 0,
    'superballdelay' => 0,
    'playersalive'   => 0,
    'level'          => 0,
    'leveldesc'      => undef,
    'leveladvance'   => 0,
  );
  bless $self, $class;
}

sub ResetGame {
  my $self = shift;

  $self->SUPER::ResetGame();
  $self->SetGameLevel(0);
  $self->{playersalive} = 0;
  $self->{spawndelay} = 0;
  $self->{superballdelay} = 2500 + $self->Rand(2500);  # 25sec - 50sec
  $self->{superballdelay} *= $::DifficultyLevel->{superball};
  $::GamePause = 0;

  foreach my $player (@::Players) {
    last if $player->{number} >= $::NumGuys;
    $self->SpawnPlayer($player);
  }
  $self->LayoutScoreBoard();
  push @::GameObjects, (new FpsIndicator);
}

sub SetGameLevel {
  my ($self, $level) = @_;

  $self->{level} = $level;
  $level = $#::GameLevels if $level > $#::GameLevels;
  $self->{leveldesc} = $::GameLevels[$level];
  die unless $self->{leveldesc};
  $::GameSpeed = $self->{leveldesc}->{gamespeed} * 0.8 * $::DifficultyLevel->{speed};
  $self->{leveladvance} = 0;

  if (($level % 10) == 9) {
    my @filenames = qw( l1.jpg  l2.jpg  l3.jpg  l4.jpg  l5.jpg  l6.jpg  l7.jpg  l8.jpg  l9.jpg );
    my $index = int($level / 10);
    $index = $#filenames if $index > $#filenames;
    &::LoadBackground($filenames[$index]);
    $::Background->blit(0, $::App, 0);
    &GameTimer::ResetTimer();
  }
}

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

  $self->SUPER::AdvanceGameObjects();
  $self->RespawnPlayers();
  $self->SpawnBalls() if $::GamePause <= 0;
  --$::GamePause if $::GamePause > 0;
}

sub SpawnPlayer {
  my ($self, $player) = @_;

  $player->{score} = 0;
  $player->{scoreforbonuslife} = 200000;
  $player->{lives} = 2;
  $player->{startX} = ($::ScreenWidth - $::NumGuys * 60) / 2 + 60 * ($player->{number}+0.5) - 32;
  $player->{respawn} = -1;
  my $guy = new Guy($player);
  push @::GameObjects, ($guy);
  ++$self->{playersalive};
  return $guy;
}

sub RespawnPlayers {
  my $self = shift;

  foreach my $player (@::Players) {
    last if $player->{number} >= $::NumGuys;
    if ($player->{respawn} > 0) {
      --$player->{respawn};
      $player->{score} = int($player->{respawn} / 100);
      if ($player->{respawn} <= 0) {
        my $guy = $self->SpawnPlayer($player);
        $guy->{invincible} = 500;
      }
    }
  }
}

sub SpawnBalls {
  my $self = shift;
  my ($randmax, $rnd, $ballName, $balldesc, $deathBallCount, $earthquakeBallCount, $hasBonus);

  --$self->{superballdelay};
  if ($self->{superballdelay} <= 0) {
    push @::GameObjects, (
      &Ball::Spawn($::BallDesc{sprintf('super%d', $self->Rand(2))}, -1, $self->Rand(40) < 20 ? 0 : 1) );
    $self->{superballdelay} = (2500 + $self->Rand(2000)) * $::DifficultyLevel->{superball}; # 25sec - 45sec
  }

  --$self->{spawndelay};
  return if $self->{spawndelay} > 0;
  $deathBallCount = $earthquakeBallCount = -1;
  $randmax = 10000;
  while ($self->{spawndelay} <= 0) {
    if ($::DifficultyLevel->{name} eq 'Miki') {
      $balldesc = $::BallDesc{'death'};
      last;
    }
    $rnd = int($self->Rand($randmax));
    $randmax = 0;

    # We try to find the balldesc that falls at $rnd
    my $ballRoulette = $self->{leveldesc}->{balls};
    for (my $i = 0; $i < scalar @{$ballRoulette}; $i+=2) {
      my $rouletteWeight = $ballRoulette->[$i+1];
      $randmax += $rouletteWeight;
      $rnd -= $rouletteWeight;
      if ($rnd < 0) {
        $ballName = $ballRoulette->[$i];
        last;
      }
    }
    next unless ($ballName); # $rnd too large.. We'll have a better $randmax this time!

    ($balldesc) = grep( $_->{name} eq $ballName, @::BallDesc);
    if ($balldesc->{class} eq 'DeathBall') {
      next unless $::DeathBallsEnabled;
      $deathBallCount = &DeathBall::CountDeathBalls() if $deathBallCount < 0; # Lazy counting
      next if $deathBallCount >= 2;
    }
    if ($balldesc->{class} eq 'EarthquakeBall') {
      next unless $::EarthquakeBallsEnabled;
      $earthquakeBallCount = &::EarthquakeBall::CountEarthquakeBalls if $earthquakeBallCount < 0;
      next if $earthquakeBallCount >= 1;
    }
    if ($balldesc->{class} eq 'WaterBall') {
      next unless $::WaterBallsEnabled;
    }
    if ($balldesc->{class} eq 'SeekerBall') {
      next unless $::SeekerBallsEnabled;
    }
    last if $balldesc;
  }

  $hasBonus = 1 if ($balldesc->{width} >= 32) and ($self->Rand(1) < $::DifficultyLevel->{bonusprobability});

  push @::GameObjects, ( &Ball::Spawn($balldesc, -1, $self->Rand(40) < 20 ? 0 : 1, $hasBonus) );
  $self->{spawndelay} = $self->{leveldesc}->{spawndelay} * $balldesc->{spawndelay} * 50;
  $self->{spawndelay} /= ($::NumGuys + 1) / 2;
  $self->{spawndelay} *= $::DifficultyLevel->{spawnmultiplier};
}

sub OnBallPopped {
  my $self = shift;

  ++$self->{leveladvance};
  if ($self->{leveladvance} >= 18) {
    ::PlaySound('level');
    $self->SetGameLevel($self->{level}+1);
  }
}

sub Delay {
  my ($self, $ticks) = @_;
  
  while ($ticks > 0) {
    my $advance = $self->CalculateAdvances();
    %::Events = ();
    &::HandleEvents();
    return if $self->{abortgame};
    $ticks -= $advance;
    $self->DrawGame();
  }
}

sub PlayerNextLife {
  my ($self, $guy) = @_;
  
  $guy->DeleteHarpoons;
  if ($guy->{player}->{lives}--) {
    $guy->{x} = $guy->{player}->{startX};
    $guy->{y} = $::ScreenHeight - $guy->{h};
    $guy->{state} = 'idle';
    $guy->{speedY} = 0;
    $guy->{invincible} = 500; # 0.5s
    $guy->{killed} = 0;
    $guy->{justkilled} = 0;
  } else {
    # One player less
    $guy->Delete();
    --$self->{playersalive};
    $guy->{player}->{respawn} = 6000; # 60s
  }
}

sub PlayerDeathSequence {
  my $self = shift;
  my (@killedGuys, @deadGuys, $guy, $i);

  $self->DrawGame();
  ::PlaySound('death');
  &::RenderBorder($::WhiteBorderSurface, $::App);
  $::App->sync();
  $self->Delay(10);
  &::RenderBorder($::RedBorderSurface, $::App);
  &::RenderBorder($::RedBorderSurface, $::Background);
  $::App->sync();
  $self->Delay(90);
  &GameTimer::ResetTimer();

  @killedGuys = grep { $_->{justkilled}; } @::GameObjects;
  foreach $guy (@killedGuys) {
    $guy->Clear();
    $guy->{killed} = 1;
    push @deadGuys, (new DeadGuy($guy));
  }
  push @::GameObjects, (@deadGuys);

  for ($i = 0; $i < 300; ++$i) {
    &::HandleEvents();
    return if $self->{abortgame};
    my $advance = $self->CalculateAdvances();
    while ($advance--) {
      foreach my $gameObject (@deadGuys) {
        $gameObject->Advance();
      }
    }
    $self->DrawGame();
    last if $deadGuys[0]->{deleted};
  }

  foreach $guy (@killedGuys) {
    $self->PlayerNextLife($guy);
  }
  
  &::RenderBorder($::BorderSurface, $::App);
  &::RenderBorder($::BorderSurface, $::Background);
  &GameTimer::ResetTimer();
}

sub SuperKill {
  my ($self, $guy) = @_;

  my @gameObjects = @::GameObjects;
  my $sound = 0;
  foreach my $ball (@gameObjects) {
    next unless $ball->isa("Ball");
    $ball->Pop($guy, 'superkill');
    $sound = 1;
  }
  ::PlaySound('pop') if $sound;
}

sub DeathballMeltdown {
  my ($self) = @_;
  my ($i, $meltdown, $allKilled, @gameObjects, @killedGuys, @deadGuys);
  
  $self->{nocollision} = 1;
  $meltdown = new Meltdown;
  push @::GameObjects, $meltdown;
  
  for ($i = 0; $i < 300; ++$i) {
    %::Events = ();
    &::HandleEvents();
    return if $self->{abortgame};
    my $advance = $self->CalculateAdvances();
    while ($advance--) {
      $self->SUPER::AdvanceGameObjects();
      $::GamePause = 0;
      if ($meltdown->{bounce} and not $allKilled) {
        $allKilled = 1;
        @gameObjects = @::GameObjects;
        foreach (@gameObjects) {
          if ($_->isa('Ball')) {
            $_->Pop(undef, 'meltdown');
          } elsif ('Guy' eq ref $_) {
            $_->{killed} = 1;
            push @deadGuys, (new DeadGuy($_));
            push @killedGuys, $_;
          }
        }
        push @::GameObjects, (@deadGuys);
        undef @gameObjects;
      }
    }
    $self->DrawGame();
  }
  
  foreach (@killedGuys) {
    $self->PlayerNextLife($_);
  }
  
  $self->{nocollision} = 0;
}

##########################################################################
# GAME DRAWING
##########################################################################

sub DrawScoreBoard {
  my ($self) = @_;
  my ($x, $y, $widthPerGuy);
  
  $self->DrawLevelIndicator( 10, $self->{scoreBoardTop} );
  for (my $i = 0; $i < $::NumGuys; ++$i) {
    $self->DrawScore( $::Players[$i], $::Players[$i]->{scoreX}, $::Players[$i]->{scoreY} );
  }
}

sub LayoutScoreBoard {
  my ($self) = @_;
  my ($i, $scoreBoardHeight, $scoreBoardTop, $rows, $rowHeight, $leftMargin, $guysPerRow, $widthPerGuy);
  
  $scoreBoardTop = $::ScreenHeight + $::ScreenMargin * 2 + 5;
  $scoreBoardHeight = $::PhysicalScreenHeight - $scoreBoardTop;
  $rowHeight = 64;
  $leftMargin = 150;
  $rows = $::NumGuys > 4 ? 2 : 1;
  $rows = 1 if ($scoreBoardTop + $rows * $rowHeight > $::PhysicalScreenHeight);
  if ($scoreBoardTop + $rows * $rowHeight > $::PhysicalScreenHeight) {
    $rowHeight = 32;
    $scoreBoardTop = $::PhysicalScreenHeight - 32;
  }
  $guysPerRow = int ($::NumGuys / $rows + 0.5);
  $widthPerGuy = ($::PhysicalScreenWidth - $leftMargin) / $guysPerRow;
  for ($i = 0; $i < $::NumGuys; ++$i) {
    $::Players[$i]->{scoreX} = $leftMargin + ($i % $guysPerRow) * $widthPerGuy;
    $::Players[$i]->{scoreY} = $scoreBoardTop + int ($i / $guysPerRow) * $rowHeight;
    $::Players[$i]->{scoreRect} = 
      new SDL::Rect(-x => $::Players[$i]->{scoreX}, -y => $::Players[$i]->{scoreY}, -width=> 130, -height=> $rowHeight);
  }
  $self->{scoreBoardTop} = $scoreBoardTop;
  $self->{scoreBoardHeight} = $scoreBoardHeight;
  $self->{rowHeight} = $rowHeight;

}

sub DrawLevelIndicator {
  my ($self, $x, $y) = @_;
  
  $self->{levelIndicatorRect} = new SDL::Rect(-x => $x, -y => $y, -width => 140, -height => $self->{scoreBoardHeight}) unless $self->{levelIndicatorRect};
  $::App->fill( $self->{levelIndicatorRect}, new SDL::Color() );
  $::LevelIndicatorSurface2->blit( 0, $::App, new SDL::Rect(-x => $x, -y => $y));
  $::LevelIndicatorSurface->blit( new SDL::Rect(-width => 130 * $self->{leveladvance} / 17, -height => 30), $::App, new SDL::Rect(-x => $x, -y => $y));
  $::App->print( $x + 25, $y + 3, 'Level ' . ($self->{level}+1) );
  $::App->print( $x, $y + 40, sprintf('spd: %d/%d', $::GameSpeed * 100, $self->{leveldesc}->{spawndelay}) ) if $self->{scoreBoardHeight} >= 64;
}

sub PrintNumber {
  my ($self, $player, $x, $y, $number) = @_;
  my ($numberText, $i, $srcrect, $dstrect);

  $numberText = sprintf("%d", $number);
  $srcrect = new SDL::Rect(-width => 16, -height => 16, -y => 224);
  $dstrect = new SDL::Rect(-width => 16, -height => 16, -y => $y, -x => $x);
  for ($i = 0; $i < length($numberText); ++$i) {
    $srcrect->x(320 + (ord(substr($numberText, $i)) - ord('0')) * 16);
    $dstrect->x($x + $i * 16);
    $player->{guySurface}->blit( $srcrect, $::App, $dstrect );
  }
}

sub DrawScore {
  my ($self, $player, $x, $y, $livesY) = @_;
  my ($i, $srcrect, $dstrect);
  
  $::App->fill($player->{scoreRect}, new SDL::Color());
  $self->PrintNumber( $player, $x, $y, $player->{score});
  
  $livesY = $self->{rowHeight} > 32 ? $y + 24 : $y + 16;
  
  $dstrect = new SDL::Rect(-width => 32, -height => 32, -x =>$x, -y => $livesY);
  if ($self->{rowHeight} <=32) {
    $srcrect = new SDL::Rect(-width => 16, -height => 16, -x =>320, -y => 240);
  } else {
    $srcrect = new SDL::Rect(-width => 32, -height => 32, -x =>320, -y => 192);
    if ($player->{lives} > 3) {
      $player->{guySurface}->blit( $srcrect, $::App, $dstrect );
      $self->PrintNumber( $player, $x + 40, $y + 32, $player->{lives} );
      return;
    }
  }

  foreach $i ( 0 .. $player->{lives}-1 ) {
    $dstrect->x( $x + $i * ($srcrect->width() + 4) );
    $player->{guySurface}->blit( $srcrect, $::App, $dstrect );
  }
}

sub PreAdvanceAction {}

sub Run {
  my ($self) = shift;

  $self->ResetGame();
  &GameTimer::ResetTimer();

  my $superKillCount = 0;
  my $superKillDelay = 0;
  my $superKillGuy;

  while (1) {

    # Calculate advance (how many game updates to perform)
    my $advance = $self->CalculateAdvances();

    # Advance the game

    %::Events = ();
    &::HandleEvents();
    while ($advance--) {
      return if $self->{abortgame};
      %::GameEvents = ();

      $self->PreAdvanceAction(); # Hook for something special

      if ($superKillCount > 0) {
        if (--$superKillDelay <= 0) {
           --$superKillCount;
           $superKillDelay = 50;
           $self->SuperKill($superKillGuy);
        }
        $::GamePause = 0;
      }

      $self->AdvanceGameObjects();
      if ($::GameEvents{earthquake}) {
        &::PlaySound('quake');
        foreach my $guy (@::GameObjects) {
          $guy->Earthquake($::GameEvents{earthquake}) if ref $guy eq 'Guy';
        }
      }
      if ($::GameEvents{pop}) {
        &::PlaySound('pop');
      }
      if ($::GameEvents{meltdown} and $::DifficultyLevel->{name} ne 'Miki') {
        $self->DeathballMeltdown();
      } elsif ($::GameEvents{kill} ) {
        $self->PlayerDeathSequence();
        last if $self->{playersalive} <= 0;
        $::GamePause = 200 if $::GamePause < 200;
        &GamePause::Show();
      } elsif ($::GameEvents{magic}) {
        if ($::GamePause < 200) {
          $::GamePause = 200; &::PlaySound('pause');
          &GamePause::Show();
        }
      } elsif ($::GameEvents{superpause}) {
        if ($::GamePause < 800) {
          $::GamePause = 800; &::PlaySound('pause');
          &GamePause::Show();
        }
      } elsif ($::GameEvents{superkill}) {
        $superKillCount = 5;
        $superKillDelay = 0;
        $superKillGuy = $::GameEvents{superkillguy};
        $self->{spawndelay} = 250;
        $self->{superballdelay} += 1000; # 10 second penalty
        my @gameObjects = @::GameObjects;
        foreach my $spawningBall (@gameObjects) { $spawningBall->Delete if $spawningBall->{spawning}; }
      }
    }
    if ($self->{playersalive} <= 0) {
      my $gameoverSurface = new SDL::Surface(-name => "$::DataDir/gameover.png");
      my @gameObjects = @::GameObjects;
      foreach (@gameObjects) { $_->Delete() if ('DeadGuy' eq ref $_); }
      $self->DrawGame();
      $gameoverSurface->blit(0, $::App, new SDL::Rect(-x => ($::PhysicalScreenWidth - $gameoverSurface->width) / 2, -y => $::PhysicalScreenHeight / 2 - 100));
      $::App->sync();
      $::App->delay(1000);
      for (my $i=0; $i < 20; ++$i) {
        $::App->delay(100);
        %::Events = ();
        &::HandleEvents();
        last if $self->{abortgame};
        last if %::Events;
      }
      return;
    }
    $self->DrawGame();
  }
}


##########################################################################
package RecordGame;
##########################################################################

@RecordGame::ISA = qw(Game);

sub Rand {
  my $self = shift;
  my $result = int(rand($_[0]) * 100) / 100;
  push @{$self->{rand}}, ($result);
  return $result;
}

sub Rewind {
  my $self = shift;
  my ($recordEnd, $playback);
  
  $recordEnd = length($self->{record}) - $::NumGuys * 1000;
  return if $recordEnd <= 0;
  $self->{record} = substr($self->{record}, 0, $recordEnd);
  $::Game = $playback = new PlaybackGame($::NumGuys, $::DifficultyLevel, $self->{record}, $self->{rand}, {});
  $playback->{skip} = 1;
  $::Background->blit(0, $::App, 0);
  $playback->Run();
  
  $playback->RestoreGameSettings();
  %{$self} = %{$playback};
  $::Game = $self;
  $self->{abortgame} = 0;
  print "Splicing {rand}: original length is ", scalar(@{$self->{rand}}), "; playback randpointer is $playback->{randpointer}.\n";
  splice @{$self->{rand}}, $playback->{randpointer};
  $::Background->blit(0, $::App, 0);
  $self->DrawGame();
  %::Events = %::Keys = ();
  while( not %::Events ) { &::HandleEvents(); $::App->delay(100); }
  &GameTimer::ResetTimer();
}

sub PreAdvanceAction {
  my $self = shift;
  my ($record);
  
  $self->Rewind() if $::Events{::SDLK_F3()};
  
  for (my $i=0; $i < $::NumGuys; ++$i) {
    my $keys = $::Players[$i]->{keys};
    $record = 0;
    $record += 1 if $::Keys{$keys->[0]};
    $record += 2 if $::Keys{$keys->[1]};
    $record += 4 if $::Events{$keys->[2]};
    if ($::Events{::SDLK_F2()} and $::NumGuys == 1) {
      $record += 8;
      $::GameEvents{superkill} = 1;
    }
    $self->{record} .= $record;
  }
}


##########################################################################
package PlaybackGame;
##########################################################################

@PlaybackGame::ISA = qw(Game);

sub new {
  my ($class, $numGuys, $difficultyLevel, $record, $rand, $messages) = @_;
  my $self;
  
  $self = new Game;
  %{$self} = (%{$self},
    'record' => $record,
    'rand' => $rand,
    'messages' => $messages,
  );
  bless $self, $class;
  $self->InitPlayback($numGuys);
  &::SetDifficultyLevel($difficultyLevel);
  return $self;
}

sub InitPlayback {
  my ($self, $numGuys) = @_;
  
  $self->{recordpointer} = 0;
  $self->{randpointer} = 0;
  $self->{oldnumguys} = $::NumGuys;
  $self->{olddifficultylevel} = $::DifficultyLevelIndex;
  
  $::NumGuys = $numGuys;
  for (my $i=0; $i < $numGuys; ++$i) {
    $::Players[$i]->{oldkeys} = $::Players[$i]->{keys};
    $::Players[$i]->{keys} = [ "DLEFT$i", "DRIGHT$i", "DFIRE$i" ];
  }
}

sub RestoreGameSettings {
  my $self = shift;
  
  for (my $i=0; $i < $::NumGuys; ++$i) {
    $::Players[$i]->{keys} = $::Players[$i]->{oldkeys};
    delete $::Players[$i]->{oldkeys};
  }
  $::NumGuys = $self->{oldnumguys};
  &::SetDifficultyLevel($self->{olddifficultylevel});
}

sub CalculateAdvances {
  my $self = shift;
  
  return length($self->{record}) if $self->{skip};
  return $self->SUPER::CalculateAdvances() * ($::Keys{::SDLK_f()} ? 10 : 1);
}

sub Rand {
  my $self = shift;
  
  my $result = $self->{rand}->[$self->{randpointer}];
  ++$self->{randpointer};
  return $result;
}

sub PreAdvanceAction {
  my $self = shift;
  my ($record, $keys);

  for (my $i=0; $i < $::NumGuys; ++$i) {

    $record = substr($self->{record}, $self->{recordpointer}++, 1);
    $keys = $::Players[$i]->{keys};
    $::Keys{$keys->[0]} = $record & 1;
    $::Keys{$keys->[1]} = $record & 2;
    $::Events{$keys->[2]} = $record & 4;
    $::GameEvents{superkill} = 1 if $::NumGuys == 1 and $record & 8;
  }
  
  $self->{abortgame} = 1 if $self->{recordpointer} >= length $self->{record};
  
  if ($self->{messages}) {
    my $message =  $self->{messages}->{$self->{recordpointer}};
    $self->DisplayMessage($message) if $message;
  }
}

sub DisplayMessage {
  my ($self, $message) = @_;

  my ($len, $adv) = (0, 0);
  my $x = ( $::PhysicalScreenWidth - &::TextWidth($message) ) / 2;
  my $y = $::PhysicalScreenHeight / 2;
  $self->DrawGame();

  while (1) {
    &::HandleEvents();
    return if $self->{abortgame};
    my $advance = $self->CalculateAdvances();
    $adv += $advance;
    $len = int($adv / 5);

    $::App->print($x, $y, substr($message, 0, $len) );
    $::App->sync();
    last if $len > length($message) + 15;
  }
  $::Background->blit(new SDL::Rect(-width=>$::PhysicalScreenWidth, -y=>$y, -height=>40), $::App, new SDL::Rect(-y => $y));
}


##########################################################################
package DemoGame;
##########################################################################

sub ResetGame {
  my $self = shift;
  $self->Game::ResetGame();

  my $ball = &Ball::Create($::BallDesc[4], 400, 0, -10, 0);
  $ball->GiveMagic();

  push @::GameObjects, (
    &Ball::Create($::BallDesc[0], 100, 0, 1),
    &Ball::Create($::BallDesc{super0}, 300, 0, 0),
    &Ball::Create($::BallDesc{super1}, 500, 0, 1),
    $ball,
  );
  $::GamePause = 0;
  $::GameSpeed = 0.8;
  $self->{spawndelay} = $self->{superballdelay} = 1000000;
  $self->{ballcounter} = 0;
  $self->{balls} =  [ qw(b0 h0 w1 quake death seeker) ];
  $self->{delays} = [ qw( 8 10 15    12    12) ];
}

sub SpawnBalls {
  my $self = shift;
  
  return if (--$self->{spawndelay} > 0);
  my $ballName = $self->{balls}->[$self->{ballcounter}];
  return unless $ballName;
  push @::GameObjects, ( &Ball::Spawn($::BallDesc{$ballName}, 100, 1, 0) );
  $self->{spawndelay} = 1000000;
  ++$self->{ballcounter};
}

sub RespawnPlayers {}
sub OnBallPopped {}


##########################################################################
package DemoRecordGame;
##########################################################################

@DemoRecordGame::ISA = qw(DemoGame RecordGame);

sub new {
  my $class = shift;
  my $self = new RecordGame(@_);
  bless $self, $class;
}


##########################################################################
package DemoPlaybackGame;
##########################################################################

@DemoPlaybackGame::ISA = qw(DemoGame PlaybackGame);

sub new {
  my $class = shift;
  my $self = new PlaybackGame(@_);
  bless $self, $class;
}

sub DrawScoreBoard {
  my $self = shift;
  my ($x, $y);
  
  $x = 10;
  $y = $::ScreenHeight + 2 * $::ScreenMargin + 5;
  $::App->print( $x, $y, "Press F to fast forward" ) if $self->{anim} < 1; return;
  $::App->fill( new SDL::Rect(-x=>0, -y=>$y, -width=>$::PhysicalScreenWidth, -height=>$::PhysicalScreenHeight - $y), new SDL::Color() );
  $::App->print( $x, $y, $self->{recordpointer} );
}


##########################################################################
package Menu;
##########################################################################

@Menu::ISA = qw(GameBase);
use vars qw(@syms);
@syms = qw(UNKNOWN FIRST BACKSPACE TAB CLEAR RETURN PAUSE ESCAPE SPACE EXCLAIM QUOTEDBL HASH DOLLAR AMPERSAND QUOTE LEFTPAREN RIGHTPAREN ASTERISK PLUS COMMA MINUS PERIOD SLASH 0 1 2 3 4 5 6 7 8 9 COLON SEMICOLON LESS EQUALS GREATER QUESTION AT LEFTBRACKET BACKSLASH RIGHTBRACKET CARET UNDERSCORE BACKQUOTE a b c d e f g h i j k l m n o p q r s t u v w x y z DELETE WORLD_0 WORLD_1 WORLD_2 WORLD_3 WORLD_4 WORLD_5 WORLD_6 WORLD_7 WORLD_8 WORLD_9 WORLD_10 WORLD_11 WORLD_12 WORLD_13 WORLD_14 WORLD_15 WORLD_16 WORLD_17 WORLD_18 WORLD_19 WORLD_20 WORLD_21 WORLD_22 WORLD_23 WORLD_24 WORLD_25 WORLD_26 WORLD_27 WORLD_28 WORLD_29 WORLD_30 WORLD_31 WORLD_32 WORLD_33 WORLD_34 WORLD_35 WORLD_36 WORLD_37 WORLD_38 WORLD_39 WORLD_40 WORLD_41 WORLD_42 WORLD_43 WORLD_44 WORLD_45 WORLD_46 WORLD_47 WORLD_48 WORLD_49 WORLD_50 WORLD_51 WORLD_52 WORLD_53 WORLD_54 WORLD_55 WORLD_56 WORLD_57 WORLD_58 WORLD_59 WORLD_60 WORLD_61 WORLD_62 WORLD_63 WORLD_64 WORLD_65 WORLD_66 WORLD_67 WORLD_68 WORLD_69 WORLD_70 WORLD_71 WORLD_72 WORLD_73 WORLD_74 WORLD_75 WORLD_76 WORLD_77 WORLD_78 WORLD_79 WORLD_80 WORLD_81 WORLD_82 WORLD_83 WORLD_84 WORLD_85 WORLD_86 WORLD_87 WORLD_88 WORLD_89 WORLD_90 WORLD_91 WORLD_92 WORLD_93 WORLD_94 WORLD_95 KP0 KP1 KP2 KP3 KP4 KP5 KP6 KP7 KP8 KP9 KP_PERIOD KP_DIVIDE KP_MULTIPLY KP_MINUS KP_PLUS KP_ENTER KP_EQUALS UP DOWN RIGHT LEFT INSERT HOME END PAGEUP PAGEDOWN F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 F13 F14 F15 NUMLOCK CAPSLOCK SCROLLOCK RSHIFT LSHIFT RCTRL LCTRL RALT LALT RMETA LMETA LSUPER RSUPER MODE COMPOSE HELP PRINT SYSREQ BREAK MENU POWER EURO UNDO LAST );

sub Exit {
  my $self = shift;
  
  &::SaveConfig();
  $self->SUPER::Exit();
}

sub MenuAdvance {
  my $self = shift;

  my $advance = $self->CalculateAdvances();
  %::Events = %::MenuEvents = ();
  %::GameEvents = ();
  &::HandleEvents('readbothaxes');
  while ($advance--) {
    $self->AdvanceGameObjects();
  }
  while (ref($::GameObjects[$#::GameObjects]) ne 'MenuItem') { unshift @::GameObjects, (pop @::GameObjects); }
  $self->DrawGame();
}

sub SetCurrentItemIndex {
  my ($self, $index) = @_;

  return if ($index < 0 or $index >= scalar @{$self->{menuItems}});
  $self->{currentItemIndex} = $index;
  $self->{currentItem} = $self->{menuItems}->[$index];
  $self->{currentItem}->Select();
}

sub EnterSubMenu {
  my $self = shift;
  my ($recall, $menuItem);
  
  $recall->{oldItems} = $self->{menuItems};
  $recall->{oldCurrentItemIndex} = $self->{currentItemIndex};
  foreach $menuItem (@{$self->{menuItems}}) { $menuItem->Hide(); }
  $self->{menuItems} = [];
  
  return $recall;
}

sub LeaveSubMenu {
  my ($self, $recall) = @_;
  my ($menuItem);

  foreach $menuItem (@{$self->{menuItems}}) { $menuItem->HideAndDelete(); }
  $self->{menuItems} = $recall->{oldItems};
  foreach $menuItem (@{$self->{menuItems}}) { $menuItem->Show(); }
  $self->SetCurrentItemIndex($recall->{oldCurrentItemIndex});
  $self->{abortgame} = 0;
}

sub HandleUpDownKeys {
  my $self = shift;

  if ($::MenuEvents{DOWN}) {
    $self->SetCurrentItemIndex( $self->{currentItemIndex} + 1 );
  }
  if ($::MenuEvents{UP}) {
    $self->SetCurrentItemIndex( $self->{currentItemIndex} - 1 );
  }
}

sub KeyToText {
  my ($key) = @_;
  eval("::SDLK_$_() eq $key") and return ucfirst(lc($_)) foreach @syms;
  print "No match for $key\n";
  return "???";
}

sub KeysToText {
  my $keys = shift;
  my ($retval);
  if ( $keys->[0] =~ /^[LRB](\d)+$/ ) {
    return "Joystick $1";
  }
  return join(' / ', &KeyToText($keys->[0]), &KeyToText($keys->[1]), &KeyToText($keys->[2]) );
}

sub RunCredits {
  my $self = shift;
  my ($recall, $i, $ball, @balls, @oldGameObjects);
  
  $recall = $self->EnterSubMenu();
  @oldGameObjects = @::GameObjects;
  foreach my $gameObject (@::GameObjects) {
    $gameObject->Clear();
  }
  @::GameObjects = ($self->{title});
  my ($y, $yinc) = (110, 36);
  push @{$self->{menuItems}}, (
    new MenuItem( 100, $y += $yinc, "Written by: UPi <upi\@sourceforge.net>"),
    new MenuItem( 100, $y += $yinc, "Music by: SAdam" ),
    new MenuItem( 100, $y += $yinc, "Graphics by: UPi, DaniGM, EBlanca" ),
    new MenuItem( 100, $y += $yinc * 1.5, "TESTERS" ),
    new MenuItem( 100, $y += $yinc, "Ulmar, Surba, Miki, Aisha, Descant" ),
    new MenuItem( 100, $y += $yinc * 1.5, "http://apocalypse.rulez.org/pangzero" ),
  );
  foreach $i (@{$self->{menuItems}}) { $i->Center(); }
  
  for ($i = 0; $i < 20; ++$i) {
    $ball = &Ball::Spawn( $::BallDesc{'credits1'}, 100, 1, 0 );
    $ball->{delay} = $i * 20;
    $ball->{y} -= 16;
    push @balls, ($ball);
    $ball = &Ball::Spawn( $::BallDesc{'credits2'}, $::ScreenWidth - 132, -1, 0 );
    $ball->{delay} = $i * 20;
    $ball->{y} -= 16;
    push @balls, ($ball);
  }
  push @::GameObjects, @balls;
  push @::GameObjects, (@{$self->{menuItems}});
  
  while (1) {
    $self->MenuAdvance();
    last if $self->{abortgame};
  }
  
  @::GameObjects = @oldGameObjects;
  foreach (@balls) { $_->Delete(); }
  $self->LeaveSubMenu($recall);
}

sub UpdateBallMixerMenu {
  my $self = shift;

  $self->{menuItems}->[1]->SetParameter( $::DeathBallsEnabled ? 'on' : 'off' );
  $self->{menuItems}->[2]->SetParameter( $::EarthquakeBallsEnabled ? 'on' : 'off' );
  $self->{menuItems}->[3]->SetParameter( $::WaterBallsEnabled ? 'on' : 'off' );
  $self->{menuItems}->[4]->SetParameter( $::SeekerBallsEnabled ? 'on' : 'off' );
}

sub RunBallMixerMenu {
  my $self = shift;
  my ($recall);
  
  $recall = $self->EnterSubMenu();
  my ($y, $yinc) = (110, 40);
  push @{$self->{menuItems}}, (
    new MenuItem( 100, $y += $yinc, "Back to options menu"),
    new MenuItem( 100, $y += $yinc, "Death Balls: " ),
    new MenuItem( 100, $y += $yinc, "Earthquake Balls: " ),
    new MenuItem( 100, $y += $yinc, "Water Balls: " ),
    new MenuItem( 100, $y += $yinc, "Seeker Balls: " ),
  );
  $self->UpdateBallMixerMenu();
  push @::GameObjects, (@{$self->{menuItems}});
  $self->SetCurrentItemIndex(0);
  
  while (1) {
    $self->MenuAdvance();
    last if $self->{abortgame};
    $self->HandleUpDownKeys();

    if ($::MenuEvents{BUTTON}) {
      last if $self->{currentItemIndex} == 0; # Back to main
      if ($self->{currentItemIndex} == 1) {
        $::DeathBallsEnabled = 1 - $::DeathBallsEnabled; $self->UpdateBallMixerMenu();
      } elsif ($self->{currentItemIndex} == 2) {
        $::EarthquakeBallsEnabled = 1 - $::EarthquakeBallsEnabled; $self->UpdateBallMixerMenu();
      } elsif ($self->{currentItemIndex} == 3) {
        $::WaterBallsEnabled = 1 - $::WaterBallsEnabled; $self->UpdateBallMixerMenu();
      } elsif ($self->{currentItemIndex} == 4) {
        $::SeekerBallsEnabled = 1 - $::SeekerBallsEnabled; $self->UpdateBallMixerMenu();
      }
    }
  }
  
  $self->LeaveSubMenu($recall);
}

sub UpdateOptionsMenu {
  my $self = shift;

  $self->{menuItems}->[1]->SetText("< Difficulty: $::DifficultyLevel->{name} >");
  $self->{menuItems}->[3]->SetText("Sound" . ($::SoundEnabled ? ' on' : ' off'));
  $self->{menuItems}->[4]->SetText("Music" . ($::MusicEnabled ? ' on' : ' off'));
  $self->{menuItems}->[5]->SetText('< ' . ('Windowed', 'Fullscreen', 'Widescreen')[$::FullScreen]
    . ($self->{restart} ? ' (requires restart)' : '') . ' >');
}

sub RunOptionsMenu {
  my $self = shift;
  my ($recall);
  
  $recall = $self->EnterSubMenu();
  my ($y, $yinc) = (110, 40);
  push @{$self->{menuItems}}, (
    new MenuItem( 100, $y += $yinc, "Back to main menu"),
    new MenuItem(  68, $y += $yinc, "<>" ), # difficulty
    new MenuItem( 100, $y += $yinc, "Ball Mixer..." ),
    new MenuItem( 100, $y += $yinc, "Sound on" ),
    new MenuItem( 100, $y += $yinc, "Music on" ),
    new MenuItem(  68, $y += $yinc, "Fullscreen" ),
  );
  $self->UpdateOptionsMenu();
  push @::GameObjects, (@{$self->{menuItems}});
  $self->SetCurrentItemIndex(0);
  
  while (1) {
    $self->MenuAdvance();
    last if $self->{abortgame};
    $self->HandleUpDownKeys();

    if ($::MenuEvents{LEFT} and $self->{currentItemIndex} == 1) {
      &::SetDifficultyLevel($::DifficultyLevelIndex - 1);
      $self->UpdateOptionsMenu();
    }
    if ($::MenuEvents{RIGHT} and $self->{currentItemIndex} == 1) {
      &::SetDifficultyLevel($::DifficultyLevelIndex + 1);
      $self->UpdateOptionsMenu();
    }
    if ($::MenuEvents{LEFT} and $self->{currentItemIndex} == 5) {
      if ($::FullScreen > 0) { --$::FullScreen; $self->{restart} = 1; }
      $self->UpdateOptionsMenu();
    }
    if ($::MenuEvents{RIGHT} and $self->{currentItemIndex} == 5) {
      if ($::FullScreen < 2) { ++$::FullScreen; $self->{restart} = 1; }
      $self->UpdateOptionsMenu();
    }
    if ($::MenuEvents{BUTTON}) {
      last if $self->{currentItemIndex} == 0; # Back to main
      if ($self->{currentItemIndex} == 2) {
        $self->RunBallMixerMenu();
      } elsif ($self->{currentItemIndex} == 3) {
        $::SoundEnabled = 1 - $::SoundEnabled; $self->UpdateOptionsMenu();
      } elsif ($self->{currentItemIndex} == 4) {
        &::SetMusicEnabled(1 - $::MusicEnabled); $self->UpdateOptionsMenu();
      }
    }
  }
  
  $self->LeaveSubMenu($recall);
}

sub RunControlsMenu {
  my $self = shift;
  my ($baseY, $menuItem, $recall, @keysAsText, @yPositions, $needToSqueeze);

  $recall = $self->EnterSubMenu();
  $needToSqueeze = 170 + ($::NumGuys + 1) * 40 > $::ScreenHeight;
  if ($needToSqueeze) {
    $self->{title}->Hide();
    $baseY = 50;
  } else {
    $baseY = 150;
  }
  
  push @{$self->{menuItems}}, (new MenuItem( 50, $baseY, "Back to main menu"));
  for ( my $i = 1; $i <= $::NumGuys; ++$i ) {
    $yPositions[$i] = $baseY + 20 + $i * 40;
    push @{$self->{menuItems}}, (new MenuItem( 50, $yPositions[$i], "Player $i"));
    push @keysAsText, (new MenuItem( 220, $yPositions[$i], &KeysToText($::Players[$i-1]->{keys})) );
  }
  push @::GameObjects, (@keysAsText, @{$self->{menuItems}});
  $self->SetCurrentItemIndex(0);

  while (1) {
    $self->MenuAdvance();
    last if $self->{abortgame};
    $self->HandleUpDownKeys();
    if ($::MenuEvents{BUTTON}) {
      last if $self->{currentItemIndex} == 0; # Back to main
      my $player = $::Players[$self->{currentItemIndex} - 1];
      my $key = 0;
      my $keysAsText = $keysAsText[$self->{currentItemIndex}-1];
      $self->{currentItem}->Hide();
      $keysAsText->Hide();
      my @prompts = ("Press 'LEFT' key or joystick button", "Press 'RIGHT' key", "Press 'FIRE' key");
      my $keyMenuItem = new MenuItem( 100, $yPositions[$self->{currentItemIndex}], $prompts[0] );
      push @::GameObjects, ($keyMenuItem);
      $keyMenuItem->Select;
      while (1) {
        $self->MenuAdvance();
        if ($self->{abortgame}) {
          $self->{abortgame} = 0;
          goto endOfKeyEntry;
        }
        if (%::Events) {
          my ($event) = %::Events;
          if ($event =~ /^B(\d+)$/) {
            $player->{keys} = ["L$1", "R$1", "B$1"];
            last;
          }
          $player->{keys}->[$key] = $event;
          ++$key;
          last if $key >= 3;
          $keyMenuItem->SetText($prompts[$key]);
        }
      }
      
      $keyMenuItem->SetText('Select character');
      my $guy = new Guy($player);
      $guy->{x} = $keyMenuItem->{targetX} + $keyMenuItem->{w} + 10;
      $guy->{y} = $keyMenuItem->{targetY} - 10;
      $guy->DemoMode();
      splice @::GameObjects, -2, 0, $guy;
      while (1) {
        $self->MenuAdvance();
        if ($self->{abortgame}) {
          $self->{abortgame} = 0;
          goto endOfKeyEntry;
        }
        if ($::Events{$player->{keys}->[0]}) {
          --$player->{imagefileindex};
          $player->{imagefileindex} = $#::GuyImageFiles if $player->{imagefileindex} < 0;
          &::MakeGuySurface($player); $guy->{surface} = $player->{guySurface};
        } elsif ($::Events{$player->{keys}->[1]}) {
          ++$player->{imagefileindex};
          $player->{imagefileindex} = 0 if $player->{imagefileindex} > $#::GuyImageFiles;
          &::MakeGuySurface($player); $guy->{surface} = $player->{guySurface};
        } elsif ($::Events{$player->{keys}->[2]}) {
          last;
        }
      }
      
      $keyMenuItem->SetText('Select color');
      while (1) {
        $self->MenuAdvance();
        if ($self->{abortgame}) {
          $self->{abortgame} = 0;
          goto endOfKeyEntry;
        }
        if ($::Events{$player->{keys}->[0]}) {
          --$player->{colorindex};
          $player->{colorindex} = $#::GuyColors if $player->{colorindex} < 0;
          &::MakeGuySurface($player); $guy->{surface} = $player->{guySurface};
        } elsif ($::Events{$player->{keys}->[1]}) {
          ++$player->{colorindex};
          $player->{colorindex} = 0 if $player->{colorindex} > $#::GuyColors;
          &::MakeGuySurface($player); $guy->{surface} = $player->{guySurface};
        } elsif ($::Events{$player->{keys}->[2]}) {
          last;
        }
      }
      
      endOfKeyEntry:
      $guy->Delete() if $guy;
      $self->{currentItem}->Show();
      $self->{currentItem}->Select;
      $keysAsText->SetText(&KeysToText($player->{keys}));
      $keysAsText->Show;
      $keyMenuItem->HideAndDelete;
    }
  }
  
  foreach my $menuItem (@keysAsText) { $menuItem->HideAndDelete(); }
  $self->LeaveSubMenu($recall);
  $self->{title}->Show();
}

sub UpdateMenu {
  my $self = shift;

  $self->{menuItems}->[1]->SetText("< Number of Players: $::NumGuys >");
}

sub Run {
  my $self = shift;
  my ($oldBallSurface, $oldHexaSurface) = ($::BallSurfaces{ball}, $::BallSurfaces{hexa});
  my ($y, $yinc, $menuFont, $idle, $retval);

  ($::BallSurfaces{ball}, $::BallSurfaces{hexa}) = ($::BallSurfaces{darkball}, $::BallSurfaces{darkhexa});
  ($y, $yinc) = ($::ScreenHeight + 15, 20);
  $::Background->print( 10, $y += $yinc, "Pang Zero $::Version Copyright (C) 2006 by UPi (upi\@sourceforge.net)" ) if $y + $yinc * 2 < $::PhysicalScreenHeight;
  $::Background->print( 10, $y += $yinc, "Use cursor keys to navigate menu, Enter to select" ) if $y + $yinc * 2 < $::PhysicalScreenHeight;
  $::Background->print( 10, $y += $yinc, "P pauses the game, Esc quits" ) if $y + $yinc * 2 < $::PhysicalScreenHeight;
  $::Background->blit(0, $::App, 0);

  $menuFont = new SDL::Font("$::DataDir/font2.png");
  @::GameObjects = ();
  $::GameSpeed = 1.0;
  $::GamePause = 0;
  $retval = 'game';

  ($y, $yinc) = (90, 40);

  $self->{menuItems} = [
    new MenuItem( 100, $y += $yinc, "Start Game" ),
    new MenuItem(  68, $y += $yinc + 5, "<>"),  # number of players
    new MenuItem( 100, $y += $yinc, "Options" ),
    new MenuItem( 100, $y += $yinc, "Setup controls" ),
    new MenuItem( 100, $y += $yinc, "Help" ),
    new MenuItem( 100, $y += $yinc, "Credits" ),
    new MenuItem( 100, $y += $yinc, "Exit Game" ),
  ];

  $self->UpdateMenu();
  $self->{title} = new MenuItem( 300,  60, "PANG ZERO" );
  $self->{title}->{filled} = 1;
  $self->{title}->{fillcolor} = new SDL::Color(-b=>255, -g=>128);
  $self->{title}->Center();

  push @::GameObjects, (
    &Ball::Spawn($::BallDesc[8], -1, 1),
    &Ball::Spawn($::BallDesc[0], -1, 0),
    &Ball::Spawn($::BallDesc{super0}, -1, 1),
    &Ball::Spawn($::BallDesc[2], -1, 0),
    &Ball::Spawn($::BallDesc[5], -1, 1),
    $self->{title},
    @{$self->{menuItems}},
  );

  $self->SetCurrentItemIndex( 0 );
  &GameTimer::ResetTimer();

  while (1) {
    $self->MenuAdvance();
    $self->Exit() if $self->{abortgame};
    $self->HandleUpDownKeys();
    if ($::MenuEvents{LEFT} and $self->{currentItemIndex} == 1) {
      --$::NumGuys if $::NumGuys > 1;
      $self->UpdateMenu();
    }
    if ($::MenuEvents{RIGHT} and $self->{currentItemIndex} == 1) {
      ++$::NumGuys if $::NumGuys < 6;
      $self->UpdateMenu();
    }
    if ($::MenuEvents{BUTTON}) {
      last if $self->{currentItemIndex} == 0; # Start game
      if ($self->{currentItemIndex} == 2) {
        $self->RunOptionsMenu();
      } elsif ($self->{currentItemIndex} == 3) {
        $self->RunControlsMenu();
      } elsif ($self->{currentItemIndex} == 4) {
        $retval = 'demo';
        last;
      } elsif ($self->{currentItemIndex} == 5) {
        $self->RunCredits();
      }
      $self->Exit() if $self->{currentItemIndex} == 6;
    }
    if (%::Events) {
      $idle = 0;
    } else {
      if (++$idle > 6000) { $retval = 'demo'; last; }
    }
  }
  
  ($::BallSurfaces{ball}, $::BallSurfaces{hexa}) = ($oldBallSurface, $oldHexaSurface);
  $::ScoreFont->use();
  return $retval;
}


##########################################################################
package main;
##########################################################################


sub SaveScreenshot {
  my $i = 0;
  my $filename;
  do { $filename = sprintf("screenshot%03d.bmp", $i); ++$i } while (-f $filename);
  $App->save_bmp($filename);
}

sub Pause {
  my $pausedSurface = new SDL::Surface(-name => "$DataDir/paused.png");
  my $event = new SDL::Event;

  $pausedSurface->blit(0, $App, new SDL::Rect(-x => ($PhysicalScreenWidth - $pausedSurface->width) / 2, -y => $PhysicalScreenHeight / 2 - 100));
  $App->sync();
  $::Keys = (); $::Events = ();
  while (1) { # Paused, wait for keypress
    $event->wait();
    last if $event->type() == SDL_KEYDOWN and $event->key_sym == SDLK_p;
    if ($event->type() == SDL_KEYDOWN and $event->key_sym == SDLK_ESCAPE) { $Game->{abortgame} = 1; last; }
    $Game->Exit() if $event->type() == SDL_QUIT;
  }
  $Background->blit(0, $App, 0);
  &GameTimer::ResetTimer();
}

sub HandleEvents {
  my ($readBothJoystickAxes) = @_;
  my ($event, $type);

  $event = new SDL::Event;
  while (1) {
    last unless $event->poll();
    $type = $event->type();

    if ($type == SDL_QUIT) {
      $Game->Exit();
    }
    elsif ($type == SDL_KEYDOWN) {
      my $keypressed = $event->key_sym;
      if ($keypressed == SDLK_ESCAPE) {
        $Game->{abortgame} = 1;
      } elsif ($keypressed == SDLK_F1) {
        &SaveScreenshot();
      } elsif ($keypressed == SDLK_p) {
        &Pause();
      } else {
        $Keys{$keypressed} = 1;
        $Events{$keypressed} = 1;
        $MenuEvents{UP} = 1 if $keypressed == SDLK_UP();
        $MenuEvents{DOWN} = 1 if $keypressed == SDLK_DOWN();
        $MenuEvents{LEFT} = 1 if $keypressed == SDLK_LEFT();
        $MenuEvents{RIGHT} = 1 if $keypressed == SDLK_RIGHT();
        $MenuEvents{BUTTON} = 1 if $keypressed == SDLK_RETURN();
      }
    }
    elsif ($type == SDL_KEYUP) {
      my $keypressed = $event->key_sym;
      $Keys{$keypressed} = 0;
    }
  }

  &Joystick::ReadJoystick($readBothJoystickAxes);
}

sub DoMenu {
  my $oldScreenHeight = $ScreenHeight;
  my $oldScreenWidth = $ScreenWidth;
  $ScreenWidth = $PhysicalScreenWidth - $ScreenMargin * 2;
  $ScreenWidth = int($ScreenWidth / 32) * 32;
  &LoadBackground('desert2.png');
  $Background->blit(0, $App, 0);

  $Game = new Menu;
  my $retval = $Game->Run();
  &SaveConfig();

  $ScreenWidth = $oldScreenWidth;
  $ScreenHeight = $oldScreenHeight;
  &LoadBackground('desert2.png');

  return $retval;
}

sub DoDemo {
  my ($numguys, $difficulty) = ($NumGuys, $DifficultyLevelIndex);

  &LoadBackground('desert2.png');
  $Background->blit(0, $App, 0);

  my $messages = $Game->{messages} = {
    1 => "Use harpoons to pop the balloons",
    140 => "Pop them, and they split in two",
    250 => "Pop them again and again",
    450 => "Popping the smallest ballons makes them disappear",
    610 => "The green Super Ball gives you a lot of free time",
    700 => "Use this time wisely!",
    1150 => "Making a lot of small balls is dangerous! Observe...",
    1500 => "Don't let the balloons touch you!",
    1700 => "Dying also gives you some free time.", 
    1705 => "So does shooting the flashing balloons.",
    1750 => "The yellow Super Ball destroys every balloon",
    2000 => "And now... THE SPECIAL BALL DEMO!",
    2330 => "The Bouncy Ball bounces twice as high as normal balls.",
    2900 => "See?",
    3400 => "The Hexa Ball is weightless and travels in a straight line.",
    4000 => "So does its offspring.",
    4650 => "The blue Water Ball splits every time it bounces.",
    5400 => "This can really cause a tide of small balls!",
    6050 => "The Earthquake Ball will really shake you up.",
    6400 => "Its offspring is not as dangerous, but still annoying.",
    7200 => "Behold, the Death Ball. It cannot be killed!!!",
    7530 => "No, really, it can't! In fact, shooting it makes it multiply.",
    8250 => "Shooting it too much will lead to the Deathball Meltdown.",
    8350 => "If you avoid it for 20 secs, Deathballs will get bored and go away.",
    8353 => "Also, the yellow Super Ball will destroy the Deathballs for you.",
    8950 => "Last but not least: here's the Seeker Ball!",
    9050 => "This ball will stalk you forever.",
    9950 => "Whew! This concludes the Special Ball Demo. Have fun playing!",
  };
  my $record = 0 x 45 . 1 x 28 . 5 x 1 . 1 x 3 . 0 x 22 . 2 x 47 . 6 x 1 . 0 x 73 . 1 x 51 . 5 x 1 . 0 x 64 . 2 x 33 . 6 x 1 . 2 x 1 . 0 x 66 . 2 x 43 . 6 x 1 . 2 x 1 . 0 x 84 . 1 x 16 . 0 x 38 . 4 x 1 . 0 x 79 . 1 x 14 . 5 x 1 . 1 x 3 . 0 x 11 . 4 x 1 . 0 x 16 . 1 x 11 . 5 x 1 . 1 x 1 . 0 x 10 . 4 x 1 . 0 x 20 . 2 x 10 . 6 x 1 . 2 x 1 . 0 x 11 . 4 x 1 . 0 x 14 . 4 x 1 . 0 x 7 . 2 x 7 . 6 x 1 . 0 x 7 . 1 x 7 . 0 x 9 . 4 x 1 . 0 x 20 . 4 x 1 . 0 x 8 . 1 x 13 . 5 x 1 . 0 x 13 . 2 x 10 . 6 x 1 . 2 x 5 . 0 x 16 . 2 x 13 . 0 x 1 . 4 x 1 . 0 x 13 . 4 x 1 . 0 x 20 . 1 x 8 . 0 x 13 . 2 x 11 . 6 x 1 . 0 x 12 . 1 x 11 . 0 x 16 . 4 x 1 . 0 x 46 . 2 x 50 . 6 x 1 . 2 x 4 . 0 x 9 . 4 x 1 . 0 x 13 . 4 x 1 . 0 x 3 . 1 x 8 . 0 x 1 . 4 x 1 . 0 x 7 . 2 x 10 . 6 x 1 . 2 x 7 . 0 x 6 . 4 x 1 . 0 x 7 . 1 x 11 . 5 x 1 . 0 x 13 . 4 x 1 . 0 x 8 . 2 x 11 . 0 x 23 . 1 x 13 . 0 x 17 . 4 x 1 . 0 x 2 . 1 x 9 . 5 x 1 . 1 x 3 . 0 x 36 . 2 x 14 . 0 x 16 . 1 x 12 . 0 x 15 . 4 x 1 . 0 x 89 . 2 x 7 . 6 x 1 . 2 x 19 . 0 x 61 . 2 x 11 . 0 x 21 . 1 x 112 . 5 x 1 . 1 x 4 . 0 x 49 . 4 x 1 . 0 x 135 . 2 x 71 . 0 x 230 . 2 x 21 . 0 x 28 . 2 x 24 . 0 x 18 . 2 x 24 . 0 x 151 . 4 x 1 . 0 x 162 . 1 x 33 . 0 x 180 . 8 x 1 . 0 x 473 . 1 x 8 . 0 x 11 . 1 x 19 . 0 x 49 . 2 x 102 . 6 x 1 . 2 x 2 . 0 x 71 . 2 x 9 . 6 x 1 . 2 x 14 . 0 x 122 . 1 x 21 . 0 x 12 . 4 x 1 . 0 x 280 . 8 x 1 . 0 x 203 . 1 x 144 . 0 x 351 . 2 x 63 . 0 x 275 . 2 x 53 . 0 x 117 . 1 x 18 . 0 x 5 . 2 x 4 . 6 x 1 . 2 x 37 . 6 x 1 . 2 x 15 . 0 x 46 . 2 x 21 . 6 x 1 . 2 x 2 . 0 x 33 . 1 x 18 . 0 x 40 . 8 x 1 . 0 x 327 . 1 x 100 . 0 x 66 . 2 x 11 . 0 x 26 . 2 x 35 . 0 x 141 . 4 x 1 . 0 x 108 . 2 x 46 . 0 x 4 . 4 x 1 . 0 x 51 . 1 x 33 . 0 x 97 . 1 x 27 . 0 x 89 . 8 x 1 . 0 x 137 . 1 x 146 . 0 x 125 . 2 x 52 . 0 x 32 . 2 x 10 . 0 x 33 . 6 x 1 . 2 x 14 . 0 x 23 . 2 x 16 . 0 x 46 . 2 x 24 . 0 x 32 . 4 x 1 . 0 x 20 . 1 x 14 . 0 x 43 . 1 x 49 . 0 x 3 . 4 x 1 . 0 x 54 . 2 x 16 . 6 x 1 . 2 x 1 . 0 x 10 . 1 x 7 . 5 x 1 . 1 x 6 . 0 x 8 . 1 x 2 . 5 x 1 . 1 x 19 . 5 x 1 . 1 x 8 . 0 x 9 . 4 x 1 . 0 x 9 . 2 x 11 . 0 x 50 . 2 x 1 . 6 x 1 . 2 x 11 . 0 x 3 . 2 x 3 . 6 x 1 . 2 x 7 . 0 x 5 . 2 x 2 . 6 x 1 . 2 x 8 . 0 x 4 . 2 x 2 . 6 x 1 . 2 x 6 . 0 x 6 . 2 x 2 . 6 x 1 . 2 x 7 . 0 x 8 . 4 x 1 . 0 x 2 . 2 x 3 . 0 x 12 . 2 x 3 . 6 x 1 . 2 x 8 . 0 x 8 . 4 x 1 . 0 x 1 . 2 x 8 . 0 x 11 . 2 x 2 . 6 x 1 . 2 x 6 . 0 x 64 . 2 x 28 . 0 x 39 . 1 x 8 . 5 x 1 . 1 x 18 . 5 x 1 . 1 x 11 . 0 x 10 . 4 x 1 . 0 x 31 . 2 x 10 . 0 x 67 . 2 x 8 . 0 x 10 . 2 x 12 . 0 x 25 . 2 x 5 . 0 x 26 . 2 x 12 . 0 x 95 . 2 x 22 . 0 x 35 . 8 x 1 . 0 x 487 . 2 x 26 . 0 x 9 . 1 x 110 . 0 x 28 . 2 x 6 . 0 x 33 . 1 x 14 . 0 x 44 . 2 x 149 . 3 x 1 . 1 x 84 . 0 x 7 . 2 x 28 . 0 x 1 . 4 x 1 . 0 x 19 . 1 x 24 . 0 x 21 . 1 x 46 . 5 x 1 . 1 x 47 . 0 x 8 . 2 x 26 . 6 x 1 . 2 x 34 . 0 x 2 . 4 x 1 . 0 x 20 . 2 x 14 . 6 x 1 . 2 x 33 . 0 x 1 . 1 x 17 . 5 x 1 . 1 x 23 . 5 x 1 . 1 x 10 . 0 x 11 . 4 x 1 . 0 x 3 . 1 x 16 . 5 x 1 . 1 x 18 . 5 x 1 . 1 x 13 . 0 x 240;
  my $rand = [533.31, 0.5, 0.31, 0.76, 0.88, 0.4, 1.14, 0.02, 0.97, 0.5, 0.68, 0.23, 0.88];

  $Game = new DemoPlaybackGame( 1, 3, $record, $rand, $messages );
  $Game->Run();
  $Game->RestoreGameSettings();
}

sub DoRecordDemo {
  my ($numguys, $difficulty) = ($NumGuys, $DifficultyLevelIndex);

  &LoadBackground('desert2.png');
  $Background->blit(0, $App, 0);

  $NumGuys = 1;
  &SetDifficultyLevel(3);
  $Game = new DemoRecordGame;
  $Game->Run();
  print "\n\$record = '", $Game->{record}, "';\n";
  print "\$rand = [", join( ', ', @{$Game->{rand}} ), "];\n\n";
  $NumGuys = $numguys;
  &SetDifficultyLevel($difficulty);
}


##########################################################################
# MAIN PROGRAM STARTS HERE
##########################################################################

eval { SDL::Init(SDL_INIT_EVERYTHING()); };
eval { SDL::Init(SDL::INIT_EVERYTHING()); } if $@;  # This is a workaround for SDL_perl 1.2.20

&FindDataDir();
&LoadConfig();
print "Data directory is at '$DataDir'\n";
my $sdlFlags = SDL_ANYFORMAT; # | SDL_HWSURFACE | SDL_DOUBLEBUF | SDL_HWACCEL | SDL_ASYNCBLIT;

($PhysicalScreenWidth, $PhysicalScreenHeight) = &FindVideoMode();
#($PhysicalScreenWidth, $PhysicalScreenHeight) = (848, 480); $FullScreen = 0;

$App = new SDL::App
  -flags => $sdlFlags,
  -title => "Pang Zero $::Version",
  -icon => "$DataDir/icon.png",
  -width => $PhysicalScreenWidth,
  -height => $PhysicalScreenHeight,
  -fullscreen => $FullScreen,
;
eval( 'use SDL::Tool::Graphic; $RotoZoomer = new SDL::Tool::Graphic;' );  # Detect if zoom / rotozoom works

&SDL::ShowCursor(0);

$Background = new SDL::Surface(
  -name =>'',
  -flags=>SDL_SWSURFACE,
  -width => $App->width,
  -height => $App->height,
  -depth => 32,
  -Amask => '0 but true');
$Background->display_format;
$ScoreFont = new SDL::Font("$DataDir/brandybun3.png");

&LoadSurfaces();
&LoadSounds();
&Joystick::InitJoystick();

#&DoDemo() while 1;
#while (1) { &DoRecordDemo(); $::App->delay(2000); }

while (1) {
  my $menuResult = &DoMenu();
  if ($menuResult eq 'demo') {
    &DoDemo();
    next;
  }

#  $Game = new RecordGame;
  $Game = new Game;
  &LoadBackground('desert2.png');
  $Background->blit(0, $App, 0);
  $Game->Run();
next;

  my ($filename, $i) = ('', 1);
  do { $filename = sprintf("record%03d.txt", $i); ++$i } while (-f $filename);
  open RECORD, ">$filename";
  print RECORD "NumGuys = $NumGuys;\nDifficultyLevelIndex = $DifficultyLevelIndex;\nrecord = '$Game->{record}';\n",
    "DeathBallsEnabled = $DeathBallsEnabled;\nEarthquakeBallsEnabled = $EarthquakeBallsEnabled;\n",
    "WaterBallsEnabled = $WaterBallsEnabled;\nSeekerBallsEnabled = $SeekerBallsEnabled;\n",
    'rand = [', join(',', @{$Game->{rand}}), "];\n\n";
  close RECORD;
  
  $Game = new PlaybackGame($NumGuys, $DifficultyLevelIndex, $Game->{record}, $Game->{rand}, {});
  &LoadBackground('desert2.png');
  $Background->blit(0, $App, 0);
  $Game->Run();
  $Game->RestoreGameSettings();
}
