ftp.nice.ch/pub/next/developer/resources/libraries/gamekit_proj.NI.sa.tar.gz#/gamekit_proj/Examples/PacMan/PacManView.m

This is PacManView.m in view mode; [Download] [Up]

#import "Text.h"
#import "PacManView.h"
#import "PacManGameBrain.h"
#import "PacManInfoController.h"
#import <libc.h>	// event stuff, misc.
#import <daymisckit/daymisckit.h>
#import "FruitView.h"
#import "Maze.h"
#import "Monster.h"
#import "Player.h"

// comment out the next line to disable the cheat modes.  Cheaters can't
// get net high scores, but they can get local high scores.
#define CHEATMODES YES

// sound definitions
#define DOTEATSOUND 0		// any normal dot
#define POWERDOTEATSOUND 2	// power dot
#define MONSTEREATSOUND 1	// monsters
#define FRUITEATSOUND 4		// fruit
#define DEADSOUND 3			// player nabbed by a ghost
#define STARTLEVELSOUND 5	// music for start of a game/level

#define POINTS (12.0 * scale)	// the size (in points) we draw the text at
#define READY_X ((x - 2 * GHOST_SIZE) * scale + mazePos.x)
#define READY_Y (y * scale + mazePos.y)
#define OFFSET_X (scale * 6)
#define OFFSET_Y (scale * 3)
// decides which screen (1-6) to load for a given level.  Allows us to
// put them in an arbitrary order.
static int screens[NUMSCREENS] = { // which maze image (1-6) to use
                      1, 1, 1, 1, 2, 2, 2, 2,
                      3, 3, 3, 4, 4, 5, 5, 5,
                      1, 2, 0, 0, 3, 4, 0, 5 };

static windowX[3] = { 0.0, 362.0, 698.0 };
static windowY[3] = { 0.0, 280.0, 536.0 };

@implementation PacManView

- initFrame:(const NXRect *)frm    // designated initializer for a view
{    
    [super initFrame:frm];
	backIsColor = NO; // override default; we want image by default
    begin = 128;
 
// initialize game variables
    
	// where the maze is located within our view.
	mazePos.x = 13; mazePos.y = 12;
	eraseReady = NO;
	myorigin.x = 0; myorigin.y = 0;
	scale = 0;
	// a convenient rect; this way I don't have to keep creating a bunch of
	// rects of dimensions GHOST_SIZE by GHOST_SIZE; I re-use this one...
	NXSetRect(&eraseRect, 0, 0, GHOST_SIZE, GHOST_SIZE);
	NXSetRect(&textEraseRect, 0, 0, 3 * GHOST_SIZE, 2 * GHOST_SIZE);
	NXSetRect(&readyRect, 0, 0, GHOST_SIZE * scale * 5, GHOST_SIZE * scale);
	// make sure power dots get drawn
	erasePwr = YES;
	
    return self;
}

- setGhostTracker:(GKTrackerId)tracker { ghostId = tracker; return self; }
- setFruitTracker:(GKTrackerId)tracker { fruitId = tracker; return self; }

// Called by appDidInit to load up images, etc. that we need.
- loadPix
{
    int i; id dotSound = [customSound soundNum:DOTEATSOUND type:0];
	const int *gh;

	[super loadPix];
	
	// the dots play through teir own private stream with our special params
	[dotSound setPlayStream:[[GKSoundStream alloc] initStreams:1]];
	[dotSound setPercentToPlay:0.01]; // this number works well... :-)
	backGround2 = [[NXImage allocFromZone:[self zone]] initSize:&bounds.size];
	fruitPointCount = 0;
	
// build the ghost objects
	gh = [maze ghosts]; 
	for (i=0; i<=3; i++) {
		ghost[i] = [[Monster alloc]
				initGhost:i player:player maze:maze at:gh[i*2] :gh[i*2+1]];
		ghostPointCount[i] = 0;
	}
	
// get the images
	fruit[1] = [NXImage findImageNamed:"Fruit.tiff"];
	fruit[2] = [NXImage findImageNamed:"FruitBig.tiff"];
	gameOver[1] = [NXImage findImageNamed:"GameOver.tiff"];
	gameOver[2] = [NXImage findImageNamed:"GameOverBig.tiff"];
	[gameOver[1] getSize:&(gameOverSize[1])];
	[gameOver[2] getSize:&(gameOverSize[2])];
	[self setScale:[preferences scale]];
	return self;
}

- ghost:(int)i				// return ghost #i
{
	return ghost[i];
}


