SLIP INTO RUBY - UNDER THE HOOD PART 20: WINDOWS NO MORE

In which we finish the endless coverage of Window classes.

  • Trihan
  • 07/23/2020 12:09 AM
  • 2272 views
Hello, sports fans! I think you'll agree that this one had less of a gap than the previous. :P That's right, it's time for an actually-timely episode of



Today: When I'm Explainin' Windows

Window_MapName
As the name suggests, this is the window that displays the name of the map the player has entered. It inherits from Window_Base.

def initialize
    super(0, 0, window_width, fitting_height(1))
    self.opacity = 0
    self.contents_opacity = 0
    @show_count = 0
    refresh
  end


Pretty standard constructor. First we call the parent method with coordinates of (0, 0), a width of the return value from window_width and the fitting height for 1 line, then set self.opacity to 0 and self.contents_opacity to 0 (so that the window is completely transparent on creation), then set @show_count to 0 and finally call refresh.

def window_width
    return 240
  end


And here we see that window_width method, which returns 240. In other words, the map name window will be 240 pixels wide.

def update
    super
    if @show_count > 0 && $game_map.name_display
      update_fadein
      @show_count -= 1
    else
      update_fadeout
    end
  end


In the frame update method, first we call the parent update method. Then if @show_count is greater than zero AND the name_display flag of $game_map is true (which it always will be unless you turn the feature off using the appropriate event command) we call update_fadein and decrement @show_count by 1. Otherwise, we call update_fadeout.

def update_fadein
    self.contents_opacity += 16
  end


In the method for updating fadein, we simply increase self.contents_opacity by 16. As 255 is the maximum value, it will take 15.94 frames to get there, or just over a quarter of a second.

def update_fadeout
    self.contents_opacity -= 16
  end


This is the opposing counterpart to update_fadein, and just decreases self.contents_opacity by 16.

def open
    refresh
    @show_count = 150
    self.contents_opacity = 0
    self
  end


In the open method, we call refresh, set @show_count to 150, set self.contents_opacity to 0, and return self. Combining this with update, we can see that the map name window will remain on screen for 2.5 seconds before it starts to fade out.

def close
    @show_count = 0
    self
  end


In the opposing method for open, we just set @Show_count to 0 and return self. (this will cause update to start fading out instead of in)

def refresh
    contents.clear
    unless $game_map.display_name.empty?
      draw_background(contents.rect)
      draw_text(contents.rect, $game_map.display_name, 1)
    end
  end


In the refresh method, we clear the window contents (obviously so that we're not drawing over things every frame), and then unless the map's display name is empty we call draw_background passing in contents.rect and then draw_text passing in contents.rect, the map's display name, and 1 for the alignment (meaning it'll be centred).

The interesting thing about this is that it means the map window *always* shows and hides itself when you enter a new map, it's just that if the map hasn't been given a name the window has no background or text on it when it unhides. It would actually be slightly more efficient if we did this check in the update method (by adding the name requirement to the name_display flag setting in Game_Map)

def draw_background(rect)
    temp_rect = rect.clone
    temp_rect.width /= 2
    contents.gradient_fill_rect(temp_rect, back_color2, back_color1)
    temp_rect.x = temp_rect.width
    contents.gradient_fill_rect(temp_rect, back_color1, back_color2)
  end


In this method, we draw the background of the map name window; it takes rect as a parameter.

First we set a variable called temp_rect to a clone of the rect object passed in, and halve its width.

Then we call gradient_fill_Rect on the window contents, passing in temp_rect, back_color2 and back_color1. Then we set the x of temp_rect to its width, and call gradient_fill_rect again, this time reversing the colours.

"That's cool and all Trihan but what does that *do*?" I hear you ask, incredulously. And after the final two methods, I'll explain.

def back_color1
    Color.new(0, 0, 0, 192)
  end

def back_color2
    Color.new(0, 0, 0, 0)
  end


Look familiar? This is the same gradient pair as we saw in the message window's "dim background" option; and funnily enough, the map name window looks very much like that one...

Okay, so basically what we've done is created a rect that's half the width of the window contents, and gradient filled the contents from fully-transparent black to semi-transparent black. Then we've moved the rect to the X coordinate of its width, which puts it over at the right-hand side of the window. Then we gradient fill the contents from semi-transparent black to fully-transparent black. The end effect is a window that slowly fades into transparency at the left and right edges. Cool, huh?

And that's it for Window_MapName!

Window_BattleLog

Here's another fun one. This is the window that displays things happening in battle, like skill use and taking damage and the like. Let us dissect it under a microscope like crazed Ruby scientists! It inherits from Window_Base.

def initialize
    super(0, 0, window_width, window_height)
    self.z = 200
    self.opacity = 0
    @lines = []
    @num_wait = 0
    create_back_bitmap
    create_back_sprite
    refresh
  end


The constructor isn't anything special. We call the parent constructor, passing in coordinates of (0,0) and calls to window_width and window_height for the width and height. We set self.z to 200 so that the window will always appear on top. We set self.opacity to 0 so that the window frame will never be visible. We set @lines to an empty array and @num_wait to 0. Then we call create_back_bitmap, create_back_sprite, and refresh. We'll see what the instance variables and methods do shortly.

def dispose
    super
    dispose_back_bitmap
    dispose_back_sprite
  end


Again, we're not doing anything new or special in the dispose method. We call the parent method and then call dispose_back_bitmap and dispose_back_sprite.

def window_width
    Graphics.width
  end


In the window_width method we just return Graphics.width, so the battle log window will be the width of the screen.

def window_height
    fitting_height(max_line_number)
  end


In window_height we return the fitting height for max_line_number lines. How many is that? Well...

def max_line_number
    return 6
  end


...by default, we're saying 6. If you want to make the battle log longer, this is where to change it.

def create_back_bitmap
    @back_bitmap = Bitmap.new(width, height)
  end


In create_back_bitmap all we're doing is creating a new Bitmap the same width and height as the window and assigning it to @Back_bitmap.

def create_back_sprite
    @back_sprite = Sprite.new
    @back_sprite.bitmap = @back_bitmap
    @back_sprite.y = y
    @back_sprite.z = z - 1
  end


In create_back_sprite we assign a new Sprite to @back_sprite and set its bitmap property to @back_bitmap. We set its y coordinate to the y of the window, and its z coordinate to the window's z minus 1 (so that it always appears below the window)

def dispose_back_bitmap
    @back_bitmap.dispose
  end

def dispose_back_sprite
    @back_sprite.dispose
  end


In the disposal methods we're just calling dispose on @back_bitmap and @back_sprite respectively. Gotta free up that memory!

def clear
    @num_wait = 0
    @lines.clear
    refresh
  end


In the clear method, we set @num_wait to 0, clear @lines and finally call refresh. We still don't know what @num_wait does but the mystery will be solved soon!

def line_number
    @lines.size
  end


Now we don't see this often, but here's a method we haven't seen the call for yet! In line_number, we just return the size of the @lines array, which...oddly enough, tells us how many lines need to be displayed.

def back_one
    @lines.pop
    refresh
  end


