// Gameballworld.cpp: implementation of the GameBallworld class and associated classes.
//
//////////////////////////////////////////////////////////////////////
#include "stdafx.h"
#include "gameballworld.h"
#include "force.h"
#include "listener.h"
#include "pop.h" //for playSound
#include "spritepolygon.h" 
#include "critterviewer.h"
#include "graphicsopengl.h"
#include "graphicsmfc.h"

IMPLEMENT_SERIAL( cGameBallworld, cGame, 0);
IMPLEMENT_SERIAL( cCritterBallProp, cCritter, 0);
IMPLEMENT_SERIAL( cCritterBallworldPlayer, cCritterArmedPlayer, 0);
IMPLEMENT_SERIAL( cCritterTreasure, cCritter, 0);
IMPLEMENT_SERIAL( cCritterBallWall, cCritterWall, 0);

#define HANDSETVIEWERPOS
	/* HANDSETVIEWERPOS turns on the option of hand setting the viewer pos in 
the InitializeViewer call.  It's more standard to comment this out, but I had some
lingering problems with the default approach for a long thin world, so now I'm preferring
the HANDSETVIEWERPOS optoin. The other way WORKS, but I don't TRUST it anymore for this
paritcualr world as I have so often seen it arbitrarily put my view BEHIND the world 
(negative z), though this coudl have been due to some cCritterViewer trackplayer code
thats' been recently revised.  So you can probably comment out HANDSETVIEWERPOS.*/

//==============================cGameBallworld=======
Real cGameBallworld::TREASURERADIUS = 1.0;
Real cGameBallworld::WALLTHICKNESS = 1.0;
Real cGameBallworld::LEVELTIMER = 99.0;

cGameBallworld::cGameBallworld()
{
//Fix the menu selections you'll allow.
	_menuflags &= ~cGame::MENU_BOUNCEWRAP; 
		/* Default _menuflags from cGame has autoplay off, hopper off. 
		Turn off wrap because it's bad with gravity, we want balls bouncing on bottom.*/
//	_menuflags &= ~cGame::MENU_3D; 
		/* Turn off 3D option because it's bad with the long thin background bitmap
		called IDB_STREET.  Other than that, though, 3D is ok for this game. */
	_menuflags &= ~cGame::MENU_TRACKPLAYER;
		/* Don't allow user to turn off the track player option. */ 
	_menuflags &= ~cGame::MENU_RIDEPLAYER;
		/* Don't ride the player */
	_menuflags |= cGame::MENU_HOPPER; //Turn on hopper listener option.
	_menuflags &= ~cGame::MENU_CHANGELISTENER; 
		/* It looks bad to use any listener other than hopper. */
	_spritetype = cGame::ST_BUBBLES;
	_border.set(100.0, 15.0, 0.0); //Use a long, thin world.
	setBackgroundBitmap(IDB_STREET); 
		/*  Sets the cSpriteBackground for the desired background bitmap.
		Note that you must call this after the _border has its size fully initialized. So each
		cGame child with a different border should call this again in its constructor. */
	setWrapflag(cCritter::BOUNCE);
	_seedcount = 7; 
	setPlayer(new cCritterBallworldPlayer(this));
	_ptreasure = new cCritterTreasure(this);
	//_arrayHCURSOR has only the _hCursorArrow inherited from cGame.
	//_arrayHCURSOR.Add(((CPopApp*)::AfxGetApp())->_hCursorPlay); //No need for the shoot cursor

	// Initial level is 0.
	_cur_level = -1;

	_numLevels = 2;
	_levels = new _level[_numLevels];

	_levels[0].init(1, 0);

	_levels[0].addEnemy(-5, 0.0, 2.0, 2.0, true, 2.0);
	//_levels[0].addEnemy(-8, 0);
	//_levels[0].addEnemy(-11, 0);
	//_levels[0].addEnemy(-14, 0);
	//_levels[0].addEnemy(-17, 0);

	_levels[1].init(2, 0);
	_levels[1].addEnemy(-5,0, 0, 0, false, -5);
	_levels[1].addEnemy(-11, 0, 0, 0, false, -5);
	//Put in some walls in the constructor.
#ifndef DEBUGDAM
	new cCritterBallWall( cVector(-4, 1),	cVector(0, 0), 1.0, this);
	new cCritterBallWall( cVector(4.0, -7),	cVector(1.0, -4.0), 0.5, this);
	new cCritterBallWall( cVector(-20, -7),	cVector(-18, -5), 0.7, this);
	new cCritterBallWall( cVector(20, -7.5),	cVector(22, -3), 0.8, this);
	//new cCritterBallWall( cVector(-8, -4),	cVector(-6, -3), 0.9, this);
	//new cCritterBallWall( cVector(-9, -8),	cVector(-3, -3), 1.1, this);
#else //DEBUGDAM is true
	_seedcount = 0;
#endif //DEBUGDAM
}

