Search This Blog

Exploring Monster Taming Mechanics in Final Fantasy XIII-2: Viewing More Monster Data and Abilities

We've been building up views and controllers in the Rails MVC architecture for the various tables of monster data we have in this Final Fantasy XIII-2 monster taming mechanics series. We're now almost ready to finish off these table views with the ability tables, but before we do that, we need a place for the links to those ability tables to exist. The number of abilities for each monster is too much to put in the main monster table page, so we'll need to first build a details page for each monster before we can link up the monster abilities with the ability tables. Let's get started on that details page.

Final Fantasy XIII-2 Battle Scene

Create a Monster Details Page

Creating a monster details page should be about as straightforward as creating the table pages. We simply need to use a different action in the monster controller to show a page for a single monster instead of the index action that shows a table of all monsters. We can't use a Rails generator for this task because we already used one to create the monster index action, and we can't add to that same controller and view with a generator. We can only create new controllers and views with a generator; hence, the name. That's okay, though, because all we have to do is add to a few files to make this page happen. First, we need to add a route in config/routes.rb. Instead of adding another get route, we can use the resource method to combine the index and show routes for the monster pages:
Rails.application.routes.draw do
  resources :monster, only: [:index, :show]
  get 'location/index'
  get 'characteristic/index'
  get 'material/index'
  get 'home/index'
  root 'home#index'
end
Using the resource method will create the routes a bit differently, so the monster index will live at localhost:3000/monster instead of localhost:3000/monster/index, and the individual monster pages will live at localhost:3000/monster/:id, where :id is the database index of the monster we're looking at. All of the links we've created will conveniently change to the new form automatically because we used the monster_index_path helper method to set the destination of the links to the monster index page.

Now that we have a new route for individual monster pages, we can create a new action in app/controllers/monster_controller.rb:
  def show
    @monster = Monster.find(params[:id])
  end
Preparing the data for the view is as simple as looking up the :id in the model that was provided in the URL. We'll see how we set this :id in the URL in a bit, but first, let's build the monster detail page in app/views/monster/show.html.erb using a two-column table layout:
<h1><%= @monster.name %></h1>

<table id="monster-table" class="table table-striped table-sm">
  <tr>
    <td><strong>Role</strong></td>
    <td><%= @monster.role %></td>
    <td/>
    <td/>
  </tr>
  <tr>
    <td><strong>Monster Type</strong></td>
    <td><%= @monster.monster_type %></td>
    <td><strong>Constellation</strong></td>
    <td><%= @monster.constellation %></td>
  </tr>
  <tr>
    <td><strong>Location</strong></td>
    <td>
      <%= link_to @monster.location.name, location_index_path(:highlight => @monster.location.name) %>
      <% if @monster.location2 %>
        <br /><%= link_to @monster.location2.name, location_index_path(:highlight => @monster.location2.name) %>
      <% end %>
      <% if @monster.location3 %>
        <br /><%= link_to @monster.location3.name, location_index_path(:highlight => @monster.location3.name) %>
      <% end %>
    </td>
    <td><strong>Tame Rate</strong></td>
    <td><%= @monster.tame_rate %></td>
  </tr>
  <tr>
    <td><strong>Max Level</strong></td>
    <td><%= @monster.max_level %></td>
    <td><strong>Growth</strong></td>
    <td><%= @monster.growth %></td>
  </tr>
  <tr>
    <td><strong>Max Base HP</strong></td>
    <td><%= @monster.maximum_base_hp %></td>
    <td><strong>Speed</strong></td>
    <td><%= @monster.speed %></td>
  </tr>
  <tr>
    <td><strong>Max Base Strength</strong></td>
    <td><%= @monster.maximum_base_strength %></td>
    <td><strong>Max Base Magic</strong></td>
    <td><%= @monster.maximum_base_magic %></td>
  </tr>
  <tr>
    <td><strong>Immune to</strong></td>
    <td><%= @monster.immune %></td>
    <td><strong>Weak to</strong></td>
    <td><%= @monster.weak %></td>
  </tr>
  <tr>
    <td><strong>Halved for</strong></td>
    <td><%= @monster.halved %></td>
    <td><strong>Resistant to</strong></td>
    <td><%= @monster.resistant %></td>
  </tr>
  <tr>
    <td><strong>Feral Link</strong></td>
    <td><%= @monster.feral_link %></td>
    <td><strong>Description</strong></td>
    <td><%= @monster.description %></td>
  </tr>
  <tr>
    <td><strong>Effect</strong></td>
    <td><%= @monster.effect %></td>
    <td><strong>Damage Modifier</strong></td>
    <td><%= @monster.damage_modifier %></td>
  </tr>
  <tr>
    <td><strong>Charge Time</strong></td>
    <td><%= @monster.charge_time.strftime("%H:%M") %></td>
    <td/>
    <td/>
  </tr>
  <tr>
    <td><strong>PS3 Combo</strong></td>
    <td><%= @monster.ps3_combo %></td>
    <td><strong>Xbox 360 Combo</strong></td>
    <td><%= @monster.xbox_360_combo %></td>
  </tr>
  <tr>
    <td><strong>Notes</strong></td>
    <td colspan="3"><%= @monster.special_notes %></td>
  </tr>