In this method, which goes back one line, we call .pop on @lines (which removes the last element from the array and returns it) and then call refresh.

def back_to(line_number)
    @lines.pop while @lines.size > line_number
    refresh
  end


This method takes us back to a designated line and takes one parameter, the line_number. Similarly to the last method, we call .pop on @lines while the size of @lines is greater than the passed in number, and then call refresh.

def add_text(text)
    @lines.push(text)
    refresh
  end


This method adds a line of text to the battle log, taking "text" as a parameter. We simply push the passed-in string to the @lines array and then call refresh.

def replace_text(text)
    @lines.pop
    @lines.push(text)
    refresh
  end


This method replaces the most recent line with a different one, again taking text as a parameter. We call pop on the @lines array (which removes the last element from it and returns it) and then push passing in text, and finally call refresh. This effectively removes the last line added and then adds the new one.

def last_text
    @lines[-1]
  end


This method gets the text from the most recent line, simply returning the -1th element of @lines. Unlike a lot of other languages, Ruby has the super-helpful ability to access an array with a negative index, which just starts at the end and works backwards. This is effectively the same thing as "@lines[@lines.size - 1]".

def refresh
    draw_background
    contents.clear
    @lines.size.times {|i| draw_line(i) }
  end


The refresh method is fairly straightforward. We call the draw_background method, then call clear on the window's contents, then run a number of loops equal to the size of @lines where we call draw_line, passing in the current line with iteration variable "i" (starting at 0). .times is essentially a shorthand for loop that can be called directly on numbers (which in Ruby are objects like everything else is). So you could do "5.times" for example, which in other languages would be something like "for (int i = 0; i < 5; i++)".

def draw_background
    @back_bitmap.clear
    @back_bitmap.fill_rect(back_rect, back_color)
  end


