REM ><Invaders$Dir>.Src.Invaders
REM Taito Space Invaders (Circa 1978) Clone
REM (c) DynaByte Software 2008
REM ASH Heap manager by Steve Revill (c)2004 7th Software.

REM Keys  |    Title      | In-Game
REM ======+===============+=============
REM SPACE |  Start Game   |
REM S     |  Screenshot   | Screenshot

REM Z     |               | Left.
REM X     |               | Right.
REM Enter |               | Fire.
REM M     |  Music On/Off | Music On/Off
REM P     |               | Pause/Resume Game.
REM Q     |  Quit Program | Suicide.


REM *************************************************************
REM Initialisation Part 1... (Files & Environment.)
REM *************************************************************

REM Setup error trapping and announce ourselves...
ON ERROR PROCtrap_error:END
MODE28:OFF                             :REM 800*600 (256 Colours)
string$="Acorn Invaders. (c)2008 DynaByte Software"
PRINT string$:PRINT STRING$(LEN(string$),"-"):PRINT

REM Check we have all our essential files...
REM Our Hiscore table is a special case as it can be either
REM generated if one doesn't exist, or we can carry on regardless
REM if we can't create it.

REM Locate files... (Mission critical...)
bail%=FALSE
app_dir$="<Invaders$Dir>.Resources."
PRINT"Checking files..."
IF NOT FNfind(app_dir$+"Gfx0","Sprites")   THEN bail%=TRUE
IF NOT FNfind(app_dir$+"Gfx1","Charset")   THEN bail%=TRUE
IF NOT FNfind(app_dir$+"Intro","Music")    THEN bail%=TRUE
IF NOT FNfind(app_dir$+"Creds","Credits")  THEN bail%=TRUE
IF NOT FNfind(app_dir$+"Msg","Messages")   THEN bail%=TRUE
REMIF NOT FNfind(app_dir$+"ImgLUT","Lookups") THEN bail%=TRUE
IF bail% THEN ERROR 255,"File Missing!!"
PRINT

REM Load messages & Image Lookup Table...
REM We do both here as they are exactly the same code, except they
REM setup different global variables...
PRINT"Loading Messages..."
string_max%=FNstring_enumprocessed(app_dir$+"Msg","SGSM")
DIM string_num%(string_max%),string_msg$(string_max%)
PROCstring_load(app_dir$+"Msg","SGSM",string_num%(),string_msg$())


REM Load and start music playing...
PRINTFNstring_get(1)
OSCLI"PlayVolume 0":OSCLI"Playmod "+app_dir$+"Intro"
OSCLI"PlayPause"

REM Setup our variables. (These are used all over...)
bail%=FALSE                            :REM TRUE to quit game.
dump_num%=0                            :REM Screendump counter.
music_status%=1                        :REM Music on if =1.
scores_save%=TRUE                      :REM TRUE to save HiScores.
cred_size%=0                           :REM Credit screen stuff.
DIM scores_name$(10),scores_value%(10) :REM HiScore table.
DIM scores_rack%(10)

PRINTFNstring_get(2)
PROCash_init                           :REM Kick the heap manager.

REM Load Image Lookup Table...
REM Further investigation needs to be carried out here to see if
REM this will adversely affect speed during gameplay.
REM PRINTFNstring_get(3):PROCstring_loadlut

REM Initialise Graphics Sub-system...
REM The variables we're interested in here will be...
REM gfx_spriteop%   -> The OS Call we'll use to draw graphics.
REM gfx_spritepool% -> Block of RAM where our sprites reside.
REM gfx_fontpool%   -> Block of RAM where our charset resides.
REM gfx_area%       -> Pointer to our current graphics pool.
SYS "OS_SWINumberFromString",, "OS_SpriteOp" TO gfx_spriteop%
PRINTFNstring_get(4)
gfx_spritepool%=FNgfx_load(app_dir$+"Gfx0")
PRINTFNstring_get(5)
gfx_fontpool%=FNgfx_load(app_dir$+"Gfx1")
gfx_area%=gfx_spritepool%

REM Load Credit screen data...
REM This was originally as strings in the listing, but
REM implementing this in a loop reading data from an array takes
REM less space and tidies up the listing somewhat.
PRINTFNstring_get(6)
hdl%=OPENIN(app_dir$+"Creds")
 INPUT#hdl%,magic$                     :REM Read magic and verify.
 IF magic$<>"DERC" THEN
  CLOSE#hdl%:ERROR 255,FNstring_get(101):END
 ENDIF
 INPUT#hdl%,cred_size%                 :REM Number of lines...
 DIM cred_str$(cred_size%),cred_spc%(cred_size%)

 FOR i%=1 TO cred_size%
  INPUT#hdl%,cred_spc%(i%),cred_str$(i%)
 NEXT i%
CLOSE#hdl%

REM HiScore Table...
REM Here, we look for and load, if it exists, our Hiscores file.
REM If we can't find one, we check whether we can write to the
REM location before we attempt to create one. If we can't
REM do that, then we can still run, but the user's scores will
REM not be retained on exit.
PRINTFNstring_get(7);
hifile$=app_dir$+"HiTable"   :REM Location of file.
IF FNfs_find(hifile$)<>1 THEN
 IF NOT FNfs_writeable(0,app_dir$) THEN
  scores_save%=FALSE:PRINTFNstring_get(8)
 ELSE
  PRINTFNstring_get(9)
 ENDIF
 PROChitable_create(scores_save%)
ELSE
 IF NOT FNfs_writeable(1,hifile$) THEN
  scores_save%=FALSE:PRINTFNstring_get(10)
 ELSE
  PRINTFNstring_get(11)
 ENDIF
 PROChitable_load
ENDIF

PRINTFNstring_get(12):PROCwait(150)
REM *************************************************************
REM Initialisation Part 2... (Object offsets.)
REM *************************************************************

REM Objects... Each object (Base, saucer, missile etc.) is
REM given a block of memory up to 68 bytes long to hold all
REM relevant data for that object. This, apart from being
REM quicker, is far easier to code and saves memory (See
REM tutorial). These are tokens to address offsets in each
REM of those blocks of memory. (See tutorial for how this
REM works...) Contents for offsets 36-68 (flags1% - flags9%) will
REM be different, depending on the nature of the object. Not all
REM objects will have all 9 option flags available. I'll define
REM LOCAL variables with more meaningful names and point them to
REM these in the object's handler routines.

REM Object offsets...
xpos%=0:ypos%=4:xsize%=8:ysize%=12     :REM Position & Size.
dir%=16:speed%=20                      :REM Movement.
active%=24:beenhit%=28:timer%=32       :REM Object Status.
flags1%=36:flags2%=40:flags3%=44       :REM Misc flags.
flags4%=48:flags5%=52:flags6%=56
flags7%=60:flags8%=64:flags9%=68

REM Define offsets for screen and rail objects.
x_eig%=16:y_eig%=20:setmode%=24        :REM Offsets in screen obj.
xfont%=28:yfont%=32
toprail%=16:baserail%=20               :REM Offsets in rail obj.

REM *************************************************************
REM Initialisation Part 3... (Global objects and variables.)
REM *************************************************************

REM Reserve memory for global object blocks...
screen%=FNash_claim(36):rail%=FNash_claim(24) :REM Global objects.
box%=FNash_claim(16)

REM Get the video display into a VGA 256 colour mode. This is
REM also the same MODE as our sprites. Call to PROCget_screeninfo
REM calls the OS to get some values and sets up our screen object
REM for us.
CLS:PROCget_screeninfo                 :REM Setup screen object.
screen%!xfont%=FNconv_units(8,1)
screen%!yfont%=FNconv_units(8,2)+7

REM Define property values for other global objects.
rail%!xsize%=FNconv_units(6,1)         :REM Rail properties.
rail%!ysize%=FNconv_units(6,2)
rail%!toprail%=screen%!ysize%-50
rail%!baserail%=30

REM *************************************************************
REM Main Loop... (Title>Game>Title.)
REM *************************************************************

OSCLI"PlayVolume 127":OSCLI"PlayStart" :REM Start music playing..
REPEAT
 CLS:bail%=FNtitle_loop
 IF NOT bail% THEN CLS:PROCmain
UNTIL bail%

REM Enclose our goodbyes in a border...
REM Setup our border size and position. Centre this on the screen
REM and then drop it down by 3px. (Raising the text by 3px would
REM break too many things!) Call PROCdraw_box() to produce our
REM border and then display our text. Once we've done all that, we
REM start to fade our music and bail back to the desktop.
CLS:box%!xsize%=220:box%!ysize%=40
box%=FNcentre_sprite_xy(box%,screen%):box%!ypos%-=3
PROCdraw_box(box%):PROCcentre_text_xy(FNstring_get(31),10)
IF music_status%=1 THEN
 FOR i%=127 TO 1 STEP-2
  OSCLI"PlayVolume "+STR$(i%):PROCwait(4)
 NEXT i%
 OSCLI"PlayStop":OSCLI"PlayVolume 127"
ENDIF
PROCash_destroy                   :REM Tear down ASH Subsystem...
END

REM *************************************************************
REM Title Loop... (Display our title sequence.)
REM *************************************************************

REM Animate in our initial screen layout and then perform a cycle
REM between Credits, Score Advance, Keyboard Controls & HiScore
REM Table screens. Do a keyboard scan periodically and check
REM for -17 (Q) and -99 (Space Bar) and return from function
REM accordingly... Object hwind% points to the area of the screen
REM where where the current screen in the sequence gets placed in
REM the attract mode. This is the area below "Invaders" and above
REM "PRESS SPACE TO PLAY" text. Objects inv1% - inv3% are
REM identical copies of each other, except for values of ypos%.
REM Our saucer% object here, is only large enough to hold screen
REM co-ordinates. It is made LOCAL to this function so we can
REM re-define it for the main game loop.

