/*
* BreakView.m, view to implement the "BreakApp" game.
* Author: Ali Ozer
* Written for 0.8 October 88.
* Modified for 0.9 March 89.
* Modified for 1.0 July 89.
* Removed use of Bitmap and threw away some classes May 90.
* Final 2.0 fixes/enhancements Sept 90.
* 3.0 update March 92.
* Sound-related changes for 3.1 March 92.
*
* BreakView implements an interactive custom view that allows the user
* to play "BreakApp," a game similar to a popular arcade classic.
*
* BreakView's main control methods are based on the target-action
* paradigm; thus you can include BreakView in an Interface-Builder based
* application. Please refer to BreakView.h for a list of "public" methods
* that you should provide links to in Interface Builder.
*
* You may freely copy, distribute and reuse the code in this example.
* NeXT disclaims any warranty of any kind, expressed or implied,
* as to its fitness for any particular use.
*/

#import <libc.h>
#import <math.h>
#import <defaults/defaults.h> // For writing/reading high score
#import <appkit/appkit.h>

#import "BreakView.h"
#import "SoundEffect.h"

// Max absolute x and y velocities of the ball, in base coordinates per msec.

#define MAXXV ((level > 6) ? 0.3 : 0.2)
#define MAXYV (0.4)

// Maximum amount of time that is allowed to pass between two calls to the
// step method. If the time is greater than MAXTIMEDIFFERENCE, then this
// value is used instead. MAXTIMEDIFFERENCE should be no greater
// than the time it takes for the ball to go the height of a tile
// or the height of the ball + height of paddle. The units
// are in milliseconds.

#define MAXTIMEDIFFERENCE (TILEHEIGHT * 0.8 / MAXYV)
#define MINTIMEDIFFERENCE 1

// Max revolution speed of the ball; this is the maximum
// number of radians it will turn per millisecond when rotating...

#define MAXREVOLUTIONSPEED (M_PI / 250.0) // Max is 2 revs/sec

// The following values are the default sizes for the various pieces.

#define RADIUS 8.0 // Ball radius
#define PADDLEWIDTH (TILEWIDTH * 1.8) // Paddle width
#define PADDLEHEIGHT (TILEHEIGHT * 0.6) // Paddle height
#define BALLWIDTH (RADIUS * 2.0) // Ball width
#define BALLHEIGHT (RADIUS * 2.0) // Ball height

// SHADOWOFFSET defines the amount the shadow is offset from the piece.

#define SHADOWOFFSET 3.0

#define LIVES 5 // Number of lives per game

#define STOPGAMEAT (-10) // Number of loops through the
// game after all tiles die

#define LEVELBONUS 50 // Bonus at the end of a level

// Starting locations...

#define PADDLEX ((gameSize.width - paddleSize.width) / 2.0)
#define PADDLEY 1.0
#define BALLX ((gameSize.width - ballSize.width) / 2.0)
#define BALLY (paddleY + paddleSize.height)

// Accelaration & score values of the different tile types.

static const float tileAccs[NUMTILETYPES] = {1.0, 1.3};
static const int tileScores[NUMTILETYPES] = {5, 25};

#define NOTILE -1

extern void srandom(); // Hmm; not in libc.h
#define RANDINT(n) (random() % ((n)+1)) // Random integer 0..n
#define ONEIN(n) ((random() % (n)) == 0) // TRUE one in n times
#define INITRAND srandom(time(0)) // Randomizer

#define gameSize bounds.size

// Restrict a value to the range -max .. max.

inline float restrictValue(float val, float max)
{
if (val > max) return max;
else if (val < -max) return -max;
else return val;
}

// Convert x-location to left/right pan for playing sounds

@implementation BreakView

- initFrame:(const NXRect *)frm
{
[super initFrame:frm];

[self allocateGState]; // For faster lock/unlockFocus

[(ball = [[NXImage allocFromZone:[self zone]] init]) setScalable:NO];
[ball useDrawMethod:@selector(drawBall:) inObject:self];

[(paddle = [[NXImage allocFromZone:[self zone]] init]) setScalable:NO];
[paddle useDrawMethod:@selector(drawPaddle:) inObject:self];

[(tile[0] = [[NXImage allocFromZone:[self zone]] init]) setScalable:NO];
[(tile[1] = [[NXImage allocFromZone:[self zone]] init]) setScalable:NO];
[tile[0] useDrawMethod:@selector(drawNormalTile:) inObject:self];
[tile[1] useDrawMethod:@selector(drawToughTile:) inObject:self];

wallSound = [[SoundEffect allocFromZone:[self zone]] initFromSection:"Wall.snd"];
tileSound = [[SoundEffect allocFromZone:[self zone]] initFromSection:"Tile.snd"];
missSound = [[SoundEffect allocFromZone:[self zone]] initFromSection:"Miss.snd"];
paddleSound = [[SoundEffect allocFromZone:[self zone]] initFromSection:"Paddle.snd"];

[self setBackgroundFile:NXGetDefaultValue([NXApp appName], "BackGround") andRemember:NO];

[self resizePieces];

[self getHighScore];

demoMode = NO;

INITRAND;

return self;
}