// State machine...called by the timed entry.  This state machine basically
// controls all the aspects of the game; depending upon it's state, different
// things can/will happen.  The best way to figure this one out is to sit
// down and draw a diagram show how the states transition from one to the
// next.  Note that there are several counters, etc. that function as smaller
// independent state machines that operate within the context of this larger
// state machine.  (Dots blinking, monster states, etc. all run independently
// from main state machine, although the main machine occasionally interrupts
// things in the sub-machines.)  I've not yet had time to fully document the
// logic involved in this state machine...if I ever have the time, I may do so,
// but I don't know that anyone would care if I did anyway.  Minor changes in
// here could render the entire game non-functional if you aren't careful!
// UNLESS YOU KNOW WHAT YOU'RE DOING, DON'T MESS WITH THIS CODE!  Take time
// to understand it fully before playing with it!
// This is the most ridiculously long method you'll ever see, but there's
// no really efficient way to break it up.  (Each section deals with what
// happens in a specific state, and breaking into smaller methods isn't worth
// while, since things are repeated...and it's silly to proliferate subroutines
// that only get called once and from one place.)
- autoUpdate:sender
{			// ALL animation is controlled from here!!!
	register int i, lcnt;
	register BOOL dotEat = NO;
	int x, y;
	BOOL flag = NO;
	BOOL updateFlag = NO;
	float gx, gy;
	NXRect tRect;

    // keep track of how many time we've been called; we can use it do
	// decide when to do certain things...
	if ([NXApp isHidden]) return self; // don't suck cycles if we're hidden
	cycles++;
	if (((![preferences speed]) || ([preferences speed] == 2))
			&& (!demoMode)) { // slow speed, so ignore 50% of entries
		if (cycles & 0x01) return self;
	}

	if (!(cycles & 0x0f)) { // power dots always blink no matter what.
		[self blinkPowerDot];	// happens every 16 cycles.
		erasePwr = YES;
	}
	
	if (fruitPointCount) {
		if (!--fruitPointCount) {
			fruitPointCount = WIPETEXT;
	}	}

	for (i=0; i<4; i++) {
		if (ghostPointCount[i]) {
			if (!--ghostPointCount[i]) {
				ghostPointCount[i] = WIPETEXT;
	}	}	}

	if (state == GAMEOVER) {
	// when demowait counter hits WAITFORDEMO, we start up demo mode.
	// it basically restarts the game--but with "demoMode" turned on.
		if (demoWait++ == WAITFORDEMO) {
			state = NORMALSTATE;
			[self restartGameForDemo:YES];
			[controller unpause];
			[[self window] setTitle:"PacMan Demo"];
			[scoreKeeper resetScore];
	}	}
	
	// in this state, the "Get Ready!" sign has been put up; we stall for a]
	// while and then we take it away and start up the next level.
	if ((state == READY) || (state == DIEREADY)) {
		for (i=0; i<=3; i++) {
			[ghost[i] move:self];
		}
		if (!(--begin)) {
			eraseReady = YES;
			if (demoMode) [player resetPlayer];
			state = NORMALSTATE;
			updateFlag = YES;
	}	}

	// make the maze blink on and off at the end of the level.
	if (state == BLINK_LEVEL) {
		if (begin--) {
			if (begin < 32) {
				if (!(cycles & 0x03)) { // make the maze blink
					[maze visible:(![maze isVisible])];
					updateFlag = YES;
			}	}
		} else {
			state = READY;
			[maze visible:YES];
			[player resetPlayer];
			[controller nextLevel];
			updateFlag = YES;
			begin = 40;
		}
	}

	// stall.  when player dies, the player object needs time to get through
	// it's whole sequence.
	if (state == DYING_PAC) {
		if (!(--begin)) {	// stall
			if (![player newPlayer]) {	// no pacs left == game over.
				[controller gameOver];
				state = GAMEOVER;
				updateFlag = YES;
			} else { // do ready, next pac...
				state = DIEREADY;
				[self startScreen];
				[player resetPlayer];
				updateFlag = YES;
				begin = 40;
	}	}	}

	// this state is the meat of the game.  move the player and monsters
	//  and deal with player/ghost collisions and eating dots and fruit.
    if (state == NORMALSTATE) {
		// move all the ghosts and the pac
		if (!paused) {
			if (demoMode) lcnt = 2;
			else {
				lcnt = [preferences speed] / 2 + 1;	// allow hyper speeds:
			// since 0 <= speed <= 3, we'll move one or two frames per update
			}
			while (lcnt) {
				// figure out how to move player
				[player move:self];

				// put up/take away the fruit
				[maze playerPosition:&x :&y]; // get position of fruit
				if ((numFruits < 3)||demoMode) { // only two fruits per level
					if (fruitCount++ == timeToFruit) { // time for new fruit ?
						numFruits++;
						if (numFruits < 3) fruitOn = DRAW;
						NXSetRect(&tRect, x * scale + mazePos.x,
								y * scale + mazePos.y, FRUIT_SIZE * scale,
								FRUIT_SIZE * scale);
						[self rebuildStaticAt:&tRect];
					} else // one or the other
					if (fruitCount == ERASEFRUIT) {
						// it's been there a while... so remove it
						timeToFruit = 200 + (random() & 0x01f0);
						fruitOn = ERASE;
						if (numFruits == 2) numFruits++;
						NXSetRect(&tRect, x * scale + mazePos.x,
								y * scale + mazePos.y, FRUIT_SIZE * scale,
								FRUIT_SIZE * scale);
						[self rebuildStaticAt:&tRect];
						fruitCount = 0;
					}
				}
			
				// handle eating dots
				if ([maze eatDotAt:[player xpos] :[player ypos]]) {
					[customSound playNum:DOTEATSOUND];
					dotEat = YES;
				}
				if ([player pacAlive]) { // if double timing, need to check.
					if ([maze powerDotAt:[player xpos] :[player ypos]]) {
						[customSound playNum:POWERDOTEATSOUND];
						[scoreKeeper resetBonus:ghostId];
						flag = YES;
						dotEat = YES;
				}	}
				if (dotEat) {
					[maze lastDot:&x :&y];
					NX_X(&eraseRect) = x * scale + mazePos.x;
					NX_Y(&eraseRect) = y * scale + mazePos.y; 
					[self rebuildStaticAt:&eraseRect];
					dotEat = NO;
				}
				if (![maze dots]) {
					state = BLINK_LEVEL;
					begin = 64;
					fruitOn = ERASE; // erase fruit if finished level
					fruitPointCount = 0;
					for (i=0; i<4; i++) ghostPointCount[i] = 0;
					[maze playerPosition:&x :&y]; // get position of fruit
					NXSetRect(&tRect, x * scale + mazePos.x,
							y * scale + mazePos.y, FRUIT_SIZE * scale,
							FRUIT_SIZE * scale);
					[self rebuildStaticAt:&tRect];
					fruitCount = 0;
				}

				// see if player ate the fruit
				[maze playerPosition:&x :&y]; // get position of fruit
				if (fruitOn == YES) { // fruit's there...
					if ((abs([player ypos] - y) < GHOST_SIZE / 2) &&
						(abs([player xpos] - x) < GHOST_SIZE / 2)) { // ate it!
						fruitOn = ERASE;
						[customSound playNum:FRUITEATSOUND];
						NXSetRect(&tRect, x * scale + mazePos.x,
								y * scale + mazePos.y, FRUIT_SIZE * scale,
								FRUIT_SIZE * scale);
						[self rebuildStaticAt:&tRect];
						// add value of fruit to the score
						fruitPoints = [scoreKeeper
								addBonusToScore:fruitId advance:NO];
						// tell the player how many points he/she got
						ftx = (x + TEXTOFFSET) * scale + mazePos.x;
						fty = y * scale + mazePos.y;
						ftx2 = ftx + OFFSET_X; fty2 = fty + OFFSET_Y;
						
						// now, align coords to the maze so erase
						// does it's job right (otherwise, we end up SOVERing
						// maze parts twice, and it looks _ugly_!
						ftx -= mazePos.x; fty -= mazePos.y;
						// chop off lower four bits (floor to mult. of 16)
						ftx /= 16 * scale;
						ftx = (ftx << (scale + 3)) + mazePos.x;
						fty /= 16 * scale;
						fty = (fty << (scale + 3)) + mazePos.y;
						ZAPRECT(textEraseRect, ftx, fty);
						fruitPointCount = 48;
				}	}

				// check for player/ghost collision: (and deal with it)
				if ([maze dots]) {
					for (i=0; i<=3; i++) {
						// tell ghost of power dot
						if (flag) [ghost[i] powerDot:YES];
						[ghost[i] at:&gx :&gy];
						if ((abs(gx - [player xpos]) < GHOST_SIZE / 2) &&
							(abs(gy - [player ypos]) < GHOST_SIZE / 2)) {
						// collision! decide who dies...
							switch ([ghost[i] munch]) {
								case YES : {	// player got ghost
									[customSound playNum:MONSTEREATSOUND];
									// (-munch already added any bonus.)
									gtx[i] = (gx + TEXTOFFSET) * scale
											+ mazePos.x;
									gty[i] = gy * scale + mazePos.y;
									gtx2[i] = gtx[i] + OFFSET_X;
									gty2[i] = gty[i] + OFFSET_Y;
									ghostPoints[i] = [scoreKeeper
										addBonusToScore:ghostId
										advance:YES];
									// now, align to maze as above
									gtx[i] -= mazePos.x; gtx[i] /= scale * 16;
									gty[i] -= mazePos.y; gty[i] /= scale * 16;
									gtx[i] = (gtx[i] << (3 + scale))
											+ mazePos.x;
									gty[i] = (gty[i] << (3 + scale))
											+ mazePos.y;
									ZAPRECT(textEraseRect, gtx[i], gty[i]);
									ghostPointCount[i] = 48;
									break;
								}
								case NO : {		// ghost got player, so die...
									if (![player pacAlive]) break;
									[customSound playNum:DEADSOUND];
									if (cheatMode) break;
									state = DYING_PAC;
									begin = 48; // stall
									[player pacDie];
									// now, erase the fruit
									fruitOn = ERASE;
									[maze playerPosition:&x :&y];
									NXSetRect(&tRect, x * scale + mazePos.x,
											y * scale + mazePos.y,
											FRUIT_SIZE * scale,
											FRUIT_SIZE * scale);
									[self rebuildStaticAt:&tRect];
									updateFlag = YES;
									break;
								}
								case HARMLESS : 
								default : {		// eyes do nothing.
									break;
				}	}	}	}	}

				// figure out where ghosts will go next
				for (i=0; i<=3; i++) {
					[ghost[i] move:self];
				}
				if (lcnt > 1) { // make movement take effect w/o render
				// doing this extra movement gives faster perceived speeds
					for (i=0; i<=3; i++) {
						[ghost[i] moveOneFrame];
						[ghost[i] powerCount];
					}
					[player moveOneFrame];
				}
				lcnt--;
	}	}	}
	// draw all the changes, if applicable.
	if (updateFlag) [self update];
    /*if ((state != READY) && (state != DIEREADY))*/ [self updateSelf:&bounds :1];
    return self;
}

