SLIP INTO RUBY - UNDER THE HOOD PART 22: SCENE IT BEFORE

In which the puns become unbearable.

  • Trihan
  • 06/10/2021 04:09 AM
  • 931 views
Hello, sports fans! Could it be that Trihan is keeping to a weekly schedule? Stranger things have happened!

We continue with the scene classes in this shiny new, super-exciting episode of



Today: Scene it before

Scene_MenuBase
This class performs basic processing for menu scenes, and inherits from Scene_Base.

def start
    super
    create_background
    @actor = $game_party.menu_actor
  end


In the overwrite to the start method, first we call the parent method. Then we call create_background and finally we set the instance variable @actor to the result of calling $game_party's menu_actor method (which gives us the actor chosen in the party status window prior to calling this scene, if there is one).

def terminate
    super
    dispose_background
  end


In the terminate method, first we call the parent method and then dispose_background, which will...well, dispose of the background, as we'll see.

def create_background
    @background_sprite = Sprite.new
    @background_sprite.bitmap = SceneManager.background_bitmap
    @background_sprite.color.set(16, 16, 16, 128)
  end


In the method for creating the background, first we create an instance variable called @background_sprite and initialise it as a new instance of the Sprite class. Then we set its bitmap to the result of calling SceneManager's background_bitmap method, and set the sprite's color to 16R, 16G, 16B and 128 alpha. This gives us a partially-transparent darkened snapshot of the map.

def dispose_background
    @background_sprite.dispose
  end


Here we're just cleaning up after ourselves by disposing of the @background_sprite object. Failing to do this would result in a memory leak every time we open a menu scene.

def create_help_window
    @help_window = Window_Help.new
    @help_window.viewport = @viewport
  end


In this method we're creating the help window for the scene. We set an instance variable called @help_window to a new instance of Window_Help and then set its viewport to @viewport, which was defined in Scene_Base.

def next_actor
    @actor = $game_party.menu_actor_next
    on_actor_change
  end


This method switches to the next actor in the party; we set @actor to the result of calling $game_party's menu_actor_next method, and then call on_actor_change, which is an "event" method which will be called whenever the actor changes.

def prev_actor
    @actor = $game_party.menu_actor_prev
    on_actor_change
  end


This is the same thing but for previous actor, using menu_actor_prev instead of menu_actor_next.

def on_actor_change
  end


And here we have on_actor_change, which in the base class is empty. Classes which inherit from it will create their own implementations depending on what they need to do when the actor is changed.

That's all for the base menu scene! Not a lot going on, but it gets the basics sorted.

Scene_Menu
Here we have an important one. What's an RPG without a menu? This is the scene you go to for the list of commands you can choose from and an overview of the party's status, and as you may be able to guess it inherits from Scene_MenuBase.

def start
    super
    create_command_window
    create_gold_window
    create_status_window
  end


In our overwrite of the start method, we first call the parent method as is the standard. Then in addition to that we're calling create_command_window, create_gold_window and create_status_window.

def create_command_window
    @command_window = Window_MenuCommand.new
    @command_window.set_handler(:item,      method(:command_item))
    @command_window.set_handler(:skill,     method(:command_personal))
    @command_window.set_handler(:equip,     method(:command_personal))
    @command_window.set_handler(:status,    method(:command_personal))
    @command_window.set_handler(:formation, method(:command_formation))
    @command_window.set_handler(:save,      method(:command_save))
    @command_window.set_handler(:game_end,  method(:command_game_end))
    @command_window.set_handler(:cancel,    method(:return_scene))
  end


In this method we create the main menu command window, giving the player all the commands they'll be using during the game. Fairly simply, we set instance variable @command_window to a new instance of Window_MenuCommand and then set the handlers for all the commands it has. The commands themselves were defined by the window class so we don't need to worry about that, but unless we set handlers for them those options won't actually do anything in the scene.

I don't think I need to break this down line by line as we've already covered in the past how set_handler works. You give it a symbol for the command (which needs to match the symbol used in the add_command call in the window class), and then provide it with the method that will be called when the command is selected. Item, formation, save and game end have their own specified methods, while skill, equip and status all share command_personal, which will handle the scene branching from there.

def create_gold_window
    @gold_window = Window_Gold.new
    @gold_window.x = 0
    @gold_window.y = Graphics.height - @gold_window.height
  end


This method creates the gold display window in the menu with which the player can see how poor they are. So we set instance variable @gold_window to a new instance of Window_Gold, then set its x coordinate to 0 and its y coordinate to the height of the game window minus the height of the gold window, which places it on the bottom left of the screen.