DEFFNtitle_loop
 LOCAL i%,space%,invt%,hwind%,str%,saucer%,inv1%,inv2%
 LOCAL inv3%,keyz%,keyx%,keye%,keyq%,keym%,x%,y%,rc%,s$,string$

 REM Allocate & initialise title sequence objects using ASH...
 space%=FNash_blockfill(FNash_claim(16),0)
 invt%=FNash_blockfill(FNash_claim(16),0)
 hwind%=FNash_blockfill(FNash_claim(16),0)
 saucer%=FNash_blockfill(FNash_claim(16),0)
 inv1%=FNash_blockfill(FNash_claim(16),0)
 keyz%=FNash_blockfill(FNash_claim(16),0)

 REM Initialise LOCAL (title sequence) variables.
 space%!xsize%=FNconv_units(100,1)  :REM Space text properties.
 space%!ysize%=FNconv_units(40,2)
 space%!xpos%=FNcentre_sprite(space%,screen%)
 space%!ypos%=800

 invt%!xsize%=FNconv_units(150,1)   :REM Invaders text properties.
 invt%!ysize%=FNconv_units(40,2)
 invt%!xpos%=FNcentre_sprite(invt%,screen%)
 invt%!ypos%=space%!ypos%-invt%!ysize%

 hwind%!xsize%=640:hwind%!ysize%=500:REM Window properties.
 hwind%!xpos%=FNcentre_sprite(hwind%,screen%)
 hwind%!ypos%=200

 saucer%!xsize%=FNconv_units(40,1)  :REM Saucer properties.
 saucer%!ysize%=FNconv_units(20,2)
 saucer%!xpos%=hwind%!xpos%+150
 saucer%!ypos%=(hwind%!ypos%+hwind%!ysize%)-160

 inv1%!xsize%=FNconv_units(20,1)    :REM Common invader props.
 inv1%!ysize%=FNconv_units(20,2)
 inv1%!xpos%=hwind%!xpos%+150+FNcentre_sprite(inv1%,saucer%)

 inv2%=FNash_blockcopy(inv1%)       :REM Create two copies.
 inv3%=FNash_blockcopy(inv1%)

 inv1%!ypos%=hwind%!ypos%+100       :REM Unique invader props.
 inv2%!ypos%=inv1%!ypos%+inv1%!ysize%+40
 inv3%!ypos%=inv2%!ypos%+inv2%!ysize%+40

 keyz%!xsize%=FNconv_units(20,1)    :REM KB Controls "keys".
 keyz%!ysize%=FNconv_units(20,2)
 keyz%!xpos%=hwind%!xpos%+150+FNcentre_sprite(keyz%,saucer%)

 keyx%=FNash_blockcopy(keyz%)       :REM Create five copies
 keye%=FNash_blockcopy(keyz%)       :REM for the other keys.
 keym%=FNash_blockcopy(keyz%)
 keyq%=FNash_blockcopy(keyz%)
 keyp%=FNash_blockcopy(keyz%)
                                    :REM Unique "keys" props.
 keyz%!ypos%=(hwind%!ypos%+hwind%!ysize%)-120
 keyx%!ypos%=keyz%!ypos%-60:keye%!ypos%=keyx%!ypos%-60
 keym%!ypos%=keye%!ypos%-90:keyp%!ypos%=keym%!ypos%-60
 keyq%!ypos%=keyp%!ypos%-60

 REM Animate the initial screen title "Space Invaders" & "PRESS
 REM SPACE TO PLAY" messages.
 PROCdraw_rail(rail%!toprail%):PROCdraw_rail(rail%!baserail%)

 FOR i%=0-space%!xsize% TO space%!xpos% STEP 5
  WAIT:PROCplot("space",i%,space%!ypos%)
  PROCwait(1)
 NEXT i%

 FOR i%=screen%!xsize% TO invt%!xpos% STEP -5
  WAIT:PROCplot("invaders",i%,invt%!ypos%)
  PROCwait(1)
 NEXT i%

 string$=FNstring_get(32):str%=LEN(string$)*screen%!xfont%:y%=170
 PROCprint(FNcentre_text(str%,screen%!xsize%),y%,string$,11)
 string$=FNstring_get(33)
 str%=LEN(string$)*screen%!xfont%:y%-=screen%!yfont%
 PROCprint(FNcentre_text(str%,screen%!xsize%),y%,string$,11)

 REM Cycle the remainder of the attract mode.
 WHILE NOT INKEY(-99) AND NOT INKEY(-17) AND NOT INKEY(-82)

  REM Display Credits Screen...
  REM Text strings are held in cred_str$() inencrypted form.
  REM Line spacing info is held in the matching element of
  REM cred_spc%(). Negative values have special meanings...

  REM Value   | Line spacing is...
  REM --------+---------------------
  REM   -1    | 1 * screen%!yfont% (Next line.)
  REM   -2    | 2 * screen%!yfont% (Leave a blank line.)
  REM  <+n>   | Leave a gap of <n> OS Units...
  y%=hwind%!ypos%+hwind%!ysize%
  FOR i%=1 TO cred_size%
   string$=FNencode_string(cred_str$(i%))
   x%=FNtitle_stringcentre(string$)
   CASE cred_spc%(i%) OF
    WHEN -1   : y%-=screen%!yfont%
    WHEN -2   : y%-=(2*screen%!yfont%)
    OTHERWISE : y%-=cred_spc%(i%)
   ENDCASE
   PROCprint(x%,y%,string$,0)
  NEXT i%

  REM Wait for 750 counts or until SPACE or Q pressed...
  rc%=FNtitle_wait
  IF rc%=1 THEN =TRUE
  IF rc%=2 THEN =FALSE
  PROCwipe_sprite(hwind%)

  REM Display Score Advance table...
  string$=FNstring_get(34):x%=FNtitle_stringcentre(string$)
  y%=hwind%!ypos%+hwind%!ysize%-20:PROCprint(x%,y%,string$,0)

  string$=FNstring_get(35):y%-=screen%!yfont%
  x%=FNtitle_stringcentre(string$):PROCprint(x%,y%,string$,0)

  x%=saucer%!xpos%+saucer%!xsize%+10
  PROCplot("saucer_0",saucer%!xpos%,saucer%!ypos%)
  string$=FNstring_get(36)+" "+FNstring_get(40)
  PROCprint(x%,saucer%!ypos%+10,string$,0)
  PROCplot("inv_3",inv3%!xpos%,inv3%!ypos%)
  string$=FNstring_get(37)+" "+FNstring_get(40)
  PROCprint(x%,inv3%!ypos%+12,string$,0)
  PROCplot("inv_2",inv2%!xpos%,inv2%!ypos%)
  string$=FNstring_get(38)+" "+FNstring_get(40)
  PROCprint(x%,inv2%!ypos%+12,string$,0)
  PROCplot("inv_1",inv1%!xpos%,inv1%!ypos%)
  string$=FNstring_get(39)+" "+FNstring_get(40)
  PROCprint(x%,inv1%!ypos%+12,string$,0)

  REM Wait for 750 counts or until SPACE or Q pressed...
  rc%=FNtitle_wait
  IF rc%=1 THEN =TRUE
  IF rc%=2 THEN =FALSE
  PROCwipe_sprite(hwind%)

  REM Display Keyboard controls screen...
  string$=FNstring_get(41):x%=FNtitle_stringcentre(string$)
  y%=hwind%!ypos%+hwind%!ysize%-20:PROCprint(x%,y%,string$,0)

  x%=keyz%!xpos%+keyz%!xsize%+10
  PROCplot("key_z",keyz%!xpos%,keyz%!ypos%)
  string$=FNstring_get(42)+" "+FNstring_get(43)
  PROCprint(x%,keyz%!ypos%+12,string$,0)
  PROCplot("key_x",keyx%!xpos%,keyx%!ypos%)
  string$=FNstring_get(42)+" "+FNstring_get(44)
  PROCprint(x%,keyx%!ypos%+12,string$,0)
  PROCplot("key_enter",keye%!xpos%,keye%!ypos%)
  PROCprint(x%,keye%!ypos%+12,FNstring_get(45),0)
  PROCplot("key_p",keyp%!xpos%,keyp%!ypos%)
  string$=FNstring_get(46)+" "+FNstring_get(49)
  PROCprint(x%,keyp%!ypos%+12,string$,0)
  PROCplot("key_m",keym%!xpos%,keym%!ypos%)
  PROCprint(x%,keym%!ypos%+12,FNstring_get(47),0)
  PROCplot("key_q",keyq%!xpos%,keyq%!ypos%)
  string$=FNstring_get(48)+" "+FNstring_get(49)
  PROCprint(x%,keyq%!ypos%+12,string$,0)

  REM Wait for 750 counts or until SPACE or Q pressed...
  rc%=FNtitle_wait
  IF rc%=1 THEN =TRUE
  IF rc%=2 THEN =FALSE
  PROCwipe_sprite(hwind%)

  REM Display HiScore table...
  REM First line... (This is conditional on scores_save%)
  string$=FNstring_get(50)
  IF scores_save% THEN string$=FNstring_get(51)
  x%=FNtitle_stringcentre(string$)
  y%=hwind%!ypos%+hwind%!ysize%-20
  PROCprint(x%,y%,string$,0)

  REM Second line...
  string$=FNstring_get(58)+" "+FNstring_get(52)
  x%=FNtitle_stringcentre(string$):y%-=screen%!yfont%
  PROCprint(x%,y%,string$,0)

  REM Loop through the HiScore table arrays displaying names and
  REM scores...
  y%=(hwind%!ypos%+hwind%!ysize%)-100
  string$=" POS."+"  PLAYER."+STRING$(12," ")+"SCORE."
  string$+=STRING$(3," ")+"RACK."
  x%=FNtitle_stringcentre(string$)
  PROCprint(x%,y%,string$,0)
  y%-=2*screen%!yfont%
  FOR i%=1 TO 10
   IF i%<10 THEN
    string$="  "+STR$(i%)+".  "
   ELSE
    string$=" "+STR$(i%)+".  "
   ENDIF
   s$=FNencode_string(scores_name$(i%))
   string$+=FNstr_pad(s$,8," ",FALSE):string$+=" .... "
   string$+=FNstr_rightalign(STR$(scores_value%(i%)),10)
   string$+=" ... "+FNstr_rightalign(STR$(scores_rack%(i%)),3)
   x%=FNtitle_stringcentre(string$)
   PROCprint(x%,y%-(i%*(screen%!yfont%+5)),string$,0)
  NEXT i%

  REM Wait for 750 counts or until SPACE or Q pressed...
  rc%=FNtitle_wait
  IF rc%=1 THEN =TRUE
  IF rc%=2 THEN =FALSE
  PROCwipe_sprite(hwind%)
 ENDWHILE

 REM This should never get executed...
 ERROR 255,FNstring_get(102)
=FALSE

REM Wait for 750 counts or SPACE or Q to be pressed. Returns one
REM of three values in rc%
REM 0 -> Timeout period expired... (750 counts up.)
REM 1 -> "Q" to Quit...
REM 2 -> SPACE to Play...
DEFFNtitle_wait
 LOCAL t%,rc%
 t%=TIME+750:rc%=FALSE
 REPEAT
  IF INKEY(-82) THEN PROCevnt_kb_screendump
  IF INKEY(-102) THEN PROCevnt_kb_togglemusic
 UNTIL TIME>=t% OR INKEY(-17) OR INKEY(-99)
 IF INKEY(-17) THEN rc%=1          :REM Get outta here (Q)...
 IF INKEY(-99) THEN rc%=2          :REM Play a game...

 REM If rc% is TRUE (non zero value) then we either start a game
 REM or quit. Either way, we need to release the memory claimed
 REM by our objects.
 IF rc% THEN
  PROCash_free(space%):PROCash_free(invt%):PROCash_free(hwind%)
  PROCash_free(saucer%):PROCash_free(inv1%):PROCash_free(inv2%)
  PROCash_free(inv3%):PROCash_free(keyz%):PROCash_free(keyx%)
  PROCash_free(keye%):PROCash_free(keyq%):PROCash_free(keym%)
  PROCash_free(keyp%)
 ENDIF
=rc%