// This renders the whole screen, much like updateSelf:: below, but since
// it always redraws the _entire_ screen, it is unnaceptably inefficient for
// handling individual animation frames.
- drawSelf:(NXRect *)rects :(int)rectCount	// redraws the screen.
{		// right now, it's stupid and always redraws the whole view.
	//register int i;//, f;
	//int x, y;
    NXPoint pos;
	//NXRect from;//, bezel, mazeRect;

    if ([self window] == nil)
        return self;	// exit if no window to draw in
    
	if ((state == BLINK_LEVEL) && ![maze isVisible])
		[dirtPile fullRedraw:self :backGround2];
	else [dirtPile fullRedraw:self :staticBuffer];
   
	if (state == GAMEOVER) {
		pos.x =  (NX_WIDTH(&bounds) - gameOverSize[scale].width) / 2;
		pos.y = (NX_HEIGHT(&bounds) - gameOverSize[scale].height) / 2;
		[gameOver[scale] composite:NX_SOVER toPoint:&pos];
	}
	if (NXDrawingStatus == NX_PRINTING) { // make sure actors are in print
		[self updateSelf:rects :rectCount];
	}
	NXPing();
	return self;
}

// This is the main drawing here.  It works like drawSelf, but only
// _changes_ stuff; it doesn't re-draw the whole view each time.  This
// has been done to speed things up.  We erase the old, and then redraw
// all the spots we erased after calculating where things have moved to.
- updateSelf:(NXRect *)rects :(int)rectCount	// redraws the screen.
{		// it redraws only what has changed since last redraw.
    register int i;
	int x, y, j, order[4], flag[4];
//	NXRect from;

    if ([self window] == nil) return self;	// exit if no window to draw in
	// if blinking maze and maze isn't on screen then this update is unneeded
	// note that technically, even if maze is visible, we could skip out, but
	// we don't because we want the last dot and the monsters, etc. to go
	// away right as we enter BLINK_LEVEL; this won't happen otherwise.
	if (((state == BLINK_LEVEL) && ![maze isVisible]) || (state == GAMEOVER))
		return self;
	[buffer lockFocus];
	// erase ghosts and ghost text fields.
	for (i=0; i<=3; i++) {
		if ((ghostPointCount[i] == WIPETEXT) && (state != GAME_OVER)) {
			ghostPointCount[i] = 0;
			ZAPRECT(textEraseRect, gtx[i], gty[i]);
			CLRRECT(textEraseRect);
		}
		if (state != GAME_OVER) {
			[ghost[i] lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)];
			NX_X(&eraseRect) = NX_X(&eraseRect) * scale + mazePos.x;
			NX_Y(&eraseRect) = NX_Y(&eraseRect) * scale + mazePos.y;
			CLRRECT(eraseRect);
		}
	}
	
	// erase ready text
	if (eraseReady) {
			[maze playerPosition:&x :&y];
			eraseReady = NO;
			ZAPRECT(readyRect, READY_X, READY_Y);	// erase ready text
			CLRRECT(readyRect);
	}
	
	// erase fruit text
	if ((fruitPointCount == WIPETEXT) && (state != GAME_OVER)) {
		fruitPointCount = 0;
		ZAPRECT(textEraseRect, ftx, fty);
		CLRRECT(textEraseRect);
	}

	// erase the player's PacMan
	if (state != GAME_OVER) {
		[player lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)];
		NX_X(&eraseRect) = NX_X(&eraseRect) * scale + mazePos.x;
		NX_Y(&eraseRect) = NX_Y(&eraseRect) * scale + mazePos.y;
		CLRRECT(eraseRect);
	}
	
	// put the player's Pac on the screen if in a state where it's visible.
	if ((state != BLINK_LEVEL) || ((state == BLINK_LEVEL) && (begin > 31))) {
		[player renderAt:mazePos.x :mazePos.y
			move:((!paused) && (state == NORMALSTATE))];
		[player lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)];
		NX_X(&eraseRect) = NX_X(&eraseRect) * scale + mazePos.x;
		NX_Y(&eraseRect) = NX_Y(&eraseRect) * scale + mazePos.y;
		[dirtPile addRegion:&eraseRect];
	}
		
	// put ghosts on screen if we're in a state where they are visible.
	if ((state != DYING_PAC) && (state != BLINK_LEVEL) &&
			(state != GAMEOVER)) {
		for (i=0; i<=3; i++) {
			[ghost[i] renderAt:mazePos.x :mazePos.y move:
				((!paused) && (state == NORMALSTATE))];
			[ghost[i] lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)];
			NX_X(&eraseRect) = NX_X(&eraseRect) * scale + mazePos.x;
			NX_Y(&eraseRect) = NX_Y(&eraseRect) * scale + mazePos.y;
			[dirtPile addRegion:&eraseRect];
	}	}

	// draw text messages
	if (fruitPointCount) {
		drawScore(ftx2, fty2, POINTS, fruitPoints);
		ZAPRECT(textEraseRect, ftx, fty);
	}
	for (i=0; i<4; i++) flag[i] = NO;
	for (i=0; i<4; i++) { // sort scores to draw in lowest to highest order
	// A selection sort is used.  O(n^2) but n=4 so who cares?
		int next = 0; int val = 100000;
		for (j=0; j<4; j++) { // find next lowest, unused score value
			if (!flag[j] && (ghostPoints[j] < val)) {
				val = ghostPoints[j]; next = j;
			}
		}
		flag[next] = YES; order[i] = next;
	}
	for (i=0; i<4; i++) if (ghostPointCount[order[i]]) {
		drawScore(gtx2[order[i]], gty2[order[i]], POINTS,
				ghostPoints[order[i]]);
		ZAPRECT(textEraseRect, gtx[order[i]], gty[order[i]]);
	}
	if ((state == READY) || (state == DIEREADY)) {
		[maze playerPosition:&x :&y];
		drawReady(READY_X + 10 * scale, READY_Y + OFFSET_Y, POINTS);
		ZAPRECT(readyRect, READY_X, READY_Y);
	}
	[buffer unlockFocus];
	
	// housekeeping for the graphics:
	[self lockFocus];
	[dirtPile doRedraw:buffer]; // flush buffer out
	[self unlockFocus];
	if (fruitOn == ERASE) fruitOn = NO;
	NXPing();
	erasePwr = NO;	// we've listened to this flag, so turn it off now.
	return self;
}

