Search This Blog

Exploring Monster Taming Mechanics in Final Fantasy XIII-2: Viewing the Monster Data

Last time in this Final Fantasy XIII-2 monster taming mechanics series, we continued building views and controllers in the Rails MVC architecture for the monster characteristics and game locations tables. Now it's time to tackle the main table of the site: the monster table. Once we have this monster table, we'll want to add links to the elements of the table so that we can jump directly to related tables of interest. We'll learn how to do that task as well, and we'll see just how easy Rails makes it.

Final Fantasy XIII-2 battle scene

Create a Monster Page

Creating a stand-alone monster page is as simple as it was to create the other pages. Let's do that first so we'll have something to work with. This will be a rapid set of steps for this now-routine process. First, we run the rails controller generator for it:
$ rails g controller Monster index
Remember, this creates Monster controller and view files and adds the route for the view page to the top of the config/routes.rb file:
Rails.application.routes.draw do
  get 'monster/index'
  get 'location/index'
  get 'characteristic/index'
  get 'material/index'
  get 'home/index'
  root 'home#index'
end
Next, load the table data into an instance variable in the controller in app/controllers/monster_controller.rb:
class MonsterController < ApplicationController
  def index
    @monsters = Monster.all
  end
end
Then, duplicate one of the other table's views we created last time in app/views/monster/index.html.erb and make the necessary modifications for the monster table:
<h1>Monsters</h1>

<table id="monster-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">Location</th>
      <th scope="col">Tame Rate</th>
      <th scope="col">Max Level</th>
      <th scope="col">Base HP</th>
      <th scope="col">Base Strength</th>
      <th scope="col">Base Magic</th>
      <th scope="col">Speed</th>
      <th scope="col">Growth</th>
      <th scope="col">Immune</th>
      <th scope="col">Resistant</th>
      <th scope="col">Halved</th>
      <th scope="col">Weak</th>
    </tr>
  </thead>

  <% @monsters.each do |monster| %>
    <tr>
      <td><%= monster.name %></td>
      <td><%= monster.role %></td>
      <td><%= monster.location.name %></td>
      <td><%= monster.tame_rate %></td>
      <td><%= monster.max_level %></td>
      <td><%= monster.max_base_hp %></td>
      <td><%= monster.max_base_strength %></td>
      <td><%= monster.max_base_magic %></td>
      <td><%= monster.speed %></td>
      <td><%= monster.growth %></td>
      <td><%= monster.immune %></td>
      <td><%= monster.resistant %></td>
      <td><%= monster.halved %></td>
      <td><%= monster.weak %></td>
    </tr>
  <% end %>
