/***********************************************************************
 *
 Copyright © 1995 - 1998, 3Com Corporation or its subsidiaries ("3Com").  
 All rights reserved.
   
 This software may be copied and used solely for developing products for 
 the Palm Computing platform and for archival and backup purposes.  Except 
 for the foregoing, no part of this software may be reproduced or transmitted 
 in any form or by any means or used to make any derivative work (such as 
 translation, transformation or adaptation) without express written consent 
 from 3Com.

 3Com reserves the right to revise this software and to make changes in content 
 from time to time without obligation on the part of 3Com to provide notification 
 of such revision or changes.  
 3COM MAKES NO REPRESENTATIONS OR WARRANTIES THAT THE SOFTWARE IS FREE OF ERRORS 
 OR THAT THE SOFTWARE IS SUITABLE FOR YOUR USE.  THE SOFTWARE IS PROVIDED ON AN 
 "AS IS" BASIS.  3COM MAKES NO WARRANTIES, TERMS OR CONDITIONS, EXPRESS OR IMPLIED, 
 EITHER IN FACT OR BY OPERATION OF LAW, STATUTORY OR OTHERWISE, INCLUDING WARRANTIES, 
 TERMS, OR CONDITIONS OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND 
 SATISFACTORY QUALITY.

 TO THE FULL EXTENT ALLOWED BY LAW, 3COM ALSO EXCLUDES FOR ITSELF AND ITS SUPPLIERS 
 ANY LIABILITY, WHETHER BASED IN CONTRACT OR TORT (INCLUDING NEGLIGENCE), FOR 
 DIRECT, INCIDENTAL, CONSEQUENTIAL, INDIRECT, SPECIAL, OR PUNITIVE DAMAGES OF 
 ANY KIND, OR FOR LOSS OF REVENUE OR PROFITS, LOSS OF BUSINESS, LOSS OF INFORMATION 
 OR DATA, OR OTHER FINANCIAL LOSS ARISING OUT OF OR IN CONNECTION WITH THIS SOFTWARE, 
 EVEN IF 3COM HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

 3Com, HotSync, Palm Computing, and Graffiti are registered trademarks, and 
 Palm III and Palm OS are trademarks of 3Com Corporation or its subsidiaries.

 IF THIS SOFTWARE IS PROVIDED ON A COMPACT DISK, THE OTHER SOFTWARE AND 
 DOCUMENTATION ON THE COMPACT DISK ARE SUBJECT TO THE LICENSE AGREEMENT 
 ACCOMPANYING THE COMPACT DISK.

 *************************************************************************
 *
 * PROJECT:  HardBall
 * FILE:     HardBall.c
 * AUTHOR:	 Roger Flores: Jan 22, 1996
 *
 * DECLARER: HardBall
 *
 * DESCRIPTION:
 *
 *	  This is considered PRE-ALPHA code!  It's intention is to demonstrate
 *   a working example of an animated game on the Pilot.  There are some
 *	  specific features worth studying for use in other games:
 *
 *		1) The timing of the game.  Waiting a constant period and then 
 *			drawing followed by updating the game state.
 *
 *		2) The handling of windows appearing above the game window.
 *
 *		3) The handling of the hard buttons.
 *
 *		4) The conversion of PICT resources (bitmaps) into window buffers
 *			for improved speed and to enable the use of masks (screen OR/ANDNOT)
 *
 *			Note: There is a patent covering the use of XORing an image in
 *			a buffer to the screen.  In order to use XORing, an agreement with 
 *			the patent holder must be reached. Typically, the patent holder requests
 *			two percent of the product's revenue.This patent expires in 1997.
 *
 *			HardBall does not contain XOR.
 *
 *		5) The handling of sound in the game. Includes priorities  and different sound 
 *			durations.
 *
 *		6) The handling of saving the game to state while switching to another app 
 *			and successfully restoring.
 *
 *		7) The design of a high score mechanism.
 *
 *
 *	  Comments: please send comments and bugs/fixes to Palm Computing for 
 *	  inclusion into further revisions. devsupp@palm.com
 *
 *
 *
 *	Model for Game World Updating
 *	
 *	The model below is used for animated games.  The idea is to draw the game
 *	state at regular time periods.  To this end, the first thing done, when a
 *	time period begins, is the game state is drawn.  Anything that is not a 
 *	draw operation is performed after.  Two copies of the game state are
 *	stored - the last and the next states.  This is useful for HardBall because
 *	if a ball becomes stuck in an object it is bounced from it's last position.
 *	After the game state is drawn the game state is advanced - the next state
 *	is moved to the last state.  Next, changes while the time period elaspses
 *	are made to form a new next state.  Buttons depressed by the user are
 *	polled and responded to.  Lastly, a sound is played.  The variable time
 *	until the beginning of the next time period is free to handle system events.
 *	Also, when the prior routines take longer than average they consume part
 *	of the variable time.  If Graffiti characters were used, the recognition
 *	should happen during the variable time and the effect should be handled 
 *	when the next period elapses.
 *	
 *	
 *	The flow of a time period T:
 *	
 *	appStartEvent
 *		
 *		GameStatus.status = gameInitializing	// suspend updates
 *	
 *	frmOpenEvent	
 *	
 *		GameStart
 *			GameInitLevel
 *		GameStateDraw
 *	
 *	0T
 *	
 *		GameBallAdd
 *	
 *	nT
 *	
 *		GameStateDrawChanges			// erase last, draw next
 *		GameStateAdvance				// move next over last
 *		GameStateElapse				// calc new next, respond to pressed buttons
 *		GamePlaySounds					// play a sound
 *		variable time
 *	
 *	nT + T
 *	
 *	
 *	
 *	Things to change
 *	
 *	1) Add bricks that require multiple	bounces to break.  This is primarily
 * to assist the creation of interesting levels.  At this point, there are 
 * enough levels (few people have played them all) so this idea is not as 
 * useful and it will complicate the code. Eventually, it will probably be 
 * worthwhile to do because everyone will tire of the standard blocks.
 *	
 *	2) A interesting change to the code would be to change the ball motion
 *	description from degrees to a vector.  Degrees seemed simpler
 *	than they were.  A vector would also allow balls to move at different 
 *	speeds.
 *
 *
 *
 * Copy Permissions
 *	
 *	This game and its code are considered freeware.  It may be freely copied  
 *	and distributed. Please change the about box if the games has changed from
 *	the original design. Also, please send a copy of the source and game to
 * devsupp@palm.com.
 *
 *
 *
 * NOTE: Before using this code for a game, check out the code for SubHunt,
 *     which demonstrates moving many objects at the same time.
 *
 *
 *
 **********************************************************************/

#include <Pilot.h>
#include <Graffiti.h>
#include <KeyMgr.h>
#include <SystemMgr.rh>			//	Needed for user name
#include	<DLServer.h>			//	Needed for user name
#include <FeatureMgr.h>			//	Needed to detect ROM versions


#include "HardBallRsc.h"

/***********************************************************************
 *
 *	Entry Points
 *
 ***********************************************************************/
#define appFileCreator	'hbdk'
#define appPrefID				0
#define appSavedGameID		0
#define appPrefVersion				2
#define appSavedGameVersion		2

#define version30					0x03000000

/***********************************************************************
 *
 *	Constants 
 *
 ***********************************************************************/

#define firstLevelPlayed			0
#define ballsPerGame					3

// List of key bindings
#define moveLeftKey			keyBitHard1		// polled every game period
#define moveLeftKeyAlt		keyBitHard2		// polled every game period
#define moveRightKey			keyBitHard4		// polled every game period
#define moveRightKeyAlt		keyBitHard3		// polled every game period
#define releaseBallChr		pageUpChr
#define restartGameChar		pageDownChr

// List of bitmaps
#define paddleBitmap					0
#define ballBitmap					1
#define solid1BrickBitmap			2
#define solid2BrickBitmap			3
#define solid3BrickBitmap			4
#define unbreakableBrickBitmap	5
#define ballBrickBitmap				6
#define sidePaddleBrickBitmap		7
#define bitmapTypeCount				8

#define firstPaddleBitmap	paddleBitmap
#define firstBallBitmap		ballBitmap
#define firstBrickBitmap	solid1BrickBitmap

#define normalPaddle			0
#define normalBall			0


// Board settings
#define boardTopLeftX	4
#define boardTopLeftY	20
#define boardWidth		(160-8)		// width of screen less width of border graphic
#define boardHeight		140

#define boardBmpXOffset	-4				// The border graphics starts outside the board.
#define boardBmpYOffset	-4


// Title bar displays

// Ball gauge position
#define ballGaugeX			53
#define ballGaugeSeparator	2
#define ballGaugeY			4
#define ballsDisplayable	5

// Score gauge position
#define scoreX					130
#define scoreY					2
#define maxScoreDigits 		5
#define maxScoreDisplayed 	100000


// Level Names
#define levelNameFont			largeFont
#define levelNameLengthMax		16


// Brick settings
#define columnsOfBricks	9
#define rowsOfBricks		11
#define brickStartRow	1
#define brickEndRow		7
#define brickWidth		16
#define brickHeight		9
#define brickMortarThickness	1
#define brokenBricksMax	10 			// 2 * ballsMax + fudge

// Ball settings
#define ballWidth				7
#define halfBallWidth		((ballWidth + 1) / 2)
#define ballAmountNeededToBreakABrick	2
#define ballHeight			7
#define halfBallHeight		((ballHeight + 1) / 2)
#define ballsMax				4
#define ballTrappedInLoopThreshold	12
#define firstBonusBallAwardedAtScore	2000

// Paddle settings
#define paddleWidth		26
#define paddleHeight		5
#define paddlesMax		2
#define paddleHorizontalSpacing	paddleWidth		// space between side by side paddles
#define paddleMovement	4			// faster than a ball

// List of all the degrees
#define degrees0			0
#define degrees22			1
#define degrees45			2
#define degrees67			3
#define degrees90			4
#define degrees112		5
#define degrees135		6
#define degrees157		7
#define degrees180		8
#define degrees202		9
#define degrees225		10
#define degrees247		11
#define degrees270		12
#define degrees292		13
#define degrees315		14
#define degrees337		15
#define degrees360		16
#define degreesMax		16

// List of surfaces
#define surfaceNone		degreesMax
#define surfaceTop		degrees0
#define surfaceRight		degrees90
#define surfaceBottom	degrees180
#define surfaceLeft		degrees270
#define surfaceHorizontal		surfaceTop
#define surfaceVertical			surfaceRight


// Motion of the surface (paddle) collided into
#define noMotion			1
#define leftMotion		2
#define rightMotion		3


#if EMULATION_LEVEL != EMULATION_NONE
#define defaultPeriodLength	3				// the emulator is slow, run as fast as possible
#else
#define defaultPeriodLength	4
#endif
#define minPeriodLength	1

// Various time intervals
#define levelOverTimeInterval	(4 * 60)
#define gameOverTimeInterval	(2 * 60)			// time to pause after game over and before high scores
#define pauseLengthBeforeResumingSavedGame			(3 * sysTicksPerSecond)
#define pauseLengthBeforeResumingInterruptedGame	(3 * sysTicksPerSecond)
#define pauseLengthToDisplayLevelName					((2 * sysTicksPerSecond) / defaultPeriodLength)


// High Scores settings
#define highScoreFont				stdFont
#define firstHighScoreY				28
#define highScoreHeight				12
#define highScoreNameColumnX		17
#define highScoreScoreColumnX		119		// Right aligned
#define highScoreLevelColumnX		149		// Right aligned
#define nameLengthMax	15
#define highScoreMax		9

// Preferences UI settings
#define startLevelsSelectable			3	
#define defaultStartLevelItem			0


/***********************************************************************
 *
 *	Internal Structures
 *
 ***********************************************************************/

// List of possible sounds
typedef enum 
	{
	noSound, 
	brickBreak, 
	brickNoBreak, 
	paddleBounce, 
	paddleSpinBounce, 
	wallBounce, 
	playBall, 
	speedBall, 
	extraBall, 
	extraPaddle,
	bonusBall, 
	newHighScore,
	soundTypeCount
	} SoundType;

typedef struct
	{
	Byte priority;
	Byte periods;
	Long frequency;
	UInt duration;
	} SoundInfo;

typedef struct
	{
	Word initDelay;
	Word period;
	Word doubleTapDelay;
	Boolean queueAhead;
	} KeyRateType;

enum gameProgress 
	{
	gameResuming, 			// don't draw or change the game state.  Do resume the save game.
	gameInitializing, 	// don't draw or change the game state
	presentingLevel,		// display level name for brief period.  Go to waitingForBall upon user input.
	waitingForBall, 		// advance and draw the game state (allows paddle to move)
	ballInMotion, 	 		// advance and draw the game state
	levelOver, 	 			// pause, start new level, and go to waitingForBall
	gameOver,				// don't draw or change the game state, pause before high score check
	checkHighScores		// check for high score, get name if high score.
	};


typedef enum 
	{
	empty, 
	brick1, brick2, brick3, 	// adds points to the score
	unbreakable, 					// This brick doesn't break
	ballBrick, 						// releases a ball
	sidePaddleBrick, 				// adds a side paddle
	brickTypeCount
	} BrickType;

typedef struct 
	{
	char					name[levelNameLengthMax];
	BrickType			brick[rowsOfBricks][columnsOfBricks];
	} LevelType;
	
typedef struct 
	{
	AbsRectType			sides;
	Boolean				usable;
	Boolean				changed;
	Byte					type;
	Byte					heading;
	Byte					bouncesWithoutBreakingABrick;	// used to detect unescapeable patterns.
	} ObjectType;

typedef struct 
	{
	Long						score;
	ObjectType				paddle[paddlesMax];
	ObjectType				ball[ballsMax];
	} WorldState;

// Removed bricks are bricks broken during the game period
typedef struct 
	{
	Int						row;
	Int						column;
	} RemovedBrick;

