#include "cmd.h" #include "../utils.h" #include "../creature.h" #include "../item.h" #include "../spellcasting.h" #include "../settings.h" #include "../weapon.h" #include "../dice.h" #include "../armor.h" #include #include #include #include #include #include namespace fs = std::filesystem; namespace cmd { // Call after applying to format printing std::string formatHealingDamage(const std::shared_ptr& c, int initHP, bool heal, int amnt, const std::string& dmgType, const std::vector& qualifiers) { std::stringstream text; text << (heal? "Healing " : "Damaging ") << c->getGivenName() << " the " << c->getCreatureName() << " by " << amnt; if(! heal) { std::string qualsString; std::vector positiveQuals; for(auto qual : qualifiers) { positiveQuals.push_back(qual.getPositive()); } if(! positiveQuals.empty()) { qualsString = " " + utils::join(positiveQuals, ", "); } text << qualsString << " " << dmgType << " damage"; } text << ". HP: " << initHP << " -> " << c->getHP() << "." << std::endl; return text.str(); } std::string healOrDamageProgrammatic(fs::path p, bool heal, int amnt, std::string dmgType, const std::vector& qualifiers) { auto c = utils::instantiate(p); int initHP = c->getHP(); if(heal) { c->applyHealing(amnt); } else { c->applyDamage(amnt, dmgType, qualifiers); } utils::saveJson(c->serialize(), p); return formatHealingDamage(c, initHP, heal, amnt, dmgType, qualifiers); } std::string healOrDamage(bool heal, std::vector args, std::map flags) { auto qualifiers = parseQualifiers(flags); fs::path p = getTruePath(args[0]); int amnt = utils::parseInt(args[1]); std::string dmgType = "force"; if(args.size() == 3) { dmgType = args[2]; } return healOrDamageProgrammatic(p, heal, amnt, dmgType, qualifiers); } std::string attack(std::vector args, std::map flags) { std::stringstream text; bool is2h = flags.find("2") != flags.end(); bool is1h = flags.find("1") != flags.end(); if(is2h and is1h) { text << "ERROR: Cannot be both 1 handed and 2 handed!" << std::endl; return text.str(); } auto c1 = utils::instantiate(getTruePath(args[0])); args.erase(args.begin()); fs::path p2 = getTruePath(args.back()); auto c2 = utils::instantiate(p2); args.erase(args.end()-1); std::string attackName = utils::join(args, " "); utils::lower(attackName); std::shared_ptr w; for(auto weap : creature::getAttacks(*c1)) { if(weap->getName() == attackName) { w = weap; break; } } text << w->getText(*c1) << std::endl; int rolled = dice::roll(20); int bonus = w->getToHitBonus(*c1); text << formatRoll(w->getName(), "attack", rolled, bonus); int ac = creature::getAC(*c2); if(rolled + bonus >= ac) { text << " Hit (" << (rolled + bonus) << " to hit >= " << ac << " ac): "; bool wants2h = true; for(auto a : utils::castPtrs(c1->getInventory())) { if(a->getArmorType() == "shield") { wants2h = false; } } if(is2h) { wants2h = true; } else if(is1h) { wants2h = false; } auto dmg = entry::rollDmg(*w, wants2h); text << entry::formatDmg(*w, *c1, dmg) << std::endl; bool isFirst = true; for(auto d : dmg) { int amnt = d.rolled; if(isFirst) { amnt += w->getDamageBonus(*c1); isFirst = false; } text << " " << healOrDamageProgrammatic(p2, false, amnt, d.dmg_type, {}); } } else { text << " Miss (" << (rolled + bonus) << " to hit < " << ac << " ac)" << std::endl; } return text.str(); } std::string heal(std::vector args) { return healOrDamage(true, args, {}); } std::string damage(std::vector args, std::map flags) { return healOrDamage(false, args, flags); } std::string save(std::vector args, std::map flags) { if(args.size() < 3) { throw std::runtime_error("Subcommand 'save' requires at least 3 arguments"); } std::stringstream text; rules::Ability ability = rules::tryGetAbilityOrSkill(args[0]); if(! ability) { throw std::runtime_error("Requires a valid ability name but received \"" + args[0] + "\"."); } args.erase(args.begin()); int DC = utils::parseInt(args[0]); args.erase(args.begin()); // Now iterate over the paths for(std::string s : args) { fs::path p = getTruePath(s); auto c = utils::instantiate(p); int initHP = c->getHP(); int rolled = dice::roll(20); int bonus = c->getAbilitySaveBonus(ability); int damage = 0; std::string type = "force"; bool halves = flags.find("halves") != flags.end(); if(flags.find("damage") != flags.end()) { damage = utils::parseInt(flags.at("damage")); if(flags.find("type") != flags.end()) type = flags.at("type"); auto qualifiers = parseQualifiers(flags); rolled = c->saveOrDamage(ability, DC, damage, type, qualifiers, halves); rolled -= bonus; // It's combined in creature } bool passed = rolled + bonus >= DC; text << c->getName() << " " << (passed? "PASS" : "FAIL") << ": "; text << formatRoll(ability.getFull(), "save", rolled, bonus); if(flags.find("damage") != flags.end() and (halves or ! passed)) { text << formatHealingDamage(c, initHP, false, damage, type, parseQualifiers(flags)); } utils::saveJson(c->serialize(), p); } return text.str(); } std::string reset(std::vector args) { for(std::string s : args) { fs::path p = getTruePath(s); auto c = utils::instantiate(p); c->longRest(); utils::saveJson(c->serialize(), p); } return ""; } std::string set(std::vector args) { if(args.size() < 3) { throw std::runtime_error("Subcommand 'set' requires at least 3 arguments"); } fs::path p = getTruePath(args[0]); args.erase(args.begin()); // remove path from args if(args[0] == "name") { args.erase(args.begin()); // remove "name" from args auto newname = utils::join(args, " "); auto e = utils::instantiate(p); if(e->getEntryType() == "creatures") { // creature, we should do given name instead auto c = utils::instantiate(p); c->setGivenName(newname); utils::saveJson(c->serialize(), p); } else { // Standard entry, no given name so we set name e->setName(newname); utils::saveJson(e->serialize(), p); } } else if(args[0] == "proficiency") { auto c = utils::instantiate(p); c->setProficiency(utils::parseInt(args[1])); utils::saveJson(c->serialize(), p); } else if(args[0] == "cost") { auto i = utils::instantiate(p); i->setCost(utils::parseInt(args[1])); utils::saveJson(i->serialize(), p); } else if(args[0] == "weight") { auto i = utils::instantiate(p); i->setWeight(utils::parseDouble(args[1])); utils::saveJson(i->serialize(), p); } else { auto c = utils::instantiate(p); // Either an ability or a skill. If skill, then it could be multiple words long. std::string toSet = args.back(); args.erase(--args.end()); std::string abilityOrSkill = utils::join(args, " "); rules::Skill skill = rules::tryGetAbilityOrSkill(abilityOrSkill); rules::Ability ability = rules::tryGetAbilityOrSkill(abilityOrSkill); if(skill) { // ensure lower case utils::lower(toSet); int level = -1; if(toSet == "none") level = 0; else if(toSet == "proficient") level = 1; else if(toSet == "expert") level = 2; if(level == -1) { throw std::runtime_error("Skill levels can be set to none, proficient, or expert, but " + toSet + " was given."); } c->setProfLevel(skill, level); } else if(ability) { c->setScore(ability, utils::parseInt(toSet)); } else { throw std::runtime_error("Subcommand 'set' expected an ability, skill, proficiency, or name field to set, but was given " + abilityOrSkill); } utils::saveJson(c->serialize(), p); } return ""; } std::string add(std::vector args) { std::stringstream text; fs::path p = getTruePath(args[0]); args.erase(args.begin()); // remove path from args auto c = utils::instantiate(p); std::string addName = utils::join(args, " "); std::shared_ptr ent; fs::path path = getTruePath(addName); if(fs::directory_entry(path).exists()) { ent = utils::instantiate(path); } else { ent = entry::Entry::create(utils::findByName(addName)); } // Determine if it is an item or a spell auto i = std::dynamic_pointer_cast(ent); if(i) { c->addInventoryItem(i); } else { auto s = std::dynamic_pointer_cast(ent); if(s) { c->addSpell(s); } else { throw std::runtime_error("Could not add the " + ent->getType() + " " + ent->getName() + " to " + c->getGivenName() + " the " + c->getName() + ": Requires a weapon, armor, or spell, but received object of type " + ent->getType()); } } utils::saveJson(c->serialize(), p); text << "Added the " << ent->getType() << " " << ent->getName() << " to " << c->getGivenName() << " the " << c->getName() << std::endl; return text.str(); } std::string del(std::vector args) { std::stringstream text; fs::path p = getTruePath(args[0]); args.erase(args.begin()); // remove path from args auto c = utils::instantiate(p); //Atempt to load the item if it's a path std::string itemName = utils::join(args, " "); try { auto i = utils::instantiate(getTruePath(itemName)); if(i) { itemName = i->getName(); } } catch(std::exception& e) {} // eat. utils::lower(itemName); // Loop through all of c's stuff, searching for itemName std::shared_ptr removed; for(auto item : c->getInventory()) { std::string name = item->getName(); if(utils::lower(name) == itemName) { c->removeInventoryItem(item); removed = item; break; } } if(! removed) { for(auto spell : c->getSpellcasting()->getSpells()) { std::string name = spell->getName(); if(utils::lower(name) == itemName) { c->removeSpell(spell); removed = spell; break; } } } utils::saveJson(c->serialize(), p); if(removed) { text << "Successfully removed the " << removed->Entry::getType() << " " << removed->getName() << std::endl; } else { text << "Could not find any inventory item nor spell by that name" << std::endl; } return text.str(); } std::string edit(std::vector args) { auto p = getTruePath(args[0]); auto e = utils::instantiate(p); auto editor = settings::getString("editor"); // General workflow: copy notes (text) from e to a temp file, edit it, then copy back. fs::path tmp("/tmp/dmtool.tmp"); std::ofstream out(tmp); out << e->Entry::getText(); out.close(); std::system((editor + " " + tmp.string()).c_str()); std::ifstream in(tmp); std::string newText(std::istreambuf_iterator{in}, {}); e->setText(newText); utils::saveJson(e->serialize(), p); return ""; } std::string spellcasting(std::vector args) { std::stringstream text; auto p = getTruePath(args[0]); auto c = utils::instantiate(p); auto subcommand = args[1]; if(subcommand != "init" && subcommand != "ability" && subcommand != "level") { throw std::runtime_error("Unknown option \"" + subcommand + "\""); } if(subcommand == "init") { c->addSpellcasting(); } else { auto sc = c->getSpellcasting(); if(! sc) { throw std::runtime_error("Creature " + c->getName() + " has no spellcasting"); } text << "Added spellcasting to " << c->getName() << std::endl; if(subcommand == "ability") { if(args.size() != 3) { throw std::runtime_error("Subcommand \"spellcasting ability\" requires an additional parameter, but none was given"); } sc->setAbility(args[2]); text << "Set " << c->getName() << " spellcasting ability to " << args[2] << std::endl; } else { // subcommand == "level" if(args.size() != 4) { throw std::runtime_error("Subcommand \"spellcasting level\" requires more parameters"); } int level = utils::parseInt(args[2]); int slots = utils::parseInt(args[3]); if(level <= 0 || slots < 0) { throw std::runtime_error("Spellcasting target out of range"); } while(sc->getSlotLevels().size() <= (std::size_t) level) { sc->addSlotLevel(); } sc->getSlotLevels()[level]->numSlots = slots; text << "Gave " << c->getName() << " " << slots << " " << utils::toOrdinal(level) << " level spell slots" << std::endl; } } utils::saveJson(c->serialize(), p); return text.str(); } std::string create(std::vector args) { auto p = getTruePath(args[0]); args.erase(args.begin()); // remove path from args auto name = args[1]; if(args[0] == "item") { nlohmann::json j = entry::Entry("item", name, "item", ""); j["cost"] = 0; j["weight"] = 0.0; utils::saveJson(j, p); } else if(args[0] == "weapon" or args[0] == "feature_attack") { nlohmann::json j = entry::Entry((args[0] == "weapon")? "item" : "feature", name, "weapons", ""); j["cost"] = 0; j["weight"] = 0.0; j["damage"] = {{{"dmg_die_count", 1}, {"dmg_die_sides", 6}, {"dmg_type", "bludgeoning"}, {"is_or", false}}}; j["properties"] = std::vector(); j["weapon_type"] = ""; j["range"] = {0, 0}; j["reach"] = 5; j["toHitOverride"] = {}; // Set to null j["dmgBonusOverride"] = {}; j["abilityOverride"] = {}; utils::saveJson(j, p); } else if(args[0] == "armor") { nlohmann::json j = entry::Entry("item", name, "armor", ""); j["cost"] = 0; j["weight"] = 0.0; j["ac"] = 10; j["strength"] = 0; j["disadvantage"] = false; j["armor_type"] = "light"; utils::saveJson(j, p); } else if(args[0] == "spell") { nlohmann::json j = entry::Entry("spells", name, "UNKNOWN", ""); j["level"] = 0; j["classes"] = std::vector(); j["casting_time"] = "1 action"; j["range"] = "Touch"; j["components"] = "V, S, M"; j["duration"] = "Instantaneous"; utils::saveJson(j, p); } else if(args[0] == "feature") { nlohmann::json j = entry::Entry("feature", name, "feature", ""); utils::saveJson(j, p); } else if(args[0] == "creature") { nlohmann::json j = entry::Entry("creatures", name, "UNKNOWN", ""); j["givenName"] = "NAME"; j["hpMax"] = j["hp"] = -1; j["inventory"] = j["saves"] = j["senses"] = j["d_resistances"] = j["d_vulnerabilities"] = j["d_immunities"] = j["c_immunities"] = j["features"] = std::vector(); j["skills"] = std::map(); j["stats"] = {{"str", 10}, {"dex", 10}, {"con", 10}, {"int", 10}, {"wis", 10}, {"cha", 10}}; j["prof"] = 2; j["size"] = "Medium"; j["alignment"] = "any alignment"; j["hit_die_count"] = 1; j["hit_die_sides"] = 8; j["speed"] = "30 ft."; j["langs"] = "any one language (usually Common)"; j["cr"] = 0.0; j["observant"] = false; j["natural_armor"] = {{"name", ""}, {"bonus", 0}}; utils::saveJson(j, p); } return "Successfully created " + args[0] + " " + args[1] + ".\n"; } std::string git(std::vector args) { std::string root = getTruePath("").string(); std::system(("cd " + root + " && " + utils::join(args, " ")).c_str()); return ""; } }