-
Notifications
You must be signed in to change notification settings - Fork 12
Documentation: General Engine Design
The idea behind Shadowcraft isn't to be for rogues by rogues, oddly enough. Shadowcraft is a general purpose code bank designed to collate data chunks, and let developers run with a baseline structure of data. So the primary goal of the engine itself is to create just the bare essentials for collecting that information, and making the right methods available to use that data. This means code needs to be broken up into objects, while inheriting the base methods available to all classes.
Because of this, parent classes should be limited to abstract methods and functionality. The most basic object should largely consist of collecting and providing data, the next layer should implement class functionality, then expanding to methods for actually creating algorithms with subsequent objects.
What people think of Shadowcraft, is actually the Aldriana extension of the Rogue module. You can better understand this point of distinction by looking towards the start of some of the __init__.py files, like class AldrianasRogueDamageCalculator(RogueDamageCalculator): in the Aldriana extension. These class definitions set inheritance for the object before them. The calcs/__init__.py object is the utmost basic, it just collects things like gear details, buffs, etc for other pieces of code to rely on. The calcs/rogue/__init__.py module expands on that by creating common rogue methods and data that, as you might imagine, would be shared among anyone trying to do rogue calculations (things like damage formulas, etc). calcs/rogue/aldriana/__init__.py is where the more abstract math gets done, and actually makes things out of bits of clay.
So lets describe how Shadowcraft groups data to be used by analyzing example scripts that are included in the repo. Every script starts off listing the objects you need to pass to the engine, ie. from shadowcraft.objects import talents. After this, the script starts to actually make those objects for later. For example, test_talents = talents.Talents('332213', test_class, test_level) asks the talent class imported earlier to create a new Talents() object with the relevant parameters. You can see how these objects are constructed and behave in the shadowcraft/objects folder, and constructors are defined as def __init__(): inside of a class definition class Talents(object):. Every class should inherit some basic functionality, which is why the Talents class is set to inherit object, which is a python generic class.
After all these objects are created, the script finally brings them together under a single object calculator = AldrianasRogueDamageCalculator(test_stats, test_talents, ...). This is partially why I drew that line about aldriana, rogue, and calcs objects, and their inheritance structure. You construct an aldriana object, which pulls from the rogue module, which then also pulls from the base calcs object. To put it slightly more visually:
class AldrianasRogueDamageCalculator(RogueDamageCalculator):
/
class RogueDamageCalculator(DamageCalculator):
/
class DamageCalculator(object):
Inside the code for those 3 classes, you can notice how neither of the first two objects have a constructor, they just implicitly run the parent constructor. In this case, creating an AldrianasRogueDamageCalculator object ends up using the DamageCalculator class constructor. If you wanted to run a special constructor that added to the parent's, you would run super(RogueDamageCalculator, self).__init__(...) much like you would another language. Super() generating the parent code, __init__() being the method (which is the constructor in this case). I believe you can also use the class name instead of __init__() and python will understand that it refers to the constructor, but __init__ is usually less keystrokes.
Once everything has been brought under one roof, the calculator that was created can have methods executed pulling from everything that's been created so far. The most important of those is dps_breakdown = calculator.get_dps_breakdown(), and at that point it starts to get mathy and complex. The methods you would call are also almost entirely limited to the aldriana module in this scenario. The methods for calculating the damage breakdowns is outside the scope of general engine design, and should exist almost exclusively in the top most levels of your modelling approach.
After this, scripts are really just formatting data and printing that data to the console.