From a80d8740b30b71d9774549f3808fde5f5d542824 Mon Sep 17 00:00:00 2001 From: Bryce Allen Date: Sat, 30 Jul 2016 20:25:11 -0500 Subject: [PATCH] add quest level filter to weapon list for mhgen --- bin/mkjsonapi.py | 8 +++ bin/weapon_level.py | 109 ++++++++---------------------- mhapi/db.py | 29 ++++++-- mhapi/model.py | 137 ++++++++++++++++++++++++++++++++++++++ web/mhgen/recommends.html | 106 +++++++++++++++++++++++++++++ web/mhgen/weaponlist.html | 108 +++++++++++++++++++++++++++++- 6 files changed, 409 insertions(+), 88 deletions(-) create mode 100644 web/mhgen/recommends.html diff --git a/bin/mkjsonapi.py b/bin/mkjsonapi.py index 23da1f2..02004e6 100755 --- a/bin/mkjsonapi.py +++ b/bin/mkjsonapi.py @@ -174,6 +174,8 @@ def weapon_json(db, path): mkdirs_p(path) write_list_file(path, weapons) + item_stars = model.ItemStars(db) + all_data = [] melodies = {} indexes = {} @@ -193,6 +195,12 @@ def weapon_json(db, path): ] data["horn_melodies"] = melodies[w.horn_notes] + stars = item_stars.get_weapon_stars(w) + data["village_stars"] = stars["Village"] + data["guild_stars"] = stars["Guild"] + data["permit_stars"] = stars["Permit"] + data["arena_stars"] = stars["Arena"] + all_data.append(data) with open(weapon_path, "w") as f: diff --git a/bin/weapon_level.py b/bin/weapon_level.py index 23283d9..7639dd5 100755 --- a/bin/weapon_level.py +++ b/bin/weapon_level.py @@ -1,99 +1,42 @@ #!/usr/bin/env python2 import sys +import argparse import _pathfix from mhapi.db import MHDB, MHDBX -from mhapi.model import get_costs - - -def find_cost_level(db, c): - monsters = { "HR": set(), "LR": set() } - materials = { "HR": set(), "LR": set() } - stars = dict(Village=None, Guild=None, Permit=None, Arena=None) - for item in c["components"].keys(): - if item.startswith("HR ") or item.startswith("LR "): - if not item.endswith(" Materials"): - print "Error: bad item format '%s'" % item - rank = item[:2] - item = item[len("HR "):-len(" Materials")] - monster = db.get_monster_by_name(item) - if monster: - monsters[rank].add(monster) - #print "Monster", rank, monster.name, monster.id - else: - materials[rank].add(item) - #print "Material", rank, item - else: - data = db.get_item_by_name(item) - current_stars = find_item_level(db, data.id) - # keep track of most 'expensive' item - for k, v in current_stars.iteritems(): - if v is None: - continue - if stars[k] is None or v > stars[k]: - stars[k] = v - return stars - - -def find_item_level(db, item_id): - stars = dict(Village=None, Guild=None, Permit=None, Arena=None) - - quests = db.get_item_quests(item_id) - - gathering = db.get_item_gathering(item_id) - gather_locations = set() - for gather in gathering: - gather_locations.add((gather["location_id"], gather["rank"])) - for location_id, rank in list(gather_locations): - gather_quests = db.get_location_quests(location_id, rank) - quests.extend(gather_quests) - - monsters = db.get_item_monsters(item_id) - monster_ranks = set() - for monster in monsters: - monster_ranks.add((monster["monster_id"], monster["rank"])) - for monster_id, rank in list(monster_ranks): - monster_quests = db.get_monster_quests(monster_id, rank) - quests.extend(monster_quests) - - # find least expensive quest for getting the item - for quest in quests: - if quest.stars == 0: - # ignore training quests - if "Training" not in quest.name: - print "Error: non training quest has 0 stars", \ - quest.id, quest.name - continue - if quest.hub in stars: - current = stars[quest.hub] - if current is None or quest.stars < current: - stars[quest.hub] = quest.stars - else: - print "Error: unknown hub", quest.hub - - return stars +from mhapi.model import ItemStars def main(): - weapon_name = sys.argv[1] db = MHDB(game="gen", include_item_components=True) - weapon = db.get_weapon_by_name(weapon_name) - if weapon is None: - print "Weapon '%s' not found" % weapon_name + item_stars = ItemStars(db) + + parser = argparse.ArgumentParser() + parser.add_argument("-i", "--item") + parser.add_argument("-w", "--weapon") + + args = parser.parse_args() + if args.item: + item = db.get_item_by_name(args.item) + if item is None: + print "Item '%s' not found" % args.item + sys.exit(1) + if item.type == "Materials": + stars = item_stars.get_material_stars(item.id) + else: + stars = item_stars.get_item_stars(item.id) + elif args.weapon: + weapon = db.get_weapon_by_name(args.weapon) + if weapon is None: + print "Weapon '%s' not found" % args.weapon + sys.exit(1) + stars = item_stars.get_weapon_stars(weapon) + else: + print "Specify -w or -i" sys.exit(1) - costs = get_costs(db, weapon) - stars = dict(Village=None, Guild=None, Permit=None, Arena=None) - # find least 'expensive' path - for c in costs: - current_stars = find_cost_level(db, c) - for k, v in current_stars.iteritems(): - if v is None: - continue - if stars[k] is None or v < stars[k]: - stars[k] = v for k, v in stars.iteritems(): print k, v diff --git a/mhapi/db.py b/mhapi/db.py index 2866ead..d38ae6c 100644 --- a/mhapi/db.py +++ b/mhapi/db.py @@ -141,19 +141,26 @@ class MHDB(object): WHERE type IN (%s) """ % placeholders, tuple(args), model_cls=field_model("name")) - def get_items(self, item_types=None): + def get_items(self, item_types=None, exclude_types=None): """ List of item objects. """ q = "SELECT * FROM items" + args = [] if item_types: item_types = sorted(item_types) placeholders = ", ".join(["?"] * len(item_types)) q += "\nWHERE type IN (%s)" % placeholders - item_types = tuple(item_types) + args.extend(item_types) + if exclude_types: + exclude_types = sorted(exclude_types) + placeholders = ", ".join(["?"] * len(exclude_types)) + q += "\nWHERE type NOT IN (%s)" % placeholders + args.extend(exclude_types) else: - item_types = () - return self._query_all("items", q, item_types, model_cls=model.Item) + args = [] + args = tuple(args) + return self._query_all("items", q, args, model_cls=model.Item) def get_item(self, item_id): """ @@ -177,6 +184,8 @@ class MHDB(object): """ Single wyporium row or None. """ + if self.game != "4u": + return None return self._query_one("wyporium", """ SELECT * FROM wyporium WHERE item_in_id=? @@ -514,6 +523,18 @@ class MHDB(object): WHERE notes=? """, (notes,), model_cls=model.HornMelody) + def get_material_items(self, material_item_id): + """ + Get dict rows of items that satisfy the given material, containing + item_id and amount keys. MHGen only. + """ + assert self.game == "gen" + return self._query_all("material_items", """ + SELECT item_id, amount FROM item_to_material + WHERE item_to_material.material_item_id = ? + ORDER BY amount ASC + """, (material_item_id,)) + def _add_components(self, key, item_results): """ Add component data to item results from _query_one or _query_all, diff --git a/mhapi/model.py b/mhapi/model.py index 68320cf..db9fcd6 100644 --- a/mhapi/model.py +++ b/mhapi/model.py @@ -641,3 +641,140 @@ def get_costs(db, weapon): create_cost["components"][item.name] = item.quantity costs = [create_cost] + costs return costs + + +class ItemStars(object): + """ + Get the game progress (in hub stars) required to make an item. Caches + values. + """ + + def __init__(self, db): + self.db = db + self._item_stars = {} # item id -> stars dict + self._weapon_stars = {} # weapon id -> stars dict + + def get_weapon_stars(self, weapon): + """ + Get lowest star levels needed to make weapon, among the different + paths available. + """ + stars = self._weapon_stars.get(weapon.id) + if stars is not None: + return stars + + stars = dict(Village=None, Guild=None, Permit=None, Arena=None) + costs = get_costs(self.db, weapon) + # find least 'expensive' path + for c in costs: + current_stars = self._get_component_stars(c) + for k, v in current_stars.iteritems(): + if v is None: + continue + if stars[k] is None or v < stars[k]: + stars[k] = v + self._weapon_stars[weapon.id] = stars + return stars + + def _get_component_stars(self, c): + # need to track unititialized vs unavailable + stars = dict(Village=0, Guild=0, Permit=0, Arena=0) + for item_name in c["components"].keys(): + item = self.db.get_item_by_name(item_name) + if item.type == "Materials": + current_stars = self.get_material_stars(item.id) + else: + current_stars = self.get_item_stars(item.id) + # keep track of most 'expensive' item + for k, v in current_stars.items(): + if stars[k] is None: + # another item was unavailable from the hub + continue + if v is None: + if k == "Village" and current_stars["Guild"] is not None: + # available from guild and not from village, + # e.g. certain HR parts. Mark entire item as + # unavailable from village, don't allow override. + stars[k] = None + continue + if v > stars[k]: + stars[k] = v + + # check for hubs that had no candidate item, and null them out + for k in list(stars.keys()): + if stars[k] == 0: + stars[k] = None + return stars + + def get_material_stars(self, material_item_id): + """ + Find the level of the cheapest item that satisfies the material + that is not a scrap. + """ + stars = self._item_stars.get(material_item_id) + if stars is not None: + return stars + + stars = dict(Village=None, Guild=None, Permit=None, Arena=None) + rows = self.db.get_material_items(material_item_id) + for row in rows: + item = self.db.get_item(row["item_id"]) + if "Scrap" in item.name: + continue + stars = self.get_item_stars(item.id) + break + self._item_stars[material_item_id] = stars + return stars + + def get_item_stars(self, item_id): + stars = self._item_stars.get(item_id) + if stars is not None: + return stars + + stars = dict(Village=None, Guild=None, Permit=None, Arena=None) + + quests = self.db.get_item_quests(item_id) + + gathering = self.db.get_item_gathering(item_id) + gather_locations = set() + for gather in gathering: + gather_locations.add((gather["location_id"], gather["rank"])) + for location_id, rank in list(gather_locations): + gather_quests = self.db.get_location_quests(location_id, rank) + quests.extend(gather_quests) + + monsters = self.db.get_item_monsters(item_id) + monster_ranks = set() + for monster in monsters: + monster_ranks.add((monster["monster_id"], monster["rank"])) + for monster_id, rank in list(monster_ranks): + monster_quests = self.db.get_monster_quests(monster_id, rank) + quests.extend(monster_quests) + + # find least expensive quest for getting the item + for quest in quests: + if quest.stars == 0: + # ignore training quests + if "Training" not in quest.name: + print "Error: non training quest has 0 stars", \ + quest.id, quest.name + continue + if quest.hub in stars: + current = stars[quest.hub] + if current is None or quest.stars < current: + stars[quest.hub] = quest.stars + else: + print "Error: unknown hub", quest.hub + + # if available guild or village, then null out permit/arena values, + # because they are more useful for filtering if limited to items + # exclusively available from permit or arena. Allows matching + # on based on meeting specified critera for + # (guild or village) and permit and arena. + if stars["Village"] or stars["Guild"]: + stars["Permit"] = None + stars["Arena"] = None + + self._item_stars[item_id] = stars + return stars + diff --git a/web/mhgen/recommends.html b/web/mhgen/recommends.html new file mode 100644 index 0000000..69c65ca --- /dev/null +++ b/web/mhgen/recommends.html @@ -0,0 +1,106 @@ + + + Poogie Recommends + + + + + + + + + + + + + +
+
+ + + + Understanding Results + (source) +
+
+ +
+ diff --git a/web/mhgen/weaponlist.html b/web/mhgen/weaponlist.html index 949fe07..54e0fc0 100644 --- a/web/mhgen/weaponlist.html +++ b/web/mhgen/weaponlist.html @@ -170,14 +170,27 @@ return { "weapon_type": $("#weapon_type").val(), "weapon_element": $("#weapon_element").val(), "weapon_final": $("#weapon_final").is(":checked"), - "weapon_name_text": $("#weapon_name_text").val() }; + "weapon_name_text": $("#weapon_name_text").val(), + "village_stars": $("#village_stars").val(), + "guild_stars": $("#guild_stars").val(), + "permit_stars": $("#permit_stars").val(), + "arena_stars": $("#arena_stars").val() }; } function load_state(state) { $("#weapon_type").val(state["weapon_type"]); $("#weapon_element").val(state["weapon_element"]); + final = state["weapon_final"]; + if (typeof final == "string") { + final = final.toLowerCase(); + state["weapon_final"] = (final == "true" || final == "1"); + } $("#weapon_final").prop("checked", state["weapon_final"]); $("#weapon_name_text").val(state["weapon_name_text"]); + $("#village_stars").val(state["village_stars"]); + $("#guild_stars").val(state["guild_stars"]); + $("#permit_stars").val(state["permit_stars"]); + $("#arena_stars").val(state["arena_stars"]); } function save_state(state, replace) { @@ -189,11 +202,55 @@ } } + function match_stars(match_value, weapon_value) { + // NOTE: a null weapon_value can be not available, or no data + // available (should probably fix this) + if (match_value == "Any") { + return true; + } + if (match_value == "None") { + // for None, allow null values, because null can be no requirements + // or no data available + if (weapon_value != null) { + return false; + } + return true; + } + // if matching a specific value, require a non-null weapon value + if (weapon_value == null) { + return false; + } + match_value = parseInt(match_value); + if (weapon_value > match_value) { + return false; + } + return true; + } + function weapon_predicate(state, weapon_data) { var weapon_type = state["weapon_type"]; var weapon_element = state["weapon_element"]; var final_only = state["weapon_final"]; var weapon_names = state["weapon_name_text"].split("|"); + var village_stars = state["village_stars"]; + var guild_stars = state["guild_stars"]; + var permit_stars = state["permit_stars"]; + var arena_stars = state["arena_stars"]; + + // allow satisfying quest filter with village or guild, since they + // involve essentially the same quests, rewards, and mosnters, + // but if permit or arena filters are set, they must be satisfied + // independently + if (! match_stars(village_stars, weapon_data["village_stars"]) + && ! match_stars(guild_stars, weapon_data["guild_stars"])) { + return false; + } + if (! match_stars(permit_stars, weapon_data["permit_stars"])) { + return false; + } + if (! match_stars(village_stars, weapon_data["village_stars"])) { + return false; + } if (final_only && weapon_data["final"] != 1) { return false; @@ -321,6 +378,55 @@ + + + + + + + + +