This is a lot of HTML, but it's not complicated, just tedious to enter. Notice that I kept the links and conditional statements for the locations so they would be clickable just like on the monster index page, and we have numerous additional monster attributes shown here. We do not yet have the list of abilities, but that list is going to be even more tedious to enter because there's almost 100 levels worth of abilities that need to be checked to conditionally include in this table. We'd much rather do that programmatically. Let's start by creating a sub-heading to set off this part of the table and create a row for the default passive abilities and skills:
  <tr class="table-dark">
    <td/>
    <td class="lead">Passive Abilities</td>
    <td/>
    <td class="lead">Skills</td>
  </tr>
  <tr>
    <td><strong>Default</strong></td>
    <td>
      <% (1..4).each do |n| %>
        <% if @monster.send "default_passive#{n}" %>
          <%= @monster.send("default_passive#{n}").name %><br />
        <% end %>
      <% end %>
    </td>
    <td/>
    <td>
      <% (1..8).each do |n| %>
        <% if @monster.send "default_skill#{n}" %>
          <%= @monster.send("default_skill#{n}").name %><br />
        <% end %>
      <% end %>
    </td>
  </tr>
Here we can see that the passive ability cell could list up to four abilities and the skill cell could list up to eight skills. Each ability is checked by creating the name of the method in the model for that ability as a string (e.g. "default_passive1"), and sending that method name as a message. If the message returns a non-nil value, then we know it exists for that monster, and we can get its name to display in the table. That takes care of the default abilities for a monster, but the same task for the rest of the abilities is a bit more involved so we'll loop through each potential level, one per row, and call a helper method, a.k.a. a partial, to do the work:
  </tr>
  <% (2..99).each do |n| %>
    <%= render "level_ability", n: n, passive: "lv_%02d_passive" % [n], skill: "lv_%02d_skill" % [n] %>
  <% end %>
</table>
The render method will look for a partial in app/views/monster/_level_ability.html.erb and pass in three parameters: n, passive, and skill with the corresponding values shown in the code. The level number needs to be a two-digit number, so I use a formatted string instead of string interpolation. Then, we can use these parameters in the partial, and it cleans up the code in the partial a bit:
  <% if @monster.has_ability?(passive) || @monster.has_ability?(skill) %>
    <tr>
      <td><strong>Level <%= "%02d" % [n] %></strong></td>
      <td>
        <% if @monster.has_ability? passive %>
          <%= @monster.send(passive).name %><br />
        <% end %>
      </td>
      <td/>
      <td>
        <% if @monster.has_ability? skill %>
          <%= @monster.send(skill).name %><br />
        <% end %>
      </td>
    </tr>
  <% end %>
Notice that in the case of each level ability, we first have to check if there are any passive abilities or skills at that level and only create a row if at least one of them exists. For any given monster, most levels will not have any abilities, and we don't want empty rows cluttering up the table. Then, we check that the ability exists again when creating the cell so that we don't get an exception for trying to call .name on nil for abilities that don't exist. So, why are we calling this has_ability? method instead of trying to send the level ability method name to the model like we did before? Well, there are a few level abilities that don't exist for any monster, so trying to send a message with that method name will cause an exception. We have to be a bit more careful, so we can add this utility method to the Monster model in app/models/monster.rb in order to do a safe check:
  def has_ability?(ability)
    has_attribute?(ability + '_id') && send(ability)
  end
The has_attribute? check will make sure that that attribute exists before sending the message to the monster model to see if it's non-nil for that monster in particular. With that, we're ready to check on what our new monster details table looks like:

Monster detail page screenshot for Apkallu

