

/*
Asteroids by Bjorn Carlin
Applet released April 2001
Source released December 2001

This source code is in the public domain,
you may use it in your own projects. Please
give credit where credit is due.

Applet works best with (width,height) = (500,350)

Asteroids requires GameApplet or GameForm
*/


import java.awt.*;
import java.awt.event.*;
import java.applet.Applet;
import java.util.*;


public class Asteroids extends GameApplet implements MouseListener
{
	// max angle = full revolution
	final double					kma = 2 * Math.PI;
	
	// size of reusable node arrays
	final int						kMaxPolygonNodes = 10;
	final int						kAsteroidShapeCount = 6;
	
	// graphics: a,r,a,rÉ for angle = 0; radius = 1
	final double[]					kShipCoordinates = new double[] {
									kma * 0.00, 1.8,
									kma * 0.38, 1.8,
									kma * 0.50, 0.6,
									kma * 0.62, 1.8
									};
	
	// [asteroid model][~node]
	double[][]						cAsteroidCoordinates = new double[][] {{0,0,0,0}};
	
	// keys
	static final int				kLeft = 0;
	static final int				kRight = 1;
	static final int				kThrust = 2;
	static final int				kFire = 3;
	
	// destroyed ship parts
	static final int				kDebrisParts = 6;
	static final int				kMaxDebrisLife = 20;
	static final double				kDebrisLength = 12.0;
	
	// alien constants
	static final double				kAlienProbability = 0.002;
	static final int				kMaxAlienArmor = 7;
	static final double				kAlienRadius = 20;
	static final double				kAlienSpeed = 1.5;
	static final int				kAlienRings = 5;
	static final double				kAlienRotationSpeed = 0.2;
	static final int				kMaxAlienBulletLife = 40;
	static final double				kAlienBulletSpeed = 9.0;
	
	// misc. constants
	static final int				kMoveRepetitionCount = 2;
	static final int				kMaxAsteroids = 50;
	static final int				kMaxBullets = 6;
	static final int				kSpaceOverlap = 25; // for warping between top and bottom, should be greater than or equal to max visible radius
	static final int				kExtraLifeScore = 25000;
	
	static final int				kMinAsteroidFragmentCount = 1;
	static final int				kMaxAsteroidFragmentCount = 5;
	static final double				kMinAsteroidFragmentSize = 0.3; // fraction of original asteroid size
	static final double				kMaxAsteroidFragmentSize = 0.7; // fraction of original asteroid size
	static final double				kMinAsteroidSize = 6.0; // smaller asteroids get removed
	static final int				kMaxAsteroidLife = 15;
	
	// stars
	int[]							cStarX = new int[20];
	int[]							cStarY = new int[20];
	
	// ship statistics
	double							cTurnSpeed = 0.2;
	double							cThrustSpeed = 0.3;
	double							cMaxSpeed = 5.0;
	double							cBulletSpeed = 8.0;
	
	boolean							cShipActive;
	double							cShipAngle; // 0 = 2¹ = right; incr. ccw
	double							cShipRadius;
	double							cShipCenterX;
	double							cShipCenterY;
	double							cShipMoveX;
	double							cShipMoveY;
	
	int								cShipInvincibleFrames; // -1 = not invincible
	
	// destroyed ship
	int								cDebrisLife; // -1 = no debris
	double[]						cDebrisCenterX = new double[kDebrisParts];
	double[]						cDebrisCenterY = new double[kDebrisParts];
	double[]						cDebrisMoveX = new double[kDebrisParts];
	double[]						cDebrisMoveY = new double[kDebrisParts];
	
	// reused objects for drawing
	int[]							cNodeX = new int[kMaxPolygonNodes];
	int[]							cNodeY = new int[kMaxPolygonNodes];
	
	// player statistics
	int								cLives; // 1 life = last life
	int								cScore;
	
	// bullet statistics
	int								cSuggestedBulletIndex; // suggestion for finding vacant slot
	int[]							cBulletLife = new int[kMaxBullets]; // frames remaining, -1 = vacant
	double[]						cBulletCenterX = new double[kMaxBullets];
	double[]						cBulletCenterY = new double[kMaxBullets];
	double[]						cBulletMoveX = new double[kMaxBullets];
	double[]						cBulletMoveY = new double[kMaxBullets];
	
	// alien
	boolean							cAlienActive;
	int								cAlienArmor;
	int								cAlienLoading;
	double							cAlienAngle;
	double							cAlienCenterX;
	double							cAlienCenterY;
	double							cAlienMoveX;
	double							cAlienMoveY;
	Color							cAlienColor;
	
	int								cAlienBulletLife; // frames remaining, -1 = inactive
	double							cAlienBulletCenterX;
	double							cAlienBulletCenterY;
	double							cAlienBulletMoveX;
	double							cAlienBulletMoveY;
	Color							cAlienBulletColor;
	
	// asteroid statistics
	double							cMaxAsteroidSpeed = 0.9; // double for fragments, plus original speed
	double							cMaxAsteroidRotationSpeed = 0.2; // double for fragments
	double							cMinNewAsteroidRadius = 16.0;
	double							cMaxNewAsteroidRadius = 21.0;
	
	int								cSuggestedAsteroidIndex; // suggestion for finding vacant slot
	int[]							cAsteroidModel = new int[kMaxAsteroids]; // -1 = vacant slot
	int[]							cAsteroidLife = new int[kMaxAsteroids];
	double[]						cAsteroidAngle = new double[kMaxAsteroids];
	double[]						cAsteroidRotationSpeed = new double[kMaxAsteroids];
	double[]						cAsteroidRadius = new double[kMaxAsteroids];
	double[]						cAsteroidCenterX = new double[kMaxAsteroids];
	double[]						cAsteroidCenterY = new double[kMaxAsteroids];
	double[]						cAsteroidMoveX = new double[kMaxAsteroids];
	double[]						cAsteroidMoveY = new double[kMaxAsteroids];
	