REM Prepare a string for display...
REM Centering a string requires a number of parameters to be setup
REM before calling PROCprint(). The process is largely repeatitive
REM All we do is change a few values.
DEFFNtitle_stringcentre(string$)
 LOCAL str%
 str%=LEN(string$)*screen%!xfont%
=hwind%!xpos%+FNcentre_text(str%,hwind%!xsize%)

REM *************************************************************
REM Game Loop... (Play one instance of the game.)
REM *************************************************************

REM Loop through one instance of the game. This basically means...
REM Setup in-game stuff. -> Play until out of bases. -> Manage
REM hiscore entry. -> Return.
DEFPROCmain
 LOCAL base%,shell%,saucer%,rack%,player% :REM Objects.
 LOCAL inv%
 LOCAL invstat%(),invfire%()              :REM Invader status.
 LOCAL lives%,racknum%,suicide%,score%    :REM Player properties.
 LOCAL paused%
 LOCAL colgap%,xnum%,ynum%,rownum%,frcap% :REM Rack properties.

 LOCAL evnt_kb%,str%,string$,x%,y%,i%,j%  :REM Misc.

 REM Claim memory for local objects using ASH...
 base%=FNash_claim(32):shell%=FNash_claim(40)
 saucer%=FNash_claim(48):rack%=FNash_claim(68)
 inv%=FNash_claim(16):player%=FNash_claim(36)

 REM Setup arrays and object pointers...
 DIM invstat%(10,6),invfire%(7)
 lives%=0:score%=4:racknum%=8:suicide%=12    :REM Player tokens.
 paused%=16:pstatus%=20
 colgap%=flags2%:xnum%=flags3%:ynum%=flags4% :REM Rack tokens.
 rownum%=flags7%:frcap%=flags8%

 REM Initialise LOCAL (in-game) variables.
 base%!xsize%=FNconv_units(30,1)          :REM Base properties.
 base%!ysize%=FNconv_units(15,2)
 base%!xpos%=FNcentre_sprite(base%,screen%)
 base%!ypos%=50:base%!speed%=6:base%!dir%=base%!speed%

 inv%!xsize%=FNconv_units(20,1)           :REM Invader properties.
 inv%!ysize%=FNconv_units(20,2)

 saucer%!xsize%=FNconv_units(40,1)        :REM Saucer properties.
 saucer%!ysize%=FNconv_units(20,2)
 saucer%!ypos%=rail%!toprail%-(10+saucer%!ysize%)
 saucer%!speed%=5:saucer%!active%=FALSE
 saucer%!flags3%=1

 shell%!xsize%=FNconv_units(5,1)          :REM Shell properties.
 shell%!ysize%=FNconv_units(20,2)
 shell%!active%=FALSE:shell%!speed%=10

 player%!racknum%=1:player%!score%=0      :REM Player properties.
 player%!lives%=3:player%!suicide%=FALSE
 player%!paused%=0:player%!timer%=0

 rack%!colgap%=FNconv_units(20,1)         :REM Rack properties.
 rack%!xnum%=10:rack%!ynum%=6
 rack%!active%=TRUE:rack%!frcap%=4

 FOR i%=1 TO 7                                 :REM Invader fire.
  invfire%(i%)=FNash_claim(44)                 :REM Claim memory.
  invfire%(i%)=FNash_blockfill(invfire%(i%),0) :REM Clear block.
  invfire%(i%)!xsize%=FNconv_units(5,1)
  invfire%(i%)!ysize%=FNconv_units(20,2)
 NEXT i%

 REM Initialise game screen...
 REM Draw the top and bottom rails, prepare and display the HUD &
 REM sort out the first rack of invaders. Finally... Display the
 REM player's base.
 PROCdraw_rail(rail%!toprail%):PROCdraw_rail(rail%!baserail%)
 PROChud_init(0,scores_value%(1),player%!lives%,player%!racknum%)

 PROCrack_gameinit                          :REM Setup invaders.
 PROCplot("base",base%!xpos%,base%!ypos%)   :REM Display our base.

 REM Our event generator...
 REM This is the main polling loop. The program sits tight in this
 REM loop firing off events until the player runs out of lives.
 REPEAT

  REM Keyboard poll...
  REM We only check the top four if we haven't paused the game.
  REM The rest we always need to check.
  evnt_kb%=FALSE
  IF player%!paused%=0 THEN
   IF INKEY(-74) THENPROCevnt_kb_basefire(base%!xpos%,base%!ypos%)
   IF INKEY(-98) THENPROCevnt_kb_baseleft:evnt_kb%=TRUE
   IF INKEY(-67) THENPROCevnt_kb_baseright:evnt_kb%=TRUE
   IF INKEY(-17) THENPROCevnt_kb_suicide     :REM(Q)Suicide...
  ENDIF

  IF INKEY(-102)THENPROCevnt_kb_togglemusic :REM(M)Music On/Off
  IF INKEY(-82) THENPROCevnt_kb_screendump  :REM(S)Screenshot...
  IF INKEY(-56) THENPROCevnt_kb_togglepause :REM(P)Pause Game...

  REM Update screen...
  REM This is our equivalent of an arcade game's VBlank interrupt.
  REM We've polled the input device (our keyboard) and we're
  REM idling whilst processing events (evnt_kb%=FALSE). Either
  REM way, something's going on, usually AI/Collision detection
  REM related.

  REM Post-process evnt_kb event updates. (Keyboard.)
  REM The only two that require action here are left & right as
  REM we need to display the base at the new position.
  IF player%!paused%=0
   IF evnt_kb% THEN
    base%!xpos%+=base%!dir%
    PROCplot("base",base%!xpos%,base%!ypos%)
   ENDIF
  ENDIF

  REM Process evnt_fr events. (Frame Refresh.)
  IF player%!lives%>0 THEN
   IF player%!paused%=0 THEN
    PROCevnt_fr_basefire       :REM Animate player's shots.
    PROCevnt_fr_saucer         :REM Saucer handling & AI.
    PROCevnt_fr_rackmove       :REM Move rack & Collision detect.
    PROCevnt_fr_rackfire       :REM Rack missile fire control.
   ELSE
    PROCevnt_fr_pausegame      :REM Game paused display.
   ENDIF
  ENDIF

  REM Wait for next VBlank
  WAIT
 UNTIL player%!lives%=0

 REM Display GAME OVER!!! in a bordered rectangle in the middle
 REM of the screen.
 box%!xsize%=240:box%!ysize%=40
 box%=FNcentre_sprite_xy(box%,screen%):box%!ypos%-=3
 PROCwipe_sprite(box%):PROCdraw_box(box%)
 string$=FNstring_get(49)+" "+FNstring_get(53)
 PROCcentre_text_xy(string$,11):PROCwait(750)

 REM Check & Handle HiTable entry...
 IF NOT player%!suicide% THEN
  IF player%!score%>=scores_value%(10) THEN
   PROChitable_entry(player%)
  ENDIF
 ENDIF

 REM Free memory used for objects...
 PROCash_free(base%):PROCash_free(rack%):PROCash_free(saucer%)
 PROCash_free(shell%):PROCash_free(player%):PROCash_free(inv%)
 FOR i%=1 TO 7:PROCash_free(invfire%(i%)):NEXT i%
ENDPROC

REM *************************************************************
REM Proceedures (Keyboard events.)
REM *************************************************************

REM Move base left. If we're already at left edge then bail...
DEFPROCevnt_kb_baseleft
 REM Collision detection. (Base & Playfield.)
 IF base%!xpos%<=0 THEN
  base%!xpos%=0                            :REM Don't move.
 ELSE
  base%!dir%=0-base%!speed%                :REM Change direction.
 ENDIF
ENDPROC

REM Move base right. If we're already at the right edge, bail...
DEFPROCevnt_kb_baseright
 REM Collision detection. (Base & Playfield.)
 IF base%!xpos%>=screen%!xsize%-base%!xsize% THEN
  base%!xpos%=screen%!xsize%-base%!xsize%  :REM Don't move.
 ELSE
  base%!dir%=0+base%!speed%                :REM Change direction.
 ENDIF
ENDPROC

REM Fire a shell. If we're already firing a shell bail. (We can
REM only fire one shell at a time or else the animation gets
REM confused.) shell%!active% is a property of the shell% object
REM that is set boolean TRUE when player fires a shell and is
REM cleared when either the shell scrolls off the top of the
REM screen or hits something.
DEFPROCevnt_kb_basefire(x%,y%)
 REM Bail if shell already active.
 IF NOT shell%!active% THEN
  shell%!active%=TRUE                       :REM Set active.
  shell%!xpos%=x%+(base%!xsize%/2)          :REM Initial position.
  shell%!ypos%=y%+base%!ysize%
 ENDIF
ENDPROC

REM Debug only... Dump a screen to disc as "<Invaders$Dir>.Dump"
DEFPROCevnt_kb_screendump
 REPEAT:UNTIL NOT INKEY(-82)
 OSCLI"ScreenSave <Invaders$Dir>.Dump"+STR$(dump_num%)
 dump_num%+=1
ENDPROC

REM Player has chickened out. (Committed suicide. 'Q' in-game.)
REM Set lives to zero and flag it up as a suicide event in the
REM player object. (Skip Hiscore entry if suicide set.)
DEFPROCevnt_kb_suicide
 LOCAL i%
 FOR i%=1 TO 7:invfire%(i%)!active%=FALSE:NEXT i%
 player%!lives%=0:player%!suicide%=TRUE
ENDPROC

REM Toggle Music on & off...
DEFPROCevnt_kb_togglemusic
 REPEAT:UNTIL NOT INKEY(-102)              :REM De-bounce key.
 music_status%=FNtoggle(music_status%)
 IF music_status%=1 THEN OSCLI"PlayStart" ELSE OSCLI"PlayPause"
ENDPROC

REM Pause & Resume game...
REM Toggle between pause and resume. The actual logic is done
REM elsewhere.
DEFPROCevnt_kb_togglepause
 REPEAT UNTIL NOT INKEY(-56)               :REM De-bounce key.
 player%!paused%=FNtoggle(player%!paused%) :REM Toggle On/Off.
 player%!pstatus%=player%!paused%          :REM Set visible flag.
 PROChud_pause(player%!pstatus%)           :REM Update HUD.
ENDPROC

REM Test the saucer explosion timer...
REM Generate a fake collision detection event between the player's
REM shell and the saucer... Setup required flags and return. Score
REM is not affected. (Testing only..)
DEFPROCevnt_kb_saucerkill
 IF saucer%!active% THEN
  IF NOT saucer%!beenhit% THEN
   saucer%!beenhit%=TRUE:saucer%!flags1%=saucer%!timer%
   PROCplot("saucer_2",saucer%!xpos%,saucer%!ypos%)
  ENDIF
 ENDIF
ENDPROC

REM *************************************************************
REM Proceedures (VBlank/Frame Events)
REM *************************************************************

