#include "creature.h" #include "dice.h" #include "rules.h" #include "weapon.h" #include "armor.h" #include "attack.h" #include "spellcasting.h" #include #include #include #include #include namespace creature { struct creatureImpl { std::string givenName; int hpMax; int hp; std::vector> inventory; std::map stats; std::map skills; int prof; std::string size; std::string alignment; int hit_die_count; int hit_die_sides; std::string speed; std::vector saves; std::vector senses; std::string langs; double cr; bool observant; NatArmor natural_armor; std::vector d_immunities; std::vector d_resistances; std::vector d_vulnerabilities; std::vector c_immunities; std::vector> features; }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(creatureImpl, givenName, hpMax, hp, inventory, stats, skills, prof, size, alignment, hit_die_count, hit_die_sides, speed, saves, senses, langs, cr, observant, natural_armor, d_immunities, d_resistances, d_vulnerabilities, c_immunities, features); NLOHMANN_FRIEND_DEFS(entry::Entry, Creature, data); Creature::Creature() : data(new creatureImpl()) {} // Pile of (simple) getters std::string Creature::getCreatureName(void) const {return getName();} std::string Creature::getGivenName(void) const {return data->givenName;} std::string Creature::getSize(void) const {return data->size;} std::string Creature::getAlignment(void) const {return data->alignment;} double Creature::getCR(void) const {return data->cr;} NatArmor Creature::getNaturalArmor(void) const {return data->natural_armor;} std::string Creature::getLanguages(void) const {return data->langs;} int Creature::getHP(void) const {return data->hp;} int Creature::getHPMax(void) const {return data->hpMax;} std::vector Creature::getSenses(void) const {return data->senses;} std::string Creature::getSpeed(void) const {return data->speed;} int Creature::getScore(const rules::Ability& ability) const {return data->stats.at(ability);} int Creature::getBonus(const rules::Ability& ability) const {return std::floor((getScore(ability) - 10) / 2.0);} int Creature::getProficiency(void) const {return data->prof;} int Creature::getInitiative(void) const {return getBonus(rules::Ability::Dex());} std::vector> Creature::getFeatures(void) const {return data->features;} std::vector> Creature::getInventory(void) const {return data->inventory;} std::vector Creature::getDmgImmunities(void) const {return data->d_immunities;} std::vector Creature::getDmgResistances(void) const {return data->d_resistances;} std::vector Creature::getDmgVulnerabilities(void) const {return data->d_vulnerabilities;} std::vector Creature::getCondImmunities(void) const {return data->c_immunities;} // Simple setters void Creature::setGivenName(const std::string& name) {data->givenName = name;} void Creature::setProficiency(int p) {data->prof = p;} void Creature::init() { if(data->hpMax == -1) { // Initialize hp data->hpMax = getBonus(rules::Ability::Con()) * data->hit_die_count; for(int i = 0; i < data->hit_die_count; i++) { data->hpMax += dice::roll(data->hit_die_sides); } // If less than zero (caused by negative con bonus), set to minimum of 1 if(data->hpMax <= 0) { data->hpMax = 1; } data->hp = data->hpMax; } } // True if type without matching qualifiers is in subdata bool conditionApplies(const std::string& type, const std::vector& qualifiers, const std::vector subdata) { bool applies = false; for(dmgType con : subdata) { if(con.type == type) { applies = true; for(auto qualifier : qualifiers) { if(find(con.qualifiers.begin(), con.qualifiers.end(), qualifier) != con.qualifiers.end()) { applies = false; } } } } return applies; } void Creature::applyDamage(int amount, const std::string& type, const std::vector& qualifiers) { if(! conditionApplies(type, qualifiers, getDmgImmunities())) { if(conditionApplies(type, qualifiers, getDmgResistances())) { data->hp -= amount / 2; } else if(conditionApplies(type, qualifiers, getDmgVulnerabilities())) { data->hp -= amount * 2; } else { data->hp -= amount; } } } int Creature::saveOrDamage(const rules::Ability& ability, int DC, int amount, const std::string& type, const std::vector& qualifiers, bool saveHalves) { // TODO: Something about evasion int rolled = dice::roll(20) + getAbilitySaveBonus(ability); if(rolled >= DC and saveHalves) { applyDamage(amount/2, type, qualifiers); } else if(rolled < DC) { applyDamage(amount, type, qualifiers); } return rolled; } void Creature::applyHealing(int amount) { data->hp += amount; if(data->hp > data->hpMax) { data->hp = data->hpMax; } } int Creature::getSkillBonus(const rules::Skill& skill) const { int bonus = this->getBonus(skill.getAbility()); if(data->skills.contains(skill)) { bonus += data->skills.at(skill) * getProficiency(); } return bonus; } int Creature::getAbilitySaveBonus(const rules::Ability& ability) const { int bonus = this->getBonus(ability); if(find(data->saves.begin(), data->saves.end(), ability) != data->saves.end()) { bonus += getProficiency(); } return bonus; } void Creature::addInventoryItem(std::shared_ptr item) { data->inventory.push_back(item); } void Creature::addSpellcasting() { if(getSpellcasting()) { throw std::runtime_error("Creature " + getName() + " already has spellcasting"); } std::shared_ptr sc(new entry::Spellcasting()); data->features.push_back(sc); } std::shared_ptr Creature::getSpellcasting() const { for(auto f : getFeatures()) { if(f->getType() == "spells") { return dynamic_pointer_cast(f); } } return std::shared_ptr(); } void Creature::addSpell(std::shared_ptr spell) { auto sc = getSpellcasting(); if(! sc) { throw std::runtime_error("Creature " + getName() + " has no spellcasting"); } std::size_t level = spell->getLevel(); while(sc->getSlotLevels().size() <= level) { sc->addSlotLevel(); } sc->getSlotLevels()[level]->spells.push_back(spell); } void Creature::removeSpell(std::shared_ptr spell) { auto sc = getSpellcasting(); if(sc) { if(sc->getSlotLevels().size() > (size_t) spell->getLevel()) { auto sl = sc->getSlotLevels()[spell->getLevel()]; for(auto it = sl->spells.begin(); it != sl->spells.end(); it++) { if(*it == spell) { sl->spells.erase(it); break; } } } } } void Creature::removeInventoryItem(std::shared_ptr item) { for(auto it = data->inventory.begin(); it != data->inventory.end(); it++) { if(*it == item) { data->inventory.erase(it); break; } } } std::map Creature::getSkills() const { std::map s; for(auto skill : data->skills) { s.insert({skill.first, getSkillBonus(skill.first)}); } return s; } std::map Creature::getSaves() const { std::map s; for(auto save : data->saves) { s.insert({save, this->getBonus(save) + getProficiency()}); } return s; } void Creature::setScore(const rules::Ability& ability, int score) { int initBonus = getBonus(ability); data->stats[ability] = score; if(ability == rules::Ability::Con()) { int delta = getBonus(ability) - initBonus; data->hpMax += delta * data->hit_die_count; data->hp += delta * data->hit_die_count; } } void Creature::setProfLevel(const rules::Skill& skill, int level) { data->skills[skill] = level; if(level == 0) { data->skills.erase(skill); } } void Creature::longRest() { data->hp = data->hpMax; } int getShieldBonus(const Creature& c) { for(auto a : utils::castPtrs(c.getInventory())) { if(a->getArmorType() == "shield") { return a->getACBonus(); } } return 0; } const int getAC(const Creature& c) { auto natArmor = c.getNaturalArmor(); if(! natArmor.name.empty()) { // Shields stack with nat armor (see lizardfolk) return natArmor.bonus + getShieldBonus(c); } int dex = c.getBonus(rules::Ability::Dex()); int baseBonus = 10 + dex; int miscBonus = 0; for(auto a : utils::castPtrs(c.getInventory())) { if(c.getScore(rules::Ability::Str()) < a->getStrRequirement()) { continue; } auto armorType = a->getArmorType(); if(armorType == "misc" || armorType == "shield") { miscBonus += a->getACBonus(); } else { baseBonus = a->getACBonus(); if(armorType == "light") { baseBonus += dex; } else if(armorType == "medium") { baseBonus += (dex > 2)? 2 : dex; } } } return baseBonus + miscBonus; } std::vector> getAttacks(const Creature& c) { std::vector> a = utils::castPtrs(c.getInventory()); for(auto attack : utils::castPtrs(c.getFeatures())) { a.push_back(std::shared_ptr(new entry::Weapon(attack->getWeapon()))); } return a; } rules::Ability getBestAbility(const std::vector& abilities, const Creature& c) { const rules::Ability* bestA = &abilities[0]; for(auto a : abilities) { if(c.getBonus(a) > c.getBonus(*bestA)) { bestA = &a; } } return *bestA; } template std::vector mapItems(const std::vector>& items) { std::vector out; for(auto i : items) { out.push_back(i->getName()); } return out; } std::vector dmgTypes2text(std::vector dmg) { std::vector ret; for(dmgType t : dmg) { ret.push_back(t.getText()); } return ret; } std::string formatDmgTypeVector(std::string title, std::vector dmg) { return title + ": " + utils::join(dmgTypes2text(dmg), "; "); } std::string Creature::getText() const { std::stringstream text; text << getGivenName() << " (" << getCreatureName() << "): " << getHP() << "/" << getHPMax() << " hp, " << getAC(*this) << " ac"; if(! getNaturalArmor().name.empty()) { text << " (" << getNaturalArmor().name; if(getShieldBonus(*this) != 0) { text << ", shield"; } text << ")"; } else { std::string armor = utils::join(mapItems(utils::castPtrs(getInventory())), ", "); if(! armor.empty()) { text << " (" << armor << ")"; } } text << ", speed " << getSpeed() << std::endl; text << "A cr " << getCR() << " " << getAlignment() << " " << getSize() << " " << getType() << "." << std::endl; text << "Stats:" << std::endl; using namespace rules; std::vector abilities {Ability::Str(), Ability::Dex(), Ability::Con(), Ability::Int(), Ability::Wis(), Ability::Cha()}; for(auto ability : abilities) { text << " " << std::setw(6) << std::left << ability.getAbbrev(); } text << std::endl; for(auto ability : abilities) { text << std::setw(7) << std::left << (std::to_string(getScore(ability)) + "(" + std::to_string(getBonus(ability)) + ")"); } text << std::endl; text << "Senses: "; if(! getSenses().empty()) { text << utils::join(getSenses(), ", ") << ", "; } text << "Passive Perception " << 10 + getSkillBonus(rules::Skill::Perception()) + (data->observant? 5 : 0) << std::endl; if(! getLanguages().empty()) { text << "Languages: " << getLanguages() << std::endl; } text << "Proficiency: " << getProficiency() << std::endl; if(! getSkills().empty()) { text << std::endl << "Skills:" << std::endl; for(auto skill : getSkills()) { text << " * " << skill.first.getName() << " (+" << skill.second << ")" << std::endl; } } if(! getSaves().empty()) { text << std::endl << "Saves:" << std::endl; for(auto save : getSaves()) { text << " * " << save.first.getAbbrev() << " (+" << save.second << ")" << std::endl; } } if(! getDmgImmunities().empty()) { text << formatDmgTypeVector("\nDamage Immunities", getDmgImmunities()) << std::endl; } if(! getDmgResistances().empty()) { text << formatDmgTypeVector("\nDamage Resistances", getDmgResistances()) << std::endl; } if(! getDmgVulnerabilities().empty()) { text << formatDmgTypeVector("\nDamage Vulnerabilities", getDmgVulnerabilities()) << std::endl; } if(! getCondImmunities().empty()) { text << formatDmgTypeVector("\nCondition Immunities", getCondImmunities()) << std::endl; } if(! getFeatures().empty()) { text << std::endl << "Features:" << std::endl; for(auto f: getFeatures()) { text << " * " << f->getText(*this) << std::endl; } } if(! getInventory().empty()) { text << std::endl << "Inventory:" << std::endl; for(auto i : getInventory()) { text << " * " << i->getText(*this) << std::endl; } } if(! Entry::getText().empty()) { text << std::endl << Entry::getText() << std::endl; } return text.str(); } }