! -
! MOVECLASS, a library file to provide random, directed and 'intelligent'
!            movement for NPCs
!
! Version 8.10, written by Neil Brown          [email protected]
!                      and Alan Trewartha      [email protected]
!               with Glulx additions by Matthew T. Russotto 13 March 2001
!
! Last altered 5th April 2001.
!
! The functions of this library are too complex to go into here, so please
! refer to the brief manual which should be near to where you found this
! file, and is named 'moveman.txt'.
!
! If you are including the library file FOLLOWER.H in your game code, please
! include this file AFTERWARDS and not before, otherwise errors will occur.
! -

System_file;
! This is necessary to compile with Graham's current Inform 6.21 compiler.
 #ifndef WORDSIZE;
 Constant TARGET_ZCODE;
 Constant WORDSIZE 2;
 #endif;

                                    ! NPC PROPERTIES
Property before_action;              ! Run before moving.
Property after_action;               ! Run after a successful move.
Property caprice alias time_left;    ! %age chance of moving when random

Property npc_open;                   ! A property of doors

Global path_size_limit = 10;         ! Depth of path searching

Constant   RANDOM_MOVE = 0;          ! The different possible move_types
Constant    AIMED_MOVE = 1;
Constant       NO_MOVE = 2;
Constant   PRESET_MOVE = 3;

Constant      ANY_PATH = $$00000000; ! The different types of AIMED_MOVEs.
Constant UNLOCKED_PATH = $$00001000; ! Bitmaps so that they can be combined
Constant     OPEN_PATH = $$00010000; ! in principle
Constant DOORLESS_PATH = $$00100000;

Ifndef Room;
 Class Room with link_data 0 0 0;
EndIf;

#ifdef TARGET_ZCODE;
Class MTR_npcdirclass
 with npc_dirs 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0;
                       ! The calculated directions that the NPC takes.
                       ! Note this is a word array, but the dirs are held as
                       ! single bytes, so a path of 64 moves is possible.
#ifnot; !TARGET_GLULX
Class MTR_npcdirclass
 with npc_dirs 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0;
                       ! The assumption that a direction will be a small
                       ! (byte-sized) integer is bogus in Glulx
                       ! However, Glulx allows big properties, so we don't
                       ! lose.  --MTR
#endif;