def create_status_window
    @status_window = Window_MenuStatus.new(@command_window.width, 0)
  end


In this method we're creating the status window showing our party and their condition. Just one line of code needed: we set instance variable @status_window to a new instance of Window_MenuStatus, passing in the width of @command_window as the x coordinate and 0 as the y coordinate (this will cause it to take up the rest of the horizontal screen space and the entire height).

def command_item
    SceneManager.call(Scene_Item)
  end


This is the method which is called when the player chooses the "Item" command, where we call the call method of SceneManager and pass in Scene_Item, which will take the player to the item scene.

def command_personal
    @status_window.select_last
    @status_window.activate
    @status_window.set_handler(:ok,     method(:on_personal_ok))
    @status_window.set_handler(:cancel, method(:on_personal_cancel))
  end


This is the method called when the player chooses "Skill", "Equip" or "Status". We call @status_window's select_last method, which selects the most recent party member index the window was active on, then activate it, and set the handlers for the OK and cancel keys to on_personal_ok and on_personal_cancel respectively.

def command_formation
    @status_window.select_last
    @status_window.activate
    @status_window.set_handler(:ok,     method(:on_formation_ok))
    @status_window.set_handler(:cancel, method(:on_formation_cancel))
  end


This method handles choosing the "Formation" command. As before, we call @status_window's select_last and activate methods, but then we set the handlers for OK and cancel to on_formation_ok and on_formation_cancel. This causes the status window to have different functionality depending on which command was chosen before it activated.

def command_save
    SceneManager.call(Scene_Save)
  end


In the method for callin the save scene, we quite simply call SceneManager's call method and pass in Scene_Save.

def command_game_end
    SceneManager.call(Scene_End)
  end


And for the "Game End" command, it's the same but we pass in Scene_End instead.

def on_personal_ok
    case @command_window.current_symbol
    when :skill
      SceneManager.call(Scene_Skill)
    when :equip
      SceneManager.call(Scene_Equip)
    when :status
      SceneManager.call(Scene_Status)
    end
  end


This method is called when the player chooses "Skill", "Equip" or "Status"; we run a case statement on the current_symbol of @command_window: in each case we call SceneManager's call method, passing in Scene_Skill if the symbol is :skill, Scene_Equip if the symbol is :equip, and Scene_Status if it's :status.

def on_personal_cancel
    @status_window.unselect
    @command_window.activate
  end


This method is called if the player cancels while selecting a party member after choosing a "personal" menu command. We unselect @status_window and activate @command_window, which returns us to the command the player chose originally.

def on_formation_ok
    if @status_window.pending_index >= 0
      $game_party.swap_order(@status_window.index,
                             @status_window.pending_index)
      @status_window.pending_index = -1
      @status_window.redraw_item(@status_window.index)
    else
      @status_window.pending_index = @status_window.index
    end
    @status_window.activate
  end


This method is called when the player hits ok on a party member after choosing "Formation". If the pending_index of @status_window is greater than or equal to 0, we call $game_party's swap_order method passing in @status_window's index and pending_index. Then we set pending_index to -1, and call redraw_item passing in @status_window's index. Otherwise, if the pending index is not greater than or equal to 0, we set pending_index to @status_window's index.

Either way, we activate @status_window.

So basically how this works is, the first time you hit ok on a party member, it sets the "pending index", indicating that we're designating a party member to swap. And if we've already designated a pending index, it swaps the pending party member with the one you selected and unsets the pending index for the next selection, and redraws the entry to show the new party order.

def on_formation_cancel
    if @status_window.pending_index >= 0
      @status_window.pending_index = -1
      @status_window.activate
    else
      @status_window.unselect
      @command_window.activate
    end
  end


Last but not least, the method for hitting cancel while in formation mode. If the pending index is greater than or equal to 0, we set it to -1 and activate the status window. Otherwise, we unselect the status window and activate the command window.

And that's it for the menu!

Scene_ItemBase
This scene is the base for both the item scene and skill scene, which have quite a few common functions between them. It itself inherits from Scene_MenuBase.

def start
    super
    create_actor_window
  end


Our start method is pretty simple: call the parent method and then call create_actor_window.

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


In this method, we create the window showing the actor details for choosing a target of an item or skill. First we set instance variable @actor_window to a new instance of Window_MenuActor and then set the ok and cancel handlers for it to on_actor_ok and on_actor_cancel.

