SLIP INTO RUBY - UNDER THE HOOD PART 19: MOAR WINDOWS

I accidentally overwrote the master copy of this one, so I've had to redo it.

  • Trihan
  • 05/30/2017 02:05 PM
  • 1900 views
(Text taken verbatim from the Patreon page as I lost the RMN version)

It's that time again, folks. Time for another early-access (and late) edition of



Today: MOAR WINDOWS

Window_NameEdit
This is the window used to display an actor's name and face in the name input scene. It inherits from Window_Base and has three public instance variables:

attr_reader :name # name
attr_reader :index # cursor position
attr_reader :max_char # maximum number of characters


:name is the name of the actor, :index is the position of the cursor in the name string, and :max_char is the maximum number of characters the player can enter.

def initialize(actor, max_char)
  x = (Graphics.width - 360) / 2
  y = (Graphics.height - (fitting_height(4) + fitting_height(9) + 8)) / 2
  super(x, y, 360, fitting_height(4))
  @actor = actor
  @max_char = max_char
  @default_name = @name = actor.name[0, @max_char]
  @index = @name.size
  deactivate
  refresh
end


The constructor takes two parameters: the actor object representing the actor for whom the player is entering a new name, and the maximum number of chars that the name can be.

We set a temp variable called x to half of (the game window width minus 360) and y to half of (the game window height minus the fitting height for 13 lines of text plus 8 pixels). What this will basically do is position the window in the centre of the screen and 8 pixels above the grid of letters (which is 9 lines high; the window edit window is 4). We then pass these in as x and y coordinates to the super method, passing in a width of 360 and a height of the fitting height for 4 lines. After this we set @actor to the actor parameter, @max_char to the max_char parameter, @default_name and @name to the actor's name truncated to @max_char characters, @index to the length of the name (using the size method), and finally we deactivate and refresh the window. Phew!

def restore_default
  @name = @default_name
  @index = @name.size
  refresh
  return !@name.empty?
end


This method reverts the name being edited to the default (what it started as). We set @name to @default_name, @index to the length of the name, then refresh the window. Finally, we return true if @name is not empty, and false otherwise.

def add(ch)
  return false if @index >= @max_char
  @name += ch
  @index += 1
  refresh
  return true
end


This method adds a character to the name, taking ch as a parameter, which is the character to add. We return false if @index is greater than or equal to @max_char, as that means we can no longer add characters. Following that check, we add ch to @name (the great thing about Ruby is that you can use the + operator on strings to concatenate them), add 1 to @index, refresh the window, and return true.

def back
  return false if @index == 0
  @index -= 1
  @name = @name[0, @index]
  refresh
  return true
end


This method goes back a character. We return false if @index is 0, because we can't go any further back than the beginning of the name. Then we subtract 1 from index, and set @name to itself truncated from character 0 up to @index. Then we refresh the window and return true.

def face_width
  return 96
end


This method gets the width of the face graphic, and is hardcoded at 96 pixels.

def char_width
  text_size($game_system.japanese? ? "あ" : "A").width 
end


This method gets the width of a character. We return the width property of the result of calling text_size, passing in "あ" if the game is using Japanese localisation, and "A" otherwise.

def left
  name_center = (contents_width + face_width) / 2
  name_width = (@max_char + 1) * char_width
  return [name_center - name_width / 2, contents_width - name_width].min
end


This method gets the coordinates of the left side of the window for drawing the name. We set a temp variable called name_center to half of the contents width plus the width of the face (which will centre the name in the remaining space between the face and the edge), then one called name_width to 1 more than @max_char multiplied by char_width (we add 1 because the index starts at 0 and we want the width of the actual number of characters allowable), and finally we return the minimum value between half of name_center minus name_width, or the contents width minus name_width.

def item_rect(index)
  Rect.new(left + index * char_width, 36, char_width, line_height)
end


The overloaded item_rect method returns a new Rect with an x coordinate of left plus index multiplied by char_width, a y coordinate of 36, a width of char_width and a height of line_height. This will display the cursor as a small rectangle at the position of the character currently being entered.

def underline_rect(index)
  rect = item_rect(index)
  rect.x += 1
  rect.y += rect.height - 4
  rect.width -= 2
  rect.height = 2
  rect
end