// free simply gets rid of everything we created for BreakView, including
// the instance of BreakView itself. This is how nice objects clean up.

- free
{
int cnt;

if (gameRunning) {
DPSRemoveTimedEntry (timer);
}
for (cnt = 0; cnt < NUMTILETYPES; cnt++) {
[tile[cnt] free];
}

[ball free];
[paddle free];
[backGround free];
[wallSound free];
[tileSound free];
[missSound free];
[paddleSound free];

return [super free];
}

// resizePieces calculates the new sizes of all the pieces after the game is
// started or the playing field (the BreakView) is resized.

- resizePieces
{
int cnt;
float xRatio = gameSize.width / GAMEWIDTH;
float yRatio = gameSize.height / GAMEHEIGHT;

[backGround setSize:&gameSize];

tileSize.width = floor(xRatio * TILEWIDTH);
tileSize.height = floor(yRatio * TILEHEIGHT);
for (cnt = 0; cnt < NUMTILETYPES; cnt++) {
[tile[cnt] setSize:&tileSize];
}
leftMargin = floor((gameSize.width - (tileSize.width + INTERTILE) *
NUMTILESX) / 2.0 + 1.0);

paddleSize.width = floor(xRatio * PADDLEWIDTH);
paddleSize.height = floor(yRatio * PADDLEHEIGHT);
[paddle setSize:&paddleSize];

ballSize.width = floor(xRatio * BALLWIDTH);
ballSize.height = floor(yRatio * BALLHEIGHT);
[ball setSize:&ballSize];

return self;
}

// The following allows BreakView to grab the mousedown event that activates
// the window. By default, the View's acceptsFirstMouse returns NO.

- (BOOL)acceptsFirstMouse
{
return YES;
}

// This methods allows changing the file used to paint the background of the
// playing field. Set fileName to NULL to revert to the default. Set
// remember to YES if you wish the write the value out in the defaults.

- setBackgroundFile:(const char *)fileName andRemember:(BOOL)remember
{
[backGround free];
backGround = [[NXImage allocFromZone:[self zone]] initSize:&gameSize];
if (fileName) {
[backGround useFromFile:fileName];
[backGround setScalable:YES];
if (remember) {
NXWriteDefault ([NXApp appName], "BackGround", fileName);
}
} else {
[backGround useDrawMethod:@selector(drawDefaultBackground:) inObject:self];
[backGround setScalable:NO];
if (remember) {
NXRemoveDefault ([NXApp appName], "BackGround");
}
}
[backGround setBackgroundColor:NX_COLORWHITE];
[self display];

return self;
}

// The following two methods allow changing the background image from
// menu items or buttons.

- changeBackground:sender
{
if ([[OpenPanel new] runModalForTypes:[NXImage imageFileTypes]]) {
[self setBackgroundFile:[[OpenPanel new] filename] andRemember:YES];
[self display];
}

return self;
}

- revertBackground:sender
{
[self setBackgroundFile:NULL andRemember:YES];
[self display];
return self;
}

// getHighScore reads the previous high score from the user's defaults file.
// If no such default is found, then the high score is set to zero.

- getHighScore
{
const char *tmpstr;
if (((tmpstr = NXGetDefaultValue ([NXApp appName], "HighScore")) &&
(sscanf(tmpstr, "%d", &highScore) != 1))) highScore = 0;

return self;
}

// setHighScore should be called when the user score for a game is above
// the current high score. setHighScore sets the high score and
// writes it out the defaults file so that it can be remembered for eternity.

- setHighScore:(int)hScore
{
char str[10];
[hscoreView setIntValue:(highScore = hScore)];
sprintf (str, "%d", highScore);
NXWriteDefault ([NXApp appName], "HighScore", str);
return self;
}

- (int)score
{
return score;
}

- (int)level
{
return level;
}

- (int)lives
{
return lives;
}

// gotoFirstLevel: sets everything up for a new game.

- gotoFirstLevel:sender
{
score = 0;
level = 0;
lives = LIVES;
return [self gotoNextLevel:sender];
}

// gotoNextLevel: sets everything up for the next level of the game; the level
// count is incremented and the pieces are set up on the field. The ball and
// the paddle are also brought to the starting locations.
//
// This routine can of course be made infinitely more complicated in
// determining where the tiles go. Left as an exercise to the reader. 8-)

