Drop Rate Quests
Goal : My plan was to turn this more into an MMO style game instead of the single player style. A common quest in an MMO is a quest that relies on drop rates. ie. Collect x amount of an item from y monster. I wanted the item to ONLY drop when the player has the quest that asks for them, and STOP dropping when they have the desired amount the quest is asking for. It's common to have a < 100% chance for this item, making the player kill more monsters to collect the desired amount. This posses a problem with the way the current game works, not having monster respawns, but I will address that in another post as I get there.
Execution : So here is what I did to achieve my goal.
1) In the RolePlayingGameWindows project I went into Content->Characters->Monsters. Each monster xml file in here has a section called GearDrops. If I want "gear", ie a Glimmering Ruby, to drop from a monster I would add it to the GearDrops section. The only problem is given the current way the system works, that monster would ALWAYS drop a Glimmering Ruby at the given %. so clearly a change is needed.
2) Inside each monsters xml file I added another tag to the GearDrops tag. I called it QuestName. The result looked something like:
| <GearDrops> |
| <Item> |
| <GearName>Items\MinorHealingPotion</GearName> |
| <DropPercentage>60</DropPercentage> |
| <QuestName></QuestName> |
| </Item> |
| <Item> |
| <GearName>Items\GlimmeringRuby</GearName> |
| <DropPercentage>100</DropPercentage> |
| <QuestName>The Goblin Brigade</QuestName> |
| </Item> |
| </GearDrops> |
3) So as you can see the MinorHealingPotion doesn't have anything in the QuestName section, which is fine. Code will be added to the project saying that if the QuestName is blank, just give this item a chance to drop on every kill. Notice the GlimmeringRuby has something in it's QuestName tag. Code will be added to say that if the player is on this quest, then give this item the drop % chance, otherwise don't drop at all.
4) Bring forth the code. So how did I make this work. First off I have to load and store the new xml tag QuestName. To do this, I needed to modify the GearDrop class in the RolePlayingGameDataWindows. I added another property in there called questName after the other 2 properties (I read somewhere that the order of the properties in your code is important. I think some reflection is being done with this content pipeline). So that looks like:
| /// <summary> |
| /// The content name of the gear. |
| /// </summary> |
| private string gearName; |
| |
| /// <summary> |
| /// The content name of the gear. |
| /// </summary> |
| public string GearName |
| { |
| get { return gearName; } |
| set { gearName = value; } |
| } |
| |
| |
| /// <summary> |
| /// The percentage chance that the gear will drop, from 0 to 100. |
| /// </summary> |
| private int dropPercentage; |
| |
| /// <summary> |
| /// The percentage chance that the gear will drop, from 0 to 100. |
| /// </summary> |
| public int DropPercentage |
| { |
| get { return dropPercentage; } |
| set { dropPercentage = (value > 100 ? 100 : (value < 0 ? 0 : value)); } |
| } |
| |
| /// <summary> |
| /// The content name of the gear. |
| /// </summary> |
| private string questName; |
| |
| /// <summary> |
| /// The content name of the gear. |
| /// </summary> |
| public string QuestName |
| { |
| get { return questName; } |
| set { questName = value; } |
| } |
5) Then in the GearDropReader I added the read in code which looks like:
| public class GearDropReader : ContentTypeReader<GearDrop> |
| { |
| protected override GearDrop Read(ContentReader input, |
| GearDrop existingInstance) |
| { |
| GearDrop gearDrop = existingInstance; |
| if (gearDrop == null) |
| { |
| gearDrop = new GearDrop(); |
| } |
| |
| gearDrop.GearName = input.ReadString(); |
| gearDrop.DropPercentage = input.ReadInt32(); |
| gearDrop.QuestName = input.ReadString(); |
| |
| return gearDrop; |
| } |
| } |
6) Now I have to do the write code. That is found in the RolePlayingGameProcessors project under Gear->GearDropWriter and will look like:
| public class GearDropWriter : RolePlayingGameWriter<GearDrop> |
| { |
| protected override void Write(ContentWriter output, GearDrop value) |
| { |
| output.Write(value.GearName); |
| output.Write(value.DropPercentage); |
| output.Write(value.QuestName); |
| } |
| } |
7) So that is all that is needed to get the data from the xml file into our project for us to use. So now we have to actually use it. After digging around it seems the Monster is responsible for deciding what it will drop. So we'll modify the Monster class from the RolePlayingGameDataWindows class.There is a function called CalculateGearDrop in Monster.cs. I first needed to know what the current quest the player was on, so I changed the definition to include a Quest object. I'll explain later where I pass this into this function. So in here it's going to loop through each GearDrop item tag in the xml file and use the drop % to determine if the item will drop or not. Well we want some additional logic in here to determine if an item that is specific to a quest should drop at all. Here is what I did to achieve that.
| public List<string> CalculateGearDrop(Random random, Quest q) |
| { |
| List<string> gearRewards = new List<string>(); |
| |
| Random useRandom = random; |
| if (useRandom == null) |
| { |
| useRandom = new Random(); |
| } |
| |
| foreach (GearDrop gearDrop in GearDrops) |
| { |
| // if the gear drop quest name is blank then just do normal random |
| // on this item |
| if (gearDrop.QuestName.Length == 0) |
| { |
| if (random.Next(100) < gearDrop.DropPercentage) |
| { |
| gearRewards.Add(gearDrop.GearName); |
| } |
| } |
| else |
| { |
| |
| // if there is a name, then this item will only drop off this |
| // mob if they are currently on the said quest name |
| if (q.Name == gearDrop.QuestName) |
{
| // if we already met our gear requirements, then skip this
if (q.AreGearRequirementsMet) continue;
|
// item from possibly dropping, note that I created the below property from the Quest.AreRequirementsMet method. This method checks if the gear requirements and monster requirements are complete all in once property. I felt it better to split these out into their own properties since in this case I only care about the gear requirements. I then use both new properties inside the AreRequirementsMet property so existing code continues to work. |
if (q.AreGearRequirementsMet) |
continue; |
|
| if (random.Next(100) < gearDrop.DropPercentage) |
| { |
| gearRewards.Add(gearDrop.GearName); |
| } |
| } |
| } |
| } |
| |
| return gearRewards; |
| } |
8) Alright so this is all fine and dandy but your build will fail because we added another parameter to the function, but the actual call hasn't passed the 2nd parameter yet. So I basically did a Find for the function name CalculateGearDrop. Turns out it's being called in the RolePlayingGameWindows project under Combat->CombatEngine.cs. I modified the function call to look like:
| monster.CalculateGearDrop(Session.Random, Session.Quest) |
After digging around in the Session object I noticed that the Quest property returned the current quest the player was on.
That's it. We now have a monster that will drop a Glimmering Ruby ONLY if the player is on the quest. Also, once you have received the amount of Glimmering Rubys the quest requires, more kills of that same monster while on that same quest will not drop a Glimmering Ruby. Feel free to ask question or give comments. More to come...