	// space statistics
	int								cLevel;
	int								cAsteroidCount;
	int								cLevelAge; // in frames
	int								cNextLevelCountdown;
	int								cTotalFadedAsteroids;
	int								cTotalAliens;
	
	
	public void init() {
		super.init(kDoubleBuffer);
		
		setKeys(0, 6,
				new int[] {KeyEvent.VK_LEFT,
							KeyEvent.VK_RIGHT,
							KeyEvent.VK_DOWN,
							KeyEvent.VK_SPACE},
				new int[] {kKeyContinous,
							kKeyContinous,
							kKeyContinous,
							kKeyRepeat});
		
		setCheats(new int[][] {{KeyEvent.VK_E}, {KeyEvent.VK_W, KeyEvent.VK_R, KeyEvent.VK_Y}});
		
		cBufferGraphics.setFont(new Font("SansSerif", Font.PLAIN, 10));
		
		cFrameLength = 50; // 20 fps
		
		addMouseListener(this);
	}
	
	public void pauseGame(boolean pause) {
		super.pauseGame(pause);
	}
	
	public void newGame() {
		cLives = 5;
		cScore = 0;
		cDebrisLife = -1; // no debris
		
		cAlienActive = false;
		cAlienBulletLife = -1;
		cTotalFadedAsteroids = 0;
		cTotalAliens = 0;
		
		for (int starCounter = 0; starCounter < 20; starCounter++) {
			cStarX[starCounter] = (int) (Math.random() * getWidth());
			cStarY[starCounter] = (int) (Math.random() * getHeight());
		}
		
		prepareLevel(1, true);
		super.newGame();
	}
	
	public void endGame() {
		super.endGame();
	}
	
	public void cheatActivated(int cheatID) {
		if (cheatID == 0 && cShipActive)
			destroyShip();
		else if (cheatID == 1)
			cLives++;
	}
	
	public void addScore(int amount) {
		if ((cScore + amount) / kExtraLifeScore > cScore / kExtraLifeScore)
			cLives++;
		
		cScore += amount;
	}
	
	public void prepareAlien() {
		int							location;
		
		if (cAlienActive)
			return;
		
		cTotalAliens++;
		cAlienActive = true;
		cAlienArmor = kMaxAlienArmor;
		cAlienLoading = kMaxAlienBulletLife;
		
		location = (int) (Math.random() * (getWidth() + getHeight()));
		if (location < getWidth() / 2) {
			// from top
			cAlienCenterX = location + getWidth() / 4;
			cAlienCenterY = -kSpaceOverlap + 5;
			cAlienAngle = kma * 3 / 4 + Math.random() - 0.5;
		} else if (location < (getWidth() + getHeight()) / 2) {
			// from right
			cAlienCenterX = getWidth() + kSpaceOverlap - 5;
			cAlienCenterY = location - getWidth() / 2 + getHeight() / 4;
			cAlienAngle = kma * 2 / 4 + Math.random() - 0.5;
		} else if (location < getWidth() + getHeight() / 2) {
			// from bottom
			cAlienCenterX = location - (getWidth() + getHeight()) / 2 + getWidth() / 4;
			cAlienCenterY = getHeight() + kSpaceOverlap - 5;
			cAlienAngle = kma * 1 / 4 + Math.random() - 0.5;
		} else {
			// from left
			cAlienCenterX = -kSpaceOverlap + 5;
			cAlienCenterY = location - getWidth() - getHeight() / 2 + getHeight() / 4;
			cAlienAngle = kma * 0 / 4 + Math.random() - 0.5;
		}
		
		cAlienMoveX = kAlienSpeed * Math.cos(cAlienAngle);
		cAlienMoveY = -kAlienSpeed * Math.sin(cAlienAngle);
		
		cAlienColor = new Color((int) (100 + 155 * Math.random()),
				(int) (100 + 155 * Math.random()),
				(int) (100 + 155 * Math.random()));
	}
	
	public void prepareShip(boolean invincible) {
		cShipActive = true;
		cShipAngle = kma / 4; // up
		cShipRadius = 8.0;
		cShipCenterX = getWidth() / 2;
		cShipCenterY = getHeight() / 2;
		cShipMoveX = 0.0;
		cShipMoveY = 0.0;
		cShipInvincibleFrames = invincible ? 40 : -1;
	}
	
	public void prepareLevel(int index, boolean newShip) {
		int							bulletCounter;
		int							asteroidCount;
		int							asteroidCounter;
		double						location;
		double						speed;
		
		cLevel = index;
		cLevelAge = 0;
		cNextLevelCountdown = 60; // will start counting when no asteroids are left
		
		cMaxAsteroidSpeed = 0.7 + 0.2 * cLevel;
		if (cMaxAsteroidSpeed > 1.7)
			cMaxAsteroidSpeed = 1.7;
		cMaxAsteroidRotationSpeed = 0.2;
		
		cMinNewAsteroidRadius = cLevel > 4 ? 19.0 : 15.0 + cLevel;
		cMaxNewAsteroidRadius = cLevel > 5 ? 23.0 : 18.0 + cLevel;
		
		if (newShip) {
			prepareShip(false);
			
			// prepare bullets
			for (bulletCounter = 0; bulletCounter < kMaxBullets; bulletCounter++)
				cBulletLife[bulletCounter] = -1;
		}
		
		// prepare asteroid shapes
		cAsteroidCoordinates = new double[kAsteroidShapeCount][];
		for (int shapeCounter = 0; shapeCounter < kAsteroidShapeCount; shapeCounter++) {
			int						nodeCount;
			int						nodeCounter;
			
			nodeCount = 6 + (int) (Math.random() * 5);
			cAsteroidCoordinates[shapeCounter] = new double[nodeCount * 2];
			for (nodeCounter = 0; nodeCounter < nodeCount; nodeCounter++) {
				cAsteroidCoordinates[shapeCounter][nodeCounter * 2] = kma * ((double) nodeCounter / nodeCount + Math.random() * (0.9 / nodeCount));
				if (nodeCounter % 2 == 0)
					cAsteroidCoordinates[shapeCounter][nodeCounter * 2 + 1] = 1.1 - Math.random() * 0.2;
				else
					cAsteroidCoordinates[shapeCounter][nodeCounter * 2 + 1] = 1.2 - Math.random() * Math.random() * 0.8;
			}
		}
		
		// prepare asteroids
		for (asteroidCounter = 0; asteroidCounter < kMaxAsteroids; asteroidCounter++)
			cAsteroidModel[asteroidCounter] = -1;
		
		cAsteroidCount = cLevel + 2 < kMaxAsteroids ? cLevel + 2 : kMaxAsteroids;
		for (asteroidCounter = 0; asteroidCounter < cAsteroidCount; asteroidCounter++) {
			cAsteroidModel[asteroidCounter] = (int) (Math.random() * cAsteroidCoordinates.length);
			cAsteroidLife[asteroidCounter] = kMaxAsteroidLife;
			cAsteroidAngle[asteroidCounter] = Math.random() * 2 * Math.PI;
			cAsteroidRotationSpeed[asteroidCounter] = Math.random() * cMaxAsteroidRotationSpeed * 2 - cMaxAsteroidRotationSpeed;
			cAsteroidRadius[asteroidCounter] = cMinNewAsteroidRadius + Math.random() * (cMaxNewAsteroidRadius - cMinNewAsteroidRadius);
			location = Math.random() * (getWidth() + getHeight());
			if (location < getWidth()) {
				cAsteroidCenterX[asteroidCounter] = location;
				cAsteroidCenterY[asteroidCounter] = -kSpaceOverlap;
			} else {
				cAsteroidCenterX[asteroidCounter] = -kSpaceOverlap;
				cAsteroidCenterY[asteroidCounter] = location - getWidth();
			}
			speed = Math.random() * (cMaxAsteroidSpeed - 0.4) + 0.4;
			cAsteroidMoveX[asteroidCounter] = speed * Math.cos(cAsteroidAngle[asteroidCounter]);
			cAsteroidMoveY[asteroidCounter] = speed * Math.sin(cAsteroidAngle[asteroidCounter]);
		}
		cSuggestedAsteroidIndex = cAsteroidCount % kMaxAsteroids;
	}
	
