// GameActor.java - Implementation of the Actor class.

//   Main game applet will manipulate the objects through an array of Actors.
//   Derive subclasses from GameActor to create individual game objects.
//   The GameActor class by itself is practically useless, and no objects
//   should be made from this class.  I decided not to make it "abstract".

import java.awt.*;
import java.applet.*;
import Stoneroids;


class GameActor
{
	static Rectangle  screenRect;	// Rectangle of visible onscreen window.
	static boolean    screenWrap;	// True makes objects wrap to opposite side
	static Stoneroids thisApp;		// "Global" Pointer to this applet
	static GameActor objList[];		// Local Pointer to entire array of objects
	static AudioClip  SFX[];		// Local Pointer to Sound Clip Array
	static int sfxExplode = 0;		// SFX Indexes:  Explosion
	static int sfxBuzz    = 1;		//   Invulnerability
	static int sfxPlrHit  = 2;		//   Player Hit
	static int sfxShot    = 3;		//   Laser shot
	static int sfxSwitch  = 4;		//   Weapon switch or Weapon Powerup
	static int sfxSiren   = 5;		//   Siren (Laser Freeze)
	static int sfxAlHit   = 6;		//   AlienShip is hit
	static int sfxPwrup1  = 7;		//   Shield or Energy Powerups
	static int sfxPwrup2  = 8;		//   Invulnerability, SubZero or Other Powerups

	GameActor pOwner;			// Pointer to object's owner (if any)
	Polygon polyO, poly;	// Original and current shape of object
	Rectangle bbox;			// Bounding box enclosing the object
	Color color;			// Color of object (outline or filled)
	double cx, cy, px, py;	// current/previous (x,y) location
	double angle, pAngle, speed;	// Current/Previous angle and velocity
	double dx, dy;			// x,y components of velocity
	double shield, energy;	// Shield and Energy remaining
	boolean solidFill;		// Fills Polygon if true, outline if false.
	boolean active;			// True if alive, false if dead or inactive
	boolean onscreen;		// True if alive and it's in the playing area
	boolean movable;		// True if object can move (i.e. not walls)
	boolean invincible;		// True if object can't die (i.e. walls)
	int KeyList[], keys[];	// Key List array and current key status
	int NumChildren;		// Current # living children	(Max: 10 Lasers)
	int ScoreVal;			// Point value when killed (for all like objects)
	int status;				// Status: 0=dead, 1=alive, -1=dying (others are custom)
	int ticker, ticker2;	// Timers/temps used in status-like events (like dying)
	int lifetime;			// # Frames object has been active...


	// Default constructor calls Create to initialize the object.
	GameActor()
	{
		Create();
	}


	// Creates and Initialize a new object.
	// Base class initializes GameActor members to 0 or false.
	// Subclasses call this to modify values and initialize new behavior.
	void Create()
	{
		// Initialize GameActor variables...
		cx = cy = px = py = 0;
		angle = pAngle = 0;
		speed = dx = dy = 0;
		shield = energy = ScoreVal = 0;
		status = ticker = ticker2 = lifetime = NumChildren = 0;
		active = onscreen = movable = invincible = false;
		KeyList = null;    keys = null;    pOwner = null;

		// Initialize object's Color and Paint style...
		color = new Color(0, 0, 255);		// Default color is Color.blue
		solidFill = true;
	}


