Pardon the mess, Play My Code is in beta!

READY TO PLAY?
CLICK TO LOG IN!

sign up - lost password

Asteroids »

You do not own this project, so changes will not be saved

/*
 * Asteroids   Version 1.2
 * By Andy White   @krakatomato   http://www.fivesprites.com
 *
 * A clone of the classic Atari game.
 * 
 * Feel free to use this code as you wish.  Hopefully others will 
 * learn from it in some way.
 * 
 * V1.0 : Original
 * V1.1 : Fixed an issue with player movement when at maximum speed
 * V1.2 : Minor changes
 */
 
// DEBUG MODE - enable to see various counts and bounding boxes
$DEBUG_MODE     = false

$PI             = 3.1415926535
$HALF_PI        = $PI / 2
$TWO_PI         = $PI * 2

$controls       = getControls()
$screenWidth    = getScreenWidth()
$screenHeight   = getScreenHeight()
$halfWidth      = $screenWidth / 2
$halfHeight     = $screenHeight / 2

// Rock polygon definitions in (x,y) pairs
$largeRock1 = [-39,-25, -33,-8, -38,21, -23,25, -13,39, 24,34, 38,7, 33,-15, 38,-31, 16,-39, -4,-34, -16,-39]                        
$largeRock2 = [-32, 35, -4, 32, 24, 38, 38, 23, 31, -4, 38, -25, 14, -39, -28, -31, -39, -16, -31, 4, -38, 22]                        
$largeRock3 = [12, -39, -2, -26, -28, -37, -38, -14, -21, 9, -34, 34, -6, 38, 35, 23, 21, -14, 36, -25]           
$medRock1   = [-7, -19, -19, -15, -12, -5, -19, 0, -19, 13, -9, 19, 12, 16, 18, 11, 13, 6, 19, -1, 16, -17]        
$medRock2   = [9, -19, 18, -8, 7, 0, 15, 15, -7, 13, -16, 17, -18, 3, -13, -6, -16, -17]          
$medRock3   = [2, 18, 18, 10, 8, 0, 18, -13, 6, -18, -17, -14, -10, -3, -13, 15]            
$smallRock1 = [-8, -8, -5, -1, -8, 3, 0, 9, 8, 4, 8, -5, 1, -9]        
$smallRock2 = [-6, 8, 1, 4, 8, 7, 10, -1, 4, -10, -8, -6, -4, 0]           
$smallRock3 = [-8, -9, -5, -2, -8, 5, 6, 8, 9, 6, 7, -3, 9, -9, 0, -7]

// Ship polygon definition using x/y pairs 
$ship       = [0,-11, -7,11, -3,7, 3,7, 7,11]
$flame      = [-2,7, 0,12, 2,7, -2,7]

// UFO polygon definition using x/y pairs
$ufo        = [-5, -5, -3, -9, 3, -9, 5, -5, -5, -5, -12, 0, 12, 0, 5, -5, 12, 0, 7, 4, -7, 4, -12, 0]

// Random rock types (only large ones at the start of a level)
$lgeRocks   = { 0 : $largeRock1, 1 : $largeRock2, 2 : $largeRock3 }
$medRocks   = { 0 : $medRock1,   1 : $medRock2,   2 : $medRock3   }
$smlRocks   = { 0 : $smallRock1, 1 : $smallRock2, 2 : $smallRock3 }

$lgeRockSpeed  = 0.8
$medRockSpeed  = 1.1
$smlRockSpeed  = 2.2

$RAND_LOC      = -999

// Scoring
$pointsLgeRock = 20
$pointsMedRock = 50
$pointsSmlRock = 100
$pointsLgeUfo  = 200
$pointsSmlUfo  = 1000

$maxBullets    = 4      // Maximum bullets at any one time
$numStartRocks = 4      // Start with 4 large rocks
$maxRocks      = 100    // Max number of rocks onscreen at once
$maxScoreDigits= 10

// Currently logged in user
$user           = new User()
$highScore      = $user.loadGame()
if ($highScore == null)
    $highScore = 1000
end

// Instance of the font class for writing the Vector style text
$font           = new Font()

// Instance of the explosion manager class which handles all
// of the explosions
$MAX_EXPLOSIONS = 20
$expManager     = new ExplosionManager($MAX_EXPLOSIONS)

// Create a new instance of the game 
$game           = new Game()

if ($DEBUG_MODE)
    showFPS()
end
setFont( 'Arial', 12, 'bold' )

lX           = 50
lgLogo       = new Image("fivesprites_l_lg.png")
logoTimer    = new Timer(5000)
logoFinished = true
bgImg        = new Image("backdrop2.png")

showCursor(false)

// Main game loop
onEachFrame() do |delta|
    fill( 0, 0, 0 )
    setAlpha(0.8) do
        drawImage(bgImg, 0, 0, $screenWidth, $screenHeight, false)
    end
    if (logoFinished)
        $game.update(delta)  
        $game.render()
    else
        if (logoTimer.isExpired())
            lX = lX - 25
        end
        setColor(255, 255, 255, logoTimer.getPercent())
        drawImage(lgLogo, lX, 50, false)
        if ( lX < -lgLogo.getWidth() )
            logoFinished = true
        end
    end
end