	public void destroyShip() {
		cShipActive = false;
		
		// spawn debris
		cDebrisLife = kMaxDebrisLife;
		
		for (int partCounter = 0; partCounter < kDebrisParts; partCounter++) {
			double			angle;
			double			speed;
			
			angle = ((double) partCounter + Math.random()) * kma / kDebrisParts;
			speed = Math.random() + 2.0;
			
			cDebrisCenterX[partCounter] = cShipCenterX + 2 * speed * Math.sin(angle);
			cDebrisCenterY[partCounter] = cShipCenterY + 2 * speed * Math.cos(angle);
			
			cDebrisMoveX[partCounter] = cShipMoveX + speed * Math.sin(angle);
			cDebrisMoveY[partCounter] = cShipMoveY + speed * Math.cos(angle);
		}
	}
	
	public void advanceAsteroid(int index, int repetition) {
		cAsteroidAngle[index] += cAsteroidRotationSpeed[index] / kMoveRepetitionCount;
		cAsteroidCenterX[index] += cAsteroidMoveX[index] / kMoveRepetitionCount;
		cAsteroidCenterY[index] += cAsteroidMoveY[index] / kMoveRepetitionCount;
		
		if (repetition == kMoveRepetitionCount - 1) {
			// "warp" if asteroid passes edge of space
			if (cAsteroidCenterX[index] < -kSpaceOverlap)
				cAsteroidCenterX[index] += getWidth() + 2 * kSpaceOverlap;
			if (cAsteroidCenterY[index] < -kSpaceOverlap)
				cAsteroidCenterY[index] += getHeight() + 2 * kSpaceOverlap;
			
			if (cAsteroidCenterX[index] > getWidth() + kSpaceOverlap)
				cAsteroidCenterX[index] -= getWidth() + 2 * kSpaceOverlap;
			if (cAsteroidCenterY[index] > getHeight() + kSpaceOverlap)
				cAsteroidCenterY[index] -= getHeight() + 2 * kSpaceOverlap;
			
			if (cAsteroidRadius[index] < kMinAsteroidSize)
				cAsteroidLife[index]--;
			
			if (cAsteroidLife[index] <= 0) {
				cAsteroidModel[index] = -1;
				cAsteroidCount--;
				cTotalFadedAsteroids++;
			}
		}
	}
	
	public void advanceShip() {
		if (isKeyActive(kLeft))
			cShipAngle += cTurnSpeed;
		if (isKeyActive(kRight))
			cShipAngle -= cTurnSpeed;
		
		// keep angle in bounds, debugging aid
		if (cShipAngle < 0)
			cShipAngle += kma;
		if (cShipAngle > kma)
			cShipAngle -= kma;
		
		if (isKeyActive(kThrust)) {
			double					speed;
			
			cShipMoveX += cThrustSpeed * Math.cos(cShipAngle);
			cShipMoveY -= cThrustSpeed * Math.sin(cShipAngle);
			
			// keep velocity in bounds
			speed = Math.sqrt(cShipMoveX * cShipMoveX + cShipMoveY * cShipMoveY);
			if (speed > cMaxSpeed) {
				cShipMoveX *= cMaxSpeed / speed;
				cShipMoveY *= cMaxSpeed / speed;
			}
		}
		
		if (isKeyActive(kFire)) {
			int						slot;
			
			slot = -1;
			for (int slotCounter = 0; slotCounter < kMaxBullets; slotCounter++)
				if (cBulletLife[(slotCounter + cSuggestedBulletIndex) % kMaxBullets] == -1) {
					slot = (slotCounter + cSuggestedBulletIndex) % kMaxBullets;
					break;
				}
			
			if (slot >= 0) { // found a vacant slot
				cBulletLife[slot] = 27;
				cBulletCenterX[slot] = cShipCenterX + cBulletSpeed * Math.cos(cShipAngle);
				cBulletCenterY[slot] = cShipCenterY - cBulletSpeed * Math.sin(cShipAngle);
				cBulletMoveX[slot] = cShipMoveX + cBulletSpeed * Math.cos(cShipAngle);
				cBulletMoveY[slot] = cShipMoveY - cBulletSpeed * Math.sin(cShipAngle);
				cSuggestedBulletIndex = (slot + 1) % kMaxBullets;
			} else {
				// no vacant slots, can't fire
			}
		}
		
		cShipCenterX += cShipMoveX;
		cShipCenterY += cShipMoveY;
		
		// "warp" if ship passes edge of space
		if (cShipCenterX < -kSpaceOverlap)
			cShipCenterX += getWidth() + 2 * kSpaceOverlap;
		if (cShipCenterY < -kSpaceOverlap)
			cShipCenterY += getHeight() + 2 * kSpaceOverlap;
		
		if (cShipCenterX > getWidth() + kSpaceOverlap)
			cShipCenterX -= getWidth() + 2 * kSpaceOverlap;
		if (cShipCenterY > getHeight() + kSpaceOverlap)
			cShipCenterY -= getHeight() + 2 * kSpaceOverlap;
		
		if (cShipInvincibleFrames > 0)
			cShipInvincibleFrames--;
		else
			cShipInvincibleFrames = -1; // not invincible
	}
	