	// Initialize current/original shapes, and bounding box.
	// Default shape is a triangle.  Subclasses override for custom shapes.
	void MakePolyShape()
	{
		poly = new Polygon();
		poly.addPoint(0,10);   poly.addPoint(7,-10);   poly.addPoint(-7,-10);
		bbox = poly.getBoundingBox();
		polyO = new Polygon(poly.xpoints, poly.ypoints, poly.npoints);
	}

	
	// Moves the object for the next game frame...
	// Base class only moves the object based on dx & dy of speed.
	void Move()
	{
		if (active == true)    lifetime++;

		// If she's a virgin, then translate & rotate Poly...  ;)
		if (lifetime == 1)    RotatePoly();

		// Only move movable objects that are moving. Confused? Good!
		if (movable  &&  speed > 0)
		{
			px = cx;    py = cy;		// Remember previous coords
			cx += dx;   cy += dy;		// Move object to new coords
		}

		// Wrapping to keep angles within (-PI < angle <= PI)
		if (pAngle != angle)   pAngle = angle;
		if (angle >  Math.PI)  {   angle -= 2*Math.PI;   pAngle -= 2*Math.PI;   }
		if (angle < -Math.PI)  {   angle += 2*Math.PI;   pAngle += 2*Math.PI;   }

		// Checks to wrap object to opposite side if "screenWrap" is set
		if (screenWrap  &&  bbox.intersects(screenRect) == false)
		{
			px = cx;    py = cy;
			if (bbox.y+bbox.height-1 < screenRect.y)		// Off top side
				cy = screenRect.y+screenRect.height + cy-bbox.y;
			if (bbox.y > screenRect.y+screenRect.height-1)	// Off bottom side
				cy = screenRect.y - bbox.y-bbox.height+cy;
			if (bbox.x+bbox.width-1 < screenRect.x)			// Off left side
				cx = screenRect.x+screenRect.width + cx-bbox.x;
			if (bbox.x > screenRect.x+screenRect.width-1)	// Off right side
				cx = screenRect.x - bbox.x-bbox.width+cx;

			RotatePoly();					// Updates all polygon coordinates
		}
	}

	
	// Thrust modifies object's movement with acceleration
	void Thrust(double accel, double maxSpd)
	{
		double dx1 = dx,  dy1 = dy;				// Remember current dx & dy
		double dx2 = (Math.cos(angle)*accel);	// Get x&y components of accel
		double dy2 = (Math.sin(angle)*accel);
		dx += dx2;    dy += dy2;				// Get new components of speed
		
		// Limit max speed to maxSpd pix/frame in both x & y directions...
		if (Math.abs(dx) > maxSpd)
		{
			dx1 = (dx>0) ?maxSpd :-maxSpd;	// Limit maximum dx
			dy = dx1 * (dy/dx);				// Keeps dy in proportion
			dx = dx1;
		}
		if (Math.abs(dy) > maxSpd)
		{
			dy1 = (dy>0) ?maxSpd :-maxSpd;	// Limit maximum dy
			dx = dy1 * (dx/dy);				// Keeps dx in proportion
			dy = dy1;
		}
		speed = Math.sqrt(dx*dx + dy*dy);	// Calculate new speed
	}


	// Handles the keyDown event for particular object(s)...
	boolean keyDown(Event ev, int key)
	{
		return true;
	}


	// Handles the keyUp event for particular object(s)...
	boolean keyUp(Event ev, int key)
	{
		return true;
	}


	// What to do when this object collides with Object Y.
	// This is not collision detection.
	// Base class calls both objects to Explode.
	void OnHit(GameActor objY)
	{
		if (!invincible)         Explode();
		if (!objY.invincible)    objY.Explode();
	}


	// Sister function to OnHit.  Action/Damage taken when object gets hit.
	void Damage(GameActor objY)
	{
		// Overload to Customize this function
	}


	// Draws an explosion as the object dies.
	// Base class doesn't draw explosion, and object dies immediately.
	void Explode()
	{
		status = -1;    ticker = 0;					// Sets dying ticker
		ticker2 = (bbox.width + bbox.height) / 2;	// Max explosion size
		invincible = false;
		speed /= 3;    dx /= 3;    dy /= 3;			// Slow down explosion speed...
	}

	
	// Object is now truly dead.  Set to inactive and stop movement.
	void Death()
	{
		status = ticker = ticker2 = 0;
		shield = energy = speed = dx = dy = 0;	// Stops object
		active = false;							// Dies now
		if (pOwner != null)						// Remove Ownership (if any)
		{   pOwner.NumChildren--;    pOwner = null;   }
	}


	// Translates points in polyO to "angle" and puts into poly.  Updates bbox 
	// to fit new shape.  Use poly for collision detection and painting.
	void RotatePoly()
	{
		int i, x, y;
		double r, theta;

		// If object never moved or translated, don't need to compute
		if (cx == px  &&  cy == py  &&  angle == pAngle)    return;
		if (status == -1)    return;

		// Find each rotated point and add it to the polygon.
		for (i=0;  i < poly.npoints;  i++)
		{
			// Convert from rectangular to polar coordinates...
			x = polyO.xpoints[i];        y = polyO.ypoints[i];
			r = Math.sqrt(x*x + y*y);    theta = Math.atan2(x, y);
			
			// Fill poly with rotated and translated coordinates...
			poly.xpoints[i] = (int)(r * Math.cos(theta + angle)) + (int)cx;
			poly.ypoints[i] = (int)(r * Math.sin(theta + angle)) + (int)cy;
		}
		
		bbox = CalculateBounds(poly);		// Recalculate the bounding box...
	}