REM Continue animation of a player's shell... If one hasn't been
REM created then we bail back to event loop.
DEFPROCevnt_fr_basefire
 LOCAL out%,sh_cpy%:out%=flags1%

 IF shell%!active% THEN
  REM shell%!out% is set TRUE if the current shell is out of play.
  shell%!out%=FALSE
  IF shell%!ypos%>=rail%!toprail%-shell%!ysize% THEN
   shell%!active%=FALSE:shell%!out%=TRUE       :REM Hit top.
  ENDIF

  REM Decide what to plot. Wipe sprite if we're out of play.
  IF shell%!out% THEN
   sh_cpy%=FNash_blockcopy(shell%)             :REM Clear shell.
   sh_cpy%!ypos%-=2:PROCwipe_sprite(sh_cpy%):PROCash_free(sh_cpy%)
  ELSE
   PROCplot("shell",shell%!xpos%,shell%!ypos%) :REM Display shell.
  ENDIF

  shell%!ypos%+=shell%!speed%                  :REM Advance frame.
 ENDIF
ENDPROC

REM Process saucer animation & generation events. A saucer can be
REM randomely activated n% of the time, but not if one is
REM currently on the screen. When activated, there is a 50% chance
REM of it starting on the left hand side of the screen, likewise
REM we have a 50% chance of it starting on the right.
DEFPROCevnt_fr_saucer
 LOCAL hit%,spr%,toggle%,fr$
 hit%=flags1%:spr%=flags2%  :REM Object tokens.
 toggle%=flags3%

 IF NOT saucer%!active% THEN
  IF FNpct(4) AND FNpct(5) THEN
   saucer%!active%=TRUE:saucer%!beenhit%=FALSE:saucer%!spr%=0
   saucer%!toggle%=0

   REM Decide which side of the screen to start from.
   IF FNpct(50) THEN
    saucer%!dir%=0-saucer%!speed%          :REM Right edge. (50%)
    saucer%!xpos%=screen%!xsize%
   ELSE
    saucer%!dir%=0+saucer%!speed%          :REM Left edge. (50%)
    saucer%!xpos%=0-saucer%!xsize%
   ENDIF
  ENDIF
 ELSE
  saucer%!spr%+=1:saucer%!timer%+=1

  REM Has saucer gone off screen??
  IF saucer%!xpos%<=0-saucer%!xsize%ANDSGN(saucer%!dir%)=-1 THEN
   saucer%!hit%=FALSE:saucer%!active%=FALSE:saucer%!timer%=0
   ENDPROC
  ENDIF
  IF saucer%!xpos%>screen%!xsize%ANDSGN(saucer%!dir%)=1 THEN
   saucer%!hit%=FALSE:saucer%!active%=FALSE:saucer%!timer%=0
   ENDPROC
  ENDIF

  REM Has saucer been hit by player shot??
  IF NOT saucer%!beenhit% THEN
   IF FNhit(shell%,saucer%) THEN
    PROCwipe_sprite(shell%)
    PROCplot("saucer_2",saucer%!xpos%,saucer%!ypos%)
    saucer%!hit%=saucer%!timer%:saucer%!beenhit%=TRUE
    shell%!active%=FALSE:PROChud_scorehit(500)
   ENDIF
  ELSE
   IF FNhit(shell%,saucer%) THEN
    PROCwipe_sprite(shell%):shell%!active%=FALSE
   ENDIF
  ENDIF

  REM Animate saucer...
  IF NOT saucer%!beenhit% THEN
   IF saucer%!spr%>=1000 THEN saucer%!spr%=0

   REM Change image every ten frames.
   IF saucer%!spr%MOD10=0 THEN
    saucer%!toggle%=FNtoggle(saucer%!toggle%)
   ENDIF

   REM Display one of two images.
   fr$="saucer_"+STR$(saucer%!toggle%)
   PROCplot(fr$,saucer%!xpos%,saucer%!ypos%)
   saucer%!xpos%+=saucer%!dir%
  ELSE

   REM Animate/Remove explosion...
   IF saucer%!timer%>=saucer%!hit%+80 THEN
    PROCwipe_sprite(saucer%)
    saucer%!active%=FALSE:saucer%!hit%=FALSE
    saucer%!timer%=0:saucer%!toggle%=TRUE
   ELSE
    IF saucer%!timer%>=saucer%!hit%+60 THEN
     PROCplot("saucer_5",saucer%!xpos%,saucer%!ypos%)
    ELSE
     IF saucer%!timer%>=saucer%!hit%+40 THEN
      PROCplot("saucer_4",saucer%!xpos%,saucer%!ypos%)
     ELSE
      IF saucer%!timer%>=saucer%!hit%+20 THEN
       PROCplot("saucer_3",saucer%!xpos%,saucer%!ypos%)
      ENDIF
     ENDIF
    ENDIF
   ENDIF
  ENDIF
 ENDIF
ENDPROC

REM Check out the rack of invaders. Move left, right & down. Deal
REM with collision detection between player missiles and invaders.
DEFPROCevnt_fr_rackmove
 LOCAL rack_ext%,z%,col%,row%,eclr%,sclr%,rclr%,offset%
 LOCAL frame%,scol%,ecol%

 REM Setup meaningful pointers to our object flags...
 REM Vars colgap%, xnum%, ynum%, rownum% & frcap% have already
 REM been tied to flags2%, flags3%, flags4%, flags7% & flags8%
 REM respectively.
 frame%=flags1%:scol%=flags5%:ecol%=flags6%

 REM Check timer related stuff. Can't let this value get too high
 REM or it will overwrite other memory locations. Still need to
 REM find the precise limit for this, but resetting the counter
 REM every 1,000 calls or so should prevent any over-runs.
 rack%!timer%+=1
 IF rack%!timer% MOD 5=0 THEN rack%!frame%=FNtoggle(rack%!frame%)
 IF rack%!timer%>1000 THEN rack%!timer%=0

 REM Deal with rack movement...
 REM Move rack left, right & down. Check each time we move down
 REM to make sure that the rack hasn't invaded the base. If it has
 REM then end the game.
 rack_ext%=rack%!xpos%+rack%!xsize%
 offset%=0-((rack%!scol%-1)*(inv%!xsize%+rack%!colgap%))

 IF rack_ext%>=screen%!xsize% THEN
  rack%!dir%=0-rack%!speed%:PROCrack_advance
 ENDIF
 IF rack%!xpos%<=offset% THEN
  rack%!dir%=0+rack%!speed%:PROCrack_advance
 ENDIF

 REM Have we reached the base?
 offset%=rack%!ypos%+((rack%!rownum%-1)*inv%!ysize%)
 IF offset%<=base%!ypos%+base%!ysize% THEN
  PROCrack_endgame:ENDPROC
 ENDIF

 REM Deal with collision detection...
 REM Firstly we check to see if the player's shell lays somewhere
 REM inside the rack of invaders. If this is not the case then we
 REM bail and take no further action this time. However, if we
 REM are inside the rack (FNhit(shell%,rack%)=TRUE) then we need
 REM to see what, if anything, we've actually hit.
 IF FNhit(shell%,rack%) THEN
  z%=inv%!xsize%+rack%!colgap%
  IF (shell%!xpos%-rack%!xpos%) MOD z% <= inv%!xsize% THEN

   REM We've hit something... Find out what.
   col%=((shell%!ypos%-rack%!ypos%) DIV inv%!ysize%)+1
   row%=((shell%!xpos%-rack%!xpos%) DIV z%)+1

   REM Act on it...
   REM Vars col% & row% now contain the index of the invader that
   REM the player hit. We now check the status of that invader
   REM and act accordingly. A value of zero indicates that it's
   REM already dead and buried, whilst a negative value is used
   REM as a countdown timer for the explosion sprite. We are only
   REM interested in positive non-zero values. Anything else, we
   REM take no action.
   IF SGN(invstat%(row%,col%))=1 THEN
    invstat%(row%,col%)=-5            :REM Set explosion timer.
    PROCwipe_sprite(shell%)           :REM Remove shell.
    shell%!active%=FALSE              :REM De-activate shell.
    rack%!beenhit%+=1                 :REM Another one down.

    REM Scoring...
    CASE col% OF
     WHEN 1,2 : PROChud_scorehit(50)  :REM Bottom two rows.
     WHEN 3,4 : PROChud_scorehit(75)  :REM Middle two rows.
     WHEN 5,6 : PROChud_scorehit(100) :REM Top two rows.
    ENDCASE
   ENDIF
  ENDIF
 ENDIF

 REM Have we destroyed the last invader in the current rack?
 IF rack%!beenhit%>=rack%!xnum%*rack%!ynum% THEN
  player%!racknum%+=1                 :REM Advance rack counter.
  PROCwipe_sprite(rack%)              :REM Clear the last invader.
  PROCdraw_rail(rail%!baserail%)      :REM Re-draw bottom rail.
  PROChud_lives(player%!lives%)       :REM Update HUD.
  PROChud_rack(player%!racknum%)
  PROCrack_init                       :REM Create new rack.
 ENDIF

 REM Do we need to resize the rack? The rack is defined as a block
 REM of a fixed size. We display the invaders in this block. Each
 REM time we clear the left or rightmost column or the bottom row,
 REM we need to update the size of this block to keep the display
 REM accurate.
 eclr%=TRUE:sclr%=TRUE
 FOR col%=1 TO rack%!ynum%
  IF invstat%(rack%!ecol%,col%)<>0 THEN eclr%=FALSE
  IF invstat%(rack%!scol%,col%)<>0 THEN sclr%=FALSE
 NEXT col%

 rclr%=TRUE
 FOR row%=1 TO rack%!xnum%
  IF invstat%(row%,rack%!rownum%)<>0 THEN rclr%=FALSE
 NEXT row%

 IF sclr% THEN rack%!scol%+=1
 IF eclr% THEN
  rack%!xsize%-=(inv%!xsize%+rack%!colgap%):rack%!ecol%-=1
 ENDIF
 IF rclr% THEN rack%!rownum%+=1

 REM Finally... Redraw the rack...
 rack%!xpos%+=rack%!dir%:PROCrack_redraw
ENDPROC