def item
    @item_window.item
  end


This method gets the currently-selected item. For this, we return the result of calling @item_window's item method. The base class doesn't actually do anything to set @item_window, but as we'll see the child classes do.

def user
    $game_party.movable_members.max_by {|member| member.pha }
  end


This method gets the item or skill's user; to determine this, we return the result of calling the max_by method on $game_party's movable_members passing in a block using block variable "member" and returning the member's pha property.

Even if you remember some of the similar things we've seen, this might be a bit confusing, so let's break it down.

.pha is PHArmacology, which is an sp-parameter that determines how effective curative effects are on items or skills.

max_by iterates through an array running a block of code on each element and then returns the maximum value that was returned. So in the array this is running on, the return value will be the highest PHA.

movable_members, as you may remember, returns an array of all party members who are capable of acting (aren't dead or afflicted by a restriction state).

So when you put all this together, this method will return the highest PHA value of all party members who can take actions.

This has actually caused quite a bit of confusion in the community over the years with people who didn't realise that the user of an item in the menu isn't also the party member who was the target for it. It will always be the non-restricted member of your party with the highest pharmacology.

def cursor_left?
    @item_window.index % 2 == 0
  end


This method determines whether the cursor is in the left-hand column. And to do that, all we do is return whether the index of @item_window mod 2 is 0. It's pretty easy to see how this works: in a 2-column layout, the left-hand column indexes will be 0, 2, 4, 6, 8 etc. while the right-hand ones will be 1, 3, 5, 7, 9 etc. So taking the modulus 2 of the index will return 0 for a left-hand index and 1 for a right-hand one (for anyone who might have missed maths class that day or forgot the time we covered it, modulus (%) divides a value by another value and returns the remainder. There is never a remainder when you modulus an even number by 2, and always a remainder of 1 when you do it with an odd one, so it's a great mathematical process for determining index positions like this)

def show_sub_window(window)
    width_remain = Graphics.width - window.width
    window.x = cursor_left? ? width_remain : 0
    @viewport.rect.x = @viewport.ox = cursor_left? ? 0 : window.width
    @viewport.rect.width = width_remain
    window.show.activate
  end


This method, as the name suggests, shows a subwindow, and takes that window as a parameter. First, we set a variable called width_remain to the width of the game window minus the width of the passed-in window. Then we set the window's x coordinate to width_remain if the cursor is in the left column or 0 if it's in the right (this prevents the subwindow from covering the item selected). Then we set the x coordinate and x origin of @viewport's rect to 0 if the cursor is in the left column or the window's width otherwise, and set the rect's width to width_remain.

The reason for this is that the subwindow is created before the other windows are, and technically is underneath them. Modifying the viewport's rect x and width reduces the display area so that nothing appears over the subwindow, and since the window can move depending on the cursor position, we need to move the viewport accordingly.

After moving the window and modifying the viewport as needed, we show and activate the window.

def hide_sub_window(window)
    @viewport.rect.x = @viewport.ox = 0
    @viewport.rect.width = Graphics.width
    window.hide.deactivate
    activate_item_window
  end


This method hides the subwindow, and again takes the subwindow as a parameter. We revert any changes to @viewport by setting its rect's x coordinate and x origin to 0, and the rect's width to the width of the game window. Then we hide and deactivate the window, and call activate_item_window.

def on_actor_ok
    if item_usable?
      use_item
    else
      Sound.play_buzzer
    end
  end


The method called when the player hits ok on an actor. If the item is usable, we call use_item, and otherwise we call the play_buzzer method of the Sound module.

def on_actor_cancel
    hide_sub_window(@actor_window)
  end


The cancel handler for the actor window. We call hide_sub_window passing in @actor_window as the argument.

def determine_item
    if item.for_friend?
      show_sub_window(@actor_window)
      @actor_window.select_for_item(item)
    else
      use_item
      activate_item_window
    end
  end


This method confirms an item, and will either start the actor selection process or just use it if it doesn't target an ally.