- gotoNextLevel:sender
{
int xcnt, ycnt, yFrom, yTo, xFrom, xTo;

// We are at the next level... Stop the game and increment the level.

[self stop:sender];

level++;

// Now place the tiles. Here's where we could do some fancy tile layout,
// depending on the game level. yFrom, yTo, xFrom, and xTo define the "box"
// in which we will lay the tiles out. These values are inclusive.

switch (level % 6) {
case 0: yTo = NUMTILESY-2; break;
case 4: yTo = NUMTILESY-4; break;
case 5: yTo = 2 * (NUMTILESY / 3); break;
default: yTo = 3 * (NUMTILESY / 4); break;
}

xFrom = 0; xTo = NUMTILESX-1; yFrom = yTo - 3;

switch (level % 10) {
case 1: yFrom++; break;
case 2: yFrom--; xFrom++; xTo--; break;
case 4: xFrom += 2; xTo -= 2; break;
case 6: yFrom = MIN(yFrom, NUMTILESY / 4); xFrom++; xTo--; break;
case 7: xTo -= 3; break;
case 8: yFrom -= 2; xFrom += 2; xTo -= 2; break;
case 9: yFrom = MIN(yFrom, NUMTILESY / 4);
yTo = MAX(yTo, yFrom+4);
break;
case 0: yFrom = MIN(yFrom, NUMTILESY / 5);
xFrom += (NUMTILESX / 2);
break;
default: break;
}

// The area in the playing field where we place tiles is at least 3 tiles
// high and at least NUMTILESX-4 tiles wide.

// Empty out the whole playing field.
for (xcnt = 0; xcnt < NUMTILESX; xcnt++) {
for (ycnt = 0; ycnt < NUMTILESY; ycnt++) {
tiles[xcnt][ycnt] = NOTILE;
}
}

// Fill up the tile area with wimpy tiles
for (ycnt = yFrom; ycnt <= yTo; ycnt++) {
for (xcnt = xFrom; xcnt <= xTo; xcnt++) {
tiles[xcnt][ycnt] = 0;
}
}

// Erase or change some of the tiles, depending on the level.
// Assumption is that we have at least 3 rows of tiles, yFrom..yTo.

switch (level % 7) {
case 2: // clear two rows in the middle
for (xcnt = xFrom; xcnt <= xTo; xcnt++) {
tiles[xcnt][yFrom+1] = tiles[xcnt][yTo-1] = NOTILE;
}
break;
case 3: // randomly clear out some tiles
for (xcnt = 0; xcnt < 5; xcnt++) {
tiles[xFrom+RANDINT(xTo-xFrom)][yFrom+1+RANDINT(yTo-yFrom-2)] =
NOTILE;
}
break;
case 4: // clear middle columns
for (xcnt = xFrom + 2; xcnt <= xTo - 2; xcnt++) {
for (ycnt = yFrom; ycnt <= yTo; ycnt++) {
tiles[xcnt][ycnt] = NOTILE;
}
}
break;
case 6: // clear out the insides
for (ycnt = yFrom+1; ycnt < yTo; ycnt++) {
for (xcnt = xFrom+1; xcnt < xTo; xcnt++) {
tiles[xcnt][ycnt] = NOTILE;
}
}
break;
default:
break;
}

// Drop in some tough tiles in all rows except the first one
for (xcnt = 0; xcnt < 5; xcnt++) {
tiles[xFrom+RANDINT(xTo-xFrom)][yFrom+1+RANDINT(yTo-yFrom-1)] = 1;
}

// Compute the number of tiles we actually ended up putting down...
numTilesLeft = 0;
for (ycnt = yFrom; ycnt <= yTo; ycnt++) {
for (xcnt = xFrom; xcnt <= xTo; xcnt++) {
if (tiles[xcnt][ycnt] != NOTILE) numTilesLeft++;
}
}

// Of course you might think there are too many braces in the above code,
// where probably none would've sufficed. Too many braces never hurt, & it
// will save you from some bozo bug some day. So use them! They're cheap!

[self resetBallAndPaddle];

[levelView setIntValue:level];
[scoreView setIntValue:score];
[livesView setIntValue:lives];
[hscoreView setIntValue:highScore];
[statusView setStringValue:NXLocalString("Game Ready", NULL, "Message indicating the game is ready to run")];

killerBall = ((level % 12) == 0); // Every 12 turns, the loses its
// ability to bounce off tiles
niceBall = (level % 5 == 0); // Every 5 turns, make the ball
// bounce towards the paddle

// If the background image is not from a file but our own default,
// poke it so its redrawn. This way every level will look different.
// We could've simply used a BOOL to remember if the image is the default
// one, but this test here works as well.

if ([[backGround lastRepresentation] isKindOf:[NXCustomImageRep class]]) {
[backGround recache];
}

[self display]; // Display the new arrangement

if (demoMode) {
[self go:sender]; // If in demo mode, start rolling
}

return self;
}