typedef struct 
	{
	enum gameProgress		status;
	Byte						periodLength;		// duration of period in ticks
	ULong						nextPeriodTime;	// time when next period occurs
	ULong						periodsToWait;		// time until something should happen
	Boolean					paused;				// indicates that time should not pass
	ULong						pausedTime;			// Used to 
	BrickType				brick[rowsOfBricks][columnsOfBricks];
	Byte						bricksRemaining;	// bricks remaining this level 
														// unbreakable bricks are ignored
	Byte						level;				// controls the brick layout
	WorldState				last;					// world last drawn
	WorldState				next;					// world to be drawn
	RemovedBrick			brokenBricks[brokenBricksMax];	// bricks to erase
	Int						brokenBricksCount;
	Byte						ballsRemaining;	// balls remaining this game
	Boolean					movePaddleLeft;	// paddle moved and value cleared during GameStateElapse
	Boolean					movePaddleRight;	// paddle moved and value cleared during GameStateElapse
	SoundType				soundToMake;		// one sound can be made per game period
	SByte						soundPeriodsRemaining;	// times to repeat the sound
	Long						scoreToAwardBonusBall;	// reaching this score awards an extra ball
	Boolean					lowestHighScorePassed;	// User beat the lowest high score
	Boolean					highestHighScorePassed;	// User beat the highest high score
	Boolean					gameSpedUp;			// reduces period time.  True if ball touches top wall
	Boolean					cheatMode;			// if true don't accept high score
	ULong						startTime;			// Time since starting HardBall
	} GameStatusType;


typedef struct
	{
	char						name[nameLengthMax + 1];
	Long						score;
	Int						level;
	} SavedScore;

typedef struct
	{
	SavedScore				highScore[highScoreMax];
	Byte						lastHighScore;
	Byte						startLevel;			// the level to start at
	ULong						accumulatedTime;	// Total time spent by player playing HardBall
	} HardBallPreferenceType;



/***********************************************************************
 *
 *	Global variables
 *
 ***********************************************************************/

#include "HardBallLevels.h"		// common settings are separated for ease of changing

static GameStatusType		GameStatus;
static Handle					ObjectBitmapHandles[bitmapTypeCount];
static BitmapPtr				ObjectBitmapPtr[bitmapTypeCount];
static WinHandle				ObjectWindowHandles[bitmapTypeCount];

static enum gameProgress	SavedGameStatus;


static Int						MovementX[degreesMax] = {
	2, 2, 2, 1, 0, -1, -2, -2, -2, -2, -2, -1, 0, 1, 2, 2
	};
static Int						MovementY[degreesMax] = {
	0, -1, -2, -2, -2, -2, -2, -1, 0, 1, 2, 2, 2, 2, 2, 1
	};

#define startHeadingsCount 4
static Byte BallStartHeadings[startHeadingsCount] = {
	degrees45, degrees67, degrees112, degrees135
	};

static Byte BallReflections[2][degreesMax] = 
	{
		// List the horizontal reflections first
		{
		degrees180, degrees337, degrees315, degrees292,
		degrees270, degrees247, degrees225, degrees202,
		degrees0, degrees157, degrees135, degrees112,
		degrees90, degrees67, degrees45, degrees22
		},
		// List the vertical reflections second
		{
		degrees180, degrees157, degrees135, degrees112,
		degrees270, degrees67, degrees45, degrees22,
		degrees0, degrees337, degrees315, degrees292,
		degrees90, degrees247, degrees225, degrees202
		}
	};
		
static UInt SoundAmp;		// default sound amplitude

static SoundInfo  Sound[soundTypeCount] = 
	{
		{0,	0,		0, 	0},			// no sound
		{30,	1,		200,	30},			// break brick
		{20,	1,		160,	50},			// no break brick
		{40,	1,		320,	30},			// paddle bounce
		{50,	1,		480,	50},			// paddle spin
		{10,	1,		180,	40},			// wall bounce
		{60,	1,		550,	60},			// play a ball
		{80,	4,		760,	50},			// speed up ball
		{60,	3,		550,	50},			// add a ball
		{70,	4,		860,	50},			// add a paddle
		{90,	6,		3740,	20},			// extra ball awarded
		{90,	8,		2500,	20},			// high score passed
	};
		

// The original values for key rates.		
KeyRateType KeyRate;

// Mappings for the start level UI
Byte StartLevelMappings[startLevelsSelectable] =
	{
	0, 5, 10
	};

// Used by the Preference Dialog
Byte NewStartLevel;
		
// The following global variable are saved to a state file.

// Scores
HardBallPreferenceType		Prefs;


/***********************************************************************
 *
 *	Macros
 *
 ***********************************************************************/

#define noItemSelection			-1


#define BrickX(c)					((c) * (brickWidth + brickMortarThickness))
#define BrickY(r)					((r) * (brickHeight + brickMortarThickness))
#define BrickAtX(c) 				((c) / (brickWidth + brickMortarThickness))
#define BrickAtY(r) 				((r) / (brickHeight + brickMortarThickness))
#define BrickExists(c, r)  	((r < rowsOfBricks) && (c < columnsOfBricks) && \
										(GameStatus.brick[r][c] != empty))

#define ScoreForBrick(r, c) 	(BrickScores[GameStatus.brick[r][c]])

#define headingVertically(h)	((h >= degrees45 && h <= degrees135) || \
										(h >= degrees225 && h <= degrees315))

#define HeadingDown(h)			(degrees180 < (h)  && (h) < degrees360)
#define HeadingUp(h)				(degrees0 < (h)  && (h) < degrees180)
#define HeadingLeft(h)			(degrees90 < (h)  && (h) < degrees270)
#define HeadingRight(h)			(degrees270 < (h)  || (h) < degrees90)
#define GetPaddleBitmap(t)		((t) + firstPaddleBitmap)
#define GetBallBitmap(t)		((t) + firstBallBitmap)
#define GetBrickBitmap(t)		((t) - 1 + firstBrickBitmap)

#define randN(N)	((((long) SysRandom (TimGetTicks())) * N) / ((long) sysRandomMax + 1))


/***********************************************************************
 *
 *	Internal Functions
 *
 ***********************************************************************/
static void GameStart ();
static void GameDrawBallGauge (void);
static void HighScoresAddScore (CharPtr name, Long score, Int level, 
	Boolean dontAddIfExists);
static void HighScoresCheckScore (void);


/***********************************************************************
 *
 * FUNCTION:    MenuGetVisible
 *
 * DESCRIPTION: Return if the menu is visible
 *   This call should exist in the api but wasn't included.  It is added
 *   here for now.
 *
 * PARAMETERS:	 nothing
 *
 * RETURNED:	 true if the menu is visible
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/31/96	Initial Revision
 *
 ***********************************************************************/
static Boolean MenuGetVisible()
{
	if (UICurrentMenu)
		return UICurrentMenu->attr.visible;
	
	return false;
}


/***********************************************************************
 *
 * FUNCTION:     TimeUntillNextPeriod
 *
 * DESCRIPTION:  Return the time until the next world advance.
 *
 * PARAMETERS:   nothing
 *
 * RETURNED:     system ticks until the next world advance.
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/24/95	Initial Revision
 *
 ***********************************************************************/
static Long TimeUntillNextPeriod (void)
{
	Long timeRemaining;
	
	
	if (GameStatus.status == gameInitializing || 
		GameStatus.status == gameResuming || 
		GameStatus.status == checkHighScores ||
		GameStatus.paused)
		return evtWaitForever;
		
		
	timeRemaining = GameStatus.nextPeriodTime - TimGetTicks();
	if (timeRemaining < 0)
		timeRemaining = 0;
		
	return timeRemaining;
}


/***********************************************************************
 *
 * FUNCTION:     StartApplication
 *
 * DESCRIPTION:  This routine sets up the application.  It gets the 
 * system volume preferences, loads in the graphics used, gets the high
 * scores or enters default ones, resumes any saved game or starts a new
 * one, plus other startup details.
 *
 * PARAMETERS:   nothing
 *
 * RETURNED:     true if there is an error starting the application.
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	4/6/95	Initial Revision
 *
 ***********************************************************************/
static Word StartApplication (void)
{
	int i;
	WinHandle oldDrawWinH;
	Word error;
	Word prefsSize;

	
	// Get SoundAmp for the sound code.  If game sounds are desired use
	// the default sound else set the volume to zero to turn it off.
	SoundAmp = PrefGetPreference(prefGameSoundVolume);


	// Get the key repeat rate for when we want to restore it after the game.
	KeyRates(false, &KeyRate.initDelay, &KeyRate.period, &KeyRate.doubleTapDelay, 
		&KeyRate.queueAhead);
	
	
	// Keep the Object graphics locked because they are frequently used
	oldDrawWinH = WinGetDrawWindow();
	for (i = 0; i < bitmapTypeCount; i++)
		{
		ObjectBitmapHandles[i] = DmGetResource( bitmapRsc, firstObjectBmp + i);
		ObjectBitmapPtr[i] = MemHandleLock(ObjectBitmapHandles[i]);
		
		// It is actually faster and more versatile to store the graphics
		// as window images.  It is faster because WinDrawBitmap constructs a
		// window from the bitmap on the fly before drawing.  It is more
		// versatile because when the window is copied to the screen a 
		// screen copy mode like scrCopyNot can be used.  This makes
		// images masks possible.
		// We can do this as long as there is enough memory free in the dynamic
		// ram.  We don't do this to large images.
		if (i == 999)			// don't skip any bitmaps we use
			{
			ObjectWindowHandles[i] = 0;
			}
		else
			{
			ObjectWindowHandles[i] = WinCreateOffscreenWindow(
				ObjectBitmapPtr[i]->width, ObjectBitmapPtr[i]->height,
				screenFormat, &error);
			ErrFatalDisplayIf(error, "Error loading images");
			WinSetDrawWindow(ObjectWindowHandles[i]);
			WinDrawBitmap(ObjectBitmapPtr[i], 0, 0);
			}
		
		}
	WinSetDrawWindow(oldDrawWinH);
	
	
	// Restore the app's preferences.
	prefsSize = sizeof (HardBallPreferenceType);
	if (PrefGetAppPreferences (appFileCreator, appPrefID, &Prefs, &prefsSize, 
		true) == noPreferenceFound)
		{
		// There aren't any preferences
		
		// Clear the high scores.
		for (i = 0; i < highScoreMax; i++)
			{
			Prefs.highScore[i].name[0] = '\0';
			Prefs.highScore[i].score = 0;
			Prefs.highScore[i].level = 1;
			}
		
		// Add Best Score
		HighScoresAddScore ("Rocket Boy", 7236, 11, true);
		HighScoresAddScore ("Briester", 2415, 5, true);
		HighScoresAddScore ("the Jode", 2900, 5, true);
		HighScoresAddScore ("Mr. P", 9825, 17, true);

		// No last high score
		Prefs.lastHighScore = highScoreMax;
		
		// Begin at the first level.
		Prefs.startLevel = firstLevelPlayed;
		
		// No time has been recorded.
		Prefs.accumulatedTime = 0;
		}
	
	
	// Restore a saved game.  Games are kept in the unsaved preference database.
	prefsSize = sizeof (GameStatus);
	if (PrefGetAppPreferences (appFileCreator, appSavedGameID, &GameStatus, 
		&prefsSize, false) == noPreferenceFound)
		{
		// Initialize this now so that the GetNextEvent wait time is set properly.	
		GameStatus.status = gameInitializing;		// don't draw yet!
		
		
		// Now display the about box and instructions.  This appears automatically
		// the first time the program is run.
#ifndef DETERMINISTIC_PLAY
//		InfoDisplay();
		FrmHelp (InstructionsStr);
#endif
		}
	else
		{
		if (GameStatus.status != checkHighScores)
			{
			SavedGameStatus = GameStatus.status;
			GameStatus.status = gameResuming;		// don't draw yet!
			}
		}


	// Record the start time of this game session.
	GameStatus.startTime = TimGetTicks();

	// Initialize the random number generator
	SysRandom (GameStatus.startTime);
	
	return 0;		// no error
}


/***********************************************************************
 *
 * FUNCTION:    StopApplication
 *
 * DESCRIPTION: Release the graphics and save the high scores, time played,
 * and any game in progress.
 *
 * PARAMETERS:  nothing
 *
 * RETURNED:    nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	4/6/95	Initial Revision
 *
 ***********************************************************************/
static void StopApplication (void)
{
	int i;
	
	
	// Unlock and release the locked bitmaps
	for (i = 0; i < bitmapTypeCount; i++)
		{
		MemPtrUnlock(ObjectBitmapPtr[i]);
		DmReleaseResource(ObjectBitmapHandles[i]);

		if (ObjectWindowHandles[i]) 
			WinDeleteWindow(ObjectWindowHandles[i], false);
		}
	
	
	// Update the time accounting.
	Prefs.accumulatedTime += (TimGetTicks() - GameStatus.startTime);
	
	// If we are saving a game resuming (it hasn't started playing yet)
	// then preserve the game status.
	if (GameStatus.status == gameResuming)
		{
		GameStatus.status = SavedGameStatus;
		}
	
	
	// Perform error checking on the saved data
	ErrNonFatalDisplayIf(Prefs.startLevel > StartLevelMappings[startLevelsSelectable - 1], "Bad start level");
	
	
	PrefSetAppPreferences (appFileCreator, appPrefID, appPrefVersion, 
		&Prefs, sizeof (Prefs), true);

	PrefSetAppPreferences (appFileCreator, appSavedGameID, appSavedGameVersion, 
		&GameStatus, sizeof (GameStatus), false);
	
}


/***********************************************************************
 *
 * FUNCTION:    RomVersionCompatible
 *
 * DESCRIPTION: This routine checks that a ROM version meets your
 *              minimum requirement.
 *
 * PARAMETERS:  requiredVersion - minimum rom version required
 *                                (see sysFtrNumROMVersion in SystemMgr.h 
 *                                for format)
 *              launchFlags     - flags that indicate if the application 
 *                                UI is initialized.
 *
 * RETURNED:    error code or zero if rom is compatible
 *                             
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			art	11/15/96	Initial Revision
 *
 ***********************************************************************/
