SLIP INTO RUBY - UNDER THE HOOD PART 15: WHAT'S IN A WINDOW?

Quite a lot, as it turns out.

  • Trihan
  • 02/14/2017 05:04 PM
  • 4037 views
Gentle readers, after a brief sojourn into the depths of Javascript I return to give you your regular dose of



As we finished with spritesets last time, this marks the beginning of tearing down one of the classes you're probably most curious about the inner workings of: windows. if you ever create a custom scene you will most likely have to create your own windows for it, so it's handy to know how the ones you interact with in most generic VX Ace projects tick. Let's begin!

Window_Base

This class is the superclass of all windows used in the game, though it's not quite the Godfather, as it itself inherits from Window, which is a built-in class we don't have access to the code for (well, if you know where to look online you can actually get the code for the uneditable .rb files and using certain methods you can actually overwrite parts of them if you want to but that goes beyond the scope of what I'm trying to accomplish with this series so forget I said that).

def initialize(x, y, width, height)
    super
    self.windowskin = Cache.system("Window")
    update_padding
    update_tone
    create_contents
    @opening = @closing = false
  end


The constructor takes four parameters: x, y, width and height. First we call Window's initialize method, set the instance's windowskin to the file named "Window" in the system folder, update the padding, update the tone, create the window's contents, and set both @opening and @closing to false.

def dispose
    contents.dispose unless disposed?
    super
  end


The destructor disposes contents unless the window has already been disposed; this is a built-in method that checks whether the window is still taking up memory (if you try to dispose contents in a window that's already been disposed you'll get an error).

def line_height
    return 24
  end


This is a method which determines how many pixels tall a "line" in the window is, and is hardcoded at 24.

def standard_padding
    return 12
  end


This method determines the standard number of pixels of "padding" which will be added to elements in a window, and is hardcoded at 12.

def update_padding
    self.padding = standard_padding
  end


This method updates the padding by setting the instance's padding property to the result of calling standard_padding. The padding property is hardcoded into Window and you don't need to do anything else to make it work.

def contents_width
    width - standard_padding * 2
  end


This method calculates the width of the window's contents, and returns the width of the window minus double the standard padding.

def contents_height
    height - standard_padding * 2
  end


Same as before but for the height of the contents.

def fitting_height(line_number)
    line_number * line_height + standard_padding * 2
  end


This incredibly handy method calculates the number of pixels required to display a given number of lines, taking line_number as a parameter. It returns the number of lines multiplied by the line height, plus double the standard padding.

def update_tone
    self.tone.set($game_system.window_tone)
  end


This method updates the window's tone by setting its tone property to the window_tone property of $game_system. tone, like padding, is built in to Window and no further code is needed on the RGSS end for this to work.

def create_contents
    contents.dispose
    if contents_width > 0 && contents_height > 0
      self.contents = Bitmap.new(contents_width, contents_height)
    else
      self.contents = Bitmap.new(1, 1)
    end
  end


This method creates the window's contents. First we dispose of contents to ensure that we're starting with a blank window, then if the width and height of the contents are both greater than 0 we set contents to a new Bitmap contents_width pixels wide and contents_height pixels tall. Otherwise, we create a 1x1 Bitmap (because why bother with anything bigger if there's nothing to show?)

def update
    super
    update_tone
    update_open if @opening
    update_close if @closing
  end


The frame update method calls Window's update, then updates the tone, then updates the window opening if @opening is true, and updates the window closing if @closing is true.

def update_open
    self.openness += 48
    @opening = false if open?
  end


The window opening update method adds 48 to the window's openness property, then sets @opening to false if the window is open (openness equal to 255). openness is a built-in property and no further code is required to animate the window opening.

def update_close
    self.openness -= 48
    @closing = false if close?
  end


Same thing but for closing. Subtracts 48 from openness instead of adding, then sets @closing to false if openness is equal to 0.

def open
    @opening = true unless open?
    @closing = false
    self
  end


This method opens the window by first setting @opening to true unless the window is already open, setting @closing to false, and then returning itself.

def close
    @closing = true unless close?
    @opening = false
    self
  end


Same thing for closing the window. @closing is set to true unless the window is already closed, @opening is set to false, and then we return the instance object.

def show
    self.visible = true
    self
  end


This method shows the window by setting its visible property to true and then returning itself.

def hide
    self.visible = false
    self
  end


Same thing for hiding the window, setting visible to false.

def activate
    self.active = true
    self
  end


This method causes the window to become the active one (the one which has a blinking cursor) by setting its active property to true and then returning itself.

def deactivate
    self.active = false
    self
  end


Same thing for deactivating the window, setting active to false.

def text_color(n)
    windowskin.get_pixel(64 + (n % 8) * 8, 96 + (n / 8) * 8)
  end


This method gets the text colour corresponding to a given number n. get_pixel is a method of the Bitmap class which gets the Color at the specified x/y coordinates of the bitmap (a Color itself consisting of R, G, B and alpha components).

If we look at the windowskin graphic, we can puzzle this out a bit.



The text colour portion is an 8x4 grid of pixels, each of which consists of an 8 pixel by 8 pixel square, located on the bottom right of the graphic.

As the full graphic is 128 pixels wide, we have to start the x coordinate at 64 to get the left edge of the grid. We add n % 8 to get the appropriate "column" coordinate; as there are 8 colours per row, we want to return the x coordinate to the beginning when we hit the column for index 8 (as the first one is 0, index 8 is the first column on the second row) and so on for rows 3 and 4. We multiply by 8 because there are 8 pixels in each block. This results in the x coordinate always being the far left pixel of the block corresponding to the passed-in index. For example, if we do text_color(6) we want a pale yellow. The x coordinate will be 64 + (6 % 8) * 8 = 64 + (6) * 8 = 64 + 48 = 112. Sure enough, if we look at x 112 on the graphic, it lines up with the far left edge of the pale yellow block.

We have a similar calculation for the y coordinate. The blocks start at 96 pixels down so that's where we start the algorithm. We divide n by 8 to get which row the block is in (0-3) and multiply by 8 because each block is 8 pixels tall. This gives us the y coordinate of the top edge for each block (meaning that the full algorithm gives us the block's top left pixel).

This actually opens up a rather interesting opportunity for developers who want more colour options from their text (and want to use text_color rather than specifying a Color object for it); you can actually edit the windowskin to have up to 2048 colours! All you have to do is make each pixel of the grid a different colour and rewrite the method slightly:

windowskin.get_pixel(64 + (n % 64), 96 + (n / 64))


Just an interesting little aside. Note that if you do add more colours to the windowskin, you'll also have to edit the next set of methods to use the appropriate numbers.

def normal_color;      text_color(0);   end;    # Normal
  def system_color;      text_color(16);  end;    # System
  def crisis_color;      text_color(17);  end;    # Crisis
  def knockout_color;    text_color(18);  end;    # Knock out
  def gauge_back_color;  text_color(19);  end;    # Gauge background
  def hp_gauge_color1;   text_color(20);  end;    # HP gauge 1
  def hp_gauge_color2;   text_color(21);  end;    # HP gauge 2
  def mp_gauge_color1;   text_color(22);  end;    # MP gauge 1
  def mp_gauge_color2;   text_color(23);  end;    # MP gauge 2
  def mp_cost_color;     text_color(23);  end;    # TP cost
  def power_up_color;    text_color(24);  end;    # Equipment power up
  def power_down_color;  text_color(25);  end;    # Equipment power down
  def tp_gauge_color1;   text_color(28);  end;    # TP gauge 1
  def tp_gauge_color2;   text_color(29);  end;    # TP gauge 2
  def tp_cost_color;     text_color(29);  end;    # TP cost


This is technically fourteen separate methods, but as they do more or less the same thing they're grouped together. Anyway, for those who don't know, there are a number of "shortcut" methods for various system colours that you can also use in your custom windows if you wish.

Just to give you a reference guide to what these colours are, normal_color is white, system_color is a shade of light blue, crisis_color is yellow, knockout_color is red, gauge_back_color is black, hp_gauge_color1 is orange, hp_gauge_color2 is a paler orange, mp_gauge_color1 is blue, mp_gauge_color2 is a kind of aqua, mp_cost_color is the same aqua, power_up_color is pale green, power_down_color is brown, tp_gauge_color1 is green, tp_gauge_color2 is light green and tp_cost_color is the same light green.

def pending_color
    windowskin.get_pixel(80, 80)
  end


This method gets the background colour of a pending item by getting the colour of the windowskin pixel at (80, 80), which is transparent by default.

def translucent_alpha
    return 160
  end


This method determines the alpha value of a translucent image, and is hardcoded at 160 (full opacity is 255).

def change_color(color, enabled = true)
    contents.font.color.set(color)
    contents.font.color.alpha = translucent_alpha unless enabled
  end


This method changes the text drawing colour and takes two parameters: color, the Color object to change to, and an enabled flag to determine whether to draw semi-transparently or not.

First we set the color of the font in contents to the passed-in colour, then unless enabled is true we set alpha to translucent_alpha, which we've just seen will make it 160.

def draw_text(*args)
    contents.draw_text(*args)
  end


The draw_text method takes one parameter...*args? What's up with the asterisk? This is known in Ruby as the splat operator and basically hoovers up all remaining arguments passed to the call after other parameters are assigned and puts them in an array. In other words, *args will take in any number of arguments.

What use is this? Well if you look at the help file under draw_text for the Bitmap class, it has two overloads: one that takes an x/y coordinate, width, height, string and optional alignment, and one that takes a rect, str and optional alignment. Rather than have two different draw_text methods in Window_Base, we'll just convert whatever was passed in to an array and let Bitmap handle the decision of which overload to call!

def text_size(str)
    contents.text_size(str)
  end


This method gets the rect required to draw the string str in the window's contents bitmap.

def draw_text_ex(x, y, text)
    reset_font_settings
    text = convert_escape_characters(text)
    pos = {:x => x, :y => y, :new_x => x, :height => calc_line_height(text)}
    process_character(text.slice!(0, 1), text, pos) until text.empty?
  end


Okay, now we're getting into some of the more interesting stuff. So draw_text_ex takes three parameters: x, y and the text to draw. This method draws text containing control characters.

First we reset the font settings, like colour and size. Then we convert any escape characters in the text to the appropriate values, set pos to a hash consisting of :x, :y, :new_x (the x position to begin a new line at) and :height, and process the first character of the text until there's none left (slice! will remove the first character while processing the string, so eventually there won't be any characters left).