void cGameBallworld::initializeView(CPopView *pview)
{
	cGame::initializeView(pview);
	pview->setUseBackgroundBitmap(TRUE);
//	pview->setGraphicsClass(RUNTIME_CLASS(cGraphicsOpenGL)); //Comment this in for a 3D start
	pview->pviewpointcritter()->setTrackplayer(TRUE); //Always look at the player.
	pview->pviewpointcritter()->setFoveaProportion(0.45); /* A critterviewer has a _foveaproortion
		that ranges from 0.0 to 1.0.  We make it smaller than the default value of 0.85 so
		that the trackplayer image will scroll before player gets all the way to edge. 
		Reduce still more if you want to scroll even sooner.*/
}

void cGameBallworld::initializeViewpoint(cCritterViewer *pviewer)
{
	if (pviewer->is3D())
	{
#ifdef HANDSETVIEWERPOS
		pviewer->setViewpoint(cVector3(0.0, -8.0, 40.0), 
			cVector(border().lox(), border().midy()),
			FALSE);  
			/* The setViewpoint args are (directiontoviewer, lookatpoint, trytoseewholeworld),
			the default third argument is TRUE, which means to back off enough to
			see the whole world.  But in a long thin world, we don't want to do that.  */
#else //HANDSETVIEWERPOS
		pviewer->setViewpoint(cVector3(0.0, -0.2, 1.0), 
			cVector(border().lox(), border().midy()));  /* the default third TRUE argument
			will move the viewpoint far from the world and try to see it all. */
		pviewer->zoom(7.0); /* The bigger the world and the further away you are, 
			the more zoom you need. */
		/* I don't like this way as it makes the player trackign a little flaky to be in a
		high zoom. */
#endif //HANDSETVIEWERPOS
	}
	else //2D case
	{
		pviewer->setViewpoint(cVector::ZAXIS, //pplayer()->position());
			cVector(border().lox(), border().midy()));
		pviewer->zoom(4.0);
	}
}

void cGameBallworld::seedCritters()
{
	pbiota()->purgeCritters(RUNTIME_CLASS(cCritterBullet));
	pbiota()->purgeCritters(RUNTIME_CLASS(cCritterBallProp));
	//for (int i=0; i<_seedcount; i++)
	//	new cCritterBallProp(this);

	gotoNextLevel();

	pplayer()->moveTo(border().locorner());
}

void cGameBallworld::adjustGameParameters()
{
// (1) End the game if the player is dead
	if (!health()  && !_gameover) //Player's been killed and game's not over.
	{
		_gameover = TRUE;
		pplayer()->addScore(_scorecorrection); // So user can reach _maxscore 
		playSound("Tada");
		return; 
	}
// (2) Also don't let the ballcount diminish.
			//(need to recheck propcount in case we just called seedCritters).
	int ballcount = pbiota()->count(RUNTIME_CLASS(cCritterBallProp));
	int ballstoadd = _seedcount - ballcount;
 	//for (int i=0; i < ballstoadd; i++)
	//	new cCritterBallProp(this);
// (3) Maybe check some other conditions.

	if ((int)(cGameBallworld::LEVELTIMER - age()) <= 0 && !_gameover)
	{
		_gameover = TRUE;
		return;
	}


/*
	if ( (int)(age() - ( (cCritterBallworldPlayer*) pplayer())->lasthit()) < 5)
	{
		( (cCritterBallworldPlayer*) pplayer())->setInvincible(
			( (cCritterBallworldPlayer*) pplayer())->invincible() - 1
			);
	}
	else
	{
		( (cCritterBallworldPlayer*) pplayer())->setInvincible(0);
	}
	*/
 }

