/* MissileCommando.java - based on the arcade game Missile Command. */

/* 
 * Copyright (C) 1996 Mark Boyns <boyns@sdsu.edu>
 *
 * Missile Commando II
 * <URL:http://www.sdsu.edu/~boyns/java/mcii/>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

import java.applet.Applet;
import java.applet.AudioClip;
import java.util.*;
import java.awt.*;

public
class MissileCommando extends java.applet.Applet implements Runnable
{
    final String version = "MCII 1.1";
    final int worldWidth = 500;
    final int worldHeight = 300;
    final int scoreHeight = 25;
    final int controlsHeight = 50;
    final int screenWidth = worldWidth;
    final int screenHeight = worldHeight + scoreHeight + controlsHeight;
    
    final int baseWidth = 64;
    final int baseHeight = 32;
    final int baseGap = 30;
    final int maxBases = 5;

    final int missilePoints = 75;
    final int mrvPoints = 150;
    final int bombPoints = 0;
    final int extraShotPoints = 10;
    final int extraBasePoints = 50;
    final int rebuildBasePoints = 10000;

    AudioClip missileSound = null;
    AudioClip missileExplosionSound = null;
    AudioClip mrvExplosionSound = null;
    AudioClip baseExplosionSound = null;
    AudioClip bombSound = null;

    boolean playing = false;
    boolean readyToPlay = false;
    boolean clearScreen = false;
    boolean paused = false;
    boolean soundEnabled = true;
    
    Thread thread = null;
    GameObjectMover mover = null;

    Controls controls;

    Vector bases;
    Vector missiles;
    Vector shots;
    Vector mrvs;
    Vector explosions;
    Vector bombs;
    Vector bombDebris;

    int score = 0;
    int extraBaseScore = 0;
    int level = 0;
    
    int shotCount = 0;
    int missileCount = 0;
    int missileSpeed = 0;
    int missileDelay = 0;
    int mrvSpeed = 0;
    int bombSpeed = 0;

    Color skyColor = new Color (148, 198, 231);
    Graphics world = null;

    MediaTracker tracker;
    Image baseImage;
    Image mrvImage;
    Image bombImage;

    Font font;
    FontMetrics fontMetrics;

    String screenMessage;

    public
    void init ()
    {
	int i, x;

	font = new Font ("TimesRoman", Font.BOLD, 20);
	fontMetrics = getFontMetrics (font);
	setFont (font);

	tracker = new MediaTracker (this);
	baseImage = getImage (getDocumentBase (), "images/base.gif");
	tracker.addImage (baseImage, 1);
	mrvImage = getImage (getDocumentBase (), "images/mrv.gif");
	tracker.addImage (mrvImage, 1);
	bombImage = getImage (getDocumentBase (), "images/bomb.gif");
	tracker.addImage (bombImage, 1);

	/* Load all the sounds. */
	missileSound = getAudioClip (getDocumentBase (), "sounds/Rocket.au");
	missileExplosionSound = getAudioClip (getDocumentBase (), "sounds/Oomph.au");
	mrvExplosionSound = getAudioClip (getDocumentBase (), "sounds/Oomph.au");
	baseExplosionSound = getAudioClip (getDocumentBase (), "sounds/Explosion-2.au");
	bombSound = getAudioClip (getDocumentBase (), "sounds/ship_alarm.au");

	setBackground (Color.white);

	setLayout (new BorderLayout ());
	controls = new Controls (this);
	add ("South", controls);

	resize (screenWidth, screenHeight);

	/* Start the game! */
	thread = new Thread (this);
	thread.start ();
    }

    public
    void start ()
    {
	if (thread == null)
	{
	    thread = new Thread (this);
	    thread.start ();
	}
    }

    public
    void stop ()
    {
	if (thread != null && thread.isAlive ())
	{
	    thread.stop ();
	    thread = null;
	}
	if (mover != null && mover.isAlive ())
	{
	    mover.stop ();
	    mover = null;
	}
    }
    
    public
    boolean mouseDown (Event e, int x, int y)
    {
	if (playing)
	{
	    createShot (x, y - scoreHeight);
	}
	return true;
    }

    void startGame ()
    {
	readyToPlay = true;
    }

    void quitGame ()
    {
	playing = false;
    }
    
    void resetScore ()
    {
	clearScreen = true;
	
	score = 0;
	extraBaseScore = 0;
	level = 0;

	message (null);
    }
    
    void resetGame ()
    {
	clearScreen = true;
	
	missiles = new Vector (100);
	shots = new Vector (100);
	mrvs = new Vector (100);
	explosions = new Vector (100);
	bombs = new Vector (100);
	bombDebris = new Vector (100);

	//controls.playButton.setLabel ("Start");
	//controls.suspendButton.setLabel ("Suspend");
	//controls.soundButton.setLabel ("Sound Off");
	    
	createBases ();
	message (null);
    }

    void suspendGame ()
    {
	if (thread != null && playing)
	{
	    thread.suspend ();
	    if (mover != null && mover.isAlive ())
	    {
		mover.suspend ();
	    }
	    paused = true;
	    clearScreen = true;
	    repaint ();
	}
    }

    void resumeGame ()
    {
	if (thread != null && playing)
	{
	    thread.resume ();
	    if (mover != null && mover.isAlive ())
	    {
		mover.resume ();
	    }
	    paused = false;
	    clearScreen = true;
	}
    }

    void enableSound ()
    {
	soundEnabled = true;
    }
    
    void disableSound ()
    {
	soundEnabled = false;
    }
    
    public
    void run ()
    {
	showStatus ("Loading images...");
 	tracker.checkAll (true);
 	try
 	{
 	    tracker.waitForAll ();
 	}
 	catch (Exception e)
 	{
 	    return;
 	}
	showStatus ("");

	resetGame ();
	resetScore ();

	for (;;)
	{
	    if (!playing && readyToPlay)
	    {
		resetGame ();
		resetScore ();
		playing = true;
		readyToPlay = false;
	    }

	    if (playing)
	    {
		level++;
	    }
	    else if (level == 0)
	    {
		level = 1;
	    }
	    
	    /* Set the delay between missiles. */
	    if (playing)
	    {
		missileDelay = 2000 - ((level-1)*200);
		if (missileDelay < 500)
		{
		    missileDelay = 500;
		}
	    }
	    else
	    {
		missileDelay = 500;
	    }

	    /* Set the missile speed. */
	    missileSpeed = 5;

	    /* Set the MRV speed. */
	    mrvSpeed = 6;

	    /* Set the bomb speed. */
	    bombSpeed = 4;

	    /* Set the number of missiles to be fired. */
	    missileCount = 5 + ((level-1) * 5);

	    /* Set the maximum number of shots. */
	    shotCount = missileCount * 2; // + missileCount/2;

	    /* Display the new level message. */
	    message ("Level " + level);
	    try
	    {
		Thread.sleep (3000);
	    }
	    catch (Exception e)
	    {
	    }
	    message (null);

	    /* Start the object mover thread to move all the
	       game objects. */
	    mover = new GameObjectMover (100, this);
	    mover.start ();

	    while (missileCount > 0)
	    {
		/* Fire a missile. */
		int speed = missileSpeed;
		if (level > 1 && Math.random () > 0.75)
		{
		    speed += (int) (Math.random () * missileSpeed);
		}
		createMissile (speed);
		missileCount--;

		/* Maybe create a mrv. */
		if (mrvs.size () < level)
		{
		    double chance = Math.random ();
		    if (chance > 0.70)
		    {
			createMRV (mrvSpeed);
		    }
		}

		/* Maybe drop a bomb. */
		if (level > 1 && bombs.size () < 2)
		{
		    double chance = Math.random ();
		    if (chance > 0.85)
		    {
			createBomb (bombSpeed);
		    }
		}

		if (!playing)
		{
		    starWars ();
		}

		if (!playing && readyToPlay)
		{
		    break;
		}
		
		try
		{
		    Thread.sleep (missileDelay);
		}
		catch (Exception e)
		{
		}
	    }

	    if (!playing && readyToPlay)
	    {
		mover.stop ();
		continue;
	    }
	    
	    /* Wait for everything to disappear. */
	    while (missiles.size () > 0
		   || mrvs.size () > 0
		   || explosions.size () > 0
		   || bombs.size () > 0
		   || bombDebris.size () > 0
		   || shots.size () > 0)
	    {
		if (!playing && readyToPlay)
		{
		    break;
		}
		if (!playing && (missiles.size () > 0 || mrvs.size () > 0 || bombs.size () > 0))
		{
		    starWars ();
		}
		try
		{
		    Thread.sleep (500);
		}
		catch (Exception e)
		{
		}
	    }

	    /* Stop the object mover thread. */
	    mover.stop ();

	    /* Calculate bonus points. */
	    int bonus = shotCount * extraShotPoints;
	    bonus += countAliveBases () * extraBasePoints;
	    incrementScore (bonus);

	    if (playing)
	    {
		/* Display the bonus points message. */
		message ("Bonus: " + bonus);
		try
		{
		    Thread.sleep (2000);
		}
		catch (Exception e)
		{
		}
		message (null);
	    }

	    /* Possibly rebuild any destroyed bases. */
	    rebuildBases ();

	    /* The game is over when no bases are left. */
	    if (countAliveBases () == 0)
	    {
		playing = false;

		/* Destroy the world. */
		mover = new GameObjectMover (400, this);
		mover.start ();
		destroyWorld ();
		while (explosions.size () > 0)
		{
		    try
		    {
			Thread.sleep (500);
		    }
		    catch (Exception e)
		    {
		    }
		}
		mover.stop ();

		resetGame ();
	    }
	}
    }

    void starWarsFireAt (Vector v)
    {
	int x, y;
	Enumeration objs = v.elements ();
	while (objs.hasMoreElements ())
	{
	    GameObject o = (GameObject) objs.nextElement ();
	    if (o instanceof Missile)
	    {
		Missile m = (Missile) o;
		x = (int) m.x;
		y = (int) m.y;
	    }
	    else if (o instanceof MRV)
	    {
		MRV m = (MRV) o;
		x = m.x;
		y = m.y;
	    }
	    else if (o instanceof Bomb)
	    {
		Bomb b = (Bomb) o;
		x = b.x;
		y = b.y;
	    }
	    else
	    {
		x = worldWidth/2;
		y = worldHeight/2;
	    }
	    
	    if (y > worldHeight/5)
	    {
		int d;
		if (Math.random () > 0.5)
		{
		    d = 1;
		}
		else
		{
		    d = -1;
		}
		shots.addElement (new Shot (x+d*(int)(Math.random ()*40),
					    y+d*(int)(Math.random ()*40),
					    60));
	    }
	}

    }
    
    void starWars ()
    {
	starWarsFireAt (missiles);
	starWarsFireAt (mrvs);
	starWarsFireAt (bombs);
    }
    
    void createShot (int x, int y)
    {
	if (shotCount > 0)
	{
	    shots.addElement (new Shot (x, y, 60));
	    shotCount--;
	}
    }

    void createMissile (int x1, int y1, int speed)
    {
	int x2 = baseGap + (int) (Math.random () * (worldWidth - baseGap));
	int y2 = worldHeight - 1;
	
	if (soundEnabled && missileSound != null && playing)
	{
	    missileSound.play ();
	}
	
	missiles.addElement (new Missile (x1, y1, x2, y2, speed));
    }

    void createMissile (int speed)
    {
	int x1 = (int) (Math.random () * worldWidth);
	int y1 = 1;
	createMissile (x1, y1, speed);
    }

    void destroyMissile (Missile m)
    {
	if (soundEnabled && missileExplosionSound != null && playing)
	{
	    missileExplosionSound.play ();
	}
	m.explode ();
	createExplosion ((int)m.x, (int)m.y, 20);
	incrementScore (missilePoints);
    }

    void createMRV (int speed)
    {
	int x1 = (int) (Math.random () * worldWidth);
	int y1 = 1;
	int y2 = worldHeight - 1;

	mrvs.addElement (new MRV (x1, y1, y2, speed, mrvImage, this));
    }

    void destroyMRV (MRV m)
    {
	if (soundEnabled && mrvExplosionSound != null && playing)
	{
	    mrvExplosionSound.play ();
	}
	m.explode ();
	createExplosion (m.x, m.y, 10);
	incrementScore (mrvPoints);
    }

    void createBomb (int speed)
    {
	if (soundEnabled && bombSound != null && playing)
	{
	    bombSound.play ();
	}
	
	int x1 = (int) (Math.random () * worldWidth);
	int y1 = 1;
	int y2 = worldHeight - 1;

	bombs.addElement (new Bomb (x1, y1, x1, y2, speed, bombImage, this));
    }

    void destroyBomb (Bomb b)
    {
	b.explode ();
	createBombDebris (b.x, b.y);
    }

    void createBombDebris (int x, int y)
    {
	bombDebris.addElement (new BombDebris (x, y, 200));
    }

    void createBases ()
    {
	bases = new Vector (maxBases);
	for (int i = 0, x = baseGap; i < maxBases; i++)
	{
	    bases.addElement (new Base (x, worldHeight - baseHeight,
					baseWidth, baseHeight, baseImage, this));
	    x += baseWidth + baseGap;
	}
    }

    int countAliveBases ()
    {
	int count = 0;
	for (int i = 0; i < maxBases; i++)
	{
	    Base b = (Base) bases.elementAt (i);
	    if (b.alive ())
	    {
		count++;
	    }
	}
	return count;
    }

    void destroyBase (Base b, Missile m)
    {
	if (soundEnabled && baseExplosionSound != null && playing)
	{
	    baseExplosionSound.play ();
	}
	m.explode ();
	b.explode ();
	createExplosion ((int)m.x, (int)m.y, 100);
    }
    
    void destroyBase (Base b, MRV m)
    {
	if (soundEnabled && baseExplosionSound != null && playing)
	{
	    baseExplosionSound.play ();
	}
	m.explode ();
	b.explode ();
	createExplosion (m.x, m.y, 100);
    }

    void destroyBase (Base b, BombDebris debris)
    {
	if (soundEnabled && baseExplosionSound != null && playing)
	{
	    baseExplosionSound.play ();
	}
	b.explode ();
	createExplosion (b.x + b.w/2,
			 b.y + b.h/2, 100);
    }

    void rebuildBases ()
    {
	int order[] = new int[maxBases];
	int i;

	for (i = 0; i < order.length; i++)
	{
	    order[i] = -1;
	}
	
	for (i = 0; i < maxBases; i++)
	{
	    int n = (int) (Math.random () * maxBases);
	    while (order[n] != -1)
	    {
		n++;
		if (n == maxBases)
		{
		    n = 0;
		}
	    }
	    order[n] = i;
	}

	for (i = 0; i < maxBases; i++)
	{
	    Base b = (Base) bases.elementAt (order[i]);
	    if (! b.alive ())
	    {
		if (extraBaseScore >= rebuildBasePoints)
		{
		    extraBaseScore -= rebuildBasePoints;
		    b.rebuild ();
		}
	    }
	}
    }
    
    void createExplosion (int x, int y, int size)
    {
	explosions.addElement (new Explosion (x, y, size));
    }

    void incrementScore (int points)
    {
	if (playing)
	{
	    score += points;
	    extraBaseScore += points;
	}
    }

    void destroyWorld ()
    {
	for (int i = 0; i < 10; i++)
	{
	    int x = (int)(Math.random () * worldWidth);
	    int y = (int)(Math.random () * worldHeight);
	    createExplosion (x, y, 100);
	}
    }
    
    public
    void update (Graphics g)
    {
	paint (g);
    }

    void paintGameObjects (Vector objs, Graphics g)
    {
	if (objs == null)
	{
	    return;
	}
	
	Enumeration e = objs.elements ();
	while (e.hasMoreElements ())
	{
	    GameObject o = (GameObject) e.nextElement ();
	    o.paint (g);
	}
    }

    void message (String message)
    {
	if (message != null)
	{
	    screenMessage = message;
	}
	else
	{
	    screenMessage = null;
	    clearScreen = true;
	}
	repaint ();
    }
    
    void paintMessage (String message, Graphics g)
    {
	int h = fontMetrics.getHeight ();
	int w = fontMetrics.stringWidth (message);
	g.setColor (Color.black);
	g.drawString (message, worldWidth/2 - w/2, worldHeight/2);
    }
    
    void paintStatus (Graphics g)
    {
	StringBuffer s = new StringBuffer ();
	s.append ("Score: ");
	s.append (score);
	s.append (" Level: ");
	s.append (level);
	s.append (" Shots: ");
	s.append (shotCount);

	int h = fontMetrics.getHeight ();
	int w = fontMetrics.stringWidth (s.toString ());
	int n = screenWidth/2 - w/2;

	g.setColor (Color.white);
	g.fillRect (0, 0, screenWidth, scoreHeight);
	
	g.setColor (Color.black);
	g.drawString (s.toString (), n, h);

	w = fontMetrics.stringWidth (version);
	g.drawString (version, screenWidth - w, h);
    }
    
    public
    void paint (Graphics g)
    {
	if (world == null)
	{
	    world = g.create (0, scoreHeight, worldWidth, worldHeight);
	}
	
	if (clearScreen)
	{
	    g.setColor (Color.white);
	    g.fillRect (0, 0, worldWidth, worldHeight);
	    
	    world.setColor (skyColor);
	    world.fillRect (0, 0, worldWidth, worldHeight);
	    clearScreen = false;
	}

	if (paused)
	{
	    paintMessage ("PAUSED", world);
	    return;
	}

	paintStatus (g);
	paintGameObjects (mrvs, world);
	paintGameObjects (missiles, world);
	paintGameObjects (bombs, world);
	paintGameObjects (bases, world);
	paintGameObjects (shots, world);
	paintGameObjects (explosions, world);
	paintGameObjects (bombDebris, world);

	if (!playing)
	{
	    paintMessage ("GAME OVER", world);
	}
	else if (screenMessage != null)
	{
	    paintMessage (screenMessage, world);
	}
    }
}
