diff --git a/bin/mharmor.py b/bin/mharmor.py new file mode 100755 index 0000000..be61dee --- /dev/null +++ b/bin/mharmor.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python + +import sys +import argparse +import codecs + +import _pathfix + +from mhapi.db import MHDB + +def get_utf8_writer(writer): + return codecs.getwriter("utf8")(writer) + +def parse_args(argv): + parser = argparse.ArgumentParser(description= + "Find armor with the specified skills and sort by" + " (max points, defense). Takes into account native points" + " and slots that fit decorations for the first skill." + ) + parser.add_argument("-g", "--gunner", action="store_true", + default=False, + help="search for gunner instead of blademaster") + parser.add_argument("-d", "--min-defense", type=int, + help="Only include armors with min defense") + parser.add_argument("-t", "--type", + help="Head, Body, Arms, Waist, or Legs") + parser.add_argument("skills", nargs="+", + help="One or more armor skills to search for") + + return parser.parse_args(argv) + + +def find_armors(args): + db = MHDB(_pathfix.db_path) + + skills = {} + skill_ids = [] # preserve arg order + decorations = {} + for skill_name in args.skills: + sid = db.get_skill_tree_id(skill_name) + if sid is None: + raise ValueError("Skill '%s' not found" % skill_name) + skills[skill_name] = sid + skill_ids.append(sid) + #print skill_name, sid + ds = db.get_decorations_by_skills([sid]) + for d in ds: + d.set_skills(db.get_item_skills(d.id)) + decoration_values = get_decoration_values(sid, ds) + decorations[sid] = (ds, decoration_values) + print "%s[%s]:" % (skill_name, sid), ", ".join(d.name for d in ds), \ + decoration_values + + htype = "Gunner" if args.gunner else "Blade" + + armors = db.get_armors_by_skills(skill_ids, htype) + + skill_totals = {} + for a in armors: + skills = db.get_item_skills(a.id) + if not skills: + print "Error getting skills for '%s' (%d)" % (a.name, a.id) + sys.exit(1) + a.set_skills(skills) + # calculate total using decorations for first skill only. This + # works great if all skill shave same slot values; if not it's + # very messy to figure out what is 'best' + total = 0 + first = True + for sid in skill_ids: + if first: + dv = decorations[sid][1] + first = False + else: + dv = [] + total += a.skill(sid, dv) + skill_totals[a.id] = total + + armors.sort(key=lambda a: (skill_totals[a.id], a.defense), reverse=True) + + for a in armors: + if args.min_defense and a.defense < args.min_defense: + continue + if args.type and a.slot != args.type: + continue + total = skill_totals[a.id] + print a.id, skill_totals[a.id], a.one_line_u() + print " ", a.one_line_skills_u(args.skills) + + +def get_decoration_values(skill_id, decorations): + # TODO: write script to compute this and shove innto skill_tree table + values = [0, 0, 0] + for d in decorations: + assert d.num_slots is not None + # some skills like Handicraft have multiple decorations with + # same number of slots - use the best one + new = d.skills[skill_id] + current = values[d.num_slots-1] + if new > current: + values[d.num_slots-1] = new + return values + + +if __name__ == '__main__': + args = parse_args(None) + + sys.stdout = get_utf8_writer(sys.stdout) + find_armors(args) diff --git a/bin/mhdamage.py b/bin/mhdamage.py index 1570725..0014591 100755 --- a/bin/mhdamage.py +++ b/bin/mhdamage.py @@ -18,9 +18,11 @@ def percent_change(a, b): def parse_args(argv): - parser = argparse.ArgumentParser( + parser = argparse.ArgumentParser(description= "Calculate damage to monster from different weapons of the" - " same class" + " same class. The average motion value for the weapon class" + " is used for raw damage calculations, to get a rough idea of" + " the relative damage from raw vs element when comparing." ) parser.add_argument("-s", "--sharpness-plus-one", action="store_true", default=False, diff --git a/bin/mkjsonapi.py b/bin/mkjsonapi.py index 7de04f7..8c22e8a 100755 --- a/bin/mkjsonapi.py +++ b/bin/mkjsonapi.py @@ -5,6 +5,7 @@ import json import sys import errno from collections import defaultdict +import urllib import _pathfix @@ -20,6 +21,17 @@ def mkdirs_p(path): raise +SAFE_CHARS = " &'+\"" + + +def file_path(path, model_object, use_name=False): + if use_name and "name" in model_object: + key = urllib.quote(model_object.name.encode("utf8"), SAFE_CHARS) + else: + key = str(model_object.id) + return os.path.join(path, "%s.json" % key) + + def write_list_file(path, model_list): list_path = os.path.join(path, "_list.json") with open(list_path, "w") as f: @@ -39,7 +51,7 @@ def monster_json(db, path): indexes = {} for m in monsters: - monster_path = os.path.join(path, "%s.json" % m.id) + monster_path = file_path(path, m) m.update_indexes(indexes) data = m.as_data() damage = db.get_monster_damage(m.id) @@ -58,7 +70,7 @@ def weapon_json(db, path): indexes = {} for w in weapons: - weapon_path = os.path.join(path, "%s.json" % w.id) + weapon_path = file_path(path, w) w.update_indexes(indexes) with open(weapon_path, "w") as f: w.json_dump(f) @@ -73,7 +85,7 @@ def items_json(db, path): indexes = {} for item in items: - item_path = os.path.join(path, "%s.json" % item.id) + item_path = file_path(path, item) item.update_indexes(indexes) with open(item_path, "w") as f: item.json_dump(f) diff --git a/mhapi/damage.py b/mhapi/damage.py index 7099e7e..0fdda2f 100644 --- a/mhapi/damage.py +++ b/mhapi/damage.py @@ -199,6 +199,11 @@ class WeaponMonsterDamage(object): self.etype = self.weapon.awaken self.eattack = self.weapon.awaken_attack + if self.eattack: + self.eattack = int(self.eattack) + else: + self.eattack = 0 + self.true_raw = skills.AttackUp.modified(attack_skill, self.true_raw) self.affinity = skills.CriticalEye.modified(critical_eye_skill, diff --git a/mhapi/db.py b/mhapi/db.py index 016dd43..92c8dc7 100644 --- a/mhapi/db.py +++ b/mhapi/db.py @@ -102,12 +102,12 @@ class MHDB(object): """ List of unicode strings. """ - item_types.sort() + args = sorted(item_types) placeholders = ", ".join(["?"] * len(item_types)) return self._query_all("item_names", """ SELECT _id, name FROM items WHERE type IN (%s) - """ % placeholders, tuple(item_types), model_cls=field_model("name")) + """ % placeholders, tuple(args), model_cls=field_model("name")) def get_items(self, item_types=None): """ @@ -115,7 +115,7 @@ class MHDB(object): """ q = "SELECT * FROM items" if item_types: - item_types.sort() + item_types = sorted(item_types) placeholders = ", ".join(["?"] * len(item_types)) q += "\nWHERE type IN (%s)" % placeholders item_types = tuple(item_types) @@ -304,6 +304,104 @@ class MHDB(object): WHERE items.name=? """, (name,), model_cls=model.Weapon) + def get_armors(self): + return self._query_all("armors", """ + SELECT * FROM armor + LEFT JOIN items ON armor._id = items._id + """, model_cls=model.Armor) + + def get_armor(self, armor_id): + return self._query_one("armor", """ + SELECT * FROM armor + LEFT JOIN items ON armor._id = items._id + WHERE armor._id=? + """, (armor_id,), model_cls=model.Armor) + + def get_armor_by_name(self, name): + return self._query_one("armor", """ + SELECT * FROM armor + LEFT JOIN items ON armor._id = items._id + WHERE items.name=? + """, (name,), model_cls=model.Armor) + + def get_item_skills(self, item_id): + return self._query_all("item_skills", """ + SELECT item_to_skill_tree.*, skill_trees.name + FROM item_to_skill_tree + LEFT JOIN skill_trees + ON item_to_skill_tree.skill_tree_id = skill_trees._id + WHERE item_to_skill_tree.item_id=? + """, (item_id,), model_cls=model.ItemSkill) + + def get_decorations(self): + return self._query_all("decorations", """ + SELECT * + FROM decorations + INNER JOIN items + ON items._id = decorations._id + """, model_cls=model.Decoration) + + def get_decoration(self, decoration_id): + return self._query_one("decoration", """ + SELECT * + FROM decorations + INNER JOIN items + ON items._id = decorations._id + WHERE decorations._id = ? + """, (decoration_id,), model_cls=model.Decoration) + + def get_decoration_by_name(self, name): + return self._query_all("decoration", """ + SELECT * + FROM decorations + INNER JOIN items + ON items._id = decorations._id + WHERE items.name = ? + """, (name,), model_cls=model.Decoration) + + def get_skill_tree_id(self, skill_tree_name): + result = self._query_one("skill", """ + SELECT _id FROM skill_trees + WHERE name=? + """, (skill_tree_name,)) + if result: + return result["_id"] + return None + + def get_decorations_by_skills(self, skill_tree_ids): + args = sorted(skill_tree_ids) + placeholders = ", ".join(["?"] * len(skill_tree_ids)) + return self._query_all("decorations", """ + SELECT items.*, decorations.* + FROM item_to_skill_tree + LEFT JOIN items + ON items._id = item_to_skill_tree.item_id + LEFT JOIN decorations + ON decorations._id = item_to_skill_tree.item_id + WHERE items.type = 'Decoration' + AND item_to_skill_tree.skill_tree_id IN (%s) + AND item_to_skill_tree.point_value > 0 + GROUP BY item_to_skill_tree.item_id + """ % placeholders, tuple(args), model_cls=model.Decoration) + + def get_armors_by_skills(self, skill_tree_ids, hunter_type): + args = sorted(skill_tree_ids) + placeholders = ", ".join(["?"] * len(skill_tree_ids)) + args += [hunter_type] + return self._query_all("decorations", """ + SELECT items.*, armor.* + FROM item_to_skill_tree + LEFT JOIN items + ON items._id = item_to_skill_tree.item_id + LEFT JOIN armor + ON armor._id = item_to_skill_tree.item_id + WHERE items.type = 'Armor' + AND item_to_skill_tree.skill_tree_id IN (%s) + AND item_to_skill_tree.point_value > 0 + AND armor.hunter_type IN ('Both', ?) + GROUP BY item_to_skill_tree.item_id + """ % placeholders, tuple(args), model_cls=model.Armor) + def get_monster_breaks(self, monster_id): """ List of strings. diff --git a/mhapi/model.py b/mhapi/model.py index 5d10aaf..c0af82f 100644 --- a/mhapi/model.py +++ b/mhapi/model.py @@ -54,6 +54,9 @@ class RowModel(ModelBase): def __getitem__(self, key): return self._data[key] + def __contains__(self, key): + return key in self._data + def fields(self): return self._data.keys() @@ -190,6 +193,76 @@ class WeaponSharpness(ModelBase): return self.value_list +class ItemWithSkills(RowModel): + def __init__(self, item_row): + super(ItemWithSkills, self).__init__(item_row) + self.skills = None + self.skill_ids = [] + self.skill_names = [] + + def set_skills(self, item_skills): + self.skills = {} + for s in item_skills: + self.skills[s.skill_tree_id] = s.point_value + self.skills[s.name] = s.point_value + self.skill_ids.append(s.skill_tree_id) + self.skill_names.append(s.name) + + def skill(self, skill_id_or_name): + return self.skills.get(skill_id_or_name, 0) + + def one_line_skills_u(self, skill_names=None): + if skill_names is None: + skill_names = sorted(self.skill_names) + return ", ".join("%s %d" % (name, self.skills[name]) + for name in skill_names + if name in self.skills) + + +class Armor(ItemWithSkills): + _indexes = { "name": ["id"], + "slot": ["id", "name"] } + + _one_line_template = string.Template( + "$name ($slot) Def $defense-$max_defense Slot $num_slots" + ) + + def __init__(self, armor_item_row): + super(Armor, self).__init__(armor_item_row) + + def one_line_u(self): + return self._one_line_template.substitute(self.as_data()) + + def skill(self, skill_id_or_name, decoration_values=()): + """ + decoration_values should be a list of points from the given + number of slots, e.g. [1, 3] means that one slot gets 1 point + and two slots get 3 points, [1, 0, 4] means that one slot gets 1 point, + there is no two slot gem, and three slots gets 4 points. + """ + assert self.skills is not None + total = self.skills.get(skill_id_or_name, 0) + slots_left = self.num_slots + for slots in xrange(len(decoration_values), 0, -1): + if slots_left == 0: + break + decoration_value = decoration_values[slots-1] + if not decoration_value: + continue + if slots <= slots_left: + total += decoration_value + slots_left -= slots + return total + + +class Decoration(ItemWithSkills): + pass + + +class ItemSkill(RowModel): + pass + + class Weapon(RowModel): _list_fields = ["id", "wtype", "name"] _indexes = { "name": ["id"],