static Err RomVersionCompatible (DWord requiredVersion, Word launchFlags)
{
	DWord romVersion;

	// See if we have at least the minimum required version of the ROM or later.
	FtrGet(sysFtrCreator, sysFtrNumROMVersion, &romVersion);
	if (romVersion < requiredVersion)
		{
		if ((launchFlags & (sysAppLaunchFlagNewGlobals | sysAppLaunchFlagUIApp)) ==
			(sysAppLaunchFlagNewGlobals | sysAppLaunchFlagUIApp))
			{
			FrmAlert (RomIncompatibleAlert);
		
			// Pilot 1.0 will continuously relaunch this app unless we switch to 
			// another safe one.
			if (romVersion < 0x02000000)
				AppLaunchWithCommand(sysFileCDefaultApp, sysAppLaunchCmdNormalLaunch, NULL);
			}
		
		return (sysErrRomIncompatible);
		}

	return (0);
}


/***********************************************************************
 *
 * FUNCTION:    GetObjectPtr
 *
 * DESCRIPTION: This routine returns a pointer to an object in the current
 *              form.
 *
 * PARAMETERS:  objectID - id of the object to get
 *
 * RETURNED:    pointer to the object
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	4/6/95	Initial Revision
 *
 ***********************************************************************/
static VoidPtr GetObjectPtr (Word objectID)
{
	FormPtr frm;
	VoidPtr obj;
	
	frm = FrmGetActiveForm ();
	obj = FrmGetObjectPtr (frm, FrmGetObjectIndex (frm, objectID));

	return obj;
}


/***********************************************************************
 *
 * FUNCTION:    MapToPosition
 *
 * DESCRIPTION:	Map a value to it's position in an array.  If the passed
 *						value is not found in the mappings array, a default
 *						mappings item will be returned.
 *
 * PARAMETERS:  value	- value to look for
 *
 * RETURNED:    position value found in
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	11/21/96	Initial Revision
 *
 ***********************************************************************/
static UInt MapToPosition (Byte *mappingArray, Byte value,
									UInt mappings, UInt defaultItem)
{
	UInt i;
	
	i = 0;
	while (mappingArray[i] != value && i < mappings)
		i++;
	if (i >= mappings)
		return defaultItem;

	return i;
}


/***********************************************************************
 *
 * FUNCTION:     GameMaskKeys
 *
 * DESCRIPTION:  Mask the keys to reduce keyDownEvents from being sent.
 * This saves time.
 *
 * PARAMETERS:   nothing
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	11/25/96	Initial Revision
 *
 ***********************************************************************/
static void GameMaskKeys ()
{
	Word initDelay;
	Word period;
	Boolean queueAhead;
	
	
	// Set the keys we poll to not generate events.  This saves cpu cycles.
	KeySetMask(	~(moveLeftKey | moveLeftKeyAlt | moveRightKey | moveRightKeyAlt) );
	
	// Avoid the code below because KeyRates is broken.
	return;
	
	// Also set the key repeat rate low to avoid constantly checking them.
	initDelay = slowestKeyDelayRate;
	period = slowestKeyPeriodRate;
	queueAhead = false;
	KeyRates(true, &initDelay, &period, &period, &queueAhead);
}


/***********************************************************************
 *
 * FUNCTION:     GameUnmaskKeys
 *
 * DESCRIPTION:  Unmask the keys.
 *
 * PARAMETERS:   nothing
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	11/25/96	Initial Revision
 *
 ***********************************************************************/
static void GameUnmaskKeys ()
{
	// Set the keys we poll to not generate events.  This saves cpu cycles.
	KeySetMask(	keyBitsAll );
	
	// Avoid the code below because KeyRates is broken.
	return;
	
	// Also set the key repeat rate low to avoid constantly checking them.
	KeyRates(true, &KeyRate.initDelay, &KeyRate.period, &KeyRate.doubleTapDelay, 
		&KeyRate.queueAhead);
}


/***********************************************************************
 *
 * FUNCTION:		DrawBitmap
 *
 * DESCRIPTION:	Get and draw a bitmap at a specified location
 *
 * PARAMETERS:	resID		-- bitmap resource id
 *					x, y		-- bitmap origin relative to current window
 *
 * RETURNED:	nothing.
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			vmk	10/9/95	Initial Revision
 *
 ***********************************************************************/
static void DrawBitmap(Int resID, Short x, Short y)
{
	Handle		resH;
	BitmapPtr	resP;


	resH = DmGetResource( bitmapRsc, resID );
	ErrFatalDisplayIf( !resH, "Missing bitmap" );
	resP = MemHandleLock(resH);
	WinDrawBitmap (resP, x, y);
	MemPtrUnlock(resP);
	DmReleaseResource( resH );
}


/***********************************************************************
 *
 * FUNCTION:		DrawObject
 *
 * DESCRIPTION:	Draw an object at a specified location and mode
 *
 * PARAMETERS:	bitmapNumber -- bitmap number
 *					x, y		-- bitmap origin relative to current window
 *					mode		-- transfer mode (scrANDNOT for masks)
 *
 * RETURNED:	nothing.
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	2/29/96	Initial Revision
 *
 ***********************************************************************/
static void DrawObject(Int bitmapNumber, Short x, Short y, ScrOperation mode)
{
	RectangleType srcR;


	ErrFatalDisplayIf (ObjectWindowHandles[bitmapNumber] == 0, "Unhandled object image");
	// Copy the entire source window.
	MemMove (&srcR, &(ObjectWindowHandles[bitmapNumber]->windowBounds), sizeof(RectangleType));

	// Copy the source window (contains the image to draw) to the draw window.
	WinCopyRectangle(ObjectWindowHandles[bitmapNumber], 0, &srcR, x, y, mode);
}


/***********************************************************************
 *
 * FUNCTION:     GameRequestSound
 *
 * DESCRIPTION:  Setup to play a game sound. Sound will be played unless
 *				a higher priority sound is already requested.
 *
 * PARAMETERS:   nothing
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/30/96	Initial Revision
 *
 ***********************************************************************/
static void GameRequestSound (SoundType sound)
{
	if (Sound[sound].priority >= Sound[GameStatus.soundToMake].priority)
		{
		GameStatus.soundToMake = sound;
		GameStatus.soundPeriodsRemaining = Sound[sound].periods;
		}
}


/***********************************************************************
 *
 * FUNCTION:     IncreaseScore
 *
 * DESCRIPTION:  Increase the score by some amount
 *
 * PARAMETERS:   score - the amount to add to the score.
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	3/20/96	Initial Revision
 *
 ***********************************************************************/
static void IncreaseScore (Long score)
{
	GameStatus.next.score += score;
	
	// Beep if the user is setting a new high score. Don't beep if not
	// all the high scores were already set (it's annoying).
	if (!GameStatus.lowestHighScorePassed &&
		GameStatus.next.score > Prefs.highScore[highScoreMax - 1].score &&
		Prefs.highScore[highScoreMax - 1].score > 0)
		{
		GameStatus.lowestHighScorePassed = true;
		if (GameStatus.next.score > 0)
			GameRequestSound (newHighScore);
		}


	// Beep if the user is setting the highest score.
	if (!GameStatus.highestHighScorePassed &&
		GameStatus.next.score > Prefs.highScore[0].score &&
		Prefs.highScore[0].score > 0)
		{
		GameStatus.highestHighScorePassed = true;
		if (GameStatus.next.score > 0)
			GameRequestSound (newHighScore);
		}


	// Beep if the user is awarded an extra ball.
	if (GameStatus.next.score >= GameStatus.scoreToAwardBonusBall)
		{
		GameStatus.scoreToAwardBonusBall *= 2;
		
		// Add a ball to those remaining and update the ball gauge.
		GameStatus.ballsRemaining++;
		GameDrawBallGauge();

		GameRequestSound (bonusBall);
		}

}


/***********************************************************************
 *
 * FUNCTION:     PaddleAddSidePaddle
 *
 * DESCRIPTION:  Add a paddle to play
 *
 * PARAMETERS:   nothing
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	2/1/96	Initial Revision
 *
 ***********************************************************************/

static void PaddleAddSidePaddle ()
{
	Boolean paddlePaddleOnRightSide;
	
	
	GameStatus.next.paddle[1].usable = true;
	GameStatus.next.paddle[1].changed = true;
	GameStatus.next.paddle[1].type = normalPaddle;
	
	// Place at the same height as the first paddle
	GameStatus.next.paddle[1].sides.top = GameStatus.next.paddle[0].sides.top;
	GameStatus.next.paddle[1].sides.bottom = GameStatus.next.paddle[0].sides.bottom;
	

	paddlePaddleOnRightSide = GameStatus.next.paddle[0].sides.left + 
		(GameStatus.next.paddle[0].sides.right - GameStatus.next.paddle[0].sides.left)
		> boardWidth / 2;
	if (paddlePaddleOnRightSide)
		{
		// Add the next one of the left side
		GameStatus.next.paddle[1].sides.left = GameStatus.next.paddle[0].sides.left -
			paddleHorizontalSpacing - paddleWidth;
		}
	else
		{
		// Add the next one of the right side
		GameStatus.next.paddle[1].sides.left = GameStatus.next.paddle[0].sides.right +
			paddleHorizontalSpacing;
		}
	
	GameStatus.next.paddle[1].sides.right = GameStatus.next.paddle[1].sides.left + paddleWidth;
}


/***********************************************************************
 *
 * FUNCTION:     ResetPaddles
 *
 * DESCRIPTION:  Resets the paddle to only one showing
 *
 * PARAMETERS:   nothing
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	2/1/96	Initial Revision
 *
 ***********************************************************************/

static void ResetPaddles ()
{
	int i;
	
	
	// Reset the paddles
	for (i = paddlesMax - 1; i >= 0; i--)
		{
		// Allow only one paddle
		if (i > 0)
			{
			// If was usable mark it changed so it erases
			if (GameStatus.next.paddle[i].usable)
				GameStatus.next.paddle[i].changed = true;
			GameStatus.next.paddle[i].usable = false;
			
			}
		}
}


/***********************************************************************
 *
 * FUNCTION:     BallAdd
 *
 * DESCRIPTION:  Add a ball to play
 *
 * PARAMETERS:   type - type of ball (i.e. normalBall)
 *					  left - left bounds of the ball
 *					  top - top bounds of the ball
 *					  heading - heading of the ball (i.e. degrees45)
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/23/96	Initial Revision
 *
 ***********************************************************************/

static void BallAdd (Byte type, Int left, Int top, Byte heading)
{
	int i;
	
	
	// Find an unused ball
	i = 0;
	while (GameStatus.next.ball[i].usable && i < ballsMax)
		i++;
	
	
	if (i < ballsMax)
		{
		// Set the ball up to draw
		GameStatus.next.ball[i].usable = true;
		GameStatus.next.ball[i].changed = true;
		GameStatus.next.ball[i].type = type;
		GameStatus.next.ball[i].heading = heading;
		GameStatus.next.ball[i].bouncesWithoutBreakingABrick = 0;
		
		// place the ball in the bottom center
		GameStatus.next.ball[i].sides.left = left;
		GameStatus.next.ball[i].sides.top = top;
		GameStatus.next.ball[i].sides.right = left + ballWidth;
		GameStatus.next.ball[i].sides.bottom = top + ballHeight;
		}
	else
		{
		// The user should receive a ball but there isn't a spot for it.
		// Add the ball to the ballsRemaining.
		GameStatus.ballsRemaining++;
		GameDrawBallGauge();
		}
}


/***********************************************************************
 *
 * FUNCTION:     BallRemove
 *
 * DESCRIPTION:  Remove a ball from play.  Ends the game if no balls 
 * remain.
 *
 * PARAMETERS:   ballNumber - which ball to remove
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/30/96	Initial Revision
 *
 ***********************************************************************/

static void BallRemove (Int ballNumber)
{
	int i;
	
	
	if (GameStatus.next.ball[ballNumber].usable)
		{
		GameStatus.next.ball[ballNumber].usable = false;
		GameStatus.next.ball[ballNumber].changed = true;
		
		
		
		// Find a used ball
		i = 0;
		while (!GameStatus.next.ball[i].usable && i < ballsMax)
			i++;
		
		// If no ball is found either wait for a ball if other balls
		// remain or declare the game over.
		if (i >= ballsMax)
			{
			if (GameStatus.ballsRemaining > 0)
				{
				GameStatus.status = waitingForBall;
				
				// Now loose any special bonuses
				ResetPaddles();

				// Cancel any speed up
				if (GameStatus.gameSpedUp)
					{
					GameStatus.periodLength++;
					GameStatus.gameSpedUp = false;
					}
				}
			else
				{
				GameStatus.status = gameOver;
				GameStatus.nextPeriodTime += gameOverTimeInterval;
				}
			}

		}

}


/***********************************************************************
 *
 * FUNCTION:     BallMove
 *
 * DESCRIPTION:  Move a ball from it's last position to it's next position.
 *
 * PARAMETERS:   ballNumber - the ball to move
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/24/96	Initial Revision
 *
 ***********************************************************************/
static void BallMove (Int ballNumber)
{
	ErrFatalDisplayIf(GameStatus.next.ball[ballNumber].heading >= degreesMax, 
		"Bad heading");
	GameStatus.next.ball[ballNumber].sides.left = GameStatus.last.ball[ballNumber].sides.left +
		MovementX[GameStatus.next.ball[ballNumber].heading];
	GameStatus.next.ball[ballNumber].sides.top = GameStatus.last.ball[ballNumber].sides.top +
		MovementY[GameStatus.next.ball[ballNumber].heading];
	GameStatus.next.ball[ballNumber].sides.right = GameStatus.last.ball[ballNumber].sides.right +
		MovementX[GameStatus.next.ball[ballNumber].heading];
	GameStatus.next.ball[ballNumber].sides.bottom = GameStatus.last.ball[ballNumber].sides.bottom +
		MovementY[GameStatus.next.ball[ballNumber].heading];
}


