First, allow me to apologize, because this is going to be a fairly long post. I've given this topic a lot of thought, and even experimented with concepts and previous implementations.

When I imagine a loot table, I always think of something a little like how World of Warcraft does theirs. Here is a picture example:

enter image description here

Now, just using visual cues, when can pretty much dissect the way the loot system works, but let's also go ahead and use some Wowhead statistics to glean a better understanding for what we're dealing with. Here's a quick mock up: enter image description here

As you can see, the loot system that World of Warcraft uses is quite dynamic. It's not exactly a roulette, but can, at it's base level, resemble one in ways. My first implementation of a Loot Table avoided the concept of a roulette, and even a weighing mechanic(which gives certain items a greater probability of being taken from the table, and leave others). I did not like the idea of weighing items in this nature. So I made something sort of like this:

enter image description here

This Loot Table was more of a "List". Basically, I would loop through each item, test it's probability, and if it was succesful, I would add it to a list of drops. This worked well in theory, but I was quick to notice a fatal flaw in the system.

Consider this output:

Ore_Copper x1
Nugget_Copper x2
Gem_Diamond x1
Gem_Emerald x1
Gem_Topaz x1

That's an exgaggeration, but if you look at the basic structure it's entirely possible...but what if we didnt want to drop 4 types of gem? And also, the collective probability of the gems also makes you practically guaranteed to recieve at least one, because you have a 10%*5 chances to get one. Now, this may be ok for a game, but it's not what I had intended. I wanted the ability to drop gems, but with only one chance. I wanted the probability of recieving a gem to be exactly 10%. So...how do we fix this? Here's what I came up with:

enter image description here

So what's happening now? We have essentially built a roulette system. Instead of a direct collection of LootTableItems, we now defer items into independent collections. LootTableLists. Each list has it's own probability, much like my original LootTable, but now we can successfully organize items into their own "pool" within a single LootTable. If we want an item to be less rare than another, we just add extra instances to the desired LootTableList. Then, when my LootTable is running it's algorithm, it selects one item from the LootTableList. This is great, but it also brought up another issue. What if I wanted all items in a LootTableList to be dropped, or have a chance of dropping? Well, I went ahead and added an extra field, called CollectAll. When this field is checked, it avoids picking one item, and instead picks all items in the LootTableList.

After planning all this, and doing my homework on the subject, I came to creating this kind of LootTable. I'm also fairly certain that this is also the type of LootTable that World of Warcraft is using, or at least conceptually they're very close. Here's the code:

using System;
using System.Collections.Generic;
using System.Linq;
using GrimoireDevelopmentKit.Framework.Interfaces;
using GrimoireDevelopmentKit.Framework.Maths;
using GrimoireDevelopmentKit.Framework.Utilities;

namespace GrimoireDevelopmentKit.Framework.Collections
{
    [Serializable]
    public class LootTable : Dictionary<string, LootTableList>, ICopy<LootTable>
    {
        public string Identifier { get; set; }
        public AdvancedRandom Random { get; set; }

        public int TotalItems
        {
            get
            {
                int total = 0;
                foreach (LootTableList list in this.Values)
                {
                    total += list.Values.Count;
                }
                return total;
            }
        }

        public LootTable(string identifier = null)
            : this(identifier, new AdvancedRandom())
        {
        }

        public LootTable(string identifier, AdvancedRandom random)
        {
            Identifier = identifier;
            Random = random;
        }

        public void Add(LootTableList list)
        {
            if (!Contains(list))
            {
                Add(list.Identifier, list);
            }
            else
            {
                throw new ArgumentException("Key: '" + list.Identifier + "' already exists!");
            }
        }

        public bool Remove(LootTableList list)
        {
            return Remove(list.Identifier);
        }

        public bool Contains(string listIdentifier)
        {
            return ContainsKey(listIdentifier);
        }

        public bool Contains(LootTableList list)
        {
            return Contains(list.Identifier);
        }

        public List<LootTableResult> Next()
        {
            Range probabilityScope = new Range(0, 100);
            List<LootTableResult> drops = new List<LootTableResult>(TotalItems);
            foreach (LootTableList list in Values)
            {
                bool success = Random.NextRange(probabilityScope) <= list.Probability;
                if (success)
                {
                    if (list.CollectAll)
                    {
                        foreach (LootTableItem item in list.Values)
                        {
                            int quantity = Random.NextRange(item.Quantity);
                            if(quantity > 0)
                            {
                                drops.Add(new LootTableResult(item.Identifier, quantity));
                            }
                        }
                    }
                    else
                    {
                        LootTableItem item = list.ElementAt(Random.NextInteger(0, list.Count)).Value;
                        int quantity = Random.NextRange(item.Quantity);
                        if(quantity > 0)
                        {
                            drops.Add(new LootTableResult(item.Identifier,quantity));
                        }
                    }
                }
            }
            return drops;
        }