This method determines the rect for drawing the dotted underlines, taking index as a parameter. We set a temp variable called rect to the result of calling item_rect passing in index. We add 1 to the rect's x, and the rect's height - 4 to the rect's y. Then we reduce its width by 2 and set its height to 2, and return rect. This will give us a 2-pixel-high rect with coordinates just underneath those of a letter.

def underline_color
  color = normal_color
  color.alpha = 48
  color
end


This method gets the colour for the underline. First we set a temp variable called color to normal_color, then we set its alpha to 48, and finally return color. So basically the underline colour is just the normal colour with an alpha value of 48. Crazy.

def draw_underline(index)
  contents.fill_rect(underline_rect(index), underline_color)
end


This method draws the underline, taking index as a parameter. We call fill_rect on the window contents, passing in the underline_rect for index as the rect, and underline_color as the colour. This will draw a small partially-transparent underline underneath a letter.

def draw_char(index)
  rect = item_rect(index)
  rect.x -= 1
  rect.width += 4
  change_color(normal_color)
  draw_text(rect, @name[index] || "")
end


This method is used to draw a character in the window (which is used to draw the name) and takes index as a parameter. We set a temp variable called rect to the item_rect for index, then subtract 1 from its x property and add 4 to its width. Then we change to normal_color and draw the character in @name at index, or "" if there isn't a character to draw.

def refresh
  contents.clear
  draw_actor_face(@actor, 0, 0)
  @max_char.times {|i| draw_underline(i) }
  @name.size.times {|i| draw_char(i) }
  cursor_rect.set(item_rect(@index))
end



The refresh method first clears the window contents, then draws the actor's face at (0, 0). We execute a loop with @max_char iterations using the iteration variable "i" and in the block draw the underline passing in i as the index. Then we do the same thing for the size of @name, calling draw_char. Finally, we set the cursor rect to the item_rect for @index. These three lines will draw the actor's current name, an underline for every possible letter in the name, and highlight the character currently being entered.

Window_NameInput
This is the window used to select the characters while entering an actor's name in the name input scene. It inherits from Window_Selectable.

The first thing in this class is some arrays of character tables, like this one:

LATIN1 = [ 'A','B','C','D','E', 'a','b','c','d','e',
'F','G','H','I','J', 'f','g','h','i','j',
'K','L','M','N','O', 'k','l','m','n','o',
'P','Q','R','S','T', 'p','q','r','s','t',
'U','V','W','X','Y', 'u','v','w','x','y',
'Z','[',']','^','_', 'z','{','}','|','~',
'0','1','2','3','4', '!','#','$','%','&',
'5','6','7','8','9', '(',')','*','+','-',
 '/','=','@','<','>', ':',';',' ','Page','OK']


There are four in total. LATIN1, LATIN2, JAPAN1, JAPAN2 and JAPAN3. As you can see here, for ease of reading they are arranged in the code in the same layout the player will see them in when entering a name. This is purely for convenience and has no actual effect when it comes to processing.

def initialize(edit_window)
  super(edit_window.x, edit_window.y + edit_window.height + 8, edit_window.width, fitting_height(9))
  @edit_window = edit_window
  @page = 0
  @index = 0
  refresh
  update_cursor
  activate
end


The constructor for this class takes one parameter: edit_window, which will be an instance of Window_NameEdit. First we call the super method passing in the edit window's x, the edit window's y plus its height plus 8 (which will place this one 8 pixels beneath it), the edit window's width, and the fitting height for 9 lines of text. We set @edit_window to the edit_window parameter, @page to 0, and @index to 0, before refreshing the window, updating the cursor, and activating the window.

def table
  return [JAPAN1, JAPAN2, JAPAN3] if $game_system.japanese?
  return [LATIN1, LATIN2]
end


This method gets the table used to determine the characters available for name entry. If the game system is set to use Japanese, we return an array consisting of JAPAN1, JAPAN2 and JAPAN3. Otherwise, we return an array consisting of LATIN1 and LATIN2. If you wanted other languages in your game, you could add additional character tables and alias this method to return your tables when other languages are selected.

def character
  @index < 88 ? table[@page][@index] : ""
end