/***********************************************************************
 *
 * FUNCTION:     BallReflect
 *
 * DESCRIPTION:  Reflect a ball off a surface.
 *
 * PARAMETERS:   heading - heading of the moving object
 *					  surface - the direction of the hit surface
 *					  surfaceMotion - surfaces like paddles may have motion
 *							which changes the angle a little more
 *
 * RETURNED:     the new heading
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/24/96	Initial Revision
 *			roger 1/30/96	Rewritten to use tables.
 *
 ***********************************************************************/
static Byte BallReflect (Byte heading, Byte surface, Byte surfaceMotion)
{
	Byte newHeading;
	
	
	// Lookup the new heading in the BallReflections table.  Moving
	// surfaces are accounted for later.
	if (surface == surfaceTop || surface == surfaceBottom)
		newHeading = BallReflections[surfaceHorizontal][heading];
	else
		newHeading = BallReflections[1][heading];


	// Affect the reflection angle based on the movement of the surface
	if (surfaceMotion == leftMotion)
		{
		if ((degrees45 <= newHeading && newHeading <= degrees67) ||
			(degrees135 <= newHeading && newHeading <= degrees157))
			{
			newHeading--;
			GameRequestSound (paddleSpinBounce);
			}
		else if ((degrees202 <= newHeading && newHeading <= degrees225) ||
			(degrees292 <= newHeading && newHeading <= degrees315))
			{
			newHeading++;
			GameRequestSound (paddleSpinBounce);
			}
		}
	else if (surfaceMotion == rightMotion)
		{
		if ((degrees22 <= newHeading && newHeading <= degrees45) ||
			(degrees112 <= newHeading && newHeading <= degrees135))
			{
			newHeading++;
			GameRequestSound (paddleSpinBounce);
			}
		else if ((degrees225 <= newHeading && newHeading <= degrees247) ||
			(degrees315 <= newHeading && newHeading <= degrees337))
			{
			newHeading--;
			GameRequestSound (paddleSpinBounce);
			}
		}
	
	
	return newHeading;
}


/***********************************************************************
 *
 * FUNCTION:     GameDrawBallGauge
 *
 * DESCRIPTION:  Draw the ball gauge.  Balls remaining are drawn.  Balls
 * no longer remaining are erased.
 *
 * PARAMETERS:   nothing
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/25/96	Initial Revision
 *
 ***********************************************************************/

static void GameDrawBallGauge (void)
{
	int i;
	RectangleType bounds;


	// Draw some of the balls remaining	
	for (i = 0; i < ballsDisplayable; i++)
		{
		if (GameStatus.ballsRemaining > i)
			{
			WinDrawBitmap (ObjectBitmapPtr[firstBallBitmap], 
				ballGaugeX + i * (ballWidth + ballGaugeSeparator), ballGaugeY);
			}
		else
			{
			bounds.topLeft.x = ballGaugeX + i * (ballWidth + ballGaugeSeparator);
			bounds.topLeft.y = ballGaugeY;
			bounds.extent.x = ballWidth;
			bounds.extent.y = ballHeight;
			WinEraseRectangle(&bounds, 0);
			}
		}
}


/***********************************************************************
 *
 * FUNCTION:     GameDrawScoreGauge
 *
 * DESCRIPTION:  Draw the score gauge given a score to display.
 *
 * PARAMETERS:   score - the score to display
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	2/12/96	Initial Revision - code clean up
 *
 ***********************************************************************/
static void GameDrawScoreGauge (Long score)
{
	char scoreText[maxScoreDigits + 1];
	FontID currFont;


	// Draw the score
	if (score > 0)
		{
		score = score % maxScoreDisplayed;
		StrIToA(scoreText, score);
		}
	else
		{
		// Write numeric spaces to remove old score
		MemSet(scoreText, maxScoreDigits, numericSpaceChr);			
		scoreText[0] = '0';
		scoreText[maxScoreDigits - 1] = '\0';
		}
	currFont = FntSetFont(boldFont);
	WinDrawChars (scoreText, StrLen(scoreText), scoreX, scoreY);
	FntSetFont(currFont);
}


/***********************************************************************
 *
 * FUNCTION:     GameStateDraw
 *
 * DESCRIPTION:  Redraw the world.  Everything in the last world is 
 * is erased and redrawn
 *
 * PARAMETERS:   nothing
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/23/96	Initial Revision
 *
 ***********************************************************************/

static void GameStateDraw (void)
{
	int i;
	int x;
	int y;
	
	
	// Draw a blank board
	DrawBitmap(BoardBmp, 
		boardTopLeftX + boardBmpXOffset, 
		boardTopLeftY + boardBmpYOffset);


	// Draw the bricks that exist
	for (y = 0; y < rowsOfBricks; y++)
		{
		for (x = 0; x < columnsOfBricks; x++)
			{
			if (GameStatus.brick[y][x] != empty)
				{
				DrawObject (GetBrickBitmap(GameStatus.brick[y][x]), 
					boardTopLeftX + BrickX(x), boardTopLeftY + BrickY(y),
					scrCopy);
				}
			}
		}


	// Draw the paddles
	for (i = paddlesMax - 1; i >= 0; i--)
		{
		// Draw the paddle
		if (GameStatus.last.paddle[i].usable)
			{
			DrawObject (GetPaddleBitmap(GameStatus.last.paddle[i].type), 
				boardTopLeftX + GameStatus.last.paddle[i].sides.left, 
				boardTopLeftY + GameStatus.last.paddle[i].sides.top,
				scrCopy);
			}
		}


	// Draw the balls
	for (i = ballsMax - 1; i >= 0; i--)
		{
		// Draw the ball
		if (GameStatus.last.ball[i].usable)
			{
			DrawObject (GetBallBitmap(GameStatus.last.ball[i].type), 
				boardTopLeftX + GameStatus.last.ball[i].sides.left, 
				boardTopLeftY + GameStatus.last.ball[i].sides.top,
				scrCopy);
			}
		}
	
	
	GameDrawScoreGauge (GameStatus.last.score);
	GameDrawBallGauge();
}



/***********************************************************************
 *
 * FUNCTION:     GameStateDrawChanges
 *
 * DESCRIPTION:  Show the world.  Visually moves the balls and the paddle.  
 * Removes bricks.  Redraws the score.
 *
 * No changes are made to the world here.  Scores do not change, nothing 
 * moves internally.  All visual moves are the display now reflecting the 
 * changes made to the world during the last period.
 *
 * PARAMETERS:   nothing
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/22/96	Initial Revision
 *
 ***********************************************************************/

static void GameStateDrawChanges (void)
{
	RectangleType bounds;
	Int i;
	
	
	// Visually remove broken bricks
	while (GameStatus.brokenBricksCount > 0)
		{
		GameStatus.brokenBricksCount--;
		
		bounds.topLeft.x = boardTopLeftX + BrickX(GameStatus.brokenBricks[GameStatus.brokenBricksCount].column);
		bounds.topLeft.y = boardTopLeftY + BrickY(GameStatus.brokenBricks[GameStatus.brokenBricksCount].row);
		bounds.extent.x = brickWidth;
		bounds.extent.y = brickHeight;
		WinEraseRectangle(&bounds, 0);
		}

		
	// Visually move the paddles
	for (i = paddlesMax - 1; i >= 0; i--)
		{
		// Erase the old paddle
		if (GameStatus.last.paddle[i].usable &&
			GameStatus.next.paddle[i].changed)
			{
			bounds.topLeft.x = boardTopLeftX + GameStatus.last.paddle[i].sides.left;
			bounds.topLeft.y = boardTopLeftY + GameStatus.last.paddle[i].sides.top;
			bounds.extent.x = GameStatus.last.paddle[i].sides.right - GameStatus.last.paddle[i].sides.left;
			bounds.extent.y = GameStatus.last.paddle[i].sides.bottom - GameStatus.last.paddle[i].sides.top;
			WinEraseRectangle(&bounds, 0);
			}

		// Draw the new paddle
		if (GameStatus.next.paddle[i].usable &&
			GameStatus.next.paddle[i].changed)
			{
			DrawObject (GetPaddleBitmap(GameStatus.next.paddle[i].type), 
				boardTopLeftX + GameStatus.next.paddle[i].sides.left, 
				boardTopLeftY + GameStatus.next.paddle[i].sides.top,
				scrCopy);
			}
		}


	// Visually move the balls
	for (i = ballsMax - 1; i >= 0; i--)
		{
		// Erase the old ball
		if (GameStatus.last.ball[i].usable &&
			GameStatus.next.ball[i].changed)
			{
			DrawObject (GetBallBitmap(GameStatus.last.ball[i].type), 
				boardTopLeftX + GameStatus.last.ball[i].sides.left, 
				boardTopLeftY + GameStatus.last.ball[i].sides.top,
				scrANDNOT);
			}

		// Draw the new ball
		if (GameStatus.next.ball[i].usable &&
			GameStatus.next.ball[i].changed)
			{
			DrawObject (GetBallBitmap(GameStatus.next.ball[i].type), 
				boardTopLeftX + GameStatus.next.ball[i].sides.left, 
				boardTopLeftY + GameStatus.next.ball[i].sides.top,
				scrOR);
			}
		}


	// Update the score
	if (GameStatus.last.score != GameStatus.next.score)
		{
		GameDrawScoreGauge(GameStatus.next.score);
		}
		
}


/***********************************************************************
 *
 * FUNCTION:     GameInitLevel
 *
 * DESCRIPTION:  Set the data to start a new level
 *
 * PARAMETERS:   nothing
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/22/96	Initial Revision
 *
 ***********************************************************************/

static void GameInitLevel (void)
{
	int i;
	int x;
	int y;
	FontID currFont;
	CharPtr nameP;
	Int nameLength;
	Int nameWidth;
	RectangleType bounds;
	
	
	// The game speeds up every time the levels wrap
	GameStatus.periodLength = max(defaultPeriodLength - GameStatus.level / levelCount, 
		minPeriodLength);
	GameStatus.status = presentingLevel;

	// Cancel any gameSpedUp
	if (GameStatus.gameSpedUp)
		{
		GameStatus.periodLength++;
		GameStatus.gameSpedUp = false;
		}
	
	// Set up the paddles.  All the paddles except the first are not used.  The
	// first is set to the bottom center of the board.

	// Reset the paddles
	for (i = paddlesMax - 1; i >= 0; i--)
		{
		GameStatus.next.paddle[i].usable = false;
		GameStatus.next.paddle[i].changed = false;
		}
	
	// Set the paddle up to draw
	GameStatus.next.paddle[0].usable = true;
	GameStatus.next.paddle[0].changed = false;
	GameStatus.next.paddle[0].type = normalPaddle;
	
	// place the paddle in the bottom center
	GameStatus.next.paddle[0].sides.left = (boardWidth - paddleWidth) / 2;
	GameStatus.next.paddle[0].sides.top = boardHeight - paddleHeight;
	GameStatus.next.paddle[0].sides.right = GameStatus.next.paddle[0].sides.left + paddleWidth;
	GameStatus.next.paddle[0].sides.bottom = GameStatus.next.paddle[0].sides.top + paddleHeight;


	// Start without any balls.  The user will add a ball into play later.
	for (i = ballsMax - 1; i >= 0; i--)
		{
		GameStatus.next.ball[i].usable = false;
		GameStatus.next.ball[i].changed = false;
		}


	// Setup a pattern of bricks for this level
	GameStatus.bricksRemaining = 0;
	MemMove(&GameStatus.brick, &levelInfo[GameStatus.level % levelCount].brick, 
		sizeof(BrickType) * rowsOfBricks * columnsOfBricks);
	
	// Count the bricks remaining to break
	for (y = 0; y < rowsOfBricks; y++)
		{
		for (x = 0; x < columnsOfBricks; x++)
			{
			if (GameStatus.brick[y][x] != empty &&
				GameStatus.brick[y][x] != unbreakable)
				GameStatus.bricksRemaining++;
			}
		}

	
	GameStatus.brokenBricksCount = 0;
	
	// Update the screen to draw the new level
	MemMove(&GameStatus.last, &GameStatus.next, sizeof(WorldState));
	GameStateDraw ();
	
	
	// Prepare to draw the level name
	currFont = FntSetFont(levelNameFont);
	nameP = levelInfo[GameStatus.level % levelCount].name;
	nameLength = StrLen(nameP);
	nameWidth = FntCharsWidth(nameP, nameLength);
	
	// Clear a surrounding box to make the text more distinguishable
	bounds.extent.y = FntLineHeight();
	bounds.extent.y += bounds.extent.y / 2;
	bounds.extent.x = nameWidth + bounds.extent.y;
	bounds.topLeft.x = boardTopLeftX + (boardWidth - bounds.extent.x) / 2;
	bounds.topLeft.y = boardTopLeftY + (boardHeight - FntLineHeight()) / 3;
	WinEraseRectangle(&bounds, 7);
	WinDrawRectangleFrame(boldRoundFrame, &bounds);

	// Draw the level name
	WinDrawChars (nameP, nameLength, 
		boardTopLeftX + (boardWidth - nameWidth) / 2,
		bounds.topLeft.y + FntLineHeight() / 4);
	FntSetFont(currFont);
	
	// Display the name for a while.  We'll remove it if the time is fully passed.
	GameStatus.periodsToWait = pauseLengthToDisplayLevelName;
}


/***********************************************************************
 *
 * FUNCTION:     GameStateAdvance
 *
 * DESCRIPTION:  Advance the world state by copying the next state
 * to the last state.
 *
 * PARAMETERS:   nothing
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/24/96	Initial Revision
 *
 ***********************************************************************/

static void GameStateAdvance (void)
{
	MemMove(&GameStatus.last, &GameStatus.next, sizeof(WorldState));

	if (GameStatus.status == levelOver)
		{
		GameStatus.level++;
		GameStatus.ballsRemaining++;
		GameInitLevel();
		}
		
}


/***********************************************************************
 *
 * FUNCTION:     GameStart
 *
 * DESCRIPTION:  Initialize the game to start.  Nothing visual.
 *
 * PARAMETERS:   nothing
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/23/96	Initial Revision
 *
 ***********************************************************************/