Class moveclass
class MTR_npcdirclass
 with move_type
          RANDOM_MOVE, ! The default move_type is to move randomly
      caprice      20, ! Chance the NPC will move each turn when RANDOM_MOVE
      prepath_name  0, ! The name of the predetermined path array
      prepath_size  0, ! The length of the set path
      npc_stage     0, ! Position along set path array
      npc_target,      ! The target destination
      npc_blocked [; NPC_Path(self,RANDOM_MOVE); ],
                       ! Alternatively do nothing and wait for the path to
                       ! unblock. Or, more intelligently look for a less
                       ! restrictive path.
      npc_ifblocked 0, ! Free for use by npc_blocked
      npc_arrived [; NPC_Path(self,RANDOM_MOVE); ],
                       ! Redefine this within the actual NPC object
                       ! for more sophisticated results. Deals with what
                       ! happens when an NPC arrives at its destination. In
                       ! this case, it returns to random movement.
      walkoff "walks off",
      walkon  "arrives",
      follow_action,   ! } In case Follower has been included but the NPC
      follow_object,   ! } isn't of FollowClass.
      daemon [ i n k;
          if (RunRoutines(self,before_action)) rtrue;

          switch(self.move_type)
          { 0, RANDOM_MOVE:                          ! Random movement
#ifdef DEBUG;
if (parser_trace>1)
print "[RANDOM_MOVE daemon for ", (the) self ,"]^";
#endif;
             if (random(100)>self.caprice) rfalse;
             objectloop (i in compass)
               if (LeadsTo(i,parent(self),ANY_PATH))
               {  n++;
#ifdef DEBUG;
if (parser_trace>1)
print "[Choice ",n, ": ",(GiveDir) i ,"]^";
#endif;
               }
             if (n==0) rfalse;
             k=random(n); n=0;                       ! Choose one direction
#ifdef DEBUG;
if (parser_trace>1)
print "[Choosing ",k, "]^";
#endif;
             objectloop (i in compass)
             { if (LeadsTo(i,parent(self),ANY_PATH)) n++;
               if (n==k)
               {  MoveNPCDir(self,i);
                  break;
               }
             }

            1, AIMED_MOVE :                   ! Moving on a calculated path
#ifdef TARGET_ZCODE;
             i=self.&npc_dirs->self.npc_stage;
#ifnot; ! TARGET_GLULX
             i=self.&npc_dirs-->self.npc_stage;
#endif; ! TARGET

#ifdef DEBUG;
if (parser_trace>1)
print "[AIMED_MOVE daemon moving ", (the) self, " ", (GiveDir) i,"]^";
#endif;
             if (i==0  || MoveNPCDir(self,i)) ! Routine only called if i~=0
                 self.npc_stage++;
             if (parent(self)==self.npc_target)
                 self.npc_arrived();

            2, NO_MOVE :                                ! Not moving at all
#ifdef DEBUG;
if (parser_trace>1)
print "[NO_MOVE daemon for ", (the) self, " doing nothing]^";
#endif;

            3, PRESET_MOVE :               ! Moving on a predetermined path
#ifdef TARGET_ZCODE;
             i=(self.prepath_name)->self.npc_stage;
#ifnot; ! TARGET_GLULX
             i=(self.prepath_name)-->self.npc_stage;
#endif; ! TARGET

#ifdef DEBUG;
if (parser_trace>1)
print "[PRESET_MOVE daemon moving ", (the) self," ", (GiveDir) i, "]^";
#endif;
             if (i==0 ||MoveNPCDir(self,i))   ! Routine only called if i~=0
                 self.npc_stage++;
             if (self.npc_stage>=self.prepath_size)
                 self.npc_arrived();

      default: "** MoveClass Error: move_type set to an unacceptable
                                    value for ", (the) self, " **";
         }
       ];