This method gets the character currently selected. We have a ternary if here with the condition "@index < 88"; if the condition is true, we return the element of table[@page] at @index, otherwise we return a blank string (the reason we check for the index being less than 88 is that 87 is the last index that isn't "page" or "ok").

The 2D array structure makes picking out the current character nice and easy to understand. Let's say we're on page 0 of the Latin character set, and the index is currently 52; this will return table[0][52], and if we count along the elements in LATIN1 (the table at index 0) until index 52, we see that it's a circumflex (^).

def is_page_change?
  @index == 88
end


This method determines whether the cursor is currently on the "page" option, and simply returns true if @index is equal to 88 and false if not.

def is_ok?
  @index == 89
end


This is a similar method to determine whether the cursor is currently on the "ok" option, returning true if @index is equal to 89 and false otherwise.

def item_rect(index)
  rect = Rect.new
  rect.x = index % 10 * 32 + index % 10 / 5 * 16
  rect.y = index / 10 * line_height
  rect.width = 32
  rect.height = line_height
  rect
end


As with the previous window class, as the highlight rect here is slightly different from standard selectable windows we need to overwrite it. First we get a new Rect and assign it to the temp variable rect. Then we set the rect's x coordinate to the current index MOD 10 multiplied by 32 plus index mod 10 divided by 5 multiplied by 16. Then we set the rect's y coordinate to the index divided by 10 multiplied by line_height. The width is set to 32, and the height to line_height. Then we return rect.

Okay, so what the hell is going on with the numbers? It's not really any different from the arithmetic acrobatics we've dealt with before for things like slicing windowskin colours.

We mod the index by 10 because there are 10 items per row (this gives us the x for the current column regardless of what row we're on) which is multiplied by 32 because that's the width of each character. The added part is to put extra space between the left five characters and the right five characters, which also results in the edges being equal. Again, we mod by 10 to get the actual column, divide by 5 so that only characters 5+ will be affected (since the ones below this will round down to 0) multiplied by 16 because we're putting 16 pixels of space in the middle. Not as complex as it looks!

The principle is the same for the y coordinate. We divide by 10 to get the row (remember that as we're dealing with integers, fractions will automatically round down), then multiply that by line_height to get the y of the line we're on.

def refresh
  contents.clear
  change_color(normal_color)
  90.times {|i| draw_text(item_rect(i), table[@page][i], 1) }
end


When refreshing the window, we clear the contents and change to normal_color. Then we start a 90-iteration loop using the iteration variable "i"; in the block, we call draw_text passing in the item_rect for i as the rect, table[@page][i] as the text, and 1 for align (center).

def update_cursor
  cursor_rect.set(item_rect(@index))
end


We overwrite the update_cursor method so it just set the cursor_rect to the item_rect for @index. This overwrite isn't strictly necessary as it doesn't make any functional difference to using the update_cursor method from Window_Selectable, but it does cut down on code that this window doesn't actually need (such as checking for the @cursor_all flag).

def cursor_movable?
  active
end


And we also overwrite cursor_movable? to return true if the window is active. This one is necessary due to item_max for this window returning 0 (which would prevent you from moving the cursor if we used the cursor_movable? method from Window_Selectable).

def cursor_down(wrap)
  if @index < 80 or wrap
    @index = (index + 10) % 90
  end
end


We overwrite cursor_down, again because the original uses item_max and col_max in its calculations and this window uses neither. If @index is less than 80 or the wrap parameter is true, we set @index to (index plus 10) MOD 90. It's fairly easy to understand the logic here: there are 10 items per row, so moving down would be moving down 10 items, and we mod by 90 because that's how many items there are. This is only relevant if wrap is true, because if this is the case moving down while on the bottom row needs to go back to the top (let's say the player has index 82 highlighted, pressing the down key would result in index being set to (82 + 10) % 90 = 92 % 90 = 2, so the index will go back to 2).

def cursor_up(wrap)
  if @index >= 10 or wrap
    @index = (index + 80) % 90
  end
end


The overwrite of cursor_up is similar, but we add 80 to index instead of 10. This might look a bit counterintuitive, but essentially it's to wrap back to the bottom if the player was on the top row. The alternative would be to subtract 10 from the index, but then it would end up being a negative value, and modding, say, -8 by 90 still gives -8.

def cursor_right(wrap)
  if @index % 10 < 9
    @index += 1
  elsif wrap
    @index -= 9
  end
end


Next up is the overwrite of cursor_right. if @index MOD 10 is less than 9, we add 1 to @index. Otherwise, if wrap is true, we subtract 9 from @index. Obviously the only index at which @index % 10 will be 9 or more is at index 9 (the far right of the window) so the net effect of the elsif is returning the index to whatever it is on the far left of the current row.

def cursor_left(wrap)
  if @index % 10 > 0
    @index -= 1
  elsif wrap
    @index += 9
  end
end


cursor_left is a similar thing but obviously in the opposite direction. This time we're checking for @index MOD 10 being greater than 0; if so, we subtract 1 from index. Otherwise, if wrap is true, we add 9 to @index.

def cursor_pagedown
  @page = (@page + 1) % table.size
  refresh
end


The overwrite to cursor_pagedown sets @page to itself plus 1 MOD the size of the character table array, then refreshes the window. Modding by the array size allows it to wrap back to page 0 if the player was on the last one.

def cursor_pageup
  @page = (@page + table.size - 1) % table.size
  refresh
end


cursor_pageup is almost identical, though we're adding the array size - 1 to @page rather than 1. The modulo operation either gives us 1 less than the current page, or the last one if we were already on page 0.

def process_cursor_move
  last_page = @page
  super
  update_cursor
  Sound.play_cursor if @page != last_page
end


The overwrite for process_cursor_move sets a temp variable called last_page to @page, then calls the super method, then updates the cursor, and finally plays the cursor SE if @page is not equal to last_page (this allows the cursor sound to play on page up/page down, which won't normally happen).

def process_handling
  return unless open? && active
  process_jump if Input.trigger?(:A)
  process_back if Input.repeat?(:B)
  process_ok if Input.trigger?(:C)
end


This method handles processing for inputs. We return unless the window is open and active. If the player presses the :A key (shift by default) we call process_jump. If they're holding down the :B key (esc by default) we call process_back, and if they press the :C key (enter by default) we call process_ok.

def process_jump
  if @index != 89
    @index = 89
    Sound.play_cursor
  end
end


This method is called when the player presses :A. If @index is not equal to 89, we set it to 89 and play the cursor sound. This will jump the cursor to the "OK" selection.

def process_back
  Sound.play_cancel if @edit_window.back
end


This method is called when the player presses (or holds) :B. We play the cancel sound if the back method of @edit_window returns true; as we covered in the previous class, this method returns true if the index was greater than 0.

def process_ok
  if !character.empty?
    on_name_add
  elsif is_page_change?
    Sound.play_ok
    cursor_pagedown
  elsif is_ok?
    on_name_ok
  end
end


This method is called when the player presses :C. If the current character is not empty, we call on_name_add. Otherwise, if the "page" option is selected, we play the OK sound and call cursor_pagedown. Otherwise if the "ok" option is selected, we call on_name_ok.

def on_name_add
  if @edit_window.add(character)
    Sound.play_ok
  else
    Sound.play_buzzer
  end
end


on_name_add is the method called when the player presses OK on a character. If the add method of @edit_window returns true when the selected character is passed in, we play the OK sound. Otherwise, we play the buzzer sound.

def on_name_ok
  if @edit_window.name.empty?
    if @edit_window.restore_default
      Sound.play_ok
    else
      Sound.play_buzzer
    end
  else
    Sound.play_ok
    call_ok_handler
  end
end


on_name_ok is the method called when the player presses OK on the OK option. If the @edit_window's name is empty, then we check whether its restore_default method returns true. If so, we play the OK sound, and otherwise we play the buzzer sound. If the name is not empty, we play the OK sound and then call call_ok_handler.

Window_ChoiceList
This is the window that shows the available choices for a "Show Choices" event command. It inherits from Window_Command.

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


The constructor takes message_window as a parameter, which will be an instance of Window_Message. We set the @message_window instance variable to the parameter value, call the super method passing in coordinates of (0, 0), set the window's openness to 0 (so it's closed to begin with) and then deactivate it so that it won't process input.

def start
  update_placement
  refresh
  select(0)
  open
  activate
end


The start method allows the window to start processing input. We call update_placement, which we'll look at next, then refresh the window, select the first item in it, open it, and activate it.

def update_placement
  self.width = [max_choice_width + 12, 96].max + padding * 2
  self.width = [width, Graphics.width].min
  self.height = fitting_height($game_message.choices.size)
  self.x = Graphics.width - width
  if @message_window.y >= Graphics.height / 2
    self.y = @message_window.y - height
  else
    self.y = @message_window.y + @message_window.height
  end
end


This method updates the window placement. We set its width to the maximum value between (max_choice_width + 12) and 96, plus double padding. We then set it to the minimum value between width and the width of the game window (as we don't want it to be wider than the screen). We set the window height to the fitting height for the number of choices in $game_message. We set the x coordinate to the game window width minus width (which places it on the right hand side). Then, if the y coordinate of the message window is greater than or equal to half the game window height (in other words, it's on the bottom) we set this window's y to the y of the @message_window minus height (placing it above the message). Otherwise, we set this window's y for the y of the @message_window plus its height (which places it below).

def max_choice_width
  $game_message.choices.collect {|s| text_size(s).width }.max
end


This method gets the maximum width of a choice. We call collect on $game_message's choices property, using block variable "s". In the block, we return the width of calling text_size on s. Then we return the max value returned by the collect, which gives us the width of the widest choice.

def contents_height
  item_max * item_height
end


This overwrite of the parent contents_height method returns the maximum number of items multiplied by the height of an item. As far as I can tell this makes no actual difference and you'll get the same result from calling the parent method.

def make_command_list
  $game_message.choices.each do |choice|
    add_command(choice, :choice)
  end
end


To make the command list, we execute an each loop on $game_message's choices property, using the iteration variable "choice". In the block, we call add_command, passing in the choice and :choice as the symbol (the handlers will be determined by another method, as we'll see).

def draw_item(index)
  rect = item_rect_for_text(index)
  draw_text_ex(rect.x, rect.y, command_name(index))
end


To draw an item in this window, we set rect to the result of item_rect_for_text passing in index, then call draw_text_ex, passing in an x coordinate of the rect's x, a y coordinate of the rect's y, and the result of command_name passing in index which will give us the name we gave the command when adding it.

def cancel_enabled?
  $game_message.choice_cancel_type > 0
end


This method gets whether "cancel" is enabled in the choice window, returning true if the choice_cancel_type property of $game_message is greater than 0.

def call_ok_handler
  $game_message.choice_proc.call(index)
  close
end


This method calls the OK handler for the current choice. We call the choice_proc of $game_message passing in index, then close the window.

def call_cancel_handler
  $game_message.choice_proc.call($game_message.choice_cancel_type - 1)
  close
end


This method calls the cancel handler for the choice window; the only difference from the ok handler is the value we're passing in, which in this case is the choice_cancel_type property minus 1.

Now that we've got the second half of this, I can finish explaining how exactly choices work, which I covered a little way back when we looked at Game_Interpreter.

So when a choice window is set up, the interpreter runs the following line:

$game_message.choice_proc = Proc.new {|n| @branch[@indent] = n }


What this does is stores a Proc in the "choice_proc" identifier, which takes a value and sets @branch[@indent] to the value that was passed in.

For example, when we call call_ok_handler, if index is 2, then the Proc will use 2 as the value for n, and @branch[@indent] will be set to 2. This tells the interpreter which branch of the show choice tree to execute. How does it do this? Well...

Okay, so when you use "Show Choices" as an event command, what is actually done behind the scenes is that the beginning of each branch is set up as a call to command_402 in Game_Interpreter. When the interpreter reaches this command code, it skips the command if @branch[@indent] is not equal to @params[0], which at the time will contain the ID of the branch being looked at. In other words, it'll only run branches that match the value that the proc passed in.

Window_NumberInput
This window is used to input digits for the "Input Number" event command, and inherits from Window_Base.

def initialize(message_window)
  @message_window = message_window
  super(0, 0, 0, 0)
  @number = 0
  @digits_max = 1
  @index = 0
  self.openness = 0
  deactivate
end


The constructor is similar to that of Window_ChoiceList, taking the same parameter. Again, we set the @message_window instance variable to the parameter value. We call the super method (though in this case we pass in a width and height of 0 as the size of the window will be determined by the maximum digits). We set @number to 0, @digits_max to 1, @index to 0, the openness of the window to 0 (again so that it won't be visible to begin with) and then deactivate it to prevent it from responding to input.

def start
  @digits_max = $game_message.num_input_digits_max
  @number = $game_variables[$game_message.num_input_variable_id]
  @number = [[@number, 0].max, 10 ** @digits_max - 1].min
  @index = 0
  update_placement
  create_contents
  refresh
  open
  activate
end


The start method is also similar to the previous one. We set @digits_max to the num_input_digits_max property of $game_message (which was set by the "digits" value you entered when creating the Input Number command), and @number to the value of $game_variables with an index matching num_input_variable_id, which, again, was set by the value entered when the command was created. @number is then set to the minimum value between the maximum number between @number and 0, and 10 to the power of the max number of digits minus 1. Again I hear you cry, wha?

This is basically just a very complicated-looking way to clamp the number's value. [@number, 0].max just prevents the value from going into negatives, and 10 ** @digits_max - 1 will convert to the maximum value you could possibly enter for the number with the number of digits available.

For example, let's say you have an Input Number command and select variable 5, which already contains the value 23984, and set the max digits to 3.

First of all, @number will be set to 23984, so far so good.

Then, [23984, 0].max will return 23984, and 10 ** 3 - 1 will return 999. The minimum value between 23984 and 999 is, of course, 999--which incidentally is also the highest value you'd be able to enter with the number of digits you've given the player, so that's what the number is set to initially.

Anyway, after this mathematical fun we set @index to 0, call update_placement, then call create_contents, refresh the window, open it, and finally activate it.

def update_placement
  self.width = @digits_max * 20 + padding * 2
  self.height = fitting_height(1)
  self.x = (Graphics.width - width) / 2
  if @message_window.y >= Graphics.height / 2
    self.y = @message_window.y - height - 8
  else
    self.y = @message_window.y + @message_window.height + 8
  end
end


update_placement should look relatively familiar by now as well. So we set the width of the window to @digits_max multiplied by 20 plus double the padding. This basically gives you 20 pixels per digit and the padding amount on either side. The height is simply set to the fitting height for 1 line, as that's all we need.

The window's x is set to half of (the game window width minus the window's width) which will centre it on the x axis. Then, if the message window's y coordinate is greater than half the game window height (it's on the bottom), this window's y is set to the message window's y minus this window's height minus 8, which just puts it 8 pixels above the message. Otherwise, it's set to the message window's y plus the message window's height plus 8, which puts it 8 pixels below.

def cursor_right(wrap)
  if @index < @digits_max - 1 || wrap
    @index = (@index + 1) % @digits_max
  end
end


We need to overwrite cursor_right to give slightly different behaviour: if @index is less than 1 less than @digits_max OR the wrap flag is true, we set @index to (@index plus 1) MOD @digits_max. You should be accustomed enough to this kind of construct by now that I can stop explicitly explaining what it does.

def cursor_left(wrap)
  if @index > 0 || wrap
    @index = (@index + @digits_max - 1) % @digits_max
  end
end


Same thing for for moving the cursor left, so we're checking for @index being greater than 0, and setting it to @index plus @digits_max minus 1 before modding it.

def update
  super
  process_cursor_move
  process_digit_change
  process_handling
  update_cursor
end


We overwrite the frame update method: first we call the super method as always, then we call process_cursor_move, process_digit_change, process_handling and update_cursor. This is necessary because with this window inheriting from Window_Base and not Window_Selectable it has no inherited code for handling cursor movement and we have to add it manually.

def process_cursor_move
  return unless active
  last_index = @index
  cursor_right(Input.trigger?(:RIGHT)) if Input.repeat?(:RIGHT)
  cursor_left (Input.trigger?(:LEFT)) if Input.repeat?(:LEFT)
  Sound.play_cursor if @index != last_index
end


This method processes cursor movement. We return to the calling method unless the window is active, because we don't want to move the cursor if it isn't. Then we set last_index to @index. We call cursor_right if the player is pressing or holding the :RIGHT key passing in whether the player has just pressed it, and the same thing for cursor_left and :LEFT. Then we play the cursor sound if @index is not the same as last_index. The reason the wrap parameter is passed this way is so that if you're holding down a key from, say, index 1, and reach the last index, it'll stay on the last index instead of looping around because Input.trigger?(:RIGHT) will return false due to the delay between when you initially pressed the key down and now.

def process_digit_change
  return unless active
  if Input.repeat?(:UP) || Input.repeat?(:DOWN)
    Sound.play_cursor
    place = 10 ** (@digits_max - 1 - @index)
    n = @number / place % 10
    @number -= n * place
    n = (n + 1) % 10 if Input.repeat?(:UP)
    n = (n + 9) % 10 if Input.repeat?(:DOWN)
    @number += n * place
    refresh
  end
end


This method processes a change in digits. We return to the calling method unless the window is active, again because we don't want to process input if it isn't. If the player is holding :UP or :DOWN, we play the cursor sound, then set place to 10 to the power of @digits_max minus 1 minus @index, then set n to @number divided by place MOD 10, then subtract n multiplied by place from @number, then set n to (n plus 1) MOD 10 if the player is holding :UP, and (n plus 9) MOD 10 if the player is holding :DOWN, and finally add n multiplied by place to number.

I think this might be another one I need to explain.

Okay, so say you've got a number with 6 maximum digits which is currently at 10537, and you're currently at index 3. You hit the up key.

place will be set to 10 ** (6 - 1 - 3) = 10 ** 2 = 100. n will be set to 10537 / 100 % 10 = 5, then @number will be reduced by 5 * 100 = 500 making it 10037. Then n is set to (5 + 1) % 10 = 6, and @number is increased by 6 * 100 = 600. The net effect is that the number goes up by 100.

Conversely, let's take a number with 5 maximum digits which is currently also at 10537, and you're currently at index 0. You hit down.

place will be set to 10 ** (5 - 1 - 0) = 10 ** 4 = 10000. n will be set to 10537 / 10000 % 10 = 1, then @number will be reduced by 1 * 10000 = 10000 making it 537. Then n is set to (1 + 9) % 10 = 0 and number is increased by 0 * 10000 = 0. This results in number now having a value of 537.

def process_handling
  return unless active
  return process_ok  if Input.trigger?(:C)
  return process_cancel if Input.trigger?(:B)
end


process_handling isn't really much different from other ones we've seen. We return unless the window is active, as we don't want to process handling if it isn't. Then we call process_ok if the player has pressed :C, and process_cancel if the player has pressed :B.

def process_ok
  Sound.play_ok
  $game_variables[$game_message.num_input_variable_id] = @number
  deactivate
  close
end


This method is called when the player presses :C while entering a number. We play the OK sound effect, set the element of $game_variables with an index corresponding to $game_message's num_input_variable_id property to the value of @number, then deactivate and close the window. This will set the value of the chosen variable to the value entered.

def process_cancel
end


In this case, we overwrite process_cancel with a blank method. This is just because we don't want to do anything in a number window when cancel is pressed, so it overrides the original method from the parent class.

def item_rect(index)
  Rect.new(index * 20, 0, 20, line_height)
end


As with the other windows with item rects that don't cover the entire width of the window, we need to overwrite this one (if you've ever used an Input Number command, you'll know that the item rect only highlights the current digit). We create a new instance of Rect, passing in index multiplied by 20 for the x coordinate, 0 for y, 20 for width, and line_height for height.

def refresh
  contents.clear
  change_color(normal_color)
  s = sprintf("%0*d", @digits_max, @number)
  @digits_max.times do |i|
    rect = item_rect(i)
    rect.x += 1
    draw_text(rect, s[i,1], 1)
  end
end


In the refresh method, we clear the contents, and change the text colour to normal_color. Then we set a variable called s to an sprintf formatted string. As you'll see if you consult the help file, this format syntax basically says "put a 0 in place of blank spaces, get the width from an argument, and display a number". The width argument is @digits_max, and @number is the number displayed. This will get a string that's @number padded out with 0s up to the maximum number of digits, so if you have a 6-digit-max number that's currently 364, s will be set to "000364". Then we execute a .times loop from 0 to @digits_max using iteration variable "i". In the block, we set a temp variable called rect to the result of passing i to item_rect, then add 1 to its x coordinate and draw character i of s with centre alignment. This will output the number with some space between each digit.

def update_cursor
  cursor_rect.set(item_rect(@index))
end


As we saw previously, to update the cursor we just set the cursor_rect to the result of passing @index to item_rect.

That's it for this issue. It should take us roughly two more to get the rest of the window classes covered. Until next time!