// Handle player movement.  We respond to the arrow keys.  If we don't
// recognize the key, we pass it on.  This method just shunts the key
// off to the player object, which is what really deals with it. 
- keyDown:(NXEvent *)myevent
{
	unsigned short charCode;
	unsigned short charSet;
	register int i;
	
	if (!myevent) return self; // if no event when coalescing, go away
	PSobscurecursor();
#ifdef CHEATMODES
	// secret cheat mode:  control-c
	if (myevent->data.key.charCode == 0x03) {
		cheatMode = YES;
		fflush(stderr);
		[preferences setUnfair]; // don't allow net high scores.  local OK
		[customSound playNum:POWERDOTEATSOUND];
		return self;
	}
	// secret cheat:  control-d (eats a power dot)
	if (myevent->data.key.charCode == 0x04) {
		for (i=0; i<4; i++) [ghost[i] powerDot:YES];
		[preferences setUnfair]; // don't allow net high scores.  local OK
		[customSound playNum:POWERDOTEATSOUND];
		[scoreKeeper resetBonus:ghostId];
		return self;
	}
	// secret cheat:  control-f (put fruit on screen)
	if (myevent->data.key.charCode == 0x06) {
		fruitCount = timeToFruit - 1;
		numFruits = 0;
		[preferences setUnfair]; // don't allow net high scores.  local OK
		return self;
	}
	// secret cheat:  control-l (advance a level instantly)
	if (myevent->data.key.charCode == 0x0c) {
		NXRect tRect;
		int x, y;
		state = BLINK_LEVEL;
		begin = 64;
		fruitPointCount = 0; for (i=0; i<4; i++) ghostPointCount[i] = 0;
		fruitOn = ERASE; // erase fruit if finished level
		[maze playerPosition:&x :&y]; // get position of fruit
		NXSetRect(&tRect, x * scale + mazePos.x,
				y * scale + mazePos.y, FRUIT_SIZE * scale,
				FRUIT_SIZE * scale);
		[self rebuildStaticAt:&tRect];
		fruitCount = 0;
		[preferences setUnfair]; // don't allow net high scores.  local OK
		return self;
	}
#endif
	// check for arrow keys or space bar
	if (!(myevent->flags&(NX_CONTROLMASK|NX_ALTERNATEMASK|NX_COMMANDMASK))) {
		charCode = myevent->data.key.charCode;
		charSet = myevent->data.key.charSet;
		
		if (charSet == NX_SYMBOLSET) { // symbol set contains the arrows
			if		  (charCode == 0xAD) { // Up Arrow
				[player newDirection:PAC_UP];
				if (paused) [controller unpause];
				return self;
			} else if (charCode == 0xAF) { // Down Arrow
				[player newDirection:PAC_DOWN];
				if (paused) [controller unpause];
				return self;
			} else if (charCode == 0xAC) { // Left Arrow
				[player newDirection:PAC_LEFT];
				if (paused) [controller unpause];
				return self;
			} else if (charCode == 0xAE) { // Right Arrow
				[player newDirection:PAC_RIGHT];
				if (paused) [controller unpause];
				return self;
			}
		} else if (myevent->data.key.charCode == ' ') { // Space Bar
			[player newDirection:PAC_STOP];
			if (paused) [controller unpause];
			return self;
		} else if ((myevent->data.key.charCode == 'a') && cheatMode) {
#ifdef CHEATMODES
			// abort pac -- it's the only way you can die in cheat mode.
			[customSound playNum:DEADSOUND];
			state = DYING_PAC;
			begin = 48; // stall
			[player pacDie];
#endif
		} else {
			[super keyDown:myevent];
		}
	} else [super keyDown:myevent];
	[self keyDown:[NXApp peekAndGetNextEvent:NX_KEYDOWN]]; // coalesce keydowns
	// this is done recursively...primitive, but easy to implement :-)
    return self;
}

