SLIP INTO RUBY - UNDER THE HOOD PART 23: SWORDS AND SHIELDS AND HELMETS, OH MY!

In which we get to grips with our equipment (that's what she said)

  • Trihan
  • 06/16/2021 12:12 AM
  • 1270 views
Hello, sports fans! Y'know, I address you like that every episode, but I don't even know if any of you like sports. I don't even really like sports, so I'm not sure why I do that. It's just a thing that I did in the beginning to be cute and it stuck, I guess. XD

But regardless of why I do it and no matter your likingness of sportsball, it's time once again for a seat-of-your-pants episode of



Today: Swords and Shields and Helmets, oh my!

Scene_Equip
Where would we be in an RPG without the equipment screen? This is where you augment your characters with all that shiny loot you pick up in dungeons and shops. It inherits from Scene_MenuBase.

def start
    super
    create_help_window
    create_status_window
    create_command_window
    create_slot_window
    create_item_window
  end


Pretty standard start overwrite; we call the parent start method and then call methods to create a bunch of scene-specific windows: in this case the help window status window, command window, slot window and item window. It should hopefully be fairly obvious what all those are, but we'll be going over them in a sec anyway (except create_help_window since that is just pulled from the parent class and doesn't need to be redefined).

def create_status_window
    @status_window = Window_EquipStatus.new(0, @help_window.height)
    @status_window.viewport = @viewport
    @status_window.actor = @actor
  end


First up, the status window. We set instance variable @status_window to a new instance of Window_EquipStatus, passing in 0 as the x coordinate argument and @help_window's height as the y coordinate. This will place our status window on the far-left and just under the help window. We set @status_window's viewport to @viewport, and its actor to @actor.

def create_command_window
    wx = @status_window.width
    wy = @help_window.height
    ww = Graphics.width - @status_window.width
    @command_window = Window_EquipCommand.new(wx, wy, ww)
    @command_window.viewport = @viewport
    @command_window.help_window = @help_window
    @command_window.set_handler(:equip,    method(:command_equip))
    @command_window.set_handler(:optimize, method(:command_optimize))
    @command_window.set_handler(:clear,    method(:command_clear))
    @command_window.set_handler(:cancel,   method(:return_scene))
    @command_window.set_handler(:pagedown, method(:next_actor))
    @command_window.set_handler(:pageup,   method(:prev_actor))
  end


A bit more going on with the command window. We set variable wx to @status_window's width, wy to @help_window's height and ww to the width of the game window minus @status_window's width, then assign to @command_window a new instance of Window_EquipCommand passing in wx as the x coordinate, wy as the y, and ww as the width. This gives us a window that's just to the right of the status window, just under the help window, and takes up the rest of the horizontal screen space. As before, we set its viewport to @viewport and then also set its help_window to @help_window.

The rest is just setting handlers: We'll be calling command_equip for the "Equip" command, command_optimize for the "Optimize" command, command_clear for "Clear", return_scene for hitting cancel, next_actor for page down, and prev_actor for page up.

def create_slot_window
    wx = @status_window.width
    wy = @command_window.y + @command_window.height
    ww = Graphics.width - @status_window.width
    @slot_window = Window_EquipSlot.new(wx, wy, ww)
    @slot_window.viewport = @viewport
    @slot_window.help_window = @help_window
    @slot_window.status_window = @status_window
    @slot_window.actor = @actor
    @slot_window.set_handler(:ok,       method(:on_slot_ok))
    @slot_window.set_handler(:cancel,   method(:on_slot_cancel))
  end


Now we create the slot window. Set set wx to @Status_window's width, wy to @command_window's y plus its height, and ww to the width of the game window minus @status_window's width, then assign to @slot_window a new instance of Window_EquipSlot passing in wx, wy and ww for the x, y and width. This gives us a window that's just to the right of the status window, just below the command window and takes up the rest of the horizontal scren space.

We set @slot_window's viewport to @viewport, its help_window to @help_window and its status_window to @status_window, which establishes all the window links we need. We also set its actor to @actor and set handlers for the "ok" and "cancel" keys to call on_slot_ok and on_slot_cancel.

def create_item_window
    wx = 0
    wy = @slot_window.y + @slot_window.height
    ww = Graphics.width
    wh = Graphics.height - wy
    @item_window = Window_EquipItem.new(wx, wy, ww, wh)
    @item_window.viewport = @viewport
    @item_window.help_window = @help_window
    @item_window.status_window = @status_window
    @item_window.actor = @actor
    @item_window.set_handler(:ok,     method(:on_item_ok))
    @item_window.set_handler(:cancel, method(:on_item_cancel))
    @slot_window.item_window = @item_window
  end


