REM ><Invaders$Dir>.!RunImage
REM Taito Space Invaders (Circa 1978) Clone

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

REM Q     |  Quit Program | Suicide.
REM Z     |               | Left.
REM X     |               | Right.
REM Enter |               | Fire.

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

REM Setup error trapping.
ON ERROR REPORT:PRINT" at line ";ERL:END

REM Setup our variables.
bail%=FALSE                            :REM TRUE to quit game.
scores_save%=TRUE                      :REM TRUE to save HiScores.
dump_num%=0                            :REM Screendump counter.
DIM scores_name$(10),scores_value%(10) :REM HiScore table.
DIM scores_rack%(10)

REM Check we have all our files. File HiTable is a special case,
REM as we can carry on without it. If it doesn't exist then we try
REM to create it with FNcreate_hitable. If that returns FALSE
REM (We can't create the file for some reason.) then we change the
REM value of a global variable which we check before we try to
REM save our Hiscore Table. In the event that we can't create the
REM file, scores will be lost when we exit the game.

REM Locate files... (Mission critical...)
file_err%=0
IF FNfile("<Invaders$Dir>.Resources.Gfx0")<>1 THEN file_err%+=1
IF FNfile("<Invaders$Dir>.Resources.Gfx1")<>1 THEN file_err%+=1
IF file_err% THEN ERROR 255,"File Missing!!"

REM Load files...   (Mission critical...)
OSCLI"SLoad  <Invaders$Dir>.Resources.Gfx0"    :REM Sprites.
OSCLI"SMerge <Invaders$Dir>.Resources.Gfx1"    :REM Charset.

REM HiScore Table...
IF FNfile("<Invaders$Dir>.Resources.HiTable")<>1 THEN
 IF NOT FNhitable_create THEN scores_save%=FALSE
ELSE
 PROChitable_load
ENDIF

REM *************************************************************
REM Initialisation Part 2... (Object offsets.)
REM *************************************************************

REM Objects... Each object (Base, saucer, missile etc.) is
REM given a block of memory 52 bytes long to hold all relevant
REM data for that object. This, apart from being quicker, is far
REM easier to code. These are tokens to address offsets in each
REM of those blocks of memory. (See tutorial for how this
REM works...) Contents for offsets 32-48 (flags1% - flags5%) will
REM be different, depending on the nature of the object. I'll
REM define LOCAL variables with more meaningful names and point
REM them to 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                 :REM Object Status.
flags1%=32:flags2%=36:flags3%=40       :REM Misc flags.
flags4%=44:flags5%=48

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

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

REM Reserve memory for global object blocks...
DIM screen%36,rail%52                  :REM Global objects.

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.
MODE28:OFF                             :REM 1280*1024 (256 cols.)
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 *************************************************************

REPEAT
 CLS:bail%=FNtitle
 IF NOT bail% THEN CLS:PROCmain
UNTIL bail%

CLS:PROCcentre_text_xy("GOODBYE!!!",10):END


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

REM Animate in our initial screen layout and then perform a cycle
REM between Score Advance & HiScore tables. Do a keyboard scan
REM periodically and check for -17 (Q) and -99 (Space Bar) and
REM return from function accordingly... Object hwind% points to
REM the area of the screen where either the HiScore or Score
REM Advance table gets placed in attract mode. This is the area
REM below "Invaders" and above "PRESS SPACE TO PLAY" text. Objects
REM inv1% - inv3% are identical copies of each other, except for
REM xpos & ypos values.

DEFFNtitle
 LOCAL i%,space%,invt%,hwind%,str%,string$,x%,saucer%,inv1%,inv2%
 LOCAL inv3%,t%,y%,s$

 DIM space%16,invt%16,hwind%16      :REM Title sequence objects.
 DIM saucer%16,inv1%16

 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%=630: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%=FNcopy_object(inv1%,16)      :REM Create two copies.
 inv3%=FNcopy_object(inv1%,16)

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

 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
  PROCplot("space",i%,space%!ypos%)
 NEXT i%

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

 string$="PRESS SPACE TO PLAY"
 str%=LEN(string$)*screen%!xfont%:y%=170
 PROCprint(FNcentre_text(str%,screen%!xsize%),y%,string$,10)
 string$="OR Q TO QUIT"
 str%=LEN(string$)*screen%!xfont%:y%-=screen%!yfont%
 PROCprint(FNcentre_text(str%,screen%!xsize%),y%,string$,10)

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

  REM Display Score Advance table...
  string$="SCORE ADVANCE"
  str%=LEN(string$)*screen%!xfont%
  x%=hwind%!xpos%+FNcentre_text(str%,hwind%!xsize%)
  y%=hwind%!ypos%+hwind%!ysize%-20
  PROCprint(x%,y%,string$,0)

  string$="TABLE":y%-=screen%!yfont%
  str%=LEN(string$)*screen%!xfont%
  x%=hwind%!xpos%+FNcentre_text(str%,hwind%!xsize%)
  PROCprint(x%,y%,string$,0)

  x%=saucer%!xpos%+saucer%!xsize%+10
  PROCplot("saucer_0",saucer%!xpos%,saucer%!ypos%)
  PROCprint(x%,saucer%!ypos%+10,"= 500 POINTS !",0)
  PROCplot("inv_3",inv3%!xpos%,inv3%!ypos%)
  PROCprint(x%,inv3%!ypos%+20,  "= 100 POINTS !",0)
  PROCplot("inv_2",inv2%!xpos%,inv2%!ypos%)
  PROCprint(x%,inv2%!ypos%+20,  "=  75 POINTS !",0)
  PROCplot("inv_1",inv1%!xpos%,inv1%!ypos%)
  PROCprint(x%,inv1%!ypos%+20,  "=  50 POINTS !",0)

  REM Wait for 750 counts or until SPACE or Q pressed...
  t%=TIME+750
  REPEAT:UNTIL TIME=t% OR INKEY(-17) OR INKEY(-99)
  IF INKEY(-17) THEN =TRUE          :REM Get outta here (Q)...
  IF INKEY(-99) THEN =FALSE         :REM Play a game...
  IF INKEY(-82) THEN PROCevnt_kb_screendump

  REM Display HiScore table...
  REM First line... (This is conditional on scores_save%)
  PROCwipe_sprite(hwind%)
  string$="TODAY'S"
  IF scores_save% THEN string$="ALL TIME"
  str%=LEN(string$)*screen%!xfont%
  x%=hwind%!xpos%+FNcentre_text(str%,hwind%!xsize%)
  y%=hwind%!ypos%+hwind%!ysize%-20
  PROCprint(x%,y%,string$,0)

  REM Second line...
  string$="HI SCORES":y%-=screen%!yfont%
  str%=LEN(string$)*screen%!xfont%
  x%=hwind%!xpos%+FNcentre_text(str%,hwind%!xsize%)
  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."+STRING$(21," ")+"SCORE."+STRING$(3," ")+"RACK."
  str%=LEN(string$)*screen%!xfont%
  x%=hwind%!xpos%+FNcentre_text(str%,hwind%!xsize%)
  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)
   str%=LEN(string$)*screen%!xfont%
   x%=hwind%!xpos%+FNcentre_text(str%,hwind%!xsize%)
   PROCprint(x%,y%-(i%*(screen%!yfont%+5)),string$,0)
  NEXT i%

  REM Wait for 750 counts or until SPACE or Q pressed...
  t%=TIME+750
  REPEAT:UNTIL TIME=t% OR INKEY(-17) OR INKEY(-99)
  IF INKEY(-17) THEN =TRUE          :REM Get outta here (Q)...
  IF INKEY(-99) THEN =FALSE         :REM Play a game...
  IF INKEY(-82) THEN PROCevnt_kb_screendump
 ENDWHILE
 IF INKEY(-17) THEN =TRUE           :REM Get outta here (Q)...