CString cGameBallworld::statusMessage()
{
	CString cStrStatusBar;
	int nUpdatesPerSecond;
	CString cStrUpdatespersecond;
	CString cStrCount;
	CString cStrScore;
	CString cStrHealth;
	CString cStrTimer;

	if (!gamepaused())
	{
		nUpdatesPerSecond = int(((CPopApp*)::AfxGetApp())->_timer.updatesPerSecond());
		if (!nUpdatesPerSecond)
			cStrUpdatespersecond.Format("Less than one update per second.");
		else	
			cStrUpdatespersecond.Format("Updates per second: %d.", nUpdatesPerSecond);
		if (((CPopApp*)::AfxGetApp())->_timer.runningNearMinSpeed())
			cStrUpdatespersecond += " (Slower than Real Time)";
	}
	else
			cStrUpdatespersecond.Format("Animation is paused.");
	cStrScore.Format("Score: %d.", score());
	cStrHealth.Format("Health: %d.", health());
	cStrTimer.Format("Time Left: %d.", (int)(cGameBallworld::LEVELTIMER - age()));
	int crittercount = _pbiota->count(RUNTIME_CLASS(cCritter));
	int bulletcount = _pbiota->count(RUNTIME_CLASS(cCritterBullet));
	cStrCount.Format("Ball Critters: %d.", crittercount-bulletcount-2); //Subtract two for player and treasure
	cStrStatusBar = cStrTimer + " " + cStrScore + cStrHealth + "  " + cStrCount + "  " + cStrUpdatespersecond; 
	return cStrStatusBar;
}

void cGameBallworld::Serialize(CArchive& ar) /* This is a bit of work, as I have to match
	_ppuck , _phockeyrobot , _pmygoal , _probotgoal to the pointers in the cBiota array. */
{
	int treasureindex;
	cGame::Serialize(ar);
	if (ar.IsStoring()) // Save 
 	{
		ar << _index(_ptreasure);
	}
	else //Load
	{
		ar >> treasureindex;
			// I have to cast from cCritter*
		_ptreasure = (cCritterTreasure*)(_pbiota->GetAt(treasureindex));
	}
}

void cGameBallworld::gotoNextLevel()
{
	if(++_cur_level > _numLevels)
	{
	//	endGame();
	}

	if (_cur_level > 0)
	{
		for (int i = 0; i < _levels[_cur_level - 1]._numEnemies; ++i)
		{
			if (! _theseEnemies[i]->isDead())
				_theseEnemies[i]->die();
		}

        delete [] _theseEnemies;
	}

	_theseEnemies = new cCritterBallProp*[_levels[_cur_level]._numEnemies];
	
	for (int i = 0; i < _levels[_cur_level]._numEnemies; ++i)
	{
		cCritterBallProp* enemy;
		if (!(_levels[_cur_level]._enemies+i)->isFlying)
			enemy = new cCritterBallProp(this, 
										(_levels[_cur_level]._enemies+i)->getVector().x() - 
											(_levels[_cur_level]._enemies+i)->lrange,
										(_levels[_cur_level]._enemies+i)->getVector().x() + 
											(_levels[_cur_level]._enemies+i)->rrange,
										(_levels[_cur_level]._enemies+i)->isFlying,
										(_levels[_cur_level]._enemies+i)->initVel);
		else
			enemy = new cCritterBallProp(this, 
										(_levels[_cur_level]._enemies+i)->getVector().y() - 
											(_levels[_cur_level]._enemies+i)->lrange,
										(_levels[_cur_level]._enemies+i)->getVector().y() + 
											(_levels[_cur_level]._enemies+i)->rrange,
										(_levels[_cur_level]._enemies+i)->isFlying,
										(_levels[_cur_level]._enemies+i)->initVel);
		
		enemy->moveTo( (_levels[_cur_level]._enemies + i)->getVector() );
		_theseEnemies[i] = enemy;
	}
	


}
//====================Critters for the Ballworld Game ==========================
//==================================================================