static void GameStart ()
{

	// Set the keys
	GameMaskKeys ();
	
	GameStatus.paused = false;
	GameStatus.pausedTime = 0;

	if (GameStatus.status != gameResuming)
		{
		GameStatus.next.score = 0;
		GameStatus.scoreToAwardBonusBall = firstBonusBallAwardedAtScore;
		GameStatus.soundToMake = noSound;
		GameStatus.level = Prefs.startLevel;
		GameStatus.lowestHighScorePassed = false;
		GameStatus.highestHighScorePassed = false;
		
		GameStatus.ballsRemaining = ballsPerGame;
		GameStatus.gameSpedUp = false;
		GameStatus.cheatMode = false;
		
		
		GameInitLevel ();
		GameStateAdvance();
		}
	else
		{
		GameStatus.status = SavedGameStatus;
		
		// Give the player time to get ready to play
		GameStatus.nextPeriodTime = TimGetTicks() + pauseLengthBeforeResumingSavedGame;
		
		// Show where the player left off
		GameStateDraw ();
		}

}


/***********************************************************************
 *
 * FUNCTION:     GamePlayABall
 *
 * DESCRIPTION:  Place one of the remaining balls into play moving up from
 * the paddle.
 *
 * PARAMETERS:   nothing
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/23/96	Initial Revision
 *
 ***********************************************************************/
static void GamePlayABall ()
{
	Int	left;
	Int	top;
	
	
	GameStatus.ballsRemaining--;
	
	// Set play in motion
	if (GameStatus.status == waitingForBall)
		{
		GameStatus.status = ballInMotion;
		GameStatus.nextPeriodTime = TimGetTicks();
		}
		
	
	// Place a ball above paddle 0 heading up somewhere
	left = GameStatus.next.paddle[0].sides.left + (GameStatus.next.paddle[0].sides.right - 
		GameStatus.next.paddle[0].sides.left - ballWidth) / 2;
	top = GameStatus.next.paddle[0].sides.top - ballHeight;
	BallAdd(normalBall, left, top, BallStartHeadings[randN(startHeadingsCount)]);
	GameRequestSound (playBall);
	
	GameDrawBallGauge();
}


/***********************************************************************
 *
 * FUNCTION:     BrickBreak
 *
 * DESCRIPTION:  Record a brick as broken. Updates score.  Note that
 * sometimes bricks don't break.
 *
 * PARAMETERS:   row - row of the brick
 *					  column - column of the brick
 *					  ball - the ball breaking the brick.
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/24/96	Initial Revision
 *
 ***********************************************************************/
static void BrickBreak (Int row, Int column, ObjectType *ball)
{
	Int left;
	Int top;
	
	
	if (BrickExists(column, row))
		{
		if (GameStatus.brick[row][column] == unbreakable)
			{
			GameRequestSound (wallBounce);
			ball->bouncesWithoutBreakingABrick++;
			}
		else
			{
			// Add the brick to the brokenBricks list for removal during GameStateDrawChanges
			GameStatus.brokenBricks[GameStatus.brokenBricksCount].row = row;
			GameStatus.brokenBricks[GameStatus.brokenBricksCount].column = column;
			GameStatus.brokenBricksCount++;

			// Since this brick is being broken the ball won't bounce here again.  
			// Clear bouncesWithoutBreakingABrick.
			ball->bouncesWithoutBreakingABrick = 0;
			
			// Some bricks have special effects
			switch (GameStatus.brick[row][column])
				{
				case ballBrick:
					left = BrickX(column) + (brickWidth - ballWidth) / 2;
					top = BrickY(row) + (brickHeight - ballHeight) / 2;
					BallAdd(normalBall, left, top, BallStartHeadings[randN(startHeadingsCount)]);
					GameRequestSound (extraBall);
					break;
					
				case sidePaddleBrick:
					PaddleAddSidePaddle ();
					GameRequestSound (extraPaddle);
					break;
				}
							
			IncreaseScore(ScoreForBrick(row, column));
			GameStatus.brick[row][column] = empty;
			GameStatus.bricksRemaining--;
			
			GameRequestSound (brickBreak);
			}
		}
}


/***********************************************************************
 *
 * FUNCTION:     GetNearestSurface
 *
 * DESCRIPTION:  Get the rectangle surface that a ball is closest to.
 *   Most importantly this routine determines the tricky corner cases.
 *
 * PARAMETERS:   ball - the ball to check
 *					  r - rectangle of surfaces
 *
 * RETURNED:     the surface.  surfaceNone may be returned if the ball missed.
 *   The ball may miss because the course collision checking is retangular
 *   but the bitmap is round.
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/29/96	Initial Revision
 *
 ***********************************************************************/
static Byte GetNearestSurface (ObjectType	*ball, AbsRectType *r)
{
	int Xb, Yb;			// ball center.
	int Xs, Ys;			// rectangle surface
	int b;				// intersection of ball with Y axis. (point slope equ.)
	int d2;				// distance between the ball and point of intersection
	int shortestDistance;	// shortest distance;
	Byte surface;		// surface with shortest distance
	
	
	shortestDistance = boardWidth + boardHeight;
	surface = surfaceNone;
	
	
	// For each surface find the distance of the ball to it's intersection
	// with the surface.
	
	// First, make a line equation describing the motion of the ball.
	// Yb = h * Xb + b.  h is the ball heading Yb and Xb are the ball's center.
	// b is unknown.  Solve for it.
	Xb = ball->sides.left + (ball->sides.right - ball->sides.left) / 2;
	Yb = ball->sides.top + (ball->sides.bottom - ball->sides.top) / 2;
	b = Yb - ((Xb * MovementY[ball->heading]) / MovementX[ball->heading]);
	
	
	// Now find the intersection of the center of the ball to the top surface.
	if (HeadingDown(ball->heading))
		{
		Ys = r->top;
		Xs = (((Ys - b) * MovementX[ball->heading]) / MovementY[ball->heading]);
		if ((r->left <= Xs + halfBallWidth) && (Xs - halfBallWidth <= r->right))
			{
			d2 = (Xb - Xs) * (Xb - Xs) + (Yb - Ys) * (Yb - Ys);
			
			shortestDistance = d2;
	
			// The corner cases should go to the other edge
			if (Xs < r->left && HeadingRight(ball->heading))
				surface = surfaceLeft;
			else if (Xs > r->right && HeadingLeft(ball->heading))
				surface = surfaceRight;
			else
				surface = surfaceTop;
			}
		}
	
	
	// Now find the intersection of the center of the ball to the bottom surface.
	if (HeadingUp(ball->heading))
		{
		Ys = r->bottom;
		Xs = (((Ys - b) * MovementX[ball->heading]) / MovementY[ball->heading]);
		if ((r->left <= Xs + halfBallWidth) && (Xs - halfBallWidth <= r->right))
			{
			d2 = (Xb - Xs) * (Xb - Xs) + (Yb - Ys) * (Yb - Ys);
			
			if (d2 < shortestDistance)
				{
				// The corner cases should go to the other edge
				if (Xs < r->left && HeadingRight(ball->heading))
					surface = surfaceLeft;
				else if (Xs > r->right && HeadingLeft(ball->heading))
					surface = surfaceRight;
				else
					surface = surfaceBottom;
	
				shortestDistance = d2;
				}
			}
		}
	
	
	// Now find the intersection of the center of the ball to the left surface.
	if (HeadingRight(ball->heading))
		{
		Xs = r->left;
		Ys = ((Xs * MovementY[ball->heading]) / MovementX[ball->heading]) + b;
		if ((r->top <= Ys + halfBallHeight) && (Ys - halfBallHeight <= r->bottom))
			{
			d2 = (Xb - Xs) * (Xb - Xs) + (Yb - Ys) * (Yb - Ys);
			
			if (d2 < shortestDistance)
				{
				// The corner cases should go to the other edge
				if (Ys < r->top && HeadingDown(ball->heading))
					surface = surfaceTop;
				else if (Ys > r->bottom && HeadingUp(ball->heading))
					surface = surfaceBottom;
				else
					surface = surfaceLeft;
	
				shortestDistance = d2;
				}
			}
		}
	
	
	// Now find the intersection of the center of the ball to the right surface.
	if (HeadingLeft(ball->heading))
		{
		Xs = r->right;
		Ys = ((Xs * MovementY[ball->heading]) / MovementX[ball->heading]) + b;
		if ((r->top <= Ys + halfBallHeight) && (Ys - halfBallHeight <= r->bottom))
			{
			d2 = (Xb - Xs) * (Xb - Xs) + (Yb - Ys) * (Yb - Ys);
			
			if (d2 < shortestDistance)
				{
				// The corner cases should go to the other edge
				if (Ys < r->top && HeadingDown(ball->heading))
					surface = surfaceTop;
				else if (Ys > r->bottom && HeadingUp(ball->heading))
					surface = surfaceBottom;
				else
					surface = surfaceRight;
					
				shortestDistance = d2;
				}
			}
		}
		
	
	return surface;
}
	
	
/***********************************************************************
 *
 * FUNCTION:     CheckBallWithWallCollisions
 *
 * DESCRIPTION:  Check for collisions of a ball into the walls.
 *
 * If the ball is past a boundary, the ball is reflected and moved from
 * it's last position.  The caller should call this again for corners
 * which involve two collisions.
 *
 * PARAMETERS:   ballNumber - the ball to check
 *
 * RETURNED:     true if a collision occurred
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/24/96	Initial Revision
 *
 ***********************************************************************/
static Boolean CheckBallWithWallCollisions (Int ballNumber)
{
	ObjectType	*ball;
	
	
	ball = &GameStatus.next.ball[ballNumber];
	if (ball->sides.left < 0)
		{
		ball->heading = BallReflect(ball->heading, surfaceVertical, noMotion);
		BallMove(ballNumber);
		GameRequestSound (wallBounce);
		return true;
		}
	else if (ball->sides.right > boardWidth)
		{
		ball->heading = BallReflect(ball->heading, surfaceVertical, noMotion);
		BallMove(ballNumber);
		GameRequestSound (wallBounce);
		return true;
		}
	
	if (ball->sides.top < 0)
		{
		ball->heading = BallReflect(ball->heading, surfaceHorizontal, noMotion);
		BallMove(ballNumber);
		GameRequestSound (wallBounce);
		
		// When the top wall is hit the game speeds up if the period is slower than one tick.
		// At one tick or faster the game is too fast!
		if (!GameStatus.gameSpedUp &&
			GameStatus.periodLength > minPeriodLength)
			{
			GameStatus.periodLength--;
			GameStatus.gameSpedUp = true;
			GameRequestSound (speedBall);
			}
		return true;
		}

	// Check if the ball completely moved off the bottom
	if (ball->sides.top > boardHeight)
		{
		BallRemove (ballNumber);
		return false;
		}

	return false;

}


/***********************************************************************
 *
 * FUNCTION:     CheckBallWithBrickCollisions
 *
 * DESCRIPTION:  Check for collisions of a ball into the bricks.
 *
 * Each ball corner is capable of breaking a brick.  For each corner
 * find out which brick it touches.  The combination of bricks touched
 * determines which bricks are broken and how the ball reflects.
 *
 * No ball may move more than the height of a brick in one period.
 *
 * 0.   B		the top left and bottom right are in the same brick
 *      B		reflect once, break brick
 *
 * 1.  B x		the top left and bottom right are in different bricks
 *     x B		reflect twice, break both
 *
 * 2.  x B		the top right and bottom left are in different bricks
 *     B x		reflect twice, break both
 *
 * 3.  B B		the tops touch one or two bricks
 *     x x		reflect once, break one or two bricks
 *
 * 4.  x x		the bottoms touch one or two bricks
 *     B B		reflect once, break one or two bricks
 *
 * 5.  B x		the lefts touch one or two bricks
 *     B x		reflect once, break one or two bricks
 *
 * 6.  x B		the rights touch one or two bricks
 *     x B		reflect once, break one or two bricks
 *
 * 7.  B B		not possible because ball can't move more than one brick height
 *     B B		
 *
 * Cases of only one brick existing are covered by cases 3 and 4.  They perform
 * special checking for the ball bouncing off a corner when there is one brick.
 *
 * Cases 3 to 6 check to see if enough of the ball is touching a brick before it
 *	is removed.  The ball is fat enough compared to the bricks to often touch two
 * bricks at the same time.  This code requires more than the corner of a ball to
 * touch before removing a brick.
 *
 *		2) When a corner is touched cases 3 & 4 tend to activate when a vertical
 *			bounce is expected because the ball touches more vertical than horizontal
 *			surface.  Code detects this and leaves cases 5 & 6 to reflect the ball.
 *
 * PARAMETERS:   ballNumber - the ball to check
 *
 * RETURNED:     true if a collision occurred
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/24/96	Initial Revision
 *
 ***********************************************************************/