	// CanCollide determines which pairs objects need collision detection.
	// Base class assumes any pair of objects need detection.
	boolean CanCollide(GameActor objY)
	{	return true;   }


// 	Collide		// Point-In-Polygon based collision detection
//Collision Detection:
//1.	Check only "movable" objects (Object X).  If X doesn't move (walls, etc),
//	no need to check if it collides with anything.
//2.	Check bbox of X with bboxes of all objects (movable or not).
//3.	If bboxes intersect, begin a pt-2-pt detection with intersecting Obj Y.
//4.	For each point in Object X...
//	a.	Detect if point is in bbox of Y.
//	b.	If so, detect if point is in the Poly Region of Y.
//	c.	If so, then Collision is TRUE.
	synchronized boolean Collide(GameActor objY)
	{
		// Don't check non-moving or dying objects
		if (movable == false)    return false;
		//if (status == -1)        return false;
		
		// Check for bounding box intersection...
		if (bbox.intersects(objY.bbox) == false)    return false;

		// Check if this pair of objects should allow collision detection...
		if (CanCollide(objY) == false)    return false;

		// If bboxes intersect, begin pt-2-pt detection with intersecting objY.
		for (int i=0;  i < poly.npoints;  i++)
		{
			// Detect if this point is in the bbox of objY.
			if (objY.bbox.inside(poly.xpoints[i], poly.ypoints[i]) == true)
			{
				// If so, detect if point is in poly region of objY.
				if (objY.PointInPoly(poly.xpoints[i], poly.ypoints[i]) == true)
					return true;
			}
		}
		return false;
	}


	// Draws the individual object
	// Base class version simply draws the Polygon poly.
	void paint(Graphics g)
	{
		if (active == false)    return;

		if (status == -1)			// Is the object dying?  Draw Explosion.
		{
			Color col;
			float xratio = (float)ticker / ((ticker2<12) ?ticker2 :12);
			float ratio = (xratio<=1.0) ?xratio :(2f-xratio);

			// Object is truly dead when its Explosion timer runs out...
			if (ticker>0  &&  ratio<0.0)    {   Death();    return;   }

			// Paint explosion with explosion color and shape...
			col = new Color(255, (int)(ratio*255), 0);		// Red->Org->Yel->Org->Red
			g.setColor(col);
			g.fillOval((int)(cx-(ratio*ticker2)/2-4), (int)(cy-(ratio*ticker2)/2-4),
				(int)(ratio*ticker2)+8, (int)(ratio*ticker2)+8);

			++ticker;
		}
		else
		{
			g.setColor(color);						// Set object's color
			
			if (solidFill)    g.fillPolygon(poly);	// Fill solid if true
			else              g.drawPolygon(poly);	// Draw outline if false

			// For debug purposes, draw the bounding box around each object...
			//g.setColor(Color.darkGray);
			//g.drawRect(bbox.x, bbox.y, bbox.width, bbox.height);
		}
	}


