//
//
//  Asteroids Game
//
//
//  Copyright (c) 2007-2008 Knute Johnson, all rights reserved.
//
//
//  Version     Date        Modification
//  ----------------------------------------------------------------------------
//  01.00       02 Nov 07   cleaned up and ready to come out
//  01.01       07 Nov 07   housekeeping
//  01.02       02 Dec 07   better rock shape and smallRock is now a smaller
//                           version of largeRock
//  01.03       05 Dec 07   cleanup a couple of methods
//  01.04       01 Feb 08   run through findbugs and remove unused field from
//                           Bullet
//  01.05       06 May 08   housekeeping
//  01.06       30 Oct 08   increase bullet velocity for better play
//
//

package com.knutejohnson.games.asteroids;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.io.*;
import java.net.*;
import java.util.*;
import javax.sound.sampled.*;
import javax.swing.*;

public class Asteroids extends JPanel implements ActionListener {
    final Path2D.Double ship = new Path2D.Double();
    final Path2D.Double largeRock = new Path2D.Double();
    final Path2D.Double smallRock;
    final ArrayList<Bullet> bullets = new ArrayList<Bullet>();
    final ArrayList<Rock> rocks = new ArrayList<Rock>();
    final Random random = new Random(new Date().getTime());
    final Font font = new Font("Monospaced",Font.BOLD,18);
    final javax.swing.Timer displayTimer;
    final javax.swing.Timer rockTimer;

    double x,y,theta,deltaX,deltaY;
    int points,ammo = 1000, fuel = 10000;
    int shipDestroyed;
    boolean leftKeyIsPressed,rightKeyIsPressed;
    boolean upKeyIsPressed,spaceKeyIsPressed;
    boolean firstDisplay = true;

    public Asteroids() {
        addKeyListener(new KeyAdapter() {
            public void keyPressed(KeyEvent ke) {
                int keyCode = ke.getKeyCode();
                if (keyCode == KeyEvent.VK_SPACE) {
                    if (!spaceKeyIsPressed && shipDestroyed == 0 && ammo > 0) {
                        spaceKeyIsPressed = true;
                        Bullet bullet = new Bullet(x,y,theta,deltaX,deltaY);
                        bullets.add(bullet);
                        --ammo;
                        playSound("sounds/bullet.wav");
                    }
                } else if (keyCode == KeyEvent.VK_LEFT) {
                    leftKeyIsPressed = true;
                } else if (keyCode == KeyEvent.VK_RIGHT) {
                    rightKeyIsPressed = true;
                } else if (keyCode == KeyEvent.VK_UP) {
                    upKeyIsPressed = true;
                } else if (keyCode == KeyEvent.VK_ENTER) {
                    x = getWidth() / 2.0;
                    y = getHeight() / 2.0;
                    deltaX = 0;
                    deltaY = 0;
                    theta = 0;
                    points = 0;
                    fuel = 10000;
                    ammo = 1000;
                    shipDestroyed = 0;
                }
            }
            public void keyReleased(KeyEvent ke) {
                int keyCode = ke.getKeyCode();
                if (keyCode == KeyEvent.VK_SPACE)
                    spaceKeyIsPressed = false;
                else if (keyCode == KeyEvent.VK_LEFT)
                    leftKeyIsPressed = false;
                else if (keyCode == KeyEvent.VK_RIGHT)
                    rightKeyIsPressed = false;
                else if (keyCode == KeyEvent.VK_UP)
                    upKeyIsPressed = false;
            }
        });

        addMouseListener(new MouseAdapter() {
            public void mousePressed(MouseEvent me) {
                x = getWidth() / 2.0;
                y = getHeight() / 2.0;
                deltaX = 0;
                deltaY = 0;
                theta = 0;
                points = 0;
                fuel = 10000;
                ammo = 1000;
                shipDestroyed = 0;

                requestFocusInWindow();
            }
        });

        // create ship outline
        ship.moveTo(0,-30);
        ship.lineTo(-15,20);
        ship.lineTo(0,5);
        ship.lineTo(15,20);
        ship.closePath();

        // create rock outlines
        largeRock.moveTo(-32,8);
        largeRock.lineTo(-36,-12);
        largeRock.lineTo(-26,-24);
        largeRock.lineTo(-18,-34);
        largeRock.lineTo(-10,-36);
        largeRock.lineTo(0,-40);
        largeRock.lineTo(8,-38);
        largeRock.lineTo(24,-28);
        largeRock.lineTo(34,-26);
        largeRock.lineTo(38,-12);
        largeRock.lineTo(38,14);
        largeRock.lineTo(26,22);
        largeRock.lineTo(12,38);
        largeRock.lineTo(-8,40);
        largeRock.lineTo(-16,32);
        largeRock.lineTo(-26,28);
        largeRock.lineTo(-34,18);
        largeRock.closePath();

        smallRock = new Path2D.Double(largeRock,
         AffineTransform.getScaleInstance(0.25,0.25));

        /* test rock
        largeRock.moveTo(-25,-25);
        largeRock.lineTo(25,-25);
        largeRock.lineTo(25,25);
        largeRock.lineTo(-25,25);
        largeRock.closePath();
        */

        // create display timer
        displayTimer = new javax.swing.Timer(15,this);
        displayTimer.setInitialDelay(250);
        displayTimer.setActionCommand("Display");

        // create new rock timer
        rockTimer = new javax.swing.Timer(2500,this);
        rockTimer.setInitialDelay(250);
        rockTimer.setActionCommand("Rock");
    }

