SLIP INTO RUBY - UNDER THE HOOD PART 24: THE FINAL EPISODE

In which we complete a journey started 8 years ago, and say our fond farewells.

  • Trihan
  • 06/25/2021 04:56 PM
  • 357 views
Hello, sports fans! We're so close to the end of this code breakdown so let's just jump right into the next fun-filled episode of



Today: Name a debugged battle

Scene_Name
This is one that's not used by every game, but is invaluable for the ones that do. Scene_Name handles the scene where you rename actors, and it inherits from Scene_MenuBase.

def prepare(actor_id, max_char)
    @actor_id = actor_id
    @max_char = max_char
  end


For our prepare method, we take actor_id and max_char as parameters. We simply set the instance variables @actor_id and @max_char to the values passed in.

def start
    super
    @actor = $game_actors[@actor_id]
    @edit_window = Window_NameEdit.new(@actor, @max_char)
    @input_window = Window_NameInput.new(@edit_window)
    @input_window.set_handler(:ok, method(:on_input_ok))
  end


In the start method, first we call the parent method with super. Then we set the instance variable @actor to the instance of $game_actors at index @actor_id. We set @edit_window to a new instance of Window_NameEdit passing in @actor and @max_char as the "actor" and "max_char" arguments, and @input_window to a new instance of Window_NameInput passing in @edit_window as the "edit_window" argument. Then we call set_handler on @input_window to set the :ok handler to call the on_input_ok method.

def on_input_ok
    @actor.name = @edit_window.name
    return_scene
  end


And in the ok handler, we set @actor's name to the name from @edit_window, and then call return_scene to go back to the scene we were on previously.

That's it for Scene_Name! The beauty is that all the other legwork has been covered either by the parent class or the window classes themselves, so nothing else is needed.

Scene_Debug
Definitely one of the most important scenes from a developer perspective; this class handles the debugging menu which opens when you hit F9 in test play, and it also inherits from Scene_MenuBase.

def start
    super
    create_left_window
    create_right_window
    create_debug_help_window
  end


For the start method, we're just calling the parent method and then a number of window creation methods, namely create_left_window to create the switch/variable list, create_right_window to create the value display/edit list, and create_debug_help_window to create the help window down the bottom of the screen.

def terminate
    super
    #$game_map.refresh
  end


The terminate method only calls the parent method; it used to refresh $game_map as well, but for whatever reason the devs commented that line out. Technically since "super" is the only line that's here now, we could remove this entirely as it doesn't really do anything.

def create_left_window
    @left_window = Window_DebugLeft.new(0, 0)
    @left_window.set_handler(:ok,     method(:on_left_ok))
    @left_window.set_handler(:cancel, method(:return_scene))
  end


In this method, we set @left_window to a new instance of Window_DebugLeft passing in 0 for the x coordinate and 0 for the y. Then we set the :ok handler to on_left_ok, and the :cancel handler to return_scene.

def create_right_window
    wx = @left_window.width
    ww = Graphics.width - wx
    @right_window = Window_DebugRight.new(wx, 0, ww)
    @right_window.set_handler(:cancel, method(:on_right_cancel))
    @left_window.right_window = @right_window
  end