static Boolean CheckBallWithBrickCollisions (Int ballNumber)
{
	ObjectType	*ball;
	PointType ballCorner[4];		// topleft, topRight, bottomRight, bottomLeft
											// Brick touched by the corner
	Boolean brickExists[4];			// true if a brick exists at the ball corner
	PointType brick1, brick2;		// two bricks may be broken
	Boolean brickFound = false;
	AbsRectType	bounds;				// bounds of a brick
	Int amountTouching;				// amount of the ball touching a brick
	Byte surface;						// surface to bounce the ball off of
	
	
	ball = &GameStatus.next.ball[ballNumber];
	
	// Trivial rejection case: the ball is below all the bricks.
	if (ball->sides.top >= BrickY(rowsOfBricks))
		return false;
	
	// Calculate two of the corners for case 0
	ballCorner[0].x = BrickAtX(ball->sides.left);
	ballCorner[0].y = BrickAtY(ball->sides.top);
	ballCorner[2].x = BrickAtX(ball->sides.right);
	ballCorner[2].y = BrickAtY(ball->sides.bottom);
	
	
	// Case 0:
	if (ballCorner[0].x == ballCorner[2].x &&
		ballCorner[0].y == ballCorner[2].y)
		{
		if (!BrickExists(ballCorner[0].x, ballCorner[0].y))
			return false;
		
		ErrFatalDisplayIf(ballCorner[0].y > brickEndRow, "Bad bounce");
		
		// Reflect the ball once
		ball->heading = BallReflect(ball->heading, surfaceHorizontal, noMotion);
		BallMove(ballNumber);
		
		// Designate one brick to break.
		brickFound = true;
		brick1.x = ballCorner[0].x;
		brick1.y = ballCorner[0].y;
		brick2.x = ballCorner[0].x;
		brick2.y = ballCorner[0].y;
		goto brickFound;
		}
	
	
	// Calculate the remaining two of the corners for the rest of the cases
	ballCorner[1].x = BrickAtX(ball->sides.right);
	ballCorner[1].y = BrickAtY(ball->sides.top);
	ballCorner[3].x = BrickAtX(ball->sides.left);
	ballCorner[3].y = BrickAtY(ball->sides.bottom);
	
	// Determine which corners touch a brick.
	brickExists[0] = BrickExists(ballCorner[0].x, ballCorner[0].y);
	brickExists[1] = BrickExists(ballCorner[1].x, ballCorner[1].y);
	brickExists[2] = BrickExists(ballCorner[2].x, ballCorner[2].y);
	brickExists[3] = BrickExists(ballCorner[3].x, ballCorner[3].y);
	
	
	// If no corner touches a brick then return no collision
	if (brickExists[0] + brickExists[1] + brickExists[2] + brickExists[3] == 0)
		return false;
		
	
	// Case 1:  This case and Case 2 must be done before the rest
	if (brickExists[0] && brickExists[2])
		{
		// Reflect the ball twice
		ball->heading = BallReflect(ball->heading, surfaceHorizontal, noMotion);
		ball->heading = BallReflect(ball->heading, surfaceVertical, noMotion);
		BallMove(ballNumber);
		
		// Designate two bricks to break.
		brickFound = true;
		brick1.x = ballCorner[0].x;
		brick1.y = ballCorner[0].y;
		brick2.x = ballCorner[2].x;
		brick2.y = ballCorner[2].y;
		goto brickFound;
		}
	
	// Case 2:  This case and Case 1 must be done before the rest
	else if (brickExists[1] && brickExists[3])
		{
		// Reflect the ball twice
		ball->heading = BallReflect(ball->heading, surfaceHorizontal, noMotion);
		ball->heading = BallReflect(ball->heading, surfaceVertical, noMotion);
		BallMove(ballNumber);
		
		// Designate two bricks to break.
		brickFound = true;
		brick1.x = ballCorner[1].x;
		brick1.y = ballCorner[1].y;
		brick2.x = ballCorner[3].x;
		brick2.y = ballCorner[3].y;
		goto brickFound;
		}
	
	
	// Case 5:  Handle this case and case 6 next.
	if ((brickExists[0] && brickExists[3])) 
		{
		// Reflect the ball once
		ball->heading = BallReflect(ball->heading, surfaceVertical, noMotion);
		BallMove(ballNumber);
		
		// Designate two bricks to break.
		brickFound = true;
		brick1.x = ballCorner[0].x;
		brick1.y = ballCorner[0].y;
		brick2.x = ballCorner[3].x;
		brick2.y = ballCorner[3].y;
			
		// Detect if both bricks are hit.  If so require that they be hit by
		// a certain amount of the ball.
		if (brickExists[0] && brickExists[3] &&
			(brick1.x != brick2.x))
			{
			amountTouching = ball->sides.bottom - BrickY(brick2.y);
			// Touching brick2 enough?
			if (amountTouching < ballAmountNeededToBreakABrick)
				{
				brick2.x = brick1.x;
				brick2.y = brick1.y;
				}
			// Touching brick1 enough?
			else if ((ballHeight - amountTouching) < ballAmountNeededToBreakABrick)
				{
				brick1.x = brick2.x;
				brick1.y = brick2.y;
				}
			}
		}
	
	// Case 6:
	else if ((brickExists[1] && brickExists[2])) 
		{
		// Reflect the ball once
		ball->heading = BallReflect(ball->heading, surfaceVertical, noMotion);
		BallMove(ballNumber);
		
		// Designate two bricks to break.
		brickFound = true;
		brick1.x = ballCorner[1].x;
		brick1.y = ballCorner[1].y;
		brick2.x = ballCorner[2].x;
		brick2.y = ballCorner[2].y;

		// Detect if both bricks are hit.  If so require that they be hit by
		// a certain amount of the ball.
		if (brickExists[1] && brickExists[2] &&
			(brick1.x != brick2.x))
			{
			amountTouching = ball->sides.bottom - BrickY(brick2.y);
			// Touching brick2 enough?
			if (amountTouching < ballAmountNeededToBreakABrick)
				{
				brick2.x = brick1.x;
				brick2.y = brick1.y;
				}
			// Touching brick1 enough?
			else if ((ballHeight - amountTouching) < ballAmountNeededToBreakABrick)
				{
				brick1.x = brick2.x;
				brick1.y = brick2.y;
				}
			}
		}
	
	// Case 3:  Probably the most likely case.  The following two cases have
	// been generalized to handle one or two bricks present.  This means they
	// cover and therefore must follow the cases above which are more specific.
	else if ((brickExists[0] || brickExists[1])) 
		{
		// Calculate the surface.  Since we cover the cases where there exists only
		// one brick the ball may actually hit a corner.  If only one corner is
		// in a brick let GetNearestSurface determine which sides of the brick
		// is hit.
		if (brickExists[0] && !brickExists[1])
			{
			bounds.top = BrickY(ballCorner[0].y);
			bounds.bottom = bounds.top + brickHeight;
			bounds.left = BrickX(ballCorner[0].x);
			bounds.right = bounds.left + brickWidth;
			
			surface = GetNearestSurface (ball, &bounds);
			}
		else if (!brickExists[0] && brickExists[1])
			{
			bounds.top = BrickY(ballCorner[1].y);
			bounds.bottom = bounds.top + brickHeight;
			bounds.left = BrickX(ballCorner[1].x);
			bounds.right = bounds.left + brickWidth;
			
			surface = GetNearestSurface (ball, &bounds);
			}
		else 
			{
			surface = surfaceHorizontal;
			}
		
		if (surface != surfaceNone)
			{
			// Reflect the ball once
			ball->heading = BallReflect(ball->heading, surface, noMotion);
			BallMove(ballNumber);
			
			// Designate two bricks to break.
			brickFound = true;
			brick1.x = ballCorner[0].x;
			brick1.y = ballCorner[0].y;
			brick2.x = ballCorner[1].x;
			brick2.y = ballCorner[1].y;
			
			// Detect if both bricks are hit.  If so require that they be hit by
			// a certain amount of the ball.
			if (brickExists[0] && brickExists[1] &&
				(brick1.x != brick2.x))
				{
				amountTouching = ball->sides.right - BrickX(brick2.x);
				// Touching brick2 enough?
				if (amountTouching < ballAmountNeededToBreakABrick)
					{
					brick2.x = brick1.x;
					brick2.y = brick1.y;
					}
				// Touching brick1 enough?
				else if ((ballWidth - amountTouching) < ballAmountNeededToBreakABrick)
					{
					brick1.x = brick2.x;
					brick1.y = brick2.y;
					}
				}
			}
		}
	
	// Case 4:  Next most likely case
	else if ((brickExists[2] || brickExists[3])) 
		{
		// Calculate the surface.  Since we cover the cases where there exists only
		// one brick the ball may actually hit a corner.  If only one corner is
		// in a brick let GetNearestSurface determine which sides of the brick
		// is hit.
		if (brickExists[2] && !brickExists[3])
			{
			bounds.top = BrickY(ballCorner[2].y);
			bounds.bottom = bounds.top + brickHeight;
			bounds.left = BrickX(ballCorner[2].x);
			bounds.right = bounds.left + brickWidth;
			
			surface = GetNearestSurface (ball, &bounds);
			}
		else if (!brickExists[2] && brickExists[3])
			{
			bounds.top = BrickY(ballCorner[3].y);
			bounds.bottom = bounds.top + brickHeight;
			bounds.left = BrickX(ballCorner[3].x);
			bounds.right = bounds.left + brickWidth;
			
			surface = GetNearestSurface (ball, &bounds);
			}
		else 
			{
			surface = surfaceHorizontal;
			}
		
		if (surface != surfaceNone)
			{
			// Reflect the ball once
			ball->heading = BallReflect(ball->heading, surface, noMotion);
			BallMove(ballNumber);
			
			// Designate two bricks to break.
			brickFound = true;
			brick1.x = ballCorner[3].x;
			brick1.y = ballCorner[3].y;
			brick2.x = ballCorner[2].x;
			brick2.y = ballCorner[2].y;
			
			// Detect if both bricks are hit.  If so require that they be hit by
			// a certain amount of the ball.
			if (brickExists[2] && brickExists[3] &&
				(brick1.x != brick2.x))
				{
				amountTouching = ball->sides.right - BrickX(brick2.x);
				// Touching brick2 enough?
				if (amountTouching < ballAmountNeededToBreakABrick)
					{
					brick2.x = brick1.x;
					brick2.y = brick1.y;
					}
				// Touching brick1 enough?
				else if ((ballWidth - amountTouching) < ballAmountNeededToBreakABrick)
					{
					brick1.x = brick2.x;
					brick1.y = brick2.y;
					}
				}
			}
		}
	
	
	
	
	if (brickFound)
		{
brickFound:
		// Award points for the brick and remove it.
		BrickBreak(brick1.y, brick1.x, ball);
		
		// If brick2 is the same as brick1 nothing will be done (it is empty now).
		BrickBreak(brick2.y, brick2.x, ball);
		}
	
	return brickFound;
}


/***********************************************************************
 *
 * FUNCTION:     CheckBallWithPaddleCollisions
 *
 * DESCRIPTION:  Check for collisions of a ball into the paddles.
 *
 * If the ball touches a paddle, the ball is reflected and moved from
 * it's last position.  The caller should call this again for corners
 * which involve two collisions.
 *
 * PARAMETERS:   ballNumber - the ball to check
 *
 * RETURNED:     true if a collision occurred
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/24/96	Initial Revision
 *
 ***********************************************************************/
static Boolean CheckBallWithPaddleCollisions (Int ballNumber)
{
	ObjectType	*ball;
	int i;
	Byte surface;
	Byte motion;
	
	
	ball = &GameStatus.next.ball[ballNumber];

	for (i = paddlesMax - 1; i >= 0; i--)
		{
		if (GameStatus.next.paddle[i].usable)
			{
			if ((ball->sides.bottom >= GameStatus.next.paddle[i].sides.top &&
				ball->sides.bottom <= GameStatus.next.paddle[i].sides.bottom) ||
				(ball->sides.top >= GameStatus.next.paddle[i].sides.top &&
				ball->sides.top <= GameStatus.next.paddle[i].sides.bottom))
				{
				// Now worry about the side of the paddle
				if (ball->sides.right < GameStatus.next.paddle[i].sides.left ||
					ball->sides.left > GameStatus.next.paddle[i].sides.right)
					continue;
				
				
				surface = GetNearestSurface (ball, &GameStatus.next.paddle[i].sides);
				if (surface == surfaceNone)
					continue;
				
				if (GameStatus.movePaddleLeft)
					motion = leftMotion;
				else if (GameStatus.movePaddleRight)
					motion = rightMotion;
				else
					motion = noMotion;
				
				// The ball did hit the paddle.  Bounce it off.
				ball->heading = BallReflect(ball->heading, surface, motion);
				BallMove(ballNumber);
				ball->bouncesWithoutBreakingABrick = 0;
				
				// If the ball collided with the side of a paddle while it was moving
				// give the ball an extra kick to keep it outside of the bounds of the
				// paddle.  The physically correct way should be to increase the velocity
				// of the ball because momentum is transfered from the paddle to the ball.
				// Our model keeps the ball at a fixed velocity so we just move the ball
				// some more.
				if (motion != noMotion)
					{
					// Did the ball hit a side surface of a paddle?
					if (surface == surfaceLeft)
						{
						ball->sides.left -= paddleMovement;
						ball->sides.right -= paddleMovement;
						}
					else if (surface == surfaceRight)
						{
						ball->sides.left += paddleMovement;
						ball->sides.right += paddleMovement;
						}
					}

				// The paddle bounced the ball. 
				GameRequestSound (paddleBounce);

				return true;
				}
			}
		}

	return false;
}


/***********************************************************************
 *
 * FUNCTION:     GameStateElapse
 *
 * DESCRIPTION:  Increment the state of the game world.
 *
 * PARAMETERS:   nothing
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/23/96	Initial Revision
 *
 ***********************************************************************/