// setDemoMode: allows the user to put the game in a demo mode.
// In the demo mode, the paddle constantly follows the ball.

- setDemoMode:sender
{
if (demoMode = ([sender state] == 0 ? NO : YES)) {
[self go:sender];
} else {
[self stop:sender];
}
return self;
}

// This method should be called when a new level or game is started or the
// player misses the ball. It resets the ball & paddle locations back to
// default.

- resetBallAndPaddle
{
paddleX = PADDLEX;
paddleY = PADDLEY;
ballX = BALLX;
ballY = BALLY;

ballXVel = 0.0;
ballYVel = 0.0;

// The ball shouldn't start out rotating...
revolutionsLeft = 0;

return self;
}

// The directBallAt: initializes the velocity vector of the ball so that
// the ball will go from its current location to the specified destination
// point. The speed of the ball is determined by the current level. If ballYVel
// is already set, then only the x velocity & y direction is changed.

- directBallAt:(NXPoint *)dest
{
float desiredYVel = dest->y - (ballY + ballSize.height / 2.0);
float desiredXVel = dest->x - (ballX + ballSize.width / 2.0);

// Transform back to original game coords (velocity values are measured
// in these).

desiredYVel /= (gameSize.height / GAMEHEIGHT);
desiredXVel /= (gameSize.width / GAMEWIDTH);

if (fabs(desiredYVel) < 1.0) {
desiredYVel = desiredYVel < 0.0 ? -1.0 : 1.0;
}
if (ballYVel == 0.0) {
// Come up with a value between 60 and 100% of MAXYV.
ballYVel = restrictValue(((RANDINT(level * 8) + 60.0) / 100.0) * MAXYV,
MAXYV);
}
ballYVel = fabs(ballYVel) * (desiredYVel < 0.0 ? -1.0 : 1.0);
ballXVel = restrictValue(ballYVel * (desiredXVel / desiredYVel) ,MAXXV);

return self;
}

// The stop method will pause a running game. The go method will start it up
// again. They can be assigned to buttons or other appkit objects through IB.

- go:sender
{
void runOneStep ();
if (lives && !gameRunning) {
// If the ball velocity wasn't initialized, start it rolling
// towards the mouse location...
if (ballXVel == 0.0 && ballYVel == 0.0) {
NXPoint mouseLoc;
[[self window] getMouseLocation:&mouseLoc];
[self convertPoint:&mouseLoc fromView:nil];
[self directBallAt:&mouseLoc];
ballYVel = fabs(ballYVel);
}
gameRunning = YES;
timer = DPSAddTimedEntry(0.03, &runOneStep, self, NX_BASETHRESHOLD);
[statusView setStringValue:NXLocalString("Running", NULL, "Message indicating that the game is running")];
}
return self;
}

- stop:sender
{
if (gameRunning) {
gameRunning = NO;
DPSRemoveTimedEntry (timer);
[statusView setStringValue:NXLocalString("Paused", NULL, "Message indicating that the game is paused")];
}

return self;
}

- sizeTo:(NXCoord)width :(NXCoord)height
{
NXSize oldSize = bounds.size;

[super sizeTo:width :height];

ballX = (ballX * width / oldSize.width);
ballY = (ballY * height / oldSize.height);
paddleX = (paddleX * width / oldSize.width);
paddleY = (paddleY * height / oldSize.height);

[self resizePieces];

[self display];
return self;
}

// A mousedown effectively allows pausing and unpausing the game by
// alternately calling one of the above two functions (stop/go).

- mouseDown:(NXEvent *)event
{
if (gameRunning) {
[self stop:self];
} else if (lives) {
[self go:self];
}
return self;
}

// The following few methods draw the pieces.

- drawBall:imageRep
{
PSscale (ballSize.width / BALLWIDTH, ballSize.height / BALLHEIGHT);

// First draw the shadow under the ball.

PSarc (RADIUS+SHADOWOFFSET/2, RADIUS-SHADOWOFFSET/2,
RADIUS-SHADOWOFFSET-1.0, 0.0, 360.0);
PSsetgray (NX_BLACK);
if (NXDrawingStatus == NX_DRAWING) {
PSsetalpha (0.666);
}
PSfill ();
if (NXDrawingStatus == NX_DRAWING) {
PSsetalpha (1.0);
}

// Then the ball.

PSarc (RADIUS-SHADOWOFFSET/2, RADIUS+SHADOWOFFSET/2,
RADIUS-SHADOWOFFSET-1.0, 0.0, 360.0);
PSsetgray (NX_LTGRAY);
PSfill ();

// And the lighter & darker spots on the ball...

PSarcn (RADIUS-SHADOWOFFSET/2, RADIUS+SHADOWOFFSET/2,
RADIUS-SHADOWOFFSET-3.0, 170.0, 100.0);
PSarc (RADIUS-SHADOWOFFSET/2, RADIUS+SHADOWOFFSET/2,
RADIUS-SHADOWOFFSET-2.0, 100.0, 170.0);
PSsetgray (NX_WHITE);
PSfill ();
PSarcn (RADIUS-SHADOWOFFSET/2, RADIUS+SHADOWOFFSET/2,
RADIUS-SHADOWOFFSET-2.0, 350.0, 280.0);
PSarc (RADIUS-SHADOWOFFSET/2, RADIUS+SHADOWOFFSET/2,
RADIUS-SHADOWOFFSET-2.0, 280.0, 350.0);
PSsetgray (NX_DKGRAY);
PSfill ();

return self;
}

