Source code for Macintosh 'Missile' Program
This is the source code for the "Missile" game program for the Macintosh, originally written in June 1984 using Lisa PASCAL. Although the strings in the resource file seem to indicate that this is verion 2.3 from 1984 Aug 18, the functionality appears to be a version newer than 3.0 which was never released. However, all of the core features, including the compatibility features that allow it to run properly on today's Mac's, were present in all versions from 2.3 on.
The binary runs unmodified on all Mac models and MacOS's from the original Lisa and 128K Mac up through the current (last time I updated this page, the current was a Dual G5 running MacOS X 10.4 "Tiger"). The gameplay is unmodified and it correctly adapts to the screen size, pauses when another application is brought forward, etc. I accomplished this simply by following Apple's guidelines and trusting their promise that doing so would ensure compatibility with any future Mac models. So this is really more a testament to Apple's ongoing commitment to backwards compatibility than any expertise of my own — however it can hardly be insignificant that no other games written for the 1980's Macs still run today.
This source code is Copyright (c) 1984-2005 by Robert P. Munafo, and made available under the terms and conditions of the GNU General Public License, version 2.0.
This file is "missile.p":
{ %title 'Missile - a game for the Apple Macintosh.' } { %subtitle 'Overall program description.' } { $X- } { Turns off A.R.S.E. } PROGRAM Game; { Missile -- A Missile Command game for the Macintosh by Robert P. Munafo at Dartmouth College. I wrote this to learn a bit about Mac programming. It has a few 'standard user interface' bugs: It doesn't update the window during the 'End of round X' and 'GAME OVER' routines. (It only updates while playing.) It won't quit when you select "quit" while a desk accessory is on screen. Instead, it will wait until you close all the desk accessories, then quit. The cursor looks the same all the time. It should change shape to let the user know when he can and cannot fire missiles. It also has some missing features (which I hope to add soon): o Smart Bombs o Add an "Ammunition: [xxx]% of normal" item to the dialog box. o Sound o User should be able to pick what round the game starts with o High scores list saved on the disk o Two-player option o The animation needs to be made faster (in some manner) } { %subtitle 'USES statements' } USES {$U Obj/QuickDraw } QuickDraw, {$U Obj/OSIntf } OSIntf, {$U Obj/ToolIntf } ToolIntf, {$U Obj/PackIntf } Packintf; { %subtitle 'Constants' } CONST lastMenu = 4; { number of menus } appleMenu = 1; { menu ID for desk accessory menu } fileMenu = 256; { menu ID for File menu } EditMenu = 257; { menu ID for the Edit menu. } GameMenu = 258; { menu ID for Game menu } str_1_ID = 300; {ID for my first string of text} str_2_ID = 350; {ID for my second string of text} box_ID = 450; {ID of "about" dialog box} box2_id = 451; {ID of options box} fbIdle = 0; fbGrow = 1; fbShrink = 2; maxfb = 60; { Maximum number of fireballs allowed. } fbRad = 15; { Radius of fireballs. } fbRadRate = 2; { Rate of expansion/contraction of fireballs. } msIdle = 0; msActive = 1; msNormal = 1; msMirv = 2; msSmart = 3; maxms = 20; { Maximum number of missiles allowed. } mWidth = 3; { Width of missile tracks. } CityHeight = 40; { Distance from bottom up to cities. } CityWidth = 50; { Width of cities. } BuildHeight = 30; { Maximum height of buildings. } BuildWidth = 4; { Width of buildings. } nCities = 6; { Number of cities. } TYPE fireball = RECORD { Data type to describe a fireball. } bounds: Rect; { Rectanggle to copy bits into. } size: INTEGER; { Current radius of fireball. } mode: INTEGER; { Idle, Growing, or shrinking. } END; missile = RECORD { Data type to describe a missile. } start: Point; { One endpoint of the missile's path. } pos: Point; { Position within path - 0 to 1. } dh, dv: INTEGER; { Vertical and horizontal speed in pixels. } city: INTEGER; { Number of the target city. } kind: INTEGER; { MIRV or normal. } mode: INTEGER; { Idle or Active. } END; VAR myMenus: ARRAY [1..lastMenu] OF MenuHandle; screenRect: Rect; doneFlag: BOOLEAN; myEvent: EventRecord; code, refNum: INTEGER; wRecord: WindowRecord; myWindow, whichWindow: WindowPtr; first_string, second_string : StringHandle; { Handles to strings of text for AboutMissile. } { My game variables are here. } fbBitMap: BitMap; { BitMap for fireball pictures. } fbBits: ARRAY[1..3600] OF INTEGER; { Actual bits for fireball pictures. } crosshairs: Cursor; { Crosshairs cursor read in from resource. } hCurs: CursHandle; { Handle to the cursor. } CityPattern: PatHandle;{ Handle to pattern in which to draw cities. } FieldWidth: INTEGER; { Width of the playing field. } FieldHeight: INTEGER; { Height of the playing field. } i: INTEGER; { Loop variable. } fbs: ARRAY [1..maxfb] OF fireball; { Each entry describes one fireball. } missiles: ARRAY [1..maxms] OF missile; { Each entry describes one missile. } nMissiles: INTEGER; { Number of currently active missiles. } lastTick: LONGINT; { System clock at time of previous update. } gameOver: BOOLEAN; { Set true when all cities are destroyed. } PauseFlag: BOOLEAN; { True when game is being paused. } Playing: BOOLEAN; { True except buring bonus points and GAME OVER screens. } esFlag: BOOLEAN; { True if "GAME OVER" is to be drawn after exiting main loop. } enddelay: INTEGER; { Used to wait a while after end of round. } citiesLeft: INTEGER; { Number of cities still alive. } bcities: INTEGER; { Number of bonus cities they have left. } cities: ARRAY [1..nCities] OF BOOLEAN; { State of each city : false = destroyed. } cities2: ARRAY [1..nCities] OF BOOLEAN; { State of the city before the round began. } nukeHeight: INTEGER; { Vertical position of fireball required to destroy city. } endv: INTEGER; { Y-coordinate of the end of a missile's path. } MirvRate: INTEGER; mirvHeight: INTEGER; { Height at which Mirvs MIRV. } mirvNasty: INTEGER; { Percent chance of sub-missile on mirv for each city. } msSpeed: INTEGER; { Vertical speed of missiles, in 1/16ths of a pixel.} msRate: INTEGER; { Percent rate of missile production. } RoundNumber: INTEGER; { Number of the current round. } Score: LONGINT; { The player's current score. } DisScore: LONGINT; { Score currently displayed on the screen. } HighScore: LONGINT; { The current highest score. } RoundH: INTEGER; Scoreh: INTEGER; { Horiz. position of text 'Score:' } ScoreNumH: INTEGER; { Horiz. position of score. } statsH: INTEGER; { Horiz. position of text 'Enemy left:' } statsNumH: INTEGER; statsRect: Rect; eLeft: INTEGER; { How many enemy missiles are left this round. } yLeft: INTEGER; { How many missiles you (the player) have left. } eDestroyed: INTEGER; { Number of enemy missiles destroyed. } eDesH: INTEGER; { Horiz. position of text 'Enemy Destroyed:' } eDesNumH: INTEGER; { Horiz. position of # of enemy missiles destroyed. } gameSpeed: INTEGER; { Number of ticks between each AdvanFb call. } StartRound: INTEGER; { Round to start off with. } mFlag1: BOOLEAN; { Do missiles aim for dead cities? } MFlag2: INTEGER; { Extent to which enemy missiles blow up. } MExists: ARRAY[1..3] OF BOOLEAN; { Does this type of missile occur? } MPBase: ARRAY[1..3] OF INTEGER; { Value of missile at first round... } MPExtra: ARRAY[1..3] OF INTEGER; { ...plus this much each additional round... } MPoints: ARRAY[1..3] OF INTEGER; { ...is this much in all. } { %subtitle 'Init routine for menus.' } PROCEDURE SetUpMenus; VAR i: INTEGER; BEGIN myMenus[1] := GetMenu(appleMenu); { Load menu from disk. } MyMenus[1]^^.MenuData[1] := Chr(AppleSymbol); { Change title to the Apple symbol. } AddResMenu(myMenus[1],'DRVR'); { Add items for desk accessories. } myMenus[2] := GetMenu(fileMenu); myMenus[3] := GetMenu(editMenu); myMenus[4] := GetMenu(GameMenu); FOR i:=1 TO lastMenu DO InsertMenu(myMenus[i],0); DrawMenuBar; END; { of SetUpMenus } { %subtitle 'Init routine for fireball BitMaps.' } { This routine is called by the main Init routine, below. It creates a bunch of bit images of circles off-screen, which can later be drawn on the screen much faster than FrameOval. } PROCEDURE InitFbs; VAR i: FIXED; aRect: Rect; { Rectangle for drawing FrameOvals. } saveBits: BitMap; { To save the current GrafPort while drawing off-screen. } SaveRect: Rect; BEGIN SaveBits := ThePort^.PortBits; SaveRect := ThePort^.PortRect; fbBitMap.rowBytes := 8; SetRect(fbBitMap.bounds, 0, 0, 60, 60); ThePort^.portRect := fbBitMap.Bounds; PenSize(fbRadRate,fbRadRate); PenPat(black); PenMode(PatCopy); SetRect(aRect, 30, 30, 30, 30); { Start with an empty rectangle in the center. } FOR i := 0 TO 14 DO BEGIN fbBitMap.baseAddr := @fbBits[240*i + 1]; { Pick starting address for this picture. } insetRect(aRect, -2, -2); { Make rect a bit larger. } SetPortBits(fbBitMap); EraseRect(thePort^.portBits.bounds); { Erase to all white. } FrameOval(aRect); { Draw the circle. } END; SetPortBits(SaveBits); { Return to normal screen drawing. } ThePort^.PortRect := SaveRect; END; { of InitFbs } { %subtitle 'Init routine' } PROCEDURE Setup; VAR wRect: Rect; { Window Rectangle. } i: INTEGER; BEGIN { Macintosh System initialization. } InitGraf(@thePort); { Quickdraw. } InitFonts; { Font Manager. } InitWindows; { Window Manager. } InitMenus; { Menu Manager. } TEInit; { TextEdit. } InitDialogs(NIL); { Dialog manager. } InitCursor; { Cursor handler. } InitAllPacks; { Package Manager. } SetUpMenus; { My routine to insert the menus. } hCurs := POINTER(GetCursor(256)); { Load the Crosshairs cursor that I use in the game. } crosshairs := hCurs^^; SetCursor(crosshairs); CityPattern := GetPattern(256); screenRect := screenBits.bounds; { Don't actually use this, but might later. } doneFlag := FALSE; { This flag is set to False when user selects 'Quit'. } HighScore := 0; GameSpeed := 8; { Set up all the user-settable options. } StartRound := 1; MFlag1 := True; MFlag2 := 1; FOR i := 1 TO 3 DO BEGIN MExists[i] := True; MPBase[i] := 10*i; MPExtra[i] := 10*i; END; { myWindow := GetNewWindow(256, @wRecord, POINTER(-1)); } wRect := ScreenRect; wRect.top := 20; myWindow := NewWindow(@wRecord, wRect, { BoundsRect } 'Missile Command by Robert Munafo', True, { Visible } 0, { ProcID } POINTER(-1), { behind } False, { GoAwayFlag } 0); { RefCon } SetPort(myWindow); { Might as well play the game in a window... } FieldWidth := MyWindow^.portRect.right; { These variables are used by the game. } FieldHeight := MyWindow^.portRect.bottom; TextFont(0); TextFace([]); InitFbs; { Set up the fireball BitMaps. } second_string := GetString(str_2_ID); { Get strings from resource. } first_string := GetString(str_1_ID); FlushEvents(everyEvent,0); END; { %Subtitle 'Gets a text string' } { This routine conducts a dialog to get a text string from the user. The function value returned indicates whether the user stopped by entering OK or cancel. The ID is the dialog ID number to use and Text is the text they typed in. To get the text, we display the dialog window, get the handle of the edit text and execute a small event loop wich waits until one of the two buttons is pressed. When done, we dispose of the dialogue window, set the flag depending on the means of exiting and return the text. The predefined dialogue window has the following items in it: Item Number Item 1 OK button, Enabled 2 Cancel button, Enabled 3 EditText area, Enabled 4 StatText area, Disabled (Prompt) 5... Anything else, Disabled *) { the actual function has been deleted. } { %subtitle '"About Missile" routine.' } { This routine is nearly identical to Jason Ansley's About_Windows routine. } PROCEDURE AboutMissile; VAR item_chosen : integer; {which botton was hit (I only have one so it doesn't really matter)} item_type : integer; {type of item in res. def. file (needed to pass as parameter)} first_handle, second_handle : Handle; {handles to my items} the_dialog : DialogPtr; {a pointer to my dialog box} box : rect; {the display rectangle of the item} BEGIN the_dialog := GetNewDialog(box_ID, NIL, pointer(-1)); {get dialog box from resource file} GetDItem(the_dialog, 2, item_type, first_handle, box); {get text format info from file} GetDItem(the_dialog, 3, item_type, second_handle, box); SetIText(first_handle, first_string^^); {set the lines of text into the box} SetIText(second_handle, second_string^^); ModalDialog(NIL, item_chosen); {operate the box} DisposDialog(the_dialog); {get rid of the box now that I'm done with it} END; { %subtitle 'My own random number routine.' } FUNCTION Rnd (n: FIXED) : FIXED; { Function to generate a random integer from 1 to N. } BEGIN Rnd := (ABS(Random) MOD n) + 1; END; { %subtitle 'DrawNumber - converts number to string and draws it on the screen.' } PROCEDURE DrawNumber (n: LONGINT); VAR s: Str255; BEGIN NumToString(n, s); DrawString(s); END; { %subtitle 'Update the score at the bottom of the screen.' } PROCEDURE UpdScore; BEGIN TextMode(SrcBic); MoveTo(ScoreNumH, FieldHeight-5); TextSize(24); DrawNumber(disScore); TextMode(SrcOr); MoveTo(ScoreNumH, FieldHeight-5); DrawNumber(Score); disScore := Score; EraseRect(statsRect); TextMode(SrcOr); MoveTo(statsNumH, FieldHeight-20); TextSize(12); DrawNumber(eLeft); MoveTo(statsNumH, FieldHeight-5); DrawNumber(yLeft); TextMode(SrcCopy); MoveTo(eDesNumH, FieldHeight-20); DrawNumber(eDestroyed); END; { %subtitle 'Print the score.' } PROCEDURE DrawScore; BEGIN TextMode(srcCopy); MoveTo(ScoreH, FieldHeight-14); TextSize(12); DrawString('Score: '); MoveTo(ScoreNumH, FieldHeight-5); TextSize(24); DrawNumber(Score); disScore := Score; TextSize(12); MoveTo(statsH, FIeldHeight-20); DrawString('Enemy left: '); DrawNumber(eLeft); MoveTo(statsH, FieldHeight-5); DrawString('Yours left: '); MoveTo(statsNumH, FieldHeight-5); DrawNumber(yLeft); MoveTo(eDesH, FieldHeight-20); DrawString('Destroyed: '); MoveTo(eDesNumH, FieldHeight-20); DrawNumber(eDestroyed); END; { %subtitle 'Print round #, score, and # killed.' } PROCEDURE BottomLine; BEGIN; TextSize(12); RoundH := 10 + StringWidth('Round: '); ScoreH := RoundH + 2*StringWidth('00 '); ScoreNumH := ScoreH + StringWidth('Score: '); eDesH := FieldWidth - StringWidth('Destroyed: 0000 '); eDesNumH := FieldWidth - StringWidth('0000 '); statsH := eDesH - StringWidth('Enemy left: 000 '); statsNumH := eDesH - StringWidth('000 '); SetRect(statsRect, statsNumH, FieldHeight-30, eDesH-2, FieldHeight); TextMode(srcCopy); MoveTo(10, FieldHeight-14); DrawString('Round: '); MoveTo(RoundH, FieldHeight-5); TextSize(24); DrawNumber(RoundNumber); DrawScore; MoveTo(eDesH, FieldHeight-5); DrawString('High Score: '); DrawNumber(HighScore); END; { %subtitle 'Create a new fireball.' } PROCEDURE AllocFb (location: Point); VAR i: INTEGER; idlefb: INTEGER; BEGIN idlefb := 0; FOR i := 1 TO maxfb DO IF (fbs[i].mode = fbIdle) AND (idlefb = 0) THEN idlefb := i; IF idlefb <> 0 THEN BEGIN SetRect(fbs[idlefb].bounds, location.h-30, location.v-30, location.h+30, location.v+30); fbs[idlefb].size := 0; fbs[idlefb].mode := fbGrow; END; END; { of AllocFb } { %subtitle 'Advance (animate) the fireballs.' } PROCEDURE AdvanFb; VAR i: INTEGER; aRect: Rect; fbrect: Rect; { Rectangle for drawing fireballs. } BEGIN PenSize(fbRadRate,fbRadRate); PenPat(black); SetRect(aRect, 0, 0, 60, 60); FOR i := 1 TO maxfb DO BEGIN IF fbs[i].mode <> fbidle THEN CASE fbs[i].mode OF fbGrow: BEGIN fbs[i].size := fbs[i].size + 1; fbBitMap.baseAddr := @fbBits[240*fbs[i].size - 239]; { Select a fireball picture. } CopyBits(fbBitMap, thePort^.PortBits, aRect, fbs[i].bounds, SrcOr, NIL); IF fbs[i].size >= fbRad THEN fbs[i].mode := fbShrink; END; fbShrink: BEGIN fbBitMap.baseAddr := @fbBits[240*fbs[i].size - 239]; { Select the correct fireball picture. } CopyBits(fbBitMap, thePort^.PortBits, aRect, fbs[i].bounds, SrcBic, NIL); fbs[i].size := fbs[i].size - 1; IF fbs[i].size = 0 THEN fbs[i].mode := fbIdle; END; END; { of mode case } END; { of loop } END; { of AdvanFb } { %subtitle 'Create a new enemy missile.' } PROCEDURE AllocMs(position: Point; city: INTEGER; kind: INTEGER); VAR i: INTEGER; idleMs: INTEGER; targetcity: integer; targeth: integer; BEGIN idlems := 0; FOR i := 1 TO maxMs DO IF (missiles[i].mode = msIdle) AND (idlems = 0) THEN idlems := i; IF idlems <> 0 THEN BEGIN i := idlems; missiles[i].start.v := position.v; { Set coordinates for beginning of path. } missiles[i].start.h := position.h; missiles[i].pos.v := missiles[i].start.v * 16; { Initial current position is } missiles[i].pos.h := missiles[i].start.h * 16; { at start of path.o} missiles[i].dv := msSpeed; { Speed of missile. } missiles[i].city := city; targeth := (((missiles[i].city*2 - 1)*FieldWidth) DIV (2*nCities))*16; missiles[i].dh := (targeth-(missiles[i].start.h*16)) DIV (((endv-missiles[i].start.v)*16) DIV missiles[i].dv); missiles[i].kind := kind; missiles[i].mode := msActive; nMissiles := nMissiles + 1; END; END; FUNCTION BlackPixel(aPoint: Point) : BOOLEAN; BEGIN aPoint.h := aPoint.h DIV 16; aPoint.v := aPoint.v DIV 16; BlackPixel := (GetPixel(aPoint.h, aPoint.v) AND GetPixel(aPoint.h+1, aPoint.v)); END; { %subtitle 'Advance (animate) the enemy missiles.' } PROCEDURE AdvanMs; VAR i, j: INTEGER; newpos, newpixel: Point; detonate: BOOLEAN; Points: INTEGER; BEGIN penMode(PatCopy); FOR i := 1 TO maxms DO BEGIN IF missiles[i].mode <> msIdle THEN BEGIN newpos.v := missiles[i].pos.v + missiles[i].dv; newpos.h := missiles[i].pos.h + missiles[i].dh; newpixel.v := newpos.v DIV 16; newpixel.h := newpos.h DIV 16; detonate := FALSE; IF BlackPixel(newpos) OR BlackPixel(missiles[i].pos) THEN detonate := TRUE; PenSize(mWidth, mWidth); PenPat(gray); MoveTo(missiles[i].pos.h DIV 16, missiles[i].pos.v DIV 16); LineTo(newpixel.h, newpixel.v); missiles[i].pos := newpos; IF (missiles[i].kind = msMirv) { If it's a Mirv, } AND (newpixel.v > mirvHeight) { and it's reached the right altitude, } THEN BEGIN { then make lots of little missiles... } FOR j := 1 TO nCities DO IF (j <> missiles[i].city) AND (Rnd(100) < mirvNasty) THEN allocMs(newpixel, j, msNormal); missiles[i].kind := msNormal; END; IF (newpos.v DIV 16 >= endv) { If the missile has reached its target } OR detonate { or hit a fireball, } THEN BEGIN PenSize(mWidth+2, mWidth+2); PenPat(white); MoveTo(missiles[i].start.h-1, missiles[i].start.v-1); { Erase the line. } LineTo(newpixel.h-1, newpixel.v-1); missiles[i].mode := msIdle; { turn off the missile, } nMissiles := nMissiles-1; IF detonate THEN BEGIN IF (MFlag2 = 2) OR ((MFlag2 = 1) AND (Rnd(25) > RoundNumber)) THEN allocFb(newpixel); { Make a new fireball. } eDestroyed := eDestroyed + 1; Score := Score + MPoints[missiles[i].kind]; UpdScore; END ELSE IF cities[missiles[i].city] THEN BEGIN allocfb(newpixel); cities[missiles[i].city] := false; citiesLeft := citiesLeft - 1; IF citiesLeft+bCities = 0 THEN gameOver := true; END; END; { Of detonate routine. } END; { Of THEN clause for this active missile. } END; { of missile loop } END; { of AdvanMs } { %subtitle 'Print a string in the center of the window.' } PROCEDURE Centre (TheText: Str255; posV: INTEGER); BEGIN MoveTo((FieldWidth-StringWidth(TheText))DIV 2, posV); DrawString(TheText); END; { %subtitle 'Clear the screen.' } PROCEDURE ClearScreen; VAR aRect: Rect; BEGIN aRect.top := 0; aRect.left := 0; aRect.bottom := FieldHeight; aRect.right := FieldWidth; FillRect(aRect, White); END; { %subtitle 'Draw a city.' } PROCEDURE DrawCity (city: INTEGER); VAR CityLoc: Point; bOffset: INTEGER; aRect: Rect; BEGIN; RandSeed := 2; { Make each city look the same. } cityloc.v := FieldHeight - CityHeight; cityloc.h := ((city*2-1) * FieldWidth) DIV (2*nCities); bOffset := -(CityWidth DIV 2); WHILE bOffset<(CityWidth DIV 2) DO BEGIN aRect.left := cityloc.h + bOffset; aRect.top := cityloc.v - Rnd(BuildHeight); aRect.bottom := cityloc.v + 2; aRect.right := aRect.left + BuildWidth; FillRect(aRect, CityPattern^^); bOffset := bOffset + BuildWidth; END; END; { %subtitle 'Draw cities and bottom line.' } PROCEDURE DrawStuff; VAR i: INTEGER; aRect: Rect; BEGIN ClearScreen; FOR i := 1 TO nCities DO IF cities[i] THEN DrawCity(i); PenPat(Black); RandSeed := TickCount; { Randomize } BottomLine; END; { of DrawStuff } { %subtitle 'MinMax - Convert Num to String with range checking. } FUNCTION MinMax(TheString: Str255; minimum: INTEGER; Maximum: INTEGER) : INTEGER; VAR TheNumber: LONGINT; BEGIN StringToNum(TheString, TheNumber); IF TheNumber<Minimum THEN TheNumber := Minimum; IF TheNumber>Maximum THEN TheNumber := Maximum; MinMax := TheNumber; END; { %subtitle 'DoGameOptions - Does the big dialog box.' } Procedure DoGameOptions; CONST { Here are all the magic constants that go with that monster dialog box: } OK = 1; { OK button } GSpeed = 5; { Game Speed text box } SRound = 7; { Start Round text box } MPB = 18; { MPBase text boxes } MPE = 24; { MPExtra text boxes } MF1 = 8; { MFlag1 check box } MF2 = 10; { MFlag2 radio buttons } MEX = 15; { MExists check boxes } VAR savePort:grafptr; { For saving the GrafPort and restoring later. } DStorage: DialogRecord; { Storage for my dialog box. } DPtr: DialogPtr; { Pointer to my dialog box. } ItemHit: integer; { Item that was just hit by the user. } TheType: integer; { not used } TheHandle: Handle; { Temp. handle } TheRect: Rect; { not used. } TheString: str255; { Temp. string } TheValue: INTEGER; { Temp. integer value. } i: INTEGER; { Loop Variable. } Station: INTEGER; { Current setting of radio buttons. } BEGIN DPtr := getNewDialog(box2_id, @DStorage, pointer(-1)); { Load the data from disk . . . } GetDItem(Dptr, GSpeed, theType, theHandle, theRect); { Set up all the EditText boxes } NumToString(GameSpeed, TheString); SetIText(theHandle, TheString); GetDItem(Dptr, SRound, theType, theHandle, theRect); NumToString(StartRound, TheString); SetIText(theHandle, TheString); FOR i := 0 TO 2 DO BEGIN GetDItem(Dptr, MPB+i, theType, theHandle, theRect); { Text for points each missile is worth. } NumToString(MPBase[i+1], TheString); SetIText(theHandle, TheString); GetDItem(Dptr, MPB+3+i, theType, theHandle, theRect); { Text for plus sign. } TheString := '+'; SetIText(theHandle, TheString); GetDItem(Dptr, MPE+i, theType, theHandle, theRect); { Text for additional points each round } NumToString(MPExtra[i+1], TheString); SetIText(theHandle, TheString); GetDItem(Dptr, MPE+3+i, theType, theHandle, theRect); { text : '* round' } TheString := '* round'; SetIText(theHandle, TheString); GetDItem(Dptr, MEX+i, theType, theHandle, theRect); { Check boxes for types of missiles } TheValue := 0; IF MExists[i+1] THEN TheValue := 1; SetCtlValue(TheHandle, TheValue); END; FOR i := 0 to 2 DO BEGIN { Program the radio buttons. } GetDItem(Dptr, MF2+i, theType, theHandle, theRect); TheValue := 0; IF MFlag2 = i THEN TheValue := 1; SetCtlValue(Pointer(TheHandle), TheValue); END; Station := MF2 + MFlag2; GetDItem(Dptr, MF1, theType, theHandle, theRect); { Set check box for MFlag1. } TheValue := 0; IF MFlag1 THEN TheValue := 1; SetCtlValue(Pointer(TheHandle), TheValue); SelIText(DPtr, sRound, 0, 1000); { Initial selection is StartRound. } REPEAT ModalDialog(NIL, ItemHit); { Let them hit an item... } IF ItemHit IN [10,11,12] { Was it one of the radio buttons? } THEN BEGIN GetDItem(Dptr, Station, theType, theHandle, theRect); { Get the old one, and turn it off. } SetCtlValue(Pointer(TheHandle), 0); Station := ItemHit; GetDItem(Dptr, Station, theType, theHandle, theRect); { Turn this one on. } SetCtlValue(Pointer(TheHandle), 1); END; IF ItemHit IN [8,15,16,17] { Was it a check box? } THEN BEGIN GetDItem(Dptr, ItemHit, TheType, TheHandle, TheRect); TheValue := 1-GetCtlValue(Pointer(TheHandle)); { Find the value and invert it. } SetCtlValue(Pointer(TheHandle), TheValue); END; UNTIL ItemHit in [1,2]; if itemHit = OK then begin { Have to get all the new values now! } GetDItem(Dptr, GSpeed, theType, theHandle, theRect); { Set up all the EditText boxes } GetIText(theHandle, TheString); GameSpeed := MinMax(TheString, 1, 99); GetDItem(Dptr, SRound, theType, theHandle, theRect); GetIText(theHandle, TheString); StartRound := MinMax(TheString, 1, 99); FOR i := 0 TO 2 DO BEGIN GetDItem(Dptr, MPB+i, theType, theHandle, theRect); { Text for points each missile is worth. } GetIText(theHandle, TheString); MPBase[i+1] := MinMax(TheString, -1000, 1000); GetDItem(Dptr, MPE+i, theType, theHandle, theRect); { Text for additional points each round } GetIText(theHandle, TheString); MPExtra[i+1] := MinMax(TheString, -1000, 1000); GetDItem(Dptr, MEX+i, theType, theHandle, theRect); { Check boxes for types of missiles } IF GetCtlValue(Pointer(TheHandle)) = 0 THEN MExists[i+1] := False ELSE MExists[i+1] := True; END; FOR i := 0 to 2 DO BEGIN { radio buttons. } GetDItem(Dptr, MF2+i, theType, theHandle, theRect); IF GetCtlValue(Pointer(TheHandle)) = 1 THEN MFlag2 := i; END; GetDItem(Dptr, MF1, theType, theHandle, theRect); { MFlag1. } IF GetCtlValue(Pointer(TheHandle)) = 0 THEN MFlag1 := False ELSE MFlag1 := True; end; {if itemHit = OK} DisposDialog(DPtr); GetPort(savePort); {save whatever port was current} SetPort(MyWindow); InvalRect(ScreenRect); {force the entire screen, including frame, to be redrawn} SetPort(savePort); end; { of DoGameOptions } { %subtitle 'DoCommand - Does all the menu commands.' } PROCEDURE DoCommand (mResult: LongInt); VAR name: STR255; theMenu, theItem: INTEGER; { Menu and item numbers. } dummy: BOOLEAN; BEGIN theMenu := HiWord(mResult); theItem := LoWord(mResult); CASE theMenu OF appleMenu: { About Missile and Desk Accessories. } IF theItem = 1 THEN AboutMissile ELSE BEGIN GetItem(myMenus[1],theItem,name); refNum := OpenDeskAcc(name); END; fileMenu: { There is currently only one option in this menu. } BEGIN doneFlag := TRUE; PauseFlag := False; esFlag := False; END; EditMenu: { Cut, copy, paste, and undo. } BEGIN dummy := SystemEdit(theItem-1); { Can't cut and paste in game window. } END; { of editMenu } GameMenu: BEGIN { SetPort(myWindow); } CASE theItem OF 1: PauseFlag := True; { pause game } 2: PauseFlag := False; { resume game } 3: ; { Empty line in menu. } 4: DoGameOptions; 5: BEGIN { New Game. } GameOver := True; EndDelay := 100; esFlag := False; PauseFlag := False; END; END; { of item case } END; { of editMenu } END; { of menu case } HiliteMenu(0); END; { of DoCommand } { %subtitle 'Routine to check for events.' } PROCEDURE CheckEvents; VAR MousePoint: Point; MouseCode: INTEGER; TheChar: CHAR; i: FIXED; fbrect: Rect; { Rectangle for drawing fireballs. } BEGIN REPEAT SystemTask; GetMouse(MousePoint); LocalToGlobal(MousePoint); MouseCode := FindWindow(MousePoint,whichWindow); IF FrontWindow = MyWindow { If my window is activated, set cursor: } THEN IF (WhichWindow = MyWindow) { If it's above my window, } AND (Mousecode <> inMenuBar) { but not the scroll bar, } AND (NOT PauseFlag) { the game is 'active', } AND (FrontWindow = MyWindow) { my window is in front, } AND (yLeft > 0) { and the user has missiles left, then: } THEN SetCursor(crosshairs) { Set the Missile Command cursor. } ELSE SetCursor(Arrow); { Otherwise, use an arrow. } WHILE GetNextEvent(everyEvent,myEvent) DO CASE myEvent.what OF mouseDown: BEGIN code := FindWindow(myEvent.where,whichWindow); CASE code OF inMenuBar: DoCommand(MenuSelect(myEvent.where)); inSysWindow: SystemClick(myEvent,whichWindow); inDrag: ; { Can't drag game window - it's too hard to figure out collisions off-screen. } inGrow, inContent: BEGIN IF (whichWindow <> FrontWindow) AND (WhichWindow <> MyWindow) { Can't select MyWindow - must close other windows. } THEN SelectWindow(whichWindow) ELSE IF (NOT PauseFlag) { Can't make fireballs while paused. } AND (FrontWindow = MyWindow) { Game must be running } AND (yLeft > 0) { Make sure they have some missiles left. } THEN BEGIN GlobalToLocal(myEvent.where); IF myEvent.where.v > NukeHeight { If it's too low, } THEN myEvent.where.v := NukeHeight; { make it legal. } AllocFb(myEvent.where); { Put a fireball in the list. } yLeft := yLeft - 1; { They have less missiles now... } updScore; { let them know. } END; END; END; { of code case } END; { of mouseDown } keyDown: { No Autokey - don't want to repeat Option-N. } IF ((MyEvent.modifiers DIV 256)MOD 2<>0) { If command key was held down } THEN BEGIN TheChar := CHR(myEvent.message MOD 256); DoCommand(MenuKey(TheChar)); END; updateEvt: BEGIN SetPort(myWindow); BeginUpdate(myWindow); IF Playing THEN BEGIN FOR i:= 1 TO nCities DO IF cities[i] THEN BEGIN DrawCity(i); END; RandSeed := TickCount; { Randomize after drawing cities. } FOR i:= 1 TO maxMs DO IF missiles[i].mode = msActive THEN BEGIN PenSize(mWidth, mWidth); PenPat(Gray); MoveTo(missiles[i].start.h, missiles[i].start.v); LineTo(missiles[i].pos.h DIV 16, missiles[i].pos.v DIV 16); END; FOR i:= 1 TO maxFb DO IF fbs[i].mode <> fbIdle THEN BEGIN fbRect := fbs[i].bounds; insetRect(fbRect, 30-2*fbs[i].size, 30-2*fbs[i].size); PenMode(PatCopy); FillOval(fbRect, Black); END; END; BottomLine; EndUpdate(myWindow); END; { of updateEvt } END; { of event case } UNTIL (FrontWindow = MyWindow) { If another window has been selected, } AND NOT PauseFlag; { don't do anything except process events } { until game window is re-selected. } END; { of CheckEvents } { %subtitle 'Print the GAME OVER message.' } PROCEDURE EndScreen; VAR aRect: Rect; i: INTEGER; GameOffset, OverOffset: INTEGER; Center: Point; BEGIN TextSize(72); TextMode(srcBic); PenMode(patOr); PenPat(Black); PenSize(5,5); GameOffset := StringWidth('GAME') DIV 2; OverOffset := StringWidth('OVER') DIV 2; Center.v := FieldHeight DIV 2; Center.h := FieldWidth DIV 2; SetRect(aRect, Center.h, Center.v, Center.h, Center.v); FOR i := 1 TO 30 DO BEGIN CheckEvents; InsetRect(aRect,-5,-5); FrameOval(aRect); TextSize(72); TextMode(srcBic); Centre('GAME', Center.v - 12); Centre('OVER', Center.v + 60); END; PenMode(patBic); FOR i := 1 TO 30 DO BEGIN CheckEvents; FrameOval(aRect); InsetRect(aRect,5,5); END; END; { of EndScreen } { %subtitle 'Play a single game.' } PROCEDURE PlayGame; VAR i: INTEGER; s: Str255; msPoint: Point; msCity, msKind: INTEGER; rMissiles: INTEGER; { "round missiles" - number to set eLeft to each round. } yMissiles: INTEGER; { "your Missiles" - similar to rMissiles. } RoundOver: BOOLEAN; aString: Str255; Points: INTEGER; PROCEDURE InitRound; VAR i: INTEGER; BEGIN RoundOver := False; endDelay := 0; Playing := True; DrawStuff; { Redraw screen. } FOR i := 1 TO maxfb DO fbs[i].mode := fbIdle; FOR i := 1 TO maxms DO missiles[i].mode := msIdle; FOR i := 1 TO 3 DO MPoints[i] := MPBase[i] + MPExtra[i] * RoundNumber; FOR i := 1 TO nCities DO cities2[i] := cities[i]; nMissiles := 0; MirvRate := 0; mirvHeight := FieldHeight DIV 4; mirvNasty := 30; msSpeed := 60; IF RoundNumber >2 THEN MirvRate := 10; IF RoundNumber >4 THEN msSpeed := 80; IF RoundNumber >6 THEN MirvRate := 20; IF RoundNumber >8 THEN MirvNasty := 50; IF RoundNumber >10 THEN MirvHeight := FieldHeight DIV 3; IF RoundNumber >12 THEN msSpeed := 90; IF RoundNumber >14 THEN MirvRate := 30; IF RoundNumber >19 THEN msSpeed := 100; msRate := 5 + RoundNumber; rMissiles := (RoundNumber*RoundNumber)DIV 6 + RoundNumber + 6; { Compute # of enemy missiles } yMissiles := rMissiles + RoundNumber; { Adjust yMissiles accordingly. } eLeft := rMissiles; yLeft := yMissiles; RoundNumber := RoundNumber + 1; IF (RoundNumber MOD 5) = 0 THEN bCities := bCities + 1; BottomLine; FlushEvents(everyEvent,0); { Ignore unprocessed events from previous round. } END; PROCEDURE EndBonus; VAR i: INTEGER; BEGIN; ClearScreen; BottomLine; TextSize(24); TextMode(srcCopy); aString := 'End of round '; aString[15] := Chr(48+(RoundNumber Mod 10)); IF RoundNumber>9 THEN aString[14] := Chr(48+(RoundNumber DIV 10)); IF CitiesLeft>0 THEN BEGIN { give bonus points for cities } Centre(aString, 100); Centre('Bonus:', 130); Points := 0; FOR i:= 1 TO nCities DO IF (cities[i] AND NOT DoneFlag) THEN BEGIN LastTick := TickCount; DrawCity(i); Points := Points + RoundNumber*20; Score := Score + RoundNumber*20; TextSize(24); NumtoString(Points, S); Centre(S, 160); updScore; REPEAT CheckEvents; UNTIL (TickCount >= LastTick + 30) OR DoneFlag; END END; { of bonus for cities. } IF (bCities > 0) AND (CitiesLeft < 6) THEN BEGIN { Give a bonus city. } LastTick := TickCount; TextSize(24); Centre('* Bonus City *', 190); CitiesLeft := CitiesLeft + 1; bCities := bCities - 1; REPEAT i := Rnd(6) UNTIL (NOT Cities[i]); Cities[i] := true; REPEAT CheckEvents; UNTIL (TickCount >= LastTick + 60) OR DoneFlag; END { of bonus city routine. } END; { of EndBonus } BEGIN RoundNumber := StartRound; { Start out at this round. } Score := 0; eDestroyed := 0; FOR i := 1 TO nCities DO cities[i] := true; citiesLeft := nCities; bCities := 0; NukeHeight := FieldHeight - CityHeight - (BuildHeight DIV 2) - fbRad*fbRadRate; endv := FieldHeight - CityHeight - (BuildHeight DIV 2); GameOver := False; esFlag := True; lastTick := TickCount; REPEAT { Repeat loop for playing rounds. } InitRound; REPEAT { Repeat loop for animating objects in game. } CheckEvents; IF TickCount >= lastTick + gameSpeed { If enough time has elapsed since the last update, update objects on screen. } THEN BEGIN lastTick := TickCount; IF (eLeft <= 0) AND (nMissiles = 0) THEN RoundOver := True; IF RoundOver OR GameOver THEN endDelay := endDelay + 1 ELSE IF (rnd(100) < msRate) AND (eLeft > 0) THEN BEGIN { Launch an enemy missile every now and then. } eLeft := eLeft - 1; msPoint.v := 0; msPoint.h := Rnd(FieldWidth - 4); REPEAT msCity := Rnd(nCities); { Pick a city. } UNTIL ((Rnd(3) = 1) AND mFlag1) OR cities2[msCity]; { Try again for most missiles if city was destroyed. } msKind := 0; IF MExists[msNormal] THEN msKind := msNormal; IF (rnd(100) <= MirvRate) AND MExists[msMirv] THEN msKind := msMirv; IF msKind = 0 THEN msKind := msNormal; AllocMs(msPoint, msCity, msKind); END; AdvanFb; { Advance state of fireballs. } AdvanMs; { Advance position of missiles. } END; UNTIL ((GameOver OR RoundOver) AND (endDelay > 30)) OR doneFlag; { End of loop to move objects on screen. } Playing := False; IF esFlag AND (CitiesLeft+bCities>0) AND NOT DoneFlag THEN EndBonus; UNTIL GameOver OR DoneFlag; { End of loop to play rounds. } IF Score > HighScore THEN HighScore := Score; IF esFlag THEN EndScreen; END; { of PlayGame } { %subtitle 'Main program.' } BEGIN { main program } SetUp; REPEAT PlayGame; UNTIL doneFlag; END.
This file is "missile.r":
* Missiler -- resource file for Missile * (Robert Munafo @ Dartmouth College) RJUNK27.RSRC * These are the menus for the program - * Menu ID * Menu title * Menu Item * Menu Item, etc. Type MENU ,1 @ About Missile (- ,256 File Quit ,257 Edit Undo/Z (- Cut/X Copy/C Paste/V Clear ,258 Game Pause/S Resume/Q (- Game Options/O New Game/N * String information * ID * string Type STR ,300 Missile Command by Robert P. Munafo ,350 Vers 2.3 August 18,1984 * Dialog Box for About Missile * ID * display rectangle * visible, standard box, no close box, RefCon * ID of content list Type DLOG ,450 100 110 190 402 Visible 1 NoGoAway 0 500 * Dialog Item List for About Missile * ID of list * number of items in list * a button that can be hit * display rectangle (coordinates local to box) * title of button Type DITL ,500 3 BtnItem Enabled 65 106 85 186 OK StatText Disabled 10 10 30 300 StatText Disabled 35 10 55 300 * Dialog box for Game Options Type DLOG ,451 40 40 302 472 Visible 1 NoGoAway 0 501 * Dialog item list for Game Options * Lots and lots of little options for people to play around with. * Run the program if you want to see what the dialog box looks like... Type DITL ,501 29 BtnItem Enabled 200 320 220 400 OK BtnItem Enabled 230 320 250 400 Cancel StatText Disabled 8 130 24 400 Missile Command Options StatText Disabled 40 4 56 124 Animation delay EditText Enabled 40 140 56 170 num StatText Disabled 40 200 56 350 Start off with round # EditText Enabled 40 360 56 380 num ChkItem Enabled 64 4 80 400 Missiles aim for dead cities StatText Disabled 88 4 104 400 Missiles blow up when destroyed: RadioItem Enabled 104 4 120 150 Always RadioItem Enabled 104 160 120 400 Never RadioItem Enabled 120 4 136 400 Less often with each round StatText disabled 144 4 160 100 Type: StatText disabled 144 160 160 400 Points for destroying: ChkItem enabled 176 4 192 140 Missiles ChkItem enabled 196 4 212 140 MIRVs ChkItem disabled 216 4 232 140 Smart bombs EditText enabled 176 160 192 190 num EditText enabled 196 160 212 190 num EditText enabled 216 160 232 190 num StatText disabled 176 200 192 208 plus StatText disabled 196 200 212 208 plus StatText disabled 216 200 232 208 plus EditText enabled 176 215 192 245 num EditText enabled 196 215 212 245 num EditText enabled 216 215 232 245 num StatText disabled 176 255 192 300 x round StatText disabled 196 255 212 300 x round StatText disabled 216 255 232 300 x round * Crosshairs cursor. It has no mask, so it will look * like a white crosshairs on a black background. * ID * hex data * hex mask * hotspot (v,h) Type CURS ,256 0180018001800180018001800180FFFFFFFF0180018001800180018001800180 0000000000000000000000000000000000000000000000000000000000000000 0008 0008 * Pattern of closely spaced vertical stripes, for drawing * the cities. Type PAT ,256 5555555555555555 * This is the window the game graphics are drawn in. * It is so big that all of its edges are off-screen - * this makes it appear that the program is drawing on * the entire screen without using a window. Type WIND ,256 Missile Command 20 0 342 512 Visible NoGoAway 0 0 * The Finder must be able to find this string - * it identifies the version number of the program. Type RJUN = STR ,0 Game Vers 2.3 August 18,1984 * Part of the "Finder Info" : * Resource ID (purgable) * Hex for "APPL" * Local icon ID * name of file that must be transferred along with * the application when it is copied ("00" means none) Type FREF = HEXA ,128(32) 4150504C 0000 00 * Special icon and mask for application. * The mask is a big solid rectangle to make it easy * to select in the Finder. Type ICN# = HEXA ,128 (32) 00000000 00000000 00000000 00000000 00000000 00000000 01000100 01000200 00800400 00800800 00401000 00402000 00204000 00008380 000107C0 000207C0 000467C0 0008F380 0010F000 00206000 00400000 00800000 01000000 00000000 00000000 00000000 20202020 28282828 3A3A3A3A 7E7E7E7E 00000000 00000000 00000000 00000000 00000000 00000000 00000000 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 00000000 * Bundle data for Missile: * Bundle ID * the owner ("RJUN" in hex), and ID of version data * # of types in the bundle (less one) - in this case, * there are two types. * "ICN#" in hex, and # of icons less one. * Local ID 0 maps to global ID 256. * "FREF" in hex, and # of FREF's less one. * Local ID 0 maps to global ID 256. * (note that if the game created any documents, this * bundle would need at least two icons and two FREF's.) Type BNDL = HEXA ,128 524A554E 0000 0001 49434E23 0000 0000 0080 46524546 0000 0000 0080 * The code - my Exec file always saves the executable * code with this name. Type CODE RJUNK27L,0
This page was written in the "embarrassingly readable" markup language RHTF, and was last updated on 2010 Feb 08. s.27