Here we create the right-hand window. We set wx to the width of @left_window, and ww to the width of the game window minus wx. Then we set @right_window to a new instance of Window_DebugRight passing in wx for the x coordinate, 0 for the y, and ww for the width. This will place the window to the right of the left window, taking up all available remaining horizontal screen space (the window itself defines the height, which is enough to fit 10 lines of text). Then we set the :cancel handler to on_right_cancel, and set the right_window property of @left_window to @right_window, which creates the link between the windows allowing the right window to "know" whether a switch or variable is selected in the left one. (we don't need an :ok handler for this window because the functionality of the enter key is handled already by the window class itself)

def create_debug_help_window
    wx = @right_window.x
    wy = @right_window.height
    ww = @right_window.width
    wh = Graphics.height - wy
    @debug_help_window = Window_Base.new(wx, wy, ww, wh)
  end


This method creates the help window for the debug scene. We set wx to the x of @right_window, wy to its height, ww to its width, and wh to the height of the game window minus wy. Then we set @debug_help_window to a new instance of Window_Base, passing in wx, wy, ww and wh as the x, y, width and height arguments respectively. This will cause the help window to take up the remaining screen space to the right of the left window and underneath the right window.

It may seem odd not to use Window_Help here, but I believe the reason is that usually the help windows are at the top of the screen and it's the first thing drawn so it doesn't need to have any data on other windows, but here it's the last thing so it needs to "know" where the others are, and Window_Help doesn't facilitate that since it needs the number of lines it will display as an argument.

def on_left_ok
    refresh_help_window
    @right_window.activate
    @right_window.select(0)
  end


This is the handler method for :ok on the left window. We call refresh_help_window, then activate @right_window and select index 0.

def on_right_cancel
    @left_window.activate
    @right_window.unselect
    @debug_help_window.contents.clear
  end


And this is the :cancel handler for the right window; first we activate @left_window, then unselect @right_window, and finally clear the contents of @debug_help_window.

def refresh_help_window
    @debug_help_window.draw_text_ex(4, 0, help_text)
  end


This method refreshes the help window, and to do so we call draw_text_ex on @debug_help_window, passing in 4 for the x coordinate, 0 for the y, and help_text for the text. This will draw help_text at the top left of the window. What is that going to be? Well...

def help_text
    if @left_window.mode == :switch
      "C (Enter) : ON / OFF"
    else
      "← (Left)    :  -1\n" +
      "→ (Right)   :  +1\n" +
      "L (Pageup)   : -10\n" +
      "R (Pagedown) : +10"
    end
  end


This. So if @left_window's mode is :switch, which it will be if you have a switch selected, it will show that you can hit C (or enter) to turn that switch ON or OFF. Otherwise (meaning a variable is selected) it will show that you can hit left for -1, right for +1, page up for -10 and page down for +10.

And that's all you need for the debug scene! Now on to the mother of all scenes, the Big Kahuna.

Scene_Battle
This one is, obviously, the scene that handles battle processing. And there's a lot of stuff that goes into a battle, as evidenced by the class for it being almost as long as the interpreter itself. It inherits from Scene_Base.

def start
    super
    create_spriteset
    create_all_windows
    BattleManager.method_wait_for_message = method(:wait_for_message)
  end


In the start method, we call the parent (which creates the main viewport) and then we call create_spriteset to create our battle spriteset, create_all_windows to create the windows you'll see on screen, and we set BattleManager's method_wait_for_message to the wait_for_message method.

def post_start
    super
    battle_start
  end


And for post_start, we call the parent method (which performs the transition and updates Input) and then call battle_start, which is what truly gets the ball rolling for the combat processing.

def pre_terminate
    super
    Graphics.fadeout(30) if SceneManager.scene_is?(Scene_Map)
    Graphics.fadeout(60) if SceneManager.scene_is?(Scene_Title)
  end


In pre_terminate, we call the parent method (which doesn't actually do anything so this line isn't needed) then if SceneManager's scene_is? method returns true when passing in Scene_Map, we call Graphics' fadeout method passing in 30 for the number of frames argument, and if scene_is? returns true when passing in Scene_Title, we pass in 60 to fadeout instead.

The only reason I can think of for this check is to account for a battle event having a "Return to Title Screen" command in it.

def terminate
    super
    dispose_spriteset
    @info_viewport.dispose
    RPG::ME.stop
  end


For the terminate method, we once again call the parent method (which freezes Graphics, disposes all windows and disposes of the main viewport) after which we call dispose_spriteset, dispose @info_viewport, and call stop from the RPG::ME module, which will stop any playing ME audio.

def update
    super
    if BattleManager.in_turn?
      process_event
      process_action
    end
    BattleManager.judge_win_loss
  end


Here we overwrite the update method. We call the parent method, then if BattleManager's in_turn? method returns true (which it will if its @phase instance variable is set to :turn) we call process_event and then process_action. Either way, we then call BattleManager's judge_win_loss method.

def update_basic
    super
    $game_timer.update
    $game_troop.update
    @spriteset.update
    update_info_viewport
    update_message_open
  end


We also need to overwrite update_basic with a few more bits of functionality. First, as usual, we call the parent method, which updates Graphics, Input and the windows. Then we update $game_timer (which will decrement the timer by 1 frame and call its expire method if it's reached 0) and $game_troop (which just updates the screen) as well as the @spriteset (which updates battlebacks, enemies, actors, pictures, the timer sprite and viewports), and finally call update_info_viewport and update_message_open.

def update_for_wait
    update_basic
  end


update_for_wait is a method which determines what gets updated when we're "waiting" and here we just call update_basic.

def wait(duration)
    duration.times {|i| update_for_wait if i < duration / 2 || !show_fast? }
  end


The wait method performs a wait for a number of frames, taking duration as a parameter. We run a loop for as many times as the passed-in value using iteration variable "i", and in the block for it we call update_for_wait is i is less than half duration OR if show_fast? returns false.

Basically this means that while waiting, no actions or events will process and only the timer/graphics will update each frame. If the player is holding the "fast forward" key, the wait will be over in half its duration.

def show_fast?
    Input.press?(:A) || Input.press?(:C)
  end


The aforementioned "fast forward" key is either :A or :C (which map to Shift and Space/Enter/Z by default) so we return true if Input's press? method returns true passing in either :A OR :C.

def abs_wait(duration)
    duration.times {|i| update_for_wait }
  end


We also have abs_wait, a method that's similar to wait but doesn't allow for fast forwarding. So again we take duration as a parameter, and again we run a loop for as many times as the value passed, but all we do in the block is call update_for_wait.

def abs_wait_short
    abs_wait(15)
  end


abs_wait_short is just a wrapper method which performs a short wait with no fast forward, and here we're just calling abs_wait passing in 15 for the duration. This will call nothing but update_basic for 15 frames.

def wait_for_message
    @message_window.update
    update_for_wait while $game_message.visible
  end


This method waits for a message to finish displaying. We call update on _message_window and then call update_for_wait while the visible property of $game_message is true (this is what stops other stuff from happening until you hit enter to close the message window)

def wait_for_animation
    update_for_wait
    update_for_wait while @spriteset.animation?
  end


This method waits for an animation to finish playing. First we call update_for_wait and then we call update_for_wait while @spriteset's animation? method returns true (which it will if any battler with a sprite is displaying an animation). The first call just ensures that there will be at least 1 frame where update_for_wait is called when this method is.

def wait_for_effect
    update_for_wait
    update_for_wait while @spriteset.effect?
  end


This is just the same but for effects rather than animations (effects being things like whiten, blink, appear, disappear, boss collapse etc).

def update_info_viewport
    move_info_viewport(0)   if @party_command_window.active
    move_info_viewport(128) if @actor_command_window.active
    move_info_viewport(64)  if BattleManager.in_turn?
  end


This method updates the info viewport, as the name suggests. We have 3 potential calls to move_info_viewport, each with a condition and the only difference being the argument passed to it. We pass 0 to it if @party_command_window is active, 128 if @actor_command_window is active, and 64 is BattleManager has started processing a turn.

def move_info_viewport(ox)
    current_ox = @info_viewport.ox
    @info_viewport.ox = [ox, current_ox + 16].min if current_ox < ox
    @info_viewport.ox = [ox, current_ox - 16].max if current_ox > ox
  end


This method is the one that moves the info viewport, as called above, and it takes ox as a parameter. First we set current_ox to the ox (origin X) of @info_viewport. Then if current_ox is less than the passed-in ox, we set it to the minimum value between ox and current_ox plus 16. If current_ox is greater than ox, we set it to the minimum value between ox and current_ox minus 16.

This is what creates the "slide" effect with the command/status windows at the bottom of the battle screen. If you consider that the party command window starts at ox 0, when you switch to the actor command window it moves to 128. When the current ox is 0, this means that current_ox is less than ox and so @info_viewport's ox is set to the minimum between 128 and 0 + 16, which results in 16. The next frame, current_ox is 16 and it's set to the minimum between 128 and 16 + 16, which results in 32. And so on and so forth until it reaches 128, at which point it's no longer less than or greater than ox, so the update won't move it any more.

If you've forgotten, the origin X basically defines which part of the viewport's display area is considered to be at "0" or the far left of the rectangle. So if ox is 128, then the far left of the viewport display will show the pixels 128 pixels to the right of 0 in the actual image being displayed. This causes the party command window to look like it's sliding over to the left when the actor command window activates, but really we're sliding the viewport viewing area itself; the rectangle never moves.

def update_message_open
    if $game_message.busy? && !@status_window.close?
      @message_window.openness = 0
      @status_window.close
      @party_command_window.close
      @actor_command_window.close
    end
  end


This method updates the opening of the message window. If $game_message's busy? method returns true AND the close? method of @status_window returns false, we set @message_window's opennness to 0 and then close @status_window, @party_command_window and @actor_command_window.

This will prevent the openness of the message window from increasing until the status window has closed.

def create_spriteset
    @spriteset = Spriteset_Battle.new
  end


This method creates the spriteset, which contains all of the graphical elements which will be shown in the battle. All we're doing there is setting @spriteset to a new instance of Spriteset_Battle.

def create_all_windows
    create_message_window
    create_scroll_text_window
    create_log_window
    create_status_window
    create_info_viewport
    create_party_command_window
    create_actor_command_window
    create_help_window
    create_skill_window
    create_item_window
    create_actor_window
    create_enemy_window
  end


This method creates all the battle windows, and boy howdy are there a lot of them. This set of method calls will create the message window, the scrolling text window, the battle log window, the status window, the information viewport, the party and actor command windows, the help window, the skill and item selection windows, and the actor and enemy selection windows.

def create_message_window
    @message_window = Window_Message.new
  end


We start off fairly simply with the method for creating the message window, in which we just set @message_window to a new instance of Window_Message.

def create_log_window
    @log_window = Window_BattleLog.new
    @log_window.method_wait = method(:wait)
    @log_window.method_wait_for_effect = method(:wait_for_effect)
  end


Creating the log window is a little more involved. We set @log_window to a new instance of Window_BattleLog, then set its method_wait property to the "wait" method, and its method_wait_for_effect property to "wait_for_effect".

def create_status_window
    @status_window = Window_BattleStatus.new
    @status_window.x = 128
  end


For creating the status window, we set @status_window to a new instance of Window_BattleStatus, then set its x coordinate to 128 (which places it to the right of the party command window, which is 128 pixels wide)

def create_info_viewport
    @info_viewport = Viewport.new
    @info_viewport.rect.y = Graphics.height - @status_window.height
    @info_viewport.rect.height = @status_window.height
    @info_viewport.z = 100
    @info_viewport.ox = 64
    @status_window.viewport = @info_viewport
  end


This method creates the information viewport, which is the section at the bottom of the screen housing the party command window, the status window, the actor command window, and the actor/enemy selection windows.

We set @info_viewport to a new instance of Viewport, then set its rect's y coordinate to the game window height minus the height of @status_window, and its rect's height to @status_window's height. In other words, our viewport rect takes up the entire area below the main battle display. We also set the viewport's z to 100, and its ox to 64, and then set @status_window's viewport to @info_viewport.

The ox being 64 at the start creates a very subtle blink-and-you'll-miss-it effect where the command and status windows slide into place when battle begins due to update_info_viewport gradually moving it to 0.

def create_party_command_window
    @party_command_window = Window_PartyCommand.new
    @party_command_window.viewport = @info_viewport
    @party_command_window.set_handler(:fight,  method(:command_fight))
    @party_command_window.set_handler(:escape, method(:command_escape))
    @party_command_window.unselect
  end


This method creates the party command window. We set @party_command_window to a new instance of Window_PartyCommand. We set its viewport to @info_viewport and then set a couple of handlers for it. The handler for the :fight symbol is set to the command_fight method, and the handler for the :escape symbol is set to command_escape.

def create_actor_command_window
    @actor_command_window = Window_ActorCommand.new
    @actor_command_window.viewport = @info_viewport
    @actor_command_window.set_handler(:attack, method(:command_attack))
    @actor_command_window.set_handler(:skill,  method(:command_skill))
    @actor_command_window.set_handler(:guard,  method(:command_guard))
    @actor_command_window.set_handler(:item,   method(:command_item))
    @actor_command_window.set_handler(:cancel, method(:prior_command))
    @actor_command_window.x = Graphics.width
  end


This method creates the actor command window. We set @actor_command_window to a new instance of Window_ActorCommand, and its viewport to @info_viewport. We set the handler for the :attack symbol to command_attack, for the :skill symbol to command_skill, for :guard to command_guard, :item to command_item, and :cancel to prior_command. Finally, we set the window's x coordinate to the width of the game window (which places it off-screen to the right of the status window initially)

def create_help_window
    @help_window = Window_Help.new
    @help_window.visible = false
  end


This method creates the help window. We set @help_window to a new instance of Window_Help and then set its visible property to false, as we don't want to see it immediately.

def create_skill_window
    @skill_window = Window_BattleSkill.new(@help_window, @info_viewport)
    @skill_window.set_handler(:ok,     method(:on_skill_ok))
    @skill_window.set_handler(:cancel, method(:on_skill_cancel))
  end


This method creates the skill selection window. We set @skill_window to a new instance of Window_BattleSkill, passing in @help_window as the help_window argument, and @info_viewport as the viewport. Then we set the :ok handler to on_skill_ok, and the :cancel handler to on_skill_cancel.

def create_item_window
    @item_window = Window_BattleItem.new(@help_window, @info_viewport)
    @item_window.set_handler(:ok,     method(:on_item_ok))
    @item_window.set_handler(:cancel, method(:on_item_cancel))
  end


And surprising nobody the item window is exactly the same, only it's a Window_BattleItem instead. Obviously the instance variable name and the handler methods are changed to use item rather than skill.

def create_actor_window
    @actor_window = Window_BattleActor.new(@info_viewport)
    @actor_window.set_handler(:ok,     method(:on_actor_ok))
    @actor_window.set_handler(:cancel, method(:on_actor_cancel))
  end


This method creates the actor selection window. We set @actor_window to a new instance of Window_BattleActor, passing in @info_viewport as the viewport argument. Then we set the :ok handler to on_actor_ok, and the :cancel handler to on_actor_cancel.

def create_enemy_window
    @enemy_window = Window_BattleEnemy.new(@info_viewport)
    @enemy_window.set_handler(:ok,     method(:on_enemy_ok))
    @enemy_window.set_handler(:cancel, method(:on_enemy_cancel))
  end


And just as skills and items are pretty much the same thing, so are actors and enemies, so to create the enemy selection window we just do the same thing but replacing actor with enemy.

def refresh_status
    @status_window.refresh
  end


This method refreshes the status display, and all we're doing in it is calling @status_window's refresh method.

def next_command
    if BattleManager.next_command
      start_actor_command_selection
    else
      turn_start
    end
  end


This method moves to the next actor's command input. If BattleManager's next_command method returns true, we call start_actor_command_selection. Otherwise, we call turn_start.

Basically, "set BattleManager's actor index to the next number up, and then start the turn if the index is higher than the party member count".

def prior_command
    if BattleManager.prior_command
      start_actor_command_selection
    else
      start_party_command_selection
    end
  end


This one is pretty much the same, but when prior_command returns false we call start_party_command_selection, as hitting cancel on the first party member's command input takes us back to the party command window.

def start_party_command_selection
    unless scene_changing?
      refresh_status
      @status_window.unselect
      @status_window.open
      if BattleManager.input_start
        @actor_command_window.close
        @party_command_window.setup
      else
        @party_command_window.deactivate
        turn_start
      end
    end
  end


And this is the method that does that. Unless scene_changing? returns true, we refresh the status window, then unselect and open it. If BattleManager's input_start method returns true (which it will if the battle isn't a surprise attack and at least one actor is able to input actions) we close @actor_command_window and call the setup method of @party_command_window. Otherwise (meaning it is a surprise attack or that no actor can move) we deactivate @party_command_window and call turn_start to start the turn.

def command_fight
    next_command
  end


command_fight is the handler method for the "Fight" command, and all we do there is call next_command, which will either open up the command window for the first actor who can act, or start the turn if none of them can.

def command_escape
    turn_start unless BattleManager.process_escape
  end


This method handles the "Escape" command. We call turn_start unless BattleManager's process_escape method returns true. In other words, an attempt will be made to escape from battle and end it, but if that fails the turn will start and allow the enemies a turn without the party doing anything.

def start_actor_command_selection
    @status_window.select(BattleManager.actor.index)
    @party_command_window.close
    @actor_command_window.setup(BattleManager.actor)
  end


This method starts the actor command selection. We call @status_window's select method, passing in BattleManager's actor's index as the index argument, then close @party_command_window and setup on @actor_command_window, passing in BattleManager's actor as the actor argument. This will highlight in the status window the actor who's acting, and show us the actor commands for that actor.

def command_attack
    BattleManager.actor.input.set_attack
    select_enemy_selection
  end


This method handles the "Attack" command for an actor. We call the set_attack method of BattleManager's actor's input, and then select_enemy_selection.

Basically, we get the current actor from BattleManager, then get that actor's current action, and set it up as a normal attack (skill 1). Then we open up the window for them to select a target for the attack.

def command_skill
    @skill_window.actor = BattleManager.actor
    @skill_window.stype_id = @actor_command_window.current_ext
    @skill_window.refresh
    @skill_window.show.activate
  end


This method handles the "Skill" command for an actor. We set @skill_window's actor to BattleManager's actor, and its stype_id to the current_ext of @actor_command_window. Then we refresh it, and finally show and activate it.

current_ext is used to create a link between the command and the skill type it represents, which was passed when creating the command in the Window_ActorCommand class. This is how "Special" opens up the list of special skills, "Magic" shows magic skills and so on. Refreshing the window means it will show the skill list for the current actor.

def command_guard
    BattleManager.actor.input.set_guard
    next_command
  end


This method handles the "Guard" command. All we're doing here is calling set_guard on BattleManager's actor's input action, and then calling next_command.

def command_item
    @item_window.refresh
    @item_window.show.activate
  end


command_item handles the "Item" command. Here, we refresh @item_window and then show and activate it.

def select_actor_selection
    @actor_window.refresh
    @actor_window.show.activate
  end


This method begins the selection of an actor as a target (for healing spells, buffs etc.) We refresh @actor_window, then show and activate it.

def on_actor_ok
    BattleManager.actor.input.target_index = @actor_window.index
    @actor_window.hide
    @skill_window.hide
    @item_window.hide
    next_command
  end


This is the method that handles hitting the :ok key with an actor selected in the actor selection window.

We set BattleManager's actor's input action's target_index to @actor_window's index. Then we hide @actor_window, @skill_window and @item_window (easier to just hide all 3 since we don't have a reference here to which one was open when the actor selection came into play; we could check, but there's no downside to hiding a window that's already hidden so there's no point) and then we call next_command to move to the next actor.

def on_actor_cancel
    @actor_window.hide
    case @actor_command_window.current_symbol
    when :skill
      @skill_window.activate
    when :item
      @item_window.activate
    end
  end


In the cancel handler, though, we do need to check. First of all, no matter what, we hide @actor_window. Then we have a case statement branching based on the current_symbol of @actor_command_window; if the symbol was :skill (meaning we got to the actor selection window from picking a skill to use) we activate @skill_window. It it's :item, we activate @item_window instead.

def select_enemy_selection
    @enemy_window.refresh
    @enemy_window.show.activate
  end


Not much to say here. The method for starting enemy selection is exactly the same as the actor one but using a different window.

def on_enemy_ok
    BattleManager.actor.input.target_index = @enemy_window.enemy.index
    @enemy_window.hide
    @skill_window.hide
    @item_window.hide
    next_command
  end


Same deal here. Other than using the enemy window, this is identical to on_actor_ok.

def on_enemy_cancel
    @enemy_window.hide
    case @actor_command_window.current_symbol
    when :attack
      @actor_command_window.activate
    when :skill
      @skill_window.activate
    when :item
      @item_window.activate
    end
  end


And this one is aaaaalmost the same, but we have to add in an additional when clause in the case statement for the :attack symbol, which activates @actor_command_window (we didn't need this in on_actor_cancel because party members can't attack allies...at least not in the default engine).

def on_skill_ok
    @skill = @skill_window.item
    BattleManager.actor.input.set_skill(@skill.id)
    BattleManager.actor.last_skill.object = @skill
    if !@skill.need_selection?
      @skill_window.hide
      next_command
    elsif @skill.for_opponent?
      select_enemy_selection
    else
      select_actor_selection
    end
  end


This method handles hitting :ok while highlighting a skill in the skill list. We set @skill to @skill_window's item. Then we call set_skill on BattleManager's actor's input action, passing in @skill's id, and we also set the actor's last_skill's object to @skill. If @skill's need_selection? method returns false, we hide @skill_window and call next_command. Otherwise, if @skill's for_opponent? method returns true, we call select_enemy_selection to start selecting an enemy. And otherwise, we call select_actor_selection, as the only possibility remaining is that the skill targets an actor.

So let's say the player is choosing the turn action for their healer, and they've chosen "Heal" (skill 26 in the default database). @skill_window's item is the Game_Item containing the data for the Heal skill, and this is assigned to the @skill variable. We pass 26 to set_skill, meaning the current action will also point to the Heal skill, and last_skill's object is set to that Game_Item. need_selection? will return true, since Heal targets one ally. for_opponent? will return false, so we start actor selection.

def on_skill_cancel
    @skill_window.hide
    @actor_command_window.activate
  end


This method handles hitting cancel while selecting a skill. Here, we hide @skill_window and activate @actor_command_window.

def on_item_ok
    @item = @item_window.item
    BattleManager.actor.input.set_item(@item.id)
    if !@item.need_selection?
      @item_window.hide
      next_command
    elsif @item.for_opponent?
      select_enemy_selection
    else
      select_actor_selection
    end
    $game_party.last_item.object = @item
  end


This one is very similar to on_skill_ok but with a couple of differences. Since items belong to the entire party rather than a specific actor, we're setting $game_party's last_item object. Besides that and using "item" rather than "skill", it's functionally identical.

def on_item_cancel
    @item_window.hide
    @actor_command_window.activate
  end


As is the cancel handler, in which we just hide @item_window and activate @actor_command_window.

def battle_start
    BattleManager.battle_start
    process_event
    start_party_command_selection
  end


battle_start is the method called...well, when the battle starts. You may remember the call for this was way back in the beginning of the class, in post_start. Anyway, what are we doing here? Well, first we call BattleManager's battle_start method, which increases the number of battles the player has fought, processes on battle start for the actors and enemies, creates the enemy names if there are duplicates, and shows messages for pre-emptive/surprise attacks. Then we call process_event (which we'll see very soon) and finally we call start_party_command_selection, which either opens up on the party command window or immediately starts the turn if it's a surprise attack or the party contains no actors who can select actions.

