[XP] TOA Caterpillar & Pathfinding

Started by mad.array, January 16, 2012, 08:22:18 pm

Previous topic - Next topic

mad.array

January 16, 2012, 08:22:18 pm Last Edit: January 16, 2012, 08:39:19 pm by mad.array
TOA Caterpillar, Move Routes & Pathfinding
Authors: mad.array, using Advanced Pathfinding by ForeverZer0
Version: 0.5a
Type: Movement Addon
Key Term: Movement Addon



Introduction

Well, I've gone and mucked about with a perfectly good script. Again. Two perfectly good scripts all be told. This script allows you to use the in built move route commands with the tons_of_addons caterpillar feature. It also allows caterpillar actors to behave like events whilst they are away from the player. It is in a very rough stage at the moment so some testing and feedback will be required.

It uses my modified Advanced Pathfinding & Carrots script, which has been modified some more. I'm surprised no-one has asked about the carrot thing, but it's gone 1 in the morning so I'm surprised by a lot of things.


Features


  • All the features of the previous AP script

  • Control members of your caterpillar group!

  • Have them say things like a regular event would.

  • Return them to you like a dog with a stick!




Screenshots

Not really a screenshot script.



Script

Goes below toa scripts (required). Not compatible with other Pathfinding scripts.

Spoiler: ShowHide

#=begin
#+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
# Advanced Pathfinding + Carrots with Caterpillar Support
# Author: ForeverZer0, mad.array
# Version: 1.0b
# Date: 10.12.2011
#+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
#
# Introduction:
#   This is an advanced an highly inteligent pathfinding system. It allows for
#   the user to either through script or script call quickly and easily have
#   events or the game player automatically walk a path to a given set of
#   coordinates. The system is smart enough to quickly find paths through
#   relatively complex areas, and asjust on the fly for any obstacle that moves
#   to block its path. I used the A* algorithm, basic search algorithm used
#   often for robotics. More on this algorithm can be read about here:
#
#               http://en.wikipedia.org/wiki/A*_search_algorithm
#
#
#   In addition, the modifications to the script allow for events to navigate to
#   other events via their event name. If more than one event with a name exists
#   it will pick one at random. You can also define multiple event names to pick
#   from in an array.
#
#   In a further addition, caterpillar actors (henceforthe referred to as
#   members) can also be moved by 'syncing' them to an event on the map. When
#   this is done, they will move instead of that event. They will also perform
#   the actions of that event when the trigger conditions are met.
#
# Features:
#   - Fast and intelligent pathfinding
#   - Easy to use script calls
#   - Optional "range" parameter can have character find alternate locations
#     if the preferred one is blocked and they are within the given range.
#   - Optional callbacks can be given to have something execute if when the
#     character reaches its goal, or when it fails to do so.
#
# Instructions:
#   - Place script below default scripts and tons_of_addons, and above "Main".
#   - Use the following script call:
#
#     pathfind(X, Y, CHARACTER, RANGE, SUCCESS_PROC, FAIL_PROC)
#    
#     The X and Y are the only required arguments. The others can be omitted.
#    
#     X - The x-coordinate to pathfind to. If X is a string or array then
#         strings must be contained within "quotation marks" and the array
#         within [square brackets].
#     Y - The y-coordinate to pathfind to. If using a string or array, DO NOT
#         leave Y blank. It will cause the script to crash.
#
#     CHARACTER - Either an instance of the character ($game_player,
#                 $game_map.events[ID], etc) or the ID of a character. The ID
#                 will be the event ID. Use -1 for the game player.
#
#     SUCCESS_PROC - A Proc object that will be executed when the player
#                    reaches the defined coordinates.
#     FAILURE_PROC - A Proc object that will be executed when the player
#                    cannot reach the defined coordinates.
#
#   - As default, the pathfinder will make 35 attempts to recalculate a route
#     that gets blocked. This value can be changed in game with the script
#     call:
#           $game_map.collision_retry = NUMBER
#
#     You can change the default value if desired by looking down to the first
#     class below in the main script.
#   - For longer pathfind routes, it is sometimes necessary to reset the
#     search limiter. This may cause increased lag when an object blocks the
#     character from being able to move, but will increase the range that the
#     system can work with. Use the following script call:
#
#         $game_map.search_limiter = NUMBER  (Default 1000)
#
#   - If you are experiencing any compatibility problems, go to the Game_Map
#     class below and set @recalculate_paths to false. This will take away some
#     of the efficiency of recalculating collisions, but will improve may fix
#     your problem.
#   - When you want a member to move, you have to sync them to an event. This
#     can be done by using the following script call:
#
#         $game_player.members[i].synced = i
#
#     Where i is the number of the event your member will use for move commands
#     and interaction.
#
#   - When returning a member to the train, have an event commands like this;
#    
#       Script: z = $game_player.calc_return_path(i)
#               pathfind(z[0],z[1],2,0)
#               Wait for moves completion
#       Script: $game_player.members[i].synced = nil
#
#     Where i is the index of the member(0 to 2)
#      
#
# Compatibility:
#   Highly compatible. May experience issues with Custom Movement scripts,
#   but even then, it is unlikely. Also modifies Interpreter command 209... as
#   in not aliased, I had to copy and paste and change modified. Sorry!
#
# Credits/Thanks:
#   - ForeverZer0, for the AP script
#   - Blizzard, for the caterpillar scipt and general awesomeness.
#   - Special thanks to Jragyn for help making the big maze for the demo and
#     help testing.
#   - Credit goes to the Peter Hart, Nils Nilsson and Bertram Raphael for the
#     original search algorithm that was implemented
#
#+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+

class Game_System
 
 alias init_toa_later initialize
 def initialize
   init_toa_later
   @CATERPILLAR                = true
end

 attr_accessor :CATERPILLAR
end

#===============================================================================
# ** Game_Map
#===============================================================================

class Game_Map
 
 attr_accessor :collision_retry
 attr_accessor :recalculate_paths
 attr_accessor :search_limiter
 
 alias zer0_pathfinding_init initialize
 def initialize
   # Initialize instance variables used for pathfinding.
   @collision_retry = 35
   @recalculate_paths = true
   @search_limiter = 1000
   # Original method
   zer0_pathfinding_init
 end
end

#===============================================================================
# ** Interpreter
#===============================================================================

class Interpreter
 
 def pathfind(x, y, *args)
   @x = x
   @y = y
   args[0] = @event_id if args[0] == nil
   args[1] = 0 if args[1] == nil
   if @x.is_a?(Array)
     decidepath(@x, @y, @args)
   end
   findpathevent(@x,@y, args)
   Pathfind.new(Node.new(@x, @y), *args)
 end

 def decidepath(x, y, args)
   r = rand(100) + 1
   for i in 0...x.size
     targ1 = 1 + (100/x.size) * i
     targ2 = (100/x.size) * (i +1)
     if r >= targ1 && r <=targ2
       @x=x[i]
     end
   end
 end

 def findpathevent(x, y, args)
   xs = []
   ys = []
   proxx = []
   proxy = []
   distance = []
   chosen = nil
   if x.is_a?(String)
     for event in $game_map.events.values
       if event.name == x
         xs.push(event.x)
         ys.push(event.y)
       end
     end
     if args[0].is_a?(Integer)
     if args[0] >  0
       runnerx = $game_map.events[args[0]].x
       runnery = $game_map.events[args[0]].y
     else
       runnerx = $game_player.x
       runnery = $game_player.y
     end
   end
   if args[0].is_a?(Game_Character)
     char = args[0]
     runnerx = char.x
     runnery = char.y
   end
     if xs.size > 1
       for i in 0...xs.size
         proxx[i] = runnerx - xs[i]
         proxy[i] = runnery - ys[i]
         proxx[i] *= -1 if proxx[i] <0
         proxy[i] *= -1 if proxy[i] <0
         if proxx[i] > proxy[i]
           distance[i] = proxx[i] - proxy[i]
         elsif proxx[i] < proxy[i]
           distance[i] = proxy[i] - proxx[i]
         elsif proxx[i]==0
           distance[i] = proxy[i]
         elsif proxy[i]==0 || proxy[i] == proxx[i]
           distance[i] = proxx[i]
         end
       end
       j = 0
       while chosen == nil
         chosen = distance.index(j)
         j+=1
       end
         @x = xs[chosen]
         @y = ys[chosen]
     else
       @x = xs[0]
       @y = ys[0]
     end
   end
 end
end


class Game_Event
 
 attr_accessor :direction_fix
 attr_accessor :step_anime
 attr_accessor :walk_anime
 attr_accessor :always_on_top
 
def name
  return @event.name
end
end

#==============================================================================
# ** Pathfind
#==============================================================================