// Function to draw a shadow under the given rectangle.

static void drawRectangularShadowUnder (NXRect *rect, float offset)
{
NXRect shadeRect = *rect;
NXOffsetRect (&shadeRect, offset, -offset);

PSsetgray (NX_BLACK);
if (NXDrawingStatus != NX_PRINTING) {
PSsetalpha (0.666);
}
NXRectFill (&shadeRect);
if (NXDrawingStatus != NX_PRINTING) {
PSsetalpha (1.0);
}
}

- drawPaddle:imageRep
{
NXRect pieceRect = {{0.0, SHADOWOFFSET},
{(paddleSize.width-SHADOWOFFSET)-1,
(paddleSize.height-SHADOWOFFSET)-1}};

drawRectangularShadowUnder (&pieceRect, SHADOWOFFSET);

NXDrawButton (&pieceRect, NULL);

return self;
}

- drawToughTile:imageRep
{
NXRect pieceRect = {{0.0, SHADOWOFFSET},
{(tileSize.width-SHADOWOFFSET)-1,
(tileSize.height-SHADOWOFFSET)-1}};

drawRectangularShadowUnder (&pieceRect, SHADOWOFFSET);

NXDrawButton (&pieceRect, NULL);
NXInsetRect (&pieceRect, 3.0, 3.0);
NXDrawWhiteBezel (&pieceRect, NULL);

return self;
}

- drawNormalTile:imageRep
{
NXRect pieceRect = {{0.0, SHADOWOFFSET},
{(tileSize.width-SHADOWOFFSET)-1,
(tileSize.height-SHADOWOFFSET)-1}};

drawRectangularShadowUnder (&pieceRect, SHADOWOFFSET);

NXDrawButton (&pieceRect, NULL);

return self;
}

#define NUMYBOXES 10
#define NUMXBOXES 6

// This method draws the default background. The default background consists of
// NUMXBOXES x NUMYBOXES raised boxes. Each box is drawn as four triangles to
// provide a raised effect. Boxes near the top left corner are lighter in color
// than the ones near the bottom right.

- drawDefaultBackground:imageRep
{
#define NOTFOUND ((id)-1)
static NXColorList *colorList = nil; // Static because it's shared
NXSize boxSize = {gameSize.width / NUMXBOXES, gameSize.height / NUMYBOXES};
int xCnt, yCnt;
NXColor color;

// The first time we're here, we load and cache the color list. If we don't find
// it, we remember that fact so that we don't go through the search again.
if (colorList == nil) {
char colorListPath[MAXPATHLEN];
if ([[NXBundle mainBundle] getPath:colorListPath forResource:"BreakApp" ofType:"clr"]) {
colorList = [[NXColorList allocFromZone:NXDefaultMallocZone()] initWithName:NULL fromFile:colorListPath];
}
if (!colorList) {
NXLogError ("Can't find color list for backgrounds.");
colorList = NOTFOUND;
}
}

// Now get the color. If the color list wasn't found, we use some default random color.
// Note that because colors in different color spaces might look different on
// different devices (although they might look identical on screen), it's
// important to always use colors from the same color space when creating
// a wash. Below we assure that our colors always start off in HSB color space.

if (colorList != NOTFOUND) {
color = [colorList colorNamed:[colorList nameOfColorAt:(level % [colorList colorCount])]];
color = NXConvertHSBToColor (NXHueComponent(color), NXSaturationComponent(color), 1.0);
} else {
color = NXConvertHSBToColor((level % 8) / 7.0, 0.8, 1.0);
}

for (yCnt = 0; yCnt < NUMYBOXES; yCnt++) {
for (xCnt = 0; xCnt < NUMXBOXES; xCnt++) {
// Determine brightness (each box has a different brightness)
color = NXChangeBrightnessComponent(color, 0.4 + (yCnt + (4 - xCnt)) * (0.2 / (NUMYBOXES + NUMXBOXES)));
// The bottom triangle
PSmoveto (xCnt * boxSize.width + boxSize.width / 2.0, yCnt * boxSize.height + boxSize.height / 2.0);
PSrlineto (-boxSize.width / 2.0, -boxSize.height / 2.0);
PSrlineto (boxSize.width, 0);
NXSetColor (color);
PSfill ();
// The right triangle
PSmoveto (xCnt * boxSize.width + boxSize.width / 2.0, yCnt * boxSize.height + boxSize.height / 2.0);
PSrlineto (boxSize.width / 2.0, boxSize.height / 2.0);
PSrlineto (0, -boxSize.height);
NXSetColor (NXChangeBrightnessComponent(color, NXBrightnessComponent(color) * 1.2));
PSfill ();
// The left triangle
PSmoveto (xCnt * boxSize.width + boxSize.width / 2.0, yCnt * boxSize.height + boxSize.height / 2.0);
PSrlineto (-boxSize.width / 2.0, boxSize.height / 2.0);
PSrlineto (0, -boxSize.height);
NXSetColor (NXChangeBrightnessComponent(color, NXBrightnessComponent(color) * 1.2 * 1.2));
PSfill ();
// The right triangle
PSmoveto (xCnt * boxSize.width + boxSize.width / 2.0, yCnt * boxSize.height + boxSize.height / 2.0);
PSrlineto (boxSize.width / 2.0, boxSize.height / 2.0);
PSrlineto (-boxSize.width, 0.0);
NXSetColor (NXChangeBrightnessComponent(color, NXBrightnessComponent(color) * 1.2 * 1.2 * 1.2));
PSfill ();
}
}
return self;
}