=FALSE

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 lives%,racknum%,frame%,colgap%     :REM Properties.
 LOCAL evnt%,str%,string$,x%,y%           :REM Misc.

 REM Reserve memory for object blocks & setup pointer tokens.
 REM (Local objects.)
 DIM base%52,shell%52,saucer%52,rack%52,inv%16,invstat%(10,6)
 DIM player%12
 lives%=0:score%=4:racknum%=8             :REM Player tokens.
 frame%=flags1%:colgap%=flags2%           :REM Rack tokens.
 timer%=flags3%:xnum%=flags4%:ynum%=flags5%


 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%

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

 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

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

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

 player%!racknum%=1:player%!score%=0      :REM Player properties.
 player%!lives%=3

 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_init:PROCrack_redraw              :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...
  evnt%=0
  IF INKEY(-74) THEN PROCevnt_kb_basefire(base%!xpos%,base%!ypos%)
  IF INKEY(-98) THEN PROCevnt_kb_baseleft:evnt%=1
  IF INKEY(-67) THEN PROCevnt_kb_baseright:evnt%=2
  IF INKEY(-82) THEN PROCevnt_kb_screendump
  IF INKEY(-17) THEN player%!lives%=0

  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%=0). Either way
  REM something's going on, usually AI/Collision detection
  REM related.

  REM Process evnt_kb event updates. (Keyboard.)
  IF evnt%>0 THEN

   REM Update base position. (Only left & right.)
   IF evnt%<=2 THEN
    base%!xpos%+=base%!dir%
    PROCplot("base",base%!xpos%,base%!ypos%)
   ENDIF

  ENDIF

  REM Process evnt_fr events. (Frame Refresh.)
  PROCevnt_fr_basefire
  PROCevnt_fr_saucer
  PROCevnt_fr_moverack
 UNTIL player%!lives%=0

 PROCcentre_text_xy("GAME OVER!!!",10):PROCwait(750)
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:ENDPROC

 base%!dir%=0-base%!speed%                 :REM Change direction.
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%:ENDPROC
 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_fired% is a global variable that is set