class Pathfind

 attr_reader   :route                  
 attr_accessor :range  
 attr_reader   :goal
 attr_reader   :found
 attr_reader   :character                                
 attr_accessor :success_proc          
 attr_accessor :failure_proc        
 attr_accessor :target    
 attr_accessor :collisions      
 
 def initialize(node, char = -1, range = 0, *callbacks)
   # Set the character. Can either us an ID or an instance of a Game_Character.
   # A value of -1, which is default, is the Game_Player.
   if char.is_a?(Integer)
     @character = (char == -1) ? $game_player : $game_map.events[char]
     $game_player.members.each {|member|
       if member.synced==char
         member.frozen = false
         @character = member
       end}
   elsif char.is_a?(Game_Character)
     @character = char
   end
   # Set forcing flag. Will be disabled for recalculating on the fly.
   @forcing = true
   # Call a public method, since this method may need to be used again,
   # and "initialize" is private.
   setup(node, range, *callbacks)
 end
 
 
 def setup(node, range = 0, *callbacks)
   # Initialize the node we are trying to get to.
   @target = Node.new(node.x, node.y)
   @goal = @target.clone
   # Set beginning nodes and required variables.
   @start_node = Node.new(@character.x, @character.y)
   @nearest = Node.new(0, 0, 0, -1)
   @range, @found, @collisions = range, false, 0
   # Set callbacks for success and failure if included, else nil.
   @success_proc = callbacks[0]
   @failure_proc= callbacks[1]
   # Initialize sets to track open and closed nodes
   @open_set, @close_set = [@start_node], {}  
   # Find the optimal path
   calculate_path  
 end

 def calculate_path
   # Only do calculation if goal is actually passable, unless we only
   # need to get close or within range
   if @character.passable?(@goal.x, @goal.y, 0) || @range > 0 || @character.is_a?(Game_Member)
     # Initialize counter
     counter, wait = 0, 0
     until @open_set.empty?
       counter += 1
       # Give up if pathfinding is taking more than 500 iterations
       if counter >= $game_map.search_limiter
         @found = false
         break
       end
       # Get the node with lowest cost and add it to the closed list
       @current_node = get_current
       @close_set[[@current_node.x, @current_node.y]] = @current_node
       if @current_node == @goal ||
          (@range > 0 && @goal.in_range?(@current_node, @range) &&
          !@character.passable?(@goal.x, @goal.y,0))
         # We reached goal, exit the loop!
         @target = @goal
         @goal, @found = @current_node, true
         break
       else # if not goal
         # Keep track of the node with the lowest cost so far
         if @current_node.heuristic < @nearest.heuristic ||
           @nearest.heuristic < 1
           @nearest = @current_node
         end
         # Get adjacent nodes and check if they can be added to the open list
         neighbor_nodes(@current_node).each {|neighbor|
           # Skip Node if it already exists in one of the lists.
           next if can_skip?(neighbor)
           # Add node to open list following the binary heap conventions
           @open_set.push(neighbor)
           arrange(@open_set.size - 1)
         }
       end
     end
   end
   # If no path was found, see if we can get close to goal
   unless @found
     if @range > 0 && @nearest.heuristic > 0  
       # Create an alternate path.
       setup(@nearest, @range, @success_proc, @failure_proc)
     elsif @failure_proc != nil && (($game_map.collision_retry == 0) ||
       (@collisions > $game_map.collision_retry))
       # If out of retries, call the Proc for failure if defined
       @failure_proc.call
     end
   end
   # Create the move route using the generated path
   create_move_route
 end

 def create_move_route
   # There's no path to generate if no path was found
   return if !@found
   # Create a new move route that isn't repeatable
   @route = RPG::MoveRoute.new
   @route.repeat = false
   # Generate path by starting from goal and following parents
   node = @goal
   while node.parent
     # Get direction from parent to node as RPG::MoveCommand
     code = case direction(node.parent.x, node.parent.y, node.x, node.y)
     when 2 then 4 # Up
     when 4 then 3 # Left
     when 6 then 2 # Right
     when 8 then 1 # Down
     else; 0
     end
     # Add movement code to the start of the array
     @route.list.unshift(RPG::MoveCommand.new(code)) if code != 0
     node = node.parent
   end
   # If the path should be assigned to the character
   if (@forcing && !@route.list.empty?)
     @collisions = 0
     @character.paths.push(self)
     @character.force_move_route(@route) if @character.paths.size == 1
   end
   # Reset forcing flag if needed
   @forcing = true
   # Return the constructed RPG::MoveRoute
   return @route
 end
 
 def arrange(index)
   # Rearrange nodes in the open_set
   while index > 0
     # Break loop unless current item's cost is less than parent's
     break if @open_set[index].score > @open_set[index / 2].score
     # Bring lowest value to the top.
     temp = @open_set[index / 2]
     @open_set[index / 2] = @open_set[index]
     @open_set[index] = temp
     index /= 2
   end
 end
 
 def get_current
   return if @open_set.empty?
   return @open_set[0] if @open_set.size == 1
   # Set current node to local variable and replace it with the last
   current = @open_set[0]
   @open_set[0] = @open_set.pop
   # Loop and rearrange array according to the A* algorithm until done.
   y = 0  
   loop {
     x = y
     # If two children exist
     if 2 * x + 1 < @open_set.size
       if @open_set[2 * x].score <= @open_set[x].score
         y = 2 * x
         if @open_set[2 * x + 1].score <= @open_set[y].score
           y = 2 * x + 1
         end
       end
     # If only one child exists
     elsif 2 * x < @open_set.size &&
       @open_set[2 * x].score <= @open_set[x].score
       y = 2 * x
     end
     # Swap a child if it is less than the parent.
     break if x == y
     temp = @open_set[x]
     @open_set[x] = @open_set[y]
     @open_set[y] = temp
   }
   # Return the original first node (which was removed)
   return current
 end

 def direction(x1, y1, x2, y2)
   # Return the numerical direction between coordinates.
   return 6 if x1 > x2 # Right
   return 4 if x1 < x2 # Left
   return 2 if y1 > y2 # Bottom
   return 8 if y1 < y2 # Top
   return 0            
 end
 
 def neighbor_nodes(node)
   # Create array to hold the nodes, then check each direction.
   nodes = []
   nodes.push(get_neighbor(node.x + 1, node.y, node)) # Right
   nodes.push(get_neighbor(node.x - 1, node.y, node)) # Left
   nodes.push(get_neighbor(node.x, node.y + 1, node)) # Down
   nodes.push(get_neighbor(node.x, node.y - 1, node)) # Up
   # Remove any nil elements, then return results.
   return nodes.compact
 end
 
 def get_neighbor(x, y, parent)
   # Calculate direction, return new node if passable.
   direction = direction(x, y, parent.x, parent.y)
   if @character.passable?(parent.x, parent.y, direction)
     # The heuristic is simply the distance
     heuristics = ((x - @goal.x).abs + (y - @goal.y).abs)
     return Node.new(x, y, parent, parent.cost + 1, heuristics)
   end
 end
 
 def can_skip?(node)
   # Branch by if node is in either the open or closed set.
   if @open_set.include?(node)
     index = @open_set.index(node)
     return true if @open_set[index].score <= node.score
     # Swap them and update list order
     @open_set[index] = node
     arrange(index)
     return true
   elsif @close_set[[node.x, node.y]] != nil
     # If the existing passed node has a lower score than this one.
     return true if @close_set[[node.x, node.y]].score <= node.score
     # Update the existing node
     @close_set[[node.x, node.y]] = node
   end
   # Return false if no criteria was met.
   return false
 end