</table>
Notice we didn't include all of the properties for each monster because it would be far too many cells for this table. Remember, there are 100 levels for skill and passive abilities, each! It would be information overload, so we're going to have to come up with another way to view the details for each monster, and just show some of the most important properties in this table. (We'll get to that task next time.) Second, the monster.location is not a string, like the other attributes. It's actually a model object for the location, and we can pull up its name by going one level deeper with monster.location.name.  This mechanism is enabled by the belongs_to reference that was set up in the Monster model several articles back. Finally, we don't need to add extra formatting to the CSS file at app/assets/stylesheets/monsters.scss to tighten up the table because the table needs to be plenty wide for all of these columns.

We should also add this page as another link to our index in app/views/home/index.html.erb so we can more easily get to it:
<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 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 let's not forget the new navigation bar. The monster page is missing from the navbar in app/views/layouts/_header.html.erb as well:
<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 "Monster Materials", material_index_path, 
        class: "nav-link" %>
    </li>
    <li class="nav-item">
      <%= link_to "Monster Characteristics", characteristic_index_path, 
        class: "nav-link" %>
    </li>
    <li class="nav-item">
      <%= link_to "Game Locations", location_index_path, 
        class: "nav-link" %>
    </li>
  </ul>
</nav>
And finally, we have another pretty table:

Screenshot of monster table

Linking Tables Together

With the introduction of this monster table, we are now able to start linking these tables together. We see right away that monsters are in a location, so we can link the cells in the location column to the game location table. To set up that link, we can start by simply adding a link to the monster table view around the text in the location cells:
  <% @monsters.each do |monster| %>
    <tr>
      <td><%= monster.name %></td>
      <td><%= monster.role %></td>
      <td><%= link_to monster.location.name, location_index_path %></td>
      <td><%= monster.tame_rate %></td>
      <td><%= monster.max_level %></td>
      <td><%= monster.max_base_hp %></td>
      <td><%= monster.max_base_strength %></td>
      <td><%= monster.max_base_magic %></td>
      <td><%= monster.speed %></td>
      <td><%= monster.growth %></td>
      <td><%= monster.immune %></td>
      <td><%= monster.resistant %></td>
      <td><%= monster.halved %></td>
      <td><%= monster.weak %></td>
    </tr>
  <% end %>
</table>
And as easy as that, we have links from each location to the location table:

Screenshot of monster table with location links

This addition was the bare minimum, though, since all links go to the same table, and then we have to search in the location table for the game area that we actually want to look at. Since it wouldn't make sense to have the link point to a different page for each location, as the location only has one other property, its source area, we should do something else to make the location clicked on easier to find in the table. Maybe we could highlight it? That's also pretty straightforward to do. First, we need to add a parameter to the link so that the location page knows which location to highlight:
  <% @monsters.each do |monster| %>
    <tr>
      <td><%= monster.name %></td>
      <td><%= monster.role %></td>
      <td>
        <%= link_to monster.location.name, 
          location_index_path(:highlight => monster.location.name) %>
      </td>
      <td><%= monster.tame_rate %></td>
      <td><%= monster.max_level %></td>
      <td><%= monster.max_base_hp %></td>
      <td><%= monster.max_base_strength %></td>
      <td><%= monster.max_base_magic %></td>
      <td><%= monster.speed %></td>
      <td><%= monster.growth %></td>
      <td><%= monster.immune %></td>
      <td><%= monster.resistant %></td>
      <td><%= monster.halved %></td>
      <td><%= monster.weak %></td>
    </tr>
  <% end %>
</table&gt
Then, we need to make that parameter available in the location controller index action to the view:
class LocationController < ApplicationController
  def index
    @location = Location.all
    @highlighted = params[:highlight]
  end
end
The params hash table contains any parameters present in the URL for the page, so we can extract the :highlight parameter and put it in the instance variable @highlighted. It'll be nil if it's not present in the URL. Finally, we use that variable to highlight the correct row with a Bootstrap class in the location view:
  <% @locations.each do |location| %>
    <tr <%= "class=table-primary" if location.name == @highlighted %>>
      <td><%= location.name %></td>
      <td><%= location.source&.name %></td>
    </tr>
  <% end %>
</table>
The table-primary class is only added to the row that has the location name corresponding to what's in @highlighted. If @highlighted is nil, then obviously nothing gets highlighted. Now when we click on a location link, the corresponding row is indeed highlighted in the location table:

Screenshot of highlighted row in location table

Nice, but we're not done. Looking back at the monster table, notice that we only included the first location for each monster, but there are potentially up to three locations a monster can be in. We want to add those other locations to the table when they exist. To add links for location2 and location3 safely, we need to conditionally add them only if they're not nil, and this is how we do that in the monster view template:
  <% @monsters.each do |monster| %>
    <tr>
      <td><%= monster.name %></td>
      <td><%= monster.role %></td>
      <td>
        <%= link_to monster.location.name, 
          location_index_path(:highlight => monster.location.name) %>
        <% if monster.location2 %>
          <%= link_to monster.location2.name, 
            location_index_path(:highlight => monster.location2.name) %>
        <% end %>
        <% if monster.location3 %>
          <%= link_to monster.location3.name, 
            location_index_path(:highlight => monster.location3.name) %>
        <% end %>
      </td>
      <td><%= monster.tame_rate %></td>
      <td><%= monster.max_level %></td>
      <td><%= monster.max_base_hp %></td>
      <td><%= monster.max_base_strength %></td>
      <td><%= monster.max_base_magic %></td>
      <td><%= monster.speed %></td>
      <td><%= monster.growth %></td>
      <td><%= monster.immune %></td>
      <td><%= monster.resistant %></td>
      <td><%= monster.halved %></td>
      <td><%= monster.weak %></td>
    </tr>
  <% end %>
</table>
We simply need to wrap each link in a condition with <%...%> template tags so the if-condition will run as Ruby code without returning any output to the view HTML. If the condition is false, meaning the location object is nil, then it will skip over creating the link to the following <% end %>. So, the template renderer actually understands Ruby code that crosses template tag boundaries. If we wanted to write multiple lines of Ruby code that either all generated HTML output or all didn't generate output, we could span lines with a single template tag, but here we needed to alternate between output and no output, so each tag is one Ruby statement. We can take advantage of the fact that the renderer crosses the template tag boundaries to make it all work. If we now take a look at our table, we see further down the list that we do indeed have two and three locations in some cells:

Screenshot of monster table with multi-location cells

Each link can be clicked to bring us to the location table with the appropriate row highlighted according to the source link. Just like we wanted, nice!

Linking Back to Monsters

We could fiddle with many more features in the monster table, but we've finished linking the monster table to the location table. It's time we think about linking back from the location table to the monster table. Remember that the monster table is much longer than the location table, and numerous monsters are going to be in any given location, so we don't want to just highlight all of the monster rows from the location that we clicked a link on. That would be tedious to scroll around the monster table finding all of the highlighted monsters strewn across the table. Instead, let's filter the table and show that filtered view. First, we can add links to the location view with the filter parameter included:
  <% @locations.each do |location| %>
    <tr <%= "class=table-primary" if location.name == @highlighted %>>
    <td>
      <%= link_to location.name, 
        monster_index_path(:filter => location.name) %>
    </td>
    <td><%= location.source&.name %></td>
    </tr>
  <% end %>
</table>
We send the location's name as the filter again here because we just want to send one name in the link, not all of the names for the monsters that can be found in that area. We'll figure out which monsters to show in the monster controller, like so:
class MonsterController < ApplicationController
  def index
    if params[:filter]
      location = Location.find_by(name: params[:filter])
      @monsters = location.location_monsters +
                  location.location2_monsters +
                  location.location3_monsters
    else
      @monsters = Monster.all
    end
  end
end
First, we check if there's a :filter parameter in the URL. If there is, we grab the object for that location name. (It's currently always a location name; maybe later we'll have to be more specific with our filters if we add more.) Then, we concatenate all of the monsters that can be found in that location, making sure to include the secondary and tertiary locations. We can use these associations because we set them up already in the location model with the has_many designation:
class Location < ApplicationRecord
  belongs_to :source, class_name: 'Location', optional: true
  has_many :location_monsters, :class_name => 'Monster', :foreign_key => 'location'
  has_many :location2_monsters, :class_name => 'Monster', :foreign_key => 'location2'
  has_many :location3_monsters, :class_name => 'Monster', :foreign_key => 'location3'
end
Now we can check to see if the filter works by going to the location page and clicking on a location, say "Academia 500 AF," and we get a smaller table of just the monsters found in that area:

Screenshot of monster table filtered by a location

That's pretty slick, and we only had to change a few lines of code to accomplish it. Rails is just so enjoyable to work with, and things are starting to get interesting. We're beginning to be able to answer some questions about the game just by browsing through the tables, like "which monsters can I find in this area?" And "how do I get to this location where such-and-such a monster is?" That's real progress! Since we didn't get to displaying all of the monster attributes, we'll do that next time, as well as starting in on our last two tables - the ability tables.

No comments:

Post a Comment