// set up the screen at the start of a new level.  Loads in the maze.
- setUpScreen
{	    
	[maze makeMaze:screens[(([controller level] >= NUMSCREENS) ?
		(NUMSCREENS - 1) : [controller level]) - 1]];
	fruitOn = NO;
	[self rebuildStaticBuffer];
	[self startScreen];
	timeToFruit = 200 + (random() & 0x01f0);
	fruitCount = 0;
	numFruits = 0;
	[[scoreKeeper bonusTracker:fruitId] advanceBonus]; // changes with level
	
    [super setUpScreen];
    return self;
}

// This lets us put the ghosts back where they are at the start of the level.
// This is separate from above because the above also resets the dots, and if
// the player dies, we only want to reset the ghosts, not the dots, too.
- startScreen
{
	const int *gh; int i;
	    
	gh = [maze ghosts]; // pointer to array of ghost coordinates
	for (i=0; i<=3; i++) {	// re-initialize each ghost
		[ghost[i] initGhost:i player:player maze:maze at:gh[i*2] :gh[i*2+1]];
		[ghost[i] setScale:scale];
		ghostPointCount[i] = 0;
	}
	fruitPointCount = 0;
	// don't let fruit come out too quickly
	if (timeToFruit - fruitCount < 200)
		timeToFruit += 250;
	
    return self;
}