Creating the item window is very similar. We set wx to 0, wy to @slot_window's y plus its height, ww to the width of the game window, and wh to the game window height minus wy, then assign to @item_window a new instance of Window_EquipItem with wx as the x, wy as the y, ww as the width and wh as the height. This results in the item window being on the far-left, underneath the slot window, taking up the full width of the screen and the remaining vertical space.

Then we set @item_window's viewport to @viewport, its help_window to @help_window, its status_window to @status_window and its actor to @actor. We set the handlers for "ok" and "cancel" to on_item_ok and on_item_cancel, and finally set @slot_window's item_window to @item_window.

def command_equip
    @slot_window.activate
    @slot_window.select(0)
  end


This method is called when the player selects the "Equip" command. Here we activate @slot_window and select index 0, which will select the first equipment slot (invariably weapon)

def command_optimize
    Sound.play_equip
    @actor.optimize_equipments
    @status_window.refresh
    @slot_window.refresh
    @command_window.activate
  end


This method is called when the player selects the "Optimize" command. We call play_equip in the Sound module, then call optimize_equipments on @actor, and refresh @status_window, @slot_window and @command_window.

def command_clear
    Sound.play_equip
    @actor.clear_equipments
    @status_window.refresh
    @slot_window.refresh
    @command_window.activate
  end


The method for clearing equipment is almost identical, except we call clear_equipments instead of optimize_equipments. Even the sound effect is the same.

def on_slot_ok
    @item_window.activate
    @item_window.select(0)
  end


When a slot is selected and the player hits ok, this method will be called. In this one, we just activate @item_window and select index 0, which will select the first item of equipment in inventory which is applicable to the selected slot.

def on_slot_cancel
    @slot_window.unselect
    @command_window.activate
  end


And if we hit cancel, we unselect @slow_window and activate @command_window.

def on_item_ok
    Sound.play_equip
    @actor.change_equip(@slot_window.index, @item_window.item)
    @slot_window.activate
    @slot_window.refresh
    @item_window.unselect
    @item_window.refresh
  end


Hitting ok on a piece of equipment will call this method. Again we call play_equip in the Sound module, then we call @actor's change_equip method, passing in @slot_window's index as the slot ID, and @item_window's item as the item. Then we activate and refresh @slot_window, and unselect and refresh @item_window, which will take us back to the slot selection and update the display to reflect the equipment change.

def on_item_cancel
    @slot_window.activate
    @item_window.unselect
  end


Hitting cancel in the item selection will activate @slow_window and unselect @item_window. Nothing exciting here really.

def on_actor_change
    @status_window.actor = @actor
    @slot_window.actor = @actor
    @item_window.actor = @actor
    @command_window.activate
  end


When the player uses page up/page down to change actors, we will set the actor property of @status_window, @slot_window and @item_window to @actor, and then activate @command_window. This ensures that no matter which window was active at the time, the new actor will begin on the command window.

Scene_Status
This is the scene which displays the actor's full status. It inherits from Scene_MenuBase, and may be the shortest class we've seen yet. Don't believe me? Well...

def start
    super
    @status_window = Window_Status.new(@actor)
    @status_window.set_handler(:cancel,   method(:return_scene))
    @status_window.set_handler(:pagedown, method(:next_actor))
    @status_window.set_handler(:pageup,   method(:prev_actor))
  end


So start. We call the parent method, as we do, and set @status_window to a new instance of Window_Status, passing in @actor as the actor. We set the "cancel" handler to return_scene, and the "pagedown"/"pageup" handlers to next_actor and prev_actor.

def on_actor_change
    @status_window.actor = @actor
    @status_window.activate
  end


When the actor is changed, we set @status_window's actor to @actor and then activate it.

And that is literally it for the status scene. Told you!

Scene_File

def start
    super
    create_help_window
    create_savefile_viewport
    create_savefile_windows
    init_selection
  end


For the start method, first we call the parent method, then call a few viewport/vindow creation methods (create_help_window, create_savefile_viewport and create_savefile_windows), and then we call init_selection.

def terminate
    super
    @savefile_viewport.dispose
    @savefile_windows.each {|window| window.dispose }
  end


In the terminate method we call the parent method, then dispose of @savefile_viewport, and then run an each loop through @savefile_windows using iteration variable "window" and in the block we dispose of the window. This will ensure that all memory used up by these windows is freed.

def update
    super
    @savefile_windows.each {|window| window.update }
    update_savefile_selection
  end