	public void advanceDebris() {
		int							partCounter;
		
		for (partCounter = 0; partCounter < kDebrisParts; partCounter++) {
			cDebrisCenterX[partCounter] += cDebrisMoveX[partCounter];
			cDebrisCenterY[partCounter] += cDebrisMoveY[partCounter];
			
			// "warp" if debris passes edge of space
			if (cDebrisCenterX[partCounter] < -kSpaceOverlap)
				cDebrisCenterX[partCounter] += getWidth() + 2 * kSpaceOverlap;
			if (cDebrisCenterY[partCounter] < -kSpaceOverlap)
				cDebrisCenterY[partCounter] += getHeight() + 2 * kSpaceOverlap;
			
			if (cDebrisCenterX[partCounter] > getWidth() + kSpaceOverlap)
				cDebrisCenterX[partCounter] -= getWidth() + 2 * kSpaceOverlap;
			if (cDebrisCenterY[partCounter] > getHeight() + kSpaceOverlap)
				cDebrisCenterY[partCounter] -= getHeight() + 2 * kSpaceOverlap;
		}
	}
	
	public void advanceBullet(int index, int repetition) {
		if (cBulletLife[index] <= 0) {
			cBulletLife[index] = -1; // vacant
		} else {
			cBulletCenterX[index] += cBulletMoveX[index] / kMoveRepetitionCount;
			cBulletCenterY[index] += cBulletMoveY[index] / kMoveRepetitionCount;
			
			if (repetition == kMoveRepetitionCount - 1) {
				cBulletLife[index]--;
				
				// "warp" if bullet passes edge of space
				if (cBulletCenterX[index] < -kSpaceOverlap)
					cBulletCenterX[index] += getWidth() + 2 * kSpaceOverlap;
				if (cBulletCenterY[index] < -kSpaceOverlap)
					cBulletCenterY[index] += getHeight() + 2 * kSpaceOverlap;
				
				if (cBulletCenterX[index] > getWidth() + kSpaceOverlap)
					cBulletCenterX[index] -= getWidth() + 2 * kSpaceOverlap;
				if (cBulletCenterY[index] > getHeight() + kSpaceOverlap)
					cBulletCenterY[index] -= getHeight() + 2 * kSpaceOverlap;
			}
		}
	}
	
	public void advanceAlien() {
		cAlienAngle += kAlienRotationSpeed;
		cAlienCenterX += cAlienMoveX;
		cAlienCenterY += cAlienMoveY;
		
		if (cAlienLoading > 0)
			cAlienLoading--;
		
		if (cAlienArmor < -30) {
			// dead
			cAlienActive = false;
			return;
		} else if (cAlienArmor <= 0) {
			// dying
			cAlienArmor--;
			if (cAlienArmor % 4 == 0)
				cAlienColor = cAlienColor.darker();
		} else {
			// alive
			if (cAlienBulletLife < 0 && cAlienLoading <= 0) {
				// create a new bullet
				double					distX;
				double					distY;
				double					dist;
				
				distX = cShipCenterX - cAlienCenterX;
				distY = cShipCenterY - cAlienCenterY;
				
				dist = Math.sqrt(distX * distX + distY * distY);
				if (dist != 0) {
					cAlienBulletLife = kMaxAlienBulletLife;
					
					cAlienBulletMoveX = distX * kAlienBulletSpeed / dist;
					cAlienBulletMoveY = distY * kAlienBulletSpeed / dist;
					
					cAlienBulletCenterX = cAlienCenterX + cAlienBulletMoveX * 3;
					cAlienBulletCenterY = cAlienCenterY + cAlienBulletMoveY * 3;
					
					cAlienBulletColor = cAlienColor;
				}
			}
			
			// remove if alien passes edge of space
			if (cAlienCenterX < -kSpaceOverlap)
				cAlienActive = false;
			if (cAlienCenterY < -kSpaceOverlap)
				cAlienActive = false;
			
			if (cAlienCenterX > getWidth() + kSpaceOverlap)
				cAlienActive = false;
			if (cAlienCenterY > getHeight() + kSpaceOverlap)
				cAlienActive = false;
		}
	}
	
	public void advanceAlienBullet() {
		cAlienBulletCenterX += cAlienBulletMoveX;
		cAlienBulletCenterY += cAlienBulletMoveY;
		
		cAlienBulletLife--;
		if (cAlienBulletLife < 0)
			cAlienBulletLife = -1;
		
		// remove if bullet passes edge of space
		if (cAlienBulletCenterX < 0)
			cAlienBulletLife = -1;
		if (cAlienBulletCenterY < 0)
			cAlienBulletLife = -1;
		if (cAlienBulletCenterX > getWidth())
			cAlienBulletLife = -1;
		if (cAlienBulletCenterY > getHeight())
			cAlienBulletLife = -1;
	}
	