end

#==============================================================================
# ** Game_Character
#==============================================================================

class Game_Character
 
 attr_accessor :paths
 attr_accessor :move_route_forcing
 attr_accessor :move_route

 alias zer0_pathfinding_init initialize
 def initialize
   # Add public instance variable for paths
   @paths = []
   # Original method
   zer0_pathfinding_init
 end
 
 def next_route
   # Stop any custom move route that may be occuring.
   if @move_route != nil
     # Set index and disable forcing of current route
     @move_route_index = @move_route.list.size
     @move_route_forcing = false
     # Reset to what it was originally
     @move_route = @original_move_route
     @move_route_index = @original_move_route_index
     @original_move_route = nil
   end
   # Remove first path from the paths array.
   @paths.shift
   # If there is another path to follow...
   if @paths[0] != nil
     # Setup path again to reflect any changes since original creation
     @forcing = false
     @paths[0].setup(@paths[0].target, @paths[0].range,
       @paths[0].success_proc, @paths[0].failure_proc)
     force_move_route(@paths[0].route) if @paths[0].found
   end
 end
 
 alias zer0_recalculate_paths_move move_type_custom
 def move_type_custom
   if $game_map.recalculate_paths
     # Interrupt if not stopping
     return if jumping? || moving?
     # Loop until finally arriving at move command list
     while @move_route_index < @move_route.list.size
       # Get the move command at index
       command = @move_route.list[@move_route_index]
       # If command code is 0 (end of list)
       if command.code == 0
         # If [repeat action] option is ON
         if @move_route.repeat
           # Reset move route index to the top of the list
           @move_route_index = 0
         end
         # If [repeat action] option is OFF
         unless @move_route.repeat
           # If move route is forced and not repeating
           if @move_route_forcing and not @move_route.repeat
             # The move route is no longer forced (moving ended)
             @move_route_forcing = false
             # Restore original move route
             @move_route = @original_move_route
             @move_route_index = @original_move_route_index
             @original_move_route = nil
             # If there was a path to follow and we reached goal
             if @paths[0] != nil
               if self.x == @paths[0].goal.x &&
                 y == @paths[0].goal.y && @paths[0].success_proc
                 # Call success Proc if goal is reached and it is defined.
                 @paths[0].success_proc.call
               end
               next_route
             end
           end
           # Clear stop count
           @stop_count = 0
         end
         return
       end # if command.code == 0
       # For move commands (from move down to jump)
       if command.code <= 14
         # Branch by command code
         case command.code
         when 1 then move_down                 # Move down
         when 2 then move_left                 # Move left
         when 3 then move_right                # Move right
         when 4 then move_up                   # Move up
         when 5 then move_lower_left           # Move lower left
         when 6 then move_lower_right          # Move lower right
         when 7 then move_upper_left           # Move upper left
         when 8 then move_upper_right          # Move upper right
         when 9 then move_random               # Move random
         when 10 then move_toward_player       # Move toward player
         when 11 then move_away_from_player    # Move away from player
         when 12 then move_forward             # Step forward
         when 13 then move_backward            # Step backward
         when 14 then jump(command.parameters[0], command.parameters[1]) # Jump
         end
         # If movement failure occurs when "Ignore If Can't Move" is unchecked.
         if !@move_route.skippable && !moving? && !jumping?
           # If path is current and collision limit is not reached
           if @paths[0] != nil &&
             @paths[0].collisions < $game_map.collision_retry
             # Setup path again to update starting location.
             # original goal node is used because pathfinding changes
             # the goal node to current node
             goal, range = @paths[0].target, @paths[0].range
             reach = @paths[0].success_proc
             fail = @paths[0].failure_proc
             counter = @paths[0].collisions + 1
             # Find another path to goal
             @paths[0] = Pathfind.new(goal, self, range, reach, fail)
             @paths[0].collisions = counter
             force_move_route(@paths[0].route) if @paths[0].found
             # Wait a bit before starting to follow the new path
             @wait_count = 6
             return
           elsif paths[0] != nil
             # Call failure Proc if defined and set move index.
             @move_route_index = @move_route.list.size
             @paths[0].failure_proc.call if @paths[0].failure_proc != nil
             next_route
           end
           # End method
           return
         end
         # Advance index
         @move_route_index += 1
         return
       end # if command.code <= 14
       # If waiting
       if command.code == 15
         # Set wait count (from provided parameter)
         @wait_count = command.parameters[0] * 2 - 1
         @move_route_index += 1
         return
       end # if command.code == 15
       # If direction change (turning) command
       if command.code >= 16 and command.code <= 26
         # Branch by command code
         case command.code
         when 16 then turn_down                      # Turn down
         when 17 then turn_left                      # Turn left
         when 18 then turn_right                     # Turn right
         when 19 then turn_up                        # Turn up
         when 20 then turn_right_90                  # Turn 90° right
         when 21 then turn_left_90                   # Turn 90° left
         when 22 then turn_180                       # Turn 180°
         when 23 then turn_right_or_left_90          # Turn 90° right or left
         when 24 then turn_random                    # Turn at Random
         when 25 then turn_toward_player             # Turn toward player
         when 26 then turn_away_from_player          # Turn away from player
         end
         @move_route_index += 1
         return
       end
       # If other command (commands that don't 'return')
       if command.code >= 27
         # Branch by command code
         case command.code
         when 27                                              # Switch ON
           $game_switches[command.parameters[0]] = true
           $game_map.need_refresh = true
         when 28                                              # Switch OFF
           $game_switches[command.parameters[0]] = false
           $game_map.need_refresh = true
         when 29 then @move_speed = command.parameters[0]     # Change speed
         when 30 then @move_frequency = command.parameters[0] # Change freq
         when 31 then @walk_anime = true                      # Move ON
         when 32 then @walk_anime = false                     # Move OFF
         when 33 then @step_anime = true                      # Stop ON
         when 34 then @step_anime = false                     # Stop OFF
         when 35 then @direction_fix = true                   # Direction ON
         when 36 then @direction_fix = false                  # Direction OFF
         when 37 then @through = true                         # Through ON
         when 38 then @through = false                        # Through OFF
         when 39 then @always_on_top = true                   # On top ON
         when 40 then @always_on_top = false                  # On top OFF
         when 41                                              # Change Graphic
           # Can't change into a tile
           @tile_id = 0
           @character_name = command.parameters[0]
           @character_hue = command.parameters[1]
           # Update direction
           if @original_direction != command.parameters[2]
             @direction = command.parameters[2]
             @original_direction = @direction
             @prelock_direction = 0
           end
           # Update frame
           if @original_pattern != command.parameters[3]
             @pattern = command.parameters[3]
             @original_pattern = @pattern
           end
         when 42 then @opacity = command.parameters[0]        # Change Opacity
         when 43 then @blend_type = command.parameters[0]     # Change Blending
         when 44 then $game_system.se_play(command.parameters[0]) # Play SE
         when 45 then result = eval(command.parameters[0])    # Script
         end
         # Increment move index.
         @move_route_index += 1
       end
     end
   else
     # Original method
     zer0_recalculate_paths_move
   end
 end