def turn_start
    @party_command_window.close
    @actor_command_window.close
    @status_window.unselect
    @subject =  nil
    BattleManager.turn_start
    @log_window.wait
    @log_window.clear
  end


This method begins a turn once all applicable actions have been chosen. We close @party_command_window and @actor_command_window as we don't need them any more, and unselect @status_window to remove the highlight on party members. We set @subject to nil since we're no longer choosing actions, then call BattleManager's turn_start method, which increases the turn count and determines the battler order. Finally, we wait for the log window to finish displaying its text and then clear it.

def turn_end
    all_battle_members.each do |battler|
      battler.on_turn_end
      refresh_status
      @log_window.display_auto_affected_status(battler)
      @log_window.wait_and_clear
    end
    BattleManager.turn_end
    process_event
    start_party_command_selection
  end


This method processes the end of a turn. First, we run an each loop on all_battle_members using iteration variable "battler" and in that loop we call each battler's on_turn_end method, then refresh_status. We call display_auto_affected_status in @log_window passing in battler, and then we call its wait_and_clear method to wait for the text to finish and then clear the log.

After the loop, we call BattleManager's turn_end method, then process_event, and finally we call start_party_command_selection to return us to the party commands.

def all_battle_members
    $game_party.members + $game_troop.members
  end