        public void Copy(LootTable other)
        {
            this.Clear();
            this.Identifier = other.Identifier;
            this.Random = other.Random;
            foreach (LootTableList list in other.Values)
            {
                this.Add(list.MakeCopy());
            }
        }

        public LootTable MakeCopy()
        {
            LootTable copy = new LootTable();
            copy.Copy(this);
            return copy;
        }
    }

}

My Question:

Is there a better and more modern conceptual foundation that I could use to achieve this kind of loot table?

Are there any game developers on this site that have created Loot Tables of this nature? I just want to know if I could improve my concept, not just the code, but the concept itself. I'm certain there is more that can be done to improve upon my work. I'm just looking for advice from people experienced in this subject.

I can also post my implementation into a separate project if it would help people help me. In fact, I think I'm going to work on that right now actually.

EDIT

Just realized I forgot to code the Quantity count in the LootTableLists, which if coded, would allow a LootTableList to be collected from multiple times during the same drop.

  • 2
    I'm not sure what you're asking here. It's impossible to answer "how can I make this better" if you haven't given criteria on what constitutes "better". Instead I see that you had some design problems - how can I only drop one item, or drop all items - but you solved them already. – congusbongus May 12 '17 at 3:22
  • I will edit to make it more clear. – Krythic May 12 '17 at 3:25
  • 1
    Your edit only adds a comment; it does nothing to make it more clear what your actually asking. – Gnemlock May 12 '17 at 3:45
  • 2
    What is wrong with the not-so-modern approach? Is there something that you want to do that you can't with a simpler solution? - For instance, you don't like using weights, is there something that you want to do that weights would prevent you from doing? - Taken that you don't want to use weights that doesn't tell enough to know what direction to go - It seems to be you just want to know the state of the art. If that's the idea, it means that as soon something "more modern" comes along all the answers will be obsolete. – Theraot May 12 '17 at 4:10
  • 1
    If you want your loot system to deliver a more even and balanced game experience without completely eradicating surprise, you might find this question interesting: How can I make a “random” generator that is biased by prior events? – Philipp May 12 '17 at 6:40
up vote 3 down vote accepted

You are on the right track. That is where I would start. All of your components share the identifier and quantity. It makes sense to add chance property and use composite pattern.

public class Droppable
{
    public string Name { get; private set; }
    public uint QTY { get; private set; }
    public float Chance { get; private set; }

    public Droppable(string name, float chance, uint qty)
    {
      this.Name = name;
      this.QTY = qty;
      this.Chance = chance;
    }

    public virtual bool ShouldDrop()
    {
    // random drop check
    }
}

You can then create your loot table extending Droppable component.

public class DropBundle extends Droppable
{
    public bool CollectAll { get; private set; }
    private List<Droppable> _items;

    public DropBundle(string name, float chance, uint qty, bool collectAll, List<Droppable> items): base(name, chance, qty)
    {
        CollectAll = collectAll
        _items = items;
    }

    public override bool ShouldDrop()
    {
    // random drop check and if yeas run should drop check for other items
    }
}

You also need your item class that would also extend Droppable

public class DropItem extends Droppable
{

   public override bool ShouldDrop()
   {
      // random drop check
   }
}

Notice CompositeItem holds references to other Droppable components. That means you can then next them as much as you want, eg.:

  • 5% of finding 10 gold coin (DropItem)
  • 10% of finding a coin (CompositeItem) wich can be either gold (20%) or silver (80%)
  • 20% of finding a chest (CompositeItem) that can have 10% chance of finding a coin (CompositeItem) which can be either gold, silver or copper, etc.

All of those scenerios are then possible without changing dependency tree too much. The only thing you have to remember is obviously implement randomisation and keep in mind that sum of chances of nested droppables should be 1 (100%) for simplicity sake.

Another great advantage of using composite pattern is ability to later on use that implementation for data you can dynamically read from static XML (or JSON) file.

Your Answer

 
discard

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.