	public void spawnAsteroidFragments(int index) {
		int							fragmentCount;
		int							fragmentCounter;
		int							slot;
		int							slotCounter;
		double						radius;
		double						totalFragmentRadius;
		
		if (cAsteroidRadius[index] < kMinAsteroidSize)
			return; // avoid chain reactions
		
		totalFragmentRadius = 0;
		
		while (totalFragmentRadius < cAsteroidRadius[index] * 1.2) {
			slot = -1;
			for (slotCounter = 0; slotCounter < kMaxAsteroids; slotCounter++)
				if (cAsteroidModel[(slotCounter + cSuggestedAsteroidIndex) % kMaxAsteroids] == -1) {
					slot = (slotCounter + cSuggestedAsteroidIndex) % kMaxAsteroids;
					break;
				}
			
			if (slot >= 0) { // found a vacant slot
				cAsteroidRadius[slot] = cAsteroidRadius[index] * (kMinAsteroidFragmentSize + Math.random() * (kMaxAsteroidFragmentSize - kMinAsteroidFragmentSize));
				totalFragmentRadius += cAsteroidRadius[slot];
				cAsteroidModel[slot] = (int) (Math.random() * cAsteroidCoordinates.length);
				cAsteroidLife[slot] = kMaxAsteroidLife;
				cAsteroidAngle[slot] = Math.random() * 2 * Math.PI;
				cAsteroidRotationSpeed[slot] = Math.random() * cMaxAsteroidRotationSpeed * 4 - cMaxAsteroidRotationSpeed * 2;
				cAsteroidCenterX[slot] = cAsteroidCenterX[index];
				cAsteroidCenterY[slot] = cAsteroidCenterY[index];
				cAsteroidMoveX[slot] = cAsteroidMoveX[index] + Math.random() * cMaxAsteroidSpeed * 4 - cMaxAsteroidSpeed * 2;
				cAsteroidMoveY[slot] = cAsteroidMoveY[index] + Math.random() * cMaxAsteroidSpeed * 4 - cMaxAsteroidSpeed * 2;
				
				advanceAsteroid(slot, 0);
				advanceAsteroid(slot, 1);
				
				cAsteroidCount++;
				
				cSuggestedBulletIndex = (slot + 1) % kMaxAsteroids;
			} else {
				// no vacant slots, can't spawn fragment
				return;
			}
		}
	}
	
	public void handleCollisions() {
		int							bulletCounter;
		int							asteroidCounter;
		double						distX;
		double						distY;
		double						dist;
		
		// handle bullet/asteroid collisions
		for (bulletCounter = 0; bulletCounter < kMaxBullets; bulletCounter++)
			if (cBulletLife[bulletCounter] >= 0)
				for (asteroidCounter = 0; asteroidCounter < kMaxAsteroids; asteroidCounter++)
					if (cAsteroidModel[asteroidCounter] >= 0 &&
							cAsteroidLife[asteroidCounter] == kMaxAsteroidLife &&
							cBulletLife[bulletCounter] >= 0) {
						// both asteroid and bullet are used
						
						distX = cAsteroidCenterX[asteroidCounter] - cBulletCenterX[bulletCounter];
						distY = cAsteroidCenterY[asteroidCounter] - cBulletCenterY[bulletCounter];
						
						// allow "warped" collisions
						if (distX > getWidth() / 2 + kSpaceOverlap)
							distX -= getWidth() + 2 * kSpaceOverlap;
						if (distY > getHeight() / 2 + kSpaceOverlap)
							distY -= getHeight() + 2 * kSpaceOverlap;
						if (distX < -(getWidth() / 2 + kSpaceOverlap))
							distX += getWidth() + 2 * kSpaceOverlap;
						if (distY < -(getHeight() / 2 + kSpaceOverlap))
							distY += getHeight() + 2 * kSpaceOverlap;
						
						dist = Math.sqrt(distX * distX + distY * distY);
						if (dist < cAsteroidRadius[asteroidCounter]) {
							cBulletLife[bulletCounter] = -1; // remove bullet
							spawnAsteroidFragments(asteroidCounter);
							cAsteroidModel[asteroidCounter] = -1; // remove asteroid
							cAsteroidCount--;
							addScore((int) (cAsteroidRadius[asteroidCounter] * 10));
						}
					}
		
		// handle ship/asteroid collisions
		if (cShipActive)
			for (asteroidCounter = 0; asteroidCounter < kMaxAsteroids; asteroidCounter++)
				if (cAsteroidModel[asteroidCounter] >= 0 &&
						cAsteroidLife[asteroidCounter] == kMaxAsteroidLife) {
					// asteroid is used and large enough
					
					distX = cAsteroidCenterX[asteroidCounter] - cShipCenterX;
					distY = cAsteroidCenterY[asteroidCounter] - cShipCenterY;
					
					// allow "warped" collisions
					if (distX > getWidth() / 2 + kSpaceOverlap)
						distX -= getWidth() + 2 * kSpaceOverlap;
					if (distY > getHeight() / 2 + kSpaceOverlap)
						distY -= getHeight() + 2 * kSpaceOverlap;
					if (distX < -(getWidth() / 2 + kSpaceOverlap))
						distX += getWidth() + 2 * kSpaceOverlap;
					if (distY < -(getHeight() / 2 + kSpaceOverlap))
						distY += getHeight() + 2 * kSpaceOverlap;
					
					dist = Math.sqrt(distX * distX + distY * distY);
					if (dist < cAsteroidRadius[asteroidCounter] + cShipRadius) {
						// collision
						if (cShipInvincibleFrames == -1) {
							// not invincible
							destroyShip();
						}
						
						spawnAsteroidFragments(asteroidCounter);
						cAsteroidModel[asteroidCounter] = -1; // remove asteroid
						cAsteroidCount--;
					}
				}
		
		// handle alien/bullet collisions
		if (cAlienActive && cAlienArmor > 0)
			for (bulletCounter = 0; bulletCounter < kMaxBullets; bulletCounter++)
				if (cBulletLife[bulletCounter] >= 0) {
					distX = cBulletCenterX[bulletCounter] - cAlienCenterX;
					distY = cBulletCenterY[bulletCounter] - cAlienCenterY;
					
					// allow "warped" collisions
					if (distX > getWidth() / 2 + kSpaceOverlap)
						distX -= getWidth() + 2 * kSpaceOverlap;
					if (distY > getHeight() / 2 + kSpaceOverlap)
						distY -= getHeight() + 2 * kSpaceOverlap;
					if (distX < -(getWidth() / 2 + kSpaceOverlap))
						distX += getWidth() + 2 * kSpaceOverlap;
					if (distY < -(getHeight() / 2 + kSpaceOverlap))
						distY += getHeight() + 2 * kSpaceOverlap;
					
					dist = Math.sqrt(distX * distX + distY * distY);
					if (dist < kAlienRadius) {
						// collision
						cBulletLife[bulletCounter] = -1;
						cAlienArmor--;
						if (cAlienArmor <= 0) {
							// advanceAlien will control death/animation
							addScore(2000 + 1000 * cLevel);
						}
					}
				}
		
		// handle alien/ship collisions
		if (cAlienActive && cAlienArmor > 0 && cShipActive) {
			distX = cShipCenterX - cAlienCenterX;
			distY = cShipCenterY - cAlienCenterY;
			
			// allow "warped" collisions
			if (distX > getWidth() / 2 + kSpaceOverlap)
				distX -= getWidth() + 2 * kSpaceOverlap;
			if (distY > getHeight() / 2 + kSpaceOverlap)
				distY -= getHeight() + 2 * kSpaceOverlap;
			if (distX < -(getWidth() / 2 + kSpaceOverlap))
				distX += getWidth() + 2 * kSpaceOverlap;
			if (distY < -(getHeight() / 2 + kSpaceOverlap))
				distY += getHeight() + 2 * kSpaceOverlap;
			
			dist = Math.sqrt(distX * distX + distY * distY);
			if (dist < kAlienRadius + cShipRadius) {
				// collision
				cAlienArmor = 0; // advanceAlien will control death
				
				if (cShipInvincibleFrames == -1) {
					// not invincible
					destroyShip();
				}
			}
		}
		
		// handle alien bullet/ship collisions
		if (cAlienBulletLife >= 0 && cShipActive) {
			distX = cShipCenterX - cAlienBulletCenterX;
			distY = cShipCenterY - cAlienBulletCenterY;
			
			// allow "warped" collisions
			if (distX > getWidth() / 2 + kSpaceOverlap)
				distX -= getWidth() + 2 * kSpaceOverlap;
			if (distY > getHeight() / 2 + kSpaceOverlap)
				distY -= getHeight() + 2 * kSpaceOverlap;
			if (distX < -(getWidth() / 2 + kSpaceOverlap))
				distX += getWidth() + 2 * kSpaceOverlap;
			if (distY < -(getHeight() / 2 + kSpaceOverlap))
				distY += getHeight() + 2 * kSpaceOverlap;
			
			dist = Math.sqrt(distX * distX + distY * distY);
			if (dist < cShipRadius) {
				// collision
				cAlienBulletLife = -1; // inactive
				if (cShipInvincibleFrames == -1) {
					// not invincible
					destroyShip();
				}
			}
		}
	}
	