Here we see how all_battle_members is formed: we're returning $game_party's members plus $game_troop's members. The beauty of Ruby arrays is that you can perform arithmetic operations on them just like you can with numbers, so this is just adding two arrays to each other to form one made up of all elements of both.

def process_event
    while !scene_changing?
      $game_troop.interpreter.update
      $game_troop.setup_battle_event
      wait_for_message
      wait_for_effect if $game_troop.all_dead?
      process_forced_action
      BattleManager.judge_win_loss
      break unless $game_troop.interpreter.running?
      update_for_wait
    end
  end


This method processes events, and is integral to battle events working the way they do.

While scene_changing? is false, we update $game_troop's interpreter, and call its setup_battle_event method (which checks all troop event pages for events which have met their conditions). We call wait_for_message in case text was being shown, then wait_for_effect if $game_troop's all_dead? method returns true (meaning the entire troop has been killed). Then we call process_forced_action to process any forced actions, then BattleManager's judge_win_loss method to figure out if the battle was won or lost yet and then we break out of the loop unless $game_troop's interpreter's running? method returns true. Finally, we call update_for_wait.

def process_forced_action
    if BattleManager.action_forced?
      last_subject = @subject
      @subject = BattleManager.action_forced_battler
      BattleManager.clear_action_force
      process_action
      @subject = last_subject
    end
  end