static void GameStateElapse ()
{
	int i;
	Int smallestMove;
	DWord hardKeyState;
	Boolean ballCollided;
	int collisionCount;
	
	
	// Don't advance past checking high scores.
	if (GameStatus.status == checkHighScores)
		return;
	
	// We have paused after the game.  Now check for a high score.
	if (GameStatus.status == gameOver)
		{
		GameStatus.status = checkHighScores;
		if (!GameStatus.cheatMode)
			HighScoresCheckScore();
		
		// Allow the hard keys to switch to another app now that the player is done.
		GameUnmaskKeys ();
		return;
		}
	
	// When all the bricks are cleared advance to the next level.
	if (GameStatus.bricksRemaining == 0)
		{
		GameStatus.status = levelOver;
		GameStatus.nextPeriodTime += levelOverTimeInterval;
		return;
		}
		
		
	// The time between the last advance and the next is constant
	GameStatus.nextPeriodTime = GameStatus.nextPeriodTime + GameStatus.periodLength;
	
	
	// Move the paddles
	GameStatus.movePaddleLeft = false;
	GameStatus.movePaddleRight = false;
	hardKeyState = KeyCurrentState() & (moveLeftKey | moveLeftKeyAlt | moveRightKey | moveRightKeyAlt);
	smallestMove = paddleMovement;
	
	// If the level name is showing and the user is ready to play or enough time has
	// passed then remove the name
	if (GameStatus.status == presentingLevel)
		{
		GameStatus.periodsToWait--;
		if (hardKeyState ||
			GameStatus.periodsToWait == 0)
			{
			GameStateDraw();
			GameStatus.status = waitingForBall;
			}
		}
		

		
	// Move the paddles in a direction.  Remember that there may be multiple paddles.
	// If one paddle can't move then none can move.
	if (hardKeyState == moveLeftKey || hardKeyState == moveLeftKeyAlt)
		{
		// Find the smallest amount a paddle may move left
		for (i = paddlesMax - 1; i >= 0; i--)
			{
			if (GameStatus.next.paddle[i].usable)
				{
				smallestMove = min(smallestMove, GameStatus.next.paddle[i].sides.left);
				}
			}
		
		// If all the paddles can move at least a little bit do it
		if (smallestMove > 0)
			{
			for (i = paddlesMax - 1; i >= 0; i--)
				{
				if (GameStatus.next.paddle[i].usable)
					{
					GameStatus.next.paddle[i].sides.left -= smallestMove;
					GameStatus.next.paddle[i].sides.right -= smallestMove;
					GameStatus.next.paddle[i].changed = true;
					}
				}
			
			GameStatus.movePaddleLeft = true;
			}

		}
	else if (hardKeyState == moveRightKey || hardKeyState == moveRightKeyAlt)
		{
		// Find the smallest amount a paddle may move right
		for (i = paddlesMax - 1; i >= 0; i--)
			{
			if (GameStatus.next.paddle[i].usable)
				{
				smallestMove = min(smallestMove, 
					boardWidth - GameStatus.next.paddle[i].sides.right);
				}
			}
		
		// If all the paddles can move at least a little bit do it
		if (smallestMove > 0)
			{
			for (i = paddlesMax - 1; i >= 0; i--)
				{
				if (GameStatus.next.paddle[i].usable)
					{
					GameStatus.next.paddle[i].sides.left += smallestMove;
					GameStatus.next.paddle[i].sides.right += smallestMove;
					GameStatus.next.paddle[i].changed = true;
					}
				}
			
			GameStatus.movePaddleRight = true;
			}
		}
	else 	// Paddles moved neither direction
		{
		// Mark each paddle as unchanged
		for (i = paddlesMax - 1; i >= 0; i--)
			{
			GameStatus.next.paddle[i].changed = false;
			}
		}

	
	// Move the balls
	for (i = ballsMax - 1; i >= 0; i--)
		{
		// Ignore unusable balls
		if (!GameStatus.next.ball[i].usable)
			{
			GameStatus.next.ball[i].usable = false;
			GameStatus.next.ball[i].changed = false;
			}
		// Don't process balls added this period by a ball brick.  BallMove relys
		// on valid bounds from the last period and so it doesn't handle new balls. 
		else if (GameStatus.last.ball[i].usable)
			{
			GameStatus.next.ball[i].changed = true;
			// Add a movement vector based on the ball's heading
			BallMove(i);
			
			// Now check for collisions
			collisionCount = 0;
			do
				{
				// Check for paddle collisions
				ballCollided = CheckBallWithPaddleCollisions (i);
				
				// Check for a brick collision
				if (CheckBallWithBrickCollisions (i))
					ballCollided = true;
				
				// check for wall collisions.  Repeat until no more collisions (corners 
				// have two colisions).
				while (CheckBallWithWallCollisions (i))
					{
					ballCollided = true;
					};
				
				if (ballCollided)
					{
					collisionCount++;
				
				
					// See if the ball seems to be trapped in a pattern of unbreakable bricks and walls.
					if (GameStatus.next.ball[i].bouncesWithoutBreakingABrick > ballTrappedInLoopThreshold)
						{
						// Tweak the heading by one angle in the hopes of dislodging the ball from
						// it's pattern.
						if (GameStatus.next.ball[i].heading == degrees22 ||
							GameStatus.next.ball[i].heading == degrees112 ||
							GameStatus.next.ball[i].heading == degrees202 ||
							GameStatus.next.ball[i].heading == degrees292)
							{
							GameStatus.last.ball[i].heading++;
							}
						else if (GameStatus.next.ball[i].heading == degrees67 ||
							GameStatus.next.ball[i].heading == degrees157 ||
							GameStatus.next.ball[i].heading == degrees247 ||
							GameStatus.next.ball[i].heading == degrees337)
							{
							GameStatus.next.ball[i].heading--;
							}
						else 
							{
							// 50% chance of the ball going one way or the other
							if (randN(100) < 50)
								{
								GameStatus.next.ball[i].heading++;
								}
							else
								{
								GameStatus.next.ball[i].heading--;
								}
							}
						BallMove(i);				// move it in the new direction instead.

						// Tweak the ball again if it doesn't break out of it's pattern soon.
						GameStatus.next.ball[i].bouncesWithoutBreakingABrick = 0;
						}
						
					
					// Check to see if the ball can't move anywhere.  If so remove it.  It probably
					// is being crushed between the paddle and a wall.
					if (collisionCount >= 5)
						{
						// If the ball is being destroyed above the paddle then give the user
						// another ball and give them some bonus points!
						//
						// Some levels can cause the ball to be destroyed in certain cases.
						if (GameStatus.next.ball[i].sides.bottom < 
							GameStatus.next.paddle[0].sides.top)
							{
							GameStatus.ballsRemaining++;
							GameDrawBallGauge();
							
							IncreaseScore(100);		// Points for destroying the ball
							}
						BallRemove (i);
						break;
						}
					}

				}
			while (ballCollided);
			}

		}
	
}


/***********************************************************************
 *
 * FUNCTION:     GamePlaySounds
 *
 * DESCRIPTION:  Play a game sound.
 *
 * PARAMETERS:   nothing
 *
 * RETURNED:     nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/30/96	Initial Revision
 *			roger	1/30/98	Changed to play asynchronus sounds
 *
 ***********************************************************************/
static void GamePlaySounds ()
{
	SndCommandType		sndCmd;


	if (GameStatus.soundToMake != noSound)
		{
		sndCmd.cmd = sndCmdFrqOn;
		sndCmd.param1 = Sound[GameStatus.soundToMake].frequency;
		sndCmd.param2 = Sound[GameStatus.soundToMake].duration;
		sndCmd.param3 = SoundAmp;

		SndDoCmd( 0, &sndCmd, true/*noWait*/ );

		GameStatus.soundPeriodsRemaining--;
		if (GameStatus.soundPeriodsRemaining <= 0)
			GameStatus.soundToMake = noSound;
		
		}
}


/***********************************************************************
 *
 * FUNCTION:    HighScoresClear
 *
 * DESCRIPTION: Clear the high scores and the dialog.
 *
 * PARAMETERS:  nothing
 *
 * RETURNED:    nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	4/23/96	Initial Revision
 *
 ***********************************************************************/
static void HighScoresClear (void)
{
	int i;
	RectangleType bounds;
	

	// Clear the high scores.
	for (i = 0; i < highScoreMax; i++)
		{
		Prefs.highScore[i].name[0] = '\0';
		Prefs.highScore[i].score = 0;
		Prefs.highScore[i].level = 1;
		}
	
	// Clear the scores listed.  This is much easier than making the 
	// form redraw itself properly. 
	bounds.topLeft.x = 0;
	bounds.topLeft.y = firstHighScoreY;
	bounds.extent.x = highScoreLevelColumnX;
	bounds.extent.y = highScoreMax * highScoreHeight;
	WinEraseRectangle(&bounds, 0);

}


/***********************************************************************
 *
 * FUNCTION:    HighScoresEventHandler
 *
 * DESCRIPTION: Handle the Clear button
 *
 * PARAMETERS:  eventP - event to handle
 *
 * RETURNED:    true if event handled
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	4/23/96	Initial Revision
 *
 ***********************************************************************/
static Boolean HighScoresEventHandler (EventPtr eventP)
{
	if (eventP->eType == ctlSelectEvent)
		{
		if (eventP->data.ctlSelect.controlID == HighScoresClearButton)
			{
			if (FrmAlert(ClearHighScoresAlert) == ClearHighScoresYes)
				HighScoresClear();
			
			return true;
			}
		}
	
	return false;
}


/***********************************************************************
 *
 * FUNCTION:    HighScoresDisplay
 *
 * DESCRIPTION: Display the high score dialog
 *
 * PARAMETERS:  nothing
 *
 * RETURNED:    nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	3/11/96	Initial Revision
 *
 ***********************************************************************/
static void HighScoresDisplay (void)
{
	FormPtr curFormP;
	FormPtr formP;
	Handle titleH;
	CharPtr titleP;
	char string[16];
	FontID currFont;
	int i;
	short y;


	curFormP = FrmGetActiveForm ();
	formP = FrmInitForm (HighScoresDialog);
	FrmSetActiveForm (formP);
	FrmDrawForm (formP);

	// Remember the font	
	currFont = FntSetFont(boldFont);
	
	
	// Draw the titles of the columns
	titleH = DmGetResource(strRsc, NameColumnStr);
	titleP = MemHandleLock(titleH);
	WinDrawChars(titleP, StrLen(titleP), highScoreNameColumnX,
		firstHighScoreY - highScoreHeight);
	MemPtrUnlock(titleP);	

	titleH = DmGetResource(strRsc, ScoreColumnStr);
	titleP = MemHandleLock(titleH);
	WinDrawChars(titleP, StrLen(titleP), highScoreScoreColumnX - 
		FntCharsWidth(titleP, StrLen(titleP)), firstHighScoreY - highScoreHeight);
	MemPtrUnlock(titleP);	

	titleH = DmGetResource(strRsc, LevelColumnStr);
	titleP = MemHandleLock(titleH);
	WinDrawChars(titleP, StrLen(titleP), highScoreLevelColumnX - 
		FntCharsWidth(titleP, StrLen(titleP)), firstHighScoreY - highScoreHeight);
	MemPtrUnlock(titleP);	


	WinDrawLine(highScoreNameColumnX, firstHighScoreY - 1, highScoreLevelColumnX, firstHighScoreY - 1);
	// Draw each high score in the right spot
	for (i = 0; i < highScoreMax && Prefs.highScore[i].score > 0; i++)
		{
		y = firstHighScoreY + i * highScoreHeight;

		// Differentiate the last high score by choosing a different font.
		if (i == Prefs.lastHighScore)
			FntSetFont(boldFont);
		else
			FntSetFont(highScoreFont);
		
		// Display the score number
		StrIToA(string, i + 1);
		StrCat(string, ". ");
		WinDrawChars(string, StrLen(string), highScoreNameColumnX - 
			FntCharsWidth(string, StrLen(string)), y);
		
		WinDrawChars(Prefs.highScore[i].name, StrLen(Prefs.highScore[i].name),
			highScoreNameColumnX, y);
		
		StrIToA(string, Prefs.highScore[i].score);
		WinDrawChars(string, StrLen(string), highScoreScoreColumnX - 
			FntCharsWidth(string, StrLen(string)), y);
		
		StrIToA(string, Prefs.highScore[i].level);
		WinDrawChars(string, StrLen(string), highScoreLevelColumnX - 
			FntCharsWidth(string, StrLen(string)), y);
		}
	FntSetFont(currFont);

	FrmSetEventHandler(formP, HighScoresEventHandler);		// Handle the clear button
	
	FrmDoDialog (formP);
	FrmDeleteForm (formP);
	FrmSetActiveForm (curFormP);
}


/***********************************************************************
 *
 * FUNCTION:    HighScoresAddScore
 *
 * DESCRIPTION: Add the new score.
 *
 * PARAMETERS:  position - the position to add the score
 *					 name - name to add
 *					 score - score to add
 *					 level - level to add
 *					 dontAddIfExists - used when initializing scores
 *
 * RETURNED:    nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	3/13/96	Initial Revision
 *			roger	3/19/96	Broke out the check and contratulations dialog
 *
 ***********************************************************************/
static void HighScoresAddScore (CharPtr name, Long score, Int level, 
	Boolean dontAddIfExists)
{
	int position;
	
	
	// Find where the score belongs.  The new score looses any ties.
	position = highScoreMax;
	while (position > 0 &&
		score > Prefs.highScore[position - 1].score)
		{
		position--;
		}
	
	
	// Leave if the score doesn't make it into the high scores.
	if (position >= highScoreMax)
		return;
	
	if (dontAddIfExists &&
		position > 0 &&
		StrCompare(name, Prefs.highScore[position - 1].name) == 0 &&
		score == Prefs.highScore[position - 1].score &&
		level == Prefs.highScore[position - 1].level)
		return;
		
	// Move down the scores to make room for the new high score.
	MemMove(&Prefs.highScore[position + 1], &Prefs.highScore[position],
		(highScoreMax - 1 - position) * sizeof (SavedScore));
	
	
	Prefs.highScore[position].score = score;
	Prefs.highScore[position].level = level;
	StrCopy(Prefs.highScore[position].name, name);
	
	
	// Record this new score as the last one entered.
	Prefs.lastHighScore = position;
}


/***********************************************************************
 *
 * FUNCTION:    HighScoresCheckScore
 *
 * DESCRIPTION: Check if the current score is a high one and call
 *	HighScoresAddScore if so.
 *
 * PARAMETERS:  score - score to possibly add
 *
 * RETURNED:    nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	3/13/96	Initial Revision
 *			roger	3/19/96	Broke into separate routine
 *
 ***********************************************************************/