- restartGame
{
	return [self restartGameForDemo:NO];
}

- restartGameForDemo:(BOOL)doingDemo
{
	// go to READY state to start game; but want DIEREADY so we don't advance
	// the level; the controller, by virtue of calling this method, has already
	// advanced the level.
	state = DIEREADY;
	begin = 100;
	fruitOn = NO;
	
	// make sure that all artifacts of demo mode are gone
    demoWait = 0;
	cheatMode = NO;
    if (demoMode) {
		demoMode = NO;
		[[self window] setTitle:"PacMan"];
    }
	
    [scoreKeeper resetScore];	// clear the score
    [scoreKeeper resetBonus:(-1)];	// clear all bonuses
    [self getPreferences];	// make sure we're up to date
	[player newPlayer];		// get a new pac to play with
	// (above always sets up the pac, but in demoMode, the gameBrain hasn't
	// given us 3 pacs, so we end up taking a negative # of pacs, which means
	// demo mode will end as soon as the pac dies.)
	if (doingDemo) demoMode = YES;
	// cut off the info panel sound (if playing) (only if not demo mode)
	else [[controller infoController] stopSound:self];
	[customSound shutUpUntil:[[DAYTime alloc] initWithCurrentTime]];
	[self update]; [self updateSelf:&bounds :1]; NXPing(); // show screen
	// start up the sound player object with the "start game" music
	if ([preferences music]) [customSound playNum:STARTLEVELSOUND];
    return self;
}