In our overwrite to the update method, first we call the parent method (as is tradition). Then as before run an each loop through @savefile_windows. Instead of dispose, this time we're calling update. And then after that loop, we call update_savefile_selection.

def create_help_window
    @help_window = Window_Help.new(1)
    @help_window.set_text(help_window_text)
  end


Here we create the help window. We set @help_window to a new instance of Window_Help, passing in 1 as the line_number argument. Then we call its set_text method and pass in the result of help_window_text.

def help_window_text
    return ""
  end


...which in this class is empty, because we're never going to directly call Scene_File. It's a superclass for Scene_Save and Scene_Load, which will implement their own overwrites.

def create_savefile_viewport
    @savefile_viewport = Viewport.new
    @savefile_viewport.rect.y = @help_window.height
    @savefile_viewport.rect.height -= @help_window.height
  end


To create the save file viewport, first we set @savefile_viewport to a new Viewport instance. Then we set its rect's y coordinate to the height of @help_window, and subtract that height from the height of the rect. This will give us a viewport for save files which covers the entire screen space not taken up by the help window.

def create_savefile_windows
    @savefile_windows = Array.new(item_max) do |i|
      Window_SaveFile.new(savefile_height, i)
    end
    @savefile_windows.each {|window| window.viewport = @savefile_viewport }
  end