Linking to Monster Details

That's a nice new monster detail page, but how do we get to it without typing in a random number for the monster in the address bar until we get what we want? It's as easy as adding a link to the monster names in the monster index table in app/views/monster/index.html.erb:
  <% @monsters.each do |monster| %>
    <tr>
      <td><%= link_to monster.name, monster_path(monster) %></td>
      <td><%= monster.role %></td>
Rails will automatically insert the correct :id into the URL based on which monster object is passed to monster_path. That's about the easiest thing we've done so far. Now we're ready to build out the ability tables.

Building the Ability Tables

Whew, more tables. We should be able to do this in our sleep right now, so I'll lay it out as quickly as I can. First, generate the controllers, views, and routes for the two models:
$ rails g controller Ability index
$ rails g controller RoleAbility index
Then, load the table data in each of the controllers in app/controllers/ability_controller.rb:
class AbilityController < ApplicationController
  def index
    @abilities = Ability.all
  end
end
And in app/controllers/role_ability_controller.rb:
class RoleAbilityController < ApplicationController
  def index
    @role_abilities = RoleAbility.all
  end
end
Also, build the table views in app/views/ability/index.html.erb:
<h1>Monster Passive Abilities</h1>

<table id="ability-table" class="table table-striped table-sm">
  <thead class="thead-dark">
    <tr>
      <th scope="col">Name</th>
      <th scope="col">Rank</th>
      <th scope="col">Description</th>
    </tr>
  </thead>

  <% @abilities.each do |ability| %>
    <tr>
      <td><%= ability.name %></td>
      <td><%= ability.rank %></td>
      <td><%= ability.description %></td>
    </tr>
  <% end %>
</table>
And in app/views/role_ability/index.html.erb:
<h1>Monster Role Abilities</h1>

<table id="role-ability-table" class="table table-striped table-sm">
  <thead class="thead-dark">
    <tr>
      <th scope="col">Name</th>
      <th scope="col">Role</th>
      <th scope="col">Infusable</th>
    </tr>
  </thead>

  <% @role_abilities.each do |ability| %>
    <tr>
      <td><%= ability.name %></td>
      <td><%= ability.role %></td>
      <td><%= ability.infusable %></td>
    </tr>
  <% end %>
</table>
We should also adjust the table width in app/assets/stylesheets/role_ability.scss:
#role-ability-table {
  width: 350px;
}
Finally, we add links in the home index in app/views/home/index.html.erb:
<h1 class="display-3 text-center">Final Fantasy XIII-2</h1>
<h1 class="display-3 text-center">Monster Taming</h1>
<div class="container">
  <div class="row">
    <div class="col-sm"></div>
    <div class="col-sm">
      <ul class="list-group">
        <%= link_to 'Monsters', monster_index_path,
          class: "list-group-item list-group-item-action" %>
        <%= link_to 'Monster Passive Abilities', ability_index_path,
          class: "list-group-item list-group-item-action" %>
        <%= link_to 'Monster Role Abilities', role_ability_index_path,
          class: "list-group-item list-group-item-action" %>
        <%= link_to 'Monster Materials', material_index_path,
          class: "list-group-item list-group-item-action" %>
        <%= link_to 'Monster Characteristics', characteristic_index_path,
          class: "list-group-item list-group-item-action" %>
        <%= link_to 'Game Locations', location_index_path,
          class: "list-group-item list-group-item-action" %>
      </ul>
    </div>
    <div class="col-sm"></div>
  </div>
</div>
And in the navigation bar in app/views/layouts/_header.html.erb:
<nav class="navbar fixed-top navbar-expand navbar-dark bg-dark">
  <%= link_to "FFXIII-2 Monster Taming", root_path, class: "navbar-brand" %>
  <ul class="navbar-nav">
    <li class="nav-item">
      <%= link_to "Home", root_path, class: "nav-link" %>
    </li>
    <li class="nav-item">
      <%= link_to "Monsters", monster_index_path, class: "nav-link" %>
    </li>
    <li class="nav-item">
      <%= link_to "Abilities", ability_index_path, class: "nav-link" %>
    </li>
    <li class="nav-item">
      <%= link_to "Role Abilities", role_ability_index_path, class: "nav-link" %>
    </li>
    <li class="nav-item">
      <%= link_to "Materials", material_index_path, class: "nav-link" %>
    </li>
    <li class="nav-item">
      <%= link_to "Characteristics", characteristic_index_path, class: "nav-link" %>
    </li>
    <li class="nav-item">
      <%= link_to "Locations", location_index_path, class: "nav-link" %>
    </li>
  </ul>