This method processes a forced action. If BattleManager's action_forced? method returns true (meaning an action is being forced) we set last_subject to @subject, and @subject to BattleManager's action_forced_battler, which will return the battler whose action is being forced. We call BattleManager's clear_action_force to clear the action force flag, then call process_action to actually process the forced action, and finally we set @subject to last_subject to return to the subject who was going to act before the forced action happened.

def process_action
    return if scene_changing?
    if !@subject || !@subject.current_action
      @subject = BattleManager.next_subject
    end
    return turn_end unless @subject
    if @subject.current_action
      @subject.current_action.prepare
      if @subject.current_action.valid?
        @status_window.open
        execute_action
      end
      @subject.remove_current_action
    end
    process_action_end unless @subject.current_action
  end


This method processes an action. We return if scene_changing? returns true, as there's no point in continuing to process actions if we're leaving the battle scene. If @subject returns nil OR @subject has no current action, we set @subject to BattleManager's next_subject (basically, if there isn't a current acting battler or the current battler has no action to take, we move to the next one). We return turn_end unless there is now a @subject, since that would mean all applicable battlers have acted and we need to finish the turn now. If @subject's current_action is not nil, we call its prepare method. Then if the valid? method of @subject's current_action returns true, we open @status_window and call execute_action. After this check, we call remove_current_action on @subject to remove that action from their pool since it's been executed now. Then finally we call process_action_end unless @subject has a current_action, meaning that even after removing the one that just got executed they have more actions to take.