	public void advanceFrame() {
		super.advanceFrame();
		
		cLevelAge++;
		
		if (cShipActive)
			advanceShip();
		
		if (cAlienActive)
			advanceAlien();
		
		if (cAlienBulletLife >= 0)
			advanceAlienBullet();
		
		for (int repetition = 0; repetition < kMoveRepetitionCount; repetition++) {
			for (int asteroidCounter = 0; asteroidCounter < kMaxAsteroids; asteroidCounter++)
				if (cAsteroidModel[asteroidCounter] >= 0)
					advanceAsteroid(asteroidCounter, repetition);
			
			for (int bulletCounter = 0; bulletCounter < kMaxBullets; bulletCounter++)
				advanceBullet(bulletCounter, repetition);
			
			handleCollisions();
		}
		
		if (cDebrisLife > -1) {
			advanceDebris();
			cDebrisLife--;
			if (cDebrisLife == -1) {
				// time to spawn a new ship,
				cLives--;
				if (cLives > 0)
					prepareShip(true);
				else
					endGame();
			}
		}
		
		if (cTotalFadedAsteroids > (cTotalAliens + 1) * 64)
			prepareAlien();
		
		if (cAsteroidCount <= 0)
			cNextLevelCountdown--;
		
		if (cNextLevelCountdown < 0)
			prepareLevel(cLevel + 1, false);
	}
	
	public void setColor(Color color) {
		if (cGamePaused)
			cBufferGraphics.setColor(color.darker().darker());
		else
			cBufferGraphics.setColor(color);
	}
	
	public void drawShip() {
		int							nodeCount;
		int							nodeCounter;
		
		nodeCount = kShipCoordinates.length / 2;
		
		for (nodeCounter = 0; nodeCounter < nodeCount; nodeCounter++) {
			cNodeX[nodeCounter] = (int) (cShipCenterX +
					cShipRadius * kShipCoordinates[nodeCounter * 2 + 1] *
					Math.cos(cShipAngle + kShipCoordinates[nodeCounter * 2]));
			cNodeY[nodeCounter] = (int) (cShipCenterY -
					cShipRadius * kShipCoordinates[nodeCounter * 2 + 1] *
					Math.sin(cShipAngle + kShipCoordinates[nodeCounter * 2]));
		}
		
		setColor(Color.white);
		cBufferGraphics.drawPolygon(cNodeX, cNodeY, nodeCount);
	}
	
	public void drawDebris() {
		int							partCounter;
		double						angle;
		int							x1;
		int							x2;
		int							y1;
		int							y2;
		
		setColor(new Color(55 + 200 * cDebrisLife / kMaxDebrisLife,
				55 + 200 * cDebrisLife / kMaxDebrisLife,
				55 + 200 * cDebrisLife / kMaxDebrisLife));
		
		for (partCounter = 0; partCounter < kDebrisParts; partCounter++) {
			if (partCounter % 2 == 0)
				angle = - partCounter - cDebrisLife / 5.0;
			else
				angle = partCounter + cDebrisLife / 5.0;
			x1 = (int) (cDebrisCenterX[partCounter] + kDebrisLength / 2 * Math.sin(angle));
			x2 = (int) (cDebrisCenterX[partCounter] - kDebrisLength / 2 * Math.sin(angle));
			y1 = (int) (cDebrisCenterY[partCounter] + kDebrisLength / 2 * Math.cos(angle));
			y2 = (int) (cDebrisCenterY[partCounter] - kDebrisLength / 2 * Math.cos(angle));
			
			cBufferGraphics.drawLine(x1, y1, x2, y2);
		}
	}
	