REM Invader missile fire logic...
REM This... Like all the rest of these animation routines is
REM timer based. We call this event handler once every
REM repetition of the game loop. All invader missile information
REM is held in our invfire%() array. This array is just an array
REM of object pointers. Data can be accessed using offsets just
REM like all the others. Every call to this event advances the
REM animation by one frame and then handles display & collision
REM detection for that frame before returning.
DEFPROCevnt_fr_rackfire
 LOCAL frame%,type%,spd%,i%,j%,x%,y%,fpos%,frate%,fr$,err$
 frame%=flags1%:type%=flags2%

 REM Loop through all invader missile slots...
 FOR i%=1 TO 7
  IF NOT invfire%(i%)!active% THEN
   REM If we haven't already got an active missile in this slot
   REM then we create one. The likelyhood of this happening is
   REM a percentage chance based on the player's current level +5.
   REM This is capped by a second call to FNpct(), the value of
   REM which is initially 4. This value is incremented by 1 for
   REM every 100 racks the player destroys.

   REM Property | Value  | Significance
   REM ---------+--------+--------------
   REM type%    | 1 or 2 | Missile type. (Type 1 can be destroyed)
   REM frame%   | 0 or 1 | Current frame.

   REM Missile types are defined 75/25. Having a 75% chance of
   REM being type 1. Speed for type 1 is 4 OS Units per frame, for
   REM type 2, we half that.
   fpos%=RND(10):frate%=player%!racknum%+5
   IF frate%>100 THEN frate%=100:rack%!frcap%+=1
   IF FNpct(frate%) AND FNpct(rack%!frcap%) THEN
    IF invstat%(fpos%,rack%!rownum%)>0 THEN
     x%=rack%!xpos%+(fpos%-1)*(inv%!xsize%+rack%!colgap%)
     y%=rack%!ypos%+(rack%!rownum%-1)*inv%!ysize%
     invfire%(i%)!xpos%=x%+inv%!xsize%/2:invfire%(i%)!ypos%=y%
     invfire%(i%)!active%=TRUE:invfire%(i%)!beenhit%=FALSE
     invfire%(i%)!timer%=0:invfire%(i%)!type%=1
     invfire%(i%)!frame%=0:spd%=4
     IF FNpct(25) THEN invfire%(i%)!type%=2:spd%=spd%/2
     invfire%(i%)!speed%=spd%
     invfire%(i%)!dir%=invfire%(i%)!speed%
    ENDIF
   ENDIF
  ELSE
   REM Continue animating down the screen...
   REM Handle the image swapping (for the animation) and the
   REM position of the currently selected invader missile.
   invfire%(i%)!timer%+=1
   IF invfire%(i%)!timer% MOD 5=0 THEN
    invfire%(i%)!frame%=FNtoggle(invfire%(i%)!frame%)
   ENDIF
   IF invfire%(i%)!timer%>1000 THEN invfire%(i%)!timer%=0
   invfire%(i%)!ypos%-=invfire%(i%)!dir%
   fr$="miss_"+STR$(invfire%(i%)!type%)+STR$(invfire%(i%)!frame%)

   REM Display sprite...
   PROCplot(fr$,invfire%(i%)!xpos%,invfire%(i%)!ypos%)

   REM Have we reached the bottom??
   IF invfire%(i%)!ypos%<=rail%!baserail%+(rail%!ysize%+5) THEN
    invfire%(i%)!active%=FALSE:PROCwipe_sprite(invfire%(i%))
   ENDIF

   REM Collision Detection...
   REM One of two events can happen here. (We've already checked
   REM the third.) Either we hit the player's base, or a shell
   REM hits us on the way up.

   REM Have we hit the player's base??
   IF FNhit(invfire%(i%),base%) AND invfire%(i%)!active% THEN
    FOR j%=1 TO 3
     PROCplot("base_"+STR$(j%),base%!xpos%,base%!ypos%)
     PROCwait(20):WAIT
    NEXT j%
    PROCwipe_sprite(base%)
    IF shell%!active% THEN
     PROCwipe_sprite(shell%):shell%!active%=FALSE
    ENDIF
    base%!xpos%=FNcentre_sprite(base%,screen%)
    player%!lives%-=1:PROChud_lives(player%!lives%)
    PROCwipe_sprite(invfire%(i%)):invfire%(i%)!active%=FALSE
    PROCplot("base",base%!xpos%,base%!ypos%)
   ENDIF

   REM Has a shell hit us??
   IF FNhit(shell%,invfire%(i%)) AND invfire%(i%)!active% THEN
    IF invfire%(i%)!type%=1 THEN
     invfire%(i%)!active%=FALSE:PROChud_scorehit(5)
     PROCwipe_sprite(invfire%(i%))
    ENDIF
    PROCwipe_sprite(shell%):shell%!active%=FALSE
   ENDIF
  ENDIF
 NEXT i%
ENDPROC

REM Handle the pause status and update the HUD...
REM Text display is carried out by a HUD routine. We need to check
REM and fire it here only...
DEFPROCevnt_fr_pausegame
 player%!timer%+=1
 IF player%!timer%=1000 THEN player%!timer%=0
 IF player%!timer%MOD50=0 THEN
  player%!pstatus%=FNtoggle(player%!pstatus%)
 ENDIF
 PROChud_pause(player%!pstatus%)
ENDPROC

REM *************************************************************
REM Functions & Proceedures. (HiScore Table/HiScore Entry.)
REM *************************************************************

REM Create a clean HiScore table. The value passed to us in save%
REM determines whether we can save the table to disc or not. If
REM we can (save%=TRUE) then we call PROChitable_save to write the
REM file back.
DEFPROChitable_create(save%)
 LOCAL i%,n$
 FOR i%=1 TO 10
  IF i% MOD 2=0 THEN n$="Software" ELSE n$="DynaByte"
  scores_name$(i%)=FNencode_string(n$)
  scores_value%(i%)=(11-i%)*1000:scores_rack%(i%)=0
 NEXT i%
 IF save% THEN PROChitable_save
ENDPROC

REM Load our HiTable from disc. The names are stored in RAM in
REM encoded form and are only decoded for display.
DEFPROChitable_load
 LOCAL i%,file_hdl%
 file_hdl%=OPENIN"<Invaders$Dir>.Resources.HiTable"
  FOR i%=1 TO 10
   INPUT#file_hdl%,scores_name$(i%),scores_value%(i%)
   INPUT#file_hdl%,scores_rack%(i%)
  NEXT i%
 CLOSE#file_hdl%
ENDPROC

REM Save our HiTable back to disc. The names are stored in RAM in
REM encoded form and are only decoded for display.
DEFPROChitable_save
 LOCAL i%,file_hdl%
 file_hdl%=OPENOUT"<Invaders$Dir>.Resources.HiTable"
  FOR i%=1 TO 10
   PRINT#file_hdl%,scores_name$(i%),scores_value%(i%)
   PRINT#file_hdl%,scores_rack%(i%)
  NEXT i%
 CLOSE#file_hdl%
ENDPROC

REM HiScore name entry...
REM Sort through our hiscore array and find out where the player
REM is. (We already know he is worthy.) Allow to enter their name
REM and save the HiTable back to disk (Only if scores_save%=TRUE).
REM then return...
DEFPROChitable_entry(player%)
 LOCAL invt%,space%,done%,g%,i%,pos%,str%,x%,y%,string$

 space%=FNash_claim(16)             :REM Space for objects.
 invt%=FNash_claim(16)

 REM Setup graphical objects for later on...
 space%!xsize%=FNconv_units(100,1)  :REM Space text properties.
 space%!ysize%=FNconv_units(40,2)
 space%!xpos%=FNcentre_sprite(space%,screen%)
 space%!ypos%=800

 invt%!xsize%=FNconv_units(150,1)   :REM Invaders text properties.
 invt%!ysize%=FNconv_units(40,2)
 invt%!xpos%=FNcentre_sprite(invt%,screen%)
 invt%!ypos%=space%!ypos%-invt%!ysize%

 REM Find out where we are and stick it in pos%
 pos%=0
 FOR i%=10 TO 1 STEP -1
  IF player%!score%>=scores_value%(i%) THEN pos%=i%
 NEXT i%

 REM Insert a new entry into the table and drop everything below
 REM us by one place... (We are 5th, so old 5th becomes 6th... 9th
 REM becomes 10th and 10th gets dropped.) We need to do this to
 REM all three arrays...
 FOR i%=9 TO pos% STEP -1
  scores_name$(i%+1)=scores_name$(i%)
  scores_value%(i%+1)=scores_value%(i%)
  scores_rack%(i%+1)=scores_rack%(i%)
 NEXT i%

 REM Insert Player's achieved score & rack into the table...
 scores_value%(pos%)=player%!score%
 scores_rack%(pos%)=player%!racknum%

 REM Now deal with the graphics...
 REM Produce our basic Space Invaders layout a la title sequence.
 CLS:PROCdraw_rail(rail%!toprail%):PROCdraw_rail(rail%!baserail%)
 PROCplot("space",space%!xpos%,space%!ypos%)
 PROCplot("invaders",invt%!xpos%,invt%!ypos%)

 REM Build and display the text.
 string$=FNstring_get(54)+" "+STR$(player%!score%)
 string$+=" "+FNstring_get(55)+" "+STR$(pos%)+FNsuffix(pos%)
 str%=LEN(string$)*screen%!xfont%
 x%=FNcentre_text(str%,screen%!xsize%):y%=invt%!ypos%-170
 PROCprint(x%,y%,string$,0)

 REM Free memory used by objects.
 PROCash_free(space%):PROCash_free(invt%)

 REM Create and display a box...
 box%!xsize%=165:box%!ysize%=40
 box%=FNcentre_sprite_xy(box%,screen%):PROCdraw_box(box%)

 REM Handle player input...
 REM Code 13 is Return/Enter. Code 8 is BackSpace. All other codes
 REM are filtered with FNvalid() before being added to string$
 REM to prevent the program breaking when we can't find a graphic.
 x%=box%!xpos%+20:y%=box%!ypos%+12:string$="":i%=1:done%=FALSE
 OSCLI"FX 21,0"                     :REM Flush Keyboard buffer...
 REPEAT
  g%=GET                            :REM Get a char from Keyboard.

  REM Validate input...
  REM Only allow character input if our string length is less
  REM than 8 characters. Only accept Enter or Backspace for the
  REM 9th character.
  CASE g% OF
   WHEN 13   :done%=TRUE
   WHEN  8   :i%-=1:string$=LEFT$(string$,i%-1)
   OTHERWISE :IF i%<9 AND FNvalid(g%) THEN i%+=1:string$+=CHR$(g%)
  ENDCASE

  REM Update Display...
  REM Var i% will be less than 1 if the first character pressed
  REM was backspace. We need to trap that and make it 1. We also
  REM clear the input field before we re-display string$... This
  REM is so that when Backspace is pressed, the character is
  REM actually removed from the screen as well as from the input.
  REM (string$)
  REM We also need to check if we are trying to display an empty
  REM string, as our PROCprint() will fall over. This occurs if
  REM the first key typed was BackSpace or a character we can't
  REM display. (In the case of the latter, it is not added to
  REM string$, but we still come down here to update the display.)

  IF i%<1 THEN i%=1
  PROCprint(x%,y%,STRING$(8," "),0)

  IF string$="" THEN
   PROCprint(x%,y%," ",0)
  ELSE
   PROCprint(x%,y%,string$,0)
  ENDIF
 UNTIL done%

 REM Add player's name to entry already prepared earlier and save
 REM if we can... (scores_save%=TRUE)
 IF string$="" OR string$=" " THEN string$=FNstring_get(56)
 scores_name$(pos%)=FNencode_string(string$)

 IF scores_save% THEN PROChitable_save
ENDPROC

REM Have we got the ability to display this character?
REM Return TRUE if we can or FALSE if we can't
DEFFNvalid(chr%)
 LOCAL out%
 out%=FALSE
 IF chr%>=32 AND chr%<=126 THEN out%=TRUE
=out%

REM *************************************************************
REM Functions & Proceedures (HUD & Scoring)
REM *************************************************************