    public void start() {
        displayTimer.start();
        rockTimer.start();
    }

    public void stop() {
        displayTimer.stop();
        rockTimer.stop();
    }

    void playSound(final String fname) {
        Runnable r = new Runnable() {
            public void run() {
                try {
                    URL url = getClass().getResource(fname);
                    AudioInputStream ais = AudioSystem.getAudioInputStream(url);
                    AudioFormat af = ais.getFormat();
                    SourceDataLine line = AudioSystem.getSourceDataLine(af);
                    line.open(af);
                    line.start();
                    byte[] buf = new byte[1024];
                    int bytesRead;
                    while ((bytesRead = ais.read(buf)) != -1)
                        line.write(buf,0,bytesRead);
                    line.drain();
                    line.stop();
                    line.close();
                } catch (Exception e) {
                    System.out.println(e);
                }
            }
        };
        new Thread(r).start();
    }

    public void actionPerformed(ActionEvent ae) {
        String ac = ae.getActionCommand();

        if (ac.equals("Display")) {
            if (firstDisplay) {
                firstDisplay = false;
                x = getWidth() / 2.0;
                y = getHeight() / 2.0;
                requestFocusInWindow();
            }

            // turn or accelerate the ship
            if (leftKeyIsPressed && !rightKeyIsPressed && fuel > 0 &&
             shipDestroyed == 0) {
                theta -= 0.035;
                --fuel;
            } else if (rightKeyIsPressed && !leftKeyIsPressed && fuel > 0 &&
             shipDestroyed == 0) {
                theta += 0.035;
                --fuel;
            }

            if (upKeyIsPressed && fuel > 0 && shipDestroyed == 0) {
                deltaX += Math.sin(theta) * 0.01;
                deltaY += Math.cos(theta + Math.PI) * 0.01;
                fuel -= 10;
            }

            // calculate new ship's position
            x += deltaX;
            y += deltaY;
            if (x <= 0 || x >= getWidth())
                deltaX = -deltaX;
            if (y <= 0 || y >= getHeight())
                deltaY = -deltaY;
            /*
            if (x < 0)
                x = getWidth();
            if (x > getWidth())
                x = 0;
            if (y < 0)
                y = getHeight();
            if (y > getHeight())
                y = 0;
            */

            // move the bullets
            ListIterator<Bullet> bIter = bullets.listIterator();
            while (bIter.hasNext()) {
                Bullet bullet = bIter.next();
                bullet.x += bullet.deltaX;
                bullet.y += bullet.deltaY;
                // if bullet leaves screen, destroy it
                if (bullet.x < 0 || bullet.x > getWidth() ||
                 bullet.y < 0 || bullet.y > getHeight())
                    bIter.remove();
            }

            // move the rocks
            ListIterator<Rock> rIter = rocks.listIterator();
            while (rIter.hasNext()) {
                Rock rock = rIter.next();
                rock.x += rock.deltaX;
                rock.y += rock.deltaY;
                rock.theta += rock.deltaTheta;
                int rockSize = rock.large ? 40 : 10;
                // if rock is completely off the screen, destroy it
                if (rock.x < -rockSize || rock.x > getWidth() + rockSize ||
                 rock.y < -rockSize || rock.y > getHeight() + rockSize)
                    rIter.remove();
                /*
                if (rock.x < 0)
                    rock.x = getWidth();
                if (rock.x > getWidth())
                    rock.x = 0;
                if (rock.y < 0)
                    rock.y = getHeight();
                if (rock.y > getHeight())
                    rock.y = 0;
                */
            }

            // check for rock hits on the ship
            AffineTransform at = AffineTransform.getRotateInstance(theta);
            rIter = rocks.listIterator();
            loop: while (rIter.hasNext()) {
                double[] coords = new double[6];
                Rock rock = rIter.next();
                AffineTransform at2 =
                 AffineTransform.getRotateInstance(rock.theta);
                PathIterator pi = ship.getPathIterator(at);
                while (!pi.isDone()) {
                    pi.currentSegment(coords);
                    Shape shape;
                    if (rock.large)
                        shape = largeRock.createTransformedShape(at2);
                    else
                        shape = smallRock.createTransformedShape(at2);
                    if (shape.contains(rock.x-x-coords[0],rock.y-y-coords[1])) {
                        if (shipDestroyed == 0) {
                            shipDestroyed = 1;
                            playSound("sounds/explode.wav");
                        }
                        break loop;
                    }
                    pi.next();
                }
            }

            // check for bullet hits
            rIter = rocks.listIterator();
            while (rIter.hasNext()) {
                Rock rock = rIter.next();
                at = AffineTransform.getRotateInstance(rock.theta);
                bIter = bullets.listIterator();
                while (bIter.hasNext()) {
                    Bullet bullet = bIter.next();
                    Shape shape;
                    if (rock.large) {
                        shape = largeRock.createTransformedShape(at);
                        if (shape.contains(rock.x-bullet.x,rock.y-bullet.y)) {
                            playSound("sounds/implosion2.wav");
                            // remove bullet and large rock
                            bIter.remove();
                            rIter.remove();
                            points += 10;

                            // create small rocks
                            for (int i=0; i<5; i++) {
                                // offset is a random angle to rotate the new
                                //  small rocks
                                double offset = random.nextDouble() * 1.25644;
                                // deltaDelta is the change from the large
                                //  rock's delta so the little rocks will move
                                //  away from the big rock's location at
                                //  varying angles
                                double deltaDeltaX =
                                 Math.sin(rock.theta+i*1.25644+offset) * 0.2;
                                double deltaDeltaY =
                                 Math.cos((rock.theta + i * 1.25644 + offset) +
                                 Math.PI) * 0.2;
                                Rock r = new Rock(
                                 rock.x,rock.y,rock.theta,
                                 rock.deltaX + deltaDeltaX,
                                 rock.deltaY + deltaDeltaY,
                                 rock.deltaTheta,false);
                                rIter.add(r);
                            }
                            break;
                        }
                    } else {
                        shape = smallRock.createTransformedShape(at);
                        if (shape.contains(rock.x-bullet.x,rock.y-bullet.y)) {
                            playSound("sounds/implosion2.wav");
                            bIter.remove();
                            rIter.remove();
                            points += 25;
                            break;
                        }
                    }
                }
            }
            paintImmediately(0,0,getWidth(),getHeight());
        // make new rocks
        } else if (ac.equals("Rock")) {
            Rock rock = new Rock(
             random.nextInt(getWidth()),
             random.nextInt(getHeight()),
             random.nextInt(62822) / 10000.0,
             random.nextDouble() * (random.nextBoolean() ? 1.0 : -1.0),
             random.nextDouble() * (random.nextBoolean() ? 1.0 : -1.0),
             random.nextDouble() / (random.nextBoolean() ? 200.0 : -200.0),
             true);
            rocks.add(rock);
        }
    }