	public void drawAlien() {
		int							ringCounter;
		int							x;
		int							y;
		
		if (cAlienArmor > 0) {
			setColor(cAlienColor);
			cBufferGraphics.drawOval((int) (cAlienCenterX - kAlienRadius), (int) (cAlienCenterY - kAlienRadius), (int) (kAlienRadius * 2), (int) (kAlienRadius * 2));
			if (cAlienArmor <= 1)
				setColor(cAlienColor.darker().darker());
			cBufferGraphics.drawOval((int) (cAlienCenterX - 6), (int) (cAlienCenterY - 6), 12, 12);
			
			for (ringCounter = 0; ringCounter < kAlienRings; ringCounter++) {
				if (cAlienArmor <= 2 + ringCounter)
					setColor(cAlienColor.darker().darker());
				
				x = (int) (cAlienCenterX + kAlienRadius * 0.7 * Math.sin(cAlienAngle + kma * ringCounter / kAlienRings));
				y = (int) (cAlienCenterY + kAlienRadius * 0.7 * Math.cos(cAlienAngle + kma * ringCounter / kAlienRings));
				cBufferGraphics.drawOval(x - 4, y - 4, 8, 8);
			}
		} else {
			// dying animation
			setColor(cAlienColor.darker().darker());
			cBufferGraphics.drawOval((int) (cAlienCenterX - kAlienRadius), (int) (cAlienCenterY - kAlienRadius), (int) (kAlienRadius * 2), (int) (kAlienRadius * 2));
			cBufferGraphics.drawOval((int) (cAlienCenterX - 6), (int) (cAlienCenterY - 6), 12, 12);
			
			for (ringCounter = 0; ringCounter < kAlienRings; ringCounter++) {
				x = (int) (cAlienCenterX + kAlienRadius * (0.7 - 0.1 * cAlienArmor) * Math.sin(cAlienAngle + kma * ringCounter / kAlienRings));
				y = (int) (cAlienCenterY + kAlienRadius * (0.7 - 0.1 * cAlienArmor) * Math.cos(cAlienAngle + kma * ringCounter / kAlienRings));
				cBufferGraphics.drawOval(x - 4, y - 4, 8, 8);
			}
		}
	}
	
	public void drawAlienBullet() {
		setColor(cAlienBulletColor);
		cBufferGraphics.drawRect((int) cAlienBulletCenterX, (int) cAlienBulletCenterY, 1, 1);
	}
	
	public void drawBullet(int index) {
		setColor(Color.white);
		cBufferGraphics.drawRect((int) cBulletCenterX[index], (int) cBulletCenterY[index], 1, 1);
	}
	
	public void drawAsteroid(int index) {
		int							nodeCount;
		int							nodeCounter;
		
		nodeCount = cAsteroidCoordinates[cAsteroidModel[index]].length / 2;
		
		for (nodeCounter = 0; nodeCounter < nodeCount; nodeCounter++) {
			cNodeX[nodeCounter] = (int) (cAsteroidCenterX[index] +
					cAsteroidRadius[index] * cAsteroidCoordinates[cAsteroidModel[index]][nodeCounter * 2 + 1] *
					Math.cos(cAsteroidAngle[index] + cAsteroidCoordinates[cAsteroidModel[index]][nodeCounter * 2]));
			cNodeY[nodeCounter] = (int) (cAsteroidCenterY[index] -
					cAsteroidRadius[index] * cAsteroidCoordinates[cAsteroidModel[index]][nodeCounter * 2 + 1] *
					Math.sin(cAsteroidAngle[index] + cAsteroidCoordinates[cAsteroidModel[index]][nodeCounter * 2]));
		}
		
		setColor(new Color(30 + 8 * cAsteroidLife[index], 30 + 8 * cAsteroidLife[index], 30 + 8 * cAsteroidLife[index]));
		cBufferGraphics.drawPolygon(cNodeX, cNodeY, nodeCount);
	}
	
	public void drawLife(int centerX, int centerY, double angle, double size) {
		int							nodeCount;
		int							nodeCounter;
		
		nodeCount = kShipCoordinates.length / 2;
		
		for (nodeCounter = 0; nodeCounter < nodeCount; nodeCounter++) {
			cNodeX[nodeCounter] = (int) (centerX +
					size * kShipCoordinates[nodeCounter * 2 + 1] *
					Math.cos(angle + kShipCoordinates[nodeCounter * 2]));
			cNodeY[nodeCounter] = (int) (centerY -
					size * kShipCoordinates[nodeCounter * 2 + 1] *
					Math.sin(angle + kShipCoordinates[nodeCounter * 2]));
		}
		
		cBufferGraphics.drawPolygon(cNodeX, cNodeY, nodeCount);
	}
	
	public void drawGame() {
		cBufferGraphics.setColor(Color.black);
		cBufferGraphics.fillRect(0, 0, getWidth(), getHeight());
		
		cBufferGraphics.setColor(Color.white);
		for (int starCounter = 0; starCounter < 20; starCounter++)
			cBufferGraphics.drawLine(cStarX[starCounter], cStarY[starCounter], cStarX[starCounter], cStarY[starCounter]);
		
		if (cShipActive && (cShipInvincibleFrames == -1 || ((cFrame / 3) % 2 == 0)))
			drawShip();
		
		if (cDebrisLife >= 0)
			drawDebris();
		
		if (cAlienActive)
			drawAlien();
		
		if (cAlienBulletLife >= 0)
			drawAlienBullet();
		
		for (int bulletCounter = 0; bulletCounter < kMaxBullets; bulletCounter++)
			if (cBulletLife[bulletCounter] != -1)
				drawBullet(bulletCounter);
		
		for (int asteroidCounter = 0; asteroidCounter < kMaxAsteroids; asteroidCounter++)
			if (cAsteroidModel[asteroidCounter] >= 0)
				drawAsteroid(asteroidCounter);
		
		// stats
		setColor(new Color(140, 200, 90));
		if (cLevelAge < 15) {
			cBufferGraphics.setFont(new Font("SansSerif", Font.PLAIN, 24));
			cBufferGraphics.drawString("Level " + cLevel, (getWidth() - cBufferGraphics.getFontMetrics().stringWidth("Level " + cLevel)) / 2, getHeight() / 3);
		} else if (cLevelAge > 45) {
			cBufferGraphics.setFont(new Font("SansSerif", Font.PLAIN, 10));
			cBufferGraphics.drawString("Level " + cLevel, getWidth() - 5 - cBufferGraphics.getFontMetrics().stringWidth("Level " + cLevel), 18);
		} else {
			int						animationFrame;
			int						x;
			int						y;
			
			animationFrame = cLevelAge - 15;
			cBufferGraphics.setFont(new Font("SansSerif", Font.PLAIN, 24 - animationFrame * 14 / 30));
			
			x = ((getWidth() - cBufferGraphics.getFontMetrics().stringWidth("Level " + cLevel)) / 2) * (30 - animationFrame) / 30 +
				(getWidth() - 5 - cBufferGraphics.getFontMetrics().stringWidth("Level " + cLevel)) * animationFrame / 30;
			y = (getHeight() / 3) * (30 - animationFrame) * (30 - animationFrame) / 30 / 30 +
				(18) * animationFrame * animationFrame / 30 / 30;
			
			cBufferGraphics.drawString("Level " + cLevel, x, y);
		}
		cBufferGraphics.setFont(new Font("SansSerif", Font.PLAIN, 10));
		
		if (cAsteroidCount == 1)
			cBufferGraphics.drawString("1 asteroid", getWidth() - 5 - cBufferGraphics.getFontMetrics().stringWidth("1 asteroids"), 36);
		else
			cBufferGraphics.drawString(cAsteroidCount + " asteroids", getWidth() - 5 - cBufferGraphics.getFontMetrics().stringWidth(cAsteroidCount + " asteroids"), 36);
		cBufferGraphics.drawString(cScore + " points", getWidth() - 5 - cBufferGraphics.getFontMetrics().stringWidth(cScore + " points"), 54);
		
		// spinning lives
		setColor(Color.lightGray);
		for (int lifeCounter = 0; lifeCounter < cLives; lifeCounter++) {
			if (lifeCounter == cLives - 1)
				setColor(new Color(220, 200, 50)); // current life, gold
			
			drawLife(12 + 18 * lifeCounter, 12, -lifeCounter / 3.0 - cFrame / 12.0, 4.2);
		}
	}
	