REM Initialise the HUD display...
DEFPROChud_init(sc%,hi%,l%,r%)
 LOCAL x%,string$
 string$=FNstring_get(57)+FNstr_pad(STR$(sc%),10," ",TRUE)
 PROCprint(0,screen%!ysize%-screen%!yfont%,string$,0)
 string$=FNstring_get(58)+" "+FNstring_get(57)
 string$+=" "+FNstring_get(59)+FNstr_pad(STR$(hi%),10," ",TRUE)
 x%=screen%!xsize%-LEN(string$)*screen%!xfont%
 PROCprint(x%,screen%!ysize%-screen%!yfont%,string$,0)
 PROChud_lives(l%):PROChud_rack(r%)
ENDPROC

REM Update HUD lives display...
DEFPROChud_lives(num%)
 LOCAL string$
 PROCplot("base",0,0):string$=" = "+STR$(num%)
 PROCprint(base%!xsize%,0,string$,0)
ENDPROC

REM Update HUD rack display...
DEFPROChud_rack(num%)
 LOCAL x%,string$
 string$=FNstring_get(60)+" "+FNstring_get(59)
 string$+=FNstr_pad(STR$(num%),3,"0",TRUE)
 x%=screen%!xsize%-LEN(string$)*screen%!xfont%
 PROCprint(x%,0,string$,0)
ENDPROC

REM Update HUD game paused display...
DEFPROChud_pause(status%)
 LOCAL x%,y%,str%,string$
 string$=FNstring_get(49)+" "+FNstring_get(61)
 IF status%=0 OR player%!paused%=0 THEN
  string$=STRING$(LEN(string$)," ")
 ENDIF
 str%=LEN(string$)*screen%!xfont%
 x%=FNcentre_text(str%,screen%!xsize%)
 y%=screen%!ysize%-screen%!yfont%
 PROCprint(x%,y%,string$,0)
ENDPROC

REM Score a hit...
REM The first part updates the score display on the top left
REM corner of the HUD. If the player's current score is greater
REM than that of the top spot, this value is also reflected in the
REM Hi Score display in the top right.
DEFPROChud_scorehit(value%)
 LOCAL x%,string$
 player%!score%+=value%
 string$=FNstring_get(57)+" "+FNstring_get(59)
 string$+=FNstr_pad(STR$(player%!score%),10," ",TRUE)
 PROCprint(0,screen%!ysize%-screen%!yfont%,string$,0)

 IF player%!score%>scores_value%(1) THEN
  string$=FNstring_get(58)+" "+FNstring_get(57)+" "
  string$+=FNstring_get(59)
  string$+=FNstr_pad(STR$(player%!score%),10," ",TRUE)
  x%=screen%!xsize%-LEN(string$)*screen%!xfont%
  PROCprint(x%,screen%!ysize%-screen%!yfont%,string$,0)
 ENDIF
ENDPROC

REM *************************************************************
REM Functions & Proceedures (Rack & Invaders)
REM *************************************************************

REM Cold initialisation...
REM Called directly by PROCmain once at the beginning of each
REM game to initialise the first wave...
DEFPROCrack_gameinit
 LOCAL frame%,scol%,ecol%,rownum%
 frame%=flags1%:scol%=flags5%:ecol%=flags6%:rownum%=flags7%
 PROCrack_init:PROCrack_redraw
ENDPROC

REM Initialise a rack of invaders...
REM This is called at the beginning of the game and whenever
REM rack%!beenhit%=rack%!xnum%*rack%!ynum% (All invaders hit.)
REM to produce the next wave of the invasion force... We hold the
REM status of each invader in the invstat%() array...

REM Value       Significance
REM ========================
REM  -n         Just been hit.
REM   0         Dead and buried.
REM  >0         Current graphic.
DEFPROCrack_init
 LOCAL col%,row%,s%

 REM Rack properties...
 REM These are set here as they have to be reset at the beginning
 REM of each wave.
 rack%!xpos%=50:rack%!ypos%=550:rack%!frame%=0:rack%!beenhit%=0
 rack%!speed%=3:rack%!dir%=rack%!speed%:rack%!scol%=1
 rack%!ecol%=rack%!xnum%:rack%!rownum%=1

 REM Use s% as a temporary to avoid problems with line length in
 REM !Zap edit window.
 s%=(rack%!xnum%*inv%!xsize%)+((rack%!xnum%-1)*rack%!colgap%)
 rack%!xsize%=s%:rack%!ysize%=rack%!ynum%*inv%!ysize%

 REM Initialise invaders status.
 FOR row%=1 TO rack%!xnum%
  FOR col%=1 TO rack%!ynum%
    invstat%(row%,col%)=((col%+1)/2)+(rack%!frame%*10)
  NEXT col%
 NEXT row%
ENDPROC

REM Redraw the whole rack of invaders at once.
DEFPROCrack_redraw
 LOCAL col%,row%,x%,y%,blk%:blk%=FNash_claim(16)
 FOR row%=rack%!scol% TO rack%!ecol%
  x%=rack%!xpos%+((row%-1)*(inv%!xsize%+rack%!colgap%))
  FOR col%=rack%!rownum% TO rack%!ynum%
   y%=rack%!ypos%+((col%-1)*inv%!ysize%)
   IF invstat%(row%,col%)>0 THEN
    invstat%(row%,col%)=((col%+1)/2)+(rack%!frame%*10)
   ENDIF
   IF invstat%(row%,col%)<>0 THEN
    CASE SGN(invstat%(row%,col%)) OF
     WHEN -1 : PROCrack_checkinv(x%,y%,row%,col%)
     WHEN  1 : PROCplot("inv_"+STR$(invstat%(row%,col%)),x%,y%)
    ENDCASE
   ELSE
    blk%!xpos%=x%:blk%!ypos%=y%:blk%!xsize%=inv%!xsize%
    blk%!ysize%=inv%!ysize%:PROCwipe_sprite(blk%)
   ENDIF
  NEXT col%
 NEXT row%
 PROCash_free(blk%)
ENDPROC

REM We start our explosion at -5. Every time we redraw an invader
REM we add to the count until zero is reached. When this happens
REM we wipe the sprite by creating a temporary object and passing
REM it to PROCwipe_sprite() in the normal way.
REM +----+--------------------------+
REM | x% | Physical X co-ordinate.  |
REM | y% | Physical Y co-ordinate.  |
REM | r% | Row index into array.    |
REM | c% | Column index into array. |
REM +----+--------------------------+
DEFPROCrack_checkinv(x%,y%,r%,c%)
 LOCAL inv_obj%
 inv_obj%=FNash_claim(16)
 invstat%(r%,c%)+=1:PROCplot("inv_4",x%,y%)
 IF invstat%(r%,c%)=0 THEN
  inv_obj%!xpos%=x%:inv_obj%!ypos%=y%
  inv_obj%!xsize%=inv%!xsize%:inv_obj%!ysize%=inv%!ysize%
  PROCwipe_sprite(inv_obj%)
 ENDIF
 PROCash_free(inv_obj%)
ENDPROC

REM Advance rack down by one row...
REM Object blk% hovers above the top row of invaders to allow me a
REM quick use of PROCwipe_sprite() to remove the old top row. We
REM drop a row by dropping 1/3 of a row three times to lessen the
REM "jump". The original used to do something like this, but I'm
REM guessing this was down to the lack of CPU power rather than by
REM design.
DEFPROCrack_advance
 LOCAL blk%,i%:blk%=FNash_claim(16)
 FOR i%=1 TO 3
  rack%!ypos%-=inv%!ysize%/3:PROCrack_redraw
 NEXT i%
 blk%!xpos%=rack%!xpos%
 blk%!ypos%=rack%!ypos%+rack%!ysize%
 blk%!xsize%=rack%!xsize%:blk%!ysize%=inv%!ysize%
 PROCwipe_sprite(blk%):PROCash_free(blk%)
ENDPROC

REM End game...
REM We are called when the rack of invaders has reached the
REM player's base. Do a little animation before we return to the
REM Hiscore entry etc...
DEFPROCrack_endgame
 LOCAL inv_left%,inv_right%,frame%,alt%,speed%,i%,img$
 speed%=1:inv_ext%=24

 REM Reserve space...
 inv_left%=FNash_claim(28):inv_right%=FNash_claim(28)

 REM Clear any in-game debris from the screen. (Invaders, saucers
 REM and other junk.)
 PROCwipe_sprite(rack%):PROCwipe_sprite(base%)
 IF shell%!active% THEN
  PROCwipe_sprite(shell%):shell%!active%=FALSE
 ENDIF
 IF saucer%!active% THEN
  PROCwipe_sprite(saucer%):saucer%!active%=FALSE
 ENDIF
 FOR i%=1 TO 7
  IF invfire%(i%)!active% THEN
   PROCwipe_sprite(invfire%(i%)):invfire%(i%)!active%=FALSE
  ENDIF
 NEXT i%

 REM Setup initial values.
 base%!xpos%=FNcentre_sprite(base%,screen%)
 inv_left%!xpos%=0-inv%!xsize%      :REM Left invader properties.
 inv_left%!ypos%=base%!ypos%
 inv_left%!dir%=speed%

 inv_right%!xpos%=screen%!xsize%    :REM Right invader properties.
 inv_right%!ypos%=base%!ypos%
 inv_right%!dir%=0-speed%

 REM Zero any remaining lives & update bottom of HUD...
 player%!lives%=0:PROCdraw_rail(rail%!baserail%)
 PROChud_lives(player%!lives%):PROChud_rack(player%!racknum%)

 REM Hit it!!
 REM Two invaders, one from each side, approach the base and blow
 REM it up...
 PROCplot("base",base%!xpos%,base%!ypos%)

 REM The approach...
 frame%=0:alt%=FALSE
 REPEAT
  inv_left%!xpos%+=inv_left%!dir%
  inv_left%!inv_ext%=inv_left%!xpos%+inv%!xsize%
  inv_right%!xpos%+=inv_right%!dir%
  frame%+=1
  IF frame% MOD 20=0 THEN alt%=FNtoggle(alt%)
  IF frame%>1000 THEN frame%=0
  IF alt% THEN img$="inv_11" ELSE img$="inv_1"
  PROCplot(img$,inv_left%!xpos%,inv_left%!ypos%)
  PROCplot(img$,inv_right%!xpos%,inv_right%!ypos%)
  PROCwait(2):WAIT
 UNTIL inv_left%!inv_ext%>=base%!xpos%

 REM The cheer...
 FOR i%=1 TO 10
  PROCplot("inv_1",inv_left%!xpos%,inv_left%!ypos%)
  PROCplot("inv_1",inv_right%!xpos%,inv_right%!ypos%)
  PROCwait(20)
  PROCplot("inv_11",inv_left%!xpos%,inv_left%!ypos%)
  PROCplot("inv_11",inv_right%!xpos%,inv_right%!ypos%)
  PROCwait(20):WAIT
 NEXT i%

 REM The end...
 FOR i%=1 TO 3
  PROCplot("base_"+STR$(i%),base%!xpos%,base%!ypos%)
  PROCwait(20):WAIT
 NEXT i%

 PROCwait(20):PROCwipe_sprite(base%)
 PROCash_free(inv_left%):PROCash_free(inv_right%)