//==================cCritterBallworldPlayer=============

cCritterBallworldPlayer::cCritterBallworldPlayer(cGame *pownergame):
cCritterArmedPlayer(pownergame)
{
	setSprite(new cSpriteIcon(IDB_NSTAND));
	setHealth(10);
	moveTo(_movebox.locorner());
	setWrapflag(cCritter::CLAMP); //Use CLAMP so you stop dead at edges.
	setArmed(TRUE);  //Let's use bullets.
	setMaxspeed(40.0); //Need good speed to overcome gravity.
	setListener(new cListenerHopper(6.0, 0.2, 0.5)); /* The three arguments to the
		cListenerHopper constructor specify the walkspeed, the hopuptime, and the hopagainwait.
		Walkspeed is how fast the left/right (and PgUp/PgDn if in 3D where the ground is xz plane)
			arrow keys move the critter,
		Hopuptime is how many seconds you can accelerate up with the UP arrow, and
		Hopupwait is how many seconds you have to wait after the end of one hop
			before starting a new hop. */
	setListenerAcceleration(160.0); //So Hopper can overcome gravity
	addForce(new cForceGravity(50.0)); /* Uses  gravity. Default strength is 25.0.
		Gravity	will affect player using cListenerHopper. */
//	addForce(new cForceDrag(0.5)); //Better without friction.
//	setDensity(8.0 * density()); //To make it fall faster, but we don't need this.
	setRadius(0.7); //Default cCritter::PLAYERRADIUS is 0.4. 
	setAttitudeToMotionLock(FALSE);  //It looks nicer if you don't turn the player with motion.
	setAttitudeTangent(cVector::XAXIS); /* Without motion lock, we need setAttitudeTangent 
		to make sure he's pointing up at the start. */
}

BOOL cCritterBallworldPlayer::collide(cCritter *pcritter)
{	
	BOOL collided = cCritter::collide(pcritter);
	if (!collided)
		return FALSE;
/* If you're here, you collided.  We'll treat all the guys the same -- the collision
 with a Treasure is different, but we let the Treasure contol that collision. */
	if ((pgame()->age() - lasthit()) > (Real)1.0)
	{
		damage(1);
		setLasthit(pgame()->age());
	}
	pcritter->die();
	return TRUE;
}

//==================cCritterBallProp=============

cCritterBallProp::cCritterBallProp(cGame *pownergame):
cCritter(pownergame),
dead(false)
{
	if (pgame())
		setSprite(pgame()->randomSprite(pgame()->spritetype()));
	addForce(new cForceGravity(25.0, cVector( 0.0, -1, 0.0))); /* Make the
		gravity tilt towards the left to move them along. */
	addForce(new cForceDrag()); //Uses default friction strength 0.5.
	setDensity(2.0); 
	// Enemies won't bounce.
	setBounciness(0.0);
	setMaxspeed(20.0);
   	randomize(cCritter::MF_VELOCITY | cSprite::MF_RADIUS);
	setMinTwitchThresholdSpeed(4.0); //Means sprite doesn't switch direction unless it's moving fast
	Real playerx = 0.0;
	if (pownergame)
		playerx = pownergame->pplayer()->position().x();
	randomizePosition(cRealBox(cVector(playerx + 3.0,_movebox.loy()),
		_movebox.hicorner())); 
		/* I put them to the right of the player  */
}

void cCritterBallProp::die()
{
	pplayer()->addScore(value());
	dead = true;
	cCritter::die();
}