def reset_font_settings
    change_color(normal_color)
    contents.font.size = Font.default_size
    contents.font.bold = Font.default_bold
    contents.font.italic = Font.default_italic
  end


To reset the font settings, we change the colour to normal_color, the font size to the default_size property of the Font class, and the bold/italic properties also to Font's defaults.

def convert_escape_characters(text)
    result = text.to_s.clone
    result.gsub!(/\\/)            { "\e" }
    result.gsub!(/\e\e/)          { "\\" }
    result.gsub!(/\eV\[(\d+)\]/i) { $game_variables[$1.to_i] }
    result.gsub!(/\eV\[(\d+)\]/i) { $game_variables[$1.to_i] }
    result.gsub!(/\eN\[(\d+)\]/i) { actor_name($1.to_i) }
    result.gsub!(/\eP\[(\d+)\]/i) { party_member_name($1.to_i) }
    result.gsub!(/\eG/i)          { Vocab::currency_unit }
    result
  end


This method converts the escape characters in text with the values they represent (the ones that will be converted into strings at least), which is how you can do \v in a text box and end up with the value of variable 1.

First, we set result to a clone of text converted to a string.

The subsequent substitutions use gsub!, a method which replaces a regex pattern with a given string, in this case using a block.

First of all, we replace \\ (a single backslash character) with the escape character \e. Then, we replace any occurrence of two escape characters with "\\" (a single backslash character). Basically, when we have an escape character in the text we'll convert the slash into an actual ASCII escape character, and if we end up with two escape characters together then that's clearly meant to be an actual slash, so we'll convert them back.