// Game class
// Everything is managed from within here
class Game
    get(numBullets)
    
    def new()
        @score        = 0
        @lastScore    = 0
        @level        = 1
        @lives        = 3
        @level        = 0
        @state        = :menu
        @timer        = new Timer(5*1000)
        @endTimer     = new Timer(1000)
        @ufoTimer     = new Timer(5000 + (30000/(1+@level)))
        @flipMenu     = false
        @alf          = 1.0
        @alfv         = 0.02
        @player       = new Player()
        @numRocks     = 0
        @numUfos      = 0
        @ufos         = []
        @asteroids    = []
        @missiles     = []
        @sndStart     = new Sound("start.mp3")
        @bullets      = []
        @numBullets   = 0
        @numMissiles  = 0
        @beat         = 0
        @nextBeatTime = 0
        @beatIntensity= 1000
        @beatSnd1     = new Sound("thumplo.mp3")
        @beatSnd2     = new Sound("thumphi.mp3")
        @extraLifeSound = new Sound("life.mp3").setVolume(0.3)
        @beatSnd1.setVolume(0.3)
        @beatSnd2.setVolume(0.3)
        @fsLogo = new Image("fivesprites_l.png")
        init(0)
    end
    read(level)
    
    // Look for a safe area to spawn the player
    def isSafeToSpawn(x, y, bounds)
        safe = true
        i = 0
        // Ensure the area is clear of asteroids
        while (i < @numRocks)
            if (@asteroids[i].isBoundsOverlap(x, y, bounds) == true)
                safe = false
            end
            i = i + 1
        end
        return safe
    end
    
    // Jump to a random location (that's not occupied by asteroids)
    def hyperJump()
        pr = @player.Bounds()
        
        x = 0
        y = 0
        found = false
        while(found == false) 
            x = pr + rand($screenWidth-pr)
            y = pr + rand($screenHeight-pr)
            if (isSafeToSpawn(x, y, pr*3) == true)
                found = true
            else
                found = false
            end
        end
        // Looks like we've found a safe spot, so spawn the player there
        @player.setX(x)
        @player.setY(y)
    end
    
    // Re-initialise 
    def init(numRocks)
        
        // Remove all of the existing asteroids
        @numRocks.times() do
            @asteroids.deleteAt(0)
        end
        @numRocks = 0
        
        // Create some random asteroids for the menu
        if (@state == :menu)
            @numUfos.times() do |i|
                @ufos.deleteAt(0)
            end
            @numBullets.times() do |i|
                @bullets.deleteAt(0)
            end
            @numMissiles.times() do |i|
                @missiles.deleteAt(0)
            end
            @numRocks = rand(8, 20).round()
            @numRocks.times() do |i|
                @asteroids.push(new Asteroid("M" + i, rand(2).round(), $RAND_LOC, $RAND_LOC))
            end
        else
            @numRocks = 0
            // Create the asteroids for each game level
            @startRocks.times() do |i|
                addAsteroid("A" + i, 0, $RAND_LOC, $RAND_LOC)
            end
            // Reset the thumping beat intensity
            @beatIntensity = 1000
        end
    end

    def update(delta)
        // State machine
        if (@state == :menu)                // MENU state
            @asteroids.eachIndex() do |i|
                @asteroids[i].update(delta)
            end
            
            // Start a new game if 'S' key pressed
            if ($controls.isKeyPressed("s"))
                @state = :game
                @sndStart.play()
                @player.reset()
                @level  = 0
                @score  = 0
                @lastScore = 0
                @ufoTimer = new Timer(8000 + (30000/(1+@level)))
                @lives  = 3
                @startRocks = $numStartRocks
                init($numStartRocks)
            end
        else if (@state == :game)           // GAME IN PROGRESS            
            // Is it time to add a new UFO?
            if (@ufoTimer.isExpired())
                addUFO()
                @ufoTimer = new Timer(8000 + (30000/(1+@level)))
            end
            
            // Update all of the player bullets
            updateBullets(delta)
            
            updateMissiles(delta)
            
            // Check for UFO collisions
            @ufos.eachIndex() do |i|
                destUfo = false
                // Update this UFO
                @ufos[i].update(delta)
                
                // Has the UFO hit the player?
                if (@ufos[i].isCollide(@player, true))
                    @player.explode()
                    destUfo = true
                    $expManager.add(@ufos[i].X(), @ufos[i].Y(), 20)
                end
                
                // Have any of the player bullets hit this UFO?
                @bullets.eachIndex() do |b|
                    if (@ufos[i].isCollide(@bullets[b], true))
                        destUfo = true
                        $expManager.add(@ufos[i].X(), @ufos[i].Y(), 20)
                        @score = @score + $pointsSmlUFO
                    end
                end
                
                // Destroy the UFO if it was hit
                if (destUfo)
                    @ufos[i].explode()
                end
            end
            
            // Check for UFO missiles hitting player
            if (@player.isDead() == false)
                @missiles.eachIndex() do |m|
                    if (@missiles[m].isCollide(@player, true))
                        @player.explode()
                    end
                end
            end
            
            // Update all asteroids and check for collisions
            @asteroids.eachIndex() do |i|
                
                // Update this asteroid
                @asteroids[i].update(delta)
                
                destAsteroid = false
                if (@player.isDead() == false)
                    // Destroy the player if collision with an asteroid
                    if (@asteroids[i].isCollide(@player, true))
                        @player.explode()
                    end
                    
                    // Check for any collisions with UFO's
                    @ufos.eachIndex() do |u|
                        if (@asteroids[i].isCollide(@ufos[u], true))
                            $expManager.add(@ufos[u].X(), @ufos[u].Y(), 20)
                            @ufos[u].explode()
                        end
                    end
                    
                    // Check if any bullets have hit an asteroid
                    @bullets.eachIndex() do |b|
                        if (@asteroids[i].isCollide(@bullets[b], true))
                            // Asteroid hit, so create two new ones in it's place
                            if (@asteroids[i].Size() == 0)
                                addAsteroid("A" + @numRocks + 1, 1, @asteroids[i].X(), @asteroids[i].Y())
                                addAsteroid("A" + @numRocks + 2, 1, @asteroids[i].X(), @asteroids[i].Y())
                            else if (@asteroids[i].Size() == 1)
                                addAsteroid("A" + @numRocks + 1,2, @asteroids[i].X(), @asteroids[i].Y())
                                addAsteroid("A" + @numRocks + 2,2, @asteroids[i].X(), @asteroids[i].Y())
                            end
                            // Update the score based on size of asteroid hit
                            if (@asteroids[i].Size() == 0)
                                @score = @score + $pointsLgeRock
                                $expManager.add(@asteroids[i].X(), @asteroids[i].Y(), 30)
                            else if (@asteroids[i].Size() == 1)
                                @score = @score + $pointsMedRock
                                $expManager.add(@asteroids[i].X(), @asteroids[i].Y(), 20)
                            else if (@asteroids[i].Size() == 2)
                                @score = @score + $pointsSmlRock
                                $expManager.add(@asteroids[i].X(), @asteroids[i].Y(), 10)
                            end

                            // Remove this bullet
                            deleteBullet(@bullets[b])
                            destAsteroid = true
                        end
                    end
                    // Destroy this asteroid if it was hit
                    if (destAsteroid)
                        @asteroids[i].explode()
                    end
                else
                    // Oh dear, player is dead
                    if (@player.isRespawning() == false)
                        
                        // If the player just died, then wait for explosion to finish
                        if (@player.isExplosionFinished())
                            @lives = @lives - 1
                            if (@lives > 0)
                                @player.reset()
                            else
                                // Oh dear, oh dear, oh dear - player has lost all lives!
                                @state = :endGame
                                // Set timer so game over screen stays for a few seconds
                                @endTimer = new Timer(3000)
                            end
                        end
                    end
                end
            end

            // Update the player
            @player.update(delta)
            
            // Update all explosions
            $expManager.update(delta)
            
            // Update the thumping beat
            updateBeat()
        else if (@state == :endGame)        // GAME ENDED
            // Display "GAME OVER" and wait for timer to expire
            // before moving back to the menu
            setAlpha(0.5) do
                $font.drawCenteredText("GAME OVER", 130, 1.0)
            end
            if (@endTimer.isExpired())
                @state = :menu
                @ufos.eachIndex() do |i|
                    @ufos[i].explode()
                end
                init(0)
            end
            return
        end
        
        // Get an extra life for every 10000 points earned
        if ( (@score - @lastScore) > 10000)
            @lastScore = @lastScore + 10000
            @lives = @lives + 1
            @extraLifeSound.play()
        end
        
        // Record the high score in the user session
        if (@score > $highScore)
            $highScore = @score
            if (isEditor() == false)
                $user.saveGame("" + $highScore)
            end
        end
        
        // Has the end of this level been reached?
        if (@numRocks <= 0)
            // Increase the number of asteroids for each level
            @startRocks = @startRocks + 1
            // No more than 16 large rocks at once
            if (@startRocks > 16)
                @startRocks = 16
            end
            init(@startRocks)
            @level = @level + 1
        end
        debug("A: " + @numRocks, 550, 300)
        debug("B: " + @numBullets, 550, 320)
        debug("E: " + $expManager.NumExplosions(), 550, 340)
        debug("U: " + @numUfos, 550, 360)
        debug("M: " + @numMissiles, 550, 380)
    end
    
    // Draw everything
    def render()
        if (@state == :menu)
            @asteroids.eachIndex() do |i|
                @asteroids[i].render()
            end
            $font.drawCenteredText("ASTEROIDS", 100, 2)
            $font.drawCenteredText("DEVELOPED FOR PLAYMYCODE BY ANDY WHITE", 180, 0.5)
            if (@timer.isExpired())
                @flipMenu = !@flipMenu
                @timer = new Timer(5 * 1000)
            end
            
            if (@flipMenu == true)
                $font.drawCenteredText("HIGH SCORE : " + $highscore, 240, 0.8)
            else
                @alf = @alf + @alfv
                if (@alf < 0 || @alf > 1.0)
                    @alfv = -@alfv
                    @alf = @alf + @alfv
                end
                setAlpha(@alf) do
                    $font.drawCenteredText("PRESS [S] TO START", 240, 0.5)
                end
                $font.drawCenteredText("[Z] TO SHOOT", 270, 0.5)
                $font.drawCenteredText("[X OR SPACE] TO HYPERSPACE", 290, 0.5)
            end
            setAlpha(0.5) do
                $font.drawCenteredText("1 COIN 1 PLAY", 330, 1.0)
            end
            setColor(:white)
            setAlpha(0.8) do
                drawImage(@fsLogo, 510, 340, 100, 50, false)
            end
        else if (@state == :game)
            @asteroids.eachIndex() do |i|
                @asteroids[i].render()
            end
            @ufos.eachIndex() do |i|
                @ufos[i].render()
            end
            @player.render()
            @bullets.each() do |bullet|
                bullet.render()
            end 
            renderMissiles()
            $expManager.render()
        end
        
        // Always display the score & high score
        score = padLeadingZeros("" + @score, $maxScoreDigits)
        $font.drawText("SCORE: " + score, 10, 10, 0.5)
        high = padLeadingZeros("" + $highscore, $maxScoreDigits)   
        $font.drawText("HIGHSCORE: " + high, 390, 10, 0.5)
        
        // Draw a ship for each life if in-game
        if (@state == :game)
            setColor(:white)
            @lives.times() do |i|
                translate(15 + (i * 15), 30)
                scale(0.5)
                drawPolygon($ship)
                undoTransform()
                undoTransform()
            end
        end
        
        setAlpha(0.8) do
            $font.drawCenteredText("[C] 1979 ATARI INC.", 380, 0.5)
        end

    end
    
    // Rather naff way of adding zeros to front of the scores
    def padLeadingZeros(val, len)
        if (val.length() >= len)
            return val
        end
        
        j = 0
        l = len - val.length()
        s = new Array(len+1)
        l.times() do
            s[j] = "0"
            j = j + 1
        end
        val.each() do |v|
            s[j] = v
            j = j + 1
        end
        
        return s.join('')
    end
    
    // Add a new asteroid to the game
    def addAsteroid(name, size, x, y)
        if (@asteroids.size() <= 26)
            @asteroids.push(new Asteroid(name, size, x, y))
            @numRocks = @numRocks + 1
        end
    end

    // Remove an asteroid from the game
    def removeAsteroid(asteroid)
        @asteroids.delete(asteroid)
        @numRocks = @numRocks - 1
        // The intensity of the thumps increases with 
        // each asteroid destroyed
        @beatIntensity = @beatIntensity - 20
        if (@beatIntensity < 300)
            @beatIntensity = 300
        end
    end
    
    // Add a new UFO to the game
    def addUFO()
        @ufos.push(new UFO())
        @numUfos = @numUfos + 1
    end
    
    // Remove a UFO from the game
    def removeUFO(ufo)
        @ufos.delete(ufo)
        @numUfos = @numUfos - 1
    end
    
    // Add a new player bullet to the game
    def addBullet(x, y, angle)
        if (@numBullets < $maxBullets)
            @bullets.push(new Bullet(x, y, angle))
            @numBullets = @numBullets + 1
        end
    end
    
    // Delete a player bullet from the game
    def deleteBullet(bullet)
        @bullets.delete(bullet)
        @numBullets = @numBullets - 1
    end
    
    // Update all player bullets
    def updateBullets(delta)
        @bullets.eachIndex() do |i|
            bullet = @bullets[i]
            bullet.update(delta)
        end
    end    
    
    def addMissile(x, y, angle)
        @missiles.push(new Missile(x, y, angle))
        @numMissiles = @numMissiles + 1
    end
    
    def removeMissile(missile)
        @missiles.delete(missile)
        @numMissiles = @numMissiles - 1
    end
    
    def updateMissiles(delta)
        @missiles.eachIndex() do |i|
            missile = @missiles[i]
            missile.update(delta)
        end
    end
    
    def renderMissiles()
        @missiles.eachIndex() do |i|
            missile = @missiles[i]
            missile.render()
        end
    end
    
    def getPlayerLocation()
        return [@player.X(), @player.Y()]
    end
    
    // Play the heartbeat sound
    // The period of the pause inbetween beats is based on the
    // intensity of play (quickens as number of asteroids per
    // level is destroyed)
    def updateBeat()
        currentTime = new Time().toMilliseconds()
        if (currentTime >= @nextBeatTime)
            if (@beat == 0)
                if (@beatSnd1.isPlaying())
                    @beatSnd1.stop()
                end
                @beatSnd1.play()
                @beat = 1
            else
                if (@beatSnd2.isPlaying())
                    @beatSnd2.stop()
                end
                @beatSnd2.play()
                @beat = 0
            end
            @nextBeatTime = currentTime + @beatIntensity
        end
    end