	public void update(Graphics theGraphics) {
		if (cGameInProgress) {
			drawGame();
			if (cGamePaused) {
				cBufferGraphics.setColor(Color.white);
				cBufferGraphics.setFont(new Font("Serif", Font.PLAIN, 48));
				cBufferGraphics.drawString("Pause", (getWidth() - cBufferGraphics.getFontMetrics().stringWidth("Pause")) / 2, getHeight() / 2);
				cBufferGraphics.setFont(new Font("Serif", Font.PLAIN, 18));
				cBufferGraphics.drawString("Click to continue", (getWidth() - cBufferGraphics.getFontMetrics().stringWidth("Click to continue")) / 2, getHeight() / 2 + 20);
			}
		} else {
			cBufferGraphics.setColor(Color.black);
			cBufferGraphics.fillRect(0, 0, getWidth(), getHeight());
			cBufferGraphics.setColor(Color.white);
			cBufferGraphics.setFont(new Font("Serif", Font.PLAIN, 24));
			cBufferGraphics.drawString("Return of the", (getWidth() - cBufferGraphics.getFontMetrics().stringWidth("Return of the")) / 2, 40);
			cBufferGraphics.setFont(new Font("Serif", Font.PLAIN, 64));
			cBufferGraphics.drawString("Asteroids", (getWidth() - cBufferGraphics.getFontMetrics().stringWidth("Asteroids")) / 2, 95);
			cBufferGraphics.setFont(new Font("Serif", Font.PLAIN, 18));
			cBufferGraphics.drawString("by Bjšrn Carlin", (getWidth() - cBufferGraphics.getFontMetrics().stringWidth("by Bjšrn Carlin")) / 2, 120);
			
			cBufferGraphics.setColor(Color.gray);
			cBufferGraphics.drawLine(20, 130, getWidth() - 21, 130);
			
			cBufferGraphics.setColor(Color.white);
			
			if (cGameOver) {
				cBufferGraphics.setFont(new Font("Serif", Font.PLAIN, 36));
				cBufferGraphics.drawString("Game Over", (getWidth() - cBufferGraphics.getFontMetrics().stringWidth("Game Over")) / 2, 190);
				cBufferGraphics.setFont(new Font("Serif", Font.PLAIN, 18));
				cBufferGraphics.drawString("Final Score: " + cScore + " points", (getWidth() - cBufferGraphics.getFontMetrics().stringWidth("Final Score: " + cScore + " points")) / 2, 240);
				cBufferGraphics.drawString("Click to start...", (getWidth() - cBufferGraphics.getFontMetrics().stringWidth("Click to start...")) / 2, 270);
			} else {
				cBufferGraphics.setFont(new Font("Serif", Font.PLAIN, 18));
				cBufferGraphics.drawString("Click to start...", (getWidth() - cBufferGraphics.getFontMetrics().stringWidth("Click to start...")) / 2, 180);
				
				setColor(new Color(140, 200, 90));
				cBufferGraphics.drawString("Controls", (getWidth() - cBufferGraphics.getFontMetrics().stringWidth("Controls")) / 2, 210);
				cBufferGraphics.drawString("Turn left: Left arrow", getWidth() / 2 - cBufferGraphics.getFontMetrics().stringWidth("Turn left: "), 235);
				cBufferGraphics.drawString("Turn right: Right arrow", getWidth() / 2 - cBufferGraphics.getFontMetrics().stringWidth("Turn right: "), 255);
				cBufferGraphics.drawString("Thrust: Down arrow", getWidth() / 2 - cBufferGraphics.getFontMetrics().stringWidth("Thrust: "), 275);
				cBufferGraphics.drawString("Fire: Space", getWidth() / 2 - cBufferGraphics.getFontMetrics().stringWidth("Fire: "), 295);
			}
		}
		cBufferGraphics.setColor(Color.gray);
		cBufferGraphics.drawRect(0, 0, getWidth() - 1, getHeight() - 1);
		super.update(theGraphics);
	}
	
	public void mousePressed(MouseEvent theEvent) {
		if (cGameInProgress)
			pauseGame(!cGamePaused);
		else
			newGame();
	}
	
	public void mouseReleased(MouseEvent theEvent) {}
	public void mouseClicked(MouseEvent theEvent) {}
	public void mouseEntered(MouseEvent theEvent) {}
	public void mouseExited(MouseEvent theEvent) {}
}