We check whether the scene item is for a friendly battler. If so, we call show_sub_window passing in @actor_window and then call select_for_item on @actor_window passing in item (which will either select the target if the item is for a single actor or the entire party if it's for all members). Otherwise, we just call use_item and then activate_item_window.

def activate_item_window
    @item_window.refresh
    @item_window.activate
  end


This method...uh...activates the item window. That's the thing with self-explanatory code; doing a breakdown of it ends up being almost insulting. XD

So all we're doing here is calling refresh on @item_window and then activating it.

def item_target_actors
    if !item.for_friend?
      []
    elsif item.for_all?
      $game_party.members
    else
      [$game_party.members[@actor_window.index]]
    end
  end


This method gets an array of all actors which are being targeted by the item. We check if the item is NOT for an ally; if so, we return an empty array. Otherwise, if the item is for all members, we return the result of calling $game_party's members method, which will give us an array of everyone in the party. And otherwise, we return the element of the members array at the index selected in @actor_window.

def item_usable?
    user.usable?(item) && item_effects_valid?
  end


This method determines whether an item is usable. Here's we're just returning the result of calling the usable? method on the user, passing in item, AND item_effects_valid?, which will return true if the item has valid applicable effects, and false if not (as we'll see in a sec).

def item_effects_valid?
    item_target_actors.any? do |target|
      target.item_test(user, item)
    end
  end


This method determines whether the item will have an applicable effect on the target(s); we run an any? loop through each element of item_target_actors, using block variable "target", and return the result of calling target's item_test method, passing in user and item. This will, for example, stop an HP recovery item from working if a target is already at full health.

def use_item_to_actors
    item_target_actors.each do |target|
      item.repeats.times { target.item_apply(user, item) }
    end
  end


This method is what actually uses the item. Again, we loop through item_target_actors but this time with an each loop, again using block variable "target". Then for as many times as the item's "repeats" property, we call item_apply on target, passing in user and item.

def use_item
    play_se_for_item
    user.use_item(item)
    use_item_to_actors
    check_common_event
    check_gameover
    @actor_window.refresh
  end


In this method, we use the item and perform a few other checks. First we call play_se_for_item (which won't be defined until child classes), then call use_item on the user, passing in item. We call use_item_to_actors to execute the item effects on the targets. Then we call check_common_event, then check_gameover and finally we call refresh on @actor_window.

def check_common_event
    SceneManager.goto(Scene_Map) if $game_temp.common_event_reserved?
  end


This method checks whether a common event needs to execute (which will be the case if the item/skill effects include the "Common Event" option). We call SceneManager's goto method passing in Scene_Map if the common_event_reserved? method of $game_temp returns true.

And thus wraps up Scene_ItemBase! Now let's see its first child class.

Scene_Item
This scene processes game items like potions, HP ups, antidotes etc. As said above, it inherits from Scene_ItemBase.

def start
    super
    create_help_window
    create_category_window
    create_item_window
  end


In this overwrite to the start method, first we call the parent method, then create_help_window, then create_category_window, and finally create_item_window.

def create_category_window
    @category_window = Window_ItemCategory.new
    @category_window.viewport = @viewport
    @category_window.help_window = @help_window
    @category_window.y = @help_window.height
    @category_window.set_handler(:ok,     method(:on_category_ok))
    @category_window.set_handler(:cancel, method(:return_scene))
  end


This method creates the category window. We set instance variable @category_window to a new instance of Window_ItemCategory, and set that window's viewport to @viewport. We set its help_window to @help_window and its y coordinate to the help window's height. Then we set the handlers for ok and cancel to on_category_ok and return_scene respectively.

def create_item_window
    wy = @category_window.y + @category_window.height
    wh = Graphics.height - wy
    @item_window = Window_ItemList.new(0, wy, Graphics.width, wh)
    @item_window.viewport = @viewport
    @item_window.help_window = @help_window
    @item_window.set_handler(:ok,     method(:on_item_ok))
    @item_window.set_handler(:cancel, method(:on_item_cancel))
    @category_window.item_window = @item_window
  end


This method creates the item window. We set a variable called wy to the y coordinate plus height of @categiry_window, and wh to the height of the game window minus wy. We set @item_window to a new instance of Window_ItemList passing in 0 as the x coordinate, wy as the y coordinate, the game window width as width and wh as the height. This gives us a window on the left of the screen underneath the category window, as wide as the screen and taking up the rest of the vertical space.

We set @item_window's viewport to @viewport and its help_window to @help_window as before, and set the ok and cancel handlers to on_item_ok and on_item_cancel.

Finally, we set @category_window's item_window to @item_window. This creates a link between the category and item windows so that the item window "knows" which category is selected and can update its item list accordingly.

def on_category_ok
    @item_window.activate
    @item_window.select_last
  end


This method is called when the player hits ok on a category; we activate @item_window and call its select_last method to select the last item that was used.

def on_item_ok
    $game_party.last_item.object = item
    determine_item
  end


This method is called when the player hits ok on an item; we set $game_party's last_item_object to the selected item and then call determine_item.

def on_item_cancel
    @item_window.unselect
    @category_window.activate
  end


This is the method called when the player hits cancel on an item; we call unselect on @item_window and then activate @category_window.

def play_se_for_item
    Sound.play_use_item
  end


Here we see our first implementation of play_se_for_item. We just call the play_use_item method of the Sound module.

def use_item
    super
    @item_window.redraw_current_item
  end


Last but not least, we overwrite use_item, first calling the parent method and then calling redraw_current_item on @item_window. This will update item quantities and usability as soon as they're used instead of having to wait until the player cancels.

That wraps it up for Scene_Item. Now let's look at the other side of the Scene_ItemBase inheritance coin.

Scene_Skill
And here it is. This is the scene that deals with your skills, your magic, your special abilities, and all of that good stuff. It inherits, surprising nobody, from Scene_ItemBase.

def start
    super
    create_help_window
    create_command_window
    create_status_window
    create_item_window
  end


Our start method is very similar to the one from Scene_Item, but the windows are a bit different. Here we're calling the parent method, and then create_help_window, create_command_window, create_status_window and create_item_window.

def create_command_window
    wy = @help_window.height
    @command_window = Window_SkillCommand.new(0, wy)
    @command_window.viewport = @viewport
    @command_window.help_window = @help_window
    @command_window.actor = @actor
    @command_window.set_handler(:skill,    method(:command_skill))
    @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


First of our windows is the command window, created here. First we set variable wy to the height of @help_window. Then we set @command_window to a new instance of Window_SkillCommand, passing in an x coordinate of 0 and a y coordinate of wy. We set @command_window's viewport to @viewport, its help_window to @help_window, and its actor to @actor (to establish the links that will allow windows to update their contents based on the actor selected). Then we set the handlers for the "Skill" and "Cancel" commands, and for the page down and page up keys, to command_skill, return_scene, next_actor and prev_actor respectively.

def create_status_window
    y = @help_window.height
    @status_window = Window_SkillStatus.new(@command_window.width, y)
    @status_window.viewport = @viewport
    @status_window.actor = @actor
  end


Then we have the status window. We set y to the height of @help_window, and then set @status_window to a new instance of Window_SkillStatus passing in the @command_window's width for the x coordinate, and y for the y coordinate. This will place the status window to the right of the command window and under the help window. Then we set its viewport to @viewport and its actor to @actor.

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


Last of our windows in this scene is the item window, which will contain the actual list of skills to choose from based on the command selected.

First we set wx to 0, wy to @status_window's y minus its height, ww to the width of the game window and wh to the height of the game window minus wy. Then we set @item_window to a new instance of Window_SkillList, passing in wx, wy, ww and wh as the x, y, width and height. This will place the window on the far left, under the status window, taking up the full screen width and remaining height.

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

Other than a couple of coordinate and size differences, this is identical to the one from Scene_Item.

def user
    @actor
  end


Unlike the item scene, in Scene_Skill for user we return @actor, or the actor whose skill list is currently being viewed.

def command_skill
    @item_window.activate
    @item_window.select_last
  end


The method for when a skill type command is selected is pretty simple: we activate @item_window and call select_last to select the last used skill.

def on_item_ok
    @actor.last_skill.object = item
    determine_item
  end


In the method for hitting ok on an item (or skill in this case), we set the object of @actor's last_skill to item, and then call determine_item. This updates the skill that select_last will select, and sets up the skill selected for use.

def on_item_cancel
    @item_window.unselect
    @command_window.activate
  end


The cancel handler is similarly uncomplicated. We just call @item_window's unselect method and then activate @command_window.

def play_se_for_item

Sound.play_use_skill
end


play_se_for_item is similar to Scene_Item's, but this time we're calling play_use_skill from the Sound module instead.

def use_item
    super
    @status_window.refresh
    @item_window.refresh
  end


Our overwrite to use_item is similar too. First we call the parent method, as is tradition, and then we refresh @status_window and @item_window. This ensures that things like MP value and skill usability are always accurately displayed at the time of use.

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


Our actor change event method just sets the actor of @command_window, @status_window and @item_window to @actor, then activates @command_window. This ensures that all windows update their display to show the new actor and that the command window is active ready for selecting skill types.

And that's it! This feels like a good point to end on as I think doing Scene_Equip as well will make this run on just a bit too long. We've only got a couple of episodes worth of code left to break down, and then I'll need to figure out where the series is going from there.

Until next time!