end

// --== Asteroid class ==--
class Asteroid < Polygon
    def new(name, size, x, y)
        // Choose a random rock
        r = rand(2).round()
        if (size == 0)
            rockSpeed = $lgeRockSpeed
            rock = $lgeRocks[r]
        else if (size == 1)
            rockSpeed = $medRockSpeed
            rock = $medRocks[r]
        else
            rockSpeed = $smlRockSpeed
            rock = $smlRocks[r]
        end
        
        super(name, rock, 0, 0)

        if (x == -999 || y == -999)
            // Randomize asteroid location around edge of screen
            s = rand(1).round()
            if (rand(1).round() == 0)
                setX(rand($screenWidth).round())
                if (s == 0)
                    setY($screenHeight)
                else
                    setY(0)
                end
            else
                setY(rand($screenHeight).round())
                if (s == 0)
                    setX($screenWidth)
                else
                    setX(0)
                end
            end
        else    
            setX(x)
            setY(y)
        end
        // Randomize some initial properties
        setAngle(15+rand(0, 360))
        @rotationSpeed  = rand(0,0.2)

        @size           = size
        @rotSpeed       = rand(-0.02, 0.02)
        
        @speed          = rand(0.5, rockSpeed)
        @dirAngle       = Angle()
        @xVelocity      = @dirAngle.sin() * @speed
        @yVelocity      = @dirAngle.cos() * @speed
                
        // Scale the rocks down slightly
        if (size == 0)
            setScale(0.7)
        else
            setScale(0.5)
        end
    end
    read(size)
    
    // This asteroid has been destroyed
    def explode()
        $game.removeAsteroid(this)
    end
    
    def update(delta)
        // Update the rotation angle
        setAngle(Angle() + @rotSpeed * delta)

        // Move the asteroid to the new loation
        setX(X() + @xVelocity * delta)
        setY(Y() - @yVelocity * delta)
        
        // If the asteroid goes out of bounds, then
        // return it on the other side
        if (X() < -Bounds()/2)
            setX($screenWidth + Bounds()/2)
        else if (X() > $screenWidth + Bounds()/2)
            setX(-Bounds()/2)
        end
        if (Y() < -Bounds()/2)
            setY($screenHeight+Bounds()/2)
        else if (Y() > $screenHeight+Bounds()/2)
            setY(-Bounds()/2)
        end
    end
    
    // Draw the asteroid to the canvas
    def render()
        drawPoly()
    end