REM boolean TRUE when player fires a shell and is cleared when
REM either the missile scrolls off the top of the screen or
REM hits an invader.
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 *************************************************************
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%=FNcopy_object(shell%,52)            :REM Clear shell.
   sh_cpy%!ypos%-=2:PROCwipe_sprite(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%,timer%,spr%
 hit%=flags1%:timer%=flags2%:spr%=flags3%  :REM Object tokens.

 IF NOT saucer%!active% THEN
  IF FNpct(3) AND FNpct(1) THEN
   saucer%!active%=TRUE:saucer%!beenhit%=FALSE:saucer%!spr%=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%>=100 THEN saucer%!spr%=0
   IF saucer%!spr%<50 THEN
    PROCplot("saucer_0",saucer%!xpos%,saucer%!ypos%)
   ELSE
    PROCplot("saucer_1",saucer%!xpos%,saucer%!ypos%)
   ENDIF
   saucer%!xpos%+=saucer%!dir%
  ELSE

   REM Animate/Remove explosion...
   IF saucer%!timer%>=saucer%!hit%+200 THEN
    PROCwipe_sprite(saucer%)
    saucer%!active%=FALSE:saucer%!hit%=FALSE
    saucer%!timer%=0
   ELSE
    IF saucer%!timer%>=saucer%!hit%+175 THEN
     PROCplot("saucer_5",saucer%!xpos%,saucer%!ypos%)
    ELSE
     IF saucer%!timer%>=saucer%!hit%+150 THEN
      PROCplot("saucer_4",saucer%!xpos%,saucer%!ypos%)
     ELSE
      IF saucer%!timer%>=saucer%!hit%+100 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.
REM Deal with AI regarding invaders bombs.
DEFPROCevnt_fr_moverack
 LOCAL timer%,rack_ext%

 timer%=flags3%                               :REM Properties.

 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 10,000 calls or so should prevent any over-runs.
 rack%!timer%+=1
 IF rack%!timer% MOD 5=0 THEN PROCrack_toggleframe
 IF rack%!timer%>10000 THEN rack%!timer%=0

 REM Deal with left & right movement only for the time being. I'll
 REM add down to this mix later...
 rack_ext%=rack%!xpos%+rack%!xsize%
 IF rack_ext%>=screen%!xsize% THEN rack%!dir%=0-rack%!speed%
 IF rack%!xpos%<=0 THEN rack%!dir%=0+rack%!speed%
 rack%!xpos%+=rack%!dir%:PROCrack_redraw
ENDPROC

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

REM Create a clean HiScore table. Return TRUE if we can, else
REM return FALSE. Either way, we fill our two arrays with default
REM values.
DEFFNhitable_create
 LOCAL i%,file_hdl%,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%
 file_hdl%=OPENOUT"<Invaders$Dir>.Resources.HiTable"
  IF file_hdl%=0 THEN =FALSE
  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%
=TRUE

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 *************************************************************
REM Functions & Proceedures (HUD & Scoring)
REM *************************************************************

REM Initialise the HUD display...
DEFPROChud_init(sc%,hi%,l%,r%)
 LOCAL x%,string$
 string$="SCORE :-"+FNstr_pad(STR$(sc%),10," ",TRUE)
 PROCprint(0,screen%!ysize%-screen%!yfont%,string$,0)
 string$="HI SCORE :-"
 string$+=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$="RACK :-"+FNstr_pad(STR$(num%),3,"0",TRUE)
 x%=screen%!xsize%-LEN(string$)*screen%!xfont%
 PROCprint(x%,0,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$="SCORE :-"+FNstr_pad(STR$(player%!score%),10," ",TRUE)
 PROCprint(0,screen%!ysize%-screen%!yfont%,string$,0)

 IF player%!score%>scores_value%(1) THEN
  string$="HI SCORE :-"
  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 Initialise and draw 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  -1         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%

 REM Use s% as a temporary to avoid problems with line length in
 REM this 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%
 FOR row%=1 TO rack%!xnum%
  x%=rack%!xpos%+((row%-1)*(inv%!xsize%+rack%!colgap%))
  FOR col%=1 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
   PROCplot("inv_"+STR$(invstat%(row%,col%)),x%,y%)
  NEXT col%
 NEXT row%
ENDPROC

REM Toggle value of frame counter. This is used to display the two
REM alternating frames for each invader.
DEFPROCrack_toggleframe
 IF rack%!frame%=0 THEN
  rack%!frame%=1
 ELSE
  rack%!frame%=0
 ENDIF
ENDPROC

REM Reduce the size of the rack when an end column on either left
REM or right hand side has been destroyed.
DEFPROCrack_shrinkcolumn
 REM Placeholder...
ENDPROC

REM Reduce the size of the rack when the bottom row of invaders
REM has been destroyed.
DEFPROCrack_shrinkrow
 REM Placeholder...
ENDPROC

REM *************************************************************
REM Misc. Functions & Proceedures (Game Specific)
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 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%
 DIM blk% 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
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,"Unknown FNconv_units() operation : "+STR$(op%)
 ENDCASE
=conv%

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%
 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%
ENDPROC

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 Create a copy of an object and return it's address. Modified
REM 02/12/07 to handle objects of multiple sizes more gracefully.
REM obj%   = Original object block's address.
REM size%  = Size of original object block.
DEFFNcopy_object(obj%,size%)
 LOCAL offset%,newobj%
 DIM newobj% size%
 FOR offset%=0 TO size%-4 STEP 4
  newobj%!offset%=obj%!offset%
 NEXT offset%
=newobj%

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 *************************************************************
REM Functions & Proceedures (Generic & Portable Code)
REM *************************************************************

REM Display a given image.
REM sprite$ = Name of image to display.
REM x%      = X Co-ordinate.
REM y%      = Y Co-ordinate.
DEFPROCplot(sprite$,x%,y%)
 OSCLI"SChoose "+sprite$
 PLOT &ED,x%,y%
ENDPROC

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

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 f$    = Filespec to check for in current directory.
REM ...On Exit.
REM f%    = 0 Object not found.
REM       = 1 Object is a file.
REM       = 2 Object is a directory.
DEFFNfile(f$)
 LOCAL f%
 SYS"XOS_File",17,f$ TO f%
=f%

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 ************************************************************
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$