    public void paintComponent(Graphics g2D) {
        Graphics2D g = (Graphics2D)g2D;
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
         RenderingHints.VALUE_ANTIALIAS_ON);
        g.setColor(Color.BLACK);
        g.fillRect(0,0,getWidth(),getHeight());

        g.setColor(Color.WHITE);
        g.setFont(font);
        g.drawString(String.format(
         "AMMO:%5d  FUEL:%7d  POINTS:%7d",ammo,fuel,points),10,20);

        // draw the rocks
        for (Rock rock : rocks) {
            AffineTransform at = g.getTransform();
            g.translate(rock.x,rock.y);
            g.rotate(rock.theta);
            if (rock.large)
                g.draw(largeRock);
            else
                g.draw(smallRock);
            g.setTransform(at);
        }

        // draw the bullets
        for (Bullet b : bullets)
            g.fillRect((int)b.x-1,(int)b.y-1,3,3);

        // draw the ship
        if (shipDestroyed < 100) {
            g.translate(x,y);
            g.rotate(theta,0,0);
            if ((shipDestroyed & 1) == 0)
                g.draw(ship);
            else
                g.fill(ship);
            if (shipDestroyed > 0)
                ++shipDestroyed;
        } else
            g.drawString("GAME OVER",getWidth()/2-40,getHeight()/2);
    }

    static class Bullet {
        double x,y,deltaX,deltaY;

        public Bullet(double x, double y, double theta, double shipDeltaX,
         double shipDeltaY) {
            this.x = x;
            this.y = y;
            deltaX = shipDeltaX + Math.sin(theta) * 4.0;  // 3.3
            deltaY = shipDeltaY + Math.cos(theta + Math.PI) * 4.0;
        }
    }

    static class Rock {
        double x,y,theta,deltaX,deltaY,deltaTheta;
        boolean large;

        public Rock(double x, double y, double theta,
         double deltaX, double deltaY, double deltaTheta, boolean large) {
            this.x = x;
            this.y = y;
            this.theta = theta;
            this.deltaX = deltaX;
            this.deltaY = deltaY;
            this.deltaTheta = deltaTheta;
            this.large = large;
        }
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                JFrame f = new JFrame("Asteroids");
                f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                Asteroids asteroids = new Asteroids();
                asteroids.setPreferredSize(new Dimension(800,600));
                asteroids.start();
                f.add(asteroids);
                f.pack();
                f.setVisible(true);
            }
        });
    }
}