static void HighScoresCheckScore (void)
{
	int i;
	Handle nameH;
	CharPtr nameP;
	CharPtr firstSpaceP;
	FormPtr curFormP;
	FormPtr formP;
	Word objIndex;
	Word buttonHit;

	
	i = highScoreMax;
	while (i > 0 &&
		GameStatus.next.score > Prefs.highScore[i - 1].score)
		i--;
	
	
	// Leave if the score doesn't make it into the high scores.
	if (i >= highScoreMax)
		return;
	
	
	
	// Allocate a chunk for the user to edit.  The field in the dialog requires
	// the text to be in a chunk so it can be resized.
	nameH = MemHandleNew(dlkMaxUserNameLength + 1);
	nameP = MemHandleLock(nameH);
	
	
	// For the name, try and use the name last entered
	if (Prefs.lastHighScore != highScoreMax)
		{
		StrCopy(nameP, Prefs.highScore[Prefs.lastHighScore].name);
		}
	else
		{		
		StrCopy(nameP, "");
		
		// Try and use the user's name
		DlkGetSyncInfo(NULL, NULL, NULL, nameP, NULL, NULL);
		
		// Just use the first name
		firstSpaceP = StrChr(nameP, spaceChr);
		if (firstSpaceP)
		 	*firstSpaceP = '\0';
		
		// Truncate the string to insure it's not too long
		nameP[nameLengthMax] = '\0';
		}
	MemPtrUnlock(nameP);

	
	// Record this new score as the last one entered.
	Prefs.lastHighScore = i;
	
	
	// Now Display a dialog contragulating the user and ask for their name.
	curFormP = FrmGetActiveForm ();
	formP = FrmInitForm (NewHighScoresDialog);
	

	// Set the field to edit the name.
	objIndex = FrmGetObjectIndex (formP, NewHighScoresNameField);
	FldSetTextHandle(FrmGetObjectPtr (formP, objIndex),
		nameH);
	FrmSetFocus(formP, objIndex);			// Set the insertion point blinking in the only field
	
	// Set Graffiti to be shifted.
	GrfSetState(false, false, true);
	
	// Allow the user to type in a name.  Wait until a button is pushed. OK is 
	// the default button so if the app is switched the high score is still entered.
	// The user must press cancel to not record the score.
	buttonHit = FrmDoDialog (formP);

	
	// Take the text handle from the field so the text isn't deleted when the form is.
	FldSetTextHandle(FrmGetObjectPtr (formP, objIndex), 0);
	
	FrmDeleteForm (formP);					// Deletes the field's new text.
	FrmSetActiveForm (curFormP);
	
	
	// Add the score unless the user removed the name.  If so they probably didn't
	// want the score entered so don't!
	if (buttonHit == NewHighScoresOKButton &&
		nameP[0] != '\0')	
		HighScoresAddScore(nameP, GameStatus.next.score, GameStatus.level + 1, false);
	
	
	MemHandleFree(nameH);					// The name is now recorded and no longer needed
	
	
	// Now display where the new high score is in relation to the others
	if (buttonHit == NewHighScoresOKButton)
		HighScoresDisplay();
}


/***********************************************************************
 *
 * FUNCTION:    PreferencesDialogInit
 *
 * DESCRIPTION: Initialize the dialog's ui.  Sets the starting
 * level list.
 *
 * PARAMETERS:  frm
 *
 * RETURNED:    nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	11/21/96	Initial Revision
 *
 ***********************************************************************/
static void PreferencesDialogInit (FormPtr formP)
{
	ListPtr listP;
	UInt mappedValue;
	
	
	NewStartLevel = Prefs.startLevel;
	
	// Set the time format trigger and list
	listP = FrmGetObjectPtr (formP, FrmGetObjectIndex (formP, PreferencesStartingLevelList));
	mappedValue = MapToPosition ((BytePtr) StartLevelMappings,
										  Prefs.startLevel,
										  startLevelsSelectable,
										  defaultStartLevelItem);
	LstSetSelection(listP, mappedValue);

}


/***********************************************************************
 *
 * FUNCTION:    PreferencesDialogHandleEvent
 *
 * DESCRIPTION: This routine is the event handler for the 
 * "Preferences Dialog"
 *
 * PARAMETERS:  event  - a pointer to an EventType structure
 *
 * RETURNED:    true if the event has handle and should not be passed
 *              to a higher level handler.
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	11/21/96	Initial Revision
 *
 ***********************************************************************/
static Boolean PreferencesDialogHandleEvent (EventPtr event)
{
	FormPtr frm;
	Boolean handled = false;


	if (event->eType == lstSelectEvent)
		{
		switch (event->data.lstSelect.listID)
			{
			case PreferencesStartingLevelList:
				NewStartLevel = StartLevelMappings[event->data.lstSelect.selection];
				handled = true;
				break;
			}
		}

	else if (event->eType == ctlSelectEvent)
		{
		switch (event->data.ctlSelect.controlID)
			{
			case PreferencesOKButton:
				Prefs.startLevel = NewStartLevel;
				FrmReturnToForm(0);
				handled = true;
				break;

			case PreferencesCancelButton:
				FrmReturnToForm(0);
				handled = true;
				break;
				
			}
		}


	if (event->eType == frmOpenEvent)
		{
		frm = FrmGetActiveForm ();
		
		PreferencesDialogInit(frm);
		FrmDrawForm (frm);

		handled = true;
		}

		
	return (handled);
}


/***********************************************************************
 *
 * FUNCTION:    BoardViewDoCommand
 *
 * DESCRIPTION: Performs the menu command specified.
 *
 * PARAMETERS:  event  - a pointer to an EventType structure
 *
 * RETURNED:    true if the event has handle and should not be passed
 *              to a higher level handler.
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/30/96	Initial Revision
 *
 ***********************************************************************/
static Boolean BoardViewDoCommand (Word command)
{
	switch (command)
		{
		case BoardGameNewCmd:
			GameStart();
			break;

		case AboutCmd:
			AbtShowAbout (appFileCreator);
			break;
					
		case BoardGameInstructionsCmd:
			FrmHelp (InstructionsStr);
			break;
					
		case BoardGameHighScoresCmd:
			HighScoresDisplay();
			break;
					
		case BoardGamePreferencesCmd:
			FrmPopupForm (PreferencesDialog);
			break;
		}
	
	return true;
}


/***********************************************************************
 *
 * FUNCTION:    BoardViewHandleEvent
 *
 * DESCRIPTION: This routine is the event handler for the "Board View"
 *
 * PARAMETERS:  event  - a pointer to an EventType structure
 *
 * RETURNED:    true if the event has handle and should not be passed
 *              to a higher level handler.
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	11/1/95	Initial Revision
 *
 ***********************************************************************/
static Boolean BoardViewHandleEvent (EventPtr event)
{
	FormPtr frm;
	Boolean handled = false;


	if (event->eType == nilEvent)
		{
		}


	else if (event->eType == keyDownEvent)
		{
		// forward one level
		if (event->data.keyDown.chr == 'N')
			{
			GameStatus.cheatMode = true;
			GameStatus.bricksRemaining = 0;
			GameStatus.status = ballInMotion;
			}
		// back one level
		else if (event->data.keyDown.chr == 'P')
			{
			GameStatus.cheatMode = true;
			GameStatus.bricksRemaining = 0;
			GameStatus.status = ballInMotion;
			GameStatus.level = max (0, GameStatus.level - 2);
			}
		// replay level
		else if (event->data.keyDown.chr == 'R')
			{
			GameStatus.cheatMode = true;
			GameStatus.bricksRemaining = 0;
			GameStatus.status = ballInMotion;
			GameStatus.level = max (0, GameStatus.level - 1);
			}
		// bonus ball
		else if (event->data.keyDown.chr == 'B')
			{
			GameStatus.cheatMode = true;
			// Add a ball to those remaining and update the ball gauge.
			GameStatus.ballsRemaining++;
			GameDrawBallGauge();
			}
			
			
		// time spent playing		(quick code at this point.)
		else if (event->data.keyDown.chr == 't')
			{
			char timeString[timeStringLength + 5];
			ULong seconds;
			DateTimeType timeSpent;
			
			seconds = (Prefs.accumulatedTime + (TimGetTicks() - GameStatus.startTime)) / sysTicksPerSecond;
			TimSecondsToDateTime(seconds, &timeSpent);
			TimeToAscii(timeSpent.hour, timeSpent.minute, tfColon24h, timeString);
			StrCat(timeString, ":");
			
			if (timeSpent.second < 10)
				StrCat(timeString, "0");
			StrIToA(&timeString[StrLen(timeString)], timeSpent.second);
			WinDrawChars (timeString, StrLen(timeString), 68, 130);
			}
			
			
		if (event->data.keyDown.chr == releaseBallChr)
			{
			if (GameStatus.status == presentingLevel)
				{
				GameStateDraw();			// Remove the level name
				GameStatus.status = waitingForBall;
				}
				
			if (GameStatus.status == waitingForBall &&
				GameStatus.ballsRemaining > 0)
				{
				GamePlayABall();
				}
			}
		// Restart game using the page down key.  Don't allow this in the
		// middle of a game.  Othewise if the user accidentally presses it
		// they loose their game in progress!
		else if (event->data.keyDown.chr == restartGameChar)
			{
			if (GameStatus.status == checkHighScores)
				{
				GameStart();
				}
			}
		return true;
		}

		
	else if (event->eType == menuEvent)
		{
		BoardViewDoCommand (event->data.menu.itemID);
		return true;
		}

		
	else if (event->eType == frmCloseEvent)
		{
		}


	else if (event->eType == frmOpenEvent)
		{
		frm = FrmGetActiveForm ();

		FrmDrawForm (frm);
		GameStart ();

		handled = true;
		}

	else if (event->eType == frmUpdateEvent)
		{
		GameStateDraw();
		handled = true;
		}
		
	return (handled);
}



/***********************************************************************
 *
 * FUNCTION:    ApplicationHandleEvent
 *
 * DESCRIPTION: This routine loads form resources and sets the event
 *              handler for the form loaded.
 *
 * PARAMETERS:  event  - a pointer to an EventType structure
 *
 * RETURNED:    true if the event was handled and should not be passed
 *              to a higher level handler.
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	11/1/95	Initial Revision
 *
 ***********************************************************************/
static Boolean ApplicationHandleEvent (EventPtr event)
{
	Word formId;
	FormPtr frm;

	if (event->eType == frmLoadEvent)
		{
		// Load the form resource.
		formId = event->data.frmLoad.formID;
		frm = FrmInitForm (formId);
		FrmSetActiveForm (frm);		
		
		// Set the event handler for the form.  The handler of the currently
		// active form is called by FrmHandleEvent each time is receives an
		// event.
		switch (formId)
			{
			case BoardView:
				FrmSetEventHandler (frm, BoardViewHandleEvent);
				break;
		
			case PreferencesDialog:
				FrmSetEventHandler (frm, PreferencesDialogHandleEvent);
				break;
		
			}
		return (true);
		}
	return (false);
}


/***********************************************************************
 *
 * FUNCTION:    EventLoop
 *
 * DESCRIPTION: This routine is the event loop for the aplication.  
 *
 * PARAMETERS:  nothing
 *
 * RETURNED:    nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	11/1/95	Initial Revision
 *
 ***********************************************************************/
static void EventLoop (void)
{
	Word error;
	EventType event;

	do
		{
		// Wait until the next game period.
		EvtGetEvent (&event, TimeUntillNextPeriod());
		
		
		// Detect exiting the game's window.  This must be checked for.  At this
		// point there probably exists another window which may cover part of
		// the BoardView window.  Suppress drawing.  Otherwise drawing may draw
		// to part of the window covered by the new window.
		if (event.eType == winExitEvent)
			{
			if (event.data.winExit.exitWindow == (WinHandle) FrmGetFormPtr(BoardView))
				{
				GameStatus.paused = true;
				GameStatus.pausedTime = TimGetTicks();
				}
			}

		// Detect entering the game's window.  Resume drawing to our window.
		else if (event.eType == winEnterEvent)
			{
			// In the current code, the menu doesn't remove itself when it receives
			// a winExitEvent.  There also isn't a call to see if the menu is visible.
			// For now we make our own MenuGetVisible and check it each time our
			// BoardView is entered.
			if (event.data.winEnter.enterWindow == (WinHandle) FrmGetFormPtr(BoardView) &&
				event.data.winEnter.enterWindow == (WinHandle) FrmGetFirstForm())
				{
				// Sometimes we can enter the game's window without knowing it was 
				// ever left.  In that case the pause time will not have been recorded.
				// Set the current period back to it's beginning
				if (!GameStatus.paused)
					{
					GameStatus.nextPeriodTime = TimGetTicks() + GameStatus.periodLength +
						pauseLengthBeforeResumingInterruptedGame;
					}
				else
					{
					// Unpause the game.  Account for time lost during pause
					GameStatus.paused = false;
					GameStatus.nextPeriodTime += (TimGetTicks() - GameStatus.pausedTime) +
						pauseLengthBeforeResumingInterruptedGame;
					
					// Fixup the time spent playing the game by changing the startTime.
					GameStatus.startTime += (TimGetTicks() - GameStatus.pausedTime);
					}
				}
			}

		// If it's time, go to the next time period
		else if (TimeUntillNextPeriod() == 0)
			{
			GameStateDrawChanges();
			GameStateAdvance();
			GameStateElapse();
			GamePlaySounds();
			}

		// Intercept the hard keys to prevent them from switching apps
		if (event.eType == keyDownEvent && 
			event.data.keyDown.chr >= hard1Chr &&
			event.data.keyDown.chr <= hard4Chr &&
			GameStatus.status != checkHighScores &&
			!(event.data.keyDown.modifiers & poweredOnKeyMask))
			{
			continue;
			}

		
		
		if (! SysHandleEvent (&event))
		
			if (! MenuHandleEvent (0, &event, &error))
			
				if (! ApplicationHandleEvent (&event))
	
					FrmDispatchEvent (&event); 
		
		}
	while (event.eType != appStopEvent);
}


/***********************************************************************
 *
 * FUNCTION:    PilotMain
 *
 * DESCRIPTION: This is the main entry point for the application.
 *
 * PARAMETERS:  nothing
 *
 * RETURNED:    nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			roger	1/22/96	Initial Revision
 *
 ***********************************************************************/
 
DWord		PilotMain(Word cmd, Ptr cmdPBP, Word launchFlags)
{
	Word error;
	SystemPreferencesType systemPreferences;


	error = RomVersionCompatible (version30, launchFlags);
	if (error) return (error);


	if (cmd == sysAppLaunchCmdNormalLaunch)
		{
		error = StartApplication ();
	
	
		// Set the device locked preference if not set
		PrefGetPreferences(&systemPreferences);

		FrmGotoForm (BoardView);
		
		if (! error)
			EventLoop ();
	
		StopApplication ();
		}
	
	return 0;
}