void cCritterBallProp::update(CPopView *pactiveview, Real dt)
{
	cCritter::update(pactiveview, dt); //Always call this first
	if (_outcode & BOX_LOX) /* use bitwise AND to check if a flag is set. */
		delete_me(); //tell the game to remove yourself

	if (isFlying)
	{
		if (position().y() < left_range || position().y() > right_range)
		{
			setVelocity(cVector(velocity().x(), -(velocity().y())));
		}
	}

	// code we added begins here
	if (position().x() < left_range || position().x() > right_range)
	{
		setVelocity(cVector(-(velocity().x()), velocity().y()));
	}

	
	
}

// functions we've added
cCritterBallProp::cCritterBallProp(cGame *pownergame, Real _left_range, Real _right_range, bool _isFlying, Real init_velocity):
cCritter(pownergame),
dead(false),
left_range(_left_range),
right_range(_right_range),
isFlying(_isFlying)
{
	if (pgame())
		setSprite(pgame()->randomSprite(pgame()->spritetype()));
	if (!isFlying)
		addForce(new cForceGravity(25.0, cVector( 0.0, -1, 0.0))); /* Make the
	//	gravity tilt towards the left to move them along. */
	//addForce(new cForceDrag()); //Uses default friction strength 0.5.
	setDensity(2.0); 
	// Enemies won't bounce.
	setBounciness(0.0);
	setMaxspeed(20.0);
   	//randomize(cCritter::MF_VELOCITY);
	setMinTwitchThresholdSpeed(4.0); //Means sprite doesn't switch direction unless it's moving fast
	
	this->setRadius(0.5);

	//Real playerx = 0.0;
	//if (pownergame)
	//	playerx = pownergame->pplayer()->position().x();
	//randomizePosition(cRealBox(cVector(playerx + 3.0,_movebox.loy()),
	//	_movebox.hicorner())); 
		/* I put them to the right of the player  */
	if (!isFlying)
		setVelocity(cVector(-init_velocity, 0.0));
	else
		setVelocity(cVector(0.0, -init_velocity));
}


//===================cCritterDamWall
cCritterBallWall::cCritterBallWall(const cVector &enda, const cVector &endb,
	 Real thickness, cGame *pownergame):
cCritterWall(enda, endb, thickness, pownergame)
{
	//Don't presently do anything else.  Could change color with a line like
	//psprite()->setFillColor(cColorStyle::CN_LIGHTYELLOW);
}

//==================cCritterTreasure=============

cCritterTreasure::cCritterTreasure(cGame *pownergame):
cCritter(pownergame)
{
	cPolygon *ppoly = new cPolygon(24);
	ppoly->setFilled(FALSE);
	ppoly->setLineWidthWeight(0.1);
	_collidepriority = cCollider::CP_PLAYER + 1; /* Let this guy call collide on the
		player, as his method is overloaded in a special way. */
	setSprite(ppoly);
	setRadius(cGameBallworld::TREASURERADIUS);
	setFixedflag(TRUE);
	moveTo(cVector(_movebox.hix()- 1.5 * cGameBallworld::TREASURERADIUS,
		_movebox.loy()));
}

int cCritterTreasure::collidesWith(cCritter *pothercritter)
{
	if (pothercritter->IsKindOf(RUNTIME_CLASS(cCritterBallworldPlayer)))
		return cCollider::COLLIDEASCALLER;
	else
		return cCollider::DONTCOLLIDE;
}

BOOL cCritterTreasure::collide(cCritter *pcritter)
{
	// Level loading happens here.
	//if (contains(pcritter)) //disk of pcritter is wholly inside my disk
	if (pcritter->position().x() >= _movebox.hix() - 1.0)
	{
		playSound("Tada");
		pcritter->addScore(100);
		pcritter->addHealth(1);
		pcritter->moveTo(pgame()->border().locorner());
		((cGameBallworld*) pgame())->gotoNextLevel();
		return TRUE;
	}
	else
		return FALSE;
}