end

#==============================================================================
# ** Node
#==============================================================================

class Node

 attr_accessor :x                      
 attr_accessor :y                      
 attr_accessor :parent                  
 attr_accessor :cost                
 attr_accessor :heuristic                  

 def initialize(x, y, parent = nil, cost = 0, heuristic = 0)
   # Set public instance variables.
   @x, @y, @parent, @cost, @heuristic = x, y, parent, cost, heuristic
 end

 def score
   # Return the current "score" of this node
   return @cost + @heuristic
 end
 
 def in_range?(node, range)
   # Return true/false if Nodes are within RANGE of each other.
   return (@x - node.x).abs + (@y - node.y).abs <= range
 end

 def ==(node)
   # Returns true/false of whether self and other are equal.
   return ((node.is_a?(Node)) && (node.x == @x) && (node.y == @y))
 end
end

#:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=
# Caterpillar by Blizzard
# Version: 2.2b
# Type: Game Experience Improvement
# Date: 7.3.2007
# Date v1.01b: 7.3.2007
# Date v2.0: 7.8.2007
# Date v2.0b: 8.8.2007
# Date v2.1b: 17.2.2008
# Date v2.11b: 22.2.2008
# Date v2.2b: 1.11.2008
#:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=
#
# new in v1.01b:
#   - now events can't go through your party members anymore
#
# new in v2.0:
#   - completely overworked and improved
#
# new in v2.0b:
#   - improved coding and made it work more convenient
#   - fixed a few glitches
#   - added DEAD_DISPLAY option
#   - removed stop animation override upon the player character
#
# new in v2.1b:
#   - fixed bug where an empty party would cause a crash
#
# new in v2.11b:
#   - fixed a minor compatibility glitch with Blizz-ABS
#
# new in v2.2b:
#   - improved coding and made it work more convenient
#   - fixed the bug where deactivating the caterpillar wouldn't work properly
#
#
# Compatibility:
#
#   93% compatible with SDK v1.x. 60% compatible with SDK v2.x. You might
#   experience problems with pixel movement scripts or map graphic manipulating
#   scripts. Blizz-ABS disables this add-on automatically and uses the
#   Blizz-ABS Caterpillar system.
#
#
# Features:
#
#   - your party members follow you on the map
#   - less code than other caterpillar scripts
#   - use $game_player.update_buffer('reset') if you need all party members to
#     gather around the player
#:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=:=