end


// --== Player class ==-- 
// C_Player
class Player < Polygon    
    def new()
        super("P", $ship, $halfWidth, $halfHeight)
        @state              = :alive
        
        @acceleration       = 0.05
        @maxAcceleration    = 5.0
        @damping            = 0.995
        @xVelocity          = 0.0
        @yVelocity          = 0.0
        
        @thrust             = false
        
        @fireIdx            = 0
        @fireSounds         = new Array(4)
        @fireSounds[0]      = new Sound("fire.mp3").setVolume(0.2)
        @fireSounds[1]      = new Sound("fire.mp3").setVolume(0.2)
        @fireSounds[2]      = new Sound("fire.mp3").setVolume(0.2)
        @fireSounds[3]      = new Sound("fire.mp3").setVolume(0.2)
        
        @expSound           = new Sound("explode1.mp3")

        // Sound played when thrusting
        @thrustSound        = new Sound("thrust.mp3")
        
        // Scale the player down a little
        setScale(0.8)
        
        @flame = new Polygon("F", $flame, $halfWidth, $halfHeight)
        @flame.setScale(1.1)
        
        @explode = [
            new Fragment([ 0, -11, -7,  11]),
            new Fragment([-7,  11, -3,   7]),
            new Fragment([-3,   7,  3,   7]),
            new Fragment([ 3,   7,  7,  11]),
            new Fragment([ 7,  11,  0, -11])
        ]
        @expEnded = false
    end
    
    // Reset the player, but wait for a safe location to spawn first
    def reset()
        @state     = :waitForSpawn
    end
    
    // Reset all player parameters
    def respawn()
        @state     = :alive
        @fireIdx   = 0
        @thrust    = false
        @expEnded  = false
        @xVelocity = 0.0
        @yVelocity = 0.0
        setX($halfWidth)
        setY($halfHeight)
    end
    
    // Is the player dead?
    def isDead()
        if (@state == :alive)
            return false
        else
            return true
        end
    end
    
    // Is the player respawning?
    def isRespawning()
        if (@state == :waitForSpawn)
            return true
        else
            return false
        end
    end
    
    // Has the player's explosion finished rendering?
    // Note: This indicates the player is ready to spawn again
    def isExplosionFinished()
        return @expEnded
    end
    
    // Oopsie!  The player died, so create an explosion
    def explode()
        @expSound.play()
        @state = :explode
        a = 0
        @explode.each() do |p|
            p.reset(X(), Y(), Angle()+a.toRadians())
            a = rand(0, 359)
        end
    end
    
    def update(delta)
        
        // Wait until it's safe to spawn again
        if (@state == :waitForSpawn)
            if ($game.isSafeToSpawn($halfWidth, $halfHeight, Bounds()))
                respawn()
            end
        else
            
            if (@state == :alive)
                @thrust = false
        
                // Check keyboard input
                if ($controls.isKeyDown("up"))          // THRUST
                    if (@thrustSound.isPlaying() == false)
                        @thrustSound.play()
                    end
                    @thrust = true
                end
                if ($controls.isKeyPressed(["x","space"]))    // HYPERJUMP
                    $game.hyperJump()
                end
                
                if ($controls.isKeyPressed("z"))        // FIRE BULLET
                    if ($game.getNumBullets() < $maxBullets) 
                        if (@fireIdx > @fireSounds.length()-1)
                            @fireIdx = 0
                        end
                        @fireSounds[@fireIdx].play()
                        @fireIdx = @fireIdx + 1 
                        $game.addBullet(X(), Y(), Angle())
                    end
                end
                
                if ($controls.isKeyDown("left"))        // ROTATE LEFT
                    setAngle(Angle() - 0.06 * delta)
                    if (Angle() < 0)
                        setAngle($TWO_PI)
                    end
                else if ($controls.isKeyDown("right"))  // ROTATE RIGHT
                    setAngle(Angle() + 0.06 * delta)
                    if (Angle() > $TWO_PI)
                        setAngle(0)
                    end
                end
                
                if (@thrust == true)
                    // Determine velocity based on rotation and acceleration
                    @xVelocity = @xVelocity + Angle().sin() * @acceleration * delta
                    @yVelocity = @yVelocity + Angle().cos() * @acceleration * delta
                    
                    // Ensure velocity stays within range                    
                    velocity = ( (@xVelocity * @xVelocity) + (@yVelocity * @yVelocity) ).sqrt()
                    if (velocity > @maxAcceleration)
                        @xVelocity = @xVelocity / velocity
                        @yVelocity = @yVelocity / velocity
                        
                        @xVelocity = @xVelocity * @maxAcceleration
                        @yVelocity = @yVelocity * @maxAcceleration
                    end
                else
                    // Ship isn't thrusting, so apply damping factor to slow it down
                    @xVelocity = @xVelocity * @damping
                    @yVelocity = @yVelocity * @damping
                end
            end
            
            // Move the ship
            setX(X() + @xVelocity)
            setY(Y() - @yVelocity)
            
            // If the player ship goes out of bounds, then
            // return it on the other side
            if (X() < 0)
                setX($screenWidth)
            else if (X() > $screenWidth)
                setX(0)
            end
            if (Y() < 0)
                setY($screenHeight)
            else if (Y() > $screenHeight)
                setY(0)
            end
            
            if (@state == :alive)
                if (@thrust)
                    @flame.setX(X())
                    @flame.setY(Y())
                    @flame.setAngle(Angle())
                end
            else
                // Update the explosion
                @explode.each() do |p|
                    p.update(@xVelocity, @yVelocity, delta)
                end
                if (@explode[0].PAlpha() <= 0)
                    @expEnded = true
                end
            end
        end
    end
    
    // Draw the player, and also the flame if thrusting
    def render()
        if (@state == :alive)
            drawPoly()
            if (@thrust)
                @flame.drawPoly()
            end
        else
            // Draw the explosion
            @explode.each() do |p|
                p.render()
            end
        end
    end