</nav>
Note that a few of the names were shortened to make space for the new links, and we have our last two tables built and ready to go:

Monster passive ability table screenshot

Monster role ability table screenshot

Linking from Monsters to Abilities

The last thing we'll do before wrapping up for the day is to link up the monster abilities in the detailed monster pages to the ability tables we just created, similar to how we did it with the locations already. We'll use the same mechanism of passing the ability name as a parameter in the URL so that the corresponding row can be highlighted in the ability table. The links to the ability tables are added to the monster details view in both the default abilities in app/views/monster/show.html.erb:
  <tr>
    <td><strong>Default</strong></td>
    <td>
      <% (1..4).each do |n| %>
        <% if @monster.send "default_passive#{n}" %>
          <%= link_to @monster.send("default_passive#{n}").name,
            ability_index_path(:highlight => @monster.send("default_passive#{n}").name) %><br />
        <% end %>
      <% end %>
    </td>
    <td/>
    <td>
      <% (1..8).each do |n| %>
        <% if @monster.send "default_skill#{n}" %>
          <%= link_to @monster.send("default_skill#{n}").name,
            role_ability_index_path(:highlight => @monster.send("default_skill#{n}").name) %><br />
        <% end %>
      <% end %>
    </td>
  </tr>
  <% (2..99).each do |n| %>
    <%= render "level_ability", n: n, passive: "lv_%02d_passive" % [n], skill: "lv_%02d_skill" % [n] %>
  <% end %>
</table>
And each of the level abilities in app/views/monster/_level_ability.html.erb:
  <% if @monster.has_ability?(passive) || @monster.has_ability?(skill) %>
    <tr>
      <td><strong>Level <%= "%02d" % [n] %></strong></td>
      <td>
        <% if @monster.has_ability? passive %>
          <%= link_to @monster.send(passive).name,
            ability_index_path(:highlight => @monster.send(passive).name) %><br />
        <% end %>
      </td>
      <td/>
      <td>
        <% if @monster.has_ability? skill %>
          <%= link_to @monster.send(skill).name,
            role_ability_index_path(:highlight => @monster.send(skill).name) %><br />
        <% end %>
      </td>
    </tr>
  <% end %>
This setup creates links from each passive ability to the passive ability table (obviously) and from each skill to the role ability table. Next, we need to make those :highlight parameters available to the views from the controllers in app/controllers/ability_controller.rb:
class AbilityController < ApplicationController
  def index
    @abilities = Ability.all
    @highlighted = params[:highlight]
  end
end
And in app/controllers/role_ability_controller.rb:
class RoleAbilityController < ApplicationController
  def index
    @role_abilities = RoleAbility.all
    @highlighted = params[:highlight]
  end
end
Finally, we can do the appropriate highlighting by adding a conditional class "table-primary" to the corresponding row in the ability table views in app/views/ability/index.html.erb:
  <% @abilities.each do |ability| %>
    <tr <%= "class=table-primary" if ability.name == @highlighted %>>
      <td><%= ability.name %></td>
      <td><%= ability.rank %></td>
      <td><%= ability.description %></td>
    </tr>
  <% end %>
</table>
And in app/views/role_ability/index.html.erb:
  <% @role_abilities.each do |ability| %>
    <tr <%= "class=table-primary" if ability.name == @highlighted %>>
      <td><%= ability.name %></td>
      <td><%= ability.rank %></td>
      <td><%= ability.description %></td>
    </tr>
  <% end %>
</table>
Okay, wow. That was a lot of little steps to get all of that stuff built and hooked up, but it wasn't too difficult. Now we have a great set of tables that we can make improvements to by interconnecting them more, and we can start building in features to make answering the more complex questions about monster taming a bit easier to do.

Before we get to those advanced features, we'll want to fix a couple things with our new ability tables, though. For one, the passive ability table in particular is too big to just highlight the row for the linked ability and require the user to go scrolling through to find it. We'll make that feature more user-friendly next time. Additionally, we don't have a link back to monsters from the ability tables. Wouldn't it be nice to click on an ability and have the website show a list of monsters with that ability? It's not as simple as it was with filtering the monster table by location because the abilities are spread across hundreds of attributes, but we'll figure that out next time as well.

No comments:

Post a Comment