// The following methods show or erase the ball and the paddle from the field.

- showBall
{
NXRect tmpRect = {{floor(ballX), floor(ballY)},
{ballSize.width, ballSize.height}};
[ball composite:NX_SOVER toPoint:&tmpRect.origin];
return self;
}

- showPaddle
{
NXRect tmpRect = {{floor(paddleX), floor(paddleY)},
{paddleSize.width, paddleSize.height}};
[paddle composite:NX_SOVER toPoint:&tmpRect.origin];
return self;
}

- eraseBall
{
NXRect tmpRect = {{ballX, ballY}, {ballSize.width, ballSize.height}};
return [self drawBackground:&tmpRect];
}

- erasePaddle
{
NXRect tmpRect = {{paddleX, paddleY},
{paddleSize.width, paddleSize.height}};
return [self drawBackground:&tmpRect];
}

// drawBackground: just draws the specified piece of the background by
// compositing from the background image.

- drawBackground:(NXRect *)rect
{
NXRect tmpRect = *rect;

NX_X(&tmpRect) = floor(NX_X(&tmpRect));
NX_Y(&tmpRect) = floor(NX_Y(&tmpRect));
if (NXDrawingStatus == NX_DRAWING) {
PSsetgray (NX_WHITE);
PScompositerect (NX_X(&tmpRect), NX_Y(&tmpRect),
NX_WIDTH(&tmpRect), NX_HEIGHT(&tmpRect), NX_COPY);
}
[backGround composite:NX_SOVER fromRect:&tmpRect toPoint:&tmpRect.origin];
return self;
}

// drawSelf::, a method every decent View should have, redraws the game
// in its current state. This allows us to print the game very easily.

- drawSelf:(NXRect *)rects :(int)rectCount
{
int xcnt, ycnt;

[self drawBackground:(rects ? rects : &bounds)];

for (xcnt = 0; xcnt < NUMTILESX; xcnt++) {
for (ycnt = 0; ycnt < NUMTILESY; ycnt++) {
if (tiles[xcnt][ycnt] != NOTILE) {
NXPoint tileLoc = {floor(leftMargin + (tileSize.width + INTERTILE) * xcnt), floor((tileSize.height + INTERTILE) * ycnt)};
[tile[tiles[xcnt][ycnt]] composite:NX_SOVER toPoint:&tileLoc];
}
}
}

if (lives) {
[self showBall];
[self showPaddle];
}

return self;
}

// incrementGameScore: adds the value of the argument to the score if the game
// is not in demo mode.

- incrementGameScore:(int)scoreIncrement
{
if (demoMode == NO) {
score += scoreIncrement;
}
return self;
}

// hitTileAt:: checks to see if there's a tile at tile location x, y;
// if so, it is considered hit by the ball and cleared. hitTileTile:: also
// updates the score and the ball velocity. hitTileAt:: returns YES if there
// was a tile, NO otherwise.

-(BOOL) hitTileAt:(int)x :(int)y
{
NXRect rect = {{floor(leftMargin + (tileSize.width + INTERTILE) * x),
floor((tileSize.height + INTERTILE) * y)},
{tileSize.width, tileSize.height}};

if (x < NUMTILESX && y < NUMTILESY && x >= 0 && y >= 0 &&
(tiles[x][y] != NOTILE)) {
[self incrementGameScore:tileScores[tiles[x][y]]];
ballYVel = restrictValue(ballYVel * tileAccs[tiles[x][y]], MAXYV);
[self drawBackground:&rect];
tiles[x][y] = NOTILE;
numTilesLeft--;
return YES;
} else {
return NO;
}
}