end

// A Fragment is a single piece of the player's
// explosion. 
class Fragment < Polygon
    def new(points)
        super("FR", points, 0, 0)
        @vx = 0
        @vy = 0
        @angV = 0
        @xVelocity = 0
        @yVelocity = 0
    end
    
    // Reset the explosion parameters
    def reset(x, y, angle)
        setX(x)
        setY(y)
        setAngle(angle)
        setPAlpha(1.0)
        @xVelocity =  angle.sin() * rand(0.2, 1.2)
        @yVelocity = -angle.cos() * rand(0.2, 1.2)
        @angV = rand(0, 0.08)
    end
    
    // Update the explosion
    def update(sxv, syv, delta)
        setX(X() + (sxv + @xVelocity * delta))
        setY((Y() + syv) + @yVelocity * delta)
        setPAlpha(PAlpha() - 0.01)
        setAngle(Angle() + @angV)
    end
    
    // Draw the explosion
    def render()
        drawPoly()
    end
end

// --== Bullet class ==--
// A little bit of overkill for something so small, but using the Polygon
// class keeps this code cleaner and makes collision detection a lot easier
class Bullet < Polygon
    def new(x, y, angle)
        //super("B", [-0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, 0.5], x, y)
        super("B", [-1, -1, 1, -1, 1, 1, -1, 1], x, y)
        
        // Bullet angle is determined based on the angle the player's ship
        // is pointing - tiny bit of trig here :)
        setAngle(angle)
        @xVelocity =  angle.sin() * 7.5
        @yVelocity = -angle.cos() * 7.5
        // Each bullet can only last a short period of time
        @life = new Timer(700)
    end
    
    // Update this bullet
    def update(delta)
        setX(X() + @xVelocity * delta)
        setY(Y() + @yVelocity * delta)
        if (X() < 0)
            setX($screenWidth)
        else if (X() > $screenWidth)
            setX(0)
        end
        if (Y() < 0)
            setY($screenHeight)
        else if (Y() > $screenHeight)
            setY(0)
        end
            
        // Bullets can wrap around the screen limits, so a timer
        // is used so that it will eventually die
        if (@life.isExpired())
            $game.deleteBullet(this)
        end
    end
    
    // Draw the bullet
    def render()
        drawPoly(true)
    end