- setBackgroundFile:(const char *)fileName andRemember:(BOOL)remember
{
	NXRect bezel = {{NX_X(&bounds) + 5, NX_Y(&bounds) + 5},
		{NX_WIDTH(&bounds) - 10, NX_HEIGHT(&bounds) - 10}};

	[super setBackgroundFile:fileName andRemember:remember];
	[backGround2 lockFocus];
	NXDrawGrayBezel(&bezel, &bounds);
	NXFrameRectWithWidth(&bounds, 5);
	NX_WIDTH(&bezel) -= 6; NX_HEIGHT(&bezel) -= 6;
	NX_X(&bezel) += 3; NX_Y(&bezel) += 3;
	[self drawBackground:&bezel];
	[backGround2 unlockFocus];
	[self rebuildStaticBuffer];
	[self update];
	return self;
}

- buildColorBackground
{
	NXRect bezel = {{NX_X(&bounds) + 5, NX_Y(&bounds) + 5},
		{NX_WIDTH(&bounds) - 10, NX_HEIGHT(&bounds) - 10}};

	[backGround2 lockFocus];
	NXDrawGrayBezel(&bezel, &bounds);
	NXFrameRectWithWidth(&bounds, 5);
	NX_WIDTH(&bezel) -= 6; NX_HEIGHT(&bezel) -= 6;
	NX_X(&bezel) += 3; NX_Y(&bezel) += 3;
	[self drawBackground:&bezel];
	[backGround2 unlockFocus];
	[self rebuildStaticBuffer];
	return self;
}

- acceptColor:(NXColor)color atPoint:(const NXPoint *)aPoint
{ // override to redraw background buffer.
	backIsColor = YES;
	backColor = color;
	[self buildColorBackground];
	[[self writeColor] update];
	return self;
}

- rebuildStaticBuffer
{
	NXRect mazeRect = {{0, 0},
		{GHOST_SIZE * BLOCK_WIDTH, GHOST_SIZE * BLOCK_HEIGHT}};
	int x, y;
	register int f;
	NXRect from;
	NXPoint pos;
		
	[staticBuffer lockFocus];
	[backGround2 composite:NX_COPY fromRect:&bounds toPoint:&(bounds.origin)];
	[maze render:&mazeRect at:&mazePos];
	[maze playerPosition:&x :&y];
	if (fruitOn) {
		if (fruitOn != ERASE) {
			// decide which fruit to draw
			f = fruits[(([controller level] > FRUIT_LEVELS) ?
				FRUIT_LEVELS : [controller level])];
			// draw the fruit
			pos.x = x * scale + mazePos.x;
			pos.y = y * scale + mazePos.y;
			NXSetRect(&from,
				(f % FRUIT_PER_ROW) * FRUIT_SIZE * scale,
				(f / FRUIT_PER_ROW) * FRUIT_SIZE * scale,
				FRUIT_SIZE * scale, FRUIT_SIZE * scale);
			[fruit[scale] composite:NX_SOVER fromRect:&from toPoint:&pos];
			fruitOn = YES;
		} else fruitOn = NO;
	}
	[staticBuffer unlockFocus];
	
	[buffer lockFocus];
	[staticBuffer composite:NX_COPY fromRect:&bounds toPoint:&(bounds.origin)];
	[buffer unlockFocus];
	[dirtPile addRegion:&bounds];
	
	return self;
}