[ NPC_path npc   movement_type targetroom path_type
          steps last_room     found      i j k;

 if (metaclass(movement_type)==Object && movement_type ofclass Room)
 {   path_type=targetroom;
     targetroom=movement_type;
     movement_type=AIMED_MOVE;            ! To stay compatible with old code
 }
#ifdef DEBUG;
if (parser_trace>1)
{print "^[NPC_Path setting ", (the) self," to ";
switch (movement_type)
{     NO_MOVE: print "NO_MOVE";
  RANDOM_MOVE: print "RANDOM_MOVE";
  PRESET_MOVE: print "PRESET_MOVE";
   AIMED_MOVE: print "AIMED_MOVE towards ", (name) targetroom;
      default: print "**UNDEFINED**";
}
print "]^";
}
#endif;

 if (~~(npc ofclass moveclass))
 { print "*** MoveClass Error: NPC_path called for non-moveclass object '",
          (the) npc, "' ***";
   rfalse;
 }

 if (movement_type==NO_MOVE)              ! Call to set NO_MOVE
 {  npc.move_type=NO_MOVE;
    rtrue;
 }

 if (movement_type==RANDOM_MOVE)          ! Call to set RANDOM_MOVE
 {  npc.move_type=RANDOM_MOVE;
    if (path_type~=0)
        npc.caprice=path_type;
    rtrue;
 }

 if (movement_type==PRESET_MOVE)          ! Call to set PRESET_MOVE
    return NPCprepath(npc,targetroom,path_type);

 if (movement_type~=AIMED_MOVE)
    rfalse;

! VZEFH check
 if (parent(npc)==0)
 { print "^*** MoveClas Error: NPC_path called for object '",
          (the)npc, "' which has parent==0 ***";
   rfalse;
 }

 if (~~(parent(npc) ofclass Room))
    rfalse;


 ! link_data-->0 is previous ROOM in the linked list of rooms searched
 ! link_data-->1 is previous STEP on the possible path
 ! link_data-->2 is previous STEP_DIR along the possible path

 last_room=parent(npc);        ! All rooms being searched are linked in a list
 last_room.&link_data-->0=0;   ! starting with 'last_room' and stepping back
 last_room.&link_data-->1=-1;  ! along the link_room of each room.

 if (last_room==targetroom)
 { found=true;                 ! Allowing the npc_arrived property to run
   steps=0;                    ! if the npc starts in targetroom
   npc.&npc_dirs-->0=0;
 }
 else
 { for (steps=1: steps<path_size_limit:steps++)
   { i=last_room;                       ! Start at the end of the list
     while (i ~= 0)
     { objectloop (j in Compass)        ! Explore all directions
       { k=LeadsTo(j,i,path_type);
         if (k ofclass Room &&
             k.&link_data-->1==0)       ! Only want 'Room's with a 0 STEP
         { k.&link_data-->1=i;          ! Add such rooms as a STEP on from 'i'
           k.&link_data-->2=j;          ! Store direction moved to get there
           k.&link_data-->0=last_room;  ! Add this room to the linked list
           last_room=k;
           if (k==targetroom) found=true;
#Ifdef DEBUG;
if (parser_trace>1) print "[Found: ",(name) k, "]^";
#Endif;
         }
         if (found) break;
       }
       if (found) break;
       if (i.&link_data-->0==i.&link_data-->1)
         i=0;                           ! If link_data STEP = ROOM then the
       else                             ! remaining rooms on the linked list
         i=i.&link_data-->0;            ! have already been explored and we
     }                                  ! can end this iteration.
     if (found) break;
   }
 }

 if (found)
 { npc.move_type=AIMED_MOVE;            ! Set NPC to AIMED_MOVE
   npc.npc_target=targetroom;           ! and fill in all the details
   npc.prepath_size=steps;
   npc.npc_stage=0;
#ifdef DEBUG;
if (parser_trace>1)
print "[Path has ",steps, " steps...^";
#endif;
   i=last_room;                         ! Now go back to the end of the list
   while (i.&link_data-->1~=-1)
   {
#ifdef DEBUG;
if (parser_trace>1)
print (name) i," is ", (GiveDir) i.&link_data-->2, " of...^";
#endif;

#ifdef TARGET_ZCODE;
     npc.&npc_dirs->(steps-1)=i.&link_data-->2;
#ifnot; ! TARGET_GLULX
     npc.&npc_dirs-->(steps-1)=i.&link_data-->2;
#endif; ! TARGET

     steps--;                           ! Write npc_dirs with the STEP_DIRs
     i=i.&link_data-->1;                ! And go back along the STEPs

   }
#ifdef DEBUG;
if (parser_trace>1)
print "where we started!]^";
#endif;
 }
#Ifdef DEBUG;
if(parser_trace>1 && found==false) print "[No path found!]^";
#Endif;

 while (last_room~=0)
 {
#ifdef DEBUG;
if (parser_trace>4)
{print "[",(name) last_room," = ";
print (object) last_room.&link_data-->0, ", ";
print (object) last_room.&link_data-->1, ", ";
print (object) last_room.&link_data-->2, "]^";
}
#endif;
    last_room.&link_data-->1=0;           ! Go back along the linked list
    last_room=last_room.&link_data-->0;   ! resetting the STEP data. Only
 }                                        ! Rooms with a 0 STEP are added to
                                          ! the linked list (see above)
 return found;
];



[ NPCPrePath npc path_array path_length fakevar;
 fakevar=fakevar;            ! In case code tries passing a room name too
 if (npc ofclass moveclass)
 { npc.npc_stage=0;
   npc.move_type=PRESET_MOVE;
   npc.prepath_name=path_array;
   npc.prepath_size=path_length;
 }
 else
 { "*** MoveClass Error: NPCPrePath called for non-moveclass object '",
    (the) npc, "' ***";
 }
];

