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 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 TheNumberMaximum 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