#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
# START Configuration
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

# max number of party members
MAX_PARTY = 4
# actor IDs where the actor is animated even when not walking
ANIMATED_IDS = []
# 0 - shows all characters; 1 - shows "ghosts"; 2 - removes from caterpillar
DEAD_DISPLAY = 0

#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
# END Configuration
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

#==============================================================================
# Game_Character
#==============================================================================
   
class Game_Character
 
 alias passable_caterpillar_later? passable?
 def passable?(x, y, d)
   result = passable_caterpillar_later?(x, y, d)
   new_x = x + (d == 6 ? 1 : d == 4 ? -1 : 0)
   new_y = y + (d == 2 ? 1 : d == 8 ? -1 : 0)
   unless @through
     $game_player.members.each {|member|
         if member.character_name != '' && member.x == new_x && member.y == new_y && member.through==false
           return false
         end}
   end
   return result if $BlizzABS && BlizzABS::VERSION >= 1.01
   return result if self.is_a?(Game_Player) || self.is_a?(Game_Member)
   new_x = x + (d == 6 ? 1 : d == 4 ? -1 : 0)
   new_y = y + (d == 2 ? 1 : d == 8 ? -1 : 0)
   unless @through
     $game_player.members.each {|member|
         if member.character_name != '' && member.x == new_x && member.y == new_y
           return false
         end}
   end
   return result
 end
 
end

#==============================================================================
# Game_Player
#==============================================================================
   
class Game_Player
 
 attr_reader :members
 attr_reader :move_speed
 attr_accessor :active_member
 attr_accessor :return_buffer
 
 alias init_caterpillar_later initialize
 def initialize
   init_caterpillar_later
   @active_member = true
   @members = []
   @return_buffer = []
   @lastdirection = 0
   @directionpause = 4
   @directionframe = 0
   (1...MAX_PARTY).each {|i| @members.push(Game_Member.new(i))} unless $BlizzABS
 end
 
 alias upd_caterpillar_later update
 def update
   upd_caterpillar_later
   refresh if DEAD_DISPLAY > 0
   @members.each {|member| member.update}
   if $game_system.CATERPILLAR && ANIMATED_IDS.include?(actor.id)
     @step_anime = true
   end
 end
 
 alias straighten_caterpillar_later straighten
 def straighten
   straighten_caterpillar_later
   @members.each {|member| member.straighten}
 end
 
 alias refresh_caterpillar refresh
 def refresh
   unless $game_system.CATERPILLAR
     refresh_caterpillar
     return
   en d
   act, $game_party.actors[0] = $game_party.actors[0], actor
   $game_party.actors.pop if $game_party.actors[0] == nil
   refresh_caterpillar
   return if actor == nil
   $game_party.actors[0] = act
   if actor.dead? && DEAD_DISPLAY == 1
     @opacity = Graphics.frame_count % 4 / 2 * 255
     @blend_type = 1
   end
 end
 
 def actor
   if DEAD_DISPLAY > 0
     $game_party.actors.each {|actor| return actor unless actor.dead?}
   end
   return $game_party.actors[0]
 end
 
 def update_buffer(next_move)
   if next_move == nil
     @members.each {|member| member.buffer = []}
   else
     @members.each {|member| member.update_buffer(
         next_move == 'reset' ? nil : next_move)}
   end
 end
 
 def update_return_buffer(move)
   return_buffer.push(move)
   if return_buffer.size > 3
     return_buffer.delete_at(0)
   end
 end
 
 alias move_down_caterpillar_later move_down
 def move_down(turn_enabled = true)
   update_buffer(2) if passable?(@x, @y, 2)
   update_return_buffer(2) if passable?(@x, @y, 2)
   move_down_caterpillar_later
 end
 
 alias move_left_caterpillar_later move_left
 def move_left(turn_enabled = true)
   update_buffer(4) if passable?(@x, @y, 4)
   update_return_buffer(4) if passable?(@x, @y, 4)
   move_left_caterpillar_later
 end
 
 alias move_right_caterpillar_later move_right
 def move_right(turn_enabled = true)
   update_buffer(6) if passable?(@x, @y, 6)
   update_return_buffer(6) if passable?(@x, @y, 6)
   move_right_caterpillar_later
 end
 
 alias move_up_caterpillar_later move_up
 def move_up(turn_enabled = true)
   update_buffer(8) if passable?(@x, @y, 8)
   update_return_buffer(8) if passable?(@x, @y, 8)
   move_up_caterpillar_later
 end
 
 alias move_lower_left_caterpillar_later move_lower_left
 def move_lower_left
   if passable?(@x, @y, 2) && passable?(@x, @y + 1, 4) ||
      passable?(@x, @y, 4) && passable?(@x - 1, @y, 2)
     update_buffer(1)
   end
   move_lower_left_caterpillar_later
 end
 
 alias move_lower_right_caterpillar_later move_lower_right
 def move_lower_right
   if passable?(@x, @y, 2) && passable?(@x, @y + 1, 6) ||
      passable?(@x, @y, 6) && passable?(@x + 1, @y, 2)
     update_buffer(3)
   end
   move_lower_right_caterpillar_later
 end
 
 alias move_upper_left_caterpillar_later move_upper_left
 def move_upper_left
   if passable?(@x, @y, 8) && passable?(@x, @y - 1, 4) ||
      passable?(@x, @y, 4) && passable?(@x - 1, @y, 8)
     update_buffer(7)
   end
   move_upper_left_caterpillar_later
 end
 
 alias move_upper_right_caterpillar_later move_upper_right
 def move_upper_right
   if passable?(@x, @y, 8) && passable?(@x, @y - 1, 6) ||
      passable?(@x, @y, 6) && passable?(@x + 1, @y, 8)
     update_buffer(9)
   end
   move_upper_right_caterpillar_later
 end
 
 alias jump_caterpillar_later jump
 def jump(x_plus, y_plus)
   if (x_plus != 0 || y_plus != 0) && passable?(@x + x_plus, @y + y_plus, 0)
     update_buffer([x_plus, y_plus])
   end
   jump_caterpillar_later(x_plus, y_plus)
 end
 
 alias moveto_caterpillar moveto
 def moveto(x, y)
   update_buffer(nil)
   moveto_caterpillar(x, y)
   @members.each {|member|
   unless member.frozen==true
       member.moveto(x, y)
       case @direction
         when 2 then member.turn_down
         when 4 then member.turn_left
         when 6 then member.turn_right
         when 8 then member.turn_up
       end
   end}
 end
 
 def syncevent(member,eventid)
   mevent = $game_player.members[member]
   event = $game_map.events[eventid]
   mevent.synced=eventid
 end
 
 alias check_member_event_there check_event_trigger_there
 def check_event_trigger_there(triggers)
   result = false
   # If event is running
   if $game_system.map_interpreter.running?
     return result
   end
   result = check_member_event_there(triggers)
   new_x = @x + (@direction == 6 ? 1 : @direction == 4 ? -1 : 0)
   new_y = @y + (@direction == 2 ? 1 : @direction == 8 ? -1 : 0)
   for member in $game_player.members
     # If event coordinates and triggers are consistent
     if member.synced != nil
       event = $game_map.events[member.synced]
       if member.x == new_x && member.y == new_y && triggers.include?(event.trigger)
         # If starting determinant is front event (other than jumping)
         #if not member.jumping? and not member.over_trigger?
           member.lock
           event.start
           result = true
         #end
       end
     end
   end
 return result
end

 def calc_return_path(index)
   nodex=$game_player.x
   nodey=$game_player.y
   b = $game_player.members[index]
   rb = return_buffer.reverse
   if index > 0
   for i in 0...index
     move = rb[i]
     nodex +=1 if move == 4
     nodex -=1 if move == 6
     nodey -=1 if move == 2
     nodey +=1 if move == 8
   end
 else
     move = rb[0]
     nodex +=1 if move == 4
     nodex -=1 if move == 6
     nodey -=1 if move == 2
     nodey +=1 if move == 8
   end
   b.buffer = []
   if index > 0
     for i in 0...index
       b.buffer.push(rb[i])
     end
   else
     b.buffer.push(rb[0])
   end
   return [nodex, nodey]
 end
 
end
 
#==============================================================================
# Game_Member
#==============================================================================