def process_action_end
    @subject.on_action_end
    refresh_status
    @log_window.display_auto_affected_status(@subject)
    @log_window.wait_and_clear
    @log_window.display_current_state(@subject)
    @log_window.wait_and_clear
    BattleManager.judge_win_loss
  end


This method processes the end of an action. First we call @subject's on_action_end method and refresh_status. Then we call @log_window's display_auto_affected_status method passing in @subject to display any states which were automatically applied by the action, then wait_and_clear to clear it afterwards. After that we call display_current_state to update the messages of any states present, followed by another call to wait_and_clear. Finally, we call BattleManager's judge_win_loss method to check whether the action resulted in winning or losing the battle.

def execute_action
    @subject.sprite_effect_type = :whiten
    use_item
    @log_window.wait_and_clear
  end


This method executes an action. We set @subject's sprite_effect_type to :whiten, which will cause its sprite to flash white briefly, then we call use_item, and finally we call wait_and_clear on @log_window to give any logged text a chance to display before disappearing.

def use_item
    item = @subject.current_action.item
    @log_window.display_use_item(@subject, item)
    @subject.use_item(item)
    refresh_status
    targets = @subject.current_action.make_targets.compact
    show_animation(targets, item.animation_id)
    targets.each {|target| item.repeats.times { invoke_item(target, item) } }
  end