// The paddleHit method is called whenever the ball hits the paddle.
// This method bounces the ball back at an angle depending on what part of
// the paddle was hit.

- paddleHit
{
float whereHit = ((ballX + RADIUS) - paddleX) / paddleSize.width;

ballYVel = -ballYVel;
ballY = paddleSize.height;

[self playSound:paddleSound atXLoc:paddleX];

// Alter the x-velocity and make sure it is in the valid range.
// If the ball hits the edges of the paddle, bounce it back at some angle.

if (whereHit < 0.1) {
ballXVel = - MAXXV;
} else if (whereHit > 0.9) {
ballXVel = MAXXV;
} else {
// Now whereHit is in the range 0.1 .. 0.9, with 0.5 indicating middle
// of the paddle. Convert to a number in the range 0.2 to 1, with 0.2
// indicating the middle and 1 either end.
whereHit = (fabs(whereHit - 0.5) + 0.1) * 2.0;
ballXVel = ((ballXVel > 0.0) ? 1.0 : -1.0) * MAXXV * whereHit;
}

return self;
}

// If upon launch we discover that there's no sound, then we fail
// silently. Note that although the SoundEffect class has the ability
// to enable/disable sounds, because we might have multiple
// BreakViews each with its own sound state, we keep a local state in
// addition to the one in SoundEffect.

// Note that although this is an outlet method, we do not actually
// have an outlet (instance variable) named soundStateFrom; we just
// want to take a look at the initial value of the button and see
// if sound needs to be turned on...

- setSoundStateFrom:sender
{
if ([sender state]) {
[SoundEffect setSoundEnabled:YES];
if (!(soundEnabled = [SoundEffect soundEnabled])) {
[sender setState:NO]; // Silently fail
}
} else {
soundEnabled = NO;
}
return self;
}

// If user tries to enable sound once the game is launched, and it
// fails, then we do tell him/her about it.

- setSoundMode:sender
{
BOOL desiredState = [sender state];
[self setSoundStateFrom:sender];
if (desiredState && !soundEnabled) {
NXRunAlertPanel (
NXLocalString ("No Sound", NULL, "Title of alert indicating sounds aren't available"),
NXLocalString ("Can't play sounds.", NULL, "Contents of alert panel"),
NXLocalString ("Bummer", NULL, "Acceptance that sounds can't be played"),
NULL, NULL);
}
return self;
}

- (void)playSound:sound atXLoc:(float)xLoc
{
if (soundEnabled) {
[sound play:1.0 pan:restrictValue((xLoc / gameSize.width - 0.5) * 2.0, 1.0)];
}
}

// Alters the given velocity vector so that it is
// rotated by the indicated amount. We restrict both the resulting x and v
// velocity values to the maximum of their max possible values...

- rotate:(float *)xVel :(float *)yVel by:(float)radians
{
float newAngle = atan2 (*yVel, *xVel) + radians;
float velocity = hypot (*xVel, *yVel);

*yVel = restrictValue(velocity * sin(newAngle), MAX(MAXYV, MAXXV));
*xVel = restrictValue(velocity * cos(newAngle), MAX(MAXYV, MAXXV));

return self;
}

// The step method implements one step through the main game loop.
// The distance traveled by the ball is adjusted by the time between frames.

