Расы имеют много отличительных физических характеристик. Но помимо внешности, у разных рас есть и другие отличительные характеристики, все я решил построить с помощью шаблона Строителя. Я считаю, что отдельные классы строителей очень полезны, когда у многих рас есть несколько подрас. Как только расы и подрасы определены в приведенном ниже коде, определяются сопутствующие классы строителей, которые затем используются в конструкторах HumanoidWithRaceType
с.
#include <iostream>
#include <string>
#include <unordered_map>
#include <fstream>
const int NOT_FOUND = -1;
enum RaceType {HUMAN, ELF, DWARF, HALFLING};
enum HumanSubRaceType {CALASHITE, CHONDATHAN, DAMARAN}; // several more
enum ElfSubRaceType {HIGH, WOOD, DROW};
enum HighElfSubRaceType {GREY, VALLEY, SILVANESTI, GREYHAWK_HIGH, QUALINESTI, MOON, SUN};
enum WoodElfSubRaceType {WILD, KAGONESTI, GREYHAWK_WOOD, FORGOTTEN_REALMS_WOOD, FOREST};
class Race;
class HumanoidWithRaceType;
template <RaceType> class RaceClass;
using Human = RaceClass<HUMAN>;
using Elf = RaceClass<ELF>;
template <HumanSubRaceType> class HumanSubRace;
using Calashite = HumanSubRace<CALASHITE>;
using Chondathan = HumanSubRace<CHONDATHAN>;
using Damaran = HumanSubRace<DAMARAN>;
// etc... for the other Human subraces
template <ElfSubRaceType> class ElfSubRace;
using HighElf = ElfSubRace<HIGH>;
using WoodElf = ElfSubRace<WOOD>;
using Drow = ElfSubRace<DROW>;
template <HighElfSubRaceType> class HighElfSubRace;
using GreyElf = HighElfSubRace<GREY>;
using ValleyElf = HighElfSubRace<VALLEY>;
using SilvanestiElf = HighElfSubRace<SILVANESTI>;
using GreyhawkHighElf = HighElfSubRace<GREYHAWK_HIGH>;
using QualinestiElf = HighElfSubRace<QUALINESTI>;
using MoonElf = HighElfSubRace<MOON>;
using SunElf = HighElfSubRace<SUN>;
template <WoodElfSubRaceType> class WoodElfSubRace;
using WildElf = WoodElfSubRace<WILD>;
using KagonestiElf = WoodElfSubRace<KAGONESTI>;
using GreyhawkWoodElf = WoodElfSubRace<GREYHAWK_WOOD>;
using ForgottenRealmsWoodElf = WoodElfSubRace<FORGOTTEN_REALMS_WOOD>;
using ForestElf = WoodElfSubRace<FOREST>;
class LivingBeing {
int intelligence = 9, wisdom = 9, dexterity = 9, height = 180;
int hitPoints = 10;
public:
void changeIntelligenceBy (int change) { intelligence += change; }
void changeWisdomBy (int change) { wisdom += change; }
void changeDexterityBy (int change) { dexterity += change; }
void changeHeightBy (int change) { height += change; }
void setHitPoints (int hp) { hitPoints = hp; }
virtual bool notAffectedBySleepSpell() const { return false; } // Overriden by UndeadMonster and HumanoidWithRaceType with race == any kind of Elf.
virtual bool notAffectedByCharmSpell() const { return false; } // Overriden by UndeadMonster and HumanoidWithRaceType with race == any kind of Elf.
virtual bool hasAdvantageVsCharm() const { return false; } // Overriden by HumanoidWithRaceType with race == any kind of Elf.
virtual void print (std::ostream& os = std::cout) const {
os << "Intelligence: " << intelligence << '\n';
os << "Wisdom: " << wisdom << '\n';
os << "Dexterity: " << dexterity << '\n';
os << "Height: " << height << '\n';
os << "Hit Points: " << hitPoints << '\n';
}
virtual void save (std::ostream& os) const { os << intelligence << ' ' << wisdom << ' ' << dexterity << ' ' << height << ' ' << hitPoints << '\n'; }
virtual void load (std::istream& is) { is >> std::skipws >> intelligence >> wisdom >> dexterity >> height >> hitPoints; }
};
struct Weapon {
virtual std::string getName() const = 0;
};
struct LongSword : Weapon { std::string getName() const override { return "long sword"; } };
struct ShortSword : Weapon { std::string getName() const override { return "short sword"; } };
struct Armor {
virtual std::string getName() const = 0;
};
struct SmallChainMail : Armor { std::string getName() const override { return "small chainmail"; } };
struct LargeChainMail : Armor { std::string getName() const override { return "large chainmail"; } };
// DemiHuman is a being that has physical features of a human being and can use weapons and/or wear armor, which not only includes Humanoids (like humans-sized Goblins, Orcs, etc...) but also certain undead, giants, celestials, even deities, all of which use weapons and wear armor.
class DemiHuman : public LivingBeing {
Weapon* weaponInHand = nullptr;
Armor* armorWorn = nullptr;
int proficiencyWithMissiles = 5;
public:
Weapon* getWeapon() const { return weaponInHand; }
void setWeapon (Weapon* weapon) { weaponInHand = weapon; }
void setArmor (Armor* armor) { armorWorn = armor; }
void changeProficiencyWithMissilesBy (int change) { proficiencyWithMissiles += change; }
virtual void save (std::ostream& os) const override {
LivingBeing::save(os);
os << proficiencyWithMissiles << '\n'; // Skipping weaponInHand and armorWorn for simplicity.
if (weaponInHand) {
os << std::boolalpha << true << '\n';
os << weaponInHand->getName() << '\n';
}
else
os << std::boolalpha << false << '\n';
if (armorWorn) {
os << std::boolalpha << true << '\n';
os << armorWorn->getName() << '\n';
}
else
os << std::boolalpha << false << '\n';
}
virtual void load (std::istream& is) override {
LivingBeing::load(is);
is >> proficiencyWithMissiles; // Skipping weaponInHand and armorWorn for simplicity.
bool b;
std::string itemName;
is >> std::boolalpha >> b;
if (b) {
while (std::getline(is, itemName) && itemName.empty());
if (itemName == "short sword") // Factory std::map<std::string, std::function<Weapon*()>> construction and look-up omitted for brevity.
weaponInHand = new ShortSword;
}
is >> std::boolalpha >> b;
if (b) {
while (std::getline(is, itemName) && itemName.empty());
if (itemName == "small chainmail") // Factory std::map<std::string, std::function<Armor*()>> construction and look-up omitted for brevity.
armorWorn = new SmallChainMail;
}
}
virtual void print (std::ostream& os = std::cout) const override {
LivingBeing::print(os);
if (weaponInHand) os << "Holding " << weaponInHand->getName() << " as a weapon.\n";
else os << "No weapon in hand.\n";
if (armorWorn) os << "Wearing " << armorWorn->getName() << " for armor.\n";
else os << "No armor worn.\n";
os << "Proficiency With missiles = " << proficiencyWithMissiles << '\n';
}
};
class Humanoid : public DemiHuman { }; // Includes Goblins, Orcs, Kobolds, etc... (all human-sized)
class RaceDatabase {
static std::unordered_map<std::string, Race*> map; // Used for saving and loading DemiHumanWithRaceType instances from strings.
public:
static Race* getRaceFromString (const std::string& raceName) { return map.at(raceName); }
static void insertIntoMap (const std::string& tag, Race* race) { map.emplace(tag, race); std::cout << "{" << tag << ", " << race << "} inserted in RaceDatabase::map.\n"; }
private:
RaceDatabase() = default; RaceDatabase (const RaceDatabase&); const RaceDatabase& operator = (const RaceDatabase&); ~RaceDatabase() = default;
};
std::unordered_map<std::string, Race*> RaceDatabase::map;
class Race {
protected:
struct Exemplar {};
public:
virtual void build (HumanoidWithRaceType&) = 0;
virtual std::string tag() const = 0;
virtual int getWalkingSpeed() const = 0;
virtual bool isElf() const { return false; }
virtual int getDarkVisionDistance() const { return NOT_FOUND; }
virtual bool notAffectedBySleepSpell() const { return false; } // overridden by Elf
virtual bool hasAdvantageVsCharm() const { return false; } // overridden by Elf
// virtual void increaseNumCantripsByOne (SpellCasterCharacterClass*) const { } // overridden by HighElf only
};
template <RaceType>
class RaceCRTP : public Race {
virtual void build (HumanoidWithRaceType&) override;
};
// Human and its subraces
template <>
class RaceClass<HUMAN> : public RaceCRTP<HUMAN> { // Human
static const Human prototype;
public:
static Human* getInstance() {static Human* instance = new Human; return instance;} // Singleton, because all beings of this (or any other) race shall simply share the same Human* pointer (Flyweight Pattern).
protected: // The default constructor, copy constructor, assignment operator cannot be accessed in order to force the sharing of the same Human* pointer by all beings of race Human.
RaceClass() = default; RaceClass (const RaceClass&); const RaceClass& operator = (const RaceClass&); ~RaceClass() = default;
private:
RaceClass (Exemplar) { RaceDatabase::insertIntoMap(className(), getInstance()); } // Cannot implement this in the base class Race because getInstance() is being passed instead of 'this' (and virtual functions should not be called in a base constructor).
virtual std::string tag() const override { return className(); }
static std::string className() { return "Human"; }
virtual int getWalkingSpeed() const override { return 30; }
};
const Human Human::prototype(Exemplar{});
template <HumanSubRaceType>
class HumanCRTP : public Human {
// Whatever is needed using CRTP, other than build(HumanoidWithRaceType&).
};
template <HumanSubRaceType H>
class HumanCRTPWithBuilder : public HumanCRTP<H> {
virtual void build (HumanoidWithRaceType&) override; // No other methods defined here.
};
template <>
class HumanSubRace<CALASHITE> : public HumanCRTPWithBuilder<CALASHITE> { // Calashite
static const Calashite prototype;
public:
static Calashite* getInstance() {static Calashite* instance = new Calashite; return instance;} // Singleton
private:
HumanSubRace (Exemplar) { RaceDatabase::insertIntoMap(className(), getInstance()); } // Cannot implement this in the base class Race because getInstance() is being passed instead of 'this' (and virtual functions should not be called in a base constructor).
HumanSubRace() = default; HumanSubRace (const HumanSubRace&); const HumanSubRace& operator = (const HumanSubRace&); ~HumanSubRace() = default;
virtual std::string tag() const override { return className(); }
static std::string className() { return "Calashite Human"; }
};
const Calashite Calashite::prototype(Exemplar{});
// HumanBuilder<CHONDATHAN> is not to be defined because it won't do anything different from RacialClassBuilder<HUMAN>, i.e. being of race Chondathan Human is no different from Human from a builder point of view,
// so HumanSubRace<CHONDATHAN> will just derive from HumanCRTP<CHONDATHAN> instead of from HumanCRTPWithBuilder<CHONDATHAN>.
template <>
class HumanSubRace<CHONDATHAN> : public HumanCRTP<CHONDATHAN> { // Chondathan
static const Chondathan prototype;
public:
static Chondathan* getInstance() {static Chondathan* instance = new Chondathan; return instance;} // Singleton
private:
HumanSubRace (Exemplar) { RaceDatabase::insertIntoMap(className(), getInstance()); }
HumanSubRace() = default; HumanSubRace (const HumanSubRace&); const HumanSubRace& operator = (const HumanSubRace&); ~HumanSubRace() = default;
virtual std::string tag() const override { return className(); }
static std::string className() { return "Chondathan Human"; }
};
const Chondathan Chondathan::prototype(Exemplar{});
template <>
class HumanSubRace<DAMARAN> : public HumanCRTP<DAMARAN> { // Damaran (nothing special about it either from a builder point of view, so also derive from HumanCRTP<DAMARAN> instead of from HumanCRTPWithBuilder<DAMARAN>).
static const Damaran prototype;
public:
static Damaran* getInstance() {static Damaran* instance = new Damaran; return instance;} // Singleton
private:
HumanSubRace (Exemplar) {RaceDatabase::insertIntoMap (className(), getInstance());}
HumanSubRace() = default; HumanSubRace (const HumanSubRace&); const HumanSubRace& operator = (const HumanSubRace&); ~HumanSubRace() = default;
virtual std::string tag() const override {return className();}
static std::string className() {return "Damaran Human";}
};
const Damaran Damaran::prototype(Exemplar{});
// and several more Human subraces
// Elf and its subraces
template <>
class RaceClass<ELF> : public RaceCRTP<ELF> { // Elf
static const Elf prototype;
public:
static Elf* getInstance() {static Elf* instance = new Elf; return instance;} // Singleton
protected:
RaceClass() = default; RaceClass (const RaceClass&); const RaceClass& operator = (const RaceClass&); ~RaceClass() = default;
private:
RaceClass (Exemplar) { RaceDatabase::insertIntoMap(className(), getInstance()); }
virtual bool isElf() const override { return true; }
virtual std::string tag() const override { return className(); }
static std::string className() { return "Elf"; }
virtual int getWalkingSpeed() const override { return 32; }
virtual bool notAffectedBySleepSpell() const override { return true; }
virtual bool hasAdvantageVsCharm() const override { return true; }
};
const Elf Elf::prototype(Exemplar{});
template <ElfSubRaceType>
class ElfCRTP : public Elf {
// Whatever is needed using CRTP, other than build(HumanoidWithRaceType&).
};
template <ElfSubRaceType E>
class ElfCRTPWithBuilder : public ElfCRTP<E> {
virtual void build (HumanoidWithRaceType&) override; // No other methods defined here.
};
template <>
class ElfSubRace<HIGH> : public ElfCRTPWithBuilder<HIGH> { // HighElf
static const HighElf prototype;
public:
static HighElf* getInstance() { static HighElf* instance = new HighElf; return instance; } // Singleton
protected: // ElfSubRace<HIGH> will itself have subraces deriving from it.
ElfSubRace() = default; ElfSubRace (const ElfSubRace&); const ElfSubRace& operator = (const ElfSubRace&); ~ElfSubRace() = default;
private:
ElfSubRace (Exemplar) { RaceDatabase::insertIntoMap(className(), getInstance()); }
virtual std::string tag() const override { return className(); }
static std::string className() { return "High Elf"; }
};
const HighElf HighElf::prototype(Exemplar{});
template <HighElfSubRaceType>
class HighElfCRTP : public HighElf {
// Whatever is needed using CRTP, other than build(HumanoidWithRaceType&).
};
template <HighElfSubRaceType H>
class HighElfCRTPWithBuilder : public HighElfCRTP<H> {
virtual void build (HumanoidWithRaceType&) override; // No other methods defined here.
};
template <>
class HighElfSubRace<GREY> : public HighElfCRTPWithBuilder<GREY> { // GreyElf
static const GreyElf prototype;
public:
static GreyElf* getInstance() { static GreyElf* instance = new GreyElf; return instance; } // Singleton
private:
HighElfSubRace (Exemplar) { RaceDatabase::insertIntoMap(className(), getInstance()); }
HighElfSubRace() = default; HighElfSubRace (const HighElfSubRace&); const HighElfSubRace& operator = (const HighElfSubRace&); ~HighElfSubRace() = default;
virtual std::string tag() const override { return className(); }
static std::string className() { return "Grey Elf"; }
};
const GreyElf GreyElf::prototype(Exemplar{});
template <>
class HighElfSubRace<VALLEY> : public HighElfCRTPWithBuilder<VALLEY> { // ValleyElf
static const ValleyElf prototype;
public:
static ValleyElf* getInstance() { static ValleyElf* instance = new ValleyElf; return instance; } // Singleton
private:
HighElfSubRace (Exemplar) { RaceDatabase::insertIntoMap(className(), getInstance()); }
HighElfSubRace() = default; HighElfSubRace (const HighElfSubRace&); const HighElfSubRace& operator = (const HighElfSubRace&); ~HighElfSubRace() = default;
virtual std::string tag() const override { return className(); }
static std::string className() { return "Valley Elf"; }
};
const ValleyElf ValleyElf::prototype(Exemplar{});
// ***** The all-important derived type of Humanoid that has races.
class HumanoidWithRaceType : public Humanoid { // This class placed between CharacterClass and Humanoid to accomodate things like Goblin races, Orc races, Kobold races, etc...
Race* race; // Type Object Pattern (better choice than using multiple inheritance from Race).
public:
HumanoidWithRaceType (Race* race) { setRace(race); }
HumanoidWithRaceType() = default;
Race* getRace() const { return race; }
virtual void setRace (Race* thisRace) { race = thisRace; race->build(*this); } // ***** 'race->build(*this);' is the key line that initializes all the stats of 'this' according to what 'race' is. No need for the Visitor Pattern, and the 'build(HumanoidWithRaceType&)' overrides do actually belong in the Race subtypes (just one override for each concrete derived class of Race), and hence the Open-Closed Principle is not violated.
int getDarkVisionDistance() const { return race->getDarkVisionDistance(); }
virtual void save (std::ostream& os) const override {
DemiHuman::save(os);
os << race->tag() << '\n';
}
virtual void load (std::istream& is) override {
DemiHuman::load(is);
std::string raceName;
while (std::getline(is, raceName) && raceName.empty());
race = RaceDatabase::getRaceFromString(raceName); // RaceDatabase now being used. No need to call 'setRace', namely 'race->build(*this);' (which is only used for Factory-type production of HumanoidWithRaceType's) since all the special attributes have been saved and hence will be loaded.
}
virtual void print (std::ostream& os = std::cout) const override {
DemiHuman::print(os);
os << "Race: " << race->tag() << '\n';
}
private:
virtual bool notAffectedBySleepSpell() const override { return race->notAffectedBySleepSpell(); } // Elves resist with 100% success.
virtual bool hasAdvantageVsCharm() const override { return race->hasAdvantageVsCharm(); } // Elves have advantage saving throw vs Charm spells.
};
// Abstract builder classes
class HumanoidWithRaceTypeBuilder {
HumanoidWithRaceType& being; // Reference is used here because a HumanoidWithRaceType already built by some other builder (e.g. PrimaryStatsBuilder) is passed into here, instead of being instantiated here as a pointer (as the classic builder pattern follows).
public:
HumanoidWithRaceTypeBuilder (HumanoidWithRaceType& d) : being(d) {}
protected:
HumanoidWithRaceType& getBeing() const { return being; }
};
class RacialClassBuilderBase : protected HumanoidWithRaceTypeBuilder {
friend class RacialClassDirector;
public:
using HumanoidWithRaceTypeBuilder::HumanoidWithRaceTypeBuilder;
protected:
virtual void adjustAbilityScores() const = 0;
virtual void adjustWeaponProficiencies() const = 0;
virtual void adjustPhysicalAppearance() const = 0;
};
// The director class in the Builder Pattern.
class RacialClassDirector {
private:
const RacialClassBuilderBase& racialClassBuilderBase; // const, as setRacialClassBuilderBase(RacialClassBuilderBase*) is not needed I don't think, since this class is apparently only used the 'build' overides in Race subtypes.
public:
RacialClassDirector (const RacialClassBuilderBase& r) : racialClassBuilderBase(r) {}
void construct() const { // The key Facade Pattern used in the actual building process in the Builder Pattern.
racialClassBuilderBase.adjustAbilityScores();
racialClassBuilderBase.adjustWeaponProficiencies();
racialClassBuilderBase.adjustPhysicalAppearance();
// racialClassBuilderBase.adjustLifespan(); // And common diseases, etc...
// etc...
}
};
// The concrete race builder classes
template <RaceType> class RacialClassBuilder;
template <>
class RacialClassBuilder<HUMAN> : public RacialClassBuilderBase {
public:
using RacialClassBuilderBase::RacialClassBuilderBase;
protected:
virtual void adjustAbilityScores() const override { } // Do whatever applies for all humans.
virtual void adjustWeaponProficiencies() const override { } // Do whatever applies for all humans.
virtual void adjustPhysicalAppearance() const override { } // Do whatever applies for all humans.
};
template <HumanSubRaceType> class HumanBuilder;
template <>
class HumanBuilder<CALASHITE>: public RacialClassBuilder<HUMAN> {
public:
using RacialClassBuilder<HUMAN>::RacialClassBuilder;
protected:
virtual void adjustAbilityScores() const override {
RacialClassBuilder<HUMAN>::adjustAbilityScores();
getBeing().changeIntelligenceBy(2);
}
virtual void adjustWeaponProficiencies() const override {
RacialClassBuilder<HUMAN>::adjustWeaponProficiencies();
getBeing().changeProficiencyWithMissilesBy(1);
}
virtual void adjustPhysicalAppearance() const override {
RacialClassBuilder<HUMAN>::adjustPhysicalAppearance();
getBeing().changeHeightBy(7);
}
};
template <>
class RacialClassBuilder<ELF> : public RacialClassBuilderBase {
public:
using RacialClassBuilderBase::RacialClassBuilderBase;
protected:
virtual void adjustAbilityScores() const override { getBeing().changeDexterityBy(2); }
virtual void adjustWeaponProficiencies() const override { } // Do whatever applies for all elves.
virtual void adjustPhysicalAppearance() const override { getBeing().changeHeightBy(-30); } // Elves are much shorter than humans.
};
template <ElfSubRaceType> class ElfBuilder;
template <>
class ElfBuilder<HIGH>: public RacialClassBuilder<ELF> {
public:
using RacialClassBuilder<ELF>::RacialClassBuilder;
protected:
virtual void adjustAbilityScores() const override {
RacialClassBuilder<ELF>::adjustAbilityScores();
getBeing().changeIntelligenceBy(1);
// SpellCasterCharacterClass* spellCaster = dynamic_cast<SpellCasterCharacterClass*>(&getBeing());
// if (spellCaster)
// spellCaster->increaseNumCantripsByOne();
}
virtual void adjustWeaponProficiencies() const override {
RacialClassBuilder<ELF>::adjustWeaponProficiencies();
getBeing().changeProficiencyWithMissilesBy(2);
}
};
template <HighElfSubRaceType> class HighElfBuilder;
template <>
class HighElfBuilder<GREY>: public ElfBuilder<HIGH> { // Haughty, reclusive, believing themselves to be superior to non-elves and even other elves.
public:
using ElfBuilder<HIGH>::ElfBuilder;
protected:
virtual void adjustAbilityScores() const override {
ElfBuilder<HIGH>::adjustAbilityScores();
getBeing().changeWisdomBy(1);
}
virtual void adjustWeaponProficiencies() const override {
ElfBuilder<HIGH>::adjustWeaponProficiencies();
getBeing().changeProficiencyWithMissilesBy(1); // Grey Elves are even better with missiles than the already missile-adept Elf.
}
virtual void adjustPhysicalAppearance() const override {
ElfBuilder<HIGH>::adjustPhysicalAppearance();
getBeing().changeHeightBy(-10); // Grey Elves are 10 cm shorter than the already short Elf.
}
};
template <>
class HighElfBuilder<VALLEY>: public ElfBuilder<HIGH> {
public:
using ElfBuilder<HIGH>::ElfBuilder;
protected:
virtual void adjustAbilityScores() const override {
ElfBuilder<HIGH>::adjustAbilityScores();
// Add whatever unique to ValleyElf.
}
virtual void adjustWeaponProficiencies() const override {
ElfBuilder<HIGH>::adjustWeaponProficiencies();
// Add whatever unique to ValleyElf.
}
virtual void adjustPhysicalAppearance() const override {
ElfBuilder<HIGH>::adjustPhysicalAppearance();
// Add whatever unique to ValleyElf.
}
};
// Similarly for class HighElfBuilder<SILVANESTI>: public ElfBuilder<HIGH>, etc...
// The all-important build(HumanoidWithRaceType&) overrides of the Race class.
template <RaceType R>
void RaceCRTP<R>::build (HumanoidWithRaceType& h) {
const RacialClassBuilder<R> builder(h);
const RacialClassDirector director(builder);
director.construct();
}
template <HumanSubRaceType H>
void HumanCRTPWithBuilder<H>::build (HumanoidWithRaceType& h) {
const HumanBuilder<H> humanBuilder(h);
const RacialClassDirector director(humanBuilder);
director.construct();
}
template <ElfSubRaceType E>
void ElfCRTPWithBuilder<E>::build (HumanoidWithRaceType& h) {
const ElfBuilder<E> elfBuilder(h);
const RacialClassDirector director(elfBuilder);
director.construct();
}
template <HighElfSubRaceType H>
void HighElfCRTPWithBuilder<H>::build (HumanoidWithRaceType& h) {
const HighElfBuilder<H> highElfBuilder(h);
const RacialClassDirector director(highElfBuilder);
director.construct();
}
// Some totally different builder classes not related to race, but to be used sequentially with race builders. Defined briefly only.
class PrimaryStatsBuilderBase : protected HumanoidWithRaceTypeBuilder { // Builds based on level of the character class.
public:
using HumanoidWithRaceTypeBuilder::HumanoidWithRaceTypeBuilder;
void construct() const { // Doing without the Director class this time, since here construct() also needs to be defined only once.
establishHitPointsAndAbilityScores();
establishProficiencies();
establishSpells();
// etc...
}
private:
virtual void establishHitPointsAndAbilityScores() const = 0;
virtual void establishProficiencies() const = 0;
virtual void establishSpells() const = 0;
// etc...
};
class EquipmentBuilderBase : protected HumanoidWithRaceTypeBuilder { // Builds based on the type of character class (and to some extent the race already established).
public:
using HumanoidWithRaceTypeBuilder::HumanoidWithRaceTypeBuilder;
void construct() const { // Doing without the Director class this time, since here construct() also needs to be defined only once.
establishWeapons();
establishArmor();
// etc...
}
private:
virtual void establishWeapons() const = 0;
virtual void establishArmor() const = 0;
// etc...
};
enum CharacteClassType { FIGHTER, WIZARD, CLERIC }; // etc...
class CharacterClass : public HumanoidWithRaceType {
int level;
public:
using HumanoidWithRaceType::HumanoidWithRaceType;
void setLevel (int n) { level = n; }
virtual void print (std::ostream& os = std::cout) const override {
HumanoidWithRaceType::print(os);
os << "Level: " << level << '\n';
}
virtual void save (std::ostream& os) const override {
HumanoidWithRaceType::save(os);
os << level << '\n';
}
virtual void load (std::istream& is) override {
HumanoidWithRaceType::load(is);
is >> level;
}
};
template <CharacteClassType> class PrimaryStatsBuilder;
template <>
class PrimaryStatsBuilder<FIGHTER> : public PrimaryStatsBuilderBase {
public:
using PrimaryStatsBuilderBase::PrimaryStatsBuilderBase;
private:
virtual void establishHitPointsAndAbilityScores() const override {
const int level = 5;
dynamic_cast<CharacterClass&>(getBeing()).setLevel(level);
getBeing().setHitPoints(level*10);
// etc...
}
virtual void establishProficiencies() const override { std::cout << "\nProficiency added: can bend bars.\n"; }
virtual void establishSpells() const override { std::cout << "\nNo spells allowed for Fighters.\n"; }
};
// Similar specializations for PrimaryStatsBuilder<WIZARD>, PrimaryStatsBuilder<CLERIC>, etc...
template <CharacteClassType> class EquipmentBuilder;
template <>
class EquipmentBuilder<FIGHTER> : public EquipmentBuilderBase {
public:
using EquipmentBuilderBase::EquipmentBuilderBase;
private:
virtual void establishWeapons() const override { // These overrides are related to race due to racial sizes.
if (getBeing().getRace()->isElf())
getBeing().setWeapon(new ShortSword);
else
getBeing().setWeapon(new LongSword);
}
virtual void establishArmor() const override {
if (getBeing().getRace()->isElf())
getBeing().setArmor(new SmallChainMail);
else
getBeing().setArmor(new LargeChainMail);
}
};
// Similar specializations for EquipmentBuilder<WIZARD>, EquipmentBuilder<CLERIC>, etc...
template <CharacteClassType C>
class CharacterClassCRTP : public CharacterClass {
protected:
using CharacterClass::CharacterClass;
void intializeWithBuilders() {
const PrimaryStatsBuilder<C> primaryStatsBuilder(*this);
primaryStatsBuilder.construct();
const EquipmentBuilder<C> equipmentBuilder(*this); // This can be used because race has already been established.
equipmentBuilder.construct();
}
};
template <CharacteClassType> class Character;
template <>
class Character<FIGHTER> : public CharacterClassCRTP<FIGHTER> { // Fighter
public:
Character (Race* race) : CharacterClassCRTP<FIGHTER>(race) {
intializeWithBuilders(); // ***** All three separate builder classes are used in this constructor.
}
Character() = default;
};
template <> class Character<WIZARD> : public CharacterClassCRTP<WIZARD> { }; // Wizard (is similar to Fighter)
template <> class Character<CLERIC> : public CharacterClassCRTP<CLERIC> { }; // Cleric (is similar to Fighter)
using Fighter = Character<FIGHTER>;
int main() {
HumanoidWithRaceType humanoid1;
humanoid1.setRace(Human::getInstance());
std::cout << "\nAfter being built with assigned race Human:\n";
humanoid1.print();
humanoid1.setRace(HumanSubRace<CHONDATHAN>::getInstance());
std::cout << "\nAfter being built with assigned race Chondathan Human (sub-race of Human, but nothing special about it):\n";;
humanoid1.print();
HumanoidWithRaceType humanoid2;
humanoid2.setRace(HumanSubRace<CALASHITE>::getInstance());
std::cout << "\nAfter being built with assigned race Calashite Human (sub-race of Human):\n";
humanoid2.print();
HumanoidWithRaceType humanoid3;
humanoid3.setRace(Elf::getInstance());
std::cout << "\nAfter being built with assigned race Elf:\n";
humanoid3.print();
HumanoidWithRaceType humanoid4;
humanoid4.setRace(ElfSubRace<HIGH>::getInstance());
std::cout << "\nAfter being built with assigned race High Elf (sub-race of Elf):\n";
humanoid4.print();
std::cout << "\n\nGenerating a Fighter now:\n";
const std::string raceNameGenerated = "Grey Elf"; // From some Factory method.
Fighter fighter(RaceDatabase::getRaceFromString(raceNameGenerated)); // For Factory methods, race must be passed into the Fighter constructor to use EquipmentBuilder due to race requiring to be known first.
std::cout << "\nFighter, after being built with assigned race Grey Elf (sub-race of HighElf):\n";
fighter.print();
const std::string fileName = "SaveFile.txt";
std::ofstream outfile(fileName);
fighter.save(outfile);
std::cout << "\nGrey Elf fighter written to txt file.\n";
outfile.close();
std::ifstream infile(fileName);
Fighter loadedFighter;
loadedFighter.load(infile);
std::cout << "\nGrey Elf fighter constructed from txt file:\n";
loadedFighter.print();
}
Вывод:
{Human, 0x1959d0} inserted in RaceDatabase::map.
{Calashite Human, 0x195a50} inserted in RaceDatabase::map.
{Chondathan Human, 0x195ad0} inserted in RaceDatabase::map.
{Damaran Human, 0x195d40} inserted in RaceDatabase::map.
{Elf, 0x195cd0} inserted in RaceDatabase::map.
{High Elf, 0x195d50} inserted in RaceDatabase::map.
{Grey Elf, 0x195ca0} inserted in RaceDatabase::map.
{Valley Elf, 0x195be0} inserted in RaceDatabase::map.
After being built with assigned race Human:
Intelligence: 9
Wisdom: 9
Dexterity: 9
Height: 180
Hit Points: 10
No weapon in hand.
No armor worn.
Proficiency With missiles = 5
Race: Human
After being built with assigned race Chondathan Human (sub-race of Human, but nothing special about it):
Intelligence: 9
Wisdom: 9
Dexterity: 9
Height: 180
Hit Points: 10
No weapon in hand.
No armor worn.
Proficiency With missiles = 5
Race: Chondathan Human
After being built with assigned race Calashite Human (sub-race of Human):
Intelligence: 11
Wisdom: 9
Dexterity: 9
Height: 187
Hit Points: 10
No weapon in hand.
No armor worn.
Proficiency With missiles = 6
Race: Calashite Human
After being built with assigned race Elf:
Intelligence: 9
Wisdom: 9
Dexterity: 11
Height: 150
Hit Points: 10
No weapon in hand.
No armor worn.
Proficiency With missiles = 5
Race: Elf
After being built with assigned race High Elf (sub-race of Elf):
Intelligence: 10
Wisdom: 9
Dexterity: 11
Height: 150
Hit Points: 10
No weapon in hand.
No armor worn.
Proficiency With missiles = 7
Race: High Elf
Generating a Fighter now:
Proficiency added: can bend bars.
No spells allowed for Fighters.
Fighter, after being built with assigned race Grey Elf (sub-race of HighElf):
Intelligence: 10
Wisdom: 10
Dexterity: 11
Height: 140
Hit Points: 50
Holding short sword as a weapon.
Wearing small chainmail for armor.
Proficiency With missiles = 8
Race: Grey Elf
Level: 5
Grey Elf fighter written to txt file.
Grey Elf fighter constructed from txt file:
Intelligence: 10
Wisdom: 10
Dexterity: 11
Height: 140
Hit Points: 50
Holding short sword as a weapon.
Wearing small chainmail for armor.
Proficiency With missiles = 8
Race: Grey Elf
Level: 5
престоки
1 ответ
Стиль кода
Стиль вашего кода очень компактен, местами его трудно читать, и он непоследовательный. Я настоятельно рекомендую вам не писать более одного оператора в строке.
Обратите внимание, что можно быть немного небрежным при написании кода, однако вам следует использовать встроенные функции форматирования кода вашего редактора кода или использовать внешний инструмент, например Художественный стиль или же ClangФормат иногда.
Утечки памяти
Ты используешь new
в вашем коде, но нет соответствующих delete
операторы, поэтому ваш код теряет память. Вместо использования необработанных указателей рассмотрите возможность использования интеллектуальных указателей, таких как std::unique_ptr
вместе со вспомогательными функциями, такими как std::make_unique()
.
Твой CRTP
классы не являются CRTP
Удивительно повторяющийся шаблон шаблона выглядит следующим образом:
template<typename T>
class CRTP {
…
};
class Foo: CRTP<Foo> {
…
};
Обратите внимание, что параметр шаблона класса CRTP точно такой же, как тип, который наследуется от него. Однако в вашем коде у вас есть этот шаблон:
template<typename T>
class YourCRTP {
…
};
template<typename T>
class Foo: YourCRTP<T> {
…
};
Здесь параметр шаблона вашего класса «CRTP» не Foo<T>
но просто T
так что никакого любопытного повторения не происходит.
Иерархия классов
Довольно странно видеть, что часть свойств персонажа обрабатывается с помощью наследования, а часть — с помощью Race *
переменная-член. Я бы использовал наследование для каждого аспекта персонажа, где вы можете сказать, что это «есть». Итак, дикий эльф — это лесной эльф, эльф — это гуманоид, это получеловек — это живое существо. Вы бы использовали переменную-член указателя, где бы вы сказали «имеет», и вы бы не сказали, что у гуманоида есть дикий эльф.
Вы также должны подумать, уместно ли кодировать все это с помощью системы типов C++. Рассмотрим классы персонажей, такие как боец, волшебник и вор. В AD&D, хотя большинство персонажей будут иметь один и тот же класс персонажей от начала до конца, возможно, что это изменится в течение их жизни. Рассмотрим персонажей двойного класса, которые начинают с одного класса, а затем получают другой класс. Изменения класса также не являются чем-то неслыханным. Таким образом, вместо использования наследования или указателей на базовые классы вы можете просто сохранить тип как переменную-член по значению:
template<typename Race>
class Character: Race {
CharacterClassType character_class;
…
};
Нужен ли здесь шаблон строителя?
Я не понимаю, зачем вам нужен шаблон строителя. Фактически, ваш пример кода никогда не вызывает build()
. Это заставляет меня думать, принцип ЯГНИ применяется здесь.
Ненужное использование классов
В отличие от Java, не все в C++ должно находиться в классе. Рассмотреть возможность RaceDatabase
где все есть static
. Вы можете просто создать бесплатные функции и поместить их в пространство имен:
namespace RaceDatabase {
std::unordered_map<std::string, Race *> map;
Race *fromString(const std::string &raceName) {
return map.at(raceName);
}
…
}
Избегайте загрязнения глобального пространства имен
Помещение чего-то с очень общим именем, например NOT_FOUND
в глобальное пространство имен быстро станет проблемой в более крупных проектах, где есть риск конфликта с другими частями кода. Поэтому всегда полезно по возможности перемещать объекты из глобального пространства имен. Вы можете явно создать namespace
сделать это, но часто вы также можете перемещать типы и статические переменные в связанные class
э. Если это не вариант, то, по крайней мере, убедитесь, что имена менее общие.
NOT_FOUND
по-видимому, не имеет ничего общего с тем, что файлы не были найдены, а скорее указывает на то, что у расы нет инфравидения. Так что это может быть перемещено в class Race
, а также можно было бы переименовать во что-то более подходящее. Вы также можете переместить перечисление для типа расы в class Race
:
class Race {
public:
enum Type {HUMAN, ELF, DWARF, HALFLING};
constexpr int NO_DARK_VISION = -1;
…
virtual int getDarkVisionDistance() const {
return NO_DARK_VISION;
}
…
};
Конечно, это работает для Race
но ты не можешь двигаться HumanSubRaceType
в class HumanSubRace
.
Масштабируемость
Цель наличия иерархии классов состоит в том, чтобы у вас были базовые классы, которые заботятся об общих вещах, и производные классы, которые специализируются поверх этого. Это уменьшает количество кода, который вам нужно поддерживать. Тем не менее, подумайте о своем class Race
: у него есть функция-член isElf()
. Это плохая идея, потому что что, если вы хотите знать, является ли объект, производный от Race
карлик? Или лесной эльф? ты собираешься добавить isDwarf()
и isWoodlandElf()
? Это очень быстро выйдет из-под контроля. Вместо этих isSomething()
функции, вы можете рассмотреть возможность использования [dynamic_cast()][6]
чтобы проверить, является ли указатель на Race
указывает на Elf
или же ForgottenRealsWoodElf
или любой тип, который вы хотите.
А также есть notAffectedBySleepSpell()
. Почему это в Race
начать с? Что делать, если у вас есть LivingBeing
это не HumanoidWithRaceType
и вы наложили сон на это существо? Что, если наложить сон на то, что не является живым существом? А как насчет множества других заклинаний, которые вы можете использовать?
Обратите внимание, что в книгах AD&D часто есть таблицы, показывающие свойства различных рас, классов и так далее. Учитывая это, возможно, лучше закодировать эти таблицы в свою программу и сделать ее более управляемой данными.
Г. Спал