We've started asking deeper questions of the monster taming data in this Exploring Monster Taming Mechanics series, but so far we've ignored the fact that these monsters can level up once we've tamed them. Their starting stats and abilities will not stay the same while the monsters develop, so we should take that into account when we're trying to find the strongest monsters. Monsters that can level up to higher levels are going to end up stronger than other monsters that may start out stronger but can't level up as much. And the way these monsters level up is by applying certain materials dropped by defeating other monsters in battle. We need to know when and where we can get those materials, so that is the subject of this post.
Where Are The Monster Materials?
We have this monster materials table that lists all of the materials used to level up our monsters. The table has each material's name, its type, and its grade where higher grades of material are used on higher level monsters. This table does not, however, tell you where to find these materials. Since these materials are dropped by defeating monsters in battle, we could add a list of monsters that drop each material to this material table. Adding the data this way would require us to add some number of monsters to each material record, making sure that there's room in the table for the material dropped by the most monsters. The alternative is to add a material drop attribute to each monster. Since each monster drops at most one type of material, we only need to add one more attribute to the monster table to get this info, and then we can add references to and from the monster material table, just like we've done for other attributes. Conveniently, the monster table also contains the location of each monster, so it will be easy to see where we need to go for each material once this data is linked together. This approach seems to be a bit more sane, so let's go with it.DROP_SECTION_TAG = "MLDETAV"
END_DROP_SECTION_TAG = "|~|~~~~~~~~~"
NEW_DROP_MONSTER_REGEX = /^([\w\-]+(?:\s\S+)?)/
NEW_DROP_REGEX = /^Normal Drop: (?:\d\d\d% - )?(\S+(?:\s\S+)*)/
NEW_RARE_DROP_REGEX = /^Rare Drop--: (?:\d[\d\.]\d% - )?(\S+(?:\s\S+)*)/
section_tag_found = lambda do |line, data|
if line.include? SECTION_TAG
return find_separator, data
end
return section_tag_found, data
end
new_drop = lambda do |line, data|
if NEW_DROP_REGEX =~ line
drop_match = NEW_DROP_REGEX.match(line)
data.first[data.last] = [drop_match[1]]
elsif NEW_RARE_DROP_REGEX =~ line
drop_match = NEW_RARE_DROP_REGEX.match(line)
monster = data.pop
data.first[monster] << drop_match[1]
elsif NEW_DROP_MONSTER_REGEX =~ line
monster_match = NEW_DROP_MONSTER_REGEX.match(line)
return new_drop, data if monster_match[1] == 'Note'
return new_drop, data << monster_match[1]
elsif line.include? END_DROP_SECTION_TAG
return section_tag_found, data
end
return new_drop, data
end
find_drop_separator = lambda do |line, data|
if line.include? MONSTER_SEPARATOR
return new_drop, data << {}
end
return find_drop_separator, data
end
drop_section_tag_found = lambda do |line, data|
if line.include? DROP_SECTION_TAG
return find_drop_separator, data
end
return drop_section_tag_found, data
end
start = lambda do |line, data|
if line.include? DROP_SECTION_TAG
return drop_section_tag_found, data
end
return start, data
end
drops, *monsters = data
drops.default = ['None', 'None']
data = monsters.map do |monster|
name = monster['Name']
monster['Normal Drop'] = drops[name].first
monster['Rare Drop'] = drops[name].last
monster
end
- Feral Behemoth 🠊 Behemoth
- Greater Behemoth 🠊 Grand Behemoth
- Munchkin Maestro 🠊 Munchkin Boss
- Zwerg Metrodroid 🠊 Zwerg Metro
- Metal Gigantuar 🠊 Metalligantuar
- Pulsework Gladiator 🠊 Pulse Gladiator
- Pulsework Knight 🠊 Pulse Knight
- Pulsework Soldier 🠊 Pulse Soldier
- Flowering Cactuar 🠊 Cactrot
- Lieutenant Amodar 🠊 Amodar
VALID_MONSTER = {
"Name" => PROPER_NAME_REGEX,
"Role" => PROPER_NAME_REGEX,
# ... All the other attributes. They aren't all PROPER_NAME_REGEX. Really ...
"Normal Drop" => PROPER_NAME_REGEX,
"Rare Drop" => PROPER_NAME_REGEX,
}
And we can run the script to regenerate monsters.csv with the drops included.Adding the Drops to the Monster Table
class CreateMonsters < ActiveRecord::Migration[6.0]
def change
create_table :monsters do |t|
t.string :name
t.string :role
t.references :location, foreign_key: true
t.references :location2, foreign_key: {to_table: :locations}
t.references :location3, foreign_key: {to_table: :locations}
t.integer :max_level
t.integer :speed
t.string :tame_rate
t.string :growth
t.string :immune
t.string :resistant
t.string :halved
t.string :weak
t.references :normal_drop, foreign_key: {to_table: :materials}
t.references :rare_drop, foreign_key: {to_table: :materials}
# ... many more attributes ...
end
end
end
Note that the normal_drop and rare_drop attributes are created as references to the materials table because we want to link them that way. Also note that I refrained from naming normal_drop just drop. It doesn't seem right to call an attribute in a database table drop, does it? Next, we can add the connections for these attributes to the Monster model in app/models/monster.rb:
class Monster < ApplicationRecord
belongs_to :location, class_name: 'Location', optional: true
belongs_to :location2, class_name: 'Location', optional: true
belongs_to :location3, class_name: 'Location', optional: true
belongs_to :normal_drop, class_name: 'Material', optional: true
belongs_to :rare_drop, class_name: 'Material', optional: true
# ... many more belongs_to declarations ...
end
These declarations are where we get the normal_drop and rare_drop method names that we can later use in the controller, view, and db seed script, so they really need to be there right away before we import the data into the database. I forgot this and was confused for a while when my seed script didn't work. We also add the connections in the opposite direction to the Material model in app/models/material.rb:
class Material < ApplicationRecord
has_many :normal_drop_monsters, :class_name => 'Monster', :foreign_key => 'normal_drop'
has_many :rare_drop_monsters, :class_name => 'Monster', :foreign_key => 'rare_drop'
# ... no more has_many declarations because this is a small table and this is it!
end
Not much more to say about this code. It's pretty standard by now. As for the db/seeds.rb script, it's an easy addition of two lines at the end of the loop that creates all of the monsters:
csv_file_path = 'db/monsters.csv'
CSV.foreach(csv_file_path, {headers: true}) do |row|
# ... all of the other monster attribute processing code ...
monster['normal_drop'] = Material.find_by(name: monster['normal_drop'])
monster['rare_drop'] = Material.find_by(name: monster['rare_drop'])
Monster.create!(monster)
puts "#{row['name']} added!"
end
Easy as pie. This code even takes care of all of those drops that are not monster materials because it won't find them in the Material table and just assigns the monster attributes to nil. It's so elegant and simple, just how I like my code. At this point we're ready to wipe it and redo it at the command prompt:
$ rails db:drop
$ rails db:migrate
$ rails db:seed
When those commands are done running, we're almost ready to see what we've done.Adding Materials to the Monster View
<h1><%= @monster.name %></h1>
<table id="monster-table" class="table table-striped table-sm">
<!--many table rows-->
<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>Normal Drop</strong></td>
<td>
<% if @monster.normal_drop %>
<%= @monster.normal_drop.name %>
<% else %>
--
<% end %>
</td>
<td><strong>Rare Drop</strong></td>
<td>
<% if @monster.rare_drop %>
<%= @monster.rare_drop.name %>
<% else %>
--
<% end %>
</td>
</tr>
<!--many more table rows-->
</table>
We'll add the drops right after the halved and resistant attributes, and we can see that all of our script changes worked:Filtering Monsters on Materials
<h1><%= @monster.name %></h1>
<table id="monster-table" class="table table-striped table-sm">
<!--many table rows-->
<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>Normal Drop</strong></td>
<td>
<% if @monster.normal_drop %>
<%= link_to @monster.normal_drop.name,
material_index_path(highlight: @monster.normal_drop.name) %>
<% else %>
--
<% end %>
</td>
<td><strong>Rare Drop</strong></td>
<td>
<% if @monster.rare_drop %>
<%= link_to @monster.rare_drop.name,
material_index_path(highlight: @monster.rare_drop.name) %>
<% else %>
--
<% end %>
</td>
</tr>
<!--many more table rows-->
</table>
The code is now littered with these features, so it's pretty easy to copy other examples in the code to make these changes work. The mechanics are all explained in detail in the Viewing the Monster Data episode. The next change is to assign the :highlight parameter that was passed in the URL to a variable in app/controllers/material_controller.rb:
class MaterialController < ApplicationController
def index
@materials = Material.all
@highlighted = params[:highlight]
end
end
<h1>Monster Materials</h1>
<table id="material-table" class="table table-striped table-sm">
<thead class="thead-dark">
<tr>
<th scope="col">Name</th>
<th scope="col">Grade</th>
<th scope="col">Type</th>
</tr>
</thead>
<% @materials.each do |material| %>
<tr <%= "class=table-primary" if material.name == @highlighted %>>
<td><%= link_to material.name,
monster_index_path(:drop_filter => material.name) %></td>
<td><%= material.grade %></td>
<td><%= material.material_type %></td>
</tr>
<% end %>
</table>
Like in the other tables, we simply add a class of table-primary to the highlighted row. I also went ahead and added the link back to the monster table with a drop_filter parameter. To use the filter parameter, we can modify 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 }
elsif params[:drop_filter]
material = Material.find_by(name: params[:drop_filter])
@monsters = material.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
Finally, we need to define the super simple get_all_monsters for the Material class in app/models/material.rb:
class Material < ApplicationRecord
has_many :normal_drop_monsters, :class_name => 'Monster', :foreign_key => 'normal_drop'
has_many :rare_drop_monsters, :class_name => 'Monster', :foreign_key => 'rare_drop'
def get_all_monsters()
normal_drop_monsters + rare_drop_monsters
end
end
Amazingly, with this change we are done, and we have all of the functionality we were aiming for with the monster materials table. Here's a view of monsters that drop Potent Sliver and are sorted by location depth:
No comments:
Post a Comment