class Game_Member < Game_Character
 
 attr_accessor :buffer
 attr_accessor :frozen
 attr_accessor :synced
 attr_accessor :direction_fix
 attr_accessor :step_anime2
 attr_accessor :walk_anime
 attr_accessor :always_on_top
 
 def initialize(index)
   super()
   @frozen = false
   @synced = nil
   @direction_fix = false
   @step_anime2 = false
   @walk_anime = true
   @always_on_top = false
   @index, @force_movement, @buffer, @through = index, 0, [], true
 end
 
 def refresh
   unless $game_system.CATERPILLAR && actor != nil
     @character_name, @character_hue = '', 0
     return
   end
   @character_name = actor.character_name
   @character_hue = actor.character_hue
   if actor.dead? && DEAD_DISPLAY == 1
     @opacity, @blend_type = Graphics.frame_count % 4 / 2 * 255, 1
   else
     @opacity, @blend_type = 255, 0
   end
 end
 
 def actor
   case DEAD_DISPLAY
   when 0 then return $game_party.actors[@index]
   when 1
     alive = 0
     $game_party.actors.each {|actor| alive += 1 unless actor.dead?}
     if @index >= alive
       ind, flag = @index - alive, true
     else
       ind, flag = @index, false
     end
     $game_party.actors.each_index {|i|
         ind -= 1 if (flag == $game_party.actors[i].dead?)
         return $game_party.actors[i] if ind < 0}
   when 2
     ind = @index
     $game_party.actors.each_index {|i|
         ind -= 1 unless $game_party.actors[i].dead?
         return $game_party.actors[i] if ind < 0}
   end
   return nil
 end
 
 def update
   refresh
   @transparent = $game_player.transparent
   @move_speed = $game_player.move_speed
   @through = !@frozen
   unless moving? || @buffer.size <= @index && @force_movement <= 0 || @frozen == true
     if @buffer.size > 0
       move = @buffer.shift
       if move.is_a?(Array)
         jump(move[0], move[1])
       else
         case move
         when 1 then move_lower_left
         when 2 then move_down(true)
         when 3 then move_lower_right
         when 4 then move_left(true)
         when 6 then move_right(true)
         when 7 then move_upper_left
         when 8 then move_up(true)
         when 9 then move_upper_right
         end
       end
       @force_movement -= 1 if @force_movement > 0
     end
   end
   super
   @step_anime2 = @step_anime
   @step_anime = (ANIMATED_IDS.include?($game_party.actors[@index].id) || @step_anime2)
 end
 
 def update_buffer(next_move)
   if next_move == nil
     @force_movement = @buffer.size
   else
     @buffer.push(next_move)
     @force_movement = @buffer.size if next_move.is_a?(Array)
   end
 end
 
 def check_event_trigger_touch(x, y) # don't remove this, it's necessary...
 end
 
 def screen_z(height = 0)
   return (super - @index)
 end
 
end

#==============================================================================
# Spriteset_Map
#==============================================================================

class Spriteset_Map
 
 alias init_caterpillar_later initialize
 def initialize
   init_caterpillar_later
   return if $BlizzABS && BlizzABS::VERSION >= 1.01
   $game_player.members.each {|member|
       sprite = Sprite_Character.new(@viewport1, member)
       sprite.update
       @character_sprites.push(sprite)}
 end
 
end

#==============================================================================
# Scene_Map
#==============================================================================

class Scene_Map
 
 alias transfer_player_caterpillar_later transfer_player
 def transfer_player
   transfer_player_caterpillar_later
   return if $BlizzABS
   case $game_temp.player_new_direction
   when 2 then $game_player.members.each {|member| member.turn_down}
   when 4 then $game_player.members.each {|member| member.turn_left}
   when 6 then $game_player.members.each {|member| member.turn_right}
   when 8 then $game_player.members.each {|member| member.turn_up}
   end
 end
 
end

class Interpreter
 
 def command_209
   character = nil
   $game_player.members.each {|member|
     if member.synced == @parameters[0]
       character = member
       character.frozen = true
     end}
   # If no character exists
   if character == nil
     # Get character
     character = get_character(@parameters[0])
     # Continue
   end
   if character == nil
     return true
   end
   # Force move route
   character.force_move_route(@parameters[1])
   # Continue
   return true
 end
 
 alias command_end_member command_end
 def command_end
   command_end_member
   $game_player.members.each {|member|
   if member.synced == @event_id
     member.unlock
   end}
 end
 
end



Instructions

Instructions in script.


Compatibility


  • Not compatible with other Pathfinding scripts.

  • Requires Tons Of Addons, but Caterpillar boolean must be set in this script.

  • Rewrites Interpreter command 209. Place above any scripts that alias this method!





Credits and Thanks


  • ForeverZer0, for the original Pathfinding

  • Blizzard, for the original caterpillar script.

  • My girlfriend, for unwavering support and allowing me to indulge my insomnia




Author's Notes

Feel free to post any feedback/complaints/improvements. I know this thing is far from complete!

Enjoy!
"To walk at speed, manage or oversee..."

"RUN!"

ForeverZer0

Nice job! :D
That should be a feature a lot of people will find very handy. 
I am done scripting for RMXP. I will likely not offer support for even my own scripts anymore, but feel free to ask on the forum, there are plenty of other talented scripters that can help you.

mad.array

Thanks. I had a few problems posting. Something is suffering from the crazies. Not sure whether it's my end or not but my post got sliced halfway and then while I was editing it it jumped from RGSS to General. crazy!
"To walk at speed, manage or oversee..."

"RUN!"

G_G

*coughs* *blames it on G_G* It was totally G_G. Wait...shit.

mad.array

lol, at least I know I'm not going senile just yet...

At least, not as senile as 'they' keep insisting :wacko:
"To walk at speed, manage or oversee..."

"RUN!"