Ifndef NOISY_DIR_TOS;
Message "** MoveClass::LeadsTo assuming quiet *_to (NOISY_DIR_TOS not defined) **";
Endif;

[ LeadsTo direction thisroom path_type k tmp tmp2;
#ifdef DEBUG;
  if (parser_trace>2)
    print "[LeadsTo ", (name)direction, " ", (name)thisroom, "]^";
#endif;

  if (~~(direction provides door_dir)) rfalse;
  if (~~(thisroom provides direction.door_dir)) rfalse;
  k=thisroom.(direction.door_dir);

  #ifdef NOISY_DIR_TOS;
    if (ZRegion(k)==2) rfalse;
  #endif;

  #ifndef NOISY_DIR_TOS;
    if (ZRegion(k)==2)
        k=RunRoutines(thisroom, direction.door_dir);
  #endif;

  if (ZRegion(k)~=1) rfalse;
  if (k has door)
  { if (path_type & DOORLESS_PATH) rfalse;
    if ((path_type & OPEN_PATH) && k hasnt open) rfalse;
    if ((path_type & UNLOCKED_PATH) && k has locked) rfalse;
    tmp=parent(k);
    move k to thisroom;
    tmp2=k.door_to();
    if (tmp==0) remove k; else move k to tmp;
    k=tmp2;
  }
  if (~~(k ofclass Room)) rfalse;
  return k;
];



[ MoveNpcDir npc direction i j p message;
 message=2;
 p=parent(npc);
 i=LeadsTo(direction,p, ANY_PATH);
 if (i==0)
 { npc.npc_blocked();
#ifdef DEBUG;
if (parser_trace>1)
print "[MoveNPCDir blocked: Direction leads nowhere]^";
#endif;
   rfalse;
 }

 j=p.(direction.door_dir);
 if (ZRegion(j)==2) j=j();
 if (j has door)
 { if (j provides npc_open)          ! npc_open returns
   { message=j.npc_open(npc);        ! 2 to go through door as normal
     if (message==false)             ! 1 to go through door and prevent
     { npc.npc_blocked();            !      walkon/walkoff run/printing
#ifdef DEBUG;                         ! 0 to stop npc using door
if (parser_trace>1)
print "[MoveNPCDir blocked: ", (the) j, "'s npc_open returned false]^";
#endif;
       rfalse;
     }
   }
   else
     if (j hasnt open)
     {   npc.npc_blocked();
#ifdef DEBUG;
if (parser_trace>1)
print "[MoveNPCDir blocked: ", (the) j, " is closed with no npc_open]^";
#endif;
         rfalse;
     }
 }

 MoveNPC(npc, i, ##Go, direction);

 if (p==location && message==2)      ! If npc_open used then it must return 2
 { if (ZRegion(npc.walkoff)==3)      ! if it wants walkon/walkoff to execute
       print "^", (The) npc, " ", (string) npc.walkoff,
             " ", (GiveDir) direction, ".^";
   else
       npc.walkoff(direction);
 }

 if (parent(npc)==location && message==2)
 { if (ZRegion(npc.walkon)==3)
     print "^", (The) npc, " ", (string) npc.walkon, ".^";
   else
     npc.walkon(direction);
 }

 if (npc provides after_action) npc.after_action();
 rtrue;
];



Ifndef MoveNPC; ! Provides MoveNPC if program isn't including 'Follower'
[ MoveNPC npc dest actn objn;
 move npc to dest;
 actn=actn;
 objn=objn;
];
Endif;



[ GiveDir i;
 switch(i)
 { n_obj: print "to the north";
   s_obj: print "to the south";
   e_obj: print "to the east";
   w_obj: print "to the west";
  ne_obj: print "to the northeast";
  nw_obj: print "to the northwest";
  se_obj: print "to the southeast";
  sw_obj: print "to the southwest";
   u_obj: print "upwards";
   d_obj: print "downwards";
  in_obj: print "inside";
 out_obj: print "outside";
 }
];