end

// UFO missile is almost the same as a bullet
class Missile < Polygon
    def new(x, y, angle)
        super("B", [-1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0], x, y)

        setAngle(angle)
        @xVelocity =  -angle.cos() * 3.5
        @yVelocity =  -angle.sin() * 3.5
        @life = new Timer(1000)
    end
    
    // Update this missile
    def update(delta)
        setX(X() + @xVelocity * delta)
        setY(Y() + @yVelocity * delta)
        if (X() < 0)
            setX($screenWidth)
        else if (X() > $screenWidth)
            setX(0)
        end
        if (Y() < 0)
            setY($screenHeight)
        else if (Y() > $screenHeight)
            setY(0)
        end
            
        // Missiles can wrap around the screen limits, so a timer
        // is used so that it will eventually die
        if (@life.isExpired())
            $game.removeMissile(this)
        end
    end
    
    // Draw the missile
    def render()
        drawPoly(true)
    end
end

// There are two ufo's in the game, small and large.  This
// class handles both.
// There are two differences:
// - Large UFO randomly shoots around
// - Small UFO tracks the player and shoots either slightly
//   in front or behind the player
class UFO < Polygon
    def new()
        super("P", $ufo, $halfWidth, $halfHeight)
        setPAlpha(0.8)
        @missileSound  = new Sound("sfire.mp3").setVolume(0.3)
                    
        // Randomly choose size of UFO. The smaller
        // UFO is far more likely to appear on later levels
        if (rand(30).round() <= $game.Level())
            @size = 1
            @annoyingSound = new Sound("ssaucer.mp3")
        else
            @size = 0
            @annoyingSound = new Sound("lsaucer.mp3")
        end
        @annoyingSound.setRepeating(true)
        @annoyingSound.setVolume(0.5)
        @annoyingSound.play()
        
        if (@size == 0)
            // Large UFO
            setScale(1.1)
            @shootTimer = new Timer(rand(500, 1000))
        else
            // Small UFO
            setScale(0.6)
            @shootTimer = new Timer(2000 + (rand(500, 2200)/(1+$game.Level())))
        end
        
        // Time until next movement
        @moveTime  = 1800+ rand(100, 1000)/(1+$game.Level())
        @moveTimer = new Timer(@moveTime)
                
        // Choose initial random speed
        @xVelocity      = rand(1, 2.5)
        @yVelocity      = 0
        
        // Choose random location (on edges of screen)
        if (rand(1).round() == 0)
            setX(0-Bounds())
        else
            setX($screenWidth+Bounds())
            @xVelocity = -@xVelocity
        end

        setY(rand($screenHeight))
    end
    read(size)
    
    // UFO died - shot by player or hit an asteroid, so
    // kill it
    def explode()
        @annoyingSound.stop()
        $game.removeUFO(this)
    end
    
    // Update the UFO
    def update(delta)
        setX(X() + @xVelocity * delta)
        setY(Y() + @yVelocity * delta)
        if (X() < (0-Bounds()))
            setX($screenWidth + Bounds())
        else if (X() > ($screenWidth + Bounds()))
            setX(0 - Bounds())
        end
        if (Y() < (0-Bounds()))
            setY($screenHeight + Bounds())
        else if (Y() > ($screenHeight + Bounds()))
            setY(0 - Bounds())
        end
        if (@size == 0)
            updateLargeUFO(delta)
        else
            updateSmallUFO(delta)
        end
        
    end
    
    def updateLargeUFO(delta)
        if (@moveTimer.isExpired())
            @moveTimer = new Timer(@moveTime)
            @yVelocity = rand(-1.0, 1.0)
        end
        // Large UFO shoots in random directions and doesn't
        // track the player
        if (@shootTimer.isExpired())
            @shootTimer = new Timer(rand(500, 1000))
            $game.addMissile(X(), Y(), rand(360))
            @missileSound.play()
        end
    end
    
    def updateSmallUFO(delta)
        if (@moveTimer.isExpired())
            @moveTimer = new Timer(@moveTime)
            @yVelocity = rand(-2.5, 2.5)
        end
        // Small UFO tracks the player and fires just in front
        // or just behind the player
        if (@shootTimer.isExpired())
            // Reset the shoot timer
            @shootTimer = new Timer(1000 + (rand(500, 2200)/(1+$game.Level())))

            // Get the current player location
            pXY   = $game.getPlayerLocation()
            // Calculate the angle for the missile
            theta = calculateAngle(X(), Y(), pXY[0] + rand(-20, 20), pXY[1])
            // Add a new missile
            $game.addMissile(X(), Y(), theta)
            @missileSound.play()
        end
    end

    def calculateAngle(x1, y1, x2, y2)
        X = x1 - x2
        Y = y1 - y2
        A = Y.atan2(X)
        return A
    end
    
    // Draw the UFO
    def render()
        if ($DEBUG_MODE)
            setColor(:green) do
                pXY   = $game.getPlayerLocation()
                theta = calculateAngle(X(), Y(), pXY[0], pXY[1])
                drawLine(X(), Y(), pXY[0], pXY[1])
            end
        end
        drawPoly()
    end