The draw_background method just clears @back_bitmap and then calls fill_rect on it, passing in the results of the back_rect and back_color methods (which we're about to look at).

def back_rect
    Rect.new(0, padding, width, line_number * line_height)
  end


In back_rect, we return a new Rect object, passing in 0 as the upper-left X, padding as the upper-left Y, the width property as the width, and line_number * line_height as the height. As you may remember from the initialize method, width is just window_width (the full width of the game window) and line_number is just the size of @lines, which is when multiplied by 24 (assuming you haven't modified the default line height) to get the height for the background rectangle.

def back_color
    Color.new(0, 0, 0, back_opacity)
  end


back_color, as we've seen in some other windows that use this background type, is just black with an opacity of back_opacity, which is...

def back_opacity
    return 64
  end


...just 64. In other words, about a quarter opaque.

def draw_line(line_number)
    rect = item_rect_for_text(line_number)
    contents.clear_rect(rect)
    draw_text_ex(rect.x, rect.y, @lines[line_number])
  end


draw_line is the method for...well, drawing a line. It takes line_number as a parameter. First we set a temporary variable rect to the result of item_rect_for_text, passing in line_number (which gives us a rect at the position of the battle log we want to write into), then clear_rect in the contents passing in "rect" (which just clears out the rect on the window we've just created) and finally we call draw_text_ex passing in the X and Y of the rect for upper-left coordinate, and the line_numberth element of @lines as the text to draw.

def method_wait=(method)
    @method_wait = method
  end

def method_wait_for_effect=(method)
    @method_wait_for_effect = method
  end


These are what are known as setter methods, where you create a method that allows = assignment the same way as a variable does. In this case, we take "method" as a parameter and then set the instance variable @method_wait/@method_wait_for_effect to the passed-in method. As we'll see once we start looking at the Scene classes, this is intended to allow the battle log window to call Scene_Battle's own wait and wait_for_effect methods.

def wait
    @num_wait += 1
    @method_wait.call(message_speed) if @method_wait
  end

def wait_for_effect
    @method_wait_for_effect.call if @method_wait_for_effect
  end


In the window's own wait method, we add 1 to the @num_wait instance variable, then call the method stores in @method_wait passing in the result of message_speed, but only if @method_wait is not currently nil (in other words, a method has been provided that it's able to call). Again, we'll see more of this in Scene_Battle once we finally get to it.

wait_for_effect is exactly the same thing without the @num_wait increment.

def message_speed
    return 20
  end


message_speed is the value passed to the @method_wait call, and is hardcoded at 20. In other words, a wait will continue for 20 frames (a third of a second).

def wait_and_clear
    wait while @num_wait < 2 if line_number > 0
    clear
  end


wait_and_clear is, as the name suggests, a combination of wait and clear. So if line_number is greater than 0 (there is text to show), we call wait while @num_wait is less than 2. Obviously wait increments @num_wait by 1 every time it's called, and it's called once per frame, so this effectively waits 2 frames (1/30th of a second). We then call clear.

def display_current_state(subject)
    unless subject.most_important_state_text.empty?
      add_text(subject.name + subject.most_important_state_text)
      wait
    end
  end


This method displays the continuation message of the most important state affecting a character, taking "subject" as a parameter (which will be an instance of Game_Actor or Game_Enemy).

Unless the subject's most important state text is empty (in other words, there is no continuation text set for any of the states affecting the subject), we call add_text, passing in the subject's name concatenated with the most important state text (this will be the continuation text for the state with the highest priority) and then call wait.

Let's look at an example using the default VX Ace states. Let's say an actor is afflicted with sleep, paralysis and stun at the same time. Sleep has a priority of 85 and continuation text of " is sleeping......"; Paralysis has a priority of 90 and continuation text of " is unable to move!"; Stun has a priority of 60 and continuation text of " hasn't regained balance......"

When the states are sorted in order of priority, paralysis will come first. So if Eric has these three states, the method will add the text "Eric is unable to move!" Note that the continuation text in the database needs that space at the beginning because the concatenated string lacks one; if you just put "is unable to move!" in the database entry, the text would end up being "Ericis unable to move!". Adding spaces to strings that are intended to be joined together is an art unto itself sometimes.

Obviously the most_important_state_text method returns the highest-priority state text that isn't blank, so if Eric instead had Poison and Stun, although poison has a priority of 65, the stun text will display because Poison doesn't have any continuation message set.

def display_use_item(subject, item)
    if item.is_a?(RPG::Skill)
      add_text(subject.name + item.message1)
      unless item.message2.empty?
        wait
        add_text(item.message2)
      end
    else
      add_text(sprintf(Vocab::UseItem, subject.name, item.name))
    end
  end


Now we're getting into the fun stuff! We haven't seen sprintf since 2017 when we looked at Window_KeyItem, so you'd be forgiven for having forgotten how cool it is and I'll more than happily cover it again.

Okay, so this method is what shows that a paticular battler has used a particular skill or item. It takes two parameters, "subject" and "item". subject, as before, will be a Game_Actor or Game_Enemy. item will be a Game_BaseItem.

If the passed-in item is an RPG::Skill (or in other words...it's a skill we call add_text passing in the subject's name and the message1 property of the item. This is the text under "Using Message" in the skill database settings. For example, if the subject is Alice, and the item is the Cure II skill, this will add "Alice casts Cure II!" to the log. Then, unless message2 is empty, we call wait and then add_text again passing in message2. This is the input box underneath the main using message one and isn't used at all by default, but you can use it to have additional text when a skill is executed.

Otherwise (if the item is not a skill) we call add_text, passing in a formatted string using sprintf. The call to sprintf receives as its arguments Vocab::UseItem, the subject's name, and the item's name. The first argument is the original string, and the following arguments are used to determine what to replace the placeholders in that string with.

You've probably forgotten by now, but the UseItem property of the Vocab module is "%s uses %s!"; %s is a placeholder denoting a string, so wherever you see one it's essentially like saying "replace this with the next argument in the list as a string". What we end up with is "%s (subject.name) uses %s (item.name)!" In other words, if Eric just used a potion, this is converted to "Eric uses Potion!". The magic of string formatting!

def display_counter(target, item)
    Sound.play_evasion
    add_text(sprintf(Vocab::CounterAttack, target.name))
    wait
    back_one
  end


This method displays counterattacks in the log, taking as parameters "target" and "item". We call play_evasion from the Sound module, which plays the evading sound, then call add_text with a formatted string, passing in the CounterAttack property of Vocab and the target's name as arguments. Then we call wait, and finally back_one. This ensures that the counterattack notification doesn't stay on the log for long and that it doesn't replace more important messages.

Vocab::CounterAttack is "%s counterattacked!"; obviously the replacement for %s is the target's name because a counterattack is the target attacking the attacker back.

def display_reflection(target, item)
    Sound.play_reflection
    add_text(sprintf(Vocab::MagicReflection, target.name))
    wait
    back_one
  end


The method for displaying magic reflection is almost identical to the previous, but with a different sound effect and Vocab property.

Interestingly, neither of these methods actually need the item parameter as they do nothing with it. This is a pattern that will continue for quite a few methods in this class, which take an item parameter that is never used for anything.

def display_substitute(substitute, target)
    add_text(sprintf(Vocab::Substitute, substitute.name, target.name))
    wait
    back_one
  end


This method displays the substitute effect (when a battler takes the place of another as the target) and is very similar to the previous ones. We take "substitute" and "target" as parameters as no item is needed; the names of both substitute and target as passed to the sprintf call, and this one has no sound effect.

def display_action_results(target, item)
    if target.result.used
      last_line_number = line_number
      display_critical(target, item)
      display_damage(target, item)
      display_affected_status(target, item)
      display_failure(target, item)
      wait if line_number > last_line_number
      back_to(last_line_number)
    end


This method displays the results of an action, taking target and item as parameters.

If the "used" property of the target's result is true (meaning the item actually was used), we set last_line_number to line_number (in order words, the number of lines currently in the window). Then we call display_critical, display_damage, display_affected_status and display_failure. After this, we call wait if line_number is greater than last_line_number (which would mean text has been added by at least one of the called display methods) and finally we call back_to passing in last_line_number (to remove the added messages from the log).

There's an argument to be made that the decision to code it this way was a bad one: the display methods do check to make sure their related result property is true or contains data before executing the display instructions, but method calls in Ruby are quite expensive so it would probably have been better to include the if statements in this method and only call the other methods if they were necessary.

def display_failure(target, item)
    if target.result.hit? && !target.result.success
      add_text(sprintf(Vocab::ActionFailure, target.name))
      wait
    end
  end


This method displays action failure, and again takes target and item as parameters.

If the target's result was a hit (used and not missed or evaded) AND the target's success flag is false (meaning whatever the action was trying to do didn't work), we call add_text, passing in a formatted string for Vocab::ActionFailure and a placeholder replacement argument of the target's name. Finally, we call wait.

def display_critical(target, item)
    if target.result.critical
      text = target.actor? ? Vocab::CriticalToActor : Vocab::CriticalToEnemy
      add_text(text)
      wait
    end
  end


The method to display critical hits is almost identical to the previous one, only it doesn't need sprintf because the message has no placeholders in it. If the target is an actor, we set text to the CriticalToActor property of Vocab. Otherwise, we set it to the CriticalToEnemy property. Then we call add_text passing in text, and finally call wait.

By default, this will either result in "A painful blow!!" if an actor was hit, or "An excellent hit!!" if an enemy was.

def display_damage(target, item)
    if target.result.missed
      display_miss(target, item)
    elsif target.result.evaded
      display_evasion(target, item)
    else
      display_hp_damage(target, item)
      display_mp_damage(target, item)
      display_tp_damage(target, item)
    end
  end


This method displays damage effects, and as before takes target and item as parameters. If the missed flag of the target's result is true, we call display_miss passing in target and item; otherwise, is the evaded flag is true, we call display_evasion again passing in target and item. In any other case, we call display_hp_damage, display_mp_damage and display_tp_damage, each time passing in the same arguments.

This is another place where it may have been more efficient to do the if statements in the main method and only call the others if necessary to cut down on method calls.

def display_miss(target, item)
    if !item || item.physical?
      fmt = target.actor? ? Vocab::ActorNoHit : Vocab::EnemyNoHit
      Sound.play_miss
    else
      fmt = Vocab::ActionFailure
    end
    add_text(sprintf(fmt, target.name))
    wait
  end


This method displays a missed skill/item, taking target and item as parameters. If there is no item or the item has the "physical" hit type, we set fmt to Vocab::ActorNoHit if the target is an actor, or Vocab::EnemyNoHit otherwise, and call play_miss from the Sound module (which plays the miss SE). Otherwise, if there *is* an item and it is *not* physical, we set fmt to Vocab::ActionFailure. Then we call add_text with a formatted string, passing in fmt and the target's name. And finally, we call wait.

I can't actually think of any situations where !item would be true as there should always be an object passed in when this is called, but I assume they did it this way for a reason.

def display_evasion(target, item)
    if !item || item.physical?
      fmt = Vocab::Evasion
      Sound.play_evasion
    else
      fmt = Vocab::MagicEvasion
      Sound.play_magic_evasion
    end
    add_text(sprintf(fmt, target.name))
    wait
  end


This is more or less exactly the same thing but for evading a skill/item, so uses Vocab::Evasion and the play_evasion method instead. Rather then ActionFailure if there is no item or it isn't physical hit type, we use Vocab::MagicEvasion and play_magic_evasion, as this means the evaded item must have been magical in nature.

def display_hp_damage(target, item)
    return if target.result.hp_damage == 0 && item && !item.damage.to_hp?
    if target.result.hp_damage > 0 && target.result.hp_drain == 0
      target.perform_damage_effect
    end
    Sound.play_recovery if target.result.hp_damage < 0
    add_text(target.result.hp_damage_text)
    wait
  end


This method displays HP damage effects. We return if the hp_damage property of the target's result is 0 OR the item doesn't deal damage to HP. Then, if the hp_damage property is greater than 0 AND the hp_drain property of the target's result is 0 we call the target's perform_damage_effect method (which just blinks the battler/shakes the screen/plays the damage sound). Then, if the hp_damage is less than 0 (healing) we play the recovery sound effect, then call add_text passing in the hp_damage_text of target's result (which we looked at back in Game_ActionResult) and finally we call wait.

def display_mp_damage(target, item)
    return if target.dead? || target.result.mp_damage == 0
    Sound.play_recovery if target.result.mp_damage < 0
    add_text(target.result.mp_damage_text)
    wait
  end

def display_tp_damage(target, item)
    return if target.dead? || target.result.tp_damage == 0
    Sound.play_recovery if target.result.tp_damage < 0
    add_text(target.result.tp_damage_text)
    wait
  end


The MP damage display and TP damage display are a bit more straightforward. We return if the target is dead or the mp_damage/tp_damage properties are 0 (because there's no point in showing non-HP damage if the target's died, and if the damage value is 0 there's nothing to show). We play the recovery SE if the damage value is less than 0, then add_text for the appropriate damage text, and as with all the other methods we call wait.

def display_affected_status(target, item)
    if target.result.status_affected?
      add_text("") if line_number < max_line_number
      display_changed_states(target)
      display_changed_buffs(target)
      back_one if last_text.empty?
    end
  end


This method displays state changes, again taking target and item as parameters. If status_affected? on the target's result returns true (which it will if states have been addeed/removed), we call add_text passing in an empty string if line_number is less than max_line_number (meaning there will be a line between, for example, damage notifications and state updates)

def display_auto_affected_status(target)
    if target.result.status_affected?
      display_affected_status(target, nil)
      wait if line_number > 0
    end
  end


This method displays automatically affected status effects, taking only target as a parameter. It's essentially a wrapper for display_affected_status, which is called passing in "nil" for the item because the statuses already exist on the battler. Then we call wait if line_number is greater than 0, meaning there's text to show.

def display_changed_states(target)
    display_added_states(target)
    display_removed_states(target)
  end


This method displays states that have changed, taking target as a parameter. We simply call display_added_states and display_removed_states passing in the target value.

def display_added_states(target)
    target.result.added_state_objects.each do |state|
      state_msg = target.actor? ? state.message1 : state.message2
      target.perform_collapse_effect if state.id == target.death_state_id
      next if state_msg.empty?
      replace_text(target.name + state_msg)
      wait
      wait_for_effect
    end
  end


In the method for displaying added states, we start an "each" loop for the added_state_objects property or the target's result, using iteration variable "state".

First, state_msg is set to the state's message1 property if the target is an actor, or message2 otherwise. Then if the state ID matches the target's death state ID, we call perform_collapse_effect on the target. We move to the next iteration if state_msg is an empty string (meaning no message was set for it); if this isn't the case, we call replace_text passing in the target's name concatenated with state_msg, then call wait and wait_for_effect.

def display_removed_states(target)
    target.result.removed_state_objects.each do |state|
      next if state.message4.empty?
      replace_text(target.name + state.message4)
      wait
    end
  end


In the method for displaying removed states, we have a similar each loop for the target's result's removed_state_objects property. We move to the next iteration if the state's message4 property is an empty string (meaning there's no message when that state is removed); if this isn't the case, we call replace_text passing in the target's name concatenated with message4, then call wait.

def display_changed_buffs(target)
    display_buffs(target, target.result.added_buffs, Vocab::BuffAdd)
    display_buffs(target, target.result.added_debuffs, Vocab::DebuffAdd)
    display_buffs(target, target.result.removed_buffs, Vocab::BuffRemove)
  end


This method displays changes in buffs. We call display_buffs three times: once passing in target, the added_buffs property of the target's result, and Vocab::BuffAdd; once passing in target, the added_debuffs property of the result and Vocab::DebuffAdd; and finally passing in target, the removed_buffs property of the result and Vocab::BuffRemove.

def display_buffs(target, buffs, fmt)
    buffs.each do |param_id|
      replace_text(sprintf(fmt, target.name, Vocab::param(param_id)))
      wait
    end
  end


This is the method called above and takes three parameters: target, buffs, and fmt (format text).

We have an each loop on buffs, using param_id as the iteration variable (since the buffs argument is an array of which param IDs have been buffed/debuffed) and call replace_text passing in a formatted string, where we've passed in the target's name and Vocab::param with the param_id passed in. This will give us a formatted string describing the specific buff/debuff we're dealing with.

That's all for Window_BattleLog! It's a long one, but it doesn't really do anything we've never come across before elsewhere. Thankfully the next one's nice and short.

Window_PartyCommand

This is the window where you choose whether to fight or escape on the battle scene, and inherits from Window_Command.

def initialize

super(0, 0)
self.openness = 0
deactivate
end


In the constructor, we simply call the parent method passing in 0 and 0 (for the X and Y) then we set the openness of self to 0 and call deactivate so that the window isn't initially active.

"But wait a second, Trihan!" I hear you cry. "If the window's Y is 0, why isn't it at the top left? In battle, it appears down the bottom!" And right you are, but...

As you may remember from years ago, when we covered Spriteset_Battle we created 3 viewports, and as we'll see when we get to Scene_Battle (pretty far from now since it's the second-last script in the list) the party command window uses the info viewport, which happens to cover the area the battle windows take up. Keep in mind that coordinates are relative to the viewport the window belongs to, so in this case a Y of 0 means 0 *in the info viewport*.

def window_width
    return 128
  end


We overwrite the parent class' window_width method as we don't want the party command window to be 160 pixels, so in this one we return 128 instead.

def visible_line_number
    return 4
  end


Likewise we want to overwrite visible_line_number to return 4, because the parent class returns @item_max and if we do that here the window will only take up 2 lines and there will be an empty space underneath it.

def make_command_list
    add_command(Vocab::fight,  :fight)
    add_command(Vocab::escape, :escape, BattleManager.can_escape?)
  end


The method for makign the command list is pretty simple: we call add_command twice, once for the "Fight" command and once for "Escape", passing in the appropriate Vocab key for the term in question. The fight command passes in :fight as the argument to the symbol parameter and nothing for enabled because we're happy with the default of true (you can always choose to fight). The second one passes in :escape as the symbol, and an "enabled" argument of can_escape? from BattleManager, which just determines whether the party is able to escape battle. If they can, it's enabled. If they can't, it's disabled. Simples!

def setup
    clear_command_list
    make_command_list
    refresh
    select(0)
    activate
    open
  end


Last method here is setup (told you it was a small class!). This is called by Scene_Battle, as we'll see a while from now.

So we call clear_command_list to clear out the window, then make_command_list to create the commands. We call refresh, which actually displays the contents, then select(0) to select "fight" when the window is first set up, and finally activate and open to open up the window and make it the active one so the player can select options from it.

Window_ActorCommand

The window for selecting an actor's actions is almost identical in execution; the only real difference is in make_command_list and the methods for adding commands.

def initialize
    super(0, 0)
    self.openness = 0
    deactivate
    @actor = nil
  end


The only difference in the constructor is the setting of @actor to nil; unlike the party command window, this one needs a reference to which actor it's displaying commands for.

def window_width
    return 128
  end

def visible_line_number
    return 4
  end


window_Width and visible_line_number are identical, for the same reasons. We want the actor command window to be the same width as the party command one, and visible_line_number is 4 so that even with fewer than 4 commands the window will reach down to the bottom of the screen.

def make_command_list
    return unless @actor
    add_attack_command
    add_skill_commands
    add_guard_command
    add_item_command
  end


The first thing we do in make_command_list is return unless @actor is not nil, because if we don't have an actor there won't be any commands to list. Then we call the add_attack_command, add_skill_command, add_guard_command and add_item_command methods.

def add_attack_command
    add_command(Vocab::attack, :attack, @actor.attack_usable?)
  end


To add the attack command, we call add_command passing in Vocab::attack, :attack as the symbol, and the result of calling attack_usable? on the actor to determine whether the command is enabled.

def add_skill_commands
    @actor.added_skill_types.sort.each do |stype_id|
      name = $data_system.skill_types[stype_id]
      add_command(name, :skill, true, stype_id)
    end
  end


This method adds a command for each skill type the actor is able to use. We start an each loop on the sorted (alphabetically) array of added_skill_types for the actor, using stype_id as the iteration variable. We set name to the element of $data_system's skill_types array with stype_id as its index, then call add_command, passing in name, :skill as the symbol, true for enabled, and stype_id as the ext parameter (ext is just a parameter that takes additional data that can be referenced by the command handler) In this case, this will allow us to set the stype of the skill list window in Scene_Battle based on which command the player selects, which is what makes it show the list of relevant skills for that type.

def add_guard_command
    add_command(Vocab::guard, :guard, @actor.guard_usable?)
  end


In the method for adding the guard command, we call add_command passing in Vocab::guard, :guard as the symbol, and the result of calling guard_usable? on the actor to determine whether the command is enabled.

def add_item_command
    add_command(Vocab::item, :item)
  end


Adding the item command is pretty simple: we call add_command passing in Vocab::item and :item as the symbol. No need for an enabled argument as the player can always use items by default (if you wish to change that, this is the place to do it).

def setup(actor)
    @actor = actor
    clear_command_list
    make_command_list
    refresh
    select(0)
    activate
    open
  end


The setup method is identical to the last one except it takes actor as a parameter and sets @actor to the passed-in value before calling the other methods.

Ordinarily, I'd end here as the episode is getting quite long. But you know what? We're so close to finishing the window classes, and the others are fairly short, so we're going to push right through to the end so we can hit the ground running on the Scene classes next time.

Window_BattleStatus

This is the window which shows the status of your party members in battle, and inherits from Window_Selectable.

def initialize
    super(0, 0, window_width, window_height)
    refresh
    self.openness = 0
  end


The constructor calls the parent initialize method, passing in 0 and 0 for the X and Y, and window_width/window_height for the width/height. As with the party/actor command windows, the status window is in the info viewport, so a Y of 0 will display it at the bottom of the screen. Then we call refresh and set the openness of self to 0.

def window_width
    Graphics.width - 128
  end


For the window_width, we return the width of the game window minus 128 (the width of the command windows). This will have the status window take up all available space after the command windows are created.

def window_height
    fitting_height(visible_line_number)
  end


For window_height, we return the fitting height for a number of lines equal to visible_line_number...

def visible_line_number
    return 4
  end


...which is 4.

def item_max
    $game_party.battle_members.size
  end


This method gets the maximum number of items; we return the size of the battle_members array in $game_party.

def refresh
    contents.clear
    draw_all_items
  end


This refresh method is actually identical to the one in Window_Selectable, so the overwrite wasn't needed. You could safely removed this from the scripts and it wouldn't change anything.

def draw_item(index)
    actor = $game_party.battle_members[index]
    draw_basic_area(basic_area_rect(index), actor)
    draw_gauge_area(gauge_area_rect(index), actor)
  end


We do, however, need to overwrite draw_item to make this window draw our actors and their status. The method takes one parameter, index, which is the index of the current item being drawn.

First, we set actor to the element of $game_party's battle_members array with index "index", then we call draw_basic_area and draw_gauge_area, passing in calls to basic_area_rect and gauge_area_rect respectively (to which we pass the index parameter) and actor as the actor argument. That might look a bit like double Dutch right now, but we're just about to look at those methods which will hopefully unravel this a bit.

def basic_area_rect(index)
    rect = item_rect_for_text(index)
    rect.width -= gauge_area_width + 10
    rect
  end


So first we're looking at basic_area_rect. We set a variable called rect to the result of item_rect_for_text passing in index, then subtract gauge_area_width plus 10 from the width of the rect, and finally return the rect.

This effectively sets aside an area in the window for the actor's name and state icons and makes sure there's room for the HP, MP and TP gauges.

def gauge_area_rect(index)
    rect = item_rect_for_text(index)
    rect.x += rect.width - gauge_area_width
    rect.width = gauge_area_width
    rect
  end


Next up, gauge_area_rect. As before, we set rect to item_rect_for_text passing in index. This time, though, we add the rect's width minus gauge_area_width to the rect's x values, and set its width to gauge_area_width.

This will give us a rectangular area set up for the HP, MP and TP gauges.

def gauge_area_width
    return 220
  end


And as you can see, it's 220 pixels wide.

def draw_basic_area(rect, actor)
    draw_actor_name(actor, rect.x + 0, rect.y, 100)
    draw_actor_icons(actor, rect.x + 104, rect.y, rect.width - 104)
  end


This method draws the basic area, taking two parameters: rect and actor. We call draw_actor_name, passing in actor, the rect's x, rect's y, and 100 for the width (so a name that takes up more than 100 pixels will appear squished and won't fully display in the window). Interestingly the x has + 0 even though that does nothing and would behave identically if changed to just rect.x.

Then we call draw_actor_icons, passing in actor, the rect's x plus 104, the rect's y, and the rect's width minus 104 for the width. This means the icons will be drawn 104 pixels to the right of where the name started, and take up the remainder of the basic area rect's width. The available space is only enough room for 2 icons, incidentally.

def draw_gauge_area(rect, actor)
    if $data_system.opt_display_tp
      draw_gauge_area_with_tp(rect, actor)
    else
      draw_gauge_area_without_tp(rect, actor)
    end
  end


The method for drawing the gauge area also takes rect and actor as parameters.

If the "Display TP in Battle" checkbox has been checked in the System tab of the database, we call draw_gauge_area_with_tp, passing in rect and actor. Otherwise, we call draw_gauge_area_without_tp, passing in rect and actor.

def draw_gauge_area_with_tp(rect, actor)
    draw_actor_hp(actor, rect.x + 0, rect.y, 72)
    draw_actor_mp(actor, rect.x + 82, rect.y, 64)
    draw_actor_tp(actor, rect.x + 156, rect.y, 64)
  end


This method draws the gauges including the TP one. We simply call draw_actor_hp, draw_actor_mp and draw_actor_tp, passing in actor for the first argument, the rect's x plus an offset for the x, the rect's y for y, and 72/64/64 as the widths. This ensures that the HP gauge is always a little wider than the MP and TP ones. As you can see from the offsets and widths, each gauge is 10 pixels apart (HP is 72 pixels wide and takes up 0-72; MP is 64 pixels wide taking up 82-146; TP is 64 pixels wide, taking up 156-220)

def draw_gauge_area_without_tp(rect, actor)
    draw_actor_hp(actor, rect.x + 0, rect.y, 134)
    draw_actor_mp(actor, rect.x + 144,  rect.y, 76)
  end


Drawing the gauges without TP just results in making the other two wider. In this case, HP covers pixels 0-134, being 134 pixels wide, and MP takes up 144-220, being 76 pixels. The HP gauge will be just under double the width of the MP one.

The HP gauge always being the widest makes sense, since generally HP is the most important stat you need to keep track of at any given time. A dead party member deals no damage!

Window_BattleActor

This window is for selecting an actor as an action's target. It's the list of party members that pops up if you use a skill or item with allies as its scope. It inherits from Window_BattleStatus, meaning it's essentially just a copy of the window we've just looked at.

def initialize(info_viewport)
    super()
    self.y = info_viewport.rect.y
    self.visible = false
    self.openness = 255
    @info_viewport = info_viewport
  end


The constructor is overloaded from the parent, and takes info_viewport as a parameter. First we call super with no arguments (since the parent method needs no parameters), then we set the y coordinate of self to the info viewport's rect's y, the visible property of self to false, the openness property of self to 255, and the instance variable @info_viewport to the info_viewport argument.

def show
    if @info_viewport
      width_remain = Graphics.width - width
      self.x = width_remain
      @info_viewport.rect.width = width_remain
      select(0)
    end
    super
  end


In the show method, we check whether @info_viewport is nil. If it isn't, we set width_remain to the width of the game window minus the window's width, then set the x of self to width_remain. Then we set @info_viewport's rect's width to the value of width_remain, and select index 0 in the window (the first item). Then after the if statement, we call the parent show method.

The reason for this is that when the actor command window comes up in the default system, it appears on the far right of the screen and pushes the status window over to the left. We need to reposition the actor selection window to the right-hand side so that it doesn't just overlap the status window.

Setting the viewport's rect width to width_remain causes it to only render the part of the status window that isn't being overlapped by the actor selection window, meaning you don't see the status and actor command windows underneath it because the viewport is no longer showing that part of the screen (the selection window remains because it isn't assigned to a viewport).

def hide
    @info_viewport.rect.width = Graphics.width if @info_viewport
    super
  end


This method just undoes the viewport change from the previous method and then calls the parent hide method; if @info_viewport contains data, we set the @info_viewport's rect's width back to the width of the game window. Not doing this would result in a big hole in the screen after the selection window disappears.

Window_BattleEnemy

This one is almost the same, but for the enemy selection instead of actor. Because it needs to show enemies instead of actors, it inherits from Window_Selectable rather than Window_BattleStatus.

def initialize(info_viewport)
    super(0, info_viewport.rect.y, window_width, fitting_height(4))
    refresh
    self.visible = false
    @info_viewport = info_viewport
  end


The constructor, as before, takes info_viewport as a parameter. First we call super, passing in 0 for x, the info viewport's rect's y for y, window_width for width, and the fitting height for 4 lines for the height. We then call refresh, set the visible property of self to false, and set @info_viewport to the info_viewport argument.

def window_width
    Graphics.width - 128
  end


As before, window_width is the width of the game window minus 128.

def col_max
    return 2
  end


As Window_Selectable's col_max method returns 1 and we want 2 columns for enemies, we need to overwrite it.

def item_max
    $game_troop.alive_members.size
  end


As for item_max, we set it to the size of $game_troop's alive_members array. In other words, how many living enemies there are.

def enemy
    $game_troop.alive_members[@index]
  end


This method gets the object of the selected enemy; we simply return the element of $game_troop's alive_members array with an index of @index (which will be whatever the current index of the window is)

def draw_item(index)
    change_color(normal_color)
    name = $game_troop.alive_members[index].name
    draw_text(item_rect_for_text(index), name)
  end


The overwritten draw_item method, as with the parent, takes index as a parameter. First we call change_color passing in normal_color just in case it was changed to something else previously. Then we set name to the name property of the element in $game_troop's alive_members array with an index of "index", and finally we call draw_text, passing in the result of item_rect_for_text with index as the argument for the "rect" parameter, and name for the text.

def show
    if @info_viewport
      width_remain = Graphics.width - width
      self.x = width_remain
      @info_viewport.rect.width = width_remain
      select(0)
    end
    super
  end


This is identical to the show method of the previous window.

def hide
    @info_viewport.rect.width = Graphics.width if @info_viewport
    super
  end


As is this one.

Window_BattleSkill

This is the window used for selecting skills in battle. It inherits from Window_SkillList.

def initialize(help_window, info_viewport)
    y = help_window.height
    super(0, y, Graphics.width, info_viewport.rect.y - y)
    self.visible = false
    @help_window = help_window
    @info_viewport = info_viewport
  end


We overload the initialize method of the parent as we need two parameters: help_window and info_viewport.

We set a variable called y to the height of the help window, then call the parent constructor passing in 0 for x, y for y, Graphics.width for width, and the y coordinate of the info viewport's rect's y minus y as the height.

This ensures that the skill window takes up the remaining screen space between the help window and the command/status area.

Then we set self's visible property to false, the @help_window instance variable to the passed-in help_window argument, and @info_viewport to the info_viewport argument.

Note that the reason we need to take help_window as a parameter is so that the constructor can reference its height; if we left it to the @help_window instance variable, it wouldn't be set when the window is first created, so we'd get an error for trying to access the height of a nil object.

def show
    select_last
    @help_window.show
    super
  end


In the show method we call select_last (to select the most recently selected skill for the actor) then call the show method of @help_window, and finally call the parent show method.

def hide
    @help_window.hide
    super
  end


In the hide method, we call hide on @help_window and then call the parent hide method.

Window_BattleItem

This is very similar to the previous window, only for items rather than skills. Subsequently, it inherits from Window_ItemList.

def initialize(help_window, info_viewport)
    y = help_window.height
    super(0, y, Graphics.width, info_viewport.rect.y - y)
    self.visible = false
    @help_window = help_window
    @info_viewport = info_viewport
  end


The constructor is identical to that of Window_BattleSkill.

def include?(item)
    $game_party.usable?(item)
  end


We overwrite the include? method because we only want to show items that can actually be used. For this, we call the usable? method of $game_party, passing in the item argument.

def show
    select_last
    @help_window.show
    super
  end

def hide
    @help_window.hide
    super
  end


show and hide are identical to those of Window_BattleSkill as well.

def hide
    @help_window.hide
    super
  end


Window_TitleCommand

This is the window for selecting commands on the game's title screen, and inherits from Window_Command.

def initialize
    super(0, 0)
    update_placement
    select_symbol(:continue) if continue_enabled
    self.openness = 0
    open
  end


In the constructor, we call the parent constructor passing in 0 and 0 for the x and y coordinates. We then call update_placement, then select_symbol passing in the :continue symbol if continue_enabled returns true. Finally, we set self's openness to 0 and call open. This ensures that the title command window starts out closed and animates opening when the game starts.

def window_width
    return 160
  end


Another overwrite that isn't necessary; we return 160 for window_width, but Window_Command already does that, so you could safely remove this from the scripts and it wouldn't change anything.

def update_placement
    self.x = (Graphics.width - width) / 2
    self.y = (Graphics.height * 1.6 - height) / 2
  end


In update_placement, we set self's x to half of the game window's width minus the command window width, and self's y to half of the game window height multiplied by 1.6 minus the command window height. This causes the command window to be horizontally centred between the centre and bottom of the screen.

def make_command_list
    add_command(Vocab::new_game, :new_game)
    add_command(Vocab::continue, :continue, continue_enabled)
    add_command(Vocab::shutdown, :shutdown)
  end


In the overwrite of make_command_list, we add 3 commands: one for Vocab::new_game passing in :new_game as the symbol; one for Vocab::continue, passing in :continue as the symbol and continue_enabled as the "enabled" argument; and one for Vocab::shutdown passing in :shutdown as the symbol.

def continue_enabled
    DataManager.save_file_exists?
  end


This is the method which determines whether to select the continue option when starting the game, and also whether continue is enabled in the first place. We return true if DataManager's save_file_exists? method returns true, and false otherwise. In other words, if the player has any saved games, we'll immediately select the continue option when starting the game.

Window_GameEnd

This is the confirmation window that pops up when selecting the shut down command, and inherits from Window_Command.

def initialize
    super(0, 0)
    update_placement
    self.openness = 0
    open
  end


The constructor is identical to that of Window_TitleCommand with the exception of the line auto-selecting the continue symbol.

def window_width
    return 160
  end


Again, we're overwriting window_width with a version that does exactly what the parent method did, which is entirely pointless.

def update_placement
    self.x = (Graphics.width - width) / 2
    self.y = (Graphics.height - height) / 2
  end


This one's almost identical to update_placement from Window_TitleCommand. The only difference is not multiplying the game window height by 1.6 so this window appears in the very centre of the screen rather than further down.

def make_command_list
    add_command(Vocab::to_title, :to_title)
    add_command(Vocab::shutdown, :shutdown)
    add_command(Vocab::cancel,   :cancel)
  end


make_command_list is very similar too. We're still adding 3 commands, just different ones. The first is for Vocab::to_title passing in :to_title as the symbol; the second is for Vocab::shutdown passing in :shutdown as the symbol; and finally, the third is for Vocab::cancel passing in :cancel as the symbol.

Window_DebugLeft

This is the left-hand window for the debug scene, which shows the game's switches and variables. It inherits from Window_Selectable.

First of all, we have two class variables:

@@last_top_row = 0                      # For saving first line
  @@last_index   = 0                      # For saving cursor position


As the comments say, @@last_top_row is for keeping track of which line of data needs to appear at the top of the window, and @@last_index tracks the cursor position.

We also have one public instance variable:

attr_reader   :right_window             # Right window


The :right_window variable is set as an attr_reader, meaning its value can be accessed externally but it can't be assigned a value outside of the window class.

def initialize(x, y)
    super(x, y, window_width, window_height)
    refresh
    self.top_row = @@last_top_row
    select(@@last_index)
    activate
  end


In the constructor, we take two parameters, x and y. First we call the super constructor, passing in x and y as the X and Y, window_width as the width and window_height as the height. Then we call refresh. After that, we set self's top_row property to @@last_top_row and select the item with index @@last_index. Finally, we call activate to make this the active window.

def window_width
    return 164
  end


In the window_width method, we simply return 164.

def window_height
    Graphics.height
  end


And in window_height, we return the height of the game window as we want it to take up the whole vertical space.

def item_max
    @item_max || 0
  end


The overwrite of item_max returns either @item_max or 0 if @item_max is nil. This is necessary to avoid a crash, as @item_max will initially have no value until the window first refreshes.

def update
    super
    return unless @right_window
    @right_window.mode = mode
    @right_window.top_id = top_id
  end


In the frame update method, first we call the parent update, then return unless @right_window is not nil. If that isn't the case, we set @right_window's mode property to the result of calling the mode method, and its top_id property to the result of calling top_id.

You would think that having this operation happen every frame would be inefficient, but without doing that (putting it in select, say), if you close the debug window and reopen it, it will initially have a mismatch between the selection and the right window contents.

def mode
    index < @switch_max ? :switch : :variable
  end


This method gets the debug mode; we return :switch if the index is less than @switch_max, and :variable otherwise. In other words, :switch if our selection is a switch, and :variable if it's not.

def top_id
    (index - (index < @switch_max ? 0 : @switch_max)) * 10 + 1
  end


This method gets the ID of the item to show on top of the window (since it "remembers" where the user last had it). This looks like a complex algorithm but it's actually fairly simple. Let's skip to the next method and look at what @switch_max actually is, then we'll come back to this.

def refresh
    @switch_max = ($data_system.switches.size - 1 + 9) / 10
    @variable_max = ($data_system.variables.size - 1 + 9) / 10
    @item_max = @switch_max + @variable_max
    create_contents
    draw_all_items
  end


In the overwrite of the refresh method, we see how we get these values. @switch_max is set to (the size of $data_system's switches array minus 1 plus 9) divided by 10. @variable_max is the same but with variables rather than switches, then @item_max is set to the sum of @switch_max and @variable_max. After that, we call create_contents and draw_all_items.

The reason for this calculation is that 10 items are shown per page. So let's say our game has 5 switches: the size of the array is actually 6 (since there's a nil element at the beginning, like with most arrays for database items; the reason for this is that it's more user-friendly to start IDs at 1 in the data, but internally array indexes start at 0, so to get around this most arrays in the engine have a nil element at the beginning so the "real" indexing starts at 1), which is why we subtract 1 from it here. We add 9 so that the value is 10 at minimum (since with fewer than 10 switches the division by 10 would give us 0, so we'd have no pages but we always need at least 1 since you can't have fewer than 1 switch).

Unlike with update, this is a genuine example of potential inefficiency; calling create_contents disposes the contents object and creates a new bitmap. Doing this every time the window refreshes makes little sense. It would be far more efficient to just call super instead, since that clears the contents and calls draw_all_items anyway.

def draw_item(index)
    if index < @switch_max
      n = index * 10
      text = sprintf("S [%04d-%04d]", n+1, n+10)
    else
      n = (index - @switch_max) * 10
      text = sprintf("V [%04d-%04d]", n+1, n+10)
    end
    draw_text(item_rect_for_text(index), text)
  end


The overwrite of draw_item looks scarier than it is.

If index is less than @switch_max (meaning our selection is in the "switch" section of the list) we set a variable called n to index multiplied by 10, then set a variable called text to a formatted string: "S ", replacing the first placeholder with n plus 1, and the second with n plus 10. The 04 before the d means that each number will be a minimum of 4 digits long, padding with 0s. So if index is 0 (meaning n is also 0), the string will be "S ".

Otherwise, meaning our selection is in the variables section, n is set to (index minus @switch_max) multiplied by 10. The setting of text is almost identical, it's just that we have "V" instead of "S".

So let's say we have 39 switches in the game and we're on the first page of variables (meaning index is 4); this becomes (4 - 4) * 10, which is 0. If we were on the second page it'd be (5 - 4) * 10, or 10. And so on and so forth.

Either way, in the end we call draw_text, passing in item_rect_for_text as the rect (passing in index) and text as the text.

In effect, this will draw a row for every 10 switches/variables in the game. Interestingly, this means that if your max switches/variables are not a multiple of 10, you can turn on switches or set variables with higher IDs than the max you've currently set, and can even use scripts to check their value.

def process_cancel
    super
    @@last_top_row = top_row
    @@last_index = index
  end


In the overwrite of the process_cancel method, we call the parent method, then set @@last_top_row to the result of calling top_row, and @@last_index to the index property. Because these are class variables they're shared amongst all instances of the window, which is why reopening the debug window returns you to the item you left it on even though technically it's a new instance of the window every time.

def right_window=(right_window)
    @right_window = right_window
    update
  end


The setter method for the @right_window instance variable is pretty simple: set @right_window to the passed-in value, then call update.

Again, you'd think it would have been more efficient to call refresh here instead. Even not doing that, the call to update is seemingly pointless because it's called every frame by default anyway. But interestingly, if you don't do it this way, there's a split second when you first reopen the debug window where it starts out on the first page instead of the one you left it on.

Window_DebugRight

We've finally reached the last window class in the scripts! Once we make it through this one, that's us. We can get to learning what makes a scene tick, and won't that be exciting!

For now though, this is the right-hand window for the debug scene, which contains the switches and variables that can be turned on/off or have their values changed. Since you need to select things in it, it inherits from Window_Selectable.

First off, we have two public instance variables:

attr_reader   :mode                     # Mode (:switch / :variable)
  attr_reader   :top_id                   # ID shown on top


:mode determines whether we're currently editing switches or variables. :top_id is the ID to show at the top of the window, same as with Window_DebugLeft.

def initialize(x, y, width)
    super(x, y, width, fitting_height(10))
    @mode = :switch
    @top_id = 1
    refresh
  end


The constructor takes 3 parameters: x, y and width. We call the parent constructor, passing in x and y as X and Y, width as the width, and the fitting height for 10 lines as the height. We set @mode to the symbol :switch, @top_id to 1, and call refresh.

def item_max
    return 10
  end


We overwrite item_max to return 10 instead of 0 like in the parent class.

def refresh
    contents.clear
    draw_all_items
  end


Yet another overwrite of a refresh method that does exactly the same thing as the parent does.

def draw_item(index)
    data_id = @top_id + index
    id_text = sprintf("%04d:", data_id)
    id_width = text_size(id_text).width
    if @mode == :switch
      name = $data_system.switches[data_id]
      status = $game_switches[data_id] ? "[ON]" : "[OFF]"
    else
      name = $data_system.variables[data_id]
      status = $game_variables[data_id]
    end
    name = "" unless name
    rect = item_rect_for_text(index)
    change_color(normal_color)
    draw_text(rect, id_text)
    rect.x += id_width
    rect.width -= id_width + 60
    draw_text(rect, name)
    rect.width += 60
    draw_text(rect, status, 2)
  end


draw_item on the other hand is doing a fair bit.

First we set a variable called data_id to @top_id plus index. This is to ensure we get the appropriate multiple of 10. Then we set another variable called id_text to a formatted string of "%04d", where the placeholder is replaced by data_id. A third variable called id_width is set to the width property of the result of calling text_size passing in id_text (which gives us how many pixels it requires to display the ID of the switch or variable).

If @mode is equal to the symbol :switch, we set name to the element of $data_system's switches array with an index of data_id, and status to "" if that element in $game_switches is true, or "" if it's false.

If @mode is not equal to :switch (in which case it's :variable), we set name to the corresponding element of $data_system's variables array instead, and status to the value of that element in $game_variables.

We set name to an empty string unless name is not nil, then set a variable called rect to the result of item_rect_for_text passing in index. We call change_color passing in normal_color in case any previous effect changed it to something else, then call draw_text passing in rect as the rect to draw the text in, and id_text as the text to draw.

After that, we add id_width to the rect's x coordinate (so that it moves over to just after the text we've just drawn, and subtract id_width plus 60 from the rect's width (this leaves room for the status text), calling draw_text and passing in rect and name. Then we add 60 to the rect's width and call draw_text again, passing in rect, status and 2 for right-alignment.

In the end, this gives us the ID of the switch/variable on the left, followed by its name, and then its ON/OFF or numeric value over on the right.

def mode=(mode)
    if @mode != mode
      @mode = mode
      refresh
    end
  end


Here we have a setter method for the mode, which obviously takes "mode" as a parameter. If @mode is not equal to the passed-in mode, we set @mode to that value and call refresh.

As we've already seen, there's only two modes: :switch and :variable. So if we pass it :variable and it's currently :switch, it'll set to to :variable, and vice versa.

def top_id=(id)
    if @top_id != id
      @top_id = id
      refresh
    end
  end


Same deal with the setter for top_id. We take id as a parameter, and if @top_id is not equal to the argument passed in we set @top_id to that value and call refresh.

def current_id
    top_id + index
  end


This method gets the currently selected ID, returning the value of top_id plus the index.

def update
    super
    update_switch_mode   if active && mode == :switch
    update_variable_mode if active && mode == :variable
  end


In the frame update method, we first call the parent update. Then we call update_switch_mode if the window is active AND the mode is :switch, or update_variable_mode if the window is active AND the mode is :variable.

def update_switch_mode
    if Input.trigger?(:C)
      Sound.play_ok
      $game_switches[current_id] = !$game_switches[current_id]
      redraw_current_item
    end
  end


This method is called for frame updates while in switch mode. If the user presses the :C key, we play the ok sound, then toggle the element of $game_switches with an index of "current_id" (so if it's OFF it'll be turned ON, and if ON it'll be turned OFF) then call redraw_current_item to show the change in the window.

def update_variable_mode
    return unless $game_variables[current_id].is_a?(Numeric)
    value = $game_variables[current_id]
    value += 1 if Input.repeat?(:RIGHT)
    value -= 1 if Input.repeat?(:LEFT)
    value += 10 if Input.repeat?(:R)
    value -= 10 if Input.repeat?(:L)
    if $game_variables[current_id] != value
      $game_variables[current_id] = value
      Sound.play_cursor
      redraw_current_item
    end
  end


Last but not least, this method is called for frame updates while in variable mode. We return unless the element of $game_variables at current_id is a numeric value (since variables can have any valid Ruby object assigned to them via a script command and you can't exactly add or subtract from an actor).

Assuming it *is* numeric, we set a variable called value to the value of the element of $game_variables with an index of "current_id". Then we add 1 to it if the user is pressing or holding the :RIGHT key, subtract 1 from it if the user is pressing or holding the :LEFT key, add 10 to it if the user is pressing or holding the :R key, or subtract 10 from it if the user is pressing or holding the :L key. Then if the value of $game_variables at current_id is not the same as value (meaning it's changed), we set it to that value, play the cursor sound, and call redraw_current_item.

And we're done with windows! That's it, windows have been broken down in their entirety. Do you understand them better now? Would you be able to create your own window for a custom scene if you needed to? Hopefully so!

Covering the remaining window classes means that we can hit the ground running with our breakdown of the Scene classes, which I'm really looking forward to. Starting, obviously, with Scene_Base. Until next time!