1#!/usr/bin/perl -w 2 3 4$::Cheat = 0; 5$::Version = '1.3'; 6$::DataDir = ''; # Set it to a path to avoid autodetection (e.g. /opt/pangzero/data) 7 8=comment 9 10########################################################################## 11# 12# PANG ZERO 13# Copyright (C) 2006 by UPi <upi at sourceforge.net> 14# 15########################################################################## 16 17This program is free software; you can redistribute it and//or modify 18it under the terms of the GNU General Public License version 2, as 19published by the Free Software Foundation. 20 21This program is distributed in the hope that it will be useful, 22but WITHOUT ANY WARRANTY; without even the implied warranty of 23MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 24GNU General Public License for more details. 25 26You should have received a copy of the GNU General Public License 27along with this program; if not, write to the Free Software 28Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 29 30 31########################################################################## 32# TODO: 33########################################################################## 34* P4 Bonus probability is balldesc based. 35* P3 Tour mode..? 36* P5 Graphics and help for machine gun and power wire 37* P5 Demo of beating the game at 'normal' difficulty 38* P4 Even more forgiving collision detection (?) 39* P4 Smooth numbers in the scoreboard 40* P3 RotoZoomer smooth parameter to eliminate warning.. 41* P3 Set DataDir with command line parameter. 42* P2 Roll your own game 43* P4 Reorg menu: MenuItem->Update(), MenuItem->Left(), MenuItem->Right(), ... 44 45Next release: 46* Sound effect for matrix effect... 47* Handle possible HST corruption if game exits while merging scores 48 49 50########################################################################## 51# QUICK GUIDE FOR WOULD-BE DEVELOPERS 52########################################################################## 53 54' 55This file contains the entire source code of Pang Zero. I know that this is 56an odd design, but it works for me. You can split the file easily if you 57want to. 58 59The parts of the file are organized like this: 60 611. INITIALIZATION OF GLOBAL OBJECTS (configuration, balls, levels, etc) 622. HIGH SCORE TABLE 633. GAME OBJECT PACKAGES 644. UTILITY PACKAGES AND METHODS 655. GAMEBASE AND DESCENDENT PACKAGES (includes the menu) 666. "MAIN" PROGRAM LOOP 67' 68=cut 69 70 71use strict; 72use SDL; 73use SDL::App; 74use SDL::Event; 75use SDL::Surface; 76use SDL::Timer; 77use SDL::Palette; 78use SDL::Sound; 79use SDL::Mixer; 80use SDL::Font; 81use Carp; 82 83 84# SDL objects 85 86use vars qw ( 87 $App $RotoZoomer $Background $ScoreFont $MenuFont $GlossyFont 88 %BallSurfaces 89 $BorderSurface $WhiteBorderSurface $RedBorderSurface $BonusSurface $LevelIndicatorSurface $LevelIndicatorSurface2 90 $WhiteHarpoonSurface 91 %Sounds $Mixer 92); 93 94# Pang Zero variables and objects 95 96use vars qw ( 97 $DataDir $ScreenHeight $ScreenWidth $PhysicalScreenWidth $PhysicalScreenHeight $ScreenMargin 98 $SoundEnabled $MusicEnabled $FullScreen $ShowWebsite 99 $DeathBallsEnabled $EarthquakeBallsEnabled $WaterBallsEnabled $SeekerBallsEnabled $Slippery 100 @DifficultyLevels $DifficultyLevelIndex $DifficultyLevel 101 @WeaponDurations $WeaponDuration $WeaponDurationIndex 102 @GameObjects %GameEvents $GameSpeed $GamePause $Game 103 @Players @GuyImageFiles @GuyColors $NumGuys 104 @BallDesc %BallDesc @ChallengeLevels @PanicLevels 105 $UnicodeMode $LastUnicodeKey %Keys %Events %MenuEvents ); 106 107########################################################################## 108# GLOBAL CONFIGURATION 109########################################################################## 110 111%Sounds = ( 112 'pop' => 'pop.voc', 113 'shoot' => 'shoot.voc', 114 'death' => 'meow.voc', 115 'level' => 'level.voc', 116 'bonuslife' => 'magic.voc', 117 'pause' => 'pop3.voc', 118 'quake' => 'quake.voc', 119); 120 121@DifficultyLevels = ( 122 { 'name' => 'Easy', 'spawnmultiplier' => 1.2, 'speed' => 0.8, 'harpoons' => 5, 'superball' => 0.8, 'bonusprobability' => 0.2, }, 123 { 'name' => 'Normal', 'spawnmultiplier' => 1.0, 'speed' => 1.0, 'harpoons' => 3, 'superball' => 1.0, 'bonusprobability' => 0.1, }, 124 { 'name' => 'Hard', 'spawnmultiplier' => 0.9, 'speed' => 1.2, 'harpoons' => 2, 'superball' => 1.1, 'bonusprobability' => 0.05, }, 125 { 'name' => 'Nightmare','spawnmultiplier' => 0.8, 'speed' => 1.4, 'harpoons' => 2, 'superball' => 1.5, 'bonusprobability' => 0.02, }, 126 { 'name' => 'Miki', 'spawnmultiplier' => 0.4, 'speed' => 1.0, 'harpoons' => 3, 'superball' => 1.0, 'bonusprobability' => 0.1, }, 127); 128&SetDifficultyLevel(1); 129@WeaponDurations = ( 130 { 'name' => 'Short (Default)', 'durationmultiplier' => 1, }, 131 { 'name' => 'Medium', 'durationmultiplier' => 3, }, 132 { 'name' => 'Long', 'durationmultiplier' => 6, }, 133 { 'name' => 'Very Long', 'durationmultiplier' => 12, }, 134 { 'name' => 'Forever', 'durationmultiplier' => 10000, }, 135); 136&SetWeaponDuration(0); 137 138$NumGuys = 1; 139@Players = ( 140 { 'keys' => [SDLK_LEFT, SDLK_RIGHT, SDLK_UP], }, # blue 141 { 'keys' => [SDLK_a, SDLK_d, SDLK_s], }, # red 142 { 'keys' => [SDLK_j, SDLK_l, SDLK_k], }, # green 143 { 'keys' => [SDLK_KP6, SDLK_KP4, SDLK_KP5], }, # pink 144 { 'keys' => [SDLK_KP6, SDLK_KP4, SDLK_KP5], }, # yellow 145 { 'keys' => [SDLK_KP6, SDLK_KP4, SDLK_KP5], }, # cyan 146 { 'keys' => [SDLK_KP6, SDLK_KP4, SDLK_KP5], }, # gray 147 { 'keys' => [SDLK_KP6, SDLK_KP4, SDLK_KP5], }, # snot 148 { 'keys' => [SDLK_KP6, SDLK_KP4, SDLK_KP5], }, # purple 149); 150@GuyImageFiles = ( 'guyChristmas.png', 'guy_danigm.png', 'guy_pix.png', 'guy_pux.png', 'guy_r2.png', 'guy_sonic.png' ); 151@GuyColors = ( [170, 255, 'blue'], [0, 255, 'red'], [85, 255, 'green'], [212, 255, 'pink'], 152 [42, 255, 'yellow'], [128, 255, 'cyan'], [128, 0, 'gray'], [113, 128, 'snot'], [212, 64, 'purple'] ); 153for (my $i=0; $i<=$#Players; ++$i) { 154 $Players[$i]->{number} = $i; 155 $Players[$i]->{colorindex} = $i; 156 $Players[$i]->{imagefileindex} = $i % scalar(@GuyImageFiles); 157} 158 159my (%n0, %n1, %n2, %n3, %n4); 160%n0 = ('popIndex' => 0, 'rect' => new SDL::Rect(-x => 0, -y => 0, -width => 128, -height => 106)); 161%n1 = ('popIndex' => 1, 'rect' => new SDL::Rect(-x => 0, -y => 0, -width => 96, -height => 80)); 162%n2 = ('popIndex' => 2, 'rect' => new SDL::Rect(-x => 0, -y => 0, -width => 64, -height => 53)); 163%n3 = ('popIndex' => 3, 'rect' => new SDL::Rect(-x => 0, -y => 0, -width => 32, -height => 28)); 164%n4 = ('popIndex' => 4, 'rect' => new SDL::Rect(-x => 0, -y => 0, -width => 16, -height => 15)); 165 166@BallDesc = ( 167# Normal balls (n0 .. n4) 168 { 'name' => 'n0', 'class' => 'Ball', 'score' => 2000, 'spawndelay' => 1, 'speedY' => 6.5, %n0, 'surface' => 'ball0', 'nextgen' => 'n1', }, 169 { 'name' => 'n1', 'class' => 'Ball', 'score' => 1000, 'spawndelay' => 0.5, 'speedY' => 5.7, %n1, 'surface' => 'ball1', 'nextgen' => 'n2', }, 170 { 'name' => 'n2', 'class' => 'Ball', 'score' => 800, 'spawndelay' => 0.25, 'speedY' => 5, %n2, 'surface' => 'ball2', 'nextgen' => 'n3', }, 171 { 'name' => 'n3', 'class' => 'Ball', 'score' => 600, 'spawndelay' => 0.12, 'speedY' => 4, %n3, 'surface' => 'ball3', 'nextgen' => 'n4', }, 172 { 'name' => 'n4', 'class' => 'Ball', 'score' => 500, 'spawndelay' => 0.05, 'speedY' => 3, %n4, 'surface' => 'ball4', }, 173# "Bouncy" balls (b0..b2) 174 { 'name' => 'b0', 'class' => 'Ball', 'score' => 1500, 'spawndelay' => 0.5, 'speedY' => 5.7, %n2, 'surface' => 'bouncy2', 'nextgen' => 'b1', }, 175 { 'name' => 'b1', 'class' => 'Ball', 'score' => 750, 'spawndelay' => 0.2, 'speedY' => 5, %n3, 'surface' => 'bouncy3', 'nextgen' => 'b2', }, 176 { 'name' => 'b2', 'class' => 'Ball', 'score' => 500, 'spawndelay' => 0.1, 'speedY' => 4.2, %n4, 'surface' => 'bouncy4' }, 177# Hexas (h0..h2) 178 { 'name' => 'h0', 'class' => 'Hexa', 'score' => 1500, 'spawndelay' => 0.5, 'popIndex' => 5, 'hexa' => 1, 179 'surface' => 'hexa0', 'rect' => new SDL::Rect(-x => 0, -y => 0, -width => 64, -height => 52), 'nextgen' => 'h1', }, 180 { 'name' => 'h1', 'class' => 'Hexa', 'score' => 1000, 'spawndelay' => 0.2, 'popIndex' => 6, 'hexa' => 1, 181 'surface' => 'hexa1', 'rect' => new SDL::Rect(-x => 0, -y => 0, -width => 32, -height => 28), 'nextgen' => 'h2', }, 182 { 'name' => 'h2', 'class' => 'Hexa', 'score' => 500, 'spawndelay' => 0.1, 'popIndex' => 7, 'hexa' => 1, 183 'surface' => 'hexa2', 'rect' => new SDL::Rect(-x => 0, -y => 0, -width => 16, -height => 14), 184 'magicrect' => new SDL::Rect(-x => 48, -y => 0, -width => 16, -height => 14), }, 185# Water ball 186 { 'name' => 'w1', 'class' => 'WaterBall', 'score' => 1500, 'spawndelay' => 0.4, 'speedY' => 5.7, %n1, 'surface' => 'blue1', 'nextgen' => 'w2', }, 187 { 'name' => 'w2', 'class' => 'WaterBall', 'score' => 1000, 'spawndelay' => 0.2, 'speedY' => 5, %n2, 'surface' => 'blue2', 'nextgen' => 'w3', }, 188 { 'name' => 'w3', 'class' => 'WaterBall', 'score' => 800, 'spawndelay' => 0.1, 'speedY' => 4, %n3, 'surface' => 'blue3', 'nextgen' => 'w4', }, 189 { 'name' => 'w4', 'class' => 'WaterBall', 'score' => 600, 'spawndelay' => 0.05, 'speedY' => 3, %n4, 'surface' => 'blue4', }, 190# Fragile 191 { 'name' => 'f0', 'class' => 'FragileBall', 'score' => 1500, 'spawndelay' => 0.8, 'speedY' => 6.5, %n0, 'surface' => 'frag0', 'nextgen' => 'f1', }, 192 { 'name' => 'f1', 'class' => 'FragileBall', 'score' => 1500, 'spawndelay' => 0.4, 'speedY' => 5.7, %n1, 'surface' => 'frag1', 'nextgen' => 'f2', }, 193 { 'name' => 'f2', 'class' => 'FragileBall', 'score' => 1000, 'spawndelay' => 0.2, 'speedY' => 5, %n2, 'surface' => 'frag2', 'nextgen' => 'f3', }, 194 { 'name' => 'f3', 'class' => 'FragileBall', 'score' => 800, 'spawndelay' => 0.1, 'speedY' => 4, %n3, 'surface' => 'frag3', 'nextgen' => 'f4', }, 195 { 'name' => 'f4', 'class' => 'FragileBall', 'score' => 600, 'spawndelay' => 0.05, 'speedY' => 3, %n4, 'surface' => 'frag4', }, 196# Superball 197 { 'name' => 'super0', 'class' => 'SuperBall', 'score' => 1000, 'spawndelay' => 0.5, 'speedY' => 5.7, %n1, 'surface' => 'green1', }, 198 { 'name' => 'super1', 'class' => 'SuperBall', 'score' => 800, 'spawndelay' => 0.25, 'speedY' => 5, %n2, 'surface' => 'green2', }, 199 { 'name' => 'xmas', 'class' => 'XmasBall', 'score' => 1000, 'spawndelay' => 0.5, 'speedY' => 6.5, %n0, 'surface' => 'xmas', }, 200# Death 201 { 'name' => 'death', 'class' => 'DeathBall', 'score' => 0, 'spawndelay' => 0.5, 'speedY' => 5, %n2, 'surface' => 'death2', 'nextgen' => 'death', }, 202# Seeker 203 { 'name' => 'seeker', 'class' => 'SeekerBall', 'score' => 1200, 'spawndelay' => 0.2, 'speedY' => 5.7, %n2, 'surface' => 'white2', 'nextgen' => 'seeker1', }, 204 { 'name' => 'seeker1', 'class' => 'SeekerBall', 'score' => 1200, 'spawndelay' => 0.1, 'speedY' => 5, %n3, 'surface' => 'white3', }, 205# Quake 206 { 'name' => 'quake', 'class' => 'EarthquakeBall', 'score' => 1600, 'spawndelay' => 0.7, 'speedY' => 5.7, %n2, 'surface' => 'quake2', 207 'quake' => 5, 'nextgen' => 'quake1', }, 208 { 'name' => 'quake1', 'class' => 'EarthquakeBall', 'score' => 1200, 'spawndelay' => 0.2, 'speedY' => 5, %n3, 'surface' => 'quake3', 209 'quake' => 3, 'nextgen' => 'quake2', }, 210 { 'name' => 'quake2', 'class' => 'EarthquakeBall', 'score' => 1000, 'spawndelay' => 0.1, 'speedY' => 4.2, %n4, 'surface' => 'quake4', 211 'quake' => 2, }, 212# Upside down ball 213 { 'name' => 'u0', 'class' => 'UpsideDownBall', 'score' => 2000, 'spawndelay' => 1, 'speedY' => 5.8, %n0, 'surface' => 'upside0', 'nextgen' => 'u1', }, 214 { 'name' => 'u1', 'class' => 'UpsideDownBall', 'score' => 1000, 'spawndelay' => 0.5, 'speedY' => 5.8, %n1, 'surface' => 'upside1', 'nextgen' => 'u2', }, 215 { 'name' => 'u2', 'class' => 'UpsideDownBall', 'score' => 800, 'spawndelay' => 0.25, 'speedY' =>5.8, %n2, 'surface' => 'upside2', 'nextgen' => 'u3', }, 216 { 'name' => 'u3', 'class' => 'UpsideDownBall', 'score' => 600, 'spawndelay' => 0.12, 'speedY' =>5.9, %n3, 'surface' => 'upside3', 'nextgen' => 'u4', }, 217 { 'name' => 'u4', 'class' => 'UpsideDownBall', 'score' => 500, 'spawndelay' => 0.05, 'speedY' =>5.9, %n4, 'surface' => 'upside4', }, 218 219 { 'name' => 'credits1', 'class' => 'Ball', 'speedY' => 6.1, 'nextgen' => 'credits1', 'surface' => 'blue3', %n3 }, 220 { 'name' => 'credits2', 'class' => 'Ball', 'speedY' => 6.1, 'nextgen' => 'credits2', 'surface' => 'ball3', %n3 }, 221); 222{ 223 foreach my $ballDesc (@BallDesc) { 224 $ballDesc->{width} = $ballDesc->{rect}->width(); 225 $ballDesc->{height} = $ballDesc->{rect}->height(); 226 $BallDesc{$ballDesc->{name}} = $ballDesc; 227 } 228 foreach my $ballDesc (@BallDesc) { 229 my $nextgen = $ballDesc->{nextgen}; 230 $ballDesc->{nextgen} = $BallDesc{$nextgen} if $nextgen; 231 } 232} 233 234@ChallengeLevels = ( 235 'n4 n4 n4 n4 xmas', 236 'n3 n3 n3', 237 'n2 n2', 238 'b0 b0', 239 'h2 h2 h2 h2 h2 h2', 240 'h0 h0', 241 'n1 f2', 242 'w1 n2', 243 'n0 b0 w1 h0', 244# 10 245 'n1 quake', 246 'n1 b0 quake', 247 'w1 seeker u2', 248 'n0 seeker seeker', 249 'w1 w1', 250 'f1 quake h0', 251 'w1 seeker h0 h0', 252 'n0 w1 w1 b0 h0', 253 'u0 u0 quake', 254 'quake quake w1 b0 h0', 255# 20 256 'death n1 b0', 257 'n4 ' x 24, 258 'w1 w1 w1 f0', 259 'death w1 h0', 260 'n0 n0 u0 seeker h2 h2 b0', 261 'n4 b2 h2 u4 ' x 6, 262 'quake quake quake b0', 263 'h0 h0 h0 h0 h0 h0 h0 h0', 264 'quake seeker f3 n1 b0 b0', 265 'death death w1 f0 n0 u2 h0', 266# 30 267 'n0 n0 u0 u0', 268 'death quake n1', 269 'b0 h0 n2 ' x 3, 270 'w1 w1 w1 w1 f1 f1', 271 'n3 n3 n3 u3 ' x 4, 272 'quake quake seeker seeker n0 f0', 273 'seeker ' x 8, 274 'n0 n1 n2 n3 n4 b0 f2 h0 h1 h2 w1 seeker', 275 'quake quake quake h0 h0 h0 u2', 276 'death quake seeker w1 n0 b0 h0', 277# 40 278 'n0 n1 n2 ' x 3, 279 'death quake seeker u2 ' x 3, 280 'f0 f0', 281 'death quake f0 n1 ' x 2, 282 'h0 ' x 8 . ' f0 f1 ', 283 'death ' x 10, 284 'quake b0 ' x 5, 285 'w1 w1 f0 f1 death', 286 'seeker ' x 13, 287 'n0 u0 w1 f0 quake death ' x 2, 288); 289 290for ( my $i = 0; $i < 10; ++$i) { 291 $ChallengeLevels[$i + 49] = $ChallengeLevels[$i + 9] . ' ' . $ChallengeLevels[$i + 29]; 292 $ChallengeLevels[$i + 59] = $ChallengeLevels[$i + 19] . ' ' . $ChallengeLevels[$i + 39]; 293} 294foreach (@ChallengeLevels) { 295 while (/(\w+)/g) { 296 die "Unknown ball '$1' in challenge '$_'" unless defined $BallDesc{$1}; 297 } 298} 299 300my %BallMixes = ( 301 'easy' => [ qw(n0 2 n1 20 n2 10 n3 3 n4 2 f0 3 f1 5 f2 5 b0 5 b1 2 b2 1 w1 10 h0 5 h1 3 h2 1 quake 1 seeker 2 u1 1 u2 2 u3 4 u4 1) ], 302 'medium' => [ qw(n0 10 n1 20 n2 10 n3 3 n4 2 f0 3 f1 3 b0 10 b1 2 b2 1 w1 15 h0 15 h1 5 h2 1 death 2 quake 5 seeker 10 u0 2 u1 5 u2 5 u3 5) ], 303 'bouncy' => [ qw(n0 20 n1 10 n2 5 n3 1 n4 1 f0 3 f1 3 b0 30 b1 9 b2 1 w1 10 h0 15 h1 5 death 5 quake 10 seeker 15 u0 5 u1 5 u2 1 u3 1) ], 304 'hard' => [ qw(n0 20 n1 10 n2 5 n3 1 f0 5 f1 1 b0 20 b1 2 w1 20 h0 20 h1 5 death 10 quake 15 seeker 20 u0 5 u1 5 u2 1 u3 1) ], 305 'watery' => [ qw(n0 20 n1 10 n2 5 n3 1 n4 1 f0 3 f1 1 b0 10 b1 5 w1 50 h0 15 h1 5 death 5 quake 10 seeker 15 u0 1 u1 5 u2 5 u3 1) ], 306 'hexas' => [ qw(n0 20 n1 10 n2 5 n3 1 f0 3 f1 1 b0 15 b1 2 w1 20 h0 40 h1 15 death 5 quake 10 seeker 15 u0 1 u1 8 u2 2 u3 1) ], 307 'quakes' => [ qw(n0 15 n1 10 n2 5 n3 1 f0 3 f1 1 b0 15 w1 15 h0 20 h1 5 death 5 quake 40 seeker 15 u0 8 u1 1 u2 2 u3 1) ], 308); 309 310sub AddLevels { 311 my ($num, $balls, $gamespeedStart, $gamespeedEnd, $spawndelayStart, $spawndelayEnd) = @_; 312 my ($i, $level); 313 314 for ($i = 0; $i < $num; ++$i) { 315 $level = { 316 'balls' => $balls, 317 'gamespeed' => $gamespeedStart + ($gamespeedEnd - $gamespeedStart) * ($i) / ($num), 318 'spawndelay' => $spawndelayStart + ($spawndelayEnd - $spawndelayStart) * ($i) / ($num), 319 }; 320 push @PanicLevels, ( $level ); 321 } 322} 323 324&AddLevels( 9, $BallMixes{easy}, 0.75, 1.25, 20, 20 ); # 0-9 325&AddLevels( 10, $BallMixes{medium}, 0.7 , 1.3 , 20, 15 ); # 1x 326&AddLevels( 10, $BallMixes{hard}, 0.7 , 1.5 , 15, 15 ); # 2x 327&AddLevels( 10, $BallMixes{hexas}, 1.0 , 1.5 , 15, 12 ); # 3x 328&AddLevels( 10, $BallMixes{watery}, 0.7 , 1.7 , 15, 17 ); # 4x 329&AddLevels( 10, $BallMixes{bouncy}, 1.0 , 2.0 , 12, 12 ); # 5x 330&AddLevels( 10, $BallMixes{quakes}, 1.5 , 2.2 , 13, 8 ); # 6x 331&AddLevels( 10, $BallMixes{hard}, 1.0 , 2.2 , 13, 10 ); # 7x 332&AddLevels( 10, $BallMixes{hexas}, 1.3 , 2.4 , 12, 9 ); # 8x 333&AddLevels( 10, $BallMixes{hard}, 2.0 , 3.0 , 13, 10 ); # 9x 334 335# Set defaults 336 337$ScreenMargin = 16; 338$ScreenWidth = 800 - $ScreenMargin * 2; 339$ScreenHeight = 416; 340$SoundEnabled = 1; 341$MusicEnabled = 1; 342$DeathBallsEnabled = 1; 343$EarthquakeBallsEnabled = 1; 344$WaterBallsEnabled = 1; 345$SeekerBallsEnabled = 1; 346$FullScreen = 1; 347$UnicodeMode = 0; 348$Slippery = 0; 349$ShowWebsite = 0; 350 351 352########################################################################## 353# CONFIG SAVE/LOAD 354########################################################################## 355 356sub IsMicrosoftWindows { 357 return $^O eq 'MSWin32'; 358} 359 360 361sub TestDataDir { 362 return -f "$DataDir/glossyfont.png"; # Should be a file from the latest version. 363} 364 365sub FindDataDir { 366 return if $DataDir and &TestDataDir(); 367 my @guesses = qw( . .. /usr/share/pangzero /usr/share/games/pangzero /usr/local/share/pangzero /opt/pangzero/ /opt/pangzero); 368 foreach my $guess (@guesses) { 369 $DataDir = $guess; 370 return if &TestDataDir(); 371 $DataDir = "$guess/data"; 372 return if &TestDataDir(); 373 } 374 die "Couldn't find the data directory. Please set it manually."; 375} 376 377sub GetConfigFilename { 378 if ( &IsMicrosoftWindows() ) { 379 if ($ENV{USERPROFILE}) { 380 return "$ENV{USERPROFILE}\\pangzero.cfg"; 381 } 382 return "$DataDir/pangzero.cfg"; 383 } 384 if ($ENV{HOME}) { 385 return "$ENV{HOME}/.pangzerorc"; 386 } 387 if (-w $DataDir) { 388 return "$DataDir/pangzero.cfg"; 389 } 390 return "/tmp/pangzero.cfg"; 391} 392 393sub GetConfigVars { 394 my ($i, $j); 395 my @result = qw(NumGuys DifficultyLevelIndex WeaponDurationIndex Slippery MusicEnabled SoundEnabled FullScreen ShowWebsite 396 DeathBallsEnabled EarthquakeBallsEnabled WaterBallsEnabled SeekerBallsEnabled); 397 for ($i=0; $i < scalar @Players; ++$i) { 398 for ($j=0; $j < 3; ++$j) { 399 push @result, ("Players[$i]->{keys}->[$j]"); 400 } 401 push @result, ("Players[$i]->{colorindex}"); 402 push @result, ("Players[$i]->{imagefileindex}"); 403 } 404 my ($difficulty, $gameMode); 405 for ($difficulty=0; $difficulty < scalar @DifficultyLevels; ++$difficulty) { 406 foreach $gameMode ('highScoreTablePan', 'highLevelTablePan', 'highScoreTableCha', 'highLevelTableCha') { 407 next if ($DifficultyLevels[$difficulty]->{name} eq 'Miki' and $gameMode eq 'highScoreTableCha'); 408 for ($i=0; $i < 5; ++$i) { 409 push @result, "DifficultyLevels[$difficulty]->{$gameMode}->[$i]->[0]", # Name of high score 410 "DifficultyLevels[$difficulty]->{$gameMode}->[$i]->[1]", # High score 411 } 412 } 413 } 414 return @result; 415} 416 417sub SaveConfig { 418 my ($filename, $varname, $value); 419 $filename = &GetConfigFilename(); 420 421 open CONFIG, "> $filename" or return; 422 foreach $varname (&GetConfigVars) { 423 eval("\$value = \$$varname"); die $@ if $@; 424 print CONFIG "$varname = $value\n"; 425 } 426 close CONFIG; 427} 428 429sub LoadConfig { 430 my ($filename, $text, $varname); 431 432 $text = ''; 433 $filename = &GetConfigFilename(); 434 if (open CONFIG, "$filename") { 435 read CONFIG, $text, 16384; 436 close CONFIG; 437 } 438 439 foreach $varname (&GetConfigVars) { 440 my $pattern = $varname; 441 $pattern =~ s/\[/\\[/g; 442 if ($text =~ /$pattern = (.+?)$/m) { 443 eval( "\$$varname = '$1'" ); 444 } 445 } 446 &SetDifficultyLevel($DifficultyLevelIndex); 447 &SetWeaponDuration($WeaponDurationIndex); 448} 449 450sub SetDifficultyLevel { 451 my $difficultyLevelIndex = shift; 452 if ($difficultyLevelIndex < 0 or $difficultyLevelIndex > $#DifficultyLevels) { 453 $difficultyLevelIndex = $DifficultyLevelIndex; 454 } 455 $DifficultyLevelIndex = $difficultyLevelIndex; 456 $DifficultyLevel = $DifficultyLevels[$difficultyLevelIndex]; 457} 458 459sub SetWeaponDuration { 460 my $weaponDurationIndex = shift; 461 if ($weaponDurationIndex < 0 or $weaponDurationIndex > $#WeaponDurations) { 462 $weaponDurationIndex = $WeaponDurationIndex; 463 } 464 $WeaponDurationIndex = $weaponDurationIndex; 465 $WeaponDuration = $WeaponDurations[$WeaponDurationIndex]; 466} 467 468 469########################################################################## 470# HIGH SCORE TABLE 471########################################################################## 472 473use vars qw( @UnsavedHighScores ); 474 475foreach (@DifficultyLevels) { 476 $_->{highScoreTablePan} = [ ['UPI', 250000], ['UPI', 200000], ['UPI', 150000], ['UPI', 100000], ['UPI', 50000] ]; 477 $_->{highScoreTablePan} = [ ['UPI', 2500], ['UPI', 2000], ['UPI', 1500], ['UPI', 1000], ['UPI', 500] ] if $_->{name} eq 'Miki'; 478 $_->{highLevelTablePan} = [ ['UPI', 50], ['UPI', 40], ['UPI', 30], ['UPI', 20], ['UPI', 10] ]; 479 $_->{highLevelTablePan} = [ ['UPI', 20], ['UPI', 16], ['UPI', 12], ['UPI', 8], ['UPI', 4] ] if $_->{name} eq 'Miki'; 480 $_->{highScoreTableCha} = [ ['UPI', 250000], ['UPI', 200000], ['UPI', 150000], ['UPI', 100000], ['UPI', 50000] ]; 481 $_->{highLevelTableCha} = [ ['UPI', 30], ['UPI', 25], ['UPI', 20], ['UPI', 15], ['UPI', 10] ]; 482} 483 484sub AddHighScore { 485 my ($player, $score, $level) = @_; 486 487 unshift @UnsavedHighScores, [$player, $score, $level]; 488} 489 490sub MergeUnsavedHighScores { 491 my ($table) = @_; 492 my ($unsavedHighScore, $player, $score, $level); 493 494 die unless ($table =~ /^(Cha|Pan)$/); 495 foreach $unsavedHighScore (@UnsavedHighScores) { 496 ($player, $score, $level) = @{$unsavedHighScore}; 497 &MergeUnsavedHighScore( $DifficultyLevel->{"highScoreTable$table"}, $player, $score ); 498 &MergeUnsavedHighScore( $DifficultyLevel->{"highLevelTable$table"}, $player, $level ); 499 } 500 501 splice @{$DifficultyLevel->{"highScoreTable$table"}}, 5; 502 splice @{$DifficultyLevel->{"highLevelTable$table"}}, 5; 503 @UnsavedHighScores = (); 504 my $newHighScore = &InputPlayerNames($table); 505 if ($newHighScore) { 506 $Game->RunHighScore( $DifficultyLevelIndex, $table, 0 ); 507 } 508} 509 510sub MergeUnsavedHighScore { 511 my ($highScoreList, $player, $score) = @_; 512 my ($i); 513 514 for ($i = 0; $i < scalar @{$highScoreList}; ++$i) { 515 if ($highScoreList->[$i]->[1] < $score) { 516 splice @{$highScoreList}, $i, 0, [$player, $score]; 517 return; 518 } 519 } 520} 521 522sub InputPlayerNames { 523 my ($table) = @_; 524 my ($highScoreEntry, $player, $score, $message, $retval); 525 526 die unless ($table =~ /^(Cha|Pan)$/); 527 $retval = 0; 528 foreach $highScoreEntry (@{$DifficultyLevel->{"highScoreTable$table"}}, @{$DifficultyLevel->{"highLevelTable$table"}}) { 529 $player = $highScoreEntry->[0]; 530 next unless ref $player; 531 unless ($player->{highScoreName}) { 532 $score = $highScoreEntry->[1]; 533 $message = $score < 1000 ? "Level $score" : "Score $score"; 534 $player->{highScoreName} = &InputPlayerName($player, $message); 535 } 536 $highScoreEntry->[0] = $player->{highScoreName}; 537 $retval = 1; 538 } 539 foreach $player (@Players) { 540 delete $player->{highScoreName}; 541 } 542 return $retval; 543} 544 545sub InputPlayerName { 546 my ($player, $message) = @_; 547 my ($guy, $name, $nameMenuItem, @menuItems, $x, $y, $yInc); 548 549 SDL::EnableUnicode(1); $UnicodeMode = 1; 550 $name = ($player->{name} or '') . '|'; 551 552 $guy = new Guy($player); 553 ($guy->{x}, $guy->{y}) = (150, 150); 554 $guy->DemoMode(); 555 556 ($x, $y, $yInc) = (230, 80, 45); 557 push @menuItems, ( 558 new MenuItem( $x, $y += $yInc, "HIGH SCORE!!!"), 559 new MenuItem( $x, $y += $yInc, $message), 560 new MenuItem( $x, $y += $yInc, "Please enter your name:"), 561 $nameMenuItem = new MenuItem( $x, $y += $yInc, $name ), 562 ); 563 push @GameObjects, ($guy, @menuItems); 564 565 while (1) { 566 $LastUnicodeKey = 0; 567 $Game->MenuAdvance(); 568 last if $Game->{abortgame}; 569 if (%Events) { 570 my ($key) = %Events; 571 if ($key == SDLK_BACKSPACE) { 572 substr($name, -2, 1, ''); # Remove next to last char 573 $nameMenuItem->SetText($name); 574 } elsif ($key == SDLK_RETURN) { 575 last; 576 } elsif ($LastUnicodeKey < 127 and $LastUnicodeKey >= 32 and length($name) < 9) { 577 substr($name, -1, 0, chr($LastUnicodeKey)); # Insert before last char 578 $nameMenuItem->SetText($name); 579 } 580 } 581 } 582 $name =~ s/\|$//; 583 $player->{name} = $name; 584 $name = "Anonymous" if $name =~ /^\s*$/; 585 $guy->Delete(); 586 foreach (@menuItems) { $_->Delete(); } 587 SDL::EnableUnicode(0); $UnicodeMode = 0; 588 return $name; 589} 590 591 592########################################################################## 593# GAME OBJECT CLASSES 594########################################################################## 595 596package GameObject; 597package Ball; 598package Hexa; 599package SuperBall; 600package DeathBall; 601package SeekerBall; 602package EarthquakeBall; 603package WaterBall; 604package Pop; 605package GamePause; 606package BonusDrop; 607package SlowEffect; 608package Guy; 609package Harpoon; 610package MachineGun; 611package PowerWire; 612package HalfCutter; 613package DeadGuy; 614 615 616########################################################################## 617package GameObject; 618########################################################################## 619 620sub new { 621 my ($class) = @_; 622 my $self = { 623 'rect' => new SDL::Rect( -x => 0, -y => 0, -width => 0, -height => 0 ), 624 'speedX' => 0, 625 'speedY' => 0, 626 'x' => 0, 627 'y' => 0, 628 'w' => 10, 629 'h' => 10, 630 }; 631 bless $self, $class; 632} 633 634sub Delete { 635 my $self = shift; 636 my ($i); 637 638 for ($i = 0; $i < scalar @::GameObjects; ++$i) { 639 if ($::GameObjects[$i] eq $self) { 640 splice @::GameObjects, $i, 1; 641 last; 642 } 643 } 644 $self->{deleted} = 1; 645 $self->Clear(); 646} 647 648sub Advance { 649 my $self = shift; 650 651 $self->{advance}->($self) if $self->{advance}; 652} 653 654sub Clear { 655 my ($self) = @_; 656 $::Background->blit($self->{rect}, $::App, $self->{rect}); 657} 658 659sub TransferRect { 660 my ($self) = @_; 661 662 $self->{rect}->x($self->{x} + $::ScreenMargin); 663 $self->{rect}->y($self->{y} + $::ScreenMargin); 664 $self->{rect}->width($self->{w}); 665 $self->{rect}->height($self->{h}); 666} 667 668sub Draw { 669 my ($self) = @_; 670 671 $self->TransferRect(); 672 if ($self->{draw}) { 673 $self->{draw}->($self); 674 } else { 675 $::App->fill( $self->{rect}, new SDL::Color(-r => 0x80) ); 676 } 677} 678 679sub SetupCollisions { 680 my ($self) = @_; 681 682 $self->{collisionw} = ($self->{collisionw} or $self->{w}); 683 $self->{collisionh} = ($self->{collisionh} or $self->{h}); 684 $self->{collisionmarginw1} = ( $self->{w} - $self->{collisionw} ) / 2; 685 $self->{collisionmarginw2} = $self->{collisionmarginw1} + $self->{collisionw}; 686 $self->{collisionmarginh1} = ( $self->{h} - $self->{collisionh} ) / 2; 687 $self->{collisionmarginh2} = $self->{collisionmarginh1} + $self->{collisionh}; 688 $self->{centerx} = $self->{w} / 2; 689 $self->{centery} = $self->{y} / 2; 690} 691 692sub Collisions { 693 my ($self, $other) = @_; 694 695 # Bounding box detection 696 697 unless ($self->{collisionmarginw1} and $other->{collisionmarginw1}) { 698 return 0 if $self->{x} >= $other->{x} + $other->{w}; 699 return 0 if $other->{x} >= $self->{x} + $self->{w}; 700 return 0 if $self->{y} >= $other->{y} + $other->{h}; 701 return 0 if $other->{y} >= $self->{y} + $self->{h}; 702 return 1; 703 } 704 705 return 0 if $self->{x} + $self->{collisionmarginw1} >= $other->{x} + $other->{collisionmarginw2}; 706 return 0 if $other->{x} + $other->{collisionmarginw1} >= $self->{x} + $self->{collisionmarginw2}; 707 return 0 if $self->{y} + $self->{collisionmarginh1} >= $other->{y} + $other->{collisionmarginh2}; 708 return 0 if $other->{y} + $other->{collisionmarginh1} >= $self->{y} + $self->{collisionmarginh2}; 709 return 1; 710} 711 712########################################################################## 713package Ball; 714########################################################################## 715 716@Ball::ISA = qw(GameObject); 717$Ball::Gravity = 0.05; 718$Ball::MagicBallRect = new SDL::Rect(-x => 80, -y => 0, -width => 16, -height => 15); 719 720for (my $i=0; $i <= $#::BallDesc; ++$i) { 721 my $desc = $::BallDesc[$i]; 722 $desc->{speedY} = 0 unless $desc->{speedY}; 723 $desc->{bounceY} = $desc->{speedY} * $desc->{speedY} / $Ball::Gravity / 2 unless $desc->{bounceY}; 724} 725 726sub Create { 727 my ($description, $x, $y, $dir) = @_; 728 my ($retval); 729 730 eval("\$retval = new $description->{class}(\@_);"); die $@ if $@; 731 return $retval; 732} 733 734sub Spawn { 735 my ($description, $x, $dir, $hasBonus) = @_; 736 my ($retval); 737 738 $x = $::Game->Rand( $::ScreenWidth - $description->{width} ) if $x < 0; 739 $retval = &Create( $description, $x, -$description->{height} - $::ScreenMargin, $dir ); 740 $retval->GiveMagic() if $retval->{w} > 32; 741 $retval->GiveBonus() if $hasBonus; 742 743 $retval->{spawning} = 1; 744 my $surfaceName = 'dark' . $description->{surface}; 745 $retval->{surface} = $::BallSurfaces{$surfaceName}; 746 die "No surface: $surfaceName" unless $retval->{surface}; 747 return $retval; 748} 749 750sub new { 751 my ($class, $description, $x, $y, $dir) = @_; 752 my ($self); 753 754 $self = new GameObject; 755 %{$self} = ( %{$self}, 756 'x' => $x, 757 'y' => $y, 758 'w' => $description->{width}, 759 'h' => $description->{height}, 760 'surface' => $::BallSurfaces{$description->{surface}}, 761 'hexa' => $description->{hexa} ? 1 : 0, 762 'desc' => $description, 763 'hasmagic' => 0, # true if one of the ball's descendants is magic 764 'ismagic' => 0, # true if the ball IS magic 765 'spawning' => 0, 766 ); 767 $self->{speedX} = $dir > 0 ? 1.3 : -1.3; 768 $self->SetupCollisions(); 769 bless $self, $class; 770} 771 772sub NormalAdvance { 773 my $self = shift; 774 775 $self->{speedY} += $Ball::Gravity * $::GameSpeed unless ($self->{hexa}); 776 $self->{x} += $self->{speedX} * $::GameSpeed; 777 $self->{y} += $self->{speedY} * $::GameSpeed; 778 if ($self->{y} > $::ScreenHeight - $self->{h}) { 779 $self->{y} = $::ScreenHeight - $self->{h}; 780 if ($self->{hexa}) { 781 $self->{speedY} = -abs($self->{speedY}); 782 } else { 783 $self->{speedY} = -$self->{desc}->{speedY}; 784 } 785 $self->Bounce; 786 } 787 if ($self->{y} < 0) { 788 $self->{y} = 0; 789 $self->{speedY} = abs($self->{speedY}); 790 } 791 if ($self->{x} < 0) { 792 $self->{x} = 0; 793 $self->{speedX} = abs( $self->{speedX} ); 794 } 795 if ($self->{x} > $::ScreenWidth - $self->{w}) { 796 $self->{x} = $::ScreenWidth - $self->{w}; 797 $self->{speedX} = -abs( $self->{speedX} ); 798 } 799} 800 801sub SpawningAdvance { 802 my $self = shift; 803 804 $self->{y} += 0.32; 805 if ($self->{y} >= 0) { 806 $self->{spawning} = 0; 807 $self->{surface} = $::BallSurfaces{$self->{desc}->{surface}}, 808 } 809} 810 811sub Advance { 812 my $self = shift; 813 814 unless( $::GamePause > 0 ) { 815 if ($self->{spawning}) { 816 $self->SpawningAdvance(); 817 } else { 818 $self->NormalAdvance(); 819 } 820 } 821 822 $self->CheckCollisions() unless $::Game->{nocollision} or $self->{spawning}; 823} 824 825sub Bounce { 826} 827 828sub CheckCollisions { 829 my $self = shift; 830 my ($harpoon, $guy); 831 832 foreach $harpoon (values %Harpoon::Harpoons) { 833 if ($self->Collisions($harpoon)) { 834 $self->Pop($harpoon->{guy}, $harpoon->{popEffect}); 835 $harpoon->Delete(); 836 return; 837 } 838 } 839 foreach $guy (values %Guy::Guys) { 840 if ($::GamePause <= 0 and $self->Collisions($guy)) { 841 $guy->Kill(); 842 } 843 } 844} 845 846sub Draw { 847 my ($self) = @_; 848 849 return if $::GamePause > 0 and $::GamePause < 100 and (int($::GamePause / 3) % 4) < 2; 850 851 $self->TransferRect(); 852 if ($self->{ismagic} and int($::Game->{anim}/4) % 2) { 853 $::BallSurfaces{ball4}->blit( $Ball::MagicBallRect, $::App, $self->{rect} ); 854 } else { 855 $self->{surface}->blit( $self->{desc}->{rect}, $::App, $self->{rect} ); 856 } 857} 858 859sub Collisions { 860 my ($self, $other) = @_; 861 862 # Bounding box detection 863 864 return unless $self->SUPER::Collisions($other); 865 866 # Circle vs rectangle collision 867 868 my ($centerX, $centerY, $boxAxisX, $boxAxisY, $boxCenterX, $boxCenterY, $distSquare, $distance); 869 $boxAxisX = ($other->{collisionw} or $other->{w}) / 2; 870 $boxAxisY = ($other->{collisionh} or $other->{h}) / 2; 871 $boxCenterX = $other->{x} + $other->{w} / 2; 872 $boxCenterY = $other->{y} + $other->{h} / 2; 873 $centerX = $self->{x} + $self->{w} / 2; 874 $centerY = $self->{y} + $self->{h} / 2; 875 876 # Translate coordinates to the box center 877 $centerX -= $boxCenterX; 878 $centerY -= $boxCenterY; 879 $centerX = abs($centerX); 880 $centerY = abs($centerY); 881 882 if ($centerX < $boxAxisX) { 883 return 1 if $centerY < $boxAxisY + $self->{h} / 2; 884 return 0; 885 } 886 if ($centerY < $boxAxisY) { 887 return 2 if $centerX < $boxAxisX + $self->{w} / 2; 888 return 0; 889 } 890 $distSquare = ($centerX-$boxAxisX) * ($centerX-$boxAxisX); 891 $distSquare+= ($centerY-$boxAxisY) * ($centerY-$boxAxisY); 892 return 3 if $distSquare < $self->{h} * $self->{h} / 4; 893 894 return 0; 895} 896 897sub Pop { 898 my ($self, $guy, $popEffect) = @_; 899 900 Carp::confess "no $popEffect" unless defined $popEffect; 901 $::GameEvents{'pop'} = 1; 902 $::GameEvents{'magic'} = 1 if ($self->{ismagic}); 903 $guy->GiveScore($self->{desc}->{score}) if $guy; 904 $self->Delete(); 905 906 goto skipChildren if ($popEffect eq 'meltdown'); 907 908 if ($self->{desc}->{nextgen}) { 909 my @children = $self->SpawnChildren(); 910 if (scalar @children) { 911 $self->AdjustChildren(@children); 912 if ($popEffect eq 'HalfCutter') { 913 push @::GameObjects, ($self->{speedX} > 0 ? $children[1] : $children[0]); 914 } else { 915 push @::GameObjects, (@children); 916 } 917 } 918 } 919 if ($self->{bonus} and $popEffect ne 'superkill') { 920 push @::GameObjects, (new BonusDrop($self)); 921 } 922 $::Game->OnBallPopped(); 923 924 skipChildren: 925 push @::GameObjects, (new Pop($self->{x}, $self->{y}, $self->{desc}->{popIndex}, $self->{surface})); 926} 927 928sub SpawnChildren { 929 my $self = shift; 930 my ($nextgen, $child1, $child2, $x, $y); 931 932 $nextgen = $self->{desc}->{nextgen}; 933 $x = $self->{x} + $self->{w} / 2; 934 $y = $self->{y} + ( $self->{h} - $nextgen->{height} ) / 2; 935 936 $child1 = &Create($nextgen, $self->{x}, $y, 0); 937 $child2 = &Create($nextgen, $self->{x} + $self->{w} - $nextgen->{width}, $y, 1); 938 return ($child1, $child2); 939} 940 941sub AdjustChildren { 942 my ($self, @children) = @_; 943 my ($nextgen, $speedY, $altitude); 944 945 if ($self->{hasmagic}) { 946 $children[0]->GiveMagic(); 947 } 948 949 $nextgen = $self->{desc}->{nextgen}; 950 $altitude = $::ScreenHeight - $self->{y} - $self->{h}; 951 if ($altitude > $nextgen->{bounceY}) { 952 $speedY = 1.8; 953 } else { 954 $speedY = 1.8; 955 while ($speedY * $speedY / $Ball::Gravity / 2 + $altitude < $nextgen->{bounceY}) { 956 ++$speedY; 957 } 958 } 959 foreach (@children) { 960 $_->{speedY} = -$speedY; 961 } 962} 963 964sub GiveMagic { 965 my $self = shift; 966 967 $self->{hasmagic} = 1; 968 $self->{ismagic} = 1 unless $self->{desc}->{nextgen}; 969} 970 971sub GiveBonus { 972 my $self = shift; 973 974 $self->{bonus} = 1; 975} 976 977 978 979########################################################################## 980package Hexa; 981########################################################################## 982 983@Hexa::ISA = qw(Ball); 984 985sub new { 986 my $class = shift; 987 my ($self); 988 989 $self = new Ball(@_); 990 $self->{speedX} = ($::Game->Rand(1.25) + 1.25) * ($self->{speedX} > 0 ? 1 : -1); 991 $self->{speedY} = -4 + abs($self->{speedX}); 992 993 bless $self, $class; 994} 995 996sub Draw { 997 my $self = shift; 998 my ($rect, $srcx, $phase); 999 1000 return if $::GamePause > 0 and $::GamePause < 100 and (int($::GamePause / 3) % 4) < 2; 1001 1002 $self->TransferRect(); 1003 if ($self->{ismagic} and int($::Game->{anim} / 3) % 3 == 0) { 1004 $self->{surface}->blit($self->{desc}->{magicrect}, $::App, $self->{rect}); 1005 } else { 1006 $rect = $self->{desc}->{rect}; 1007 $phase = int($::Game->{anim} / 5) % 3; 1008 $phase = 2 - $phase if $self->{speedX} < 0; 1009 $srcx = $phase * $self->{w}; 1010 $rect->x( $rect->x + $srcx ); 1011 $self->{surface}->blit( $rect, $::App, $self->{rect} ); 1012 $rect->x( $rect->x - $srcx ); 1013 } 1014} 1015 1016sub AdjustChildren { 1017 my ($self, $child1, $child2) = @_; 1018 if ($self->{hasmagic}) { 1019 $child2->GiveMagic(); 1020 } 1021} 1022 1023 1024########################################################################## 1025package WaterBall; 1026########################################################################## 1027 1028@WaterBall::ISA = qw( Ball ); 1029 1030sub Bounce { 1031 my $self = shift; 1032 if ($self->{desc}->{nextgen}) { 1033 $self->{bonus} = 0; 1034 $self->Pop(undef, ''); 1035 } 1036} 1037 1038 1039########################################################################## 1040package FragileBall; 1041########################################################################## 1042 1043@FragileBall::ISA = qw( Ball ); 1044 1045sub Bounce { 1046 my $self = shift; 1047 if ($self->{desc}->{nextgen}) { 1048 $self->{bonus} = 0; 1049 $self->Pop(undef, ''); 1050 } 1051 #$self->{speedX} = ($self->{speedX} > 0) ? 1.3 : -1.3; 1052} 1053 1054sub SpawnChildren { 1055 my $self = shift; 1056 my ($nextgen, $numchildren, @children, $child, $i, $y); 1057 1058 $nextgen = $self->{desc}->{nextgen}; 1059 $numchildren = 2; 1060 while ($nextgen->{nextgen}) { 1061 $nextgen = $nextgen->{nextgen}; 1062 $numchildren *= 2; 1063 } 1064 $y = $self->{y} + ($self->{h} - $nextgen->{height}) / 2; 1065 for ($i = 0; $i < $numchildren; ++$i) { 1066 $child = &Ball::Create($nextgen, $self->{x}, $y, 0); 1067 $child->{speedX} = -1.5 + ($i / ($numchildren-1) * 3); 1068 $child->{x} = $self->{x} + ($self->{w} - $child->{w}) * ($i / ($numchildren-1)); 1069 push @children, $child; 1070 } 1071 return @children; 1072} 1073 1074 1075########################################################################## 1076package SeekerBall; 1077########################################################################## 1078 1079@SeekerBall::ISA = qw( Ball ); 1080 1081sub new { 1082 my $class = shift; 1083 my ($self); 1084 1085 $self = new Ball(@_); 1086 my @guys = grep {ref $_ eq 'Guy'} @::GameObjects; 1087 $self->{target} = $guys[$::Game->Rand(scalar @guys)]; 1088 $self->{deltaX} = (-$self->{w} + $self->{target}->{w}) / 2; 1089 die unless $self->{target}; 1090 1091 bless $self, $class; 1092} 1093 1094sub NormalAdvance { 1095 my $self = shift; 1096 1097 my $multiplier = ($self->{y} > $::ScreenHeight - 120) ? 0 : 25; 1098 unless( $::GamePause > 0 ) { 1099 if ($self->{x} + $self->{speedX} * $multiplier > $self->{target}->{x} + $self->{deltaX}) { 1100 $self->{speedX} -= 0.08; 1101 } else { 1102 $self->{speedX} += 0.08; 1103 } 1104 } 1105 $self->SUPER::NormalAdvance(); 1106} 1107 1108sub AdjustChildren { 1109 my ($self, $child1, $child2) = @_; 1110 1111 $self->SUPER::AdjustChildren($child1, $child2); 1112 $child1->{speedX} *= 2; 1113 $child1->{deltaX} -= 30; 1114 $child1->{target} = $self->{target}; 1115 $child2->{speedX} *= 2; 1116 $child2->{deltaX} += 30; 1117 $child2->{target} = $self->{target}; 1118} 1119 1120sub GiveMagic { 1121} 1122 1123sub Draw { 1124 my $self = shift; 1125 my ($guySurface, $srcrect, $dstrect); 1126 1127 $self->SUPER::Draw(); 1128 $guySurface = $self->{target}->{player}->{guySurface}; 1129 if ($self->{w} <= 32) { 1130 $srcrect = new SDL::Rect(-width => 16, -height => 16, -x =>320, -y => 176); 1131 } else { 1132 $srcrect = new SDL::Rect(-width => 32, -height => 32, -x =>320, -y => 128); 1133 } 1134 $dstrect = new SDL::Rect( 1135 -x => $self->{x} + $::ScreenMargin + ($self->{w} - $srcrect->width()) / 2, 1136 -y => $self->{y} + $::ScreenMargin + ($self->{h} - $srcrect->height()) / 2 + 2); 1137 $guySurface->blit($srcrect, $::App, $dstrect); 1138} 1139 1140 1141########################################################################## 1142package SuperBall; 1143########################################################################## 1144 1145@SuperBall::ISA = qw(Ball); 1146 1147sub new { 1148 my $class = shift; 1149 my ($self); 1150 1151 $self = new Ball(@_); 1152 $self->{effect} = 1; # 0 : superpause; 1 : superkill 1153 bless $self, $class; 1154 $self->SwitchEffect(); 1155 return $self; 1156} 1157 1158sub SwitchEffect { 1159 my $self = shift; 1160 1161 $self->{effect} = 1 - $self->{effect}; 1162 $self->{surface} = $::BallSurfaces{($self->{effect} ? 'gold' : 'green') . ($self->{w} > 64 ? 1 : 2)}; 1163} 1164 1165sub Bounce { 1166 my $self = shift; 1167 1168 $self->SwitchEffect(); 1169} 1170 1171sub SpawnChildren { 1172 return (); 1173} 1174 1175sub Pop { 1176 my $self = shift; 1177 my ($poppedBy) = @_; 1178 1179 $self->SUPER::Pop(@_); 1180 if ($self->{effect} == 0) { 1181 $::GameEvents{superpause} = 1; 1182 } else { 1183 $::GameEvents{superkill} = 1; 1184 $::GameEvents{superkillguy} = $poppedBy; 1185 } 1186} 1187 1188sub GiveMagic { 1189} 1190 1191 1192########################################################################## 1193package XmasBall; 1194########################################################################## 1195 1196@XmasBall::ISA = qw(Ball); 1197 1198sub SpawnChildren { 1199 return (); 1200} 1201 1202sub Pop { 1203 my $self = shift; 1204 my ($bonusdrop, @collectedSubs); 1205 1206 $self->SUPER::Pop(@_); 1207 $bonusdrop = new BonusDrop($self); 1208 @collectedSubs = ( \&OnCollectedLife, \&OnCollectedScore, \&OnCollectedScore, \&OnCollectedInvulnerability, \&OnCollectedInvulnerability ); 1209 if ($::Game->Rand(2 * scalar @collectedSubs) < scalar @collectedSubs) { 1210 $bonusdrop->{desc} = { 'srcRect' => new SDL::Rect(-width=>32, -height=>32, -x=>0, -y=>0), }; 1211 $bonusdrop->SetOnCollectedSub( $collectedSubs[int $::Game->Rand(scalar @collectedSubs)] ); 1212 } 1213 push @::GameObjects, $bonusdrop; 1214} 1215 1216sub GiveMagic { 1217} 1218sub GiveBonus { 1219} 1220 1221sub OnCollectedLife { 1222 my ($bonus, $guy) = @_; 1223 $guy->{player}->{lives}++; 1224 &::PlaySound('bonuslife'); 1225} 1226 1227sub OnCollectedScore { 1228 my ($bonus, $guy) = @_; 1229 $guy->GiveScore(50000); 1230 &::PlaySound('score'); 1231} 1232 1233sub OnCollectedInvulnerability { 1234 my ($bonus, $guy) = @_; 1235 $guy->{invincible} = 500; 1236} 1237 1238 1239########################################################################## 1240package DeathBall; 1241########################################################################## 1242 1243@DeathBall::ISA = qw(Ball); 1244 1245sub new { 1246 my $class = shift; 1247 my ($self); 1248 1249 $self = new Ball(@_); 1250 $self->{expires} = 2000; # 20sec 1251 $self->{speedX} *= 0.9; 1252 bless $self, $class; 1253} 1254 1255sub NormalAdvance { 1256 my $self = shift; 1257 1258 $self->SUPER::NormalAdvance(); 1259 if (--$self->{expires} < 0) { 1260 $self->{bonus} = 1 if $self->{hasmagic}; 1261 $self->Pop(undef, 'expire'); 1262 } 1263 1264} 1265 1266sub Pop { 1267 my ($self, $guy, $popEffect) = @_; 1268 1269 $self->{dontspawn} = 1 if $popEffect eq 'expire' or $popEffect eq 'superkill'; 1270 $self->SUPER::Pop($guy, $popEffect); 1271 if (&CountDeathBalls() > 30) { 1272 $::GameEvents{'meltdown'} = 1; 1273 } 1274} 1275 1276sub SpawnChildren { 1277 my $self = shift; 1278 1279 return if $self->{dontspawn}; 1280 $self->SUPER::SpawnChildren(@_); 1281} 1282 1283sub CountDeathBalls { 1284 my $count = 0; 1285 1286 foreach my $ball (@::GameObjects) { 1287 if (ref($ball) eq 'DeathBall') { ++$count; } 1288 } 1289 return $count; 1290} 1291 1292 1293########################################################################## 1294package EarthquakeBall; 1295########################################################################## 1296 1297@EarthquakeBall::ISA = qw(Ball); 1298 1299sub new { 1300 my $class = shift; 1301 my ($self); 1302 1303 $self = new Ball(@_); 1304 bless $self, $class; 1305} 1306 1307sub CountEarthquakeBalls { 1308 my $count = 0; 1309 1310 foreach my $ball (@::GameObjects) { 1311 if (ref($ball) eq 'EarthquakeBall') { ++$count; } 1312 } 1313 return $count; 1314} 1315 1316sub Bounce { 1317 my $self = shift; 1318 1319 unless ($::GameEvents{earthquake} and $::GameEvents{earthquake} > $self->{desc}->{quake}) { 1320 $::GameEvents{earthquake} = [$self->{desc}->{quake}, $self->{x}]; 1321 } 1322} 1323 1324 1325########################################################################## 1326package UpsideDownBall; 1327########################################################################## 1328 1329@UpsideDownBall::ISA = qw( Ball ); 1330 1331sub NormalAdvance { 1332 my ($self) = @_; 1333 1334 $self->{speedY} = -$self->{speedY}; 1335 $self->{y} = $::ScreenHeight - $self->{h} - $self->{y}; 1336 $self->SUPER::NormalAdvance(); 1337 $self->{speedY} = -$self->{speedY}; 1338 $self->{y} = $::ScreenHeight - $self->{h} - $self->{y}; 1339} 1340 1341 1342########################################################################## 1343package Pop; 1344########################################################################## 1345 1346@Pop::ISA = qw(GameObject); 1347 1348@Pop::Description = ( 1349 { 'xoffset' => 0, 'yoffset' => 0, 'srcx' => 128, 'srcy' => 0, 'sizex' => 128, 'sizey' => 106, }, 1350 { 'xoffset' => 0, 'yoffset' => 0, 'srcx' => 96, 'srcy' => 0, 'sizex' => 96, 'sizey' => 80, }, 1351 { 'xoffset' => 0, 'yoffset' => 0, 'srcx' => 64, 'srcy' => 0, 'sizex' => 64, 'sizey' => 53, }, 1352 { 'xoffset' => 0, 'yoffset' => 0, 'srcx' => 32, 'srcy' => 0, 'sizex' => 32, 'sizey' => 28, }, 1353 { 'xoffset' => 0, 'yoffset' => 0, 'srcx' => 16, 'srcy' => 0, 'sizex' => 16, 'sizey' => 15, }, 1354 1355 { 'xoffset' => 0, 'yoffset' => 0, 'srcx' => 192, 'srcy' => 0, 'sizex' => 64, 'sizey' => 52, }, 1356 { 'xoffset' => 0, 'yoffset' => 0, 'srcx' => 96, 'srcy' => 0, 'sizex' => 32, 'sizey' => 28, }, 1357 { 'xoffset' => 0, 'yoffset' => 0, 'srcx' => 48, 'srcy' => 0, 'sizex' => 16, 'sizey' => 14, }, 1358); 1359 1360sub new { 1361 my ($class, $x, $y, $index, $surface) = @_; 1362 my ($self, $desc); 1363 1364 $desc = $Pop::Description[$index], 1365 $self = new GameObject; 1366 %{$self} = ( %{$self}, 1367 'x' => $x + $desc->{xoffset}, 1368 'y' => $y + $desc->{yoffset}, 1369 'w' => $desc->{sizex}, 1370 'h' => $desc->{sizey}, 1371 'desc' => $desc, 1372 'anim' => 0, 1373 'surface' => $surface, 1374 ); 1375 bless $self, $class; 1376} 1377 1378sub Advance { 1379 my $self = shift; 1380 1381 ++$self->{anim}; 1382 if ($self->{anim} >= 20) { 1383 $self->Delete(); 1384 } 1385} 1386 1387sub Draw { 1388 my $self = shift; 1389 my ($phase, $srcrect); 1390 1391 $self->TransferRect(); 1392 $phase = int($self->{anim} / 5); 1393 $phase = 3 if $phase > 3; 1394 1395 $srcrect = new SDL::Rect( 1396 -x => $self->{desc}->{srcx} + $phase * $self->{w}, 1397 -y => $self->{desc}->{srcy}, 1398 -width => $self->{w}, 1399 -height => $self->{h} ); 1400 $self->{surface}->blit( $srcrect, $::App, $self->{rect} ); 1401} 1402 1403 1404########################################################################## 1405package GamePause; 1406########################################################################## 1407 1408@GamePause::ISA = qw(GameObject); 1409 1410sub Show { 1411 foreach my $gameObject (@::GameObjects) { 1412 return if (ref $gameObject eq 'GamePause'); 1413 } 1414 push @::GameObjects, (new GamePause); 1415} 1416 1417sub new { 1418 my ($class) = @_; 1419 1420 my $self = new GameObject; 1421 my ($width); 1422 $width = &::TextWidth("Time left: 9.999"); 1423 1424 %{$self} = ( %{$self}, 1425 'x' => ($::PhysicalScreenWidth - $width) / 2, 1426 'y' => 100, 1427 'w' => $width, 1428 'h' => 32, 1429 ); 1430 $self->TransferRect(); 1431 bless $self, $class; 1432} 1433 1434sub BringToFront { 1435 my $self = shift; 1436 1437 @::GameObjects = grep { $_ ne $self } @::GameObjects; 1438 push @::GameObjects, ($self); 1439} 1440 1441sub Advance { 1442 my $self = shift; 1443 1444 if ($::GamePause <= 0) { 1445 $self->Delete; 1446 return; 1447 } 1448 unless ($::GameObjects[$#::GameObjects] eq $self) { 1449 $self->BringToFront(); 1450 } 1451} 1452 1453sub Draw { 1454 my $self = shift; 1455 1456 $::App->print( $self->{rect}->x, $self->{rect}->y, "Time left: " . ($::GamePause / 100) ); 1457} 1458 1459 1460########################################################################## 1461package FpsIndicator; 1462########################################################################## 1463 1464@FpsIndicator::ISA = qw(GameObject); 1465 1466sub new { 1467 my ($class) = @_; 1468 1469 my $self = new GameObject; 1470 my ($width); 1471 $width = &::TextWidth("999"); 1472 1473 %{$self} = ( %{$self}, 1474 'x' => $::ScreenWidth - $width + $::ScreenMargin, 1475 'y' => -$::ScreenMargin, 1476 'w' => $width, 1477 'h' => 32, 1478 ); 1479 $self->TransferRect(); 1480 bless $self, $class; 1481} 1482 1483sub Draw { 1484 my $self = shift; 1485 1486 $::App->print( $self->{rect}->x, $self->{rect}->y, &GameTimer::GetFramesPerSecond() ); 1487} 1488 1489 1490########################################################################## 1491package BonusDrop; 1492########################################################################## 1493 1494@BonusDrop::ISA = qw(GameObject); 1495use vars qw(@BonusDesc); 1496 1497@BonusDesc = ( 1498 { 'weaponClass' => 'MachineGun', 'bonusDelay' => 1500, 'srcRect' => new SDL::Rect(-width=>32, -height=>32, -x=>0 , -y=>64), }, 1499 { 'weaponClass' => 'HalfCutter', 'bonusDelay' => 1000, 'srcRect' => new SDL::Rect(-width=>32, -height=>32, -x=>32, -y=>64), }, 1500 { 'weaponClass' => 'PowerWire', 'bonusDelay' => 3000, 'srcRect' => new SDL::Rect(-width=>32, -height=>32, -x=>32, -y=>96), }, 1501 { 'onCollectedSub' => \&OnCollectedSlowEffect, 'srcRect' => new SDL::Rect(-width=>32, -height=>32, -x=>32, -y=>0), }, 1502); 1503 1504 1505sub new { 1506 my ($class, $ball) = @_; 1507 my ($self); 1508 1509 $self = new GameObject; 1510 1511 %{$self} = ( %{$self}, 1512 'x' => $ball->{x} + ($ball->{w} - 32) / 2, 1513 'y' => $ball->{y} + ($ball->{h} - 32) / 2, 1514 'w' => 32, 1515 'h' => 32, 1516 'speedY' => -3, 1517 'speedX' => 0, 1518 'bottomDelay' => 500, 1519 'desc' => $BonusDesc[int $::Game->Rand(scalar @BonusDesc)], 1520 ); 1521 bless $self, $class; 1522} 1523 1524sub Advance { 1525 my $self = shift; 1526 1527 if ($self->{y} >= $::ScreenHeight - $self->{h}) { 1528 $self->{y} = $::ScreenHeight - $self->{h}; 1529 if (--$self->{bottomDelay} < 0) { 1530 $self->Delete(); 1531 } 1532 } else { 1533 $self->{speedY} += 0.1; 1534 $self->{y} += $self->{speedY}; 1535 } 1536 1537 $self->CheckCollisions() if $self->{speedY} >= 0; 1538} 1539 1540sub CheckCollisions { 1541 my $self = shift; 1542 my ($guy, @guysTouched); 1543 1544 foreach $guy (@::GameObjects) { 1545 next unless ref($guy) eq 'Guy'; 1546 next unless $self->Collisions($guy); 1547 push @guysTouched, ($guy); 1548 } 1549 return unless @guysTouched; 1550 $self->Collected($guysTouched[$::Game->Rand( scalar @guysTouched )]); 1551} 1552 1553sub SetOnCollectedSub { 1554 my ($self, $onCollectedSub) = @_; 1555 $self->{onCollectedSub} = $onCollectedSub; 1556} 1557 1558sub Collected { 1559 my ($self, $guy) = @_; 1560 1561 if ($self->{onCollectedSub}) { 1562 $self->{onCollectedSub}->($self, $guy); 1563 } elsif ($self->{desc}->{onCollectedSub}) { 1564 $self->{desc}->{onCollectedSub}->($self, $guy); 1565 } else { 1566 $guy->{weapon} = $self->{desc}->{weaponClass}; 1567 $guy->{bonusDelay} = $self->{desc}->{bonusDelay} * $::WeaponDuration->{durationmultiplier}; 1568 } 1569 $self->Delete(); 1570} 1571 1572sub Draw { 1573 my $self = shift; 1574 1575 return if $self->{bottomDelay} < 100 and (($::Game->{anim} / 4) % 2 < 1); 1576 $self->TransferRect(); 1577 $::BonusSurface->blit($self->{desc}->{srcRect}, $::App, $self->{rect}); 1578} 1579 1580sub OnCollectedSlowEffect { 1581 my ($self, $guy) = @_; 1582 1583 &SlowEffect::RemoveSlowEffects(); 1584 push @::GameObjects, (new SlowEffect()); 1585} 1586 1587 1588########################################################################## 1589package SlowEffect; 1590########################################################################## 1591 1592@SlowEffect::ISA = qw(GameObject); 1593 1594sub new { 1595 my ($class) = @_; 1596 my ($self); 1597 1598 $self = new GameObject; 1599 %{$self} = ( %{$self}, 1600 'timeout' => 1500, # Lasts for 15s 1601 ); 1602 # TODO Play a sound here 1603 bless $self, $class; 1604 return $self; 1605} 1606 1607sub RemoveSlowEffects { 1608 @::GameObjects = grep { ref $_ ne 'SlowEffect' } @::GameObjects; 1609} 1610 1611sub Advance { 1612 my ($self) = @_; 1613 my ($timeout, $slowratio); 1614 1615 $timeout = --$self->{timeout}; 1616 if ( $timeout == 256 ) { 1617 # TODO Play a sound here 1618 } 1619 if ( $timeout > 256 ) { 1620 $::GameSpeed = 0.2; 1621 } elsif ( $timeout > 0 ) { 1622 $::Game->SetGameSpeed(); 1623 $slowratio = int(256 - $timeout) / 256; 1624 $::GameSpeed = $::GameSpeed * $slowratio + 0.2 * (1.0 - $slowratio); 1625 } else { 1626 $::Game->SetGameSpeed(); 1627 $self->Delete(); 1628 return; 1629 } 1630} 1631 1632sub Draw { 1633} 1634 1635sub Clear { 1636} 1637 1638 1639########################################################################## 1640package Guy; 1641########################################################################## 1642 1643@Guy::ISA = qw(GameObject); 1644use vars qw(%Guys $GuyId); 1645 1646sub new { 1647 my ($class, $player) = @_; 1648 my ($self, $number); 1649 1650 $self = new GameObject; 1651 $number = $player->{number}; 1652 1653 %{$self} = ( %{$self}, 1654 'player' => $player, 1655 'number' => $number, 1656 'x' => $player->{startX}, 1657 'y' => $::ScreenHeight - 64, 1658 'w' => 64, 1659 'h' => 64, 1660 'collisionw' => '28', 1661 'collisionh' => '48', 1662 'delay' => 0, 1663 'speedY' => 0, 1664 'speedX' => 0, 1665 'dir' => $number % 2, 1666 'state' => 'idle', 1667 'killed' => 0, 1668 'harpoons' => 0, 1669 'invincible' => 0, 1670 'surface' => $player->{guySurface}, 1671 'whiteSurface' => $player->{whiteGuySurface}, 1672 'weapon' => 'Harpoon', 1673 'bonusDelay' => 0, 1674 'id' => ++$GuyId, 1675 ); 1676 bless $self, $class; 1677 $self->SetupCollisions(); 1678 $self->CalculateAnimPhases(); 1679 $Guys{$self->{id}} = $self; 1680 return $self; 1681} 1682 1683sub Delete { 1684 my $self = shift; 1685 1686 $self->SUPER::Delete; 1687 delete $Guys{$self->{id}}; 1688} 1689 1690sub CalculateAnimPhases { 1691 my $self = shift; 1692 1693 $self->{animPhases} = $self->{player}->{guySurface}->width() / 128, 1694} 1695 1696sub DemoMode { 1697 my ($self) = shift; 1698 $self->{state} = 'demo'; 1699 $self->{dir} = 1; 1700} 1701 1702sub Fire { 1703 my ($self) = @_; 1704 1705 if ($self->{harpoons} < $::DifficultyLevel->{harpoons}) { 1706 ++$self->{harpoons}; 1707 eval("unshift \@::GameObjects, ($self->{weapon}::Create(\$self));"); 1708 $self->{state} = 'shoot'; 1709 $self->{delay} = 7; 1710 ::PlaySound('shoot'); 1711 return 1; 1712 } 1713 return 0; 1714} 1715 1716sub AdvanceWhileFlying { 1717 my $self = shift; 1718 1719 $self->{speedY} += $Ball::Gravity * 2; 1720 $self->{y} += $self->{speedY}; 1721 $self->{x} += $self->{dir} > 0 ? 1 : -1; 1722 if ($self->{x} < -16) { 1723 $self->{x} = 0; $self->{dir} = 1; 1724 } 1725 if ($self->{x} > $::ScreenWidth - $self->{w} + 16) { 1726 $self->{x} = $::ScreenWidth - $self->{w}; $self->{dir} = 0; 1727 } 1728 if ($self->{y} >= $::ScreenHeight - $self->{h}) { 1729 $self->{state} = 'idle'; 1730 $self->{y} = $::ScreenHeight - $self->{h}; 1731 $self->{speedX} = $self->{dir} ? 1 : -1; 1732 } 1733} 1734 1735sub Advance { 1736 my ($self) = @_; 1737 my ($slippery, $keys); 1738 1739 $slippery = $::Slippery ? 0.0625 : 0; 1740 1741 return if $self->{killed}; 1742 return if $self->{state} eq 'demo'; 1743 --$self->{invincible}; 1744 1745 if ($self->{bonusDelay} > 0) { 1746 --$self->{bonusDelay}; 1747 $self->{weapon} = 'Harpoon' if $self->{bonusDelay} <= 0; 1748 } 1749 1750 if ($self->{state} eq 'fly') { 1751 $self->AdvanceWhileFlying(); 1752 return; 1753 } 1754 1755 if ($self->{delay} > 0) { 1756 --$self->{delay}; 1757 $keys = [ 0, 0, 0 ]; 1758 } else { 1759 $keys = $self->{player}->{keys}; 1760 } 1761 1762 $self->{speedX} = 0 unless $slippery; 1763 $self->{state} = 'idle'; 1764 1765 if ( $::Events{$keys->[2]} ) { 1766 return if $self->Fire(); 1767 } 1768 if ( $::Keys{$keys->[0]} ) { 1769 if ($slippery) { 1770 $self->{speedX} -= $slippery * 2 if $self->{speedX} > -3; 1771 } else { 1772 $self->{speedX} = -3; 1773 } 1774 $self->{dir} = 0; 1775 $self->{state} = 'walk'; 1776 } elsif ( $::Keys{$keys->[1]} ) { 1777 if ($slippery) { 1778 $self->{speedX} += $slippery * 2 if $self->{speedX} < 3; 1779 } else { 1780 $self->{speedX} = 3; 1781 } 1782 $self->{dir} = 1; 1783 $self->{state} = 'walk'; 1784 } else { 1785 if ($slippery) { 1786 $self->{speedX} += $slippery if $self->{speedX} < 0; 1787 $self->{speedX} -= $slippery if $self->{speedX} > 0; 1788 } 1789 } 1790 $self->{x} += $self->{speedX}; 1791 1792 if ($self->{x} < -16) { 1793 $self->{x} = -16; $self->{speedX} = 0; 1794 } 1795 if ($self->{x} > $::ScreenWidth - $self->{w} + 16) { 1796 $self->{x} = $::ScreenWidth - $self->{w} + 16; $self->{speedX} = 0; 1797 } 1798} 1799 1800sub Draw { 1801 my ($self) = @_; 1802 my ($surface, $srcrect, $srcx, $srcy, $srcw, $srch); 1803 1804 return if ($self->{killed}); 1805 $surface = $self->{surface}; 1806 $surface = $self->{whiteSurface} if $self->{invincible} > 0 and (int($self->{invincible} / 2) % 3 == 0); 1807 1808 $srcw = $srch = 64; 1809 if ($self->{state} eq 'idle') { 1810 $srcx = $self->{dir} * 128; 1811 $srcy = 64; 1812 } elsif ($self->{state} eq 'walk') { 1813 $srcx = $self->{dir} * $self->{animPhases} * 64 + (int($self->{x} / 50) % $self->{animPhases}) * 64; 1814 $srcy = 0; 1815 } elsif ($self->{state} eq 'demo') { 1816 $srcx = $self->{dir} * $self->{animPhases} * 64 + (int($::Game->{anim} / 16) % $self->{animPhases}) * 64; 1817 $srcy = 0; 1818 } elsif ($self->{state} eq 'shoot') { 1819 $srcx = $self->{dir} * 128 + 64; 1820 $srcx -= 64 if ($self->{delay} <= 1); 1821 $srcy = 64; 1822 } elsif ($self->{state} eq 'fly') { 1823 $srcx = ($self->{dir} > 0 ? 0 : 64); 1824 $srcy = 128; 1825 } 1826 $srcrect = new SDL::Rect( -width => $srcw, -height => $srch, -x => $srcx, -y => $srcy ); 1827 $self->TransferRect(); 1828 $surface->blit($srcrect, $::App, $self->{rect}); 1829} 1830 1831sub Kill { 1832 my ($self) = @_; 1833 1834 return if $::Cheat; 1835 return if $self->{invincible} > 0; 1836 $self->{justkilled} = 1; 1837 $::GameEvents{'kill'} = 1; 1838} 1839 1840sub Earthquake() { 1841 my ($self, $amplitude) = @_; 1842 1843 return if $self->{state} eq 'fly'; 1844 $self->{speedY} = -($amplitude->[0]); 1845 $self->{dir} = $amplitude->[1] > $self->{x} ? 0 : 1; 1846 $self->{state} = 'fly'; 1847 $self->{y} -= 3; 1848} 1849 1850sub DeleteHarpoons { 1851 my ($self) = @_; 1852 my (@gameObjects, $harpoon); 1853 1854 @gameObjects = @::GameObjects; 1855 foreach $harpoon (@gameObjects) { 1856 $harpoon->Delete if ($harpoon->{guy} and $harpoon->{guy} eq $self); 1857 } 1858} 1859 1860sub GiveScore { 1861 my ($self, $score) = @_; 1862 1863 my $player = $self->{player}; 1864 $player->{score} += $score; 1865 if ($player->{score} >= $player->{scoreforbonuslife}) { 1866 ++$player->{lives}; 1867 $player->{scoreforbonuslife} += 200000; 1868 &::PlaySound('bonuslife'); 1869 } 1870} 1871 1872 1873########################################################################## 1874package Harpoon; 1875########################################################################## 1876 1877@Harpoon::ISA = qw(GameObject); 1878use vars qw(%Harpoons $HarpoonId); 1879 1880sub Create { 1881 return new Harpoon(@_); 1882} 1883 1884sub new { 1885 my ($class, $guy) = @_; 1886 my ($self); 1887 1888 $self = new GameObject; 1889 %{$self} = ( %{$self}, 1890 'x' => $guy->{x} + 22, 1891 'y' => $::ScreenHeight - 32, 1892 'w' => 18, 1893 'h' => 32, 1894 'speedY' => -3, 1895 'speedX' => 0, 1896 'guy' => $guy, 1897 'surface' => $guy->{player}->{harpoonSurface}, 1898 'popEffect' => '', 1899 'id' => ++$HarpoonId, 1900 ); 1901 $Harpoons{$self->{id}} = $self; 1902 bless $self, $class; 1903} 1904 1905sub Delete { 1906 my $self = shift; 1907 1908 delete $Harpoons{$self->{id}}; 1909 --$self->{guy}->{harpoons}; 1910 $self->SUPER::Delete(); 1911} 1912 1913sub Advance { 1914 my $self = shift; 1915 1916 if ($self->{y} < 0) { 1917 $self->Delete(); 1918 return; 1919 } 1920 $self->{y} += $self->{speedY}; 1921 $self->{h} = $::ScreenHeight - $self->{y}; 1922} 1923 1924sub GetAnimPhase { 1925 my $self = shift; 1926 1927 return (int($::Game->{anim} / 4) % 3) + 1; 1928} 1929 1930sub Draw { 1931 my $self = shift; 1932 my ($x, $y, $h, $maxh, $dstrect, $srcrect); 1933 1934 $self->TransferRect(); 1935 $y = $self->{y}; 1936 $dstrect = new SDL::Rect( -w => $self->{w}, -x => $self->{x} + $::ScreenMargin ); 1937 $srcrect = new SDL::Rect( -w => $self->{w}, 1938 -x => (0, 64, 32, 96)[ $self->GetAnimPhase() ], -y => 0 ); 1939 $maxh = 160; 1940 1941 # The harpoon needs to be drawn from tile pieces. 1942 # $y iterates from $self->{y} to $::ScreenHeight 1943 # We draw at most $maxh height tiles at a time. 1944 1945 while ($y < $::ScreenHeight) { 1946 $h = $::ScreenHeight - $y; 1947 $h = $maxh if $h > $maxh; 1948 $dstrect->y( $y + $::ScreenMargin ); 1949 $dstrect->height( $h ); 1950 $srcrect->height( $h ); 1951 $self->{surface}->blit( $srcrect, $::App, $dstrect ); 1952 1953 # Prepare for next piece 1954 $y += $h; 1955 $srcrect->y( 32 ); # First piece starts at 0, rest start at 32 1956 $maxh = 128; 1957 } 1958} 1959 1960 1961########################################################################## 1962package MachineGun; 1963########################################################################## 1964 1965@MachineGun::ISA = qw(Harpoon); 1966use vars qw(@SrcRects); 1967 1968@SrcRects = ( 1969 new SDL::Rect( -x=>0, -y=>160, -w=>32, -h=>32 ), 1970 new SDL::Rect( -x=>32, -y=>160, -w=>32, -h=>32 ), 1971 new SDL::Rect( -x=>64, -y=>160, -w=>32, -h=>32 ), 1972); 1973 1974sub Create { 1975 return ( new MachineGun(@_, 0), new MachineGun(@_, 1), new MachineGun(@_, 2) ); 1976} 1977 1978sub new { 1979 my ($class, $guy, $index) = @_; 1980 my ($self); 1981 1982 $self = new Harpoon($guy); 1983 %{$self} = ( %{$self}, 1984 'x' => $guy->{x} + 16, 1985 'y' => $guy->{y} - 16, 1986 'w' => 32, 1987 'h' => 32, 1988 'index' => $index, 1989 'speedY' => -9, 1990 'speedX' => (-2, 0, 2)[$index], 1991 ); 1992 bless $self, $class; 1993} 1994 1995sub Delete { 1996 my $self = shift; 1997 1998 --$self->{guy}->{harpoons} if $self->{index} == 1; 1999 delete $Harpoon::Harpoons{$self->{id}}; 2000 $self->GameObject::Delete(); 2001} 2002 2003sub Advance { 2004 my $self = shift; 2005 2006 if ($self->{y} < 0 2007 or $self->{x} < 0 2008 or $self->{x} > $::ScreenWidth - $self->{w}) { 2009 $self->Delete(); 2010 return; 2011 } 2012 $self->{y} += $self->{speedY}; 2013 $self->{x} += $self->{speedX}; 2014} 2015 2016sub Draw { 2017 my $self = shift; 2018 2019 $self->TransferRect(); 2020 $self->{surface}->blit($SrcRects[$self->{index}], $::App, $self->{rect}); 2021} 2022 2023########################################################################## 2024package PowerWire; 2025########################################################################## 2026 2027@PowerWire::ISA = qw(Harpoon); 2028 2029sub Create { 2030 return new PowerWire(@_); 2031} 2032 2033sub new { 2034 my $class = shift; 2035 my ($self); 2036 2037 $self = new Harpoon(@_); 2038 %{$self} = ( %{$self}, 2039 'topdelay' => 200, 2040 ); 2041 bless $self, $class; 2042} 2043 2044sub Advance { 2045 my $self = shift; 2046 2047 if ($self->{y} > 0) { 2048 return $self->SUPER::Advance(); 2049 } 2050 $self->{y} = 0; 2051 --$self->{topdelay}; 2052 if ($self->{topdelay} <= 0) { 2053 $self->Delete(); 2054 } 2055} 2056 2057sub GetAnimPhase { 2058 my $self = shift; 2059 2060 if ($self->{y} <= 0) { 2061 return 0; 2062 } 2063 return $self->SUPER::GetAnimPhase(); 2064} 2065 2066 2067########################################################################## 2068package HalfCutter; 2069########################################################################## 2070 2071@HalfCutter::ISA = qw(Harpoon); 2072 2073sub Create { 2074 return new HalfCutter(@_); 2075} 2076 2077sub new { 2078 my $class = shift; 2079 my $self = new Harpoon(@_); 2080 $self->{popEffect} = 'HalfCutter'; 2081 $self->{originalSurface} = $self->{surface}; 2082 bless $self, $class; 2083} 2084 2085sub Advance { 2086 my $self = shift; 2087 2088 $self->{surface} = (($::Game->{anim} % 15) < 3) ? $::WhiteHarpoonSurface : $self->{originalSurface}; 2089 $self->SUPER::Advance(); 2090} 2091 2092 2093########################################################################## 2094package DeadGuy; 2095########################################################################## 2096 2097@DeadGuy::ISA = qw(GameObject); 2098 2099sub new { 2100 my ($class, $guy, $dir) = @_; 2101 my ($self, $player); 2102 2103 $self = new GameObject; 2104 $player = $guy->{player}; 2105 2106 %{$self} = ( %{$self}, 2107 'x' => $guy->{x}, 2108 'y' => $guy->{y}, 2109 'w' => 64, 2110 'h' => 64, 2111 'speedY' => -7, 2112 'surface' => $player->{guySurface}, 2113 'anim' => 0, 2114 'bounce' => 0, 2115 'bouncex' => 0, 2116 ); 2117 $self->{'speedX'} = ($::Game->Rand(2) + 1.5) * (($self->{x} > $::ScreenWidth / 2) ? 1 : -1); 2118 bless $self, $class; 2119} 2120 2121sub Advance { 2122 my $self = shift; 2123 2124 $self->{speedY} += 0.1; 2125 $self->{x} += $self->{speedX}; 2126 $self->{y} += $self->{speedY}; 2127 2128 unless ($self->{bouncex}) { 2129 if ($self->{x} < -16) { 2130 $self->{x} = -16; 2131 $self->{speedX} = abs( $self->{speedX} ); 2132 $self->{speedY} = -3 if $self->{speedY} > -3; 2133 $self->{bouncex} = 1; 2134 } 2135 if ($self->{x} > $::ScreenWidth - $self->{w} +16) { 2136 $self->{x} = $::ScreenWidth - $self->{w} + 16; 2137 $self->{speedX} = -abs( $self->{speedX} ); 2138 $self->{speedY} = -3 if $self->{speedY} > -3; 2139 $self->{bouncex} = 1; 2140 } 2141 } 2142 if ($self->{y} > $::ScreenHeight - 64 and not $self->{bounce}) { 2143 $self->{bounce} = 1; 2144 $self->{speedY} = -3; 2145 } 2146 2147 if ($self->{y} > $::PhysicalScreenHeight) { 2148 $self->Delete; 2149 } 2150 $self->{anim} += $self->{speedX} > 0 ? -1 : +1; 2151} 2152 2153sub Draw { 2154 my $self = shift; 2155 my ($srcrect); 2156 2157 $srcrect = new SDL::Rect( 2158 -x => ($self->{speedX} > 0 ? 0 : 64), 2159 -y => 128, 2160 -width => 64, -height => 64 ); 2161 $self->TransferRect(); 2162 if ($::RotoZoomer) { 2163 my $roto = new SDL::Surface( -name =>'', 2164 -flags=>::SDL_SWSURFACE(), -width => 64, -height => 64, -depth => 32); 2165 $self->{surface}->blit( $srcrect, $roto, new SDL::Rect ); 2166 $::RotoZoomer->rotoZoom( $roto, $self->{anim} * 5, 1, $::SmoothRotoZoom ); 2167 $self->{rect}->x( $self->{rect}->x - ($roto->width - 64) / 2 ); 2168 $self->{rect}->y( $self->{rect}->y - ($roto->height - 64) / 2 ); 2169 $roto->blit( 0, $::App, $self->{rect} ); 2170 return; 2171 } else { 2172 $self->{surface}->blit( $srcrect, $::App, $self->{rect} ); 2173 } 2174} 2175 2176 2177########################################################################## 2178package Meltdown; 2179########################################################################## 2180 2181@Meltdown::ISA = qw(GameObject); 2182 2183sub new { 2184 my ($class) = @_; 2185 my ($self, $surface); 2186 2187 $self = new GameObject; 2188 $surface = new SDL::Surface( -name => "$::DataDir/meltdown.png" ); 2189 %{$self} = ( %{$self}, 2190 'x' => ($::ScreenWidth - $surface->width) / 2, 2191 'y' => -$surface->height, 2192 'w' => $surface->width, 2193 'h' => $surface->height, 2194 'speedY' => 0, 2195 'surface' => $surface, 2196 'bounce' => 0, 2197 ); 2198 bless $self, $class; 2199} 2200 2201sub Advance { 2202 my $self = shift; 2203 $self->{speedY} += 0.1; 2204 $self->{y} += $self->{speedY}; 2205 if ($self->{bounce} == 0 and $self->{y} > $::ScreenHeight - $self->{h}) { 2206 $self->{bounce} = 1; 2207 $self->{speedY} = -5; 2208 $self->{y} = $::ScreenHeight - $self->{h}; 2209 } 2210 if ($self->{bounce} and $self->{y} > $::PhysicalScreenHeight) { 2211 $self->Delete; 2212 } 2213} 2214 2215sub Draw { 2216 my $self = shift; 2217 2218 $self->TransferRect(); 2219 $self->{surface}->blit( 0, $::App, $self->{rect} ); 2220} 2221 2222 2223########################################################################## 2224package MenuItem; 2225########################################################################## 2226 2227@MenuItem::ISA = qw(GameObject); 2228use vars qw($Gravity); 2229$Gravity = 0.2; 2230 2231sub new { 2232 my ($class, $x, $y, $text) = @_; 2233 my ($self); 2234 2235 $self = new GameObject; 2236 %{$self} = ( %{$self}, 2237 'targetX' => $x, 2238 'targetY' => $y, 2239 'h' => 42, 2240 'selected' => 0, 2241 'filled' => 0, 2242 'fillcolor' => new SDL::Color(-b=>128), 2243 'parameter' => 0, 2244 'tooltip' => [ @_[4 .. $#_] ], 2245 ); 2246 bless $self, $class; 2247 $self->SetText($text); 2248 $self->SetInitialSpeed(); 2249 return $self; 2250} 2251 2252sub Center { 2253 my $self = shift; 2254 2255 $self->{targetX} = ( $::ScreenWidth - $self->{w} ) / 2; 2256} 2257 2258sub Show { 2259 my $self = shift; 2260 return if $self->CanSelect(); 2261 $self->SetInitialSpeed(); 2262} 2263 2264sub Hide { 2265 my $self = shift; 2266 $self->SUPER::Clear(); 2267 $self->{state} = 'leaving'; 2268 $self->{speedX} = rand(10) - 5; 2269} 2270 2271sub HideAndDelete { 2272 my $self = shift; 2273 $self->Hide(); 2274 $self->{deleteAfterHiding} = 1; 2275} 2276 2277sub Delete { 2278 my $self = shift; 2279 $self->{selected} = $self->{filled} = 0; 2280 $self->SUPER::Delete(); 2281} 2282 2283sub ApproachingSpeed { 2284 my ($position, $speed, $target) = @_; 2285 2286 if ($position + $speed * abs($speed / $Gravity) / 2 > $target) { 2287 return $speed - $Gravity; 2288 } else { 2289 return $speed + $Gravity; 2290 } 2291} 2292 2293sub Advance { 2294 my $self = shift; 2295 2296 if ('entering' eq $self->{state}) { 2297 $self->{x} += $self->{speedX}; 2298 $self->{y} += $self->{speedY}; 2299 $self->{speedX} = &ApproachingSpeed($self->{x}, $self->{speedX}, $self->{targetX}); 2300 $self->{speedY} = &ApproachingSpeed($self->{y}, $self->{speedY}, $self->{targetY}); 2301 if ( abs($self->{x} - $self->{targetX}) + abs($self->{y} - $self->{targetY}) < 2 ) { 2302 $self->{x} = $self->{targetX}; 2303 $self->{y} = $self->{targetY}; 2304 $self->{state} = 'shown'; 2305 } 2306 } elsif ('leaving' eq $self->{state}) { 2307 $self->{x} += $self->{speedX}; 2308 $self->{y} += $self->{speedY}; 2309 $self->{speedY} += $Gravity; 2310 if ($self->{y} > $::PhysicalScreenWidth) { 2311 $self->{state} = 'hidden'; 2312 $self->Delete() if $self->{deleteAfterHiding} 2313 } 2314 } 2315} 2316 2317sub Draw { 2318 my $self = shift; 2319 2320 return if $self->{state} eq 'hidden'; 2321 $self->TransferRect(); 2322 if ($self->{selected} or $self->{filled}) { 2323 $::App->fill($self->{rect}, $self->{fillcolor}); 2324 } 2325 $::App->print($self->{x} + 5 +$::ScreenMargin, $self->{y} + $::ScreenMargin, $self->{text}); 2326} 2327 2328sub SetInitialSpeed { 2329 my $self = shift; 2330 2331 $self->{x} = $self->{targetX} + rand(500) - 250; 2332 $self->{y} = $::PhysicalScreenHeight; 2333 $self->{speedY} = -sqrt( 2 * $Gravity * ($self->{y} - $self->{targetY}) ); 2334 $self->{speedX} = 0; 2335 $self->{state} = 'entering'; 2336} 2337 2338sub InternalSetText { 2339 my ($self, $text) = @_; 2340 2341 $self->SUPER::Clear(); 2342 $self->{text} = $text; 2343 $self->{w} = &::TextWidth($text) + 10; 2344} 2345 2346sub SetText { 2347 my ($self, $text) = @_; 2348 2349 $self->{parameter} = ''; 2350 $self->{basetext} = $text; 2351 $self->InternalSetText($text); 2352} 2353 2354sub SetParameter { 2355 my ($self, $parameter) = @_; 2356 2357 $self->{parameter} = $parameter; 2358 $self->InternalSetText($self->{basetext} . ' ' . $parameter); 2359} 2360 2361sub Select { 2362 my ($self) = @_; 2363 2364 foreach my $item (@::GameObjects) { 2365 $item->{selected} = 0 if ref $item eq 'MenuItem'; 2366 } 2367 $self->{selected} = 1; 2368 $::Game->ShowTooltip( @{$self->{tooltip}} ); 2369} 2370 2371sub CanSelect { 2372 my ($self) = @_; 2373 2374 return $self->{state} =~ /(?:entering|shown)/; 2375} 2376 2377 2378 2379########################################################################## 2380package GameTimer; 2381########################################################################## 2382 2383use vars qw($FirstTick $LastTick $TotalAdvances $LastFpsTick $LastFps $Fps); 2384 2385sub ResetTimer { 2386 $FirstTick = $::App->ticks; 2387 $LastTick = $LastFpsTick = $FirstTick; 2388 $TotalAdvances = 0; 2389 $Fps = $LastFps = 0; 2390} 2391 2392sub GetAdvances { 2393 my ($ticks, $advance); 2394 2395 $ticks = $::App->ticks; 2396 $advance = int(($ticks - $FirstTick) / 10) - $TotalAdvances; 2397 $TotalAdvances += $advance; 2398 2399 # Calculate frames per second; 2400 ++$Fps if $advance > 0; 2401 if ($ticks - $LastFpsTick > 1000) { 2402 $LastFps = $Fps; 2403 $LastFpsTick = $ticks; 2404 $Fps = 0; 2405 } 2406 2407 return $advance; 2408} 2409 2410sub GetFramesPerSecond { 2411 return $LastFps; 2412} 2413 2414 2415########################################################################## 2416package Joystick; 2417########################################################################## 2418 2419use vars qw(@Joysticks @JoystickButtons); 2420 2421sub InitJoystick { 2422 my ($numJoysticks, $joystick, $numButtons, $i); 2423 2424 $numJoysticks = &SDL::NumJoysticks(); 2425 for ($i = 0; $i < $numJoysticks; ++$i) { 2426 print STDERR "Found joystick " , $i+1 , ": " , &SDL::JoystickName($i), "\n"; 2427 $joystick = &SDL::JoystickOpen($i); 2428 next unless $joystick; 2429 $numButtons = &SDL::JoystickNumButtons($joystick); 2430 next unless $numButtons; 2431 push @Joysticks, $joystick; 2432 push @JoystickButtons, $numButtons; 2433 print STDERR "Joystick opened, $numButtons buttons.\n"; 2434 } 2435} 2436 2437sub ReadJoystick { 2438 my ($readBothAxes) = @_; 2439 my ($i, $button, $buttonPressed); 2440 2441 $i = 0; 2442 foreach my $joystick (@Joysticks) { 2443 ++$i; 2444 my $axis = &SDL::JoystickGetAxis($joystick, 0); 2445 if ($axis <= -10000) { 2446 $::Events{"L$i"} = $::MenuEvents{LEFT} = 1 unless $::Keys{"L$i"}; 2447 $::Keys{"L$i"} = 1; 2448 $::Keys{"R$i"} = 0; 2449 } elsif ($axis >= 10000) { 2450 $::Events{"R$i"} = $::MenuEvents{RIGHT} = 1 unless $::Keys{"R$i"}; 2451 $::Keys{"R$i"} = 1; 2452 $::Keys{"L$i"} = 0; 2453 } else { 2454 $::Keys{"L$i"} = 0; 2455 $::Keys{"R$i"} = 0; 2456 } 2457 if ($readBothAxes) { 2458 $axis = &SDL::JoystickGetAxis($joystick, 1); 2459 if ($axis <= -10000) { 2460 $::Events{"U$i"} = $::MenuEvents{UP} = 1 unless $::Keys{"U$i"}; 2461 $::Keys{"U$i"} = 1; 2462 $::Keys{"D$i"} = 0; 2463 } elsif ($axis >= 10000) { 2464 $::Events{"D$i"} = $::MenuEvents{DOWN} = 1 unless $::Keys{"D$i"}; 2465 $::Keys{"D$i"} = 1; 2466 $::Keys{"U$i"} = 0; 2467 } else { 2468 $::Keys{"D$i"} = 0; 2469 $::Keys{"U$i"} = 0; 2470 } 2471 } 2472 $buttonPressed = 0; 2473 for ($button = 0; $button < $JoystickButtons[$i-1]; ++$button) { 2474 if (&SDL::JoystickGetButton($joystick, $button)) { 2475 $buttonPressed = 1; last; 2476 } 2477 } 2478 if ($buttonPressed and not $::Keys{"B$i"}) { 2479 $::Events{"B$i"} = $::MenuEvents{BUTTON} = 1; 2480 } 2481 $::Keys{"B$i"} = $buttonPressed; 2482 } 2483} 2484 2485 2486########################################################################## 2487# PALETTE MANIPULATION 2488########################################################################## 2489 2490package main; 2491 2492sub RgbToHsi { 2493 my ($r, $g, $b) = @_; 2494 my ($min, $max, $delta, $h, $s, $i); 2495 2496 if ($r > $g) { 2497 $max = $r > $b ? $r : $b; 2498 $min = $g < $b ? $g : $b; 2499 } else { 2500 $max = $g > $b ? $g : $b; 2501 $min = $r < $b ? $r : $b; 2502 } 2503 $i = ($min + $max) / 2; 2504 if ($min == $max) { 2505 return (0, 0, $i); 2506 } 2507 2508 $delta = ($max - $min); 2509 if ($i < 128) { 2510 $s = 255 * $delta / ($min + $max); 2511 } else { 2512 $s = 255 * $delta / (511 - $min - $max); 2513 } 2514 2515 if ($r == $max) { 2516 $h = ($g - $b) / $delta; 2517 } elsif ($g == $max) { 2518 $h = 2 + ($b - $r) / $delta; 2519 } else { 2520 $h = 4 + ($r - $g) / $delta; 2521 } 2522 $h = $h * 42.5; 2523 $h += 255 if $h < 0; 2524 $h -= 255 if $h > 255; 2525 2526 return ($h, $s, $i); 2527} 2528 2529sub HsiToRgb { 2530 my ($h, $s, $i) = @_; 2531 my ($m1, $m2); 2532 2533 if ($s < 1) { 2534 $i = int($i + 0.5); 2535 return ($i, $i, $i); 2536 } 2537 2538 if ($i < 128) { 2539 $m2 = ($i * (255 + $s)) / 65025.0; 2540 } else { 2541 $m2 = ($i + $s - ($i * $s) / 255.0) / 255.0; 2542 } 2543 $m1 = ($i / 127.5) - $m2; 2544 2545 return ( 2546 &GetHsiValue( $m1, $m2, $h + 85), 2547 &GetHsiValue( $m1, $m2, $h), 2548 &GetHsiValue( $m1, $m2, $h - 85) 2549 ); 2550} 2551 2552sub GetHsiValue { 2553 my ($n1, $n2, $hue) = @_; 2554 my ($value); 2555 2556 $hue -= 255 if ($hue > 255); 2557 $hue += 255 if ($hue < 0); 2558 if ($hue < 42.5) { 2559 $value = $n1 + ($n2 - $n1) * ($hue / 42.5); 2560 } elsif ($hue < 127.5) { 2561 $value = $n2; 2562 } elsif ($hue < 170) { 2563 $value = $n1 + ($n2 - $n1) * ((170 - $hue) / 42.5); 2564 } else { 2565 $value = $n1; 2566 } 2567 return int($value * 255 + 0.5); 2568} 2569 2570########################################################################## 2571# GRAPHICS-RELATED SUBS 2572########################################################################## 2573 2574package SDL::Surface; 2575 2576sub display_format_alpha { 2577 my $self = shift; 2578 my $tmp = SDL::DisplayFormatAlpha($$self); 2579 SDL::FreeSurface ($$self); 2580 $$self = $tmp; 2581 $self; 2582} 2583 2584package main; 2585 2586sub LoadSurfaces { 2587 my ($i, $transparentColor); 2588 2589 my %balls = qw ( 2590 ball0 Balls-Red128.png ball1 Balls-Red96.png ball2 Balls-Red64.png ball3 Balls-Red32.png ball4 Balls-Red16.png 2591 xmas Balls-XMAS128.png 2592 ball4 Balls-Red16.png ball3 Balls-Red32.png 2593 bouncy2 Balls-Bouncy64.png bouncy3 Balls-Bouncy32.png bouncy4 Balls-Bouncy16.png 2594 hexa0 Hexa-64.png hexa1 Hexa-32.png hexa2 Hexa-16.png 2595 blue1 Balls-Water96.png blue2 Balls-Water64.png blue3 Balls-Water32.png blue4 Balls-Water16.png 2596 frag0 Balls-Fragile128.png frag1 Balls-Fragile96.png frag2 Balls-Fragile64.png frag3 Balls-Fragile32.png frag4 Balls-Fragile16.png 2597 green1 Balls-SuperClock96.png green2 Balls-SuperClock64.png gold1 Balls-SuperStar96.png gold2 Balls-SuperStar64.png 2598 death2 Balls-Death64.png 2599 white2 Balls-Seeker64.png white3 Balls-Seeker32.png 2600 quake2 Balls-EarthQ64.png quake3 Balls-EarthQ32.png quake4 Balls-EarthQ16.png 2601 upside0 Balls-Upside128.png upside1 Balls-Upside96.png upside2 Balls-Upside64.png upside3 Balls-Upside32.png upside4 Balls-Upside16.png 2602 ); 2603 2604 foreach (sort keys %balls) { 2605 $BallSurfaces{$_} = new SDL::Surface( -name => "$DataDir/$balls{$_}" ); 2606 $transparentColor = $BallSurfaces{$_}->pixel(0,0); 2607 $BallSurfaces{$_}->set_color_key(SDL_SRCCOLORKEY, $transparentColor ); 2608 # print join(' ', $_, "\t", $transparentColor->r, $transparentColor->g, $transparentColor->b), "\n"; 2609 $BallSurfaces{$_}->display_format(); 2610 $BallSurfaces{"dark$_"} = new SDL::Surface( -name => "$DataDir/$balls{$_}" ); 2611 $BallSurfaces{"dark$_"}->set_color_key(SDL_SRCCOLORKEY, $BallSurfaces{"dark$_"}->pixel(0,0) ); 2612 $BallSurfaces{"dark$_"}->set_alpha(SDL_SRCALPHA, 128); 2613 $BallSurfaces{"dark$_"}->display_format(); 2614 } 2615 2616 $BorderSurface = new SDL::Surface( -name => "$DataDir/border.png" ); 2617 $RedBorderSurface = new SDL::Surface( -name => "$DataDir/border.png" ); 2618 $WhiteBorderSurface = new SDL::Surface( -name => "$DataDir/border.png" ); 2619 $BonusSurface = new SDL::Surface( -name => "$DataDir/bonus.png" ); 2620 $LevelIndicatorSurface = new SDL::Surface( -name => "$DataDir/level.png" ); 2621 $LevelIndicatorSurface2 = new SDL::Surface( -name => "$DataDir/level_empty.png" ); 2622 2623 &AlterPalette( $RedBorderSurface, sub { 1; }, 2624 sub { shift @_; my ($h, $s, $i) = &RgbToHsi(@_); return &HsiToRgb( $h - 30, $s, $i * 0.75 + 63); } ); 2625 &AlterPalette( $WhiteBorderSurface, sub { 1; }, 2626 sub { shift @_; my ($h, $s, $i) = &RgbToHsi(@_); return &HsiToRgb( 0, 0, $i*0.25 + 191 ); } ); 2627 2628 &MakeGuySurfaces(); 2629} 2630 2631sub MakeGuySurface { 2632 my ($player) = @_; 2633 my ($guySurfaceFile, $guySurface, $whiteGuySurface, $harpoonSurface); 2634 2635 $guySurfaceFile = $DataDir . '/' . $GuyImageFiles[ $player->{imagefileindex} % scalar(@GuyImageFiles) ]; 2636 $guySurface = new SDL::Surface( -name => ($guySurfaceFile) ); 2637 $whiteGuySurface = new SDL::Surface( -name => ($guySurfaceFile) ); 2638 $harpoonSurface = new SDL::Surface( -name => "$DataDir/harpoon.png" ); 2639 $player->{hue} = $GuyColors[$player->{colorindex}]->[0]; 2640 $player->{saturation} = $GuyColors[$player->{colorindex}]->[1]; 2641 2642 &AlterPalette($whiteGuySurface, sub {1;}, sub { return (255, 255, 255); } ); 2643 &AlterPalette( $guySurface, sub { $_[3] > $_[2] and $_[3] > $_[1]; }, 2644 sub { 2645 shift @_; 2646 my ($h, $s, $i) = &RgbToHsi(@_); 2647 return &HsiToRgb($player->{hue}, $player->{saturation}, $i); } 2648 ); 2649 &AlterPalette( $harpoonSurface, sub { 1; }, 2650 sub { 2651 shift @_; 2652 my ($h, $s, $i) = &RgbToHsi(@_); 2653 return &HsiToRgb($player->{hue}, $player->{saturation} * $s / 256, $i); } 2654 ); 2655 $player->{guySurface} = $guySurface; 2656 $player->{whiteGuySurface} = $whiteGuySurface; 2657 $player->{harpoonSurface} = $harpoonSurface; 2658} 2659 2660sub MakeGuySurfaces { 2661 foreach my $player (@Players) { 2662 &MakeGuySurface($player); 2663 } 2664 2665 $WhiteHarpoonSurface = new SDL::Surface( -name => "$DataDir/harpoon.png" ); 2666 &AlterPalette($WhiteHarpoonSurface, sub {1;}, sub { return (255, 255, 255); } ); 2667} 2668 2669sub AlterPalette($$$) { 2670 my ($surface, $filterSub, $alterSub) = @_; 2671 my ($r, $g, $b); 2672 my ($palette, $numColors, $n, $color); 2673 2674 $palette = $surface->palette(); 2675 $numColors = SDL::PaletteNColors($palette); 2676 for ($n = 1; $n < $numColors; ++$n) { 2677 $color = SDL::PaletteColors($palette, $n); 2678 ($r, $g, $b) = ( SDL::ColorR($color), SDL::ColorG($color), SDL::ColorB($color) ); 2679 2680 next unless $filterSub->($n, $r, $g, $b); 2681 ($r, $g, $b) = $alterSub->($n, $r, $g, $b); 2682 $r = $g = $b = 4 if ($r == 0 and $g == 0 and $b == 0); 2683 2684 SDL::PaletteColors($palette, $n, $r, $g, $b); 2685 } 2686 $surface->display_format(); 2687} 2688 2689sub RenderBorder { 2690 my ($borderSurface, $targetSurface) = @_; 2691 my ($dstrect, $srcrect1, $srcrect2, $xpos, $ypos, $width, $height); 2692 2693 $width = $ScreenWidth + 2 * $ScreenMargin; 2694 $height = $ScreenHeight + 2 * $ScreenMargin; 2695 2696 # Draw the corners 2697 $dstrect = new SDL::Rect( -width => 16, -height =>16); 2698 $srcrect1 = new SDL::Rect( -width => 16, -height =>16); 2699 $borderSurface->blit($srcrect1, $targetSurface, $dstrect); 2700 $dstrect->x($width - 16); $srcrect1->x(144); 2701 $borderSurface->blit($srcrect1, $targetSurface, $dstrect); 2702 $dstrect->y($height - 16); $srcrect1->y(144); 2703 $borderSurface->blit($srcrect1, $targetSurface, $dstrect); 2704 $dstrect->x(0); $srcrect1->x(0); 2705 $borderSurface->blit($srcrect1, $targetSurface, $dstrect); 2706 2707 if ($::RotoZoomer) { 2708 # Top border 2709 my $zoom = new SDL::Surface( -name =>'', 2710 -flags=>::SDL_SWSURFACE(), -width => 128, -height => 16, -depth => 32); 2711 $srcrect1->x(16); $srcrect1->y(0); $srcrect1->width(128); $srcrect1->height(16); 2712 $borderSurface->blit( $srcrect1, $zoom, new SDL::Rect ); 2713 $::RotoZoomer->zoom( $zoom, $ScreenWidth / 128, 1, $::SmoothRotoZoom); 2714 $dstrect->x(16); $dstrect->y(0); 2715 $zoom->blit( 0, $targetSurface, $dstrect ); 2716 2717 # Left border 2718 $zoom = new SDL::Surface( -name =>'', 2719 -flags=>::SDL_SWSURFACE(), -width => 16, -height => 128, -depth => 32); 2720 $srcrect1->x(0); $srcrect1->y(16); $srcrect1->height(128); $srcrect1->width(16); 2721 $borderSurface->blit( $srcrect1, $zoom, new SDL::Rect ); 2722 $::RotoZoomer->zoom( $zoom, 1, $ScreenHeight / 128, $::SmoothRotoZoom); 2723 $dstrect->x(0); $dstrect->y(16); 2724 $zoom->blit( 0, $targetSurface, $dstrect ); 2725 } 2726 2727 # Draw top and bottom border 2728 2729 $srcrect1->width(128); $srcrect1->x(16); $srcrect1->y(0); 2730 $srcrect2 = new SDL::Rect( -width => 128, -height => 16, -x => 16, -y => 144 ); 2731 for ($xpos = 16; $xpos < $width-16; ) { 2732 $dstrect->x($xpos); 2733 $dstrect->y(0); 2734 $borderSurface->blit($srcrect1, $targetSurface, $dstrect); 2735 $dstrect->y($height - 16); 2736 $borderSurface->blit($srcrect2, $targetSurface, $dstrect); 2737 $xpos += $srcrect1->width(); 2738 $srcrect1->width(16); $srcrect1->x(128); 2739 $srcrect2->width(16); $srcrect2->x(128); 2740 } 2741 2742 # Draw left and right border 2743 2744 $srcrect1->height(128); $srcrect1->y(16); $srcrect1->x(0); 2745 $srcrect2->height(128); $srcrect2->y(16); $srcrect2->x(144); 2746 for ($ypos = 16; $ypos < $height-16; ) { 2747 $dstrect->x(0); 2748 $dstrect->y($ypos); 2749 $borderSurface->blit($srcrect1, $targetSurface, $dstrect); 2750 $dstrect->x($width - 16); 2751 $borderSurface->blit($srcrect2, $targetSurface, $dstrect); 2752 $ypos += $srcrect1->height(); 2753 $srcrect1->height(16); $srcrect1->y(128); 2754 $srcrect2->height(16); $srcrect2->y(128); 2755 } 2756 2757 if ($::RotoZoomer) { 2758 # Top border 2759 my $zoom = new SDL::Surface( -name =>'', 2760 -flags=>::SDL_SWSURFACE(), -width => 128, -height => 16, -depth => 32); 2761 $srcrect1->x(16); $srcrect1->y(0); $srcrect1->width(128); $srcrect1->height(16); 2762 $borderSurface->blit( $srcrect1, $zoom, new SDL::Rect ); 2763 $::RotoZoomer->zoom( $zoom, $ScreenWidth / 128, 1, $::SmoothRotoZoom); 2764 $dstrect->x(16); $dstrect->y(0); 2765 $zoom->blit( 0, $targetSurface, $dstrect ); 2766 2767 # Left border 2768 $zoom = new SDL::Surface( -name =>'', 2769 -flags=>::SDL_SWSURFACE(), -width => 16, -height => 128, -depth => 32); 2770 $srcrect1->x(0); $srcrect1->y(16); $srcrect1->height(128); $srcrect1->width(16); 2771 $borderSurface->blit( $srcrect1, $zoom, new SDL::Rect ); 2772 $::RotoZoomer->zoom( $zoom, 1, $ScreenHeight / 128, $::SmoothRotoZoom); 2773 $dstrect->x(0); $dstrect->y(16); 2774 $zoom->blit( 0, $targetSurface, $dstrect ); 2775 } 2776} 2777 2778sub LoadBackground { 2779 my $filename = shift; 2780 my ($backgroundImage, $srcrect, $dstrect); 2781 2782 $Background->fill( new SDL::Rect(-width=>$PhysicalScreenWidth, -height=>$PhysicalScreenHeight), new SDL::Color() ); 2783 $backgroundImage = new SDL::Surface(-name => "$DataDir/$filename"); 2784 $dstrect = new SDL::Rect(-x => $ScreenMargin, -y => $ScreenMargin); 2785 $srcrect = new SDL::Rect(-width => $ScreenWidth, -height => $ScreenHeight); 2786 if ($ScreenWidth != $backgroundImage->width() or $ScreenHeight != $backgroundImage->height()) { 2787 if ($::RotoZoomer) { 2788 my $zoomX = $ScreenWidth / $backgroundImage->width(); # $zoomX = 1.0 if $zoomX < 1.0; 2789 my $zoomY = $ScreenHeight / $backgroundImage->height(); # $zoomY = 1.0 if $zoomY < 1.0; 2790 $backgroundImage = $::RotoZoomer->zoom($backgroundImage, $zoomX, $zoomY, $::SmoothRotoZoom); 2791 } 2792 } 2793 $backgroundImage->blit($srcrect, $Background, $dstrect); 2794 2795 &RenderBorder($BorderSurface, $Background); 2796} 2797 2798sub TextWidth { 2799 if (defined(&SDL::App::SDL_TEXTWIDTH)) { 2800 SDL::App::SDL_TEXTWIDTH(@_); # perl-sdl-1.x 2801 } else { 2802 SDL::SFont::SDL_TEXTWIDTH(@_); # perl-sdl-2.x 2803 } 2804} 2805 2806sub FindVideoMode { 2807 if ($FullScreen < 2) { 2808 return (800, 600); 2809 } 2810 2811 # Find a suitable widescreen mode 2812 # One native resolution: 1680 x 1050 => 1.6 : 1 2813 # Which could translate to: 840 x 525 => 1.6 : 1 2814 # Some adapters have: 848 x 480 => 1.76 : 1 2815 # 720 x 480 => 1.5 : 1 2816 # 800 x 512 => 1.56 : 1 2817 # Conclusion: Any resolution where w in [800,900], h > 480 and r in [1.5, 1.8] is good 2818 2819 my ($modes, $mode, @goodModes, $w, $h, $ratio); 2820 $modes = SDL::ListModes( 0, SDL_FULLSCREEN|SDL_HWSURFACE ); 2821 foreach $mode (@{$modes}) { 2822 $w = SDL::RectW($mode); 2823 $h = SDL::RectH($mode); 2824 $ratio = $w / $h; 2825 # print sprintf( "%4d x %4d => %0.3f\n", $w, $h, $ratio ); 2826 next if $w < 800 or $w > 900; 2827 next if $h < 480; 2828 next if $ratio < 1.5 or $ratio > 1.8; 2829 push @goodModes, ( { -w => $w, -h => $h, -score => abs($ratio - 1.6) * 1000 + abs($w - 800) } ); 2830 } 2831 @goodModes = sort { $a->{-score} <=> $b->{-score} } @goodModes; 2832 return (800, 600) unless @goodModes; 2833 foreach $mode (@goodModes) { 2834 print sprintf( '%d x %d => %0.3f (score %d)', $mode->{-w}, $mode->{-h}, $mode->{-w} / $mode->{-h}, $mode->{-score} ), "\n"; 2835 } 2836 return ($goodModes[0]->{-w}, $goodModes[0]->{-h}); 2837} 2838 2839 2840########################################################################## 2841# SOUNDS 2842########################################################################## 2843 2844sub LoadMusic { 2845 my ($filename) = @_; 2846 my ($result); 2847 2848 return undef unless -f $filename; 2849 $result = new SDL::Music($filename); 2850 return undef unless $result; 2851 return $result if $result->isa("SDL::Music"); # SDL_perl 2.? workaround 2852 return undef unless ref $result; 2853 return undef unless $result->{-data}; 2854 return $result; 2855} 2856 2857sub LoadSounds { 2858 $Mixer = eval { SDL::Mixer->new(-frequency => 22050, -channels => 2, -size => 1024); }; 2859 if ($@) { 2860 warn $@; 2861 return 0; 2862 } 2863 2864 my ($soundName, $fileName); 2865 while (($soundName, $fileName) = each %Sounds) { 2866 $Sounds{$soundName} = new SDL::Sound("$DataDir/$fileName"); 2867 } 2868 2869 $::music = LoadMusic("$DataDir/UPiPang.mp3"); 2870 $::music = LoadMusic("$DataDir/UPiPang.mid") unless $::music; 2871 &SetMusicEnabled($MusicEnabled); 2872} 2873 2874sub PlaySound { 2875 return unless $SoundEnabled; 2876 my $sound = shift; 2877 $Mixer and $Sounds{$sound} and $Mixer->play_channel(-1, $Sounds{$sound}, 0); 2878} 2879 2880sub SetMusicEnabled { 2881 return $MusicEnabled = 0 unless $::music; 2882 my $musicEnabled = shift; 2883 2884 $MusicEnabled = $musicEnabled ? 1 : 0; 2885 if ( (not $MusicEnabled) and $Mixer->playing_music() ) { 2886 $Mixer->halt_music(); 2887 } 2888 if ($MusicEnabled and not $Mixer->playing_music()) { 2889 $Mixer->play_music($::music, -1); 2890 } 2891} 2892 2893 2894 2895package PlaybackGame; 2896package RecordGame; 2897package PanicGame; 2898package ChallengeGame; 2899package TutorialGame; 2900package DemoGame; 2901package Menu; 2902 2903########################################################################## 2904package GameBase; 2905########################################################################## 2906 2907sub new { 2908 my ($class) = @_; 2909 my $self = { 2910 'abortgame' => 0, 2911 'anim' => 0, 2912 'nocollision' => 0, 2913 'backgrounds' => [ 'desert2.png', ], 2914 }; 2915 $::GameSpeed = 1.0; 2916 $::GamePause = 0; 2917 bless $self, $class; 2918} 2919 2920sub Exit { 2921 &::ShowWebPage("http://apocalypse.rulez.org/pangzero/Thanks_For_Playing_Pang_Zero_$::Version" ) if $::ShowWebsite ne $::Version; 2922 exit; 2923} 2924 2925sub Rand { 2926 shift; 2927 return rand($_[0]); 2928} 2929 2930sub Delay { 2931 my ($self, $ticks) = @_; 2932 2933 while ($ticks > 0) { 2934 my $advance = $self->CalculateAdvances(); 2935 %::Events = (); 2936 &::HandleEvents(); 2937 return if $self->{abortgame}; 2938 $ticks -= $advance; 2939 $self->DrawGame(); 2940 } 2941} 2942 2943sub SetGameSpeed { 2944} 2945 2946sub SetBackground { 2947 my ($self, $backgroundIndex) = @_; 2948 2949 return if $backgroundIndex >= scalar( @{$self->{backgrounds}} ); 2950 &::LoadBackground($self->{backgrounds}->[$backgroundIndex]); 2951 $::Background->blit(0, $::App, 0); 2952} 2953 2954sub ShowTooltip { 2955} 2956 2957sub ResetGame { 2958 my $self = shift; 2959 2960 @::GameObjects = (); 2961 %Guy::Guys = (); 2962 %Harpoon::Harpoons = (); 2963 $::GamePause = 0; 2964 %::GameEvents = (); 2965 $self->SetBackground(0); 2966} 2967 2968sub CalculateAdvances { 2969 my $advance = &GameTimer::GetAdvances(); 2970 while ($advance <= 0) { 2971 $::App->delay(3); # Wait 3ms = 0.3 game ticks 2972 $advance = &GameTimer::GetAdvances(); 2973 } 2974 if ($advance > 5) { 2975 # print STDERR "advance = $advance!\n"; 2976 $advance = 5; 2977 } 2978 return $advance; 2979} 2980 2981sub AdvanceGameObjects { 2982 my ($self) = @_; 2983 2984 ++$self->{anim}; 2985 foreach my $gameObject (@::GameObjects) { 2986 $gameObject->Advance(); 2987 } 2988} 2989 2990sub OnBallPopped { 2991} 2992 2993sub DrawGame { 2994 my ($self) = @_; 2995 2996 my ($gameObject); 2997 foreach $gameObject (@::GameObjects) { 2998 $gameObject->Clear(); 2999 } 3000 $self->DrawScoreBoard(); 3001 foreach $gameObject (@::GameObjects) { 3002 $gameObject->Draw(); 3003 } 3004 $::App->sync(); 3005} 3006 3007sub DrawScoreBoard() { 3008} 3009 3010 3011########################################################################## 3012package PlayableGameBase; 3013########################################################################## 3014 3015@PlayableGameBase::ISA = qw( GameBase ); 3016 3017sub new { 3018 my ($class) = @_; 3019 my $self = new GameBase; 3020 %{$self} = (%{$self}, 3021 'playersalive' => 0, 3022 'level' => 0, 3023 'backgrounds' => [ qw( desert2.png l1.jpg l2.jpg l3.jpg l4.jpg l5.jpg l6.jpg l7.jpg l8.jpg l9.jpg )], 3024 ); 3025 bless $self, $class; 3026} 3027 3028sub ResetGame { 3029 my $self = shift; 3030 3031 $self->SUPER::ResetGame(); 3032 $self->{playersalive} = 0; 3033 $::GamePause = 0; 3034 3035 foreach my $player (@::Players) { 3036 last if $player->{number} >= $::NumGuys; 3037 $self->SpawnPlayer($player); 3038 } 3039 $self->SetGameLevel(0); 3040 $self->LayoutScoreBoard(); 3041 push @::GameObjects, (new FpsIndicator); 3042} 3043 3044sub SetGameSpeed { 3045 my $self = shift; 3046 3047 $::GameSpeed = 0.8 * $::DifficultyLevel->{speed}; 3048} 3049 3050sub SetGameLevel { 3051 my ($self, $level) = @_; 3052 3053 $self->{level} = $level; 3054 if (($level % 10) == 9) { 3055 $self->SetBackground( int($level / 10) + 1 ); 3056 } 3057 $self->SetGameSpeed(); 3058} 3059 3060sub SpawnPlayer { 3061 my ($self, $player) = @_; 3062 3063 $player->{score} = 0; 3064 $player->{scoreforbonuslife} = 200000; 3065 $player->{lives} = 2; 3066 $player->{startX} = ($::ScreenWidth - $::NumGuys * 60) / 2 + 60 * ($player->{number}+0.5) - 32; 3067 $player->{respawn} = -1; 3068 my $guy = new Guy($player); 3069 push @::GameObjects, ($guy); 3070 ++$self->{playersalive}; 3071 return $guy; 3072} 3073 3074sub AdvanceGameObjects { 3075 my ($self) = @_; 3076 3077 $self->SUPER::AdvanceGameObjects(); 3078 $self->RespawnPlayers(); 3079 --$::GamePause if $::GamePause > 0; 3080} 3081 3082sub RespawnPlayers { 3083 my $self = shift; 3084 3085 foreach my $player (@::Players) { 3086 last if $player->{number} >= $::NumGuys; 3087 if ($player->{respawn} > 0) { 3088 --$player->{respawn}; 3089 $player->{score} = int($player->{respawn} / 100) if $self->{playersalive}; 3090 if ($player->{respawn} <= 0) { 3091 my $guy = $self->SpawnPlayer($player); 3092 $guy->{invincible} = 500; 3093 } 3094 } 3095 } 3096} 3097 3098sub PlayerNextLife { 3099 my ($self, $guy) = @_; 3100 3101 $guy->DeleteHarpoons; 3102 if ($guy->{player}->{lives}--) { 3103 $guy->{x} = $guy->{player}->{startX}; 3104 $guy->{y} = $::ScreenHeight - $guy->{h}; 3105 $guy->{state} = 'idle'; 3106 $guy->{speedY} = $guy->{speedX} = 0; 3107 $guy->{invincible} = 500; # 0.5s 3108 $guy->{killed} = 0; 3109 $guy->{justkilled} = 0; 3110 $self->{playerspawned} = 1; 3111 } else { 3112 # One player less 3113 &::AddHighScore($guy->{player}, $guy->{player}->{score}, $self->{level} + 1); 3114 $guy->Delete(); 3115 --$self->{playersalive}; 3116 $guy->{player}->{respawn} = 6000; # 60s 3117 } 3118} 3119 3120sub PlayerDeathSequence { 3121 my $self = shift; 3122 my (@killedGuys, @deadGuys, $guy, $i); 3123 3124 $self->DrawGame(); 3125 ::PlaySound('death'); 3126 &::RenderBorder($::WhiteBorderSurface, $::App); 3127 $::App->sync(); 3128 $self->Delay(10); 3129 &::RenderBorder($::RedBorderSurface, $::App); 3130 &::RenderBorder($::RedBorderSurface, $::Background); 3131 $::App->sync(); 3132 $self->Delay(90); 3133 3134 @killedGuys = grep { $_->{justkilled}; } @::GameObjects; 3135 foreach $guy (@killedGuys) { 3136 $guy->Clear(); 3137 $guy->{killed} = 1; 3138 push @deadGuys, (new DeadGuy($guy)); 3139 } 3140 push @::GameObjects, (@deadGuys); 3141 3142 for ($i = 0; $i < 300; ++$i) { 3143 &::HandleEvents(); 3144 return if $self->{abortgame}; 3145 my $advance = $self->CalculateAdvances(); 3146 while ($advance--) { 3147 foreach my $gameObject (@deadGuys) { 3148 $gameObject->Advance(); 3149 } 3150 } 3151 $self->DrawGame(); 3152 last if $deadGuys[0]->{deleted}; 3153 } 3154 3155 foreach $guy (@killedGuys) { 3156 $self->PlayerNextLife($guy); 3157 } 3158 3159 &::RenderBorder($::BorderSurface, $::App); 3160 &::RenderBorder($::BorderSurface, $::Background); 3161} 3162 3163sub SuperKill { 3164 my ($self, $guy) = @_; 3165 3166 my @gameObjects = @::GameObjects; 3167 my $sound = 0; 3168 foreach my $ball (@gameObjects) { 3169 next unless $ball->isa("Ball"); 3170 $ball->Pop($guy, 'superkill'); 3171 $sound = 1; 3172 } 3173 ::PlaySound('pop') if $sound; 3174} 3175 3176sub PopEveryBall { 3177 my $self = shift; 3178 my (@gameObjects, @guys); 3179 3180 @gameObjects = @::GameObjects; 3181 foreach (@gameObjects) { 3182 if ($_->isa('Ball')) { 3183 $_->Pop(undef, 'meltdown'); 3184 } elsif ('Guy' eq ref $_) { 3185 push @guys, $_; 3186 } 3187 } 3188 return @guys; 3189} 3190 3191sub DeathballMeltdown { 3192 my ($self) = @_; 3193 my ($i, $meltdown, $allKilled, @guys, @killedGuys, @deadGuys); 3194 3195 $self->{nocollision} = 1; 3196 $meltdown = new Meltdown; 3197 push @::GameObjects, $meltdown; 3198 3199 for ($i = 0; $i < 300; ++$i) { 3200 %::Events = (); 3201 &::HandleEvents(); 3202 return if $self->{abortgame}; 3203 my $advance = $self->CalculateAdvances(); 3204 while ($advance--) { 3205# TODO REINSTATE THIS IN 1.1!!! $self->PreAdvanceAction(); # Hook for something special 3206 $self->SUPER::AdvanceGameObjects(); 3207 $::GamePause = 0; 3208 if ($meltdown->{bounce} and not $allKilled) { 3209 $allKilled = 1; 3210 @guys = $self->PopEveryBall(); 3211 foreach (@guys) { 3212 $_->{killed} = 1; 3213 push @deadGuys, (new DeadGuy($_)); 3214 push @killedGuys, $_; 3215 } 3216 push @::GameObjects, (@deadGuys); 3217 } 3218 } 3219 $self->DrawGame(); 3220 } 3221 3222 foreach (@killedGuys) { 3223 $self->PlayerNextLife($_); 3224 } 3225 3226 $self->{nocollision} = 0; 3227} 3228 3229 3230########################################################################## 3231# GAME DRAWING 3232########################################################################## 3233 3234sub DrawScoreBoard { 3235 my ($self) = @_; 3236 my ($x, $y, $widthPerGuy); 3237 3238 $self->DrawLevelIndicator( 10, $self->{scoreBoardTop} ); 3239 for (my $i = 0; $i < $::NumGuys; ++$i) { 3240 $self->DrawScore( $::Players[$i], $::Players[$i]->{scoreX}, $::Players[$i]->{scoreY} ); 3241 } 3242} 3243 3244sub LayoutScoreBoard { 3245 my ($self) = @_; 3246 my ($i, $scoreBoardHeight, $scoreBoardTop, $rows, $rowHeight, $leftMargin, $guysPerRow, $widthPerGuy); 3247 3248 $scoreBoardTop = $::ScreenHeight + $::ScreenMargin * 2 + 5; 3249 $scoreBoardHeight = $::PhysicalScreenHeight - $scoreBoardTop; 3250 $rowHeight = 64; 3251 $leftMargin = 150; 3252 $rows = $::NumGuys > 4 ? 2 : 1; 3253 $rows = 1 if ($scoreBoardTop + $rows * $rowHeight > $::PhysicalScreenHeight); 3254 if ($scoreBoardTop + $rows * $rowHeight > $::PhysicalScreenHeight) { 3255 $rowHeight = 32; 3256 $scoreBoardTop = $::PhysicalScreenHeight - 32; 3257 } 3258 $guysPerRow = int ($::NumGuys / $rows + 0.5); 3259 $widthPerGuy = ($::PhysicalScreenWidth - $leftMargin) / $guysPerRow; 3260 for ($i = 0; $i < $::NumGuys; ++$i) { 3261 $::Players[$i]->{scoreX} = $leftMargin + ($i % $guysPerRow) * $widthPerGuy; 3262 $::Players[$i]->{scoreY} = $scoreBoardTop + int ($i / $guysPerRow) * $rowHeight; 3263 $::Players[$i]->{scoreRect} = 3264 new SDL::Rect(-x => $::Players[$i]->{scoreX}, -y => $::Players[$i]->{scoreY}, -width=> 130, -height=> $rowHeight); 3265 } 3266 $self->{scoreBoardTop} = $scoreBoardTop; 3267 $self->{scoreBoardHeight} = $scoreBoardHeight; 3268 $self->{rowHeight} = $rowHeight; 3269 3270} 3271 3272sub DrawLevelIndicator { 3273 my ($self, $x, $y) = @_; 3274 3275 $self->{levelIndicatorRect} = new SDL::Rect(-x => $x, -y => $y, -width => 100, -height => 32) unless $self->{levelIndicatorRect}; 3276 $::App->fill( $self->{levelIndicatorRect}, new SDL::Color() ); 3277 $::App->print( $x, $y + 3, 'Level ' . ($self->{level}+1) ); 3278} 3279 3280sub PrintNumber { 3281 my ($self, $player, $x, $y, $number) = @_; 3282 my ($numberText, $i, $srcrect, $dstrect); 3283 3284 $numberText = sprintf("%d", $number); 3285 $srcrect = new SDL::Rect(-width => 16, -height => 16, -y => 160); 3286 $dstrect = new SDL::Rect(-width => 16, -height => 16, -y => $y, -x => $x); 3287 for ($i = 0; $i < length($numberText); ++$i) { 3288 $srcrect->x(320 + (ord(substr($numberText, $i)) - ord('0')) * 16); 3289 $dstrect->x($x + $i * 16); 3290 $player->{guySurface}->blit( $srcrect, $::App, $dstrect ); 3291 } 3292} 3293 3294sub DrawScore { 3295 my ($self, $player, $x, $y, $livesY) = @_; 3296 my ($i, $srcrect, $dstrect); 3297 3298 $::App->fill($player->{scoreRect}, new SDL::Color()); 3299 $self->PrintNumber( $player, $x, $y, $player->{score}); 3300 3301 $livesY = $self->{rowHeight} > 32 ? $y + 24 : $y + 16; 3302 3303 $dstrect = new SDL::Rect(-width => 32, -height => 32, -x =>$x, -y => $livesY); 3304 if ($self->{rowHeight} <=32) { 3305 $srcrect = new SDL::Rect(-width => 16, -height => 16, -x =>320, -y => 176); 3306 } else { 3307 $srcrect = new SDL::Rect(-width => 32, -height => 32, -x =>320, -y => 128); 3308 } 3309 3310 if ($player->{lives} > 3) { 3311 $player->{guySurface}->blit( $srcrect, $::App, $dstrect ); 3312 $self->PrintNumber( $player, $x + $srcrect->width() + 8, $livesY + ($srcrect->height() - 16 ) / 2, $player->{lives} ); 3313 } else { 3314 foreach $i ( 0 .. $player->{lives}-1 ) { 3315 $dstrect->x( $x + $i * ($srcrect->width() + 4) ); 3316 $player->{guySurface}->blit( $srcrect, $::App, $dstrect ); 3317 } 3318 } 3319} 3320 3321sub PreAdvanceAction {} 3322 3323sub AdvanceGame { 3324 my $self = shift; 3325 3326 %::GameEvents = (); 3327 $self->PreAdvanceAction(); # Hook for something special 3328 3329 if ($self->{superKillCount} > 0) { 3330 if (--$self->{superKillDelay} <= 0) { 3331 --$self->{superKillCount}; 3332 $self->{superKillDelay} = 50; 3333 $self->SuperKill($self->{superKillGuy}); 3334 } 3335 $::GamePause = 0; 3336 } 3337 3338 $self->AdvanceGameObjects(); 3339 if ($::GameEvents{earthquake}) { 3340 &::PlaySound('quake'); 3341 foreach my $guy (@::GameObjects) { 3342 $guy->Earthquake($::GameEvents{earthquake}) if ref $guy eq 'Guy'; 3343 } 3344 } 3345 3346 if ($::GameEvents{'pop'}) { 3347 &::PlaySound('pop'); 3348 } 3349 3350 if ($::GameEvents{meltdown} and $::DifficultyLevel->{name} ne 'Miki') { 3351 $self->DeathballMeltdown(); 3352 } elsif ($::GameEvents{kill} ) { 3353 $self->PlayerDeathSequence(); 3354 return if $self->{playersalive} <= 0; 3355 $::GamePause = 200 if $::GamePause < 200; 3356 &GamePause::Show(); 3357 } elsif ($::GameEvents{magic}) { 3358 if ($::GamePause < 200) { 3359 $::GamePause = 200; &::PlaySound('pause'); 3360 &GamePause::Show(); 3361 } 3362 } elsif ($::GameEvents{superpause}) { 3363 if ($::GamePause < 800) { 3364 $::GamePause = 800; &::PlaySound('pause'); 3365 &GamePause::Show(); 3366 } 3367 } elsif ($::GameEvents{superkill}) { 3368 $self->{superKillCount} = 5; 3369 $self->{superKillDelay} = 0; 3370 $self->{superKillGuy} = $::GameEvents{superkillguy}; 3371 $self->{spawndelay} = 250; 3372 $self->{superballdelay} += 1000; # 10 second penalty 3373 my @gameObjects = @::GameObjects; 3374 foreach my $spawningBall (@gameObjects) { $spawningBall->Delete if $spawningBall->{spawning}; } 3375 } 3376} 3377 3378sub Run { 3379 my ($self) = shift; 3380 3381 $self->ResetGame(); 3382 &GameTimer::ResetTimer(); 3383 3384 $self->{superKillCount} = 0; 3385 $self->{superKillDelay} = 0; 3386 $self->{superKillGuy} = undef; 3387 3388 while (1) { 3389 3390 # Calculate advance (how many game updates to perform) 3391 my $advance = $self->CalculateAdvances(); 3392 3393 # Advance the game 3394 3395 %::Events = (); 3396 &::HandleEvents(); 3397 while ($advance--) { 3398 return if $self->{abortgame}; 3399 $self->AdvanceGame(); 3400 } 3401 if ($self->{playersalive} <= 0) { 3402 my $gameoverSurface = new SDL::Surface(-name => "$::DataDir/gameover.png"); 3403 my @gameObjects = @::GameObjects; 3404 foreach (@gameObjects) { $_->Delete() if ('DeadGuy' eq ref $_); } 3405 $self->DrawGame(); 3406 $gameoverSurface->blit(0, $::App, new SDL::Rect(-x => ($::PhysicalScreenWidth - $gameoverSurface->width) / 2, -y => $::PhysicalScreenHeight / 2 - 100)); 3407 $::App->sync(); 3408 $::App->delay(1000); 3409 for (my $i=0; $i < 20; ++$i) { 3410 $::App->delay(100); 3411 %::Events = (); 3412 &::HandleEvents(); 3413 last if $self->{abortgame}; 3414 last if %::Events; 3415 } 3416 last; 3417 } 3418 $self->DrawGame(); 3419 } 3420} 3421 3422 3423########################################################################## 3424package PanicGame; 3425########################################################################## 3426 3427@PanicGame::ISA = qw(PlayableGameBase); 3428 3429sub new { 3430 my ($class) = @_; 3431 my $self = new PlayableGameBase; 3432 %{$self} = (%{$self}, 3433 'spawndelay' => 0, 3434 'superballdelay' => 0, 3435 'leveladvance' => 0, 3436 'panicleveldesc' => undef, 3437 ); 3438 bless $self, $class; 3439} 3440 3441sub ResetGame { 3442 my $self = shift; 3443 3444 $self->SUPER::ResetGame(); 3445 $self->{spawndelay} = 0; 3446 $self->{superballdelay} = 2500 + $self->Rand(2500); # 25sec - 50sec 3447 $self->{superballdelay} *= $::DifficultyLevel->{superball}; 3448} 3449 3450sub SetGameSpeed { 3451 my ($self) = @_; 3452 3453 $::GameSpeed = $self->{leveldesc}->{gamespeed} * 0.8 * $::DifficultyLevel->{speed}; 3454} 3455 3456sub SetGameLevel { 3457 my ($self, $level) = @_; 3458 my ($levelIndex); 3459 3460 $levelIndex = ($level > $#::PanicLevels) ? $#::PanicLevels : $level; 3461 $self->{leveldesc} = $::PanicLevels[$levelIndex]; 3462 die unless $self->{leveldesc}; 3463 $self->{leveladvance} = 0; 3464 $self->SUPER::SetGameLevel($level); 3465} 3466 3467sub AdvanceGame { 3468 my ($self) = @_; 3469 3470 $self->SpawnBalls() if $::GamePause <= 0; 3471 $self->SUPER::AdvanceGame(); 3472} 3473 3474sub SpawnBalls { 3475 my $self = shift; 3476 my ($randmax, $rnd, $ballName, $balldesc, $deathBallCount, $earthquakeBallCount, $hasBonus); 3477 3478 --$self->{superballdelay}; 3479 if ($self->{superballdelay} <= 0) { 3480 push @::GameObjects, ( 3481 &Ball::Spawn($::BallDesc{sprintf('super%d', $self->Rand(2))}, -1, $self->Rand(40) < 20 ? 0 : 1) ); 3482 $self->{superballdelay} = (2500 + $self->Rand(2000)) * $::DifficultyLevel->{superball}; # 25sec - 45sec 3483 } 3484 3485 --$self->{spawndelay}; 3486 return if $self->{spawndelay} > 0; 3487 $deathBallCount = $earthquakeBallCount = -1; 3488 $randmax = 10000; 3489 while ($self->{spawndelay} <= 0) { 3490 if ($::DifficultyLevel->{name} eq 'Miki') { 3491 $balldesc = $::BallDesc{'death'}; 3492 last; 3493 } 3494 $rnd = int($self->Rand($randmax)); 3495 $randmax = 0; 3496 3497 # We try to find the balldesc that falls at $rnd 3498 my $ballRoulette = $self->{leveldesc}->{balls}; 3499 for (my $i = 0; $i < scalar @{$ballRoulette}; $i+=2) { 3500 my $rouletteWeight = $ballRoulette->[$i+1]; 3501 $randmax += $rouletteWeight; 3502 $rnd -= $rouletteWeight; 3503 if ($rnd < 0) { 3504 $ballName = $ballRoulette->[$i]; 3505 last; 3506 } 3507 } 3508 next unless ($ballName); # $rnd too large.. We'll have a better $randmax this time! 3509 3510 ($balldesc) = $::BallDesc{$ballName}; 3511 if ($balldesc->{class} eq 'DeathBall') { 3512 next unless $::DeathBallsEnabled; 3513 $deathBallCount = &DeathBall::CountDeathBalls() if $deathBallCount < 0; # Lazy counting 3514 next if $deathBallCount >= 2; 3515 } 3516 if ($balldesc->{class} eq 'EarthquakeBall') { 3517 next unless $::EarthquakeBallsEnabled; 3518 $earthquakeBallCount = &::EarthquakeBall::CountEarthquakeBalls if $earthquakeBallCount < 0; 3519 next if $earthquakeBallCount >= 1; 3520 } 3521 if ($balldesc->{class} eq 'WaterBall') { 3522 next unless $::WaterBallsEnabled; 3523 } 3524 if ($balldesc->{class} eq 'SeekerBall') { 3525 next unless $::SeekerBallsEnabled; 3526 } 3527 last if $balldesc; 3528 } 3529 3530 $hasBonus = 1 if ($balldesc->{width} >= 32) and ($self->Rand(1) < $::DifficultyLevel->{bonusprobability}); 3531 3532 push @::GameObjects, ( &Ball::Spawn($balldesc, -1, $self->Rand(40) < 20 ? 0 : 1, $hasBonus) ); 3533 $self->{spawndelay} = $self->{leveldesc}->{spawndelay} * $balldesc->{spawndelay} * 50; 3534 $self->{spawndelay} /= ($::NumGuys + 1) / 2; 3535 $self->{spawndelay} *= $::DifficultyLevel->{spawnmultiplier}; 3536} 3537 3538sub OnBallPopped { 3539 my $self = shift; 3540 3541 ++$self->{leveladvance}; 3542 if ($self->{leveladvance} >= 18) { 3543 ::PlaySound('level'); 3544 $self->SetGameLevel($self->{level}+1); 3545 } 3546} 3547 3548sub DrawLevelIndicator { 3549 my ($self, $x, $y) = @_; 3550 3551 $self->{levelIndicatorRect} = new SDL::Rect(-x => $x, -y => $y, -width => 140, -height => $self->{scoreBoardHeight}) unless $self->{levelIndicatorRect}; 3552 $::App->fill( $self->{levelIndicatorRect}, new SDL::Color() ); 3553 $::LevelIndicatorSurface2->blit( 0, $::App, new SDL::Rect(-x => $x, -y => $y)); 3554 $::LevelIndicatorSurface->blit( new SDL::Rect(-width => 130 * $self->{leveladvance} / 17, -height => 30), $::App, new SDL::Rect(-x => $x, -y => $y)); 3555 $::App->print( $x + 25, $y + 3, 'Level ' . ($self->{level}+1) ); 3556 $::App->print( $x, $y + 40, sprintf('spd: %d/%d', $::GameSpeed * 100, $self->{leveldesc}->{spawndelay}) ) if $self->{scoreBoardHeight} >= 64; 3557} 3558 3559 3560########################################################################## 3561package ChallengeGame; 3562########################################################################## 3563 3564@ChallengeGame::ISA = qw(PlayableGameBase); 3565 3566sub new { 3567 my ($class) = @_; 3568 my $self = new PlayableGameBase; 3569 %{$self} = (%{$self}, 3570 'challenge' => undef, 3571 ); 3572 bless $self, $class; 3573} 3574 3575sub CreateLevelNumberSurface { 3576 my ($level) = @_; 3577 my ($surface, $w); 3578 3579 $::GlossyFont->use(); 3580 $w = &::TextWidth("Level $level"); 3581 $surface = new SDL::Surface( -name =>'', 3582 -flags=>::SDL_SWSURFACE(), -width => $w+6, -height => 48, -depth => 32); 3583 $surface->print( 3, 3, "Level $level" ); 3584 $::ScoreFont->use(); 3585 return $surface; 3586} 3587 3588sub SetGameLevel { 3589 my ($self, $level) = @_; 3590 3591 &SlowEffect::RemoveSlowEffects(); 3592 $self->SUPER::SetGameLevel($level); 3593 $level = $#::ChallengeLevels if $level > $#::ChallengeLevels; 3594 $self->{challenge} = $::ChallengeLevels[$level]; 3595 $self->SpawnChallenge(); 3596 3597 my ($levelObject, $surface); 3598 $levelObject = new GameObject; 3599 $surface = &CreateLevelNumberSurface($level + 1); 3600 $levelObject->{surface} = $surface; 3601 $levelObject->{w} = $surface->width(); 3602 $levelObject->{h} = $surface->height(); 3603 $levelObject->{x} = ($::ScreenWidth - $levelObject->{w}) / 2; 3604 $levelObject->{y} = ($::ScreenHeight - $levelObject->{h}) / 2; 3605 $levelObject->{draw} = sub { my $self = shift; $self->{surface}->blit( 0, $::App, $self->{rect} ); }; 3606 $levelObject->{advance} = sub { my $self = shift; $self->Delete() if ++$self->{time} > 200; }; 3607 push @::GameObjects, $levelObject; 3608} 3609 3610sub AdvanceGameObjects { 3611 my ($self) = @_; 3612 3613 if ($self->{nextlevel}) { 3614 ::PlaySound('level'); 3615 $self->SetGameLevel($self->{level} + 1); 3616 delete $self->{nextlevel}; 3617 } 3618 if ($self->{playerspawned}) { 3619 $self->SpawnChallenge(); 3620 $self->{playerspawned} = 0; 3621 } 3622 $self->SUPER::AdvanceGameObjects(); 3623} 3624 3625sub SpawnChallenge { 3626 my $self = shift; 3627 my ($challenge, @guys, $balldesc, $ball, $hasBonus, %balls, $numBalls, $ballsSpawned, @ballKeys, $x); 3628 3629 @guys = $self->PopEveryBall(); 3630 foreach (@guys) { 3631 $_->{bonusDelay} = 1; 3632 $_->{invincible} = 1; 3633 } 3634 $::GamePause = 0; 3635 delete $::GameEvents{magic}; 3636 $challenge = $self->{challenge}; 3637 die unless $challenge; 3638 3639 while ($challenge =~ /(\w+)/g) { 3640 $balldesc = $::BallDesc{$1}; 3641 warn "Unknown ball in challenge: $1" unless $balldesc; 3642 $balls{$1}++; 3643 $numBalls++; 3644 } 3645 $ballsSpawned = 0; 3646 while ($ballsSpawned < $numBalls) { 3647 foreach (keys %balls) { 3648 next unless $balls{$_}; 3649 --$balls{$_}; 3650 $balldesc = $::BallDesc{$_}; 3651 $x = $::ScreenWidth * ($ballsSpawned * 2 + 1) / ($numBalls * 2) - $balldesc->{width} / 2; 3652 $x = $::ScreenWidth - $balldesc->{width} if $x > $::ScreenWidth - $balldesc->{width}; 3653 $hasBonus = (($balldesc->{width} >= 32) and ($self->Rand(1) < $::DifficultyLevel->{bonusprobability})); 3654 $ball = &Ball::Spawn($balldesc, $x, ($ballsSpawned % 2) ? 0 : 1, $hasBonus); 3655 if ($ball->{w} <= 32) { 3656 $ball->{ismagic} = $ball->{hasmagic} = 0; 3657 } 3658 push @::GameObjects, ($ball) ; 3659 ++$ballsSpawned; 3660 } 3661 } 3662} 3663 3664sub OnBallPopped { 3665 my $self = shift; 3666 my ($i); 3667 3668 for ($i = $#::GameObjects; $i >= 0; --$i) { 3669 if ($::GameObjects[$i]->isa('Ball')) { 3670 return; 3671 } 3672 } 3673 $self->{nextlevel} = 1; 3674} 3675 3676 3677########################################################################## 3678package TutorialGame; 3679########################################################################## 3680 3681@TutorialGame::ISA = qw(ChallengeGame); 3682 3683sub SetChallenge { 3684 my ($self, $challenge) = @_; 3685 3686 $self->{challenge} = $challenge; 3687} 3688 3689sub SetGameLevel { 3690 my ($self, $level) = @_; 3691 3692 $self->PlayableGameBase::SetGameLevel($level); 3693 $self->SpawnChallenge(); 3694} 3695 3696sub AdvanceGameObjects { 3697 my ($self) = @_; 3698 3699 if ($self->{nextlevel}) { 3700 $self->{countDown} = 200; 3701 delete $self->{nextlevel}; 3702 } 3703 if ($self->{playerspawned}) { 3704 $self->SpawnChallenge(); 3705 $self->{playerspawned} = 0; 3706 } 3707 if ($self->{countDown}) { 3708 if (--$self->{countDown} < 1) { 3709 $self->{abortgame} = 1; 3710 } 3711 } 3712 $self->SUPER::AdvanceGameObjects(); 3713} 3714 3715 3716########################################################################## 3717package RecordGame; 3718########################################################################## 3719 3720@RecordGame::ISA = qw(PanicGame); 3721 3722sub Rand { 3723 my $self = shift; 3724 my $result = int(rand($_[0]) * 100) / 100; 3725 push @{$self->{rand}}, ($result); 3726 return $result; 3727} 3728 3729sub Rewind { 3730 my $self = shift; 3731 my ($recordEnd, $playback); 3732 3733 $recordEnd = length($self->{record}) - $::NumGuys * 1000; 3734 return if $recordEnd <= 0; 3735 $self->{record} = substr($self->{record}, 0, $recordEnd); 3736 $::Game = $playback = new DemoPlaybackGame($::NumGuys, $::DifficultyLevel, $self->{record}, $self->{rand}, {}); 3737 $playback->{skip} = 1; 3738 $::Background->blit(0, $::App, 0); 3739 $playback->Run(); 3740 3741 $playback->RestoreGameSettings(); 3742 %{$self} = %{$playback}; 3743 $::Game = $self; 3744 $self->{abortgame} = 0; 3745 print "Splicing {rand}: original length is ", scalar(@{$self->{rand}}), "; playback randpointer is $playback->{randpointer}.\n"; 3746 splice @{$self->{rand}}, $playback->{randpointer}; 3747 $::Background->blit(0, $::App, 0); 3748 $self->DrawGame(); 3749 %::Events = %::Keys = (); 3750 while( not %::Events ) { &::HandleEvents(); $::App->delay(100); } 3751 &GameTimer::ResetTimer(); 3752} 3753 3754sub PreAdvanceAction { 3755 my $self = shift; 3756 my ($record); 3757 3758 $self->Rewind() if $::Events{::SDLK_F3()}; 3759 3760 for (my $i=0; $i < $::NumGuys; ++$i) { 3761 my $keys = $::Players[$i]->{keys}; 3762 $record = 0; 3763 $record += 1 if $::Keys{$keys->[0]}; 3764 $record += 2 if $::Keys{$keys->[1]}; 3765 $record += 4 if $::Events{$keys->[2]}; 3766 if ($::Events{::SDLK_F2()} and $::NumGuys == 1) { 3767 $record += 8; 3768 $::GameEvents{superkill} = 1; 3769 } 3770 $self->{record} .= $record; 3771 } 3772} 3773 3774 3775########################################################################## 3776package PlaybackGame; 3777########################################################################## 3778 3779@PlaybackGame::ISA = qw(PanicGame); 3780 3781sub new { 3782 my ($class, $numGuys, $difficultyLevel, $record, $rand, $messages) = @_; 3783 my $self; 3784 3785 $self = new PanicGame; 3786 %{$self} = (%{$self}, 3787 'record' => $record, 3788 'rand' => $rand, 3789 'messages' => $messages, 3790 ); 3791 bless $self, $class; 3792 $self->InitPlayback($numGuys); 3793 &::SetDifficultyLevel($difficultyLevel); 3794 return $self; 3795} 3796 3797sub InitPlayback { 3798 my ($self, $numGuys) = @_; 3799 3800 $self->{recordpointer} = 0; 3801 $self->{randpointer} = 0; 3802 $self->{oldnumguys} = $::NumGuys; 3803 $self->{olddifficultylevel} = $::DifficultyLevelIndex; 3804 3805 $::NumGuys = $numGuys; 3806 for (my $i=0; $i < $numGuys; ++$i) { 3807 $::Players[$i]->{oldkeys} = $::Players[$i]->{keys}; 3808 $::Players[$i]->{keys} = [ "DLEFT$i", "DRIGHT$i", "DFIRE$i" ]; 3809 } 3810} 3811 3812sub RestoreGameSettings { 3813 my $self = shift; 3814 3815 for (my $i=0; $i < $::NumGuys; ++$i) { 3816 $::Players[$i]->{keys} = $::Players[$i]->{oldkeys}; 3817 delete $::Players[$i]->{oldkeys}; 3818 } 3819 $::NumGuys = $self->{oldnumguys}; 3820 &::SetDifficultyLevel($self->{olddifficultylevel}); 3821} 3822 3823sub CalculateAdvances { 3824 my $self = shift; 3825 3826 return length($self->{record}) if $self->{skip}; 3827 return $self->SUPER::CalculateAdvances() * ($::Keys{::SDLK_f()} ? 15 : 1); 3828} 3829 3830sub Rand { 3831 my $self = shift; 3832 3833 my $result = $self->{rand}->[$self->{randpointer}]; 3834 ++$self->{randpointer}; 3835 return $result; 3836} 3837 3838sub PreAdvanceAction { 3839 my $self = shift; 3840 my ($record, $keys); 3841 3842 for (my $i=0; $i < $::NumGuys; ++$i) { 3843 3844 $record = substr($self->{record}, $self->{recordpointer}++, 1); 3845 $keys = $::Players[$i]->{keys}; 3846 $::Keys{$keys->[0]} = $record & 1; 3847 $::Keys{$keys->[1]} = $record & 2; 3848 $::Events{$keys->[2]} = $record & 4; 3849 $::GameEvents{superkill} = 1 if $::NumGuys == 1 and $record & 8; 3850 } 3851 3852 $self->{abortgame} = 1 if $self->{recordpointer} >= length $self->{record}; 3853 3854 if ($self->{messages}) { 3855 my $message = $self->{messages}->{$self->{recordpointer}}; 3856 $self->DisplayMessage($message) if $message; 3857 } 3858} 3859 3860sub DisplayMessage { 3861 my ($self, $message) = @_; 3862 3863 my ($len, $adv) = (0, 0); 3864 my $x = ( $::PhysicalScreenWidth - &::TextWidth($message) ) / 2; 3865 my $y = $::PhysicalScreenHeight / 2; 3866 $self->DrawGame(); 3867 3868 while (1) { 3869 &::HandleEvents(); 3870 return if $self->{abortgame}; 3871 my $advance = $self->CalculateAdvances(); 3872 $adv += $advance; 3873 $len = int($adv / 5); 3874 3875 $::App->print($x, $y, substr($message, 0, $len) ); 3876 $::App->sync(); 3877 last if $len > length($message) + 15; 3878 } 3879 $::Background->blit(new SDL::Rect(-width=>$::PhysicalScreenWidth, -y=>$y, -height=>40), $::App, new SDL::Rect(-y => $y)); 3880} 3881 3882 3883########################################################################## 3884package DemoGame; 3885########################################################################## 3886 3887sub ResetGame { 3888 my $self = shift; 3889 &::SetDifficultyLevel(1); 3890 &::SetWeaponDuration(0); 3891 $::Slippery = 0; 3892 $self->PanicGame::ResetGame(); 3893 3894 my $ball = &Ball::Create($::BallDesc[4], 400, 0, -10, 0); 3895 $ball->GiveMagic(); 3896 3897 push @::GameObjects, ( 3898 &Ball::Create($::BallDesc[0], 100, 0, 1), 3899 &Ball::Create($::BallDesc{super0}, 300, 0, 0), 3900 &Ball::Create($::BallDesc{super1}, 500, 0, 1), 3901 $ball, 3902 ); 3903 $::GamePause = 0; 3904 $::GameSpeed = 0.8; 3905 $self->{spawndelay} = $self->{superballdelay} = 1000000; 3906 $self->{ballcounter} = 0; 3907 $self->{balls} = [ qw(b0 h0 w1 quake death seeker) ]; 3908} 3909 3910sub SetGameSpeed { 3911 $::GameSpeed = 0.8; 3912} 3913 3914sub SpawnBalls { 3915 my $self = shift; 3916 3917 return if (--$self->{spawndelay} > 0); 3918 my $ballName = $self->{balls}->[$self->{ballcounter}]; 3919 return unless $ballName; 3920 push @::GameObjects, ( &Ball::Spawn($::BallDesc{$ballName}, 100, 1, 0) ); 3921 $self->{spawndelay} = 1000000; 3922 ++$self->{ballcounter}; 3923} 3924 3925sub RespawnPlayers {} 3926sub OnBallPopped {} 3927 3928 3929########################################################################## 3930package DemoRecordGame; 3931########################################################################## 3932 3933@DemoRecordGame::ISA = qw(DemoGame RecordGame); 3934 3935sub new { 3936 my $class = shift; 3937 my $self = new RecordGame(@_); 3938 bless $self, $class; 3939} 3940 3941 3942########################################################################## 3943package DemoPlaybackGame; 3944########################################################################## 3945 3946@DemoPlaybackGame::ISA = qw(DemoGame PlaybackGame); 3947 3948sub new { 3949 my $class = shift; 3950 my $self = new PlaybackGame(@_); 3951 bless $self, $class; 3952} 3953 3954sub DrawScoreBoard { 3955 my $self = shift; 3956 my ($x, $y); 3957 3958 $x = 10; 3959 $y = $::ScreenHeight + 2 * $::ScreenMargin + 5; 3960 if ($self->{anim} < 1) { 3961 $::Background->print( $x, $y, "Press F to fast forward" ); 3962 $::App->print( $x, $y, "Press F to fast forward" ); 3963 } return; 3964 $::App->fill( new SDL::Rect(-x=>0, -y=>$y, -width=>$::PhysicalScreenWidth, -height=>$::PhysicalScreenHeight - $y), new SDL::Color() ); 3965 $::App->print( $x, $y, $self->{recordpointer} ); 3966} 3967 3968 3969########################################################################## 3970package Menu; 3971########################################################################## 3972 3973@Menu::ISA = qw(GameBase); 3974use vars qw(@syms); 3975@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 ); 3976 3977sub Exit { 3978 my $self = shift; 3979 3980 &::SaveConfig(); 3981 $self->SUPER::Exit(); 3982} 3983 3984sub SetGameSpeed { 3985 $::GameSpeed = 1.0; 3986} 3987 3988sub ShowTooltip { 3989 my $self = shift; 3990 my (@lines, $y, $yinc, $rect); 3991 3992 @lines = @_; 3993 @lines = ("Pang Zero $::Version (C) 2006 by UPi (upi\@sourceforge.net)", 3994 "Use cursor keys to navigate menu, Enter to select", 3995 "P pauses the game, Esc quits") unless scalar @lines; 3996 3997 $::ScoreFont->use(); 3998 ($y, $yinc) = ($::ScreenHeight + 35, 20); 3999 $rect = new SDL::Rect( -x => 0, -y => $y, 4000 -w => $::PhysicalScreenWidth, -h => $::PhysicalScreenWidth - $y ); 4001 $::Background->fill($rect, new SDL::Color); 4002 foreach (@lines) { 4003 $::Background->print( 10, $y, $_ ) if $y + $yinc < $::PhysicalScreenHeight; 4004 $y += $yinc; 4005 } 4006 $rect = new SDL::Rect( -x => 0, -y => $::ScreenHeight + 35, 4007 -w => $::PhysicalScreenWidth, -h => $::PhysicalScreenWidth - $y ); 4008 $::Background->blit($rect, $::App, $rect); 4009 $::MenuFont->use(); 4010} 4011 4012sub MenuAdvance { 4013 my $self = shift; 4014 4015 my $advance = $self->CalculateAdvances(); 4016 %::Events = %::MenuEvents = (); 4017 %::GameEvents = (); 4018 &::HandleEvents('readbothaxes'); 4019 while ($advance--) { 4020 $self->AdvanceGameObjects(); 4021 } 4022 while (ref($::GameObjects[$#::GameObjects]) ne 'MenuItem') { unshift @::GameObjects, (pop @::GameObjects); } 4023 $self->DrawGame(); 4024} 4025 4026sub SetCurrentItemIndex { 4027 my ($self, $index) = @_; 4028 4029 return if ($index < 0 or $index >= scalar @{$self->{menuItems}} or not $self->{menuItems}->[$index]->CanSelect()); 4030 $self->{currentItemIndex} = $index; 4031 $self->{currentItem} = $self->{menuItems}->[$index]; 4032 $self->{currentItem}->Select(); 4033} 4034 4035sub EnterSubMenu { 4036 my $self = shift; 4037 my ($recall, $menuItem); 4038 4039 $recall->{oldItems} = $self->{menuItems}; 4040 $recall->{oldCurrentItemIndex} = $self->{currentItemIndex}; 4041 foreach $menuItem (@{$self->{menuItems}}) { $menuItem->Hide(); } 4042 $self->{menuItems} = []; 4043 4044 return $recall; 4045} 4046 4047sub LeaveSubMenu { 4048 my ($self, $recall) = @_; 4049 my ($menuItem); 4050 4051 foreach $menuItem (@{$self->{menuItems}}) { $menuItem->HideAndDelete(); } 4052 $self->{menuItems} = $recall->{oldItems}; 4053 foreach $menuItem (@{$self->{menuItems}}) { $menuItem->Show(); } 4054 $self->SetCurrentItemIndex($recall->{oldCurrentItemIndex}); 4055 $self->{abortgame} = 0; 4056} 4057 4058sub HandleUpDownKeys { 4059 my $self = shift; 4060 4061 if ($::MenuEvents{DOWN}) { 4062 $self->SetCurrentItemIndex( $self->{currentItemIndex} + 1 ); 4063 } 4064 if ($::MenuEvents{UP}) { 4065 $self->SetCurrentItemIndex( $self->{currentItemIndex} - 1 ); 4066 } 4067} 4068 4069sub KeyToText { 4070 my ($key) = @_; 4071 eval("::SDLK_$_() eq $key") and return ucfirst(lc($_)) foreach @syms; 4072 print "No match for $key\n"; 4073 return "???"; 4074} 4075 4076sub KeysToText { 4077 my $keys = shift; 4078 my ($retval); 4079 if ( $keys->[0] =~ /^[LRB](\d)+$/ ) { 4080 return "Joystick $1"; 4081 } 4082 return join(' / ', &KeyToText($keys->[0]), &KeyToText($keys->[1]), &KeyToText($keys->[2]) ); 4083} 4084 4085sub RunTutorial { 4086 my ($self, $ball) = @_; 4087 my ($recall, @oldGameObjects, %oldGuys, %oldHarpoons, $oldGame); 4088 4089 $recall = $self->EnterSubMenu(); 4090 @oldGameObjects = @::GameObjects; 4091 %oldGuys = %Guy::Guys; 4092 %oldHarpoons = %Harpoon::Harpoons; 4093 $oldGame = $::Game; 4094 4095 $::ScoreFont->use(); 4096 $::Game = new TutorialGame; 4097 $::Game->SetChallenge($ball); 4098 $::Game->Run(); 4099 $::MenuFont->use(); 4100 $self->SetGameSpeed(); 4101 4102 foreach (@::GameObjects) { $_->Clear(); } 4103 @::GameObjects = @oldGameObjects; 4104 %Guy::Guys = %oldGuys; 4105 %Harpoon::Harpoons = %oldHarpoons; 4106 $::Game = $oldGame; 4107 $self->LeaveSubMenu($recall); 4108} 4109 4110sub RunTutorialMenu { 4111 my $self = shift; 4112 my ($baseY, $baseX, $menuItem, $recall, @tutorials); 4113 4114 $recall = $self->EnterSubMenu(); 4115 $self->{title}->Hide(); 4116 $baseY = 50; 4117 4118 @tutorials = ( 4119 ['n2', 'Normal Ball', 'There is nothing special about this ball. Just keep shooting it.'], 4120 ['b0', 'Bouncy Ball', 'This ball bounces higher than the normal ball.', 'Otherwise it behaves the same.'], 4121 ['h0', 'Hexa', 'The Hexa is weightless and travels in a straight line.', 'With practice you can shoot it just as easily as the normal ball.'], 4122 ['w1', 'Water Ball', 'The water ball pops each time it bounces.', 'This can create a tide of small balls fast.', 'Mop it up quickly.'], 4123 ['f1', 'Fragile Ball', 'The fragile ball shatters into little bits the moment it is hit.', 'Prepare for a shower of small balls.'], 4124 ['death', 'Death Ball', 'This ball cannot be killed with your harpoon.', 'Shooting will make it multiply. Too many death balls cause meltdown.', 'Evade it for 20 seconds to get rid of it.'], 4125 ['seeker', 'Seeker Ball', 'The seeker ball will chase you forever.', 'You have to keep moving and shooting to evade it.'], 4126 ['quake', 'Earthquake Ball', 'This ball is super heavy.', 'In fact the earth will quake each time it bounces.', 'Shoot it quickly, or it will send you flying.'], 4127 ['u0', 'Upside Down Ball', 'This crazy ball bounces on the top of the screen.', 'Maybe it came from an alternate universe,', 'where gravity is negative?'], 4128 ['super0, n1', 'Super Ball', 'The Super Ball is your friend. It will still kill you on touch.', 'The green super ball will pause the game for 8 seconds.', 'The gold super ball will kill every ball.'], 4129 ); 4130 4131 push @{$self->{menuItems}}, 4132 new MenuItem( 50, $baseY, "Back to main menu"), 4133 new MenuItem( 50, $baseY += 40, "Run Demo" ); 4134 4135 $baseY = 110; 4136 $baseX = 50; 4137 foreach (@tutorials) { 4138 my @tutItem = @{$_}; 4139 my $challenge = shift @tutItem; 4140 my $menuItem = new MenuItem( $baseX, $baseY += 40, @tutItem ); 4141 $menuItem->{challenge} = $challenge; 4142 push @{$self->{menuItems}}, $menuItem; 4143 if ($baseY + 140 >= $::ScreenHeight) { 4144 $baseY = 110; 4145 $baseX = 450; 4146 } 4147 } 4148 push @::GameObjects, (@{$self->{menuItems}}); 4149 $self->SetCurrentItemIndex(1); 4150 4151 while (1) { 4152 $self->MenuAdvance(); 4153 last if $self->{abortgame}; 4154 $self->HandleUpDownKeys(); 4155 4156 if ($::MenuEvents{LEFT} and $self->{currentItemIndex} > 1) { 4157 $self->SetCurrentItemIndex($self->{currentItemIndex} - 5); 4158 } 4159 if ($::MenuEvents{RIGHT} and $self->{currentItemIndex} > 1) { 4160 $self->SetCurrentItemIndex($self->{currentItemIndex} + 5); 4161 } 4162 if ($::MenuEvents{BUTTON}) { 4163 if (0 == $self->{currentItemIndex}) { 4164 last; 4165 } elsif (1 == $self->{currentItemIndex}) { 4166 $self->{result} = 'demo'; 4167 last; 4168 } else { 4169 $self->RunTutorial($self->{currentItem}->{challenge}); 4170 } 4171 } 4172 } 4173 4174 $self->LeaveSubMenu($recall); 4175 $self->{title}->Show(); 4176} 4177 4178sub RunCredits { 4179 my ($self, $demo) = @_; 4180 my ($recall, $i, $ball, @balls, @oldGameObjects, $time); 4181 4182 $time = $self->{anim}; 4183 $recall = $self->EnterSubMenu(); 4184 @oldGameObjects = @::GameObjects; 4185 foreach my $gameObject (@::GameObjects) { 4186 $gameObject->Clear(); 4187 } 4188 @::GameObjects = ($self->{title}); 4189 push @::GameObjects, (new FpsIndicator); 4190 my ($y, $yinc) = (110, 36); 4191 push @{$self->{menuItems}}, ( 4192 new MenuItem( 100, $y += $yinc, "Written by: UPi <upi\@sourceforge.net>"), 4193 new MenuItem( 100, $y += $yinc, "Music by: SAdam" ), 4194 new MenuItem( 100, $y += $yinc, "Graphics by: UPi, DaniGM, EBlanca" ), 4195 new MenuItem( 100, $y += $yinc * 1.5, "TESTERS" ), 4196 new MenuItem( 100, $y += $yinc, "Ulmar, Surba, Miki, Aisha, Descant" ), 4197 new MenuItem( 100, $y += $yinc * 1.5, "http://apocalypse.rulez.org/pangzero" ), 4198 ); 4199 foreach $i (@{$self->{menuItems}}) { $i->Center(); } 4200 4201 for ($i = 0; $i < 20; ++$i) { 4202 $ball = &Ball::Spawn( $::BallDesc{'credits1'}, 100, 1, 0 ); 4203 $ball->{y} = $i * -5; 4204 push @balls, ($ball); 4205 $ball = &Ball::Spawn( $::BallDesc{'credits2'}, $::ScreenWidth - 132, -1, 0 ); 4206 $ball->{y} = $i * -5; 4207 push @balls, ($ball); 4208 } 4209 push @::GameObjects, @balls; 4210 push @::GameObjects, (@{$self->{menuItems}}); 4211 4212 while (1) { 4213 $self->MenuAdvance(); 4214 last if $self->{abortgame}; 4215 if ($demo) { 4216 last if %::Events; 4217 last if $self->{anim} - $time > 20 * 100; # 30s 4218 } 4219 } 4220 4221 @::GameObjects = @oldGameObjects; 4222 foreach (@balls) { $_->Delete(); } 4223 $self->LeaveSubMenu($recall); 4224} 4225 4226sub RunHighScore { 4227 my ($self, $difficultyLevel, $table, $auto) = @_; 4228 my ($time, $recall, $y, $yinc, $retval); 4229 4230 die unless $table =~/^(Cha|Pan)$/; 4231 $time = 0; 4232 $recall = $self->EnterSubMenu(); 4233 ($y, $yinc) = (110, 40); 4234 $difficultyLevel = $::DifficultyLevels[$difficultyLevel]; 4235 push @{$self->{menuItems}}, ( 4236 new MenuItem( 320, 50, ($table eq 'Cha' ? 'Challenge Game - ' : 'Panic Game - ') . $difficultyLevel->{name} ), #. " difficulty" ), 4237 new MenuItem( 50, $y, "Highest Score" ), 4238 new MenuItem( 480, $y, "Highest Level" ), 4239 ); 4240 $self->{menuItems}->[0]->Center(); 4241 $y += $yinc; 4242 foreach (@{$difficultyLevel->{"highScoreTable$table"}}) { 4243 push @{$self->{menuItems}}, ( new MenuItem( 10, $y += $yinc, $_->[0] ) ); 4244 push @{$self->{menuItems}}, ( new MenuItem( 250, $y, $_->[1] ) ); 4245 } 4246 $y = 110 + $yinc; 4247 foreach (@{$difficultyLevel->{"highLevelTable$table"}}) { 4248 push @{$self->{menuItems}}, ( new MenuItem( 460, $y += $yinc, $_->[0] ) ); 4249 push @{$self->{menuItems}}, ( new MenuItem( 700, $y, $_->[1] ) ); 4250 } 4251 push @::GameObjects, (@{$self->{menuItems}}); 4252 4253 while (not $retval) { 4254 $self->MenuAdvance(); 4255 if ($self->{abortgame}) { 4256 $retval = 'abortgame'; last; 4257 } 4258 if ($auto) { 4259 $retval = 'next' if ++$time > 100 * 6; 4260 $retval = 'abortgame' if %::Events; 4261 } else { 4262 if ($::MenuEvents{LEFT} or $::MenuEvents{UP}) { 4263 $retval = 'prev'; last; 4264 } elsif ($::MenuEvents{RIGHT} or $::MenuEvents{DOWN}) { 4265 $retval = 'next'; last; 4266 } elsif ($::MenuEvents{BUTTON}) { 4267 $retval = 'abortgame'; 4268 } 4269 } 4270 } 4271 $self->LeaveSubMenu($recall); 4272 return $retval; 4273} 4274 4275sub RunHighScores { 4276 my ($self, $auto) = @_; 4277 my ($recall, $retval, $i, $table, @tables); 4278 4279 if ($auto) { 4280 $self->ShowTooltip(); 4281 } else { 4282 $self->ShowTooltip("Use arrow keys to navigate, Esc to go back"); 4283 } 4284 $recall = $self->EnterSubMenu(); 4285 $self->{title}->Hide(); 4286 $table = 0; 4287 @tables = ( [0, 'Pan'], [0, 'Cha'], [1, 'Pan'], [1, 'Cha'], [2, 'Pan'], [2, 'Cha'], [3, 'Pan'], [3, 'Cha'], [4, 'Pan'] ); 4288 4289 while (1) { 4290 $retval = $self->RunHighScore( @{$tables[$table]}, $auto ); 4291 if ($retval eq 'next') { 4292 ++$table; 4293 $table = 0 if $table == scalar @tables; 4294 last if $table == 0 and $auto; 4295 } elsif ($retval eq 'prev') { 4296 --$table; 4297 $table = $#tables if $table < 0; 4298 } else { 4299 last; 4300 } 4301 } 4302 4303 $self->ShowTooltip(); 4304 $self->{title}->Show(); 4305 $self->LeaveSubMenu($recall); 4306} 4307 4308sub UpdateBallMixerMenu { 4309 my $self = shift; 4310 4311 $self->{menuItems}->[1]->SetParameter( $::DeathBallsEnabled ? 'on' : 'off' ); 4312 $self->{menuItems}->[2]->SetParameter( $::EarthquakeBallsEnabled ? 'on' : 'off' ); 4313 $self->{menuItems}->[3]->SetParameter( $::WaterBallsEnabled ? 'on' : 'off' ); 4314 $self->{menuItems}->[4]->SetParameter( $::SeekerBallsEnabled ? 'on' : 'off' ); 4315} 4316 4317sub RunBallMixerMenu { 4318 my $self = shift; 4319 my ($recall); 4320 4321 $recall = $self->EnterSubMenu(); 4322 my ($y, $yinc) = (110, 40); 4323 push @{$self->{menuItems}}, ( 4324 new MenuItem( 100, $y += $yinc, "Back to options menu"), 4325 new MenuItem( 100, $y += $yinc + 20, "Death Balls: ", "Death balls multiply every time you shoot them.", "You can get rid of them by NOT shooting them for 20 seconds." ), 4326 new MenuItem( 100, $y += $yinc, "Earthquake Balls: ", "Earthquake balls shake the ground when they bounce.", "This sends you flying. Very dangerous." ), 4327 new MenuItem( 100, $y += $yinc, "Water Balls: ", "Water balls quickly dissolve, creating a flood of small balls." ), 4328 new MenuItem( 100, $y += $yinc, "Seeker Balls: ", "This ball picks a target, and chases him." ), 4329 ); 4330 $self->UpdateBallMixerMenu(); 4331 push @::GameObjects, (@{$self->{menuItems}}); 4332 $self->SetCurrentItemIndex(0); 4333 4334 while (1) { 4335 $self->MenuAdvance(); 4336 last if $self->{abortgame}; 4337 $self->HandleUpDownKeys(); 4338 4339 if ($::MenuEvents{BUTTON}) { 4340 last if $self->{currentItemIndex} == 0; # Back to main 4341 if ($self->{currentItemIndex} == 1) { 4342 $::DeathBallsEnabled = 1 - $::DeathBallsEnabled; $self->UpdateBallMixerMenu(); 4343 } elsif ($self->{currentItemIndex} == 2) { 4344 $::EarthquakeBallsEnabled = 1 - $::EarthquakeBallsEnabled; $self->UpdateBallMixerMenu(); 4345 } elsif ($self->{currentItemIndex} == 3) { 4346 $::WaterBallsEnabled = 1 - $::WaterBallsEnabled; $self->UpdateBallMixerMenu(); 4347 } elsif ($self->{currentItemIndex} == 4) { 4348 $::SeekerBallsEnabled = 1 - $::SeekerBallsEnabled; $self->UpdateBallMixerMenu(); 4349 } 4350 } 4351 } 4352 4353 $self->LeaveSubMenu($recall); 4354} 4355 4356sub UpdateOptionsMenu { 4357 my $self = shift; 4358 4359 $self->{menuItems}->[1]->SetParameter( $::Slippery ? 'on' : 'off' ); 4360 $self->{menuItems}->[3]->SetParameter( $::SoundEnabled ? 'on' : 'off'); 4361 $self->{menuItems}->[4]->SetParameter( $::MusicEnabled ? 'on' : 'off'); 4362 $self->{menuItems}->[5]->SetText('< ' . ('Windowed', 'Fullscreen', 'Widescreen')[$::FullScreen] 4363 . ($self->{restart} ? ' (requires restart)' : '') . ' >'); 4364 $self->{menuItems}->[6]->SetParameter( $::ShowWebsite eq $::Version ? 'no' : 'yes' ); 4365} 4366 4367sub RunOptionsMenu { 4368 my $self = shift; 4369 my ($recall); 4370 4371 $recall = $self->EnterSubMenu(); 4372 my ($y, $yinc) = (80, 38); 4373 push @{$self->{menuItems}}, ( 4374 new MenuItem( 100, $y += $yinc, "Back to main menu"), 4375 new MenuItem( 100, $y += $yinc + 20, "Slippery floor: ", "Turning this on creates and icy floor that you slide on", "This makes the game a lot harder!" ), 4376 new MenuItem( 100, $y += $yinc, "Ball Mixer...", "Turn the special balls on and off.", "This can make the game easier." ), 4377 new MenuItem( 100, $y += $yinc, "Sound: ", "Press Enter to turn sound effects on/off." ), 4378 new MenuItem( 100, $y += $yinc, "Music: ", "Press Enter to turn the background music on/off." ), 4379 new MenuItem( 68, $y += $yinc, "Fullscreen", "Press Left/Right to set the screen mode.", "If you have a wide screen (e.g. 16:9), use the Widescreen option.", "This doesn't take effect until you quit and restart the game." ), 4380 new MenuItem( 100, $y += $yinc, "Show website at exit: ", "Should Pang Zero take you to our web site at exit?", "True enlightenment awaits you online!" ), 4381 ); 4382 $self->UpdateOptionsMenu(); 4383 push @::GameObjects, (@{$self->{menuItems}}); 4384 $self->SetCurrentItemIndex(0); 4385 4386 while (1) { 4387 $self->MenuAdvance(); 4388 last if $self->{abortgame}; 4389 $self->HandleUpDownKeys(); 4390 4391 if ($::MenuEvents{LEFT} and $self->{currentItemIndex} == 5) { 4392 if ($::FullScreen > 0) { --$::FullScreen; $self->{restart} = 1; } 4393 $self->UpdateOptionsMenu(); 4394 } 4395 if ($::MenuEvents{RIGHT} and $self->{currentItemIndex} == 5) { 4396 if ($::FullScreen < 2) { ++$::FullScreen; $self->{restart} = 1; } 4397 $self->UpdateOptionsMenu(); 4398 } 4399 if ($::MenuEvents{BUTTON}) { 4400 last if $self->{currentItemIndex} == 0; # Back to main 4401 if ($self->{currentItemIndex} == 2) { 4402 $self->RunBallMixerMenu(); 4403 } elsif ($self->{currentItemIndex} == 1) { 4404 $::Slippery = $::Slippery ? 0 : 1; $self->UpdateOptionsMenu(); 4405 } elsif ($self->{currentItemIndex} == 3) { 4406 $::SoundEnabled = 1 - $::SoundEnabled; $self->UpdateOptionsMenu(); 4407 } elsif ($self->{currentItemIndex} == 4) { 4408 &::SetMusicEnabled(1 - $::MusicEnabled); $self->UpdateOptionsMenu(); 4409 } elsif ($self->{currentItemIndex} == 6) { 4410 $::ShowWebsite = ($::ShowWebsite eq $::Version ? 0 : $::Version); $self->UpdateOptionsMenu(); 4411 } 4412 } 4413 } 4414 4415 $self->LeaveSubMenu($recall); 4416} 4417 4418sub UpdateControlsMenu { 4419 my $self = shift; 4420 4421 $self->{menuItems}->[1]->SetText("< Number of Players: $::NumGuys >"); 4422 for (my $i = 1 ; $i <= 6; ++$i) { 4423 if ($i > $::NumGuys) { 4424 $self->{menuItems}->[$i+1]->Hide(); 4425 $self->{keysAsText}->[$i-1]->Hide(); 4426 } else { 4427 $self->{menuItems}->[$i+1]->Show(); 4428 $self->{keysAsText}->[$i-1]->Show(); 4429 } 4430 } 4431} 4432 4433sub RunControlsMenu { 4434 my $self = shift; 4435 my ($baseY, $menuItem, $recall, @keysAsText, @yPositions); 4436 4437 $recall = $self->EnterSubMenu(); 4438 $self->{title}->Hide(); 4439 $baseY = 50; 4440 4441 push @{$self->{menuItems}}, 4442 new MenuItem( 50, $baseY, "Back to main menu"), 4443 new MenuItem( 18, $baseY += 40, "<>", "Use left and right key to set the number of players here.", "The more the merrier!", "Don't forget to set their keys below." ); 4444 for ( my $i = 1; $i <= 6; ++$i ) { 4445 $yPositions[$i] = $baseY + 20 + $i * 40; 4446 push @{$self->{menuItems}}, (new MenuItem( 50, $yPositions[$i], "Player $i")); 4447 push @keysAsText, (new MenuItem( 220, $yPositions[$i], &KeysToText($::Players[$i-1]->{keys})) ); 4448 } 4449 push @::GameObjects, (@keysAsText, @{$self->{menuItems}}); 4450 $self->{keysAsText} = \@keysAsText; 4451 $self->UpdateControlsMenu(); 4452 $self->SetCurrentItemIndex(1); 4453 4454 while (1) { 4455 $self->MenuAdvance(); 4456 last if $self->{abortgame}; 4457 $self->HandleUpDownKeys(); 4458 if ($::MenuEvents{LEFT} and $self->{currentItemIndex} == 1) { 4459 --$::NumGuys if $::NumGuys > 1; 4460 $self->UpdateControlsMenu(); 4461 } 4462 if ($::MenuEvents{RIGHT} and $self->{currentItemIndex} == 1) { 4463 ++$::NumGuys if $::NumGuys < 6; 4464 $self->UpdateControlsMenu(); 4465 } 4466 if ($::MenuEvents{BUTTON}) { 4467 last if $self->{currentItemIndex} == 0; # Back to main 4468 next if $self->{currentItemIndex} == 1; 4469 my $player = $::Players[$self->{currentItemIndex} - 2]; 4470 my $key = 0; 4471 my $keysAsText = $keysAsText[$self->{currentItemIndex} - 2]; 4472 $self->{currentItem}->Hide(); 4473 $keysAsText->Hide(); 4474 my @prompts = ("Press 'LEFT' key or joystick button", "Press 'RIGHT' key", "Press 'FIRE' key"); 4475 my $keyMenuItem = new MenuItem( 100, $yPositions[$self->{currentItemIndex} - 1], $prompts[0] ); 4476 push @::GameObjects, ($keyMenuItem); 4477 $keyMenuItem->Select; 4478 while (1) { 4479 $self->MenuAdvance(); 4480 if ($self->{abortgame}) { 4481 $self->{abortgame} = 0; 4482 goto endOfKeyEntry; 4483 } 4484 if (%::Events) { 4485 my ($event) = %::Events; 4486 if ($event =~ /^B(\d+)$/) { 4487 $player->{keys} = ["L$1", "R$1", "B$1"]; 4488 last; 4489 } 4490 $player->{keys}->[$key] = $event; 4491 ++$key; 4492 last if $key >= 3; 4493 $keyMenuItem->SetText($prompts[$key]); 4494 } 4495 } 4496 4497 $keyMenuItem->SetText('Select character'); 4498 my $guy = new Guy($player); 4499 $guy->{x} = $keyMenuItem->{targetX} + $keyMenuItem->{w} + 10; 4500 $guy->{y} = $keyMenuItem->{targetY} - 10; 4501 $guy->DemoMode(); 4502 splice @::GameObjects, -2, 0, $guy; 4503 while (1) { 4504 $self->MenuAdvance(); 4505 if ($self->{abortgame}) { 4506 $self->{abortgame} = 0; 4507 goto endOfKeyEntry; 4508 } 4509 if ($::Events{$player->{keys}->[0]}) { 4510 --$player->{imagefileindex}; 4511 $player->{imagefileindex} = $#::GuyImageFiles if $player->{imagefileindex} < 0; 4512 &::MakeGuySurface($player); $guy->{surface} = $player->{guySurface}; $guy->CalculateAnimPhases(); 4513 } elsif ($::Events{$player->{keys}->[1]}) { 4514 ++$player->{imagefileindex}; 4515 $player->{imagefileindex} = 0 if $player->{imagefileindex} > $#::GuyImageFiles; 4516 &::MakeGuySurface($player); $guy->{surface} = $player->{guySurface}; $guy->CalculateAnimPhases(); 4517 } elsif ($::Events{$player->{keys}->[2]}) { 4518 last; 4519 } 4520 } 4521 4522 $keyMenuItem->SetText('Select color'); 4523 while (1) { 4524 $self->MenuAdvance(); 4525 if ($self->{abortgame}) { 4526 $self->{abortgame} = 0; 4527 goto endOfKeyEntry; 4528 } 4529 if ($::Events{$player->{keys}->[0]}) { 4530 --$player->{colorindex}; 4531 $player->{colorindex} = $#::GuyColors if $player->{colorindex} < 0; 4532 &::MakeGuySurface($player); $guy->{surface} = $player->{guySurface}; 4533 } elsif ($::Events{$player->{keys}->[1]}) { 4534 ++$player->{colorindex}; 4535 $player->{colorindex} = 0 if $player->{colorindex} > $#::GuyColors; 4536 &::MakeGuySurface($player); $guy->{surface} = $player->{guySurface}; 4537 } elsif ($::Events{$player->{keys}->[2]}) { 4538 last; 4539 } 4540 } 4541 4542 endOfKeyEntry: 4543 $guy->Delete() if $guy; 4544 $self->{currentItem}->Show(); 4545 $self->{currentItem}->Select; 4546 $keysAsText->SetText(&KeysToText($player->{keys})); 4547 $keysAsText->Show; 4548 $keyMenuItem->HideAndDelete; 4549 } 4550 } 4551 4552 foreach my $menuItem (@keysAsText) { $menuItem->HideAndDelete(); } 4553 $self->LeaveSubMenu($recall); 4554 $self->{title}->Show(); 4555 delete $self->{keysAsText}; 4556} 4557 4558sub UpdateGameMenu { 4559 my $self = shift; 4560 4561 $self->{menuItems}->[3]->SetText("< Difficulty: $::DifficultyLevel->{name} >"); 4562 $self->{menuItems}->[4]->SetText("< Weapon Duration: $::WeaponDuration->{name} >"); 4563} 4564 4565sub RunGameMenu { 4566 my $self = shift; 4567 my ($recall); 4568 4569 $recall = $self->EnterSubMenu(); 4570 my ($y, $yinc) = (110, 40); 4571 push @{$self->{menuItems}}, ( 4572 new MenuItem( 100, $y += $yinc, "Back to main menu", "Press Enter to return to the main menu"), 4573 new MenuItem( 100, $y += $yinc + 20, "Start Panic Game", "In Panic Mode, the balls continuously fall from the sky.", "Can you keep up the pace?", "This game is for advanced players." ), 4574 new MenuItem( 100, $y += $yinc, "Start Challenge Game", "More and more difficult levels challenge your skill.", "This game is best for beginners." ), 4575 new MenuItem( 68, $y += $yinc, "<>", "Press the Left and Right keys to set the game difficulty.", "The game speed and number of harpoons depend on this setting.", "The `Miki' level is for Deathball Specialists (Panic mode only)." ), 4576 new MenuItem( 68, $y += $yinc, "<>", "Press the Left and Right keys to set the bonus weapon duration.", "This will determine how long you can use bonus weapons." ), 4577 ); 4578 $self->UpdateGameMenu(); 4579 push @::GameObjects, (@{$self->{menuItems}}); 4580 $self->SetCurrentItemIndex($::LastGameMenuResult ? $::LastGameMenuResult : 1); 4581 4582 while (1) { 4583 $self->MenuAdvance(); 4584 last if $self->{abortgame}; 4585 $self->HandleUpDownKeys(); 4586 4587 if ($::MenuEvents{LEFT} and $self->{currentItemIndex} == 3) { 4588 &::SetDifficultyLevel($::DifficultyLevelIndex - 1); 4589 $self->UpdateGameMenu(); 4590 } 4591 if ($::MenuEvents{RIGHT} and $self->{currentItemIndex} == 3) { 4592 &::SetDifficultyLevel($::DifficultyLevelIndex + 1); 4593 $self->UpdateGameMenu(); 4594 } 4595 if ($::MenuEvents{LEFT} and $self->{currentItemIndex} == 4) { 4596 &::SetWeaponDuration($::WeaponDurationIndex - 1); 4597 $self->UpdateGameMenu(); 4598 } 4599 if ($::MenuEvents{RIGHT} and $self->{currentItemIndex} == 4) { 4600 &::SetWeaponDuration($::WeaponDurationIndex + 1); 4601 $self->UpdateGameMenu(); 4602 } 4603 if ($::MenuEvents{BUTTON}) { 4604 last if $self->{currentItemIndex} == 0; # Back to main 4605 if ($self->{currentItemIndex} == 1) { 4606 $self->{result} = 'panic'; 4607 } elsif ($self->{currentItemIndex} == 2) { 4608 if ($::DifficultyLevel->{name} ne 'Miki') { 4609 $self->{result} = 'challenge'; 4610 } else { 4611 $self->ShowTooltip("Miki difficulty level is for panic mode only."); 4612 } 4613 } 4614 } 4615 last if $self->{result}; 4616 } 4617 4618 $::LastGameMenuResult = $self->{currentItemIndex}; 4619 $self->LeaveSubMenu($recall); 4620} 4621 4622sub OnMenuIdle { 4623 my $self = shift; 4624 4625 ++$self->{idle}; 4626 if ($self->{idle} == 1) { $self->RunHighScores('auto'); } 4627 elsif ($self->{idle} == 2) { $self->RunCredits('demo'); } 4628 elsif ($self->{idle} == 3) { $self->{idle} = 0; return 'demo'; } 4629 return ''; 4630} 4631 4632sub Run { 4633 my $self = shift; 4634 my ($y, $yinc, $idle); 4635 4636 $self->ResetGame(); 4637 $::ScoreFont->use(); 4638 ($y, $yinc) = ($::ScreenHeight + 15, 20); 4639 $::Background->print( 10, $y += $yinc, "Pang Zero $::Version (C) 2006 by UPi (upi\@sourceforge.net)" ) if $y + $yinc * 2 < $::PhysicalScreenHeight; 4640 $::Background->print( 10, $y += $yinc, "Use cursor keys to navigate menu, Enter to select" ) if $y + $yinc * 2 < $::PhysicalScreenHeight; 4641 $::Background->print( 10, $y += $yinc, "P pauses the game, Esc quits" ) if $y + $yinc * 2 < $::PhysicalScreenHeight; 4642 $::Background->blit(0, $::App, 0); 4643 4644 $::MenuFont->use(); 4645 push @::GameObjects, (new FpsIndicator); 4646 $self->SetGameSpeed(); 4647 $::GamePause = 0; 4648 4649 ($y, $yinc) = (90, 40); 4650 4651 $self->{menuItems} = [ 4652 new MenuItem( 100, $y += $yinc, "Start Game" ), 4653 new MenuItem( 100, $y += $yinc, "Options", "Various game settings" ), 4654 new MenuItem( 100, $y += $yinc, "Setup players", "Set the number of players, setup keys and joysticks" ), 4655 new MenuItem( 100, $y += $yinc, "Help", "How to play the game, demo of special balls" ), 4656 new MenuItem( 100, $y += $yinc, "Credits", "You might be wondering: Who has created Pang Zero?", "Wonder no more." ), 4657 new MenuItem( 100, $y += $yinc, "High Scores", "Hall of Fame." ), 4658 new MenuItem( 100, $y += $yinc, "Exit Game", "Press Enter to exit the game" ), 4659 ]; 4660 4661 $self->{title} = new MenuItem( 300, 60, "PANG ZERO" ); 4662 $self->{title}->{filled} = 1; 4663 $self->{title}->{fillcolor} = new SDL::Color(-b=>255, -g=>128); 4664 $self->{title}->Center(); 4665 4666 push @::GameObjects, ( 4667 &Ball::Spawn($::BallDesc[8], -1, 1), 4668 &Ball::Spawn($::BallDesc[0], -1, 0), 4669 &Ball::Spawn($::BallDesc{super0}, -1, 1), 4670 &Ball::Spawn($::BallDesc[2], -1, 0), 4671 &Ball::Spawn($::BallDesc[5], -1, 1), 4672 $self->{title}, 4673 @{$self->{menuItems}}, 4674 ); 4675 4676 $self->SetCurrentItemIndex( 0 ); 4677 &GameTimer::ResetTimer(); 4678 4679 while (1) { 4680 $self->MenuAdvance(); 4681 $self->Exit() if $self->{abortgame}; 4682 $self->HandleUpDownKeys(); 4683 last if $self->{result}; 4684 if ($::MenuEvents{BUTTON}) { 4685 if ($self->{currentItemIndex} == 0) { 4686 $self->RunGameMenu(); 4687 } elsif ($self->{currentItemIndex} == 1) { 4688 $self->RunOptionsMenu(); 4689 } elsif ($self->{currentItemIndex} == 2) { 4690 $self->RunControlsMenu(); 4691 } elsif ($self->{currentItemIndex} == 3) { 4692 $self->RunTutorialMenu; 4693 } elsif ($self->{currentItemIndex} == 4) { 4694 $self->RunCredits(); 4695 } elsif ($self->{currentItemIndex} == 5) { 4696 $self->RunHighScores(); 4697 } 4698 $self->Exit() if $self->{currentItemIndex} == 6; 4699 } 4700 if (%::Events) { 4701 $idle = 0; 4702 } else { 4703 if (++$idle > 1000) { $self->{result} = $self->OnMenuIdle(); $idle = 0; } 4704 } 4705 } 4706 4707 $::ScoreFont->use(); 4708 return $self->{result}; 4709} 4710 4711 4712########################################################################## 4713package main; 4714########################################################################## 4715 4716 4717sub SaveScreenshot { 4718 my $i = 0; 4719 my $filename; 4720 do { $filename = sprintf("screenshot%03d.bmp", $i); ++$i } while (-f $filename); 4721 $App->save_bmp($filename); 4722} 4723 4724sub Pause { 4725 my $pausedSurface = new SDL::Surface(-name => "$DataDir/paused.png"); 4726 my $event = new SDL::Event; 4727 4728 $pausedSurface->blit(0, $App, new SDL::Rect(-x => ($PhysicalScreenWidth - $pausedSurface->width) / 2, -y => $PhysicalScreenHeight / 2 - 100)); 4729 $App->sync(); 4730 $::Keys = (); $::Events = (); 4731 while (1) { # Paused, wait for keypress 4732 $event->wait(); 4733 last if $event->type() == SDL_KEYDOWN and $event->key_sym == SDLK_p; 4734 if ($event->type() == SDL_KEYDOWN and $event->key_sym == SDLK_ESCAPE) { $Game->{abortgame} = 1; last; } 4735 $Game->Exit() if $event->type() == SDL_QUIT; 4736 } 4737 $Background->blit(0, $App, 0); 4738 &GameTimer::ResetTimer(); 4739} 4740 4741sub HandleEvents { 4742 my ($readBothJoystickAxes) = @_; 4743 my ($event, $type); 4744 4745 $event = new SDL::Event; 4746 while (1) { 4747 last unless $event->poll(); 4748 $type = $event->type(); 4749 4750 if ($type == SDL_QUIT) { 4751 $Game->Exit(); 4752 } 4753 elsif ($type == SDL_KEYDOWN) { 4754 my $keypressed = $event->key_sym; 4755 if ($keypressed == SDLK_ESCAPE) { 4756 $Game->{abortgame} = 1; 4757 } elsif ($keypressed == SDLK_F1) { 4758 &SaveScreenshot(); 4759 } elsif ($keypressed == SDLK_p and not $UnicodeMode) { 4760 &Pause(); 4761 } else { 4762 $Keys{$keypressed} = 1; 4763 $Events{$keypressed} = 1; 4764 $MenuEvents{UP} = 1 if $keypressed == SDLK_UP(); 4765 $MenuEvents{DOWN} = 1 if $keypressed == SDLK_DOWN(); 4766 $MenuEvents{LEFT} = 1 if $keypressed == SDLK_LEFT(); 4767 $MenuEvents{RIGHT} = 1 if $keypressed == SDLK_RIGHT(); 4768 $MenuEvents{BUTTON} = 1 if $keypressed == SDLK_RETURN(); 4769 $LastUnicodeKey = $event->key_unicode() if $UnicodeMode; 4770 } 4771 } 4772 elsif ($type == SDL_KEYUP) { 4773 my $keypressed = $event->key_sym; 4774 $Keys{$keypressed} = 0; 4775 } 4776 } 4777 4778 &Joystick::ReadJoystick($readBothJoystickAxes); 4779} 4780 4781sub DoMenu { 4782 my $oldScreenHeight = $ScreenHeight; 4783 my $oldScreenWidth = $ScreenWidth; 4784 $ScreenWidth = $PhysicalScreenWidth - $ScreenMargin * 2; 4785 $ScreenWidth = int($ScreenWidth / 32) * 32; 4786 4787 $Game = new Menu; 4788 my $retval = $Game->Run(); 4789 &SaveConfig(); 4790 4791 $ScreenWidth = $oldScreenWidth; 4792 $ScreenHeight = $oldScreenHeight; 4793 4794 return $retval; 4795} 4796 4797sub DoDemo { 4798 my $messages = $Game->{messages} = { 4799 1 => "Use harpoons to pop the balloons", 4800 160 => "Pop them, and they split in two", 4801 300 => "Pop them again and again", 4802 530 => "Popping the smallest ballons makes them disappear", 4803 630 => "The green Super Ball gives you a lot of free time", 4804 720 => "Use this time wisely!", 4805 1150 => "Making a lot of small balls is dangerous! Observe...", 4806 1600 => "Don't let the balloons touch you!", 4807 1708 => "Dying gives you some free time.", 4808 1900 => "So does shooting the flashing balloons.", 4809 2370 => "The yellow Super Ball destroys every balloon", 4810 2650 => "And now... THE SPECIAL BALL DEMO!", 4811 2950 => "The Bouncy Ball bounces twice as high as normal balls.", 4812 3620 => "See?", 4813 4222 => "The Hexa Ball is weightless and travels in a straight line.", 4814 4500 => "So does its offspring.", 4815 5210 => "The blue Water Ball splits every time it bounces.", 4816 5900 => "This can cause a tide of small balls!", 4817 6630 => "The Earthquake Ball will really shake you up.", 4818 7100 => "Its offspring is not as dangerous, but still annoying.", 4819 7800 => "Behold, the Death Ball. It cannot be killed!!!", 4820 8120 => "No, really, it can't! In fact, shooting it makes it multiply.", 4821 8220 => "If you avoid it for 20 secs, Deathballs will get bored and go away.", 4822 8320 => "Also, the yellow Super Ball will destroy the Deathballs for you.", 4823 8800 => "Shooting it too much will lead to the Deathball Meltdown.", 4824 9550 => "Last but not least: here's the Seeker Ball!", 4825 9900 => "This ball will stalk you forever.", 4826 10100 => "Whew! This concludes the Special Ball Demo. Have fun playing!", 4827 }; 4828 my $record = 0 x 23 . 1 x 18 . 0 x 19 . 2 x 7 . 0 x 31 . 4 x 1 . 0 x 44 . 2 x 43 . 0 x 7 . 4 x 1 . 0 x 22 . 1 x 10 . 0 x 17 . 2 x 38 . 0 x 16 . 2 x 22 . 0 x 42 . 4 x 1 . 0 x 54 . 1 x 43 . 0 x 2 . 4 x 1 . 0 x 28 . 1 x 27 . 0 x 8 . 4 x 1 . 0 x 98 . 2 x 19 . 0 x 11 . 4 x 1 . 0 x 27 . 1 x 24 . 5 x 1 . 1 x 1 . 0 x 17 . 1 x 9 . 0 x 2 . 4 x 1 . 0 x 51 . 2 x 19 . 0 x 14 . 4 x 1 . 0 x 48 . 1 x 14 . 0 x 2 . 4 x 1 . 0 x 51 . 1 x 8 . 0 x 25 . 4 x 1 . 0 x 49 . 2 x 25 . 0 x 3 . 4 x 1 . 0 x 53 . 1 x 12 . 0 x 9 . 4 x 1 . 0 x 101 . 1 x 9 . 0 x 4 . 4 x 1 . 0 x 68 . 1 x 7 . 5 x 1 . 0 x 75 . 2 x 14 . 0 x 2 . 4 x 1 . 0 x 64 . 2 x 38 . 0 x 3 . 4 x 1 . 0 x 13 . 2 x 13 . 0 x 25 . 2 x 25 . 0 x 5 . 4 x 1 . 0 x 54 . 4 x 1 . 0 x 69 . 1 x 3 . 0 x 15 . 4 x 1 . 0 x 19 . 2 x 17 . 0 x 94 . 2 x 28 . 0 x 27 . 2 x 52 . 0 x 22 . 4 x 1 . 0 x 34 . 1 x 28 . 0 x 34 . 1 x 29 . 0 x 24 . 4 x 1 . 0 x 80 . 1 x 15 . 0 x 116 . 1 x 10 . 5 x 1 . 1 x 1 . 0 x 808 . 2 x 35 . 0 x 16 . 4 x 1 . 0 x 55 . 1 x 46 . 5 x 1 . 1 x 2 . 0 x 368 . 8 x 1 . 0 x 487 . 1 x 27 . 0 x 48 . 2 x 8 . 6 x 1 . 2 x 7 . 0 x 7 . 2 x 18 . 6 x 1 . 2 x 11 . 0 x 119 . 1 x 1 . 0 x 167 . 8 x 1 . 0 x 1177 . 2 x 24 . 0 x 121 . 2 x 22 . 0 x 2 . 4 x 1 . 0 x 31 . 2 x 15 . 0 x 9 . 2 x 4 . 6 x 1 . 2 x 5 . 0 x 8 . 2 x 10 . 0 x 69 . 8 x 1 . 0 x 338 . 1 x 87 . 0 x 152 . 2 x 52 . 0 x 112 . 1 x 27 . 0 x 2 . 4 x 1 . 0 x 71 . 1 x 41 . 0 x 4 . 4 x 1 . 0 x 65 . 2 x 24 . 0 x 209 . 8 x 1 . 0 x 579 . 1 x 3 . 0 x 13 . 2 x 3 . 0 x 14 . 4 x 1 . 0 x 58 . 2 x 28 . 0 x 9 . 4 x 1 . 0 x 93 . 2 x 37 . 0 x 26 . 2 x 11 . 0 x 22 . 2 x 9 . 6 x 1 . 2 x 6 . 6 x 1 . 2 x 7 . 6 x 1 . 2 x 5 . 6 x 1 . 2 x 7 . 6 x 1 . 2 x 16 . 6 x 1 . 2 x 9 . 6 x 1 . 2 x 20 . 1 x 7 . 0 x 21 . 2 x 13 . 1 x 3 . 5 x 1 . 1 x 8 . 5 x 1 . 1 x 6 . 5 x 1 . 1 x 6 . 5 x 1 . 1 x 35 . 0 x 6 . 5 x 1 . 1 x 6 . 0 x 11 . 2 x 12 . 6 x 1 . 2 x 8 . 6 x 1 . 1 x 6 . 5 x 1 . 1 x 3 . 0 x 3 . 4 x 1 . 1 x 3 . 0 x 3 . 5 x 1 . 1 x 4 . 0 x 15 . 1 x 2 . 5 x 1 . 1 x 4 . 0 x 4 . 5 x 1 . 1 x 7 . 5 x 1 . 1 x 4 . 0 x 5 . 1 x 6 . 0 x 2 . 4 x 1 . 1 x 4 . 0 x 4 . 4 x 1 . 0 x 3 . 1 x 4 . 0 x 3 . 4 x 1 . 0 x 10 . 2 x 14 . 6 x 1 . 2 x 2 . 1 x 5 . 5 x 1 . 1 x 6 . 5 x 1 . 1 x 5 . 5 x 1 . 1 x 2 . 0 x 3 . 4 x 1 . 1 x 3 . 0 x 3 . 4 x 1 . 1 x 3 . 0 x 2 . 1 x 2 . 5 x 1 . 1 x 4 . 5 x 1 . 1 x 6 . 5 x 1 . 1 x 7 . 5 x 1 . 1 x 7 . 0 x 2 . 2 x 4 . 6 x 1 . 2 x 4 . 0 x 2 . 2 x 2 . 6 x 1 . 2 x 6 . 6 x 1 . 2 x 7 . 1 x 5 . 5 x 1 . 1 x 1 . 0 x 5 . 2 x 6 . 6 x 1 . 2 x 2 . 0 x 4 . 1 x 3 . 5 x 1 . 1 x 1 . 0 x 8 . 2 x 4 . 6 x 1 . 2 x 1 . 0 x 3 . 1 x 4 . 0 x 7 . 2 x 6 . 6 x 1 . 2 x 8 . 6 x 1 . 2 x 6 . 6 x 1 . 2 x 3 . 0 x 3 . 1 x 3 . 0 x 10 . 2 x 7 . 0 x 2 . 1 x 1 . 5 x 1 . 1 x 5 . 0 x 2 . 4 x 1 . 1 x 2 . 0 x 4 . 4 x 1 . 0 x 2 . 1 x 2 . 0 x 3 . 1 x 1 . 5 x 1 . 1 x 5 . 5 x 1 . 1 x 3 . 0 x 4 . 5 x 1 . 1 x 1 . 0 x 4 . 4 x 1 . 1 x 2 . 0 x 4 . 4 x 1 . 1 x 1 . 0 x 6 . 4 x 1 . 1 x 1 . 0 x 5 . 1 x 1 . 5 x 1 . 1 x 2 . 0 x 3 . 1 x 1 . 5 x 1 . 1 x 6 . 5 x 1 . 1 x 7 . 5 x 1 . 1 x 6 . 5 x 1 . 1 x 6 . 5 x 1 . 1 x 7 . 0 x 12 . 2 x 7 . 0 x 2 . 4 x 1 . 2 x 2 . 0 x 4 . 4 x 1 . 0 x 135 . 8 x 1 . 0 x 252 . 1 x 57 . 0 x 199 . 2 x 37 . 0 x 3 . 1 x 1 . 5 x 1 . 1 x 29 . 0 x 21 . 1 x 30 . 0 x 37 . 4 x 1 . 0 x 77 . 1 x 17 . 0 x 4 . 2 x 126 . 3 x 1 . 1 x 52 . 5 x 1 . 1 x 64 . 0 x 39 . 8 x 1 . 0 x 140; 4829 my $rand = [2199.02,1.12,0.11,1.24,0.11,1.21,0.33,0.19,0.16,0.12,0.07,0.28,0.68]; 4830 4831 &::SaveConfig(); 4832 $Game = new DemoPlaybackGame( 1, 3, $record, $rand, $messages ); 4833 $Game->Run(); 4834 &::LoadConfig(); 4835 $Game->RestoreGameSettings(); 4836} 4837 4838sub DoRecordDemo { 4839 my ($numguys, $difficulty) = ($NumGuys, $DifficultyLevelIndex); 4840 4841 $NumGuys = 1; 4842 &SetDifficultyLevel(3); 4843 $Game = new DemoRecordGame; 4844 $Game->Run(); 4845 print "\n\$record = '", $Game->{record}, "';\n"; 4846 print "\$rand = [", join( ', ', @{$Game->{rand}} ), "];\n\n"; 4847 $NumGuys = $numguys; 4848 &SetDifficultyLevel($difficulty); 4849} 4850 4851 4852########################################################################## 4853# MAIN PROGRAM STARTS HERE 4854########################################################################## 4855 4856sub Initialize { 4857 4858 eval { SDL::Init(SDL_INIT_EVERYTHING()); }; 4859 eval { SDL::Init(SDL::INIT_EVERYTHING()); } if $@; # This is a workaround for SDL_perl 1.2.20 4860 die "Unable to initialize SDL: $@" if $@; 4861 4862 &FindDataDir(); 4863 &LoadConfig(); 4864 print "Data directory is at '$DataDir'\n"; 4865 my $sdlFlags; 4866 if (&IsMicrosoftWindows()) { 4867 $sdlFlags = SDL_ANYFORMAT; 4868 } else { 4869 $sdlFlags = SDL_HWSURFACE | SDL_HWACCEL | SDL_DOUBLEBUF | SDL_ANYFORMAT; 4870 } 4871 4872 ($PhysicalScreenWidth, $PhysicalScreenHeight) = &FindVideoMode(); 4873 #($PhysicalScreenWidth, $PhysicalScreenHeight) = (848, 480); $FullScreen = 0; 4874 4875 $App = new SDL::App 4876 -flags => $sdlFlags, 4877 -title => "Pang Zero $::Version", 4878 -icon => "$DataDir/icon.png", 4879 -width => $PhysicalScreenWidth, 4880 -height => $PhysicalScreenHeight, 4881 -fullscreen => $FullScreen, 4882 ; 4883 eval( 'use SDL::Tool::Graphic; $RotoZoomer = new SDL::Tool::Graphic; $::SmoothRotoZoom = 0;' ); # Detect if zoom / rotozoom works 4884 4885 &SDL::ShowCursor(0); 4886 4887 $Background = new SDL::Surface( 4888 -name =>'', 4889 -flags=> ( &IsMicrosoftWindows ? SDL_SWSURFACE : SDL_HWSURFACE ), 4890 -width => $App->width, 4891 -height => $App->height, 4892 -depth => 16, 4893 -Amask => '0 but true'); 4894 $Background->display_format; 4895 $ScoreFont = new SDL::Font("$DataDir/brandybun3.png"); 4896 $MenuFont = new SDL::Font("$::DataDir/font2.png"); 4897 $GlossyFont = new SDL::Font("$::DataDir/glossyfont.png"); 4898 4899 &LoadSurfaces(); 4900 &LoadSounds(); 4901 &Joystick::InitJoystick(); 4902} 4903 4904sub MainLoop { 4905 my $menuResult = &DoMenu(); 4906 if ($menuResult eq 'demo') { 4907 &DoDemo(); 4908 return; 4909 } 4910 4911 # $Game = new DemoRecordGame; 4912 if ($menuResult eq 'challenge') { 4913 $Game = new ChallengeGame; 4914 } else { 4915 $Game = new PanicGame; 4916 } 4917 @UnsavedHighScores = (); 4918 $Game->Run(); 4919 4920 bless $Game, 'Menu'; 4921 $Game->{abortgame} = 0; 4922 { my @gameObjects = @GameObjects; foreach (@gameObjects) { $_->Delete() if ref $_ eq 'Guy'; } } 4923 $Background->blit(0, $App, 0); 4924 $MenuFont->use(); 4925 &MergeUnsavedHighScores($menuResult eq 'challenge' ? 'Cha' : 'Pan'); 4926return; 4927 4928 my ($filename, $i) = ('', 1); 4929 do { $filename = sprintf("record%03d.txt", $i); ++$i } while (-f $filename); 4930 open RECORD, ">$filename"; 4931 print RECORD "NumGuys = $NumGuys;\nDifficultyLevelIndex = $DifficultyLevelIndex;\nrecord = '$Game->{record}';\n", 4932 "DeathBallsEnabled = $DeathBallsEnabled;\nEarthquakeBallsEnabled = $EarthquakeBallsEnabled;\n", 4933 "WaterBallsEnabled = $WaterBallsEnabled;\nSeekerBallsEnabled = $SeekerBallsEnabled;\n", 4934 'rand = [', join(',', @{$Game->{rand}}), "];\n\n"; 4935 close RECORD; 4936 4937 $Game = new DemoPlaybackGame($NumGuys, $DifficultyLevelIndex, $Game->{record}, $Game->{rand}, {}); 4938 $Game->Run(); 4939 $Game->RestoreGameSettings(); 4940} 4941 4942sub ShowErrorMessage { 4943 my ($message) = @_; 4944 4945 eval("SDL::Quit"); warn $@ if $@; 4946 $message = "Pang Zero $::Version died:\n$message"; 4947 if (&IsMicrosoftWindows()) { 4948 eval( ' 4949 use Win32; 4950 Win32::MsgBox($message, MB_ICONEXCLAMATION, "Pang Zero error"); 4951 ' ); 4952 return; 4953 } elsif ($ENV{'DISPLAY'}) { 4954 $message =~ s/\"/\\"/g; 4955 my @tryCommands = ( 4956 "kdialog --msgbox \"$message\"", 4957 "gmessage -center \"$message\"", 4958 "xmessage -center \"$message\"", 4959 ); 4960 foreach (@tryCommands) { 4961 `$_`; 4962 return if $? == 0; 4963 } 4964 } 4965} 4966 4967sub ShowWebPage { 4968 my ($url) = @_; 4969 4970 eval("SDL::Quit"); warn $@ if $@; 4971 if (&IsMicrosoftWindows()) { 4972 my $ws = "$DataDir/website.html"; 4973 $ws =~ s/\//\\\\/g; 4974 exec 'cmd', '/c', $ws; 4975 exit; 4976 } elsif ($ENV{'DISPLAY'}) { 4977 my @tryCommands = ( 4978 "gnome-open $url", 4979 "mozilla-firefox $url", 4980 "firefox $url", 4981 "mozilla $url", 4982 "konqueror $url", 4983 ); 4984 foreach (@tryCommands) { 4985 `$_`; 4986 return if $? == 0; 4987 } 4988 } else { 4989 print "Visit $url for more info about Pang Zero $::Version\n"; 4990 } 4991} 4992 4993 4994# 4995# Program Entry Point 4996# 4997 4998eval { 4999 &Initialize(); 5000 #&DoDemo() while 1; 5001 #while (1) { &DoRecordDemo(); $::App->delay(2000); } 5002 while (1) { &MainLoop(); } 5003}; 5004if ($@) { 5005 my $errorMessage = $@; 5006 &ShowErrorMessage($errorMessage); 5007 die $errorMessage; 5008} 5009 5010