Then, we replace a case-insensitive pattern consisting of an escape character, a V, an open square bracket, any number of digits from 0-9, and a close square bracket to the index of $game_variables matching the matched number as an integer.

I could go into more detail on exactly how regex patterns work but that's outside the scope of Slip into Ruby. If anyone really wants to know more about them I can write a separate article. Suffice to say that the pattern you're looking for needs to be inside a set of forward slashes, an 'i' after the end slash makes the pattern case-insensitive, brackets create a "group" which you can reference later using $1, $2, $3 etc depending on the index of the group, 'd' matches a single digit, and '+' matches 1 or more occurrences of the preceding pattern. As you may have surmised, any occurrence of a backslash needs to be repeated if you mean an actual slash, or it'll be considered an escape character modifying the character after it.

You also may hae noticed that the coders of the default scripts made a slight error here: they repeated the line that converts variables, but it won't do anything because you already substituted the pattern it's looking for into something else. It's just a useless line of code.

Then, we replace a case-insensitive pattern consisting of an escape character, an N, an open square bracket, any number of digits from 0-9, and a close square bracket to the name of the actor with an ID matching the matched number.

Then we do the same thing for the name of a party member, using P instead of N.

Our last substitution is replacing an escape character followed by G (again, case-insensitive) with the currency unit defined in Vocab.