ENDPROC

REM *************************************************************
REM Functions & Proceedures (Core Graphics Routines)
REM *************************************************************

REM Load a gfx file...
REM Doing it this way allows us to use two different buffers, one
REM for the sprites and the other for the charset.
DEFFNgfx_load(filespec$)
 LOCAL area%,size%
 size%=FNfs_filesize(filespec$)+256            :REM Find filesize.
 area%=FNash_claim(size%)                      :REM Reserve space.
 !area%=size%:area%!4=0:area%!8=16:area%!12=16 :REM Set pointers.

 REM Load Graphics into our reserved space...
 SYS gfx_spriteop%, 256+10, area%, filespec$   :REM Load file.
 SYS gfx_spriteop%, 256+17, area%              :REM Verify data.
=area%

REM Display a string character by character with a delay between
REM each one. Each character is indexed to the 8*8 character set
REM sprites using ASCII codes.
DEFPROCprint(x%,y%,string$,char_delay%)
 LOCAL i%,chsize%
 gfx_area%=gfx_fontpool%             :REM Switch to our charset.
 chsize%=screen%!xfont%
 FOR i%=1 TO LEN(string$)
  PROCplot(STR$(ASC(MID$(string$,i%,1))),x%+((i%-1)*chsize%),y%)
  IF char_delay%>0 THEN PROCwait(char_delay%)
 NEXT i%
 gfx_area%=gfx_spritepool%           :REM Switch back...
ENDPROC

REM Display a given image. Modified to use a local sprite area &
REM system calls to speed things up dramatically. (02/01/08)
REM sprite$ = Name of image to display.
REM x%      = X Co-ordinate.
REM y%      = Y Co-ordinate.
DEFPROCplot(sprite$,x%,y%)
 LOCAL address%
 SYS gfx_spriteop%, 256+24, gfx_area%, sprite$ TO ,, address%
 SYS gfx_spriteop%, 512+34, gfx_area%, address%, x%, y%, 0
ENDPROC

REM Clear a sprite from the screen. (New method.)
REM Before we created an identical bitmap in the sprite file
REM and displayed this over our image. This should be faster, and
REM result in a smaller sprite file.
DEFPROCwipe_sprite(obj%)
 GCOL0,0 TINT0
 RECTANGLE FILLobj%!xpos%,obj%!ypos%,obj%!xsize%,obj%!ysize%
ENDPROC

REM Centre text horizontally...
REM Where string_length% is the length of the string in multiples
REM of OS Units, where each multiple is the width of a character.
REM Var screen% is the screen width.
REM width...
DEFFNcentre_text(string_length%,screen%)
=(screen%/2)-(string_length%/2)

REM Centre text horizontally & vertically.
DEFPROCcentre_text_xy(string$,delay%)
 LOCAL str%,y%
 str%=LEN(string$)*screen%!xfont%
 y%=(screen%!ysize%/2)-(screen%!yfont%/2)
 PROCprint(FNcentre_text(str%,screen%!xsize%),y%,string$,delay%)
ENDPROC

REM Centre sprite horizontally...
REM Takes the two "object" blocks as parameters and extracts the
REM required values from them. Then returns the centre position.
DEFFNcentre_sprite(object%,relative%)
=(relative%!xsize%/2)-(object%!xsize%/2)

REM Centre sprite horizontally & vertically.
REM Returns the new position at offsets xpos% & ypos% in object%.
DEFFNcentre_sprite_xy(object%,relative%)
 LOCAL x%,y%
 object%!xpos%=(relative%!xsize%/2)-(object%!xsize%/2)
 object%!ypos%=(relative%!ysize%/2)-(object%!ysize%/2)
=object%

REM *************************************************************
REM Functions & Proceedures (Graphical Boxes & Lines.)
REM *************************************************************

REM Repeat a 6*6 image across the screen to produce a "rail" like
REM effect at position y%.
DEFPROCdraw_rail(ypos%)
 LOCAL i%,reps%
 reps%=screen%!xsize%/rail%!xsize%
 FOR i%=1 TO reps%+1
  PROCplot("rail",(i%-1)*rail%!xsize%,ypos%)
 NEXT i%
ENDPROC

REM Produce a box to surround text in GAME OVER!!! & GOODBYE!!!
REM texts. (Among others.)
DEFPROCdraw_box(obj%)
 LOCAL x%,y%,cxsize%,cysize%,rxsize%,rysize%,i%

 REM Setup...
 cxsize%=FNconv_units(3,1):cysize%=FNconv_units(3,2)
 rxsize%=1:rysize%=3

 REM The top & bottom...
 FOR i%=1 TO obj%!xsize%
  x%=obj%!xpos%+((i%-1)*rxsize%)
  PROCplot("box_x",x%,obj%!ypos%)
  PROCplot("box_x",x%,obj%!ypos%+(obj%!ysize%-(2*rysize%)))
 NEXT i%

 REM The two sides...
 x%=obj%!xpos%+(obj%!xsize%-cxsize%)
 FOR i%=1 TO obj%!ysize%-1
  PROCplot("box_y",obj%!xpos%,obj%!ypos%+((i%-1)*rxsize%))
  PROCplot("box_y",x%,obj%!ypos%+((i%-1)*rxsize%))
 NEXT i%

 REM The four corners...
 PROCplot("box_bl",obj%!xpos%,obj%!ypos%)
 PROCplot("box_tl",obj%!xpos%,(obj%!ypos%+obj%!ysize%)-cysize%)
 PROCplot("box_br",obj%!xpos%+(obj%!xsize%-cxsize%),obj%!ypos%)
 x%=(obj%!xpos%+obj%!xsize%)-cxsize%
 y%=(obj%!ypos%+obj%!ysize%)-cysize%
 PROCplot("box_tr",x%,y%)
ENDPROC

REM *************************************************************
REM Functions & Proceedures (String & Text Display)
REM *************************************************************

REM Unlike PROCprint() which displays a string in our graphic
REM typeface, these routines actually deal with our stored
REM strings. We store these seperately to take full advantage of
REM the way BasCrunch produces the final code.

REM # BasCrunch output is smaller.
REM # BasCrunch ouptut is harder to reverse engineer.

REM Get number of messages in a processed file...
DEFFNstring_enumprocessed(filespec$,magic$)
 LOCAL in_hdl%,count%,maggot$
 in_hdl%=OPENIN(filespec$)
  INPUT#in_hdl%,maggot$
  IF maggot$<>magic$ THEN CLOSE#in_hdl%:ERROR 255,"Bad Magic!"
  INPUT#in_hdl%,count%
 CLOSE#in_hdl%
=count%

REM Load a file off disc and return messages setup in arrays.
DEFPROCstring_load(filespec$,magic$,RETURN nums%(),RETURN strs$())
 LOCAL in_hdl%,i%,count%,maggot$
 in_hdl%=OPENIN(filespec$)
  INPUT#in_hdl%,maggot$
  IF maggot$<>magic$ THEN CLOSE#in_hdl%:ERROR 255,"Bad Magic!"
  INPUT#in_hdl%,count%
  FOR i%=1 TO count%
   INPUT#in_hdl%,nums%(i%),strs$(i%)
  NEXT i%
 CLOSE#in_hdl%
ENDPROC

REM Retrieve an encoded message for display...
DEFFNstring_get(msg%)
 LOCAL i%,found%:found%=0
 FOR i%=1 TO string_max%
  IF string_num%(i%)=msg% THEN found%=i%:i%=string_max%
 NEXT i%
 IF found%=0 THEN ERROR 255,FNstring_get(106)+" "+STR$(msg%)
=FNencode_string(string_msg$(found%))

REM *************************************************************
REM Functions & Proceedures (Misc. Game Specific.)
REM *************************************************************

REM Get various information about screen resolution. This includes
REM viewing area and conversion factors. We cannot rely on fixed
REM values because they change from mode to mode. So grab the
REM current values from the OS and bung 'em into some variables.
DEFPROCget_screeninfo
 LOCAL blk%
 blk%=FNash_claim(20)    :REM Reserve space for parameter block.

 REM Setup parameter block...
 blk%!0=4                             :REM XEigFactor (Convert.)
 blk%!4=5                             :REM YEigFactor (Convert.)
 blk%!8=11                            :REM XWindLimit (Width.)
 blk%!12=12                           :REM YWindLimit (Height.)
 blk%!16=-1                           :REM Termination byte.

 SYS "OS_ReadVduVariables", blk%, blk%:REM Call OS.

 REM Setup our screen% object...
 screen%!x_eig%=blk%!0                :REM X & Y EigFactor.
 screen%!y_eig%=blk%!4
 screen%!xsize%=(blk%!8)+1<<blk%!0    :REM Screen size.
 screen%!ysize%=(blk%!12)+1<<blk%!4
 screen%!setmode%=MODE
 PROCash_free(blk%)
ENDPROC

REM Convert between pixels & OS Units. Our screen object has been
REM setup already using PROCget_screeninfo. However, if the
REM current value of MODE is different to that in screen%!setmode%
REM then the screen mode has changed, so call PROCget_screeninfo
REM again.
REM size%   = Value to convert.
REM op%     = What to do. 1 = Width  (Pixels > OS Units)
REM                       2 = Height (Pixels > OS Units)
REM                       3 = Width  (OS Units > Pixels)
REM                       4 = Height (OS Units > Pixels)
REM Returns
REM conv%   = Converted value.
DEFFNconv_units(size%,op%)
 LOCAL conv%,mode%

 REM Check to see if the screen mode has changed. If so, then
 REM call PROCget_screeninfo to update the values. This is
 REM probably overkill and can be omitted, but still good practice
 REM for debugging.
 mode%=MODE
 IF mode%<>screen%!setmode% THEN PROCget_screeninfo

 REM Do the conversion.
 CASE op% OF
  WHEN 1 : conv%=size%<<screen%!x_eig%
  WHEN 2 : conv%=size%<<screen%!y_eig%
  WHEN 3 : conv%=size%>>screen%!x_eig%
  WHEN 4 : conv%=size%>>screen%!y_eig%
 OTHERWISE
  ERROR 255,FNstring_get(103)+" "+STR$(op%)
 ENDCASE
=conv%

REM Collision detection. Takes two objects and returns TRUE if
REM one is inside or has hit the other, else returns FALSE. Vars
REM tx_ext% & ty_ext% hold the two far edges of the sprite. We
REM calculate these by adding the target object's size to it's
REM position each time we're called. We then use these values to
REM perform a bounds check, one for x (ix% is TRUE if in.) and
REM one for y (iy% is TRUE if in.). Finally... We return an
REM evaluation of ix% AND iy%.
DEFFNhit(obj%,targ%)
 LOCAL tx_ext%,ty_ext%,ix%,iy%
 ix%=FALSE:iy%=FALSE
 IF obj%!active% AND targ%!active% THEN
  tx_ext%=targ%!xpos%+targ%!xsize%   :REM Right hand edge.
  ty_ext%=targ%!ypos%+targ%!ysize%   :REM Top edge.
  IF obj%!xpos%>=targ%!xpos% AND obj%!xpos%<tx_ext% THEN ix%=TRUE
  IF obj%!ypos%>=targ%!ypos% AND obj%!ypos%<ty_ext% THEN iy%=TRUE
 ENDIF