end

// Polygon class.  This is used for every object onscreen.
// However, to make collision detection easier, the polygon is
// actually rendered to a separate image so that we can perform
// per-pixel detection, giving a very accurate hit detection
// method.
class Polygon
    // Constructor for this polygon
    // You simply pass in the array containing the x/y pairs and the x/y canvas location
    // You can update/change the default values for the other fields by using accessors
    def new(name, points, x, y)
        @name = name
        @points = points
        @x      = x
        @y      = y
        @colour = :white
        @pAlpha = 1.0
        @scale  = 1
        @angle  = 0
        @img    = new Image(128, 128)
        
        // Calculate the bounds of this polygon
        i = 0
        xMin = 999
        xMax = -999
        yMin = 999
        yMax = -999
        while (i < points.size())
            xx = points[i]
            yy = points[i+1]
            if (xx < xMin)
                xMin = xx
            else if (xx > xMax)
                xMax = xx
            end
            if (yy < yMin)
                yMin = yy
            else if (yy > yMax)
                yMax = yy
            end
            i  = i + 2
        end
        @bounds = (xMax - xMin).max( (yMax - yMin) ) * 1.25
    end
    // Make some of the attributes visible to other classes so they
    // can be easily updated/retrieved via accessors
    set(x, y, angle, scale, colour, pAlpha)
    read(x, y, angle, scale, pAlpha, img, points, bounds)
    
    // Does the bounding box of this polygon overlap that of another
    def isBoundsOverlap(x, y, bounds)
        return isRectOverlap(X(), Y(), @bounds+bounds, @bounds+bounds, x, y, bounds, bounds, true)
    end
    
    // This is an alternative helper method that allows us to pass
    // in a polygon rather than the elements needed from it
    def isCollide(poly, centered)
        return isCollide(poly.Img(), poly.X(), poly.Y(), centered)
    end
    
    def isCollide(img, ix, iy, c)
        // To keep things simple, we're making use of PlayMyCode's collision
        // detection.  Specifically, the .isPixelOverlap() function.
        if (@img.isPixelOverlap(@x, @y, img, ix, iy, c))
            // Yes!  The two polygons overlap, meaning there's a collision
            return true
        end
        
        return false
    end
    
    def drawPoly()
        drawPoly(false)
    end
    
    def drawPoly(filled)
        // Clear the image we're going to draw to - this ensures that the 
        // background of this image is transparent
        @img.clear()
        
        // Locate the polygon in the center of the render image
        @img.translate(@img.getWidth()/2, @img.getHeight()/2)
        
        // Rotate it 
        @img.rotate(@angle)
        
        // Finally, scale it
        @img.scale(@scale)
        
        if (filled)
            @img.setColor(@colour)
            @img.setAlpha(@pAlpha)
            @img.fillPolygon(@points)
        else
            // We fill the polygon with a very very faint colour so that
            // the collision detection picks up when the player ship is
            // also "inside" an asteroid. Under normal circumstances, this
            // shouldn't happen.  However, if the framerate stutters
            // the player could skip ahead and land inside an asteroid.
            // Also, need to think about where the player ship is when it
            // spawns - not too clever if it spawns inside an asteroid :)
            @img.setColor(1, 0, 0, 0.01)
            @img.fillPolygon(@points)
                    
            // Now we've filled it, draw the polygon outline
            @img.setColor(@colour)
            @img.setAlpha(@pAlpha)
            @img.drawPolygon(@points)
        end
        
        // Clear the translate/rotate/scale transformations
        @img.clearTransforms()
        
        // Finally, draw the new image of our polygon to the main canvas
        setColor(:white)
        setAlpha(@pAlpha) do
            drawImage(@img, @x, @y, true)
            drawImage(@img, @x, @y, true)
        end
        if ($DEBUG_MODE)
            setColor(:red) do
                drawRect(@x, @y, @bounds, @bounds, true)
            end
        end
         
    end