Finally, we return result.

def actor_name(n)
    actor = n >= 1 ? $game_actors[n] : nil
    actor ? actor.name : ""
  end


This method gets the name of actor number n (note that unlike the majority of other arrays we've dealt with this is 1-indexed, not 0). If n is greater than or equal to 1, we set 'actor' to the element of $game_actors with index n, otherwise we set it to nil. Then, if the variable is not nil, we return the actor object's name, or a blank string otherwise.

def party_member_name(n)
    actor = n >= 1 ? $game_party.members[n - 1] : nil
    actor ? actor.name : ""
  end


Same thing but for party members. Note that as the members property of $game_party is 0-indexed we have to subtract 1 from the number passed in to get the correct position.

def process_character(c, text, pos)
    case c
    when "\n"   # New line
      process_new_line(text, pos)
    when "\f"   # New page
      process_new_page(text, pos)
    when "\e"   # Control character
      process_escape_character(obtain_escape_code(text), text, pos)
    else        # Normal character
      process_normal_character(c, pos)
    end
  end


This method processes a character from text, and takes three parameters: c, the character to process; text, a buffer for the string being processed; and pos, the draw position for the character.

We have a case statement against c: when it's '\n' (the newline character) we process a new line, passing in text and pos. When it's '\f' (the form feed character) we process a new page, passing in text and pos. When it's '\e' (escape character) we process the escape character, passing in its escape code, text, and pos. Otherwise, we just process the character normally, passing in c and pos.

def process_normal_character(c, pos)
    text_width = text_size(c).width
    draw_text(pos[:x], pos[:y], text_width * 2, pos[:height], c)
    pos[:x] += text_width
  end


This method processes a normal (non-escape) character, taking in two parameters, c (the character to process) and pos (a hash containing the x coordinate, y coordinate, new x coordinate, and height).

We set text_width to the width of the rect required to display the character on the Bitmap. Then we call draw_text passing in the :x and :y keys from pos, double the text width (this ensures that there is plenty of room for the character and it doesn't get squashed), the :height key from pos, and the character to draw.

Then we add the width of the character to the :x key in pos, ensuring that the next character is drawn after the one we just drew.

def process_new_line(text, pos)
    pos[:x] = pos[:new_x]
    pos[:y] += pos[:height]
    pos[:height] = calc_line_height(text)
  end


This method processes a new line character. First, the :x key of pos is set to its :new_x value, the :y key is increased by the :height value, and :height is set to the calculated line height for the text being processed.

def process_new_page(text, pos)
  end


The method for processing a new page is just an empty method in the base class. It will be overwritten by child classes.

def obtain_escape_code(text)
    text.slice!(/^[\$\.\|\^!><\{\}\\]|^[A-Z]+/i)
  end


This method destructively gets the escape code (the character following a backslash in the text). The pattern basically equates to "Going from the beginning of the text and ignoring case, match any one of: '$', '.', '|', '^', '!', '>', '<', '{', '}', '\' OR any number of letters from A to Z."

def obtain_escape_param(text)
    text.slice!(/^\[\d+\]/)[/\d+/].to_i rescue 0
  end


This method destructively gets the argument passed to a control code (like the number of the colour to change to, or the ID of the icon to draw). This pattern equates to "Going from the beginning of the text, match an open square bracket, then any number of digits, then a close square bracket." We then return all the digits that were matched converted to an integer, with a rescue clause that will just return 0 if there was an error during the conversion.

def process_escape_character(code, text, pos)
    case code.upcase
    when 'C'
      change_color(text_color(obtain_escape_param(text)))
    when 'I'
      process_draw_icon(obtain_escape_param(text), pos)
    when '{'
      make_font_bigger
    when '}'
      make_font_smaller
    end
  end


This method processes an escape character and takes three parameters: the code of the character (this is whatever was after the slash), the text being processed, and pos.

We have a case for the uppercase conversion of code: when it's 'C' we change the text to the colour matching the number in square brackets following the C; when it's 'I' we draw the icon with the ID corresponding to the number in square brackets following the I (also passing in pos); when it's '{' we increase the font size, and when it's '}' we decrease the font size.

def process_draw_icon(icon_index, pos)
    draw_icon(icon_index, pos[:x], pos[:y])
    pos[:x] += 24
  end


This method processes the drawing of an icon and takes two parameters: the icon index and the position hash. We draw the icon with ID icon_index at the x and y positions corresponding to the :x and :y keys of pos, then add 24 to the :x value (as icons are 24 pixels wide).

def make_font_bigger
    contents.font.size += 8 if contents.font.size <= 64
  end


This method makes the window font bigger by adding 8 to the size property of the contents font if it's less than or equal to 64 (the maximum size the text can be by default).

def make_font_smaller
    contents.font.size -= 8 if contents.font.size >= 16
  end


Same thing but for making the font smaller, decreasing it by 8 to a minimum of 16.

This means there are 7 possible sizes for text in a window.

def calc_line_height(text, restore_font_size = true)
    result = [line_height, contents.font.size].max
    last_font_size = contents.font.size
    text.slice(/^.*$/).scan(/\e[\{\}]/).each do |esc|
      make_font_bigger  if esc == "\e{"
      make_font_smaller if esc == "\e}"
      result = [result, contents.font.size].max
    end
    contents.font.size = last_font_size if restore_font_size
    result
  end


This method calculates the height in pixels required to display a line of text and takes two parameters: text, the text used for the calculation, and font_size, a flag determining whether to return to the original font size after the calculation is done.

We initialise result as either line_height or the size of the contents font, whichever is larger. Then we set last_font_size to the size of the contents font, and start an each loop on...what? The hell is all that gibberish?

Okay, so we've seen slice. The pattern here literally matches everything from the beginning of the line to the end. Scan returns all matches for the given pattern, which in this case is "An escape character followed by either a '{' or a '}'", and then we iterate through the array of matches using the iteration variable 'esc'. If esc is \e{, we make the font bigger, and if esc is \e}, we make the font smaller. Then, we set result to either itself or the size of the contents font, whichever is larger. Finally, we set the size of the contents font to last_font_size if the restore flag was true (because we don't want to actually make the font bigger or smaller, we just want to know how much space it needs) and return result.

Effectively, this looks through the line to see where the text gets bigger/smaller and returns the largest size the line needed at any given point, which tells us the full height we need to display the line.

def draw_gauge(x, y, width, rate, color1, color2)
    fill_w = (width * rate).to_i
    gauge_y = y + line_height - 8
    contents.fill_rect(x, gauge_y, width, 6, gauge_back_color)
    contents.gradient_fill_rect(x, gauge_y, fill_w, 6, color1, color2)
  end


This method draws a gauge on the screen and takes six parameters: x position, y position, the width of the gauge, the percentage of the gauge that's filled (1.0 being 100%), the colour to use for the left side, and the colour to use for the right side.

We set fill_w to the gauge width multiplied by the percentage fill as an integer. Then gauge_y is set to the passed-in y position plus line_height - 8 (this will draw the gauge 16 pixels lower than the coordinate you specify in the call, since gauges are usually drawn near the bottom of text and the calls to draw_gauge use the same y as the text is drawn at). We fill a rect in contents starting at (x, gauge_y) which is width pixels wide and 6 pixels tall using the gauge back colour, then horizontally gradient fill a rect starting at (x, gauge_y) which is fill_w pixels wide and 6 pixels tall using color1 and color2.

def draw_icon(icon_index, x, y, enabled = true)
    bitmap = Cache.system("Iconset")
    rect = Rect.new(icon_index % 16 * 24, icon_index / 16 * 24, 24, 24)
    contents.blt(x, y, bitmap, rect, enabled ? 255 : translucent_alpha)
  end


This method draws an icon in the window and takes four parameters: the icon index, x position, y position, and an enabled flag which, if false, will cause the icon to be semi-transparent.

We set bitmap to the cached "Iconset" graphic in the system folder and rect to a new instance of Rect, passing in the icon index mod 16 multiplied by 24 for x, icon index divided by 16 multiplied by 24 for y, 24 for width and 24 for height.

If you look at Iconset, we can decipher this algorithm as well. There are 16 icons per row, so we mod by 16 to get the column, then multiply by 24 because the icons are 24x24 in size. This gives us the left edge of the icon we want. For y, we divide by 16 to get the row and then multiply by 24 to get the top edge of the icon. This gives us the top left pixel of the icon, then we give width and height 24 because, again, icons are 24x24.

Finally, we do a block transfer to (x, y) of contents using bitmap as a source and rect as the source rect, with an alpha of 255 if enabled is true and translucent_alpha otherwise.

I haven't really done this before now and probably should have, but let me give a bit more of an explanation of block transfer using images to support the example. Some people seem to think there's more to displaying animation frames and things from spritesheets than there actually is, but it's literally just a case of block transferring the portion of the full graphic that corresponds to the graphic you want to show.

Let's create a new 100x100 window and position it at 320x240.

example = Window_Base.new(100, 100, 320, 240)




Now let's block transfer part of Iconset into it. Let's start at (134, 156) and make the width 100 and the height 25.


(the red rectangle shows which part of the graphic this rect covers)

bitmap = Cache.system("Iconset")
    rect = Rect.new(134, 156, 100, 25)
    example.contents.blt(0, 0, bitmap, rect, 255)




This also shows another interesting-ish thing, which is that due to the padding settings, (0, 0) of the window is actually 12 pixels right and down from the window's outer border. This effectively gives a 6x6 inner padding and then it'll start displaying whatever is being block transferred.

Note as well that the coordinates are relative to the window position. You don't have to figure out exactly where on the screen to show something, just work out its position inside the window and use that as the coordinates.

def draw_face(face_name, face_index, x, y, enabled = true)
    bitmap = Cache.face(face_name)
    rect = Rect.new(face_index % 4 * 96, face_index / 4 * 96, 96, 96)
    contents.blt(x, y, bitmap, rect, enabled ? 255 : translucent_alpha)
    bitmap.dispose
  end


This method draws a face and follows pretty much the same principles as the draw_icon method. It had one extra parameter, which is the filename for the face graphic, but otherwise it's pretty much the same. The rect x is passed as face_index % 4 * 96 (because there are 4 faces per row and each one is 96 pixels wide) and the y is passed as face_index / 4 * 96 for the same reason. One thing you may note is that we dispose the bitmap at the end of this method but not in draw_icon. This may be because icons tend to get used a lot, while face graphics are used less often. Or it may be a coding oversight, who knows?

def draw_character(character_name, character_index, x, y)
    return unless character_name
    bitmap = Cache.character(character_name)
    sign = character_name[/^[\!\$]./]
    if sign && sign.include?('$')
      cw = bitmap.width / 3
      ch = bitmap.height / 4
    else
      cw = bitmap.width / 12
      ch = bitmap.height / 8
    end
    n = character_index
    src_rect = Rect.new((n%4*3+1)*cw, (n/4*4)*ch, cw, ch)
    contents.blt(x - cw / 2, y - ch, bitmap, src_rect)
  end


This method draws a character graphic in the window and follows the same principles but doesn't have the enabled flag like the previous two.

We return unless a character name is specified, as there's no point in continuing otherwise.

bitmap is set to the cached graphic with character_name as its filename. We set a variable called sign to the result of a regex match in character_name looking for a ! or a $ at the beginning (can match both). If sign is not empty and includes $, we set cw to a third of the bitmap's width and ch to a quarter of its height (since $ means we want to treat the graphic as a single character; 3 animation frames per direction, 4 directions). Otherwise cw is a 12th of the width and ch is an 8th of the height (because there are 4 characters across 2 rows in a normal sheet, there will be 12 poses horizontally and 8 vertically).

n is set to the character index, then the source rect is defined as...oh dear, that looks a bit more complicated than the other ones!

But it's not really. Okay, so we have a new Rect. What's the starting x? The index mod 4 multiplied by 3 plus 1, multiplied by cw. We mod by 4 because there are 4 characters per row, multiply by 3 because there are 3 frames across, add 1 because the "idle" pose is the middle one, and multiply by cw to get the actual starting pixel for the left edge of the character. What's the starting y? The index divided by 4 multiplied by 4, multiplied by ch. We divide by 4 because there are 4 characters per row, multiply by 4 because there are 4 directions, and multiply by ch to get the actual starting pixel for the top edge of the character. The rect will be cw pixels wide and ch pixels tall, which are the exact dimensions of a single frame.

With our rect in hand, we do a block transfer at (x - half of cw, y - ch) which will draw the character slightly to the left and a full character height above the passed-in coordinates (meaning using the default dimensions that (16, 32) is the top-leftmost coordinate you can call this method at and still show the full character).

def hp_color(actor)
    return knockout_color if actor.hp == 0
    return crisis_color if actor.hp < actor.mhp / 4
    return normal_color
  end


This method gets the colour to use for HP text, and takes actor as a parameter. We return the knockout colour if the actor's HP is 0, the crisis colour if the actor's HP is less than a quarter of the maximum, and the normal colour otherwise.

def mp_color(actor)
    return crisis_color if actor.mp < actor.mmp / 4
    return normal_color
  end


Pretty much the same for MP only there's no knockout colour for it.

def tp_color(actor)
    return normal_color
  end


And for TP colour it's just straight-up normal, there is no TP crisis.

def draw_actor_graphic(actor, x, y)
    draw_character(actor.character_name, actor.character_index, x, y)
  end


This method draws a particular actor's graphic and takes actor, x and y as parameters. It simply calls draw_character passing in the actor's character name and index, and the passed-in x/y coordinates.

def draw_actor_face(actor, x, y, enabled = true)
    draw_face(actor.face_name, actor.face_index, x, y, enabled)
  end


Same thing but for the actor's face graphic. Has an additional enabled parameter to flag when the face should be transparent.

def draw_actor_name(actor, x, y, width = 112)
    change_color(hp_color(actor))
    draw_text(x, y, width, line_height, actor.name)
  end


This method draws the actor's name and takes 4 parameters: actor, x, y and width, which defaults to 112 if no width is passed to it. We change the text colour to the passed-in actor's HP colour, then draw their name in that colour at the given coordinates.

def draw_actor_class(actor, x, y, width = 112)
    change_color(normal_color)
    draw_text(x, y, width, line_height, actor.class.name)
  end


This method draws the actor's character class and takes the same parameters as the name method. The only difference is the colour being changed to and the text being drawn.

def draw_actor_nickname(actor, x, y, width = 180)
    change_color(normal_color)
    draw_text(x, y, width, line_height, actor.nickname)
  end


Pretty much the same thing again for nickname, though the default width for this is 180 rather than 112.

def draw_actor_level(actor, x, y)
    change_color(system_color)
    draw_text(x, y, 32, line_height, Vocab::level_a)
    change_color(normal_color)
    draw_text(x + 32, y, 24, line_height, actor.level, 2)
  end


This method draws the actor's level. We change to the system colour and draw the abbreviated term for level from Vocab, then change to normal colour and draw the actual level digit 32 pixels further to the right and right-aligned.

def draw_actor_icons(actor, x, y, width = 96)
    icons = (actor.state_icons + actor.buff_icons)[0, width / 24]
    icons.each_with_index {|n, i| draw_icon(n, x + 24 * i, y) }
  end


This method draws the actor's state/buff/debuff icons and takes the same parameters as the other drawing methods, though the default width for this one is 96.

We set icons to elements 0 to a 24th of the width of an array combining the actor's state icons and buff icons. The reason we limit it to width / 24 is that by default only 4 icons show when displaying states and each icon is 24x24.

We then iterate through each element of icons with an index, using the iteration variable n and index variable i, and draw the icon with ID n at the passed-in x position plus 24 times the index (so they will appear in a row), and the passed-in y.

def draw_current_and_max_values(x, y, width, current, max, color1, color2)
    change_color(color1)
    xr = x + width
    if width < 96
      draw_text(xr - 40, y, 42, line_height, current, 2)
    else
      draw_text(xr - 92, y, 42, line_height, current, 2)
      change_color(color2)
      draw_text(xr - 52, y, 12, line_height, "/", 2)
      draw_text(xr - 42, y, 42, line_height, max, 2)
    end
  end


This method draws current and maximum values as a fraction and takes seven parameters: x position, y position, width, current value, maximum value, colour 1 and colour 2.

We change the text colour to color1, and set xr to x plus width. If width is less than 96, we draw the current value 40 pixels to the left of xr. Otherwise, we draw the current value 92 pixels to the left, change to colour 2, draw a forward slash 52 pixels to the left, then draw the maximum value 42 pixels to the left.

Basically this means that if there are less than 96 pixels available for drawing the values, it'll just write the current and won't bother with the rest.

def draw_actor_hp(actor, x, y, width = 124)
    draw_gauge(x, y, width, actor.hp_rate, hp_gauge_color1, hp_gauge_color2)
    change_color(system_color)
    draw_text(x, y, 30, line_height, Vocab::hp_a)
    draw_current_and_max_values(x, y, width, actor.hp, actor.mhp,
      hp_color(actor), normal_color)
  end


This method draws an actor's HP, taking actor, x, y and width as parameters. Width defaults to 124.

First we draw a gauge at (x, y), width pixels wide and using the actor's hp_rate property to determine how much of the gauge to fill in, with hp_gauge_color1 as the left colour and hp_gauge_color2 as the right.

We then change to the system colour and draw the abbreviated HP term from Vocab at (x, y), and the actor's HP/max HP are passed to draw the current and max values at (x, y) with the actor's HP colour for the left colour and normal_color for the right.

def draw_actor_mp(actor, x, y, width = 124)
    draw_gauge(x, y, width, actor.mp_rate, mp_gauge_color1, mp_gauge_color2)
    change_color(system_color)
    draw_text(x, y, 30, line_height, Vocab::mp_a)
    draw_current_and_max_values(x, y, width, actor.mp, actor.mmp,
      mp_color(actor), normal_color)
  end


Pretty much the same for actor MP. Obviously the HP-specific things use the ones related to MP instead, but it's otherwise the same.

def draw_actor_tp(actor, x, y, width = 124)
    draw_gauge(x, y, width, actor.tp_rate, tp_gauge_color1, tp_gauge_color2)
    change_color(system_color)
    draw_text(x, y, 30, line_height, Vocab::tp_a)
    change_color(tp_color(actor))
    draw_text(x + width - 42, y, 42, line_height, actor.tp.to_i, 2)
  end


The method for drawing actor TP is almost the same, but there are a couple of differences. The main one is that we only need to draw the current TP value, so it's another call to draw_text instead of using draw_current_and_max_values. We also draw the TP number a bit further to the right.

def draw_actor_simple_status(actor, x, y)
    draw_actor_name(actor, x, y)
    draw_actor_level(actor, x, y + line_height * 1)
    draw_actor_icons(actor, x, y + line_height * 2)
    draw_actor_class(actor, x + 120, y)
    draw_actor_hp(actor, x + 120, y + line_height * 1)
    draw_actor_mp(actor, x + 120, y + line_height * 2)
  end


This method draws the simple status for an actor, taking actor, x and y as parameters. This draws the actor's name, level, icons, class, HP and MP (note that level and HP are using line_height * 1 because they're drawn 1 line below name and class, and icons and MP are using line_height * 2 because they're drawn 1 line below that).

def draw_actor_param(actor, x, y, param_id)
    change_color(system_color)
    draw_text(x, y, 120, line_height, Vocab::param(param_id))
    change_color(normal_color)
    draw_text(x + 120, y, 36, line_height, actor.param(param_id), 2)
  end


This method draws the stat values for an actor, taking actor, x, y and stat ID as parameters (I'm calling it stat here to avoid confusion between those and function parameters in Javascript).

We change the colour to the system colour and then draw the name of the parameter with the given ID as defined in Vocab. We change the colour to the normal colour and then draw the value of that parameter 120 pixels further right.

def draw_item_name(item, x, y, enabled = true, width = 172)
    return unless item
    draw_icon(item.icon_index, x, y, enabled)
    change_color(normal_color, enabled)
    draw_text(x + 24, y, width, line_height, item.name)
  end


This method draws the name of an item, taking item, x, y, an enabled flag, and width as parameters. Width defaults to 172.

We return unless there's an item object, because there's no point in drawing one if there isn't. We draw the item's icon passing in its icon index, change to the normal colour (with partial transparency depending on the value of enabled) and then draw the item's name 24 pixels to the right of the icon.

def draw_currency_value(value, unit, x, y, width)
    cx = text_size(unit).width
    change_color(normal_color)
    draw_text(x, y, width - cx - 2, line_height, value, 2)
    change_color(system_color)
    draw_text(x, y, width, line_height, unit, 2)
  end


This method draws a quantity of a given currency unit, taking value, unit, x, y and width as parameters. We set cx to the width required to display the unit text, change to the normal colour, and draw value in the window at (x, y) (setting the width to the passed-in width minus cx minus 2 ensures that the value is drawn to the left of the unit, since it's right-aligned). Then we change to the system colour and draw the unit name.

def param_change_color(change)
    return power_up_color   if change > 0
    return power_down_color if change < 0
    return normal_color
  end


This method gets the colour for a parameter change, taking change as a parameter (the amount the parameter has gone up/down by). We return the power up colour if change is greater than 0, power down colour if change is less than 0, or the normal colour otherwise.

And that, dear friends, is the basis of a window! It's taken over 40,000 characters to break it down for you, so I think we'd better leave it there for now. Coming up next is Window_Selectable, which is the one that gives you cursor movement and scrolling! Fun for everyone.

As with Jump into Javascript, I'd really appreciate some more comments on these even if it's just to let me know people are actually reading and getting something out of the series. I hope you're all finding it helpful regardless, and I'll keep doing it even if I never see a single view.

Until next time.