In this method we create the save file windows, as the name suggests. We set @savefile_windows to a new Array instance with item_max items in it (we'll see that method shortly). and pass that array a "do" block where each element (i) is assigned a new instance of Window_SaveFile, passing in savefile_height as the height argument and i as the index. Following that block, we run an each loop on @savefile_windows where the block sets the viewport of each element (window) to @savefile_viewport, thus keeping all created windows in the same viewport we just created.

def init_selection
    @index = first_savefile_index
    @savefile_windows[@index].selected = true
    self.top_index = @index - visible_max / 2
    ensure_cursor_visible
  end


Now we finally come to init_selection. We set @index to first_savefile_index, which again we'll see shortly. We set the "selected" property of the element in @savefile_windows with an index of @index to true to show that we've selected it, then set self's top_index to @index minus visible_max divided by 2. Finally, we call ensure_cursor_visible. I'll explain that top_index bit a little more when we come to Scene_Save.

def item_max
    DataManager.savefile_max
  end


This method determines the maximum number of save files the game can have. We return savefile_max from DataManager, which by default returns 16.

def visible_max
    return 4
  end


This one, on the other hand, determines the number of save files visible on screen at any one time. And here, we return 4.

def savefile_height
    @savefile_viewport.rect.height / visible_max
  end


This method determines the height of each save file in the viewport. We return the height of @savefile_viewport's rect divided by visible_max.

def first_savefile_index
    return 0
  end


This method gets the first save file index, and we quite simply return 0. Nothing more to it.

def index
    @index
  end


Similarly, the index method returns @index. I'm not going to insult your intelligence by elaborating further.

def top_index
    @savefile_viewport.oy / savefile_height
  end


top_index gets the index of the entry which should be at the top of the screen, and for that we return the origin Y of @savefile_viewport divided by its height.

def top_index=(index)
    index = 0 if index < 0
    index = item_max - visible_max if index > item_max - visible_max
    @savefile_viewport.oy = index * savefile_height
  end


We also have a setter for top_index, taking index as a parameter. We set index to 0 if it's less than that, then if index is greater than item_max minus visible_max, we set it to item_max minus visible_max. Finally, we set @savefile_viewport's origin Y to index multplied by savefile_height.

So let's say index is 14. It's not less than 0 so the first line doesn't affect it Then 14 is greater than 16 - 4, so we set it to 16 - 4, which makes it 12. Then we set the viewport's oy to 12 * (368 / 4) which gives us 1104. Therefore, the origin Y will be 1104 rather than 0. Basically what this means is that the top row of the viewport's display will start at an offset of 1104 pixels from what it usually would, resulting in the view "shifting" to accommodate the index position.

def bottom_index
    top_index + visible_max - 1
  end


This method gets the bottom index, and we take that as top_index plus visible_max - 1. So if top_index is 3, bottom_index will be 3 + 4 - 1 or 6. Therefore, save #7 will be at the bottom.

def bottom_index=(index)
    self.top_index = index - (visible_max - 1)
  end


And we also have a setter for bottom_index, taking index as a parameter. What we're actually setting though is self's top_index, and we set it to index minus (visible_max - 1). So if we're setting it to 6, we'll be setting top_index to 6 - (4 - 1), which results in 3. And we can follow that to check that it matches up; if top_index is 3, bottom_index is nw 3 + 4 - 1, which gives us 6. Simples!

def update_savefile_selection
    return on_savefile_ok     if Input.trigger?(:C)
    return on_savefile_cancel if Input.trigger?(:B)
    update_cursor
  end


This method is called during the scene's update cycle. First, we return on_savefile_ok if the player is triggering input :C, which is usually enter. We then return on_savefile_cancel if they're triggering :B (usually esc), and then we call update_cursor.

def on_savefile_ok
  end


The base implementation of the on_savefile_ok method doesn't do anything, but we have it here ready to be overwritten.

def on_savefile_cancel
    Sound.play_cancel
    return_scene
  end


The on_savefile_cancel method, on the other hand, calls play_cancel from the Sound module and then calls return_scene, which will, by default, return the player to either the title screen or the menu.

def update_cursor
    last_index = @index
    cursor_down (Input.trigger?(:DOWN))  if Input.repeat?(:DOWN)
    cursor_up   (Input.trigger?(:UP))    if Input.repeat?(:UP)
    cursor_pagedown   if Input.trigger?(:R)
    cursor_pageup     if Input.trigger?(:L)
    if @index != last_index
      Sound.play_cursor
      @savefile_windows[last_index].selected = false
      @savefile_windows[@index].selected = true
    end
  end


We need an update_cursor in the scene because if you'll remember we overwrote it in Window_Savefile to not affect indexes any more. And we need to do that because each save file uses its own window, so we need to handle indexes slightly differently.

So we set last_index to @index. Then we call methods depending on input. If the player presses DOWN, we call cursor_down, and if they press UP we call cursor_up (passing in whether the player is *triggering* down or up, which is what stops the file list from wrapping if you're holding the key and hit the bottom/top). We call cursor_pagedown if they hit :R and cursor_pageup if they hit :L. Then if @index is not equal to last_index (meaning it was changed by one of the method calls), we call play_cursor in the Sound module and swap the selected property between the last_index and @index index of @savefile_windows (meaning we deselect the old window and reselect the new one).

def cursor_down(wrap)
    @index = (@index + 1) % item_max if @index < item_max - 1 || wrap
    ensure_cursor_visible
  end


This method handles the player hitting the down arrow key. If @index is less than item_max minus 1 or wrap is true, we set @index to (@index plus 1) mod item_max. Then we call ensure_cursor_visible.

So if @index is 14, it's less than 16 - 1, so we set it to (14 + 1) % 4, which gives us 15. Conversely, if @index is 15, it isn't less than 15 - 1, so to run this line wrap needs to be true (which it will be if the player only pressed down rather than holding it) and in this case @index will now be (15 + 1) % 16 which gives us 0 and wraps it back to the top.

def cursor_up(wrap)
    @index = (@index - 1 + item_max) % item_max if @index > 0 || wrap
    ensure_cursor_visible
  end


This one handles the up arrow key, and does pretty much the opposite of cursor_down. This time though, the check we're making is @index greater than 0 or wrap, and the index setting is (@index minus 1 plus item_max) mod item_max. So say the index is 0 and the player just hit up, wrap is true so we set @index to (0 - 1 + 16) % 16 which gives us 15 and loops around to the bottom. This is a common method of doing wrapping selection windows.

def cursor_pagedown
    if top_index + visible_max < item_max
      self.top_index += visible_max
      @index = [@index + visible_max, item_max - 1].min
    end
  end


For pagedown, we're jumping multiple windows at a time. So first, we check whether top_index plus visible_max is less than item_max. If so, we add visible_max to self's top_index, then set @index to the minimum value between @index plus visible_max and item_max minus 1.

So for example, if our top index is currently 3 and @index is 5, 3 + 4 is less than 16, so the if statement runs. We add 4 to top_index so now it's 7, then set @index to the minimum between 5 + 4 and 15. 9 is less than 15, so we set it to 9.

def cursor_pageup
    if top_index > 0
      self.top_index -= visible_max
      @index = [@index - visible_max, 0].max
    end
  end


And for pageup, we check whether top_index is greater than 0. If so, we subtract visible_max from self's top_index, and set @index to the maximum value between @index minus visible_max and 0.

So for example, if top_index returns 1, we set top_index to 1 - 4, but then the setter will set it to 0 since it's less than 0. @index is then set to the maximum of 0 - 4 and 0, which obviously returns 0.

def ensure_cursor_visible
    self.top_index = index if index < top_index
    self.bottom_index = index if index > bottom_index
  end


This method, well, ensures cursor visibility. So if index is less than top_index, we set self's top_index to index. And if index is greater than bottom_index, we set self's bottom_index to index. This just makes sure that if our index changes to one that isn't visible, it becomes visible afterwards.

That's it for the base scene! Now let's take a look at its kids. They're fairly light functionality-wise since the base class does most of the heavy lifting already.

Scene_Save
So here we have the scene for saving one's game. It inherits, obviously, from Scene_File.

def help_window_text
    Vocab::SaveMessage
  end


Our first port of call is to overwrite help_window_text to return Vocab::SaveMessage. If you take a quick gander back at Vocab, you'll see that SaveMessage returns "Save to which file?".

def first_savefile_index
    DataManager.last_savefile_index
  end


This method overwrites the one that returns 0, and instead we return the last_savefile_index property in DataManager, which will be the ID of the most recently accessed file.

def on_savefile_ok
    super
    if DataManager.save_game(@index)
      on_save_success
    else
      Sound.play_buzzer
    end
  end


This is the method that's called when the player hits ok on a save file. For starters, we call the parent method. Then if DataManager's save_game method returns true with @index passed to it, we call on_save_success. Otherwise, we call play_buzzer in the Sound module.

def on_save_success
    Sound.play_save
    return_scene
  end


Finally we have the method called when the player successfully saves a game. We call the Sound module's play_save method, then call return_scene.

And that's it! That's all that's needed for creating your save games. Crazy, huh?

Scene_Load
Scene_Load is actually almost identical. There's just a couple of differences in method names and on_load_success does a couple of extra things.

def help_window_text
    Vocab::LoadMessage
  end


So this method returns LoadMessage rather than SaveMessage. Nothing complicated.

def first_savefile_index
    DataManager.latest_savefile_index
  end


This one looks similar to the one in Scene_Save but actually isn't, since we're calling latest_savefile_index rather than last_savefile_index. The difference here is that latest returns the most recent save file by timestamp, rather than the last one *accessed*.

def on_savefile_ok
    super
    if DataManager.load_game(@index)
      on_load_success
    else
      Sound.play_buzzer
    end
  end


The only differences here are that we're calling load_game instead of save_game, and on_load_success instead of on_save_success.

def on_load_success
    Sound.play_load
    fadeout_all
    $game_system.on_after_load
    SceneManager.goto(Scene_Map)
  end


And finally, when a load is successful we play_load from Sound call fadeout_all (which fades the screen out), call on_after_load in $game_system (which sets the frame count and plays BGM/BGS) and finally we call SceneManager's goto method passing in Scene_Map, which...well, takes the player to the map scene.

Scene_End
This scene is the one the player goes to when they choose "Game End" in the menu. It inherits from Scene_MenuBase.

def start
    super
    create_command_window
  end


In our start method, first we call the parent method, then we call create_command_window.

def pre_terminate
    super
    close_command_window
  end


And in pre_terminate we call the parent method and then close_command_window.

def create_background
    super
    @background_sprite.tone.set(0, 0, 0, 128)
  end


This overwrite of create_background doesn't do much. First we call the parent method and then we set the tone of @background_sprite to 0 R, 0 G, 0 B and 128 grey, which effectively just makes the background darker.

def create_command_window
    @command_window = Window_GameEnd.new
    @command_window.set_handler(:to_title, method(:command_to_title))
    @command_window.set_handler(:shutdown, method(:command_shutdown))
    @command_window.set_handler(:cancel,   method(:return_scene))
  end


In create_command_window, first we set @command_window to a new instance of Window_GameEnd. Then we set the handlers for :to_title, :shutdown and :cancel to command_to_title, command_shutdown and return_scene respectively.

def close_command_window
    @command_window.close
    update until @command_window.close?
  end


This method is called from pre_terminate. First we call close on @command_window, and then call update until its close? method returns true. In other words, do nothing but perform frame updates until the window is fully closed.

def command_to_title
    close_command_window
    fadeout_all
    SceneManager.goto(Scene_Title)
  end


This method handles the "To Title" command. We call close_command_window, then fadeout_all to fade the screen, and finally SceneManager's goto method, passing in Scene_Title, to return the player to the title screen with no scene stack.

def command_shutdown
    close_command_window
    fadeout_all
    SceneManager.exit
  end


This method handles the "Shut down" command. Again, we call close_command_window and fadeout_all, but this time we call SceneManager.exit, which closes the game completely.

And that's it for Scene_End! We're fair flying through them today.

Scene_Shop
Another scene we'd be lost without. Without this one, the player would never be able to stock up on healing items, or buy a sweet new axe, or sell all those monster parts the party inexplicably picks up and carries around. The shop scene inherits from Scene_MenuBase.

def prepare(goods, purchase_only)
    @goods = goods
    @purchase_only = purchase_only
  end


The prepare method is called just after the shop scene is called in the shop processing event command, and takes as its parameters "goods", an array of the items the shop sells, and "purchase_only", a true/false flag denoting whether the player can only buy from the shop or can buy and sell. And in this method, we set @goods to goods and @purchase_only to purchase_only.

So this brings us full circle to be able to explain where the goods that go into Window_ShopBuy come from.

Each element of the array is itself an array containing the item type (0 for item, 1 for weapons, 2 for armour), item ID, whether the price is standard or custom (0 for standard, 1 for custom), and the price (which is only used if using custom). In addition, the first element will have a 5th parameter for purchase_only: true if the player can only buy, or false if they can buy and sell. For example, taking the default database, for a shop that sells standard Potion, Hi-Potion for 5g, standard Hand Ax, Gigantes Axe for half price, standard Casual Clothes and Mithril Plate for 5g, the array would be

[, , , , , ]

def start
    super
    create_help_window
    create_gold_window
    create_command_window
    create_dummy_window
    create_number_window
    create_status_window
    create_buy_window
    create_category_window
    create_sell_window
  end


In start, all we're doing is calling the parent method and then a bunch of window creation methods: for the help window, gold window, command window, dummy window, number window, status window buy window, category window and sell window. The only one that might not be self-explanatory is the dummy window, which I'll go over when we come to it.

def create_gold_window
    @gold_window = Window_Gold.new
    @gold_window.viewport = @viewport
    @gold_window.x = Graphics.width - @gold_window.width
    @gold_window.y = @help_window.height
  end


This method creates the gold window. We set @gold_window to a new instance of Window_Gold. Then we set that window's viewport to @viewport, its x coordinate to the width of the game window minus the width of the gold window, and the y coordinate to the height of the help window. This will position the gold display at the far right of the screen underneath the help window.

def create_command_window
    @command_window = Window_ShopCommand.new(@gold_window.x, @purchase_only)
    @command_window.viewport = @viewport
    @command_window.y = @help_window.height
    @command_window.set_handler(:buy,    method(:command_buy))
    @command_window.set_handler(:sell,   method(:command_sell))
    @command_window.set_handler(:cancel, method(:return_scene))
  end


For the command window, we set @command_window to a new instance of Window_ShopCommand passing in @gold_window's x coordinate as the width and @purchase_only as purchase only. Then we set its viewport to @viewport and its y coordinate to the height of @help_window. This places the command window in the remaining horizontal space to the left of the gold window.

We then set the handlers for :buy, :sell and :cancel to command_buy, command_sell and return_scene respectively.

def create_dummy_window
    wy = @command_window.y + @command_window.height
    wh = Graphics.height - wy
    @dummy_window = Window_Base.new(0, wy, Graphics.width, wh)
    @dummy_window.viewport = @viewport
  end


This method creates the dummy window. First we set wy to @command-window's y plus its height, then wh to the game window height minus wy. Then we set @dummy_window to a new instance of Window_Base, passing in 0 as the x, wy as the y, the width of the game window as the width and wh as the height. This gives us a window on the far left of the screen, underneath the command window and taking up the remaining vertical space.

But what is the dummy window? It's just the big window that appears when the shop first opens, and exists only to fill up the space in an aesthetically-pleasing way until a command is chosen and there's other stuff to display.

def create_number_window
    wy = @dummy_window.y
    wh = @dummy_window.height
    @number_window = Window_ShopNumber.new(0, wy, wh)
    @number_window.viewport = @viewport
    @number_window.hide
    @number_window.set_handler(:ok,     method(:on_number_ok))
    @number_window.set_handler(:cancel, method(:on_number_cancel))
  end


Now we create the number window. We set wy to @dummy_window's y coordinate, and wh to its height. Then we set @number_window to a new instance of Window_ShopNumber passing in 0 as the x, wy as the y, and wh as the height. This gives us a window on the far left of the screen, at the same y position as the dummy window and the same height as it. It won't be quite as wide, though.

We set @number_window's viewport to @viewport and call hide on it so that it won't be visible when the scene first opens. Then we set its :ok handler to on_number_ok, and its :cancel handler to on_number_cancel.

def create_status_window
    wx = @number_window.width
    wy = @dummy_window.y
    ww = Graphics.width - wx
    wh = @dummy_window.height
    @status_window = Window_ShopStatus.new(wx, wy, ww, wh)
    @status_window.viewport = @viewport
    @status_window.hide
  end


On to the status window. We set wx to the width of @number_window, wy to the y coordinate of @dummy_window, ww to the game window width minus wx, and wh to @dummy_window's height. Then we set @status_Window to a new instance of Window_ShopStatus passing in wx, wy, ww and wh as the x, y, width and height. This places the status window to the right of the number window taking up the rest of the horizontal space, at the same y coordinate as the dummy window and the same height. After that, we set @status_window's viewport to @viewport and hide it so that it isn't visible when the scene opens.

def create_buy_window
    wy = @dummy_window.y
    wh = @dummy_window.height
    @buy_window = Window_ShopBuy.new(0, wy, wh, @goods)
    @buy_window.viewport = @viewport
    @buy_window.help_window = @help_window
    @buy_window.status_window = @status_window
    @buy_window.hide
    @buy_window.set_handler(:ok,     method(:on_buy_ok))
    @buy_window.set_handler(:cancel, method(:on_buy_cancel))
  end


This method creates the buy window. We set wy to @dummy_window's y, and wh to its height. Then we set @buy_window to a new instance of Window_ShopBuy passing in 0 as the x, wy as the y, wh as the height and @goods as the shop goods. This will result in the buy window being the same size and position as the number window.

Then we set @buy_window's viewport to @viewport, its help_window to @help_window and its status_window to @status_window, before hiding it. Then we set the :ok handler to on_buy_ok and the :cancel handler to on_buy_cancel.

def create_category_window
    @category_window = Window_ItemCategory.new
    @category_window.viewport = @viewport
    @category_window.help_window = @help_window
    @category_window.y = @dummy_window.y
    @category_window.hide.deactivate
    @category_window.set_handler(:ok,     method(:on_category_ok))
    @category_window.set_handler(:cancel, method(:on_category_cancel))
  end


This method creates the category window. We set @category_window to a new instance of Window_ItemCategory, then set its viewport to @viewport, its help_window to @help_window, its y coordinate to @dummy_window's y, and then we hide and deactivate it (we need to do both because otherwise, since it's a selectable window, it will be active when the scene begins). Finally, we set the handler for :ok to on_category_ok, and the one for :cancel to on_category_cancel.

def create_sell_window
    wy = @category_window.y + @category_window.height
    wh = Graphics.height - wy
    @sell_window = Window_ShopSell.new(0, wy, Graphics.width, wh)
    @sell_window.viewport = @viewport
    @sell_window.help_window = @help_window
    @sell_window.hide
    @sell_window.set_handler(:ok,     method(:on_sell_ok))
    @sell_window.set_handler(:cancel, method(:on_sell_cancel))
    @category_window.item_window = @sell_window
  end


Then we create the sell window. We set wy to @category_window's y plus its height, then wh to the game window height - wy, and set @sell_window to a new instance of Window_ShopSell, passing in 0 for the x, wy for the y, the width of the game window as the width and wh as the height. This will give us a window underneath the category window which takes up all remaining screen space. Then we set its viewport to @viewport and its help_window to @help_window before hiding it. We set the :ok handler to on_sell_ok and the :cancel handler to on_sell_cancel, and finally we set @category_window's item_window to @sell_window.

def activate_buy_window
    @buy_window.money = money
    @buy_window.show.activate
    @status_window.show
  end


This method activates the buy window. We set @buy_window's money property to the result of the money method, which is coming up in a bit. We show and activate @buy_window, and then show @status_window.

def activate_sell_window
    @category_window.show
    @sell_window.refresh
    @sell_window.show.activate
    @status_window.hide
  end


This method activates the sell window. Here we show @category_window, refresh, show and activate @sell_window, and hide @status_window.

def command_buy
    @dummy_window.hide
    activate_buy_window
  end


This method is called when the "Buy" command is selected. All we're doing here is hiding @dummy_window and calling activate_buy_window.

def command_sell
    @dummy_window.hide
    @category_window.show.activate
    @sell_window.show
    @sell_window.unselect
    @sell_window.refresh
  end


This method is called when the "Sell" command is selected. We hide @dummy_window, show and activate @category_window, then show, unselect and refresh @sell_window.

def on_buy_ok
    @item = @buy_window.item
    @buy_window.hide
    @number_window.set(@item, max_buy, buying_price, currency_unit)
    @number_window.show.activate
  end


This method is the handler for pressing :ok in the buy window. We set @item to @buy_window's item and hide @buy_window, then call @number_window's set method passing in @item for item, max_buy for max, buying_price for price and currency_unit for currency_unit, then show and activate @number_window.

def on_buy_cancel
    @command_window.activate
    @dummy_window.show
    @buy_window.hide
    @status_window.hide
    @status_window.item = nil
    @help_window.clear
  end


The handler for :cancel in the buy window. We activate @command_window and show @dummyWindow, then hide @buy_window and @status_window, set @status_window's item to nil, and clear @help_window.

def on_category_ok
    activate_sell_window
    @sell_window.select(0)
  end


This method handles hitting :ok in the category window. All we're doing is calling activate_sell_window and selecting index 0 in @sell_window.

def on_category_cancel
    @command_window.activate
    @dummy_window.show
    @category_window.hide
    @sell_window.hide
  end


This method handles hitting :cancel in the category window. We activate @command_window, show @dummy_window and then hide @category_window and @sell_window.

def on_sell_ok
    @item = @sell_window.item
    @status_window.item = @item
    @category_window.hide
    @sell_window.hide
    @number_window.set(@item, max_sell, selling_price, currency_unit)
    @number_window.show.activate
    @status_window.show
  end


This method handles hitting :ok in the sell window. We set @item to @sell_window's item and then set @status_window's item to @item. Then we hide the @category_window and sell_window. We call @number_window's set method passing in @item as item, max_sell as max, selling_price as price and currency_unit as currency_unit, and finally we show and activate @number_window and show @status_window.

def on_sell_cancel
    @sell_window.unselect
    @category_window.activate
    @status_window.item = nil
    @help_window.clear
  end


When hitting cancel in the sell window, we unselect @sell_window and activate @category_window. Then we set @status_window's item to nil and clear @help_window.

def on_number_ok
    Sound.play_shop
    case @command_window.current_symbol
    when :buy
      do_buy(@number_window.number)
    when :sell
      do_sell(@number_window.number)
    end
    end_number_input
    @gold_window.refresh
    @status_window.refresh
  end


This method handles hitting ok in the number window. We call play_shop in the Sound module, and then have a case (switch) statement on @command_window's current_symbol: if it's :buy, we call do_buy. If it's :sell, we call do_sell. In both cases we pass in @number_window's number value as the argument. After the case statement, we call end_number_input, then refresh the @gold_window and @status_window.

def on_number_cancel
    Sound.play_cancel
    end_number_input
  end


This method is called when hitting cancel in the number window. We call play_cancel in the Sound module and then end_number_input.

def do_buy(number)
    $game_party.lose_gold(number * buying_price)
    $game_party.gain_item(@item, number)
  end


This method processes final confirmation of buying an item (via hitting ok in the number window) and takes number as a parameter. We call $game_party's lose_gold method passing in number multiplied by buying_price, and its gain_item method passing in @item as the item and number as the quantity. This will remove X gold for each of the item bought, and add that number to the party's inventory.

def do_sell(number)
    $game_party.gain_gold(number * selling_price)
    $game_party.lose_item(@item, number)
  end


do_sell is effectively the opposite. We're calling gain_gold instead of lose_gold and lose_item instead of gain_item.

def end_number_input
    @number_window.hide
    case @command_window.current_symbol
    when :buy
      activate_buy_window
    when :sell
      activate_sell_window
    end
  end


This method ends the number input. We hide @number_window, and run a case statement against @command_window's current_symbol. If it's :buy, we call activate_buy_window, and if it's :sell we call activate_sell_window.

def max_buy
    max = $game_party.max_item_number(@item) - $game_party.item_number(@item)
    buying_price == 0 ? max : [max, money / buying_price].min
  end


This method determines the maximum number of the selected item which the party is ablve to buy.

We set max to $game_party's max_item_number passing in @item, minus its item_number passing in the same number. This gives the difference between how many of the item the party has and how many it can have. Then, if buying_price is 0, we return max, and if it isn't we return the minimum value between max and money divided by buying_price (which will give us the number the player can afford if that's less than the maximum they can hold).

def max_sell
    $game_party.item_number(@item)
  end


max_sell is a bit easier: we just return $game_party's item_number method passing in @item, because it stands to reason that the maximum number of a thing you can sell is the number of it that you have.

def money
    @gold_window.value
  end


The money method is just a wrapped for @gold_window's value. It's shorthand for how much gold the party has, basically.

def currency_unit
    @gold_window.currency_unit
  end


This method just returns @gold_window's currency_unit, which gets the currency unit from Vocab.

def buying_price
    @buy_window.price(@item)
  end


This method gets the buying price for the selected item. Here, we're just returning the value from @buy_window's price method when @item is passed to it.

def selling_price
    @item.price / 2
  end


selling_price, on the other hand, returns half of @item's price.

And that's all there is to it!

We'll cut off there for this episode. I think I might be able to cover all the remaining classes next week, but it may require two episodes just because of how long Scene_Battle is. We'll see when we get there, I suppose.

Until next time!