end

// To avoid memory/garbage collection issues, we re-use
// explosions rather than creating new ones.  This manager
// class handles all of the explosions in the game. 
class ExplosionManager
    def new(maxExplosions)
        @maxExplosions = maxExplosions
        @explosions = []
    
        // Create all of the initial explosions
        // These won't be seen until they're reset
        @maxExplosions.times() do |i|
            @explosions.push(new Explosion(i, 0, 0, 0))
        end
        @explosionIndex = 0
        @numExplosions = 0
    end
    read(numExplosions)
    
    // Fire an explosion at the location given
    def add(x, y, size)
        @explosionIndex = @explosionIndex + 1
        if (@explosionIndex >= @maxExplosions)
            @explosionIndex = 0
        end
        @explosions[@explosionIndex].reset(x, y, size)
        @numExplosions = @numExplosions + 1
    end
    
    // Remove an explosion from the active queue
    def remove(id)
        @numExplosions = @numExplosions - 1
    end
    
    // Update all explosions
    def update(delta)
        @explosions.each() do |exp|
            exp.update(delta)
        end
    end
    
    // Draw all explosions
    def render()
        @explosions.each() do |exp|
            exp.render()
        end
    end
end

// Class which creates a pretty explosion effect. This is simply
// a set of particles thrown out from a center at random velocities
class Explosion
    def new(id, x, y, size)
        @x         = x
        @y         = y
        @size      = size
        @id        = id
        @points    = []
        @numPoints = 0
        @alpha     = 1.0
        @state     = :expired
                
        @expSound1 = new Sound("explode1.mp3")
        @expSound2 = new Sound("explode2.mp3")
        @expSound3 = new Sound("explode3.mp3")
    end
    
    // Reset this explosion.  This will cause it to become
    // active and will update and render to the screen
    def reset(x, y, size)
        @x = x
        @y = y
        @size = size
        @state = :active
        @numPoints.times() do |i|
            @points.deleteAt(0)
        end
        @numPoints = @size
        @numPoints.times() do |i|
            p = new Point(x, y)
            @points.push(p)
        end
        @alpha = 1.0
        if (@size <= 10)
            @expSound3.play()
        else if (@size <= 20)
            @expSound2.play()
        else if (@size <= 30)
            @expSound1.play()
        end
    end
    
    def update(delta)
        // Don't update if already expired
        if (@state == :active)
            // Gradually reduce the Alpha so the
            // explosion slowly fades away
            @alpha = @alpha - 0.008
            if (@alpha <= 0)
                // Explosion is finished when it can't be seen
                // any more (alpha <= 0)
                @state = :expired
                // Inform the explosion manager that this explosion 
                // has expired
                $expManager.remove(@id)
            else
                // Update each point used for this explosions
                @points.each() do |p|
                    p.update(delta)
                end
            end
        end
    end
    
    // Draw each of the points for this explosion
    def render()
        // Only draw if the explosion is still active
        if (@state == :active)
            setColor(255, 255, 255, @alpha) do
                @points.each() do |p|
                    drawPixel(p.X(), p.Y())
                end
            end
        end
    end
end

// A two-dimensional (2D) point
// This is used for each point in an Explosion
class Point
    def new(x, y)
        // Screen coordinates
        @x = x
        @y = y
        
        @vx = 0.1 + rand(-0.5, 0.5)
        @vy = 0.1 + rand(-0.5, 0.5)
    end
    set(x, y)
    read(x, y, vx, vy)
    
    // Update the X/Y positions
    def update(delta)
        @x = @x + @vx * delta
        @y = @y + @vy * delta
    end
end

// Bitmap Font class
// Takes the font images from a bitmap and provides helper methods
// to draw text to the screen using these stored images
class Font
    def new()
        // Configure this font.  Note: the ~ character signifies a character
        // in the bitmap that is of no interest.
        @font   = new Image("font.png")
        @chars  = " !~#~~~~[]*+,-./0123456789:;<=>?~ABCDEFGHIJKLMNOPQRSTUVWXYZ~\~^_"
        @width  = 20
        @height = 20
    end
    
    // Draw the required character from the font at the specified coordinates
    def drawChar(char, x, y)
        idx = @chars.indexOf(char)
        col = (idx % 12)
        row = (idx / 12).floor()
        fx = col * @width
        fy = row * @height
        drawImage(@font, fx, fy, @width, @height, x, y, false)
        drawImage(@font, fx, fy, @width, @height, x, y, false) // Twice to brighten it up!
    end
    
    // Draw text centered on the x-axis using this font at the given y coordinate.  Scale
    // is applied once the origin has been transalated to the required coordinates
    def drawCenteredText(string, y, scale)
        x = $halfWidth - ((string.length() * @width) * scale)/2
        drawText(string, x, y, scale)
    end
    
    // Draw text using this font at the given (x,y) coords. Scale is applied
    // once the origin has been translated to the required coordinates
    def drawText(string, x, y, scale)
        // Move origin to required x/y
        translate(x, y)
        
        // Scale the font
        scale(scale)
        
        // Draw each character in turn
        i = 0
        string.each() do |c|
            drawChar(c, (i * @width), 0)
            i = i + 1
        end
        
        // Clear the scale/translate transformations so that they
        // don't affect further drawing calls
        clearTransforms()
    end
end

// Helper method to show debug text on the screen
def debug(string, x, y)
    if ($DEBUG_MODE)
        setColor(:white) do
            fillText(string, x, y, false)
        end
    end
end

ERRORS

YOUR BROWSER DOES NOT SUPPORT HTML5!

Please use one of these instead

Our games cannot run in your browser