Friday, September 4, 2020

Exploring Monster Taming Mechanics In Final Fantasy XIII-2: Asking Deeper Questions

So far in this Exploring Monster Taming Mechanics series, we've parsed a bunch of data, built up database tables, and connected them together in a website with some filtering sprinkled in. This setup allows us to browse around the data pretty easily and ask some basic questions of the data. Now it's time to think about how we can ask deeper questions. Instead of just asking things like, "What monsters have the Auto-Bravery ability," we want to be able to ask, "Where is the earliest location where I can get a monster with the Auto-Bravery ability?" Sounds like a useful think to know, right? Let's figure it out.

Final Fantasy XIII-2 Narasimha tamed

Are we asking the right question?

It would certainly be interesting to know the earliest location where we can get a monster with a given ability, but we have access to more data than that, so we could actually answer a bigger question with the same effort. How about, "what are the locations where I can get a monster with the Auto-Bravery ability, sorted from earliest to latest?" We still need to find the earliest location where a tamable monster has the Auto-Bravery ability, but we can find out more this way. We've transformed the question from a linear search through the locations from earliest to latest into a filter-and-sort query, which is easy for a database to do.

This filter-and-sort query is still more complex than previous questions because we're taking into account more types of data. Instead of just filtering monsters by ability, we also need to sort them by location, and to do that sorting, we need to know the order of the locations. We do have a complete ordering in the database table because the locations were added to the table in such a way that dependent locations were added after their source locations were added. 

That's not the only valid ordering of the locations, though. Sometimes the game path forks, and in those cases the locations that both have the same source can be reached in either order. Since the locations make a tree graph with New Bodhum 003 AF at the root, we can add a depth attribute to the location table and assign each location a depth value based on how far it is away from the root. This depth value represents how many areas must be visited if we headed straight for the area in question. We could do this algorithmically, but the table only has 30 locations, so it's easy enough to assign by hand. We've added attributes to tables and views a half dozen times now, so I'll assume it's obvious how to do this and move on.

Sorting by Location Depth

Sorting by location depth isn't as hard as it may appear. We already have the filtering done between the links in the location table and the filter parameter processing in the monster controller, and we have the filtered list of monsters in the monster controller. All we have to do is sort that list by the location depth for where we can find each monster. Also, remember that a monster can be found in up to three different locations, so we're going to have to handle that detail as well. First, let's go ahead and add the sorting by location depth to the monster controller in app/controllers/monster_controller.rb:
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
elsif params[:ability_filter]
ability = Ability.find_by(name: params[:ability_filter])
@monsters = ability.get_all_monsters.sort_by { |monster| monster.first_location_depth }
elsif params[:skill_filter]
ability = RoleAbility.find_by(name: params[:skill_filter])
@monsters = ability.get_all_monsters.sort_by { |monster| monster.first_location_depth }
else
@monsters = Monster.all
end
end

def show
@monster = Monster.find(params[:id])
end
end
It ends up reading in code just about the same as it reads in the above description. I went ahead and did the sort on both abilities and skills here because it's the same code. Now, we need to define this first_location_depth method that we've called on the monster model. As long as it returns the depth of the location where the monster can first be found, sort_by will end up sorting the list of monsters the way we want it to. Here's one way to define that method in app/models/monster.rb:
class Monster < ApplicationRecord
# ... A whole mess of belongs_to macros ...

def first_location_depth
[location&.depth, location2&.depth, location3&.depth].compact.min
end
end
We simply build up a short array of depths using the existence (&.) operator for each potential location, compact the array to remove the nil values for the locations that didn't exist, and return the minimum value. That's all there is to it. When we click on the Auto-Bravery ability in the ability table, we are presented with a list of monsters with that ability sorted by the depth of the first location where we can find them:

Screenshot of list of monsters with Auto-Bravery sorted by first location depth

What's Left?

This feature of filtering and sorting by first location depth is pretty useful for finding monsters with certain abilities to tame. It's so useful, I wanted to do something similar with monsters' base strength or max level or some combination of those, but I realized that there's already a pretty effective way of looking for the strongest monsters at a given point in the game just by filtering monsters by location. You're normally interested in seeing which monsters you want to tame and keep in the location where you are right now or heading to next, and it's simple enough to just find the strongest monsters that can be leveled up the most by visual inspection of the filtered monster table. There aren't enough monsters in any given location to make this method burdensome, and we have plenty of info in this table to make informed choices.

Trying to automate this question of finding the strongest monsters also adds its own complications. What should we sort by, base strength, max level, or some combination of them? What if it's not what the user wants? Should we give the user the option? The solution to these questions will necessarily add complexity to the user interface that we should prefer to avoid. Side-stepping the issue and just not trying to do something for the user that is easier for them to do for themselves ends up being the better solution. We should always watch out for that simple case when designing a user interface.

Since we already have the feature of finding strong monsters in each location, the only things remaining are the two tables that we haven't connected: monster materials and monster characteristics. Monster materials is an interesting table that we can use to figure out how quickly we can level up the monsters we've tamed because we need to find the monsters that drop those materials that are used to level up our tamed monsters. However, enabling that feature is going to take a bit of work because the material table doesn't currently have the info for which monsters drop each material. We'll add that missing data and connect up that table next time.

No comments: