- Ddlib at a glance
- Basic definitions
- Getting started
-
Character generation
- Create a character and randomly generate skill values the satisfy race/class boundaries
- Create a character and assign the desired skill values
- Randomly generate skill values and select race, class and moral alignment available accordingly
- Select skill values and select race, class and moral alignment available accordingly
- Character life cycle
- Money management
- Multistore market abstraction
- Advanced features
Ddlib is an open role playing game library written in C++. It aims to mimic the ruleset of Advanced Dungeons & Dragons 2nd Edition. Ddlib hides the burden of dealing with the complexity of AD&D ruleset, simplifying the creation of applications like role playing games, character generators, simulators, and things like that. It may sound an ambitious project, and in fact it is. I'm working on it in my spare time. At the time of writing a vast array of both basic and advanced features are available. They include:
- Character generation: single/multiclass characters;
- Experience: gain/loss, level advancement/loss;
- Effects of cure/cause wounds;
- Turn undead;
- Saving rolls;
- Money bag;
- Ageing effects;
- Skills modifiers for race;
- THAC0 and modifiers for strength/dexterity;
- Thief abilities for thieves, bards and rangers;
- Combat: use of weapons, initiative, damage, hit and failure;
- Encumbrance and effect on movement and THAC0;
- Market abstraction, buy and sell goods;
- Deities, plane of existence and connection to priests of specific mythos;
- Missile weapon modifiers;
- Armours and Armour Class modifiers;
- Characters' inventory/equipment;
- Spells;
- Monsters;
- Treasures;
- Equipment file.
There are some open points I'd eventually like to complete, listed in the CHANGELOG file, but I'm proud to say most of the core functionalities are available.
Ddlib at a glance
The library is organised into three tiers: the data layer, metadata (or template layer) and the object layer. The data layer is a set of tables and data viewes constituting the core of the AD&D ruleset. The library organise this information into an in-memory SQLite database for ease of use, and makes access to this data in the upper layers.
The next layer is the metadata/template layer, included in the templates
namespace. A template is an abstract concept of the AD&D framework, like a monster, a treasure or an equipment item. A template describes the common features of all objects of the same type, for instance the classes and advancement limits available to the Elven race or combat abilities of a Bugbear.
The third layer is the object layer, where AD&D objects are instantiated and made available for use. The object layer classes are included in the namespace adnd
.
While data layer cannot be directly accessed by the final user, the template information can, even though it's not required. Templates exists for every concept made available by the library and can be retrieved by specialised calls.
adnd::defs::character_class cls = adnd::defs::character_class::cleric;
auto ct = adnd::templates::metadata::get_instance().get_class_template(cls);
defs::deities::deity deityId = defs::deities::deity::lathander;
auto dt = templates::metadata::get_instance().get_deity_template(deityId);
The ct
variable is a template for the cleric class, while dt
is a template for a deity of the AD&D patheon, the Morninglord Lathander. By means of a template object, useful information can be retrieved. For example, to retrieve the hit die for a thief, simply type:
using namespace adnd;
templates::character t = adnd::templates::metadata::get_instance().get_class_template(defs::character_class::thief);
random::die d = t.hit_dice();
To enquiry if The Black Lord is worshipped by CN characters, or if you need to know his rank as a deity:
defs::deities::deity deityId = defs::deities::deity::bane;
auto dt = templates::metadata::get_instance().get_deity_template(deityId);
bool checkCN = dt.is_worshipped_by(defs::moral_alignment::chaotic_neutral);
auto rank = dt.get_rank();
Normally, the final users should not care if their CG Fighter/Thief can worship Bane, for instance. This is done by the library and exceptions are thrown in case of errors. For example, creating an Elven Paladin rises an error and should be enclosed in a try/catch
block:
using namespace adnd;
try
{
character elvenPaladin("Elven Paladin", defs::character_class::paladin, defs::character_race::elf, defs::character_sex::male, defs::moral_alignment::lawful_good);
...
}
catch (std::exception& e)
{
std::cout << e.what() << "\n";
}
At the time of writing, templates are available for a variety of library objects, like:
- character classes;
- races;
- equipment items, weapons, projectiles and armours;
- coins;
- deities;
- skills;
- treasures, gems and objects of art;
- moral alignments;
- magical items;
- spells, school of magic and spheres of influence;
- monsters.
Basic definitions
Common definitions and enumerations are defined in the defs
namespace. Definitions include all the set of possible values the library functions may accept, for instance moral alignments, spells, monsters and weapons.
The library functions make use of such definitions to ensure a coherent programming interface.
Some of the most common definitions found across the library are:
- character_class;
- character_class_type;
- character_race;
- character_sex;
- moral_alignment.
Each of these definitions enumerate the set of valid values.
Getting started
When the library functions are first accessed, the library initialises itself. This operation can take a while, depending on the library configuration. The library initialisation can also be done manually typing:
adnd::templates::metadata::init();
Manual initialisation of the library is the recommended practice because it guarantees faster response at the first call.
After initialisation is done, the final user should be uniquely concerned of the adnd
namespace. This namespace contains a set of classes and data types that the final user can come across and manipulate, the most important of which is character
. A character represents a playing character and it's the core object around which the users build their own applications. Handling a character
object it's possible for a character to gain/lose experience, acquire/drop items, use abilities, select a weapon to use, attack, being wound, die and come back to life.
using namespace adnd;
character chr("Ileria", defs::character_class::cleric, defs::character_race::half_elf, defs::character_sex::female, defs::moral_alignment::chaotic_good);
auto t = chr.turn_undead(defs::turn_ability::turn_hd2);
std::cout << chr.get_name() << ", level " << chr.level().get_level() << " requires " << t << " to turn a 2HD monster " << std::endl;
chr.level() += 460000;
auto t = chr.turn_undead(defs::turn_ability::turn_ghoul);
std::cout << chr.get_name() << ", level " << chr.level().get_level() << " requires " << t << " to turn a ghoul monster " << std::endl;
std::cout << chr.get_name() << ", level " << chr.level().get_level() << " tries to save againt petrification";
if (chr.save_against(defs::saving_throws::petrification))
std::cout << " and succeedes!" << std::endl;
else
std::cout << " and fails..." << std::endl;
The snippet above shows some of the library capabilities. The character creation is perhaps one of the most common use cases. The final user is simply required to pass in the main character attributes, those that cannot be infered by the library. Then, experience advancement, spell capabilities (if any) or other class abilities, age, weight, height and starting money budget are automatically generated according to the AD&D ruleset tables.
Character generation
The library allows for different ways to generate a character. The basic idea is to call the 'character' class constructor, passing in the desired parameters. Some parameters are mandatory (marked with *), other are optionals. They are:
Parameter | Type | Possible values | Default value |
---|---|---|---|
Name * | String | any | |
Class * | Enumerator | defs::character_class | |
Race * | Enumerator | defs::character_race | |
Sex * | Enumerator | defs::character_sex | |
Alignment * | Enumerator | defs::moral_alignment | |
Maximise HP | Boolean | true/false | false |
Deity | Enumerator | defs::deities::deity | defs::deities::deity::none |
Perhaps the most important choice done when creating a character is the class. The list of available values is (the four basic classes are in bold):
- Fighter;
- Paladin;
- Ranger;
- Mage;
- Abjurer;
- Conjurer;
- Diviner;
- Enchanter;
- Illusionist;
- Invoker;
- Necromancer;
- Transmuter;
- Cleric;
- Druid;
- Preist of specific mythos;
- Thief;
- Bard;
- Fighter/Mage;
- Fighter/Cleric;
- Fighter/Thief;
- Mage/Thief;
- Mage/Cleric;
- Cleric/Thief;
- Fighter/Mage/Cleric;
- Fighter/Mage/Thief;
- Fighter/Druid;
- Fighter/Illusionist;
- Cleric/Illusionist;
- Illusionist/Thief;
- Cleric/Ranger;
- Mage/Druid;
- Fighter/Mage/Druid.
Enumerator parameters must be chosen among the possible values in the enumeration, bearing in mind that poor choices may result in errors.
Choosing defs::character_class::paladin
and race other than defs::character_race::human
or class defs::character_class::druid
and a non neutral moral alignment is bound to raise an error.
Maximise HPs allows to assign the character the maximum score each time a hit die is rolled.
At creation time, a character can be assigned a deity and becomes his/her worshipper. This is purely decorative in most cases, except for priest of specific mythos, for whom the spell set may vary according to the spheres of influence ruled by the god.
In addition, when this parameter is specified, the library checks if the character moral alignment is compatible with the chosen deity.
The most common methods to generate a character are:
- Create a character and randomly generate skill values that satisfy race/class boundaries;
- Create a character and assign the desired skill values (exceptions are thrown if those values don't satisfy race/class boundaries);
- Randomly generate skill values and select race, class and moral alignment available accordingly;
- Select skill values and select race, class and moral alignment available accordingly.
Create a character and randomly generate skill values the satisfy race/class boundaries
This method assures the character you get is exactly what you want and the skill values are guaranteed to suit all the requirements. Simply typing:
using namespace adnd;
character chr("Gimly", defs::character_class::fighter_thief, defs::character_race::dwarf, defs::character_sex::male, defs::moral_alignment::chaotic_neutral);
The skills are randomly generated using the Standard generation method. This means for each skill three dice are rolled and summed up to form the skill value. The character starts at level 1 in each class (in case of multiclass) and no experience (i.e. zero XP). To create a character of higher level, experience has to be added manually.
Create a character and assign the desired skill values
This method gives full control over the character creation, but can raise errors if parameters are not chosen properly. For example, is the prime requisites don't satisfy the class/race requirements, or the moral alignment conflicts with the class ethos. As shown, the following snippet should be enclosed in a '''try/catch''' block:
using namespace adnd;
character humanPaladin("Paladin", defs::character_class::paladin, defs::character_race::human, defs::character_sex::male, defs::moral_alignment::lawful_good);
skills::character_skill_set set;
set.set(18, 85, 13, 16, 12, 13, 17);
try
{
humanPaladin.set_skills(set);
//...
}
catch (std::exception& e)
{
std::cout << "Unable to assign skill values: " << e.what() << std::endl;
}
Randomly generate skill values and select race, class and moral alignment available accordingly
This method may be used to create an interactive character generator, where the final users can, at each step, make their personal choice among the possible values suggested by the application. This is done generating a set of skills. In this example, the Best of four method is used (that is, for each skill four dice are rolled at once, the three best scores are chosen and summed up to form the skill value).
using namespace adnd;
skills::skill_generator sklGen(skills::generation_method::best_of_four);
skills::character_skill_set skills;
sklGen.generate(skills);
std::cout << "Generated skills:\n";
std::cout << " Strength: " << skills.strength() << "\n";
std::cout << " Dexterity: " << skills.dexterity() << "\n";
std::cout << " Constitution: " << skills.constitution() << "\n";
std::cout << " Intelligence: " << skills.intelligence() << "\n";
std::cout << " Wisdom: " << skills.wisdom() << "\n";
std::cout << " Charisma: " << skills.charisma() << "\n";
std::list<defs::character_race> races = templates::metadata::get_instance().get_available_races(skills);
//...
The list of available races (those allowing the generated skills) are returned. The basic idea is to let the final users to choose a race among them and store the selected value into a variable.
Let that variable be chosenRace
. Then a list of character classes and allowed moral alignments can be obtained simply typing:
auto rt = templates::metadata::get_instance().get_race_template(chosenRace);
std::list<defs::character_class> classes = rt.get_available_classes(skills);
// Select a class from the *classes* list and call it chosenClass...
auto ct = templates::metadata::get_instance().get_class_template(chosenClass);
std::set<defs::moral_alignment> aligns = ct.get_moral_alignments();
Then all the required parameters are available to create a character. The character name and sex don't have effect on skills and abilities. The choice of sex may influence the height and weight of the character. Infact, men tend to be taller and heavier than women. This aspect may influence the game when it comes to check if a bridge or a pitfall holds up or not when the character absently walks on it, but that's life...
Select skill values and select race, class and moral alignment available accordingly
The last method differs from the third in the first statements. Instead of a random generation, skill values are manually selected
using namespace adnd;
skills::character_skill_set skills;
skills.set(18, 85, 15, 16, 18, 14, 11);
std::list<defs::character_race> races = templates::metadata::get_instance().get_available_races(skills);
// Select a race from the *races* list and call it chosenRace...
auto rt = templates::metadata::get_instance().get_race_template(chosenRace);
std::list<defs::character_class> classes = rt.get_available_classes(skills);
// Select a class from the *classes* list and call it chosenClass...
auto ct = templates::metadata::get_instance().get_class_template(chosenClass);
std::set<defs::moral_alignment> aligns = ct.get_moral_alignments();
// Create a character...
chr.set_skills(skills);
Skill values can be modified via 'set_skill' method of a character. Modifying skills may have effect on other character statistics like hit modifier, weight allowance (and movement factor consequently), natural armour class and many other. Altering the dexterity score, for instance, changes the character natural armour class. The constitution score affects the chance to survive Resurrection (System shock). The strength score alters the character capabilities to carry weights. Modifying the prime requisite skill values can alter the experience progress. Characters with 16+ values on all prime requisite skills gain +10% extra experience.
Skill values can also be altered as a consequence of ageing. When a characters are first created, they are considered young (according to their race). Humans for instance start as 16 to 19 years old, while elves are usually 100+ years old. As they grow old, be it by the touch of a Ghost, or by natural ageing process, their skill values are modified accordingly.
Character life cycle
Once created, a character can interact with the world and live adventures. That means, events may occur that change his/her properties, like:
- gain/lose experience;
- use abilities;
- collect treasures;
- spend money and buy/sell equipment;
- combat;
- get specialised/proficient using weapons;
- casting spells;
- being wounded, cured, killed and resurrected.
All this features are performed calling methods of the character
class.
Money management
When characters are created, a starting amount of money is assigned according to their class type. Money can be collected and spent. When the amount spent exceeds the amount available, an error is raised. A character owns a money bag which can contain five different types of coins of different value:
- Copper (CP);
- Silver (SP);
- Electrum (EP);
- Gold (GP);
- Platinum (PP);
When money is collected, it's possible to specify the type of coin. The money bag keeps track of the different currencies simplifying the loot collection. The money bag also simplify the buy/sell operations when the character has got the change back. For example, a fighter starts with 5d4 x 10 GP, that means between 50 and 200 GP. Let's say he's got 180 GP and wants to buy a spear(cost 8 SP). The user is not required to deal with exchange ratios, instead the library carries this duty and changes the money bag composition. When this operation is not possible (i.e. the character hasn't enough money) an error is raised. A money bag can also be normalised. Imagine that after some adventures your character decides to optimise the content of his/her money bag, getting rid of the Copper pieces and turning them into coins of higher values. Then he/she decides to do the same with Silver pieces, and so on.
For example, a money bag containing 19 EP and 46 SP, after normalisation will contain 2 PP, 4 GP and 1 SP.
using namespace adnd;
character chr("Boromir", defs::character_class::fighter, defs::character_race::human, defs::character_sex::male, defs::moral_alignment::lawful_neutral);
for (auto cn : { adnd::defs::coin::copper, adnd::defs::coin::silver, adnd::defs::coin::electrum, adnd::defs::coin::gold, adnd::defs::coin::platinum })
{
auto t = adnd::templates::metadata::get_instance().get_coin_template(cn);
auto amount = chr.money().get(cn);
std::cout << "Boromir owns " << amount << " (" << t.get_description() << ")\n";
}
uint32_t amt = 105;
std::cout << "Boromir gains " << amt << " EP\n";
chr.money().add(adnd::defs::coin::electrum, amt);
for (auto cn : { adnd::defs::coin::copper, adnd::defs::coin::silver, adnd::defs::coin::electrum, adnd::defs::coin::gold, adnd::defs::coin::platinum })
{
auto t = adnd::templates::metadata::get_instance().get_coin_template(cn);
auto amount = chr.money().get(cn);
std::cout << "Boromir owns " << amount << " (" << t.get_description() << ")\n";
}
The snippet above shows the content of the character's money bag. Then, 105 EP are added and money bag displayed again.
Multistore market abstraction
In the AD&D framework, new items are often collected during quests, but they can also be bought and sold. Characters can buy or sell stuff at the stores.
In ddlib, the market is a virtual space where different stores exist. The market initialises itself at the first call, however, it's a good practice to do it manually calling adnd::market::market::init()
.
The following example creates two stores inside the market virtual space and restocks them:
using namespace adnd;
std::string storeName = "Friendly Arm's Store";
uint32_t mktIdFAS = market::market::get_instance().create_store(storeName, adnd::market::store::budget_model::unlimited, true, true);
market::market::get_instance()[mktIdFAS].add<equipment::weapon>(defs::equipment::item::quarterstaff);
market::market::get_instance()[mktIdFAS].add<equipment::weapon>(defs::equipment::item::knife);
market::market::get_instance()[mktIdFAS].add<equipment::armour>(defs::equipment::item::full_plate);
market::market::get_instance()[mktIdFAS].add<equipment::armour>(defs::equipment::item::hide);
market::market::get_instance()[mktIdFAS].add<equipment::armour>(defs::equipment::item::plate_mail);
market::market::get_instance()[mktIdFAS].add<equipment::armour>(defs::equipment::item::chain_mail);
market::market::get_instance()[mktIdFAS].add<equipment::armour>(defs::equipment::item::robe_common);
storeName = "Winthrop's Store";
uint32_t mktIdWS = market::market::get_instance().create_store(storeName, market::store::budget_model::random, true, true);
market::market::get_instance()[mktIdWS].add<equipment::weapon>(defs::equipment::item::battle_axe);
market::market::get_instance()[mktIdWS].add<equipment::weapon>(defs::equipment::item::long_sword);
market::market::get_instance()[mktIdWS].add<equipment::weapon>(defs::equipment::item::broad_sword);
market::market::get_instance()[mktIdWS].add<equipment::weapon>(defs::equipment::item::composite_long_bow);
market::market::get_instance()[mktIdWS].add<equipment::weapon>(defs::equipment::item::morning_star);
market::market::get_instance()[mktIdWS].add<equipment::armour>(defs::equipment::item::full_plate);
market::market::get_instance()[mktIdWS].add<equipment::armour>(defs::equipment::item::hide);
market::market::get_instance()[mktIdWS].add<equipment::armour>(defs::equipment::item::plate_mail);
market::market::get_instance()[mktIdWS].add<equipment::armour>(defs::equipment::item::chain_mail);
Each store can have a different budget model, that is, the money available. When unlimited
, the store can always buy stuff from the characters. When random
is specified, a random amount of each coins is assigned automatically.
A third value assigned
lets the library user to assign the amount of money available.
The last two parameters allow the store to apply a spread to the price. In other words, the prices are flexible and the store tends to buy goods for a cheaper price and sell them for a higher price. In the end, they sell stuff to make a living...
When an item is added to a store, a unique object is created from the item_pool
and assigned to the store.
Once a store is set up, transactions can take place. Let's take a character who wants to buy stuff from the Winthrop's Store created above:
using namespace adnd;
character chr("Gandalf", defs::character_class::mage, defs::character_race::elf, defs::character_sex::male, defs::moral_alignment::neutral_good);
defs::equipment::item itemID = defs::equipment::item::robe_common;
auto itemTempl = adnd::templates::metadata::get_instance().get_item_template(itemID);
std::cout << "\n\n" << chr.get_name() << " wants to buy a " << itemTempl->description() << " from " << storeName << "\n";
// First, retrieve the price of the item
auto price = market::market::get_instance()[mktIdWS].get_price(itemID, market::store::buy_sell_side::buy);
// Then the character should check if they can afford paying for it. Otherwise in case of errors (i.e. not enough money left), an exception is thrown.
if (!chr.money().check_availability(price))
// Do something...
// This removes the item from the store, pays the due amount and returns the item
auto it = market::market::get_instance()[mktId].buy(itemID);
// The amount is taken from the character's bag
chr.money().subtract(price);
// And finally the item is added to the inventory
chr.inventory_bag() += it;
Advanced features
Besides character creation, Ddlib enables to user to replicate other AD&D features like creating monsters and doing fine library tuning. This kind of features are considered advanced as they don't focus on the characters abilities, but they concern the world around them. Advanced features include:
- Monsters creation;
- Library configuration;
- Optional rules settings.
The monsters' den
Perhaps the most interesting feature in a role game library is the capability to create and handle monsters. They are a fundaental element in a fantasy world and part of the adventures the characters live. Monsters can be spawned through the monsters_den
, which is the place where monsters come from. Once spawned, an instance is returned and controlled by the library user.
In the example below, a Ghost is created. Then Father Jon, 12th level cleric of Lathander, tries to turn him.
using namespace adnd;
defs::monsters::monster monsterId = defs::monsters::monster::ghost;
std::string monsterName = "The sad Spectre of Angus Roy";
monster::monster* m = monster::monsters_den::get_instance().spawn(monsterId, monsterName);
character chr("Father Jon", defs::character_class::cleric, defs::character_race::human, defs::character_sex::male, defs::moral_alignment::lawful_neutral, true, defs::deities::deity::lathander);
chr.gain_level(11);
auto res = chr.turn_undead(m->turnable_as());
std::cout << chr.get_name() << " tries to turn " << m->name() << ": " << res << "\n";
Library configuration
The configuration
namespace allows for some library tuning and set library properties like the database source or error handling behavoiur.
There are some properties that can be changed before library initialisation:
Parameter | Type | Possible values | Default value |
---|---|---|---|
Database source | Enumerator | file/memory | file |
Definition script path | String | any | |
Initialisation script path | String | any | |
Database path | String | any | |
Error handling style | Enumerator | internal/exception | exception |
As introduced before, Ddlib creates a SQLite database to store tables and dataviewes that are part of the AD&D ruleset. This information is then retrieved and used across the library. To accomplish this, the library requires the path to the SQL scripts used to create the database and the database destination file. The Database source parameter selects the physical database location. It may be a physical file stored in some directory or an in-memory database. The first choice allows the user to freely peek at data, but performance will suffer. An in-memory database is much faster and require less configuration, but may result to be more opaque. The Definition script path is the absolute path to the SQL script that creates the database, and Initialisation script path contains the SQL statements that populate the database. Both the scripts are provided alongside with the library. The Database path is the absolute path to the destination database file. This configuration applies only for File database source.
Error handling style allows the user to select the behavoiur the library will show in case of errors. If issues occur when the library metadata layer accesses the underlying data layer, exceptions are thrown. Exceptions can be tedious to handle with, so the user can modify this behaviour setting it to Internal. The library then will fail silently and the last error message will be stored for use.
Although the final user is not required to be aware of such details, the core dataset is exposed to allow advanced users to write their own extensions, to export such data and make use of them.
Configuring the library is simple as that:
using namespace adnd;
configuration::get_instance().get_database_path() = "path to the DB";
configuration::get_instance().get_definition_script_path() = "path to the DEF script";
configuration::get_instance().get_init_script_path() = "path to the INIT script-";
configuration::get_instance().get_error_handler_style() = adnd::configuration::error_handler_style::internal;
configuration::get_instance().get_database_source() = adnd::configuration::database_source::file;
configuration::get_instance().init();
Optional rules settings
Advanced Dungeons & Dragons allows the DM to choose whether some optional rules apply or not. Ddlib implements some of them but the application can be toggled using the configuration
namespace.
The list of optional rules and customisable behavoiurs (and default values) is:
- Encumbrance (default true);
- Weapon proficiency (default true);
- Exception on turn undead (default false);
- Maximum number of spells per level (default true);
- Exceeding maximum level limit for demihumans (default true);
- Always learn new spells (default false).
The Encumbrance option enables the library to account for the weight a character can carry. The weight allowance dependes on the character's strength score and the movement factor depends on the character's race. According to the current amount ot items the character is carrying, the movement factor can diminuish, as well as penalties are applied in combat. When this option is disabled, there's no limit on what a character can carry.
The Weapon proficiency rule enables characters to get proficient/specialised in weapons' usage. When activated, a character must specified his degree of competence in the weapons of choice. A limited number of slots are available depending on the class. When characters use weapons they aren't proficent in, a penalty is applied. Specialisation can grant further bonuses, but only mono class fighters can become specialists.
Exception on turn undead cannot be considered an optional rule in AD&D terms, but it comes to hand when the user wants to modify the effect of the priests ability to turn undead. For instance, if a cleric tries to turn a Kobold as if it were undead, an exception is thrown. disabling this option, the function will respond with a no effect result.
The Maximum number of spells per level limits the spellcasters' capability to learn new spells. The limit is given by the character's intelligence score.
Exceeding maximum level limit for demihumans allows for demihumans with high prime requisites to reach higher levels per class.
Always learn new spells avoids the check for the probability to learn new spells when spell casters add new spells to their spell book.