REM ><Invaders$Dir>.Src.Invaders
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 E     |               | End game Sequence. (Test.)
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 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>."
IF FNfs_find(app_dir$+"Resources.Gfx0")<>1 THEN bail%=TRUE
IF FNfs_find(app_dir$+"Resources.Gfx1")<>1 THEN bail%=TRUE
IF bail% THEN ERROR 255,"File Missing!!"

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

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%
gfx_spritepool%=FNgfx_load(app_dir$+"Resources.Gfx0")
gfx_fontpool%=FNgfx_load(app_dir$+"Resources.Gfx1")
gfx_area%=gfx_spritepool%

REM HiScore Table...
REM Here, we look for and load, if it exists, our Hiscores file.
REM If we can't find one, 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. We perform the final call to make
REM sure we can write back. (File could be present, but on write
REM protected floppy.)
IF FNfs_find(app_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 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...
DIM screen%36,rail%24,box%16           :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 800*600 (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_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 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("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 ypos value. Our saucer% object here, is only large enough to
REM hold screen co-ordinates. It is made LOCAL by this function so
REM we can 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%,x%,y%,rc%,s$,string$

 DIM space%16,invt%16,hwind%16      :REM Title sequence objects.
 DIM saucer%16,inv1%16,keyz%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

 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%=FNcopy_object(keyz%,16)      :REM Create three copies
 keye%=FNcopy_object(keyz%,16)      :REM for the other keys.
 keyq%=FNcopy_object(keyz%,16)

                                    :REM Unique "keys" props.
 keyz%!ypos%=(hwind%!ypos%+hwind%!ysize%)-160
 keyx%!ypos%=keyz%!ypos%-60:keye%!ypos%=keyx%!ypos%-60
 keyq%!ypos%=keye%!ypos%-100

 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%)
  PROCwait(1):WAIT
 NEXT i%

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

 string$="PRESS SPACE TO PLAY"
 str%=LEN(string$)*screen%!xfont%:y%=170
 PROCprint(FNcentre_text(str%,screen%!xsize%),y%,string$,11)
 string$="OR Q TO QUIT"
 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)
  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%+12,  "= 100 POINTS !",0)
  PROCplot("inv_2",inv2%!xpos%,inv2%!ypos%)
  PROCprint(x%,inv2%!ypos%+12,  "=  75 POINTS !",0)
  PROCplot("inv_1",inv1%!xpos%,inv1%!ypos%)
  PROCprint(x%,inv1%!ypos%+12,  "=  50 POINTS !",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

  REM Display Keyboard controls screen...
  PROCwipe_sprite(hwind%)

  string$="KEYBOARD CONTROLS"
  str%=LEN(string$)*screen%!xfont%
  x%=hwind%!xpos%+FNcentre_text(str%,hwind%!xsize%)
  y%=hwind%!ypos%+hwind%!ysize%-20
  PROCprint(x%,y%,string$,0)

  x%=keyz%!xpos%+keyz%!xsize%+10
  PROCplot("key_z",keyz%!xpos%,keyz%!ypos%)
  PROCprint(x%,keyz%!ypos%+12,": MOVE LEFT",0)
  PROCplot("key_x",keyx%!xpos%,keyx%!ypos%)
  PROCprint(x%,keyx%!ypos%+12,": MOVE RIGHT",0)
  PROCplot("key_enter",keye%!xpos%,keye%!ypos%)
  PROCprint(x%,keye%!ypos%+12,": FIRE SHELL",0)
  PROCplot("key_q",keyq%!xpos%,keyq%!ypos%)
  PROCprint(x%,keyq%!ypos%,": END GAME",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

  REM Display HiScore table...
  PROCwipe_sprite(hwind%)

  REM First line... (This is conditional on scores_save%)
  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."+"  PLAYER."+STRING$(12," ")+"SCORE."
  string$+=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...
  rc%=FNtitle_wait
  IF rc%=1 THEN =TRUE
  IF rc%=2 THEN =FALSE

 ENDWHILE
 IF INKEY(-17) THEN =TRUE           :REM Get outta here (Q)...
=FALSE

REM Wait for 750 counts or SPACE or Q to be pressed...
REM This has been moved here due to frequency of calling in it's
REM current state it returns...
REM 0 -> Timeout period expired... (750 counts up.)
REM 1 -> "Q" to Quit...
REM 2 -> SPACE to Play...
DEFFNtitle_wait
 LOCAL t%
 t%=TIME+750
 REPEAT
  IF INKEY(-82) THEN PROCevnt_kb_screendump
 UNTIL TIME=t% OR INKEY(-17) OR INKEY(-99)
 IF INKEY(-17) THEN =1          :REM Get outta here (Q)...
 IF INKEY(-99) THEN =2          :REM Play a game...
=0

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%()                         :REM Invader status.
 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%16
 lives%=0:score%=4:racknum%=8:suicide%=12 :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%

 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%!flags4%=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

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

 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%=FALSE
  IF INKEY(-74) THEN PROCevnt_kb_basefire(base%!xpos%,base%!ypos%)
  IF INKEY(-98) THEN PROCevnt_kb_baseleft:evnt%=TRUE
  IF INKEY(-67) THEN PROCevnt_kb_baseright:evnt%=TRUE
  IF INKEY(-82) THEN PROCevnt_kb_screendump     :REM Screenshot...
  IF INKEY(-17) THEN PROCevnt_kb_suicide        :REM Suicide...
  IF INKEY(-35) THEN PROCrack_endgame           :REM End Seq...

  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.)
  REM The only two that require action here are left & right as
  REM we need to display the base at the new position.
  IF evnt% THEN
    base%!xpos%+=base%!dir%
    PROCplot("base",base%!xpos%,base%!ypos%)
  ENDIF

  REM Process evnt_fr events. (Frame Refresh.)
  IF player%!lives%>0 THEN
   PROCevnt_fr_basefire         :REM Animate player's shots.
   PROCevnt_fr_saucer           :REM Saucer handling & AI.
   PROCevnt_fr_moverack         :REM Invader handling & AI.
  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
 PROCdraw_box(box%)
 PROCcentre_text_xy("GAME OVER!!!",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
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_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 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
 player%!lives%=0:player%!suicide%=TRUE
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%,toggle%
 hit%=flags1%:timer%=flags2%:spr%=flags3%  :REM Object tokens.
 toggle%=flags4%

 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%>=100 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.
   IF saucer%!toggle%=0 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%+125 THEN
    PROCwipe_sprite(saucer%)
    saucer%!active%=FALSE:saucer%!hit%=FALSE
    saucer%!timer%=0:saucer%!toggle%=TRUE
   ELSE
    IF saucer%!timer%>=saucer%!hit%+100 THEN
     PROCplot("saucer_5",saucer%!xpos%,saucer%!ypos%)
    ELSE
     IF saucer%!timer%>=saucer%!hit%+75 THEN
      PROCplot("saucer_4",saucer%!xpos%,saucer%!ypos%)
     ELSE
      IF saucer%!timer%>=saucer%!hit%+50 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. Invader bomb AI and
REM rack trimming not implemented yet. (26/02/08)
DEFPROCevnt_fr_moverack
 LOCAL timer%,rack_ext%,z%,col%,row%

 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 rack%!frame%=FNtoggle(rack%!frame%)
 IF rack%!timer%>10000 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%
 IF rack_ext%>=screen%!xsize% THEN
  rack%!dir%=0-rack%!speed%:rack%!ypos%-=5
 ENDIF
 IF rack%!xpos%<=0 THEN
  rack%!dir%=0+rack%!speed%:rack%!ypos%-=5
 ENDIF

 REM Have we reached the base?
 IF rack%!ypos%=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.
  PROChud_rack(player%!racknum%)      :REM Update HUD.
  PROCwipe_sprite(rack%)              :REM Clear the last invader.
  PROCrack_init                       :REM Create new rack.
 ENDIF

 REM Finally... Redraw the rack...
 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 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 i%,pos%,racknum%,score%,space%,invt%,str%,x%,y%,g%,done%
 LOCAL box%,string$
 DIM space%16,invt%16,box%16        :REM Space for objects.
 score%=4:racknum%=8                :REM Offsets in player%

 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$="Congratulations!!! You have earned the rank of "
 string$+=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 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$="Anon ..."
 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$="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  -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%

 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%
 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
   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
   ENDIF
  NEXT col%
 NEXT row%
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%
 DIM inv_obj%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
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 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
 DIM inv_left%28,inv_right%28              :REM Reserve space...

 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

 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 HUD...
 player%!lives%=0:PROChud_lives(player%!lives%)

 REM Hit it!!
 REM Two invaders, one from each side, approach the base and blow
 REM blow 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 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%)
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.
 DIM area% 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 (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%
 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 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 *************************************************************
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))
 CASE u% OF
  WHEN 1    : out$="st"
  WHEN 2    : out$="nd"
  WHEN 3    : out$="rd"

  OTHERWISE : out$="th"
 ENDCASE
=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%