This method uses an item (though it also applies to skills, so here we mean item in the general sense rather than in the "potion" sense). We set item to @subject's current_action's item object, and call @log_window's display_use_item method passing in @subject as the subject and item as the item, which will show the name of the skill/item being used. We then call @subject's use_item method passing in item, which will pay any costs, consume the item if a consumable item was used, and call any common events associated with the item. We refresh_status to reflect changes in resources etc., and set targets to the compacted result of calling @subject's current_action's make_targets method, which gives us an array of the action's targets without any nil entries. We call show_animation passing in targets as the targets and the item's animation_id as the animation ID, and then for each target we call invoke_item a number of times equal to the item's repeats property, passing in target as the target and item as the item.

def invoke_item(target, item)
    if rand < target.item_cnt(@subject, item)
      invoke_counter_attack(target, item)
    elsif rand < target.item_mrf(@subject, item)
      invoke_magic_reflection(target, item)
    else
      apply_item_effects(apply_substitute(target, item), item)
    end
    @subject.last_target_index = target.index
  end


And here we have invoke_item, which as we've just seen takes target and item as parameters.

If a random number from 0 to 1 is less than the result of target's item_cnt method when @subject and item are passed to it (in other words, the chance of the target counterattacking) we call invoke_counter_attack, passing in target as the target and item as the item. Otherwise, if a random number from 0 to 1 is less than the result of target's item_mrf with @subject and item passed to it, we call invoke_magic_reflection passing in target and item, as this means the target reflected a magic attack. Otherwise, we call apply_item_effects, passing in as target the result of calling apply_substitute passing in target and item, and item as the item. This ensures that the target is already replaced with a substitute battler when effects are applied, in cases where a substitute is going to take the target's place. Finally, we set @subject's last_target_index to target's index.

def apply_item_effects(target, item)
    target.item_apply(@subject, item)
    refresh_status
    @log_window.display_action_results(target, item)
  end


This method applies item effects, taking target and item as parameters. First we call target's item_apply method, passing in @subject as the user and item as the item. We refresh_status to reflect any chances in HP, MP etc. Then we call @log_window's display_action_results method, passing in target as the target and item as the item.

def invoke_counter_attack(target, item)
    @log_window.display_counter(target, item)
    attack_skill = $data_skills[target.attack_skill_id]
    @subject.item_apply(target, attack_skill)
    refresh_status
    @log_window.display_action_results(@subject, attack_skill)
  end


This method invokes a counterattack, taking target and item as parameters. First we call the display_counter method of @log_window, passing in target as the target and item as the item, which will display in the battle log that a counterattack has occurred. We set attack_skill to the element of $data_skills with an ID corresponding to target's attack_skill_id (which is almost always going to be 1), then call @subject's item_apply method, passing in target as the user and attack_skill as the item (which will apply a normal attack from the target on the subject). We call refresh_status to reflect HP changes etc. and then once again call display_action_results from @log_window.

def invoke_magic_reflection(target, item)
    @subject.magic_reflection = true
    @log_window.display_reflection(target, item)
    apply_item_effects(@subject, item)
    @subject.magic_reflection = false
  end


This method invokes magic reflection, whereby the target reflects a spell back at the user, and takes target and item as parameters. We set @subject's magic_reflection property to true, then call display_reflection on @log_window passing in target as the target and item as the item, which will show that a magic reflection occurred. We call apply_item_effects passing in @subject as the target and item as the item, then set @subject's magic_reflection to false.

So why do we set this flag? Following the chain of method called from apply_item_effects onwards, you'll see that this is used to determine opposite? for the purposes of applying things like states etc. In other words, the flag temporarily tells the battler that they count as a hostile unit, which means state rates and luck effects will influence the reflected spell as if it had been cast on an enemy rather than an ally.

def apply_substitute(target, item)
    if check_substitute(target, item)
      substitute = target.friends_unit.substitute_battler
      if substitute && target != substitute
        @log_window.display_substitute(substitute, target)
        return substitute
      end
    end
    target
  end


This method applies the substitute effect, taking target and item as parameters.

If check_substitute returns true with target and item passed to it, we set substitute to the result of target's friends_unit's substitute_battler method, which gets the first party member who has a "substitute" trait. If there is a substitute and the substitute isn't the target, we call @log_window's display_substitute method passing in substitute and target, and return substitute. This will result in the substitute actor standing in to take damage and other effects on behalf of the original target.

def check_substitute(target, item)
    target.hp < target.mhp / 4 && (!item || !item.certain?)
  end


And in this method, we check whether a substitute can occur. It takes target and item as parameters.

We return true if target's HP is less than a quarter of their maximum AND there is either no item or the item is not "certain hit".

This tells us two important things: one, that an actor will only substitute for allies at 25% HP or less. Secondly, they cannot do this against a "certain hit" effect; it will only happen for physical or magical skills/items.

def show_animation(targets, animation_id)
    if animation_id < 0
      show_attack_animation(targets)
    else
      show_normal_animation(targets, animation_id)
    end
    @log_window.wait
    wait_for_animation
  end