	// Returns a Rectangle which is the bounding box of the polygon.
    Rectangle CalculateBounds(Polygon p)
	{
		int i, x, y;
		int MinX = Integer.MAX_VALUE,  MinY = Integer.MAX_VALUE;
		int MaxX = Integer.MIN_VALUE,  MaxY = Integer.MIN_VALUE;
	
		for (i = 0;  i < p.npoints;  i++)
		{
			x = p.xpoints[i];            y = p.ypoints[i];
			MinX = Math.min(MinX, x);    MinY = Math.min(MinY, y);
			MaxX = Math.max(MaxX, x);    MaxY = Math.max(MaxY, y);
		}
		return new Rectangle(MinX, MinY, MaxX-MinX, MaxY-MinY);
    }

	
	// Returns true if the point (x,y) is inside the Polygon poly.
	// pPt is a pointer to an array of points that defines the polygon.
	boolean PointInPoly(int x, int y)
	{
		boolean PtIn=false;
		int i, j, ix, iy, jx, jy;

		for (i=0, j=poly.npoints-1;  i<poly.npoints;  j=i++) 
		{
			ix = poly.xpoints[i];    iy = poly.ypoints[i];
			jx = poly.xpoints[j];    jy = poly.ypoints[j];
			if ( ( (iy<=y && y<jy) || (jy<=y && y<iy) ) &&
				( x < (y-iy)*(jx-ix)/(jy-iy)+ix ) )
					PtIn= !PtIn;
		}
		return PtIn;

		// Older version of PointInPoly()
/*		if (bbox.inside(x, y))
		{
			int hits = 0;
		
			// Walk the edges of the polygon
			for (int i=0;  i < poly.npoints;  i++)
			{
				int j = (i+1) % poly.npoints;
				int ddx = poly.xpoints[j] - poly.xpoints[i];
				int ddy = poly.ypoints[j] - poly.ypoints[i];
			
				// Ignore horizontal edges completely
//				if (ddy != 0)
				{
					// Check if edge intersects horiz halfline thru (x,y)
					int rx = x - poly.xpoints[i];
					int ry = y - poly.ypoints[i];
					float s;
					
					// Quick and dirty way to deal with vertices
					// that fall exactly on the halfline
					if (poly.ypoints[i] == y)    ry--;
					if (poly.ypoints[j] == y)    ddy++;
					
					if (ddy != 0)
						s = (float)ry / (float)ddy;
					else
						s = (float)ry / (float)(ddy+.05);	// In case of div by zero

					if (s >= 0.0 && s <= 1.0)
					{
						if ((int)(s * ddx) > rx)    hits++;
					}
				}
			}
			return (hits % 2) != 0;
		}
		return false;
*/	}


	// Returns the index of the first available position in the array.
	// Available positions are either null or have an "inactive" object.
	// -1 is returned if no free index can be found.
	static int GetFreeIndex(GameActor array[])
	{
		for (int i=0;  i < array.length;  i++)
			if (array[i] == null  ||  array[i].active == false)
				return i;

		return -1;		// Couldn't find a free index...
	}


	// Adds objX into the thisApp.object[] array.  Object is placed into the first
	// available position (null or not active).  Enlarges array when necessary.
    static void AddObject(GameActor objX)
	{
		int i, index;
		int ArrLen = /*objList.length;*/thisApp.object.length;

		// Find index to add this object at...
		index = GetFreeIndex(/*objList*/thisApp.object);

		// If index was not found (-1), we need to enlarge array...
		if (index == -1)
		{
			GameActor newArray[] = new GameActor[ArrLen + thisApp.ArrBlockSize];
			System.arraycopy(/*objList*/thisApp.object, 0, newArray, 0, ArrLen);
			/*objList*/thisApp.object = newArray;    newArray = null;
			index = ArrLen;
		}

		// Place (or replace) the object at location "index".
		if (/*objList*/thisApp.object[index] == null)    thisApp.NumObjects++;
		/*objList*/thisApp.object[index] = objX;
		objList = thisApp.object;
	}


	// Compacts the object array by swapping inactive indexes with active
	// objects.  Returns the total number of active objects in array after 
	// cleanup.
	static int CompactList(GameActor array[])
	{
		GameActor gaTemp;
		int i, j, k, numActive;
		boolean done = false;

		i=0;    j=array.length-1;
		while (!done)
		{
			thisApp.showStatus("Swap1:  i="+i + "  j="+j);
			// Update i to the next inactive/null item in array
			for (k=i;  k < j;  k++)
			{
				if (array[k] == null  ||  array[k].active == false)    break;
			}
			i = k;
			if (i >= j)    {   done = true;    continue;   }

			thisApp.showStatus("Swap2:  i="+i + "  j="+j);
			// Update j to the last active item in array...
			for (k=j;  k > i;  k--)
			{
				if (array[k] != null  &&  array[k].active == true)    break;
			}
			j = k;
			if (j <= i)    {   done = true;    continue;   }

			// If flow reaches here, then i points to a null item within the list
			// and j points to an active item at end of list.  Swap the items...
			thisApp.showStatus("Swap3:  i="+i + "  j="+j);
			gaTemp = array[i];
			array[i] = array[j];
			array[j] = gaTemp;
			thisApp.showStatus("Swap4:  i="+i + "  j="+j);
			done = true;
		}

		// All active items should now be a contiguous set.
		// Run a quick sweep to find how many active items now exist...
		for (i=0, numActive=0;  i < array.length;  i++)
		{
			if (array[i] != null  &&  array[i].active == true)
				numActive++;
			else
				break;
		}

		objList = array;
		return numActive;
	}
}