- step:(double)timeNow
{
NXPoint mouseLoc;
float newX;
unsigned int timeDelta = MIN(MAX((timeNow - lastFrameTime) * 1000, MINTIMEDIFFERENCE), MAXTIMEDIFFERENCE);
lastFrameTime = timeNow;

[self lockFocus];

[self eraseBall];

// If the ball is rotating, rotate it by the indicated amount.

if (revolutionsLeft > 0.0) {
float revsThisTime = revolutionSpeed * timeDelta;
[self rotate:&ballXVel :&ballYVel by:revsThisTime];
revolutionsLeft -= revsThisTime;
if (revolutionsLeft <= 0.0 && (fabs(ballYVel) < MAXYV * 0.6)) {
// Done rotating; make sure we have a good y-velocity
ballYVel = MAXYV * 0.8 * (ballYVel < 0.0 ? -1 : 1);
ballXVel = restrictValue(ballXVel,MAXXV);
}
} else if (ONEIN(1000 + (level < 8 ? (8 - level) * 250 : 0)) && (ballY > gameSize.height * 0.6)) {
// If we're not rotating, we go into rotating mode one out of
// 1500 or more steps, provided that the ball is not too close to
// the paddle at the time.
revolutionsLeft = M_PI * (2 + RANDINT(5)); // 1 to 3.5 full turns
revolutionSpeed = MAXREVOLUTIONSPEED * (RANDINT(8) + 2.0) / 10.0;
}

// Update the ball location

ballX += ballXVel * timeDelta * gameSize.width / GAMEWIDTH;
ballY += ballYVel * timeDelta * gameSize.height / GAMEHEIGHT;

if (gameRunning) {

if (ballX < 0.0) { // Hit on the left wall
ballX = 0.0;
ballXVel = -ballXVel;
[self playSound:wallSound atXLoc:ballX];
} else if (ballX > gameSize.width - ballSize.width) { // Right wall
ballX = gameSize.width - ballSize.width;
ballXVel = -ballXVel;
[self playSound:wallSound atXLoc:ballX];
}

if (ballY > gameSize.height - ballSize.height) { // Top wall
ballY = gameSize.height - ballSize.height;
ballYVel = -ballYVel;
if (niceBall && !ONEIN(5) && !demoMode) {
NXPoint mid = {paddleX + paddleSize.width / 2.0, paddleY};
[self directBallAt:&mid];
} else if (ONEIN(10)) {
ballXVel = MAXXV-(RANDINT((int)(MAXXV*20))/10.0);
}
[self playSound:wallSound atXLoc:ballX];
}

// Now checking for collisions with tiles...

{
int y1 = (int)(floor(ballY /
(tileSize.height + INTERTILE)));
int x1 = (int)(floor((ballX - leftMargin) /
(tileSize.width + INTERTILE)));
int y2 = (int)(floor((ballY + ballSize.height) /
(tileSize.height + INTERTILE)));
int x2 = (int)(floor((ballX + ballSize.width - leftMargin) /
(tileSize.width + INTERTILE)));

if ([self hitTileAt:x1 :y1] | [self hitTileAt:x2 :y1] |
[self hitTileAt:x1 :y2] | [self hitTileAt:x2 :y2]) {
[self playSound:tileSound atXLoc:ballX];
if (!killerBall) {
ballYVel = -ballYVel;
}
[scoreView setIntValue:score];
[[self window] flushWindow];
}
}
}

// Get the mouse location and convert from window to the view coords.
// If in demo, mode, make the paddle track the ball. Endless fun.

if (demoMode) {
mouseLoc.x = ballX + ballSize.width / 2.0;
} else {
[[self window] getMouseLocation:&mouseLoc];
[self convertPoint:&mouseLoc fromView:nil];
}

newX = MAX(MIN(mouseLoc.x - paddleSize.width/2,
gameSize.width - paddleSize.width), 0);

if (ballY >= paddleY + paddleSize.height) {

// Ball is above the paddle; redraw it and the paddle and continue
// We flush twice as the ball and the paddle are not too close
// together

[self showBall];
[[self window] flushWindow];
[self erasePaddle];
paddleX = newX;
[self showPaddle];
[[self window] flushWindow];

} else if (ballY + ballSize.height > 0) {

// Ball is past the paddle but not totally gone...

[self erasePaddle];
paddleX = newX;

// Check to see if the user managed to catch the ball after all

if ((ballY > paddleY - ballSize.height / 2.0) &&
(ballX <= paddleX + paddleSize.width) &&
(ballX + ballSize.width > paddleX)) {
[self paddleHit];
}

// The ball and the paddle are close, so one flushWindow is fine.

[self showBall];
[self showPaddle];
[[self window] flushWindow];

} else {

// Too late; the ball is out of sight...

[self erasePaddle];
[self stop:self];
[self playSound:missSound atXLoc:0.0];

if (--lives == 0) {
if (score > highScore) [self setHighScore:score];
[statusView setStringValue:NXLocalString("Game Over", NULL, "Message indicating that the game is over...")];
} else {
[self resetBallAndPaddle];
[self showBall];
[self showPaddle];
[statusView setStringValue:NXLocalString("Game Ready", NULL, "Message indicating the game is ready to run")];
}
[[self window] flushWindow];

[livesView setIntValue:lives];

}

// numTilesLeft <= 0 indicates that we've blown away every tile. But,
// to make the game more exciting, we start decrementing numTilesLeft,
// by one everytime through this loop, until it reaches the value
// STOPGAMEAT. This makes the ball move a bit more after all the tiles
// are gone. But, if gameRunning is NO, then it means we probably just
// missed the ball, in which case we should go ahead and jump to the
// next level.

if ((numTilesLeft <= 0) &&
((lives && !gameRunning) || (--numTilesLeft == STOPGAMEAT))) {
[self incrementGameScore:LEVELBONUS];
[self gotoNextLevel:self];
}

NXPing (); // Synchronize postscript for smoother animation

[self unlockFocus];

return self;
}

// Pretty much a dummy function to invoke the step method.

void runOneStep (DPSTimedEntry timedEntry, double timeNow, void *data)
{
[(id)data step:timeNow];
}

@end