This method shows an animation on a set of targets, taking targets and animation_id as parameters. If animation_id is less than 0 (meaning it's -1, which denotes it as a normal attack) we call show_attack_animation passing in targets. Otherwise, we call show_normal_animation passing in targets and animation_id. Either way, we call @log_window's wait method and then call wait_for_animation.

def show_attack_animation(targets)
    if @subject.actor?
      show_normal_animation(targets, @subject.atk_animation_id1, false)
      show_normal_animation(targets, @subject.atk_animation_id2, true)
    else
      Sound.play_enemy_attack
      abs_wait_short
    end
  end


This method shows the attack animation on a set of targets, taking targets as a parameter. If @subject is an actor, we call show_normal_animation on targets, passing in @subject's atk_animation_id1 and false for the mirror argument, and then call it again passing in targets, @subject's atk_animation_id2, and true for the mirror argument. Otherwise, we call the Sound module's play_enemy_attack method and then abs_wait_short to wait 15 frames.

The dual call to show_normal_animation is what causes a dual wielding actor to flip the animation of their second hit, which makes it a little more visually interesting.

def show_normal_animation(targets, animation_id, mirror = false)
    animation = $data_animations[animation_id]
    if animation
      targets.each do |target|
        target.animation_id = animation_id
        target.animation_mirror = mirror
        abs_wait_short unless animation.to_screen?
      end
      abs_wait_short if animation.to_screen?
    end
  end


And finally, we have show_normal_animation, which shows a normal animation on a set of targets, taking target, animation_id and mirror as parameters. mirror defaults to false if not supplied.

We set animation to the element of $data_animations at an index of animation_id. If animation contains data, we run an each loop on targets using iteration variable "target". In this loop, we set target's animation_id to the passed-in animation_id, its animation_mirror to the mirror argument, and then call abs_wait_short to wait 15 frames unless the animation's to_screen? method returns true, which it will if the animation is set to play on the whole screen rather than each target. After each target has been processed, we then call abs_wait_short if the animation plays on the whole screen.

What this will do is create a 15-frame wait between animations on each target when the animation hits each target individually, and otherwise it will just wait 15 frames at the end.

We are finally done! The behemoth that is Scene_Battle has been slain, and no mysteries remain to us. That leaves just one class.

Scene_Gameover
And it's a pretty short one. This scene is the one that comes up when your party fails miserably in battle and everyone is dead. It inherits from Scene_Base.

def start
    super
    play_gameover_music
    fadeout_frozen_graphics
    create_background
  end


First we overwrite the start method, starting by calling the parent method. We also call play_gameover_music, fadeout_frozen_graphics and create_background.

def terminate
    super
    dispose_background
  end


The terminate overwrite calls the parent method and then the dispose_background method.

def update
    super
    goto_title if Input.trigger?(:C)
  end


In the frame update method, we call the parent method and then call goto_title if the :C Input is triggered (meaning the player has hit enter).

def perform_transition
    Graphics.transition(fadein_speed)
  end


Here we overwrite the base perform_transition method, and we're still calling Graphics' transition method but we pass in fadein_speed rather than transition_speed, so that the game over screen fades in slightly slower than a regular scene change.

def play_gameover_music
    RPG::BGM.stop
    RPG::BGS.stop
    $data_system.gameover_me.play
  end


This method plays the game over music. First, we call the stop methods of both RPG::BGM and RPG::BGS to cut out any currently-playing background music or sound. Then we call the play method of $data_system's gameover_me.

def fadeout_frozen_graphics
    Graphics.transition(fadeout_speed)
    Graphics.freeze
  end


This method fades out the frozen graphics from the previous scene. We call Graphics' transition method passing in fadeout_speed, and then its freeze method.

def create_background
    @sprite = Sprite.new
    @sprite.bitmap = Cache.system("GameOver")
  end


This method creates the background. We set @sprite to a new instance of Sprite, and then set its bitmap to the cached GameOver system graphic.

def dispose_background
    @sprite.bitmap.dispose
    @sprite.dispose
  end


This method disposes of the background. Here we simply dispose @sprite's bitmap and then dispose @sprite itself.

def fadeout_speed
    return 60
  end


This method determines the fade out speed. Here we return 60, meaning the scene prior to the game over one takes 1 second to fade out fully.

def fadein_speed
    return 120
  end


And here we have the fade in speed. We return 120, so that fading the game over screen in takes 2 seconds.

def goto_title
    fadeout_all
    SceneManager.goto(Scene_Title)
  end


This method returns the player to the title screen. First we call fadeout_all, and then we call SceneManager's goto method passing in Scene_Title.

That's all for Scene_Gameover, and indeed for the default scripts! We've gone through every Module, every Game Object, every Window and every Scene. The only thing left is "main" with the line

rgss_main { SceneManager.run }


And that kicks off the whole process of the game running.

That's it. From the first episode of Slip into Ruby in July 2012, we've now covered every single line of code that makes up the base RPG Maker VX Ace engine. It's been a hell of a journey, and I can't tell you how grateful I am to everyone who's given me feedback and let me know how important this series was to their own journeys as indie game developers. I couldn't have predicted its impact or importance when I started, and the fact that people are still reading today something I began 8 years ago is frankly amazing.

So thank you, each and every reader who's made writing this series worthwhile. You are what has kept me going, and hopefully the series will be demystifying the engine for generations of users to come.

For those who jumped ship to RPG Maker MV and MZ, I still have Jump into Javascript going, but unless I think of something else to cover on the Ruby end, this series has reaches its conclusion. If anyone has any questions about any aspect of any Slip into Ruby episode or any of the code we've covered over the years, do feel free to email me at guitarherotrihan@gmail.com. If I get enough questions, I'll do a final Q&A episode to round things off.

But if that doesn't happen, I guess that's it from me, at least as far as Slip into Ruby is concerned.

Until...now.