=ix% AND iy%

REM Bail gracefully on error.
DEFPROCtrap_error
 LOCAL m$
 ON ERROR OFF
 m$=" Panic!!!"
 CASE ERR OF
  WHEN  17  : m$="Program Terminated."
  WHEN 254  : m$="ASH"+m$
  WHEN 255  : m$="Game"+m$
  OTHERWISE : m$="System"+m$
 ENDCASE
 PRINTm$:PRINT STRING$(LEN(m$),"=")
 PRINT:REPORT:PRINT" at line : ";ERL
 OSCLI"PlayStop":PROCash_destroy
ENDPROC

REM Display file info as we go... (Initialisation.)
DEFFNfind(fspec$,msg$)
 LOCAL disp$,rc%,found%:found%=FALSE
 disp$=STRING$(10," "):RIGHT$(disp$,3)=" : "
 LEFT$(disp$,LEN(msg$))=msg$:rc%=FNfs_find(fspec$)
 IF rc%=1 THEN
  disp$+="OK.":found%=TRUE
 ELSE
  disp$+="Missing!!"
 ENDIF
 PRINTdisp$
=found%

REM *************************************************************
REM Functions & Proceedures (Generic & Portable Code)
REM *************************************************************

REM Returns TRUE n% percent of the time.
DEFFNpct(n%)
 LOCAL rnd%
 rnd%=RND(100)
=rnd%<=n%

REM Encode or decode a string using a simple XOR algo... This
REM prevents hackers from peeking at and modifying strings in
REM files. Strings are in reverse order in BASIC datafiles, but
REM are still in plaintext format.
REM string$ = String to mash up.
REM Returns
REM mashed$ = Mashed up string.
DEFFNencode_string(string$)
 LOCAL i%,mashed$
 FOR i%=1 TO LEN(string$)
  mashed$+=CHR$(ASC(MID$(string$,i%,1)) EOR 131)
 NEXT i%
=mashed$

REM Introduce a delay of n% hundredths of a second.
DEFPROCwait(n%)
 LOCAL t%
 t%=TIME+n%
 REPEAT:UNTIL TIME>=t%
ENDPROC

REM Toggle a flag between 0 & 1.
DEFFNtoggle(n%)
=n% EOR 1

REM Returns a suffix (st, nd, rd etc...) when given a number. This
REM number can be any number of digits long, we just deal with the
REM last digit.
DEFFNsuffix(n%)
 LOCAL out$,u%
 u%=VAL(RIGHT$(STR$(n%),1))
 out$=FNstring_get(65)
 IF u%>=1 AND u%<=3 THEN out$=FNstring_get(u%+61)
=out$

REM ************************************************************
REM Functions & Proceedures (LibSTR)
REM ************************************************************

REM I've imported the following two routines from my string utils
REM library. Normally I'd just include them using the LIBRARY
REM command, but that would kill the cruncher, so I've dumped them
REM here...

REM Pad a string to len% characters by adding multiple copies of
REM char$ to it. If start% is set TRUE then we will add padding to
REM the start of the string, else we add it at the end. If longer
REM than len% characters, then chop it to len% characters.
DEFFNstr_pad(string$,len%,char$,start%)
 LOCAL diff%,out$
 diff%=len%-LEN(string$)                  :REM How many to add.
 IF diff%<=0 THEN
  out$=LEFT$(string$,len%)
 ELSE
  IF start% THEN
   out$=STRING$(len%,char$):RIGHT$(out$,LEN(string$))=string$
  ELSE
   out$=string$+STRING$(diff%,char$)
  ENDIF
 ENDIF
=out$

REM Fix a string's length to len% characters and return it right
REM aligned...
DEFFNstr_rightalign(string$,len%)
 LOCAL out$
 out$=STRING$(len%," ")
 RIGHT$(out$,LEN(string$))=string$
=out$

REM ************************************************************
REM Functions & Proceedures (LibFS)
REM ************************************************************

REM I've imported the following two routines from my fs utils
REM library. Normally I'd just include them using the LIBRARY
REM command, but that would kill the cruncher, so I've dumped them
REM here...

REM Check for the prescence of a filing system object at a given
REM location. I say object, as this can check for the existance of
REM directories as well as files.
REM Parameter block...
REM file$  = Filespec to check for in current directory.
REM ...On Exit.
REM found% = 0 Object not found.
REM        = 1 Object is a file.
REM        = 2 Object is a directory.
DEFFNfs_find(file$)
 LOCAL found%
 SYS"XOS_File",17,file$ TO found%
=found%

REM Return the size of a file. (In bytes.)
DEFFNfs_filesize(file$)
 LOCAL size%
 SYS"XOS_File" ,5,file$ TO ,,,,size%  :REM Call OS to return size.
=size%

REM Can we write to this file? Return TRUE or FALSE.
REM rc%=0 -> File doesn't exist. Var file$ contains location.
REM rc%=1 -> File exists. Can we write/replace it?
DEFFNfs_writeable(op%,file$)
 LOCAL file_hdl%,flg%,flags%,result%
 result%=FALSE

 REM Attempt to open or create a file depending on our opcode.
 CASE op% OF
  WHEN 0    : file_hdl%=OPENUP(file$+"chkfile")
  WHEN 1    : file_hdl%=OPENUP(file$)
  OTHERWISE : ERROR 255,FNstring_get(104)+" "+STR$(op%)
 ENDCASE

 REM If we get this far and we still haven't got a valid file
 REM handle then it's fairly safe to say that we're never gonna
 REM get one, so skip the next bit and return FALSE.
 IF file_hdl%<>0 THEN
  SYS "XOS_Args",254,file_hdl% TO flags%;flg%
  IF (flg% AND 1) THEN
   CLOSE#file_hdl%
   ERROR 255,FNstring_get(105)
  ENDIF
  IF ((flags% AND (1<<7))<>0) THEN result%=TRUE
  CLOSE#file_hdl%
 ENDIF
=result%

REM ************************************************************
REM Functions & Proceedures (Ash Heap manager for BASIC)
REM                         Copyright (c)2004 7th Software.
REM ************************************************************

REM Initialise ASH Subsystem...
DEFPROCash_init
 LOCAL err%
 ash_heap%=0:ash_total%=0
 SYS "Wimp_SlotSize", -1, -1 TO ash_slot%
 SYS "XOS_ReadMemMapInfo" TO ash_page% ; err%
 IF err% AND 1 THEN ash_page%=16384
 PROCash_create(ash_page%)
ENDPROC

REM Release the memory used for our heap
DEFPROCash_destroy
 IF ash_heap% THEN
  SYS "Wimp_SlotSize", ash_heap%-&8000,-1 TO ash_slot%
  ash_heap%=0:ash_total%=0
 ENDIF
ENDPROC

REM Claim a block from the heap - generate an error if claim fails
REM Our block's size is now the requested size + 3 extra locations
REM @ 4 bytes per location + 3 extra bytes.

REM   Offs   | Significance.
REM  --------+---------------
REM      0   | Low guard-word.
REM      4   | Block user area size in bytes.
REM      8   | Start of user area.
REM  <bytes> | High guard-word.

DEFFNash_claim(bytes%)
 LOCAL block%,size%
 size%=(bytes%+15) AND &FFFFFFFC
 block%=FNash_alloc(size%)
 IF block% ELSE ERROR 254,FNstring_get(111)
 block%!0=&DEADDEAD                 :REM Store low guard-word...
 block%!4=bytes%                    :REM Store user area size...
 !(block%-8+(block%!-4))=&DEADDEAD  :REM Store high guard-word...
=block%+8

REM Free a block into the heap
DEFPROCash_free(RETURN block%)
 block%-=8
 IF block%!0<>&DEADDEAD THEN ERROR 254,FNstring_get(112)
 IF !(block%-8+(block%!-4))<>&DEADDEAD THEN
  ERROR 254,FNstring_get(113)
 ENDIF
 ash_total%-=block%!-4
 SYS "OS_Heap", 3, ash_heap%, block%
 block% = 0
ENDPROC

REM Return the total number of bytes currently allocated from
REM the heap.
DEFFNash_heapused
=ash_total%

REM Return free space on the heap.
DEFFNash_heapfree
 LOCAL free%
 free%=(ash_slot%+&8000-ash_heap%)-ash_total%
 IF free%<0 THEN ERROR 254,FNstring_get(114)
=free%

REM Return total size of the heap.
DEFFNash_heapsize
=ash_slot%+&8000-ash_heap%


REM Return the size (in bytes) of a specified block.
DEFFNash_blocksize(block%)
 LOCAL blk%
 blk%=block%-4
=blk%!0

REM Fill a block of memory with a specified value...
DEFFNash_blockfill(block%,value%)
 LOCAL offset%,size%
 size%=FNash_blocksize(block%)
 FOR offset%=0 TO size%-4 STEP 4
  block%!offset%=value%
 NEXT offset%
=block%

REM Create a block with identical contents to the original.
DEFFNash_blockcopy(block%)
 LOCAL copy%,offset%,size%
 size%=FNash_blocksize(block%)
 copy%=FNash_claim(size%)
 FOR offset%=0 TO size%-4 STEP 4
  copy%!offset%=block%!offset%
 NEXT offset%
=copy%

REM *** LibASH Internals ***

REM Create a heap in the memory above HIMEM
DEFPROCash_create(size%)
 IF ash_heap% THEN ERROR 254,FNstring_get(115)
 size%=(size%+ash_page%-1) AND NOT (ash_page%-1)
 ash_heap%=HIMEM
 SYS "Wimp_SlotSize", ash_slot% + size%, -1 TO ash_slot%
 size%=ash_slot%+&8000-ash_heap%
 IF size%<1 THEN
  ash_heap%=0
  ERROR 254,FNstring_get(116)
 ELSE
  SYS "OS_Heap", 0, ash_heap%,, size%
 ENDIF
 ash_total%=0
ENDPROC

REM Attempt to add some more memory into the end of our heap
DEFPROCash_grow(bytes%)
 LOCAL prev%
 prev%=ash_slot%
 SYS "Wimp_SlotSize", ash_slot%+bytes%, -1 TO ash_slot%
 bytes%=ash_slot%-prev%
 IF bytes% THEN SYS "OS_Heap", 5, ash_heap%,, bytes% TO ,,, bytes%
ENDPROC

REM Claim a block from the heap (grow it if need be) - return
REM zero if claim fails.
DEFFNash_alloc(bytes%)
 LOCAL block%, err%
 SYS "XOS_Heap", 2, ash_heap%,, bytes% TO ,, block% ; err%
 IF err% AND 1 THEN
  PROCash_grow((bytes%+ash_page%) AND NOT (ash_page%-1))
  SYS "XOS_Heap", 2, ash_heap%,, bytes% TO ,, block% ; err%
  IF err% AND 1 THEN =0
 ENDIF
 ash_total% += block%!-4
=block%