- rebuildStaticAt:(NXRect *)rect;
{	// assumes that bezel is intact, so doesn't draw it
	NXRect mazeRect = {{(NX_X(rect) - mazePos.x) / scale,
			(NX_Y(rect) - mazePos.y) / scale},
			{(NX_WIDTH(rect) - 1.0) / scale,
			(NX_HEIGHT(rect) - 1.0) / scale}};
	NXRect xRect = {{0.0, 0.0}, {GHOST_SIZE * scale, GHOST_SIZE * scale}};
	NXPoint pos;
	NXRect from;
	register int f;
	int x, y;

	[staticBuffer lockFocus];
	[super rebuildStaticAt:rect];
	[maze render:&mazeRect at:&mazePos];
	// see if rect intersects fruit && fruit is on screen
	[maze playerPosition:&x :&y];
	NX_X(&xRect) = x * scale + mazePos.x;
	NX_Y(&xRect) = y * scale + mazePos.y;
	if (NXIntersectsRect(&xRect, rect) && fruitOn) {
		if (fruitOn != ERASE) {
			// decide which fruit to draw
			f = fruits[(([controller level] > FRUIT_LEVELS) ?
				FRUIT_LEVELS : [controller level])];
			// draw the fruit
			pos.x = x * scale + mazePos.x;
			pos.y = y * scale + mazePos.y;
			NXSetRect(&from,
				(f % FRUIT_PER_ROW) * FRUIT_SIZE * scale,
				(f / FRUIT_PER_ROW) * FRUIT_SIZE * scale,
				FRUIT_SIZE * scale, FRUIT_SIZE * scale);
			[fruit[scale] composite:NX_SOVER fromRect:&from
					toPoint:&(xRect.origin)];
			fruitOn = YES;
		} else fruitOn = NO;
	}
	[staticBuffer unlockFocus];
	
	[buffer lockFocus];
	[staticBuffer composite:NX_COPY fromRect:rect toPoint:&(rect->origin)];
	[buffer unlockFocus];
	[dirtPile addRegion:rect];

	return self;
}

- blinkPowerDot		// update static buffer where power dots are.
{
	const int *pd;	// pointer to array of power dot coords
	register int i;
	
	[maze blinkPowerDot];
	pd = [maze powerDot];	// get power dot coords
	for (i=0; i<=3; i++) {	// erase power dot every time it blinks.
			NX_X(&eraseRect) = pd[i * 2 ]    * GHOST_SIZE * scale + mazePos.x;
			NX_Y(&eraseRect) = pd[i * 2 + 1] * GHOST_SIZE * scale + mazePos.y;
			[self rebuildStaticAt:&eraseRect];
	}
	return self;
}

- (int)scale
{
	return scale;
}

- setScale:(int)newScale
{
	int i;

	if ((newScale < 1) || (newScale > 2)) return self;
	if (newScale == scale) return self; // already that size.
	
	scale = newScale;
	
	for (i=0; i<4; i++) {
		[ghost[i] setScale:scale];
	}
	[player setScale:scale];
	[maze setScale:scale];
	
	// resize self and windows
	[window disableFlushWindow];
	[window disableDisplay];
	[self sizeTo:windowX[scale] :windowY[scale]];
	NXSetRect(&eraseRect, 0, 0, GHOST_SIZE * scale, GHOST_SIZE * scale);
	NXSetRect(&readyRect, 0, 0, GHOST_SIZE * scale * 5, GHOST_SIZE * scale);
	NXSetRect(&textEraseRect, 0, 0,
			3 * GHOST_SIZE * scale, 2 * GHOST_SIZE * scale);

	
	// rebuild all the offscreen buffers.
	[buffer free];
	[staticBuffer free];
	[backGround2 free];
	buffer = [[NXImage allocFromZone:[self zone]] initSize:&bounds.size]; 
	staticBuffer = [[NXImage allocFromZone:[self zone]] initSize:&bounds.size];
	backGround2 = [[NXImage allocFromZone:[self zone]] initSize:&bounds.size];
	 
    if (backIsColor) [self buildColorBackground];
	else [self setBackgroundFile:NXGetDefaultValue([NXApp appName],
			"BackGround") andRemember:NO];

	[self rebuildStaticBuffer];
	[[window delegate] windowDidMove:window];
	[window sizeWindow:windowX[scale] :windowY[scale]];
	[window reenableFlushWindow];
	[window reenableDisplay];
	[[window display] flushWindowIfNeeded];
	[self update];
	return self;
}

@end

These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.