From 228c594ca9fcc452508b642c0ffa1486f9b8bf2a Mon Sep 17 00:00:00 2001 From: Bryce Allen Date: Sun, 31 Jul 2022 12:12:03 -0400 Subject: [PATCH] 4u stars filter, static damage gen, rise updates --- .gitignore | 4 + bin/mhdamage.py | 398 +- bin/mkjsonapi.py | 25 +- bin/parse-wikia-monsters.py | 37 +- db/mh4u.db | Bin 3407872 -> 3407872 bytes db/mhr/monster_hitboxes.json | 4408 + db/mhr/monster_list.json | 254 + db/mhr/weapon_list.json | 154311 +++++++++++++++ mhapi/damage.py | 54 +- mhapi/db.py | 91 +- mhapi/model.py | 275 +- mhapi/util.py | 12 + scrapers/fextralife-weapons.py | 268 + scrapers/mhrice_monsters.py | 159 + scrapers/mhrice_weapons.py | 352 + templates/damage/base.html | 36 + templates/damage/index.html | 67 + templates/damage/monster_damage.html | 312 + .../damage/monster_damage_by_rarity.html | 279 + web/img/Cut.png | Bin 0 -> 15316 bytes web/img/Impact.png | Bin 0 -> 1525 bytes web/img/Shot.png | Bin 0 -> 2038 bytes web/js/common.js | 12 +- web/mhr/weaponlist.html | 345 + web/mhr/weaponplanner.html | 284 + web/templates/weaponrow-rise.ejs | 63 + web/templates/weaponstats.ejs | 8 + 27 files changed, 161945 insertions(+), 109 deletions(-) create mode 100644 db/mhr/monster_hitboxes.json create mode 100644 db/mhr/monster_list.json create mode 100644 db/mhr/weapon_list.json create mode 100755 scrapers/fextralife-weapons.py create mode 100755 scrapers/mhrice_monsters.py create mode 100755 scrapers/mhrice_weapons.py create mode 100644 templates/damage/base.html create mode 100644 templates/damage/index.html create mode 100644 templates/damage/monster_damage.html create mode 100644 templates/damage/monster_damage_by_rarity.html create mode 100644 web/img/Cut.png create mode 100644 web/img/Impact.png create mode 100644 web/img/Shot.png create mode 100644 web/mhr/weaponlist.html create mode 100644 web/mhr/weaponplanner.html create mode 100644 web/templates/weaponrow-rise.ejs diff --git a/.gitignore b/.gitignore index b8b6a77..17753ab 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ README.html RECOMMENDATIONS.html tmp/ +*.bak +web/jsonapi/ +web/*/rewards/ +web/*/damage/ diff --git a/bin/mhdamage.py b/bin/mhdamage.py index b3968f3..b869632 100755 --- a/bin/mhdamage.py +++ b/bin/mhdamage.py @@ -4,14 +4,18 @@ import sys import argparse import shlex import copy +import codecs +import os +import os.path +from collections import defaultdict import _pathfix from mhapi.db import MHDB, MHDBX -from mhapi.damage import MotionValueDB, WeaponMonsterDamage +from mhapi.damage import MotionValueDB, WeaponMonsterDamage, WeaponType from mhapi.model import SharpnessLevel, Weapon, ItemStars from mhapi import skills -from mhapi.util import ELEMENTS, WEAPON_TYPES, WTYPE_ABBR +from mhapi.util import ELEMENTS, WEAPON_TYPES, WTYPE_ABBR, DAMAGE_TYPES def weapon_match_tuple(arg): @@ -65,7 +69,7 @@ def _make_db_sharpness_string(level_string): def weapon_stats_tuple(arg): parts = arg.split(",") - #print "parts %r" % parts + #print("parts %r" % parts) if len(parts) < 4: print("not enough parts") raise ValueError("Bad arg, use 'name,weapon_type,sharpness,raw'") @@ -160,8 +164,8 @@ def _add_skill_args(parser): type=int, choices=list(range(0, 5)), default=0, help="1-4 for CE+1, +2, +3 and Critical God") parser.add_argument("-e", "--element-up", - type=int, choices=list(range(0, 5)), default=0, - help="1-4 for (element) Atk +1, +2, +3 and" + type=int, choices=list(range(0, 6)), default=0, + help="1-5 for (element) Atk +1, +2, +3 and" " Element Attack Up") parser.add_argument("-t", "--artillery", type=int, choices=[0,1,2], default=0, @@ -206,6 +210,9 @@ def parse_args(argv): parser.add_argument("--mhw", "--monster-hunter-world", action="store_true", default=False, help="Adjusted attack, use MHWorld values") + parser.add_argument("--mhr", "--monster-hunter-rise", action="store_true", + default=False, + help="True attack, use MHRise values") parser.add_argument("-m", "--match", nargs="*", help="WEAPON_TYPE,ELEMENT_OR_STATUS_OR_RAW" +" Include all matching weapons in their final form." @@ -216,19 +223,27 @@ def parse_args(argv): +" Examples: 'Great Sword,Raw'" +" 'Sword and Shield,Para'" +" 'HH,Blast' 'Hammer'", - type=weapon_match_tuple, default=[]) + type=weapon_match_tuple, default=[], + action="append") parser.add_argument("-w", "--weapon-custom", nargs="*", help="NAME,WEAPON_TYPE,TRUE_RAW,AFFINITY,SHARPNESS" +"ELEMENT_TYPE,ELEMENT_ATTACK" +" Add weapon based on stats." +" Examples: 'DinoSnS,SnS,190,0,Blue,Fire,30'" +" 'AkantorHam,Hammer,240,25,Green'", - type=weapon_stats_tuple, default=[]) + type=weapon_stats_tuple, default=[], + action="append") parser.add_argument("-q", "--quest-level", help="village,guild[,permit[,arena]]", type=quest_level_tuple) - parser.add_argument("monster", - help="Full name of monster") + parser.add_argument("-r", "--rarity", + help="include weapons of given type with max rarity", + type=int, nargs="?") + parser.add_argument("--html-out", + help="Write table of values as HTML and save to path") + parser.add_argument("--html-site", + help="Write entire site of all monster & quest levels") + parser.add_argument("-n", "--monster", help="Full name of monster") parser.add_argument("weapon", nargs="*", help="One or more weapons of same class to compare," " full names") @@ -273,6 +288,99 @@ def _print_headers(parts, damage_map_base): print(" | ".join(cols)) +def write_damage_html(path, monster, monster_damage, quest_level, names, + damage_map_base, weapon_damage_map, parts, + part_max_damage, monster_breaks, monster_stars): + print(path) + def uniform_average(weapon): + return weapon_damage_map[weapon].averages["uniform"] + + names_sorted = list(names) + names_sorted.sort(key=uniform_average, reverse=True) + + from mako.lookup import TemplateLookup + from mako.runtime import Context + + tlookup = TemplateLookup(directories=["templates/damage"], + output_encoding="utf-8", + input_encoding="utf-8") + damage_template = tlookup.get_template("/monster_damage.html") + + wtype = damage_map_base.weapon.wtype + weapon_damage_type = WeaponType.damage_type(wtype) + damage_types = list(DAMAGE_TYPES) + + weapon_types = list(WEAPON_TYPES) + weapon_types.remove("Bow") + weapon_types.remove("Light Bowgun") + weapon_types.remove("Heavy Bowgun") + + with codecs.open(path, "w", "utf8") as f: + template_args = dict( + monster=monster.name, + monster_damage=monster_damage, + damage_types=DAMAGE_TYPES, + weapon_types=weapon_types, + weapon_type=wtype, + weapon_damage_type=weapon_damage_type, + village_stars=quest_level[0], + guild_stars=quest_level[1], + part_names=parts, + part_max_damage=part_max_damage, + weapon_names=names_sorted, + weapon_damage_map=weapon_damage_map, + monster_breaks=set(monster_breaks), + monster_stars=monster_stars + ) + ctx = Context(f, **template_args) + damage_template.render_context(ctx) + + +def write_damage_html_by_rarity(path, rarity, monster, monster_damage, names, + damage_map_base, weapon_damage_map, parts, + part_max_damage): + print(path) + def uniform_average(weapon): + return weapon_damage_map[weapon].averages["uniform"] + + names_sorted = list(names) + names_sorted.sort(key=uniform_average, reverse=True) + + from mako.lookup import TemplateLookup + from mako.runtime import Context + + tlookup = TemplateLookup(directories=["templates/damage"], + output_encoding="utf-8", + input_encoding="utf-8") + damage_template = tlookup.get_template("/monster_damage_by_rarity.html") + + wtype = damage_map_base.weapon.wtype + weapon_damage_type = WeaponType.damage_type(wtype) + damage_types = list(DAMAGE_TYPES) + + weapon_types = list(WEAPON_TYPES) + weapon_types.remove("Bow") + weapon_types.remove("Light Bowgun") + weapon_types.remove("Heavy Bowgun") + + with codecs.open(path, "w", "utf8") as f: + template_args = dict( + monster=monster.name, + monster_damage=monster_damage, + rarity=rarity, + damage_types=DAMAGE_TYPES, + weapon_types=weapon_types, + weapon_type=wtype, + weapon_damage_type=weapon_damage_type, + part_names=parts, + part_max_damage=part_max_damage, + weapon_names=names_sorted, + weapon_damage_map=weapon_damage_map, + ) + ctx = Context(f, **template_args) + damage_template.render_context(ctx) + + def print_sorted_damage(names, damage_map_base, weapon_damage_map, parts): def uniform_average(weapon): return weapon_damage_map[weapon].averages["uniform"] @@ -296,7 +404,8 @@ def print_sorted_damage(names, damage_map_base, weapon_damage_map, parts): print("% 2d" % part_damage.average(), end=' ') print() - if len(names) > 1: + # this is super buggy + if False and len(names) > 1: w1 = weapon_damage_map[names_sorted[0]] w2 = weapon_damage_map[names_sorted[1]] m, ratio = w1.compare_break_even(w2) @@ -387,33 +496,19 @@ def match_quest_level(match_level, weapon_level): return True -def main(): - args = parse_args(None) - - game_uses_true_raw = False - if args.quest_level: - comps = True - else: - comps = False - - if args.monster_hunter_cross: - db = MHDBX() - game_uses_true_raw = True - elif args.monster_hunter_gen: - db = MHDB(game="gu", include_item_components=comps) - game_uses_true_raw = True - elif args.mhw: - db = MHDBX(game="mhw") - game_uses_true_raw = False - else: - db = MHDB(game="4u", include_item_components=comps) - motiondb = MotionValueDB(_pathfix.motion_values_path) - +def run_comparison(args, db, motiondb, game_uses_true_raw, item_stars=None): monster = db.get_monster_by_name(args.monster) if not monster: raise ValueError("Monster '%s' not found" % args.monster) monster_damage = db.get_monster_damage(monster.id) + if not monster_damage.is_valid(): + print("WARN: invalid damage data for monster '%s'" % args.monster) + return + + if item_stars is None: + item_stars = ItemStars(db) + weapons = [] weapon_type = None names_set = set() @@ -430,10 +525,14 @@ def main(): if skill_args: skill_args_map[name] = skill_args + #print("args match", args.match) for match_tuple in args.match: # TODO: better validation + if isinstance(match_tuple, list): + # TODO: is this a bug in argparse!!!???? + match_tuple = match_tuple[0] wtype, element = match_tuple - if args.quest_level: + if args.quest_level or args.rarity: final=None else: final=1 @@ -448,7 +547,7 @@ def main(): names_set.add(w.name) if args.weapon_custom: - weapons.extend(args.weapon_custom) + weapons.extend([w[0] for w in args.weapon_custom]) if not weapons: print("Err: no matching weapons") @@ -457,6 +556,29 @@ def main(): names = [w.name for w in weapons] monster_breaks = db.get_monster_breaks(monster.id) + part_names = monster_damage.keys() + + for i in range(len(monster_breaks)): + if monster_breaks[i] not in monster_damage: + plural = monster_breaks[i] + "s" + if plural in monster_damage: + monster_breaks[i] = plural + + states = monster_damage.state_names() + print("States:", states) + print("%-20s" % monster.name, " | ".join(monster_damage.keys())) + for dtype in DAMAGE_TYPES: + print(dtype[:2], ":", end=" ") + for part_name, part_damage in monster_damage.items(): + if part_damage.state_diff(dtype): + print("% 2d (% 2d)" + % (part_damage[dtype], + part_damage.get_alt_state(dtype)), + end=" ") + else: + print("% 2d" % part_damage[dtype], end=" ") + print() + weapon_type = weapons[0]["wtype"] if args.phial and weapon_type != "Charge Blade": print("ERROR: phial option is only supported for Charge Blade") @@ -477,7 +599,6 @@ def main(): limit_parts = None if args.quest_level: - item_stars = ItemStars(db) village, guild, permit, arena = args.quest_level print("Filter by Quest Levels:", args.quest_level) weapons2 = dict() @@ -504,6 +625,48 @@ def main(): weapons = list(weapons2.values()) names = [w.name for w in weapons] + if args.rarity: + print("Filter by max rarity:", args.rarity) + weapons2 = dict() + by_name = dict() + for w in weapons: + if w.rarity <= args.rarity: + weapons2[w.id] = w + by_name[w.name] = w + # TODO: don't have parent ids for mhrise yet + if False and args.mhr: + # hack to remove most common dups + for wname in list(by_name.keys()): + if wname.endswith("+"): + base = wname[:-1] + suffix = "+" + else: + parts = wname.rsplit(" ", maxsplit=1) + if len(parts) == 1: + continue + base, suffix = parts + parent_name = None + if suffix == "+" or base.endswith("+"): + parent_name = base.rstrip("+") + elif suffix in ("II", "III", "VI", "VII"): + parent_name = wname[:-1] + elif suffix == "IV": + parent_name = base + " III" + elif suffix == "V": + parent_name = base + " IV" + if parent_name: + print("parent", parent_name) + if parent_name in by_name: + del weapons2[by_name[parent_name].id] + else: + parent_ids = set(w.parent_id for w in weapons2.values()) + for wid in list(weapons2.keys()): + if wid in parent_ids: + del weapons2[wid] + weapons = list(weapons2.values()) + names = [w.name for w in weapons] + + part_max_damage = defaultdict(int) weapon_damage_map = dict() for row in weapons: name = row["name"] @@ -512,6 +675,7 @@ def main(): raise ValueError( "Weapon '%s' is different type, got '%s' expected '%s'" % (name, row_type, weapon_type)) + #print(name, row) try: skill_args = skill_args_map.get(name, args) wd = WeaponMonsterDamage(row, @@ -526,7 +690,8 @@ def main(): limit_parts=args.parts, frenzy_bonus=skill_args.frenzy, is_true_attack=game_uses_true_raw, - blunt_power=skill_args.blunt_power) + blunt_power=skill_args.blunt_power, + game=db.game) print("%-20s: %4.0f %2.0f%%" % (name, wd.attack, wd.affinity), end=' ') if wd.etype: if wd.etype2: @@ -534,13 +699,16 @@ def main(): % (wd.eattack, wd.etype, wd.eattack2, wd.etype2), end=' ') else: print("(%4.0f %s)" % (wd.eattack, wd.etype), end=' ') - print(SharpnessLevel.name(wd.sharpness), end=' ') + print(SharpnessLevel.name(wd.sharpness), wd.sharpness_points, end=' ') if skill_args != args: print("{%s}" % ",".join(sn for sn in get_skill_names(skill_args) if sn)) else: print() + for part in wd.parts: + if wd[part].average() > part_max_damage[part]: + part_max_damage[part] = wd[part].average() weapon_damage_map[name] = wd except ValueError as e: print(str(e)) @@ -564,6 +732,162 @@ def main(): print_sorted_damage(names, damage_map_base, weapon_damage_map, parts) + if args.html_out: + if args.mhr: + if not args.rarity: + print("Error: --html-out with --mrh requires --rarity") + sys.exit(1) + write_damage_html_by_rarity(args.html_out, args.rarity, + monster, monster_damage, + names, damage_map_base, + weapon_damage_map, parts, + part_max_damage) + else: + if not args.quest_level: + print("Error: --html-out requires quest level (-q)") + sys.exit(1) + monster_stars = item_stars.get_monster_stars(monster.id) + write_damage_html(args.html_out, monster, monster_damage, + args.quest_level, names, + damage_map_base, weapon_damage_map, parts, + part_max_damage, monster_breaks, + monster_stars) + + +def write_html_site(args, db, motiondb, game_uses_true_raw): + if db.game == "4u": + village_max = 5 + guild_max = 2 + else: + raise ValueError("Not implemented") + + monsters = db.get_monsters("Boss") + monster_names = [monster.name for monster in monsters] + weapon_types = db.get_weapon_types() + + item_stars = ItemStars(db) + monster_stars = {} + for monster in monsters: + monster_stars[monster.id] = item_stars.get_monster_stars(monster.id) + + n = 0 + base_dir = args.html_site + for monster in monsters: + stars = monster_stars[monster.id] + if stars["Village"] is None: + vrange = [0] + else: + if stars["Village"] > 2: + start = stars["Village"] - 1 + else: + start = stars["Village"] + vrange = list(range(start, 11)) + if start > 2: + vrange = [0] + vrange + if stars["Guild"] is None: + grange = [0] + else: + if stars["Guild"] > 1: + start = stars["Guild"] - 1 + else: + start = stars["Guild"] + grange = list(range(start, 11)) + if start > 1: + grange = [0] + grange + for v in vrange: + for g in grange: + if v == 0: + if g == 0: + continue + v = 1 + if g == 0: + g = 1 + quest_dir = "v{}g{}".format(v, g) + quest_path = os.path.join(base_dir, monster.name, quest_dir) + if not os.path.isdir(quest_path): + os.makedirs(quest_path) + for wtype in weapon_types: + if "Bowgun" in wtype or wtype == "Bow": + continue + args.html_out = os.path.join(quest_path, wtype + ".html") + if os.path.isfile(args.html_out): + print(args.html_out, " exists, skipping") + continue + args.html_site = None + n += 1 + args.monster = monster.name + args.quest_level = (v if v else 1, g if g else 1, + None, None) + args.match = [(wtype, None)] + run_comparison(args, db, motiondb, game_uses_true_raw, + item_stars=item_stars) + print("n =", n) + + +def write_html_site_rise(args, db, motiondb, game_uses_true_raw=True): + monsters = db.get_monsters() + weapon_types = db.get_weapon_types() + + n = 0 + base_dir = args.html_site + for monster in monsters: + for rarity in range(1, 11): + args.rarity = rarity + rarity_dir = "r{}".format(rarity) + mpath = os.path.join(base_dir, monster.name, rarity_dir) + if not os.path.isdir(mpath): + os.makedirs(mpath) + for wtype in weapon_types: + if "Bowgun" in wtype or wtype == "Bow": + continue + args.html_out = os.path.join(mpath, wtype + ".html") + if os.path.isfile(args.html_out): + print(args.html_out, " exists, skipping") + continue + args.html_site = None + n += 1 + args.monster = monster.name + args.match = [(wtype, None)] + print(rarity, wtype) + run_comparison(args, db, motiondb, game_uses_true_raw) + print("n =", n) + + +def main(): + args = parse_args(None) + + game_uses_true_raw = False + if args.quest_level or args.html_site: + comps = True + else: + comps = False + + if args.monster_hunter_cross: + db = MHDBX() + game_uses_true_raw = True + elif args.monster_hunter_gen: + db = MHDB(game="gu", include_item_components=comps) + game_uses_true_raw = True + elif args.mhw: + db = MHDBX(game="mhw") + game_uses_true_raw = False + SharpnessLevel._modifier = SharpnessLevel._modifier_mhw + elif args.mhr: + db = MHDBX(game="mhr") + game_uses_true_raw = True + SharpnessLevel._modifier = SharpnessLevel._modifier_mhw + else: + db = MHDB(game="4u", include_item_components=comps) + motiondb = MotionValueDB(_pathfix.motion_values_path) + + if args.html_site: + if args.mhr: + write_html_site_rise(args, db, motiondb, game_uses_true_raw) + else: + write_html_site(args, db, motiondb, game_uses_true_raw) + else: + run_comparison(args, db, motiondb, game_uses_true_raw) + if __name__ == '__main__': main() diff --git a/bin/mkjsonapi.py b/bin/mkjsonapi.py index 912cae5..4cc36db 100755 --- a/bin/mkjsonapi.py +++ b/bin/mkjsonapi.py @@ -9,7 +9,7 @@ import argparse import _pathfix -from mhapi.db import MHDB +from mhapi.db import MHDB, MHDBX from mhapi import model ENTITIES = """item weapon monster armor @@ -202,11 +202,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"] + if db.game == "4u": + 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) @@ -274,10 +275,13 @@ def horn_melody_json(db, path): def main(): args = parse_args() - db = MHDB(game=args.game, include_item_components=True) + if args.game in ("mhx", "mhr"): + db = MHDBX(game=args.game) + else: + db = MHDB(game=args.game, include_item_components=True) if not args.outpath: - args.outpath = os.path.join(_pathfix.web_path, "jsonapi") + args.outpath = os.path.join(_pathfix.web_path, "jsonapi", args.game) if args.entities: for entity in args.entities: @@ -288,7 +292,10 @@ def main(): args.entities = ENTITIES if db.game != "4u": - args.entities.remove("wyporium") + try: + args.entities.remove("wyporium") + except: + pass for entity in args.entities: fn = globals()["%s_json" % entity] diff --git a/bin/parse-wikia-monsters.py b/bin/parse-wikia-monsters.py index 0a2f324..6c2368b 100755 --- a/bin/parse-wikia-monsters.py +++ b/bin/parse-wikia-monsters.py @@ -18,6 +18,7 @@ Returns list of dict, e.g.: import sys import re import json +import lxml.etree import requests @@ -34,14 +35,27 @@ MONSTER_RE = re.compile( '(?:)?\s*' ']* title="([^"]*)"') +# Old, MHX +""" MONSTER_LINK_RE = re.compile( '') + JAPANESE_NAME_STR = '

Japanese:

' JAPANESE_NAME_RE = re.compile( '
(.*)
') +""" +MONSTER_LINK_RE = re.compile( + '
([^<>]+)') + +""" +

ドスフロギィ
(Dosufurogi)

+""" +JAPANESE_NAME_RE = re.compile('

([^<>]*)
.*

') +JAPANESE_TITLE_RE = re.compile( + '
([^<>]*)
') def parse_wikia_monsters(f): section = None @@ -72,20 +86,17 @@ def parse_wikia_monsters(f): def get_jp_names(monster_path): url = "http://monsterhunter.wikia.com" + monster_path r = requests.get(url) - lines = r.text.split("\n") + root = lxml.etree.HTML(r.text) + names = [] - while lines: - line = lines.pop(0).strip() - if JAPANESE_NAME_STR not in line: - continue - line = lines.pop(0).strip() - while line == "": - line = lines.pop(0).strip() - m = JAPANESE_NAME_RE.match(line) - assert m, "No match: " + line - names.append(parse_japanese_name(m.group(1))) - if len(names) == 2: - break + + rbs = root.xpath('//h2[@data-source="Japanese Name"]//rb') + names.append(rbs[0].text) + + divs = root.xpath('//div[@data-source="Japanese Title"]//div') + if divs: + names.append(divs[0].text) + return names diff --git a/db/mh4u.db b/db/mh4u.db index dd8053a8a39d56d92853b9d96cbc43e16f872653..eda4ab96682c42ce7748a30efd4a628a7fae5d69 100644 GIT binary patch delta 614 zcmY+fpf*w^|$y#Idsy1S1lNlcVS&T}m9fxjG8*A8jdp@LRs`|9$U0-~UEFyi~NbJ2oF% zVzY}o_3|@=4uZB3whaOf0yKz05A=c#ebCMI!xLof_yK|%sXAPJ*j!8uSc z2IG)|G-SYr3CP0vM%!>E>eu;0rdW}QlK3Is3ZFe^MR8N41@Qy^iNE14ceqZ!=quIf z6+Nb9xMzQI9M3F`D-^twW-N6?s=pSH|pyN}x=dEpwCM+ps zR(3Lhfo}S^Nd?z?w4Pb0tZpfVgiOhf9rPh$pG0ogs+-&YGM1GxP17hEKZEerx^Z&<3f}W#sy?dx|r{a}wEnIsV?YEq%sMK=&51$>a zBz4|oVqd%yp;#6n04uUbSLihD)~f53&CL9U_XMvrV)(2bu5>fe p_t#EBM=={SWGB?rBwTixo`{7Ctk^OlC3uhhOtEp5k%NmA}fJa;|JDGfIG->49$PI~~(Enx_b< z?1}wmKiLJ__ku$76n~WDJ;kLPXk`?m`Xtxv?VWJcvJdU8Ogh%vWe6dLM0L#7ywj32 z7U~<*#6pBbHSV^cFk25$ayh%Vdk|jDY~>mq!wBiRZs=9%`n^I~ij8_l2>q=y>Nca3 zXml2`89R5-XiOy%Z;i0)dw;XPHkAC5vD1yarPaA3&nn4v&nZjCOO<8x?w3?kK_6FZ s|8>Lj{3{>l>TSF8qU2R`1V$kWF&Kk5j6(t@U=pU_6HFIXbLREme=qo 0 or self.breakable + return (self.breakable or self.raw != self.break_raw + or self.element != self.break_element) def average(self, break_weight=0.25, rage_weight=0.5): if self.break_diff(): @@ -688,7 +717,8 @@ class PartDamage(object): + self.total * (1 - rage_weight)) def set_damage(self, raw, element, hitbox, ehitbox, state=None): - if state == "Without Hide": + if state in ("Without Hide", "Charged", "Tail Inflated", "Savaged", + "Enraged", "Ice Shield"): state = "Break Part" self.states[state] = PartDamageState(raw, element, hitbox, ehitbox, state) diff --git a/mhapi/db.py b/mhapi/db.py index 5af63a3..993a523 100644 --- a/mhapi/db.py +++ b/mhapi/db.py @@ -7,6 +7,7 @@ import sqlite3 import json from mhapi import model +from mhapi.util import WEAPON_TYPES def field_model(key): @@ -257,10 +258,15 @@ class MHDB(object): return self._query_all("search_item", query, tuple(args), no_cache=True, model_cls=model.Item) - def get_monsters(self): - return self._query_all("monsters", """ - SELECT * FROM monsters - """, model_cls=model.Monster) + def get_monsters(self, monster_class=None): + args = [] + where = [] + if monster_class is not None: + where.append("WHERE class = ?") + args.append(monster_class) + args = tuple(args) + q = "SELECT * FROM monsters " + "\n".join(where) + return self._query_all("monsters", q, args, model_cls=model.Monster) def get_monster_names(self): """ @@ -317,12 +323,17 @@ class MHDB(object): WHERE quest_id=? """, (quest_id,)) - def get_monster_quests(self, monster_id, rank): - return self._query_all("monster_quests", """ - SELECT DISTINCT quests.* FROM quests, monster_to_quest - WHERE monster_to_quest.quest_id = quests._id - AND monster_to_quest.monster_id=? AND rank=? - """, (monster_id, rank), model_cls=model.Quest) + def get_monster_quests(self, monster_id, rank=None): + + query = """SELECT DISTINCT quests.* FROM quests, monster_to_quest + WHERE monster_to_quest.quest_id = quests._id + AND monster_to_quest.monster_id=?""" + args = [monster_id] + if rank is not None: + query += " AND rank=?" + args += [rank] + return self._query_all("monster_quests", query, + tuple(args), model_cls=model.Quest) def get_item_quests(self, item_id): """ @@ -381,6 +392,14 @@ class MHDB(object): WHERE monster_id=? """, (monster_id,), collection_cls=model.MonsterDamage) + def get_weapon_types(self): + """ + List of strings. + """ + return self._query_all("weapon_types", """ + SELECT DISTINCT wtype FROM weapons + """, model_cls=field_model("wtype")) + def get_weapons(self): # Note: weapons only available via JP DLC have no localized # name, filter them out. @@ -613,6 +632,13 @@ class MHDB(object): item_data.set_components(ccomps, ucomps) +def _get_rise_num_slots(slot_list): + num_slots = 0 + nslots = len(slot_list) + for i in range(nslots - 1, -1, -1): + num_slots += 10**(nslots - i - 1) * slot_list[i] + return num_slots + class MHDBX(object): """ @@ -626,12 +652,14 @@ class MHDBX(object): """ Loads JSON data, keeps in memory. """ + self.game = game module_path = os.path.dirname(__file__) self._mhx_db_path = os.path.abspath(os.path.join(module_path, "..", "db", game)) - self._4udb = MHDB() + self._4udb = MHDB(game="gu") self._weapon_list = [] self._weapons_by_name = {} + self._weapons_by_id = {} self._monsters_by_name = {} self._monster_damage = {} @@ -644,10 +672,23 @@ class MHDBX(object): with open(os.path.join(self._mhx_db_path, "weapon_list.json")) as f: wlist = json.load(f) for i, wdata in enumerate(wlist): - wdata["_id"] = i + if "_id" not in wdata: + wdata["_id"] = i + keys = ["awaken", "horn_notes", + "element", "element_attack", + "element_2", "element_2_attack", + "bug_level", "phial", "phial_value", + "shelling_type", "shelling_level", + "buy"] + for k in keys: + if k not in wdata: + wdata[k] = None + if self.game == "mhr": + wdata["num_slots"] = _get_rise_num_slots(wdata["slots"]) weapon = model.Weapon(wdata) self._weapon_list.append(weapon) self._weapons_by_name[weapon.name_jp] = weapon + self._weapons_by_id[weapon.id] = weapon def _load_monsters(self): names_path = os.path.join(self._mhx_db_path, @@ -675,7 +716,26 @@ class MHDBX(object): row["monster_id"] = mid damage_rows.append(row) self._monster_damage[mid] = model.MonsterDamage(damage_rows) - self._monster_breaks[mid] = damage["_breaks"] + self._monster_breaks[mid] = damage.get("_breaks", []) + + def get_monsters(self): + return list(self._monsters_by_name.values()) + + def get_weapons(self): + return list(self._weapon_list) + + def get_weapon(self, weapon_id): + return self._weapons_by_id[weapon_id] + + def get_weapons_by_parent(self, parent_id): + result = [] + for w in self._weapon_list: + if w.parent_id == parent_id: + result.append(w) + return result + + def get_weapon_types(self): + return WEAPON_TYPES def get_weapon_by_name(self, name): return self._weapons_by_name.get(name) @@ -705,9 +765,10 @@ class MHDBX(object): with no element. Otherwise @element is searched for in both awaken and native, and can be a status or an element. - @final should be string '1' or '0' + @final None or string '1' or '0' """ - final = int(final) + if final is not None: + final = int(final) results = [] for w in self._weapon_list: if wtype is not None and w.wtype != wtype: diff --git a/mhapi/model.py b/mhapi/model.py index 4905390..058c836 100644 --- a/mhapi/model.py +++ b/mhapi/model.py @@ -1,8 +1,11 @@ +import os import string import json +from typing import NamedTuple import urllib.request, urllib.parse, urllib.error import re import difflib +from collections import namedtuple from mhapi.util import EnumBase @@ -162,7 +165,7 @@ class SharpnessLevel(EnumBase): PURPLE: (1.44, 1.20), } - # for mhx, mhgen, mhxx, and likely mhw + # for mhx, mhgen, mhxx _modifier_mhx = { RED: (0.50, 0.25), ORANGE: (0.75, 0.50), @@ -172,14 +175,48 @@ class SharpnessLevel(EnumBase): WHITE: (1.32, 1.125), } + # world, rise + # https://twitter.com/AsteriskAmpers1/status/1372886666940137479 + # https://monsterhunterworld.wiki.fextralife.com/Sharpness + # https://monsterhunterrise.wiki.fextralife.com/Sharpness + _modifier_mhw = { + RED: (0.50, 0.25), + ORANGE: (0.75, 0.50), + YELLOW: (1.00, 0.75), + GREEN: (1.05, 1.00), + BLUE: (1.20, 1.0625), + WHITE: (1.32, 1.15), + PURPLE: (1.39, 1.25), + } @classmethod def raw_modifier(cls, sharpness): - return cls._modifier[sharpness][0] + if _game() == "4u": + d = cls._modifier + elif _game() in ["gen", "gu", "mhx"]: + d = cls._modifier_mhx + else: + d = cls._modifier_mhw + return d[sharpness][0] @classmethod def element_modifier(cls, sharpness): - return cls._modifier[sharpness][1] + if _game() == "4u": + d = cls._modifier + elif _game() in ["gen", "gu", "mhx"]: + d = cls._modifier_mhx + else: + d = cls._modifier_mhw + return d[sharpness][1] + +_GAME = None + +def _game(): + global _GAME + if _GAME is None: + _GAME = os.environ.get("MHAPI_GAME", "4u") + assert _GAME in ("4u", "gen", "gu", "mhx", "mhw", "mhr") + return _GAME class WeaponSharpness(ModelBase): @@ -195,32 +232,54 @@ class WeaponSharpness(ModelBase): self.value_list = [int(s) for s in db_string_or_list.split(".")] # For MHX, Gen, no purple sharpness, but keep model the same for # simplicity - if len(self.value_list) < SharpnessLevel.PURPLE + 1: + while len(self.value_list) < SharpnessLevel.PURPLE + 1: self.value_list.append(0) self._max = None @property def max(self): if self._max is None: - self._max = SharpnessLevel.RED - for i in range(SharpnessLevel.PURPLE+1): - if self.value_list[i] == 0: - break - else: + for i in range(SharpnessLevel.PURPLE, -1, -1): + if self.value_list[i] > 0: self._max = i + break return self._max + def get_max_points(self): + return (self.max, self.value_list[self.max]) + + def get_rise_handicraft(self, n): + """In Rise, there are 5 levels of Handicraft, each give 5 extra points; this + can be used to subtract off from sharpness_plus row""" + alt_values = list(self.value_list) + assert n >= 0 and n <= 5 + minus_points = 25 - n * 5 + for i in range(SharpnessLevel.PURPLE, -1, -1): + val = alt_values[i] + if minus_points <= val: + alt_values[i] -= minus_points + break + else: + alt_values[i] = 0 + minus_points -= val + return WeaponSharpness(alt_values) + def as_data(self): return self.value_list + def __str__(self): + return ",".join(str(v) for v in self.value_list) + class ItemCraftable(RowModel): _list_fields = ["id", "name"] def __init__(self, item_row): super(ItemCraftable, self).__init__(item_row) - self.create_components = None - self.upgrade_components = None + if "create_components" not in item_row: + self.create_components = None + if "upgrade_components" not in item_row: + self.upgrade_components = None def set_components(self, create_components, upgrade_components): self.create_components = create_components @@ -230,10 +289,10 @@ class ItemCraftable(RowModel): data = super(ItemCraftable, self).as_data() if self.create_components is not None: data["create_components"] = dict((item.name, item.quantity) - for item in self.create_components) + for item in _item_list(self.create_components)) if self.upgrade_components is not None: data["upgrade_components"] = dict((item.name, item.quantity) - for item in self.upgrade_components) + for item in _item_list(self.upgrade_components)) return data @@ -373,6 +432,8 @@ class Weapon(ItemCraftable): def __init__(self, weapon_item_row): super(Weapon, self).__init__(weapon_item_row) self._parse_sharpness() + if "name_jp" not in self._data: + self._data["name_jp"] = self._data["name"] def _parse_sharpness(self): """ @@ -385,11 +446,12 @@ class Weapon(ItemCraftable): if isinstance(self._row["sharpness"], list): # MHX JSON data, already desired format, but doesn't have # purple so we append 0 - self.sharpness = WeaponSharpness(self._row["sharpness"] + [0]) - self.sharpness_plus = WeaponSharpness( - self._row["sharpness_plus"] + [0]) - self.sharpness_plus2 = WeaponSharpness( - self._row["sharpness_plus2"] + [0]) + row_sharpness = self._row["sharpness"] + row_sharpness_plus = self._row.get("sharpness_plus", row_sharpness) + row_sharpness_plus2 = self._row.get("sharpness_plus2", row_sharpness) + self.sharpness = WeaponSharpness(row_sharpness) + self.sharpness_plus = WeaponSharpness(row_sharpness_plus) + self.sharpness_plus2 = WeaponSharpness(row_sharpness_plus2) else: # 4U or gen data from db parts = self._row["sharpness"].split(" ") @@ -410,6 +472,10 @@ class Weapon(ItemCraftable): # english weapons, and not for Japanese DLC weapons. return ord(self.name[0]) < 128 + @property + def sharpness_name(self): + return SharpnessLevel.name(self.sharpness) + class Monster(RowModel): _list_fields = ["id", "class", "name"] @@ -459,6 +525,12 @@ class MonsterPartStateDamage(RowModel): def __ne__(self, other): return not self.__eq__(other) + def __str__(self): + return str(self.as_data()) + + def __repr__(self): + return repr(self.as_data()) + class MonsterPartDamage(ModelBase): """ @@ -493,6 +565,49 @@ class MonsterPartDamage(ModelBase): damage=self.states ) + def __str__(self): + return str(self.as_data()) + + def __repr__(self): + return repr(self.as_data()) + + def state_names(self): + return list(self.states.keys()) + + def get(self, damage_type): + return self.get_state(damage_type) + + def get_break(self, damage_type): + self.get_state(damage_type, "Break Part") + + def get_alt_state(self, damage_type): + return self.get_state(damage_type, self.alt_state) + + def get_state(self, damage_type, state="Default"): + if state not in self.states: + state = "Default" + return self.states[state][damage_type] + + def __getitem__(self, damage_type): + return self.get(damage_type) + + @property + def alt_state(self): + if "Break Part" in self.states: + return "Break Part" + elif len(self.states) > 1: + alt_states = list(self.states.keys()) + if "Default" in alt_states: + alt_states.remove("Default") + return alt_states[0] + else: + return "Default" + + def state_diff(self, damage_type, state=None): + if state is None: + state = self.alt_state + return self.get_state(damage_type, state) - self.get(damage_type) + class MonsterDamage(ModelBase): """ @@ -518,6 +633,10 @@ class MonsterDamage(ModelBase): self.parts[part] = MonsterPartDamage(part) self.parts[part].add_state(state, row) + def is_valid(self): + # TODO: more validation + return (len(self.states) > 0 and len(self.parts) > 0) + def as_data(self): return dict( states=list(self.states), @@ -534,6 +653,49 @@ class MonsterDamage(ModelBase): #print "part %s is breakable [by rewards]" % name part_damage.breakable = True + def state_names(self): + names = list(self.states) + names.sort() + if "Default" in names: + names.remove("Default") + names.insert(0, "Default") + return names + + def keys(self): + return self.parts.keys() + + def values(self): + return self.parts.values() + + def items(self): + return self.parts.items() + + def __len__(self): + return len(self.parts) + + def __iter__(self): + return iter(self.parts) + + @property + def alt_state(self): + alt_states = set(self.states) + alt_states.remove("Default") + if alt_states: + return alt_states.pop() + return "Default" + + def avg(self, damage_type, state="Default"): + return sum(part.get_state(damage_type, state) + for part in self.values()) / len(self) + + def alt_avg(self, damage_type): + return sum(part.get_alt_state(damage_type) + for part in self.values()) / len(self) + + def max(self, damage_type, state="Default"): + return max(part.get_state(damage_type, state) + for part in self.values()) + def get_decoration_values(skill_id, decorations): """ @@ -622,7 +784,7 @@ def get_costs(db, weapon): for cost in costs: cost["zenny"] += upgrade_cost cost["path"] += [weapon] - for item in weapon.upgrade_components: + for item in _item_list(weapon.upgrade_components): if item.type == "Weapon": continue if item.name not in cost["components"]: @@ -638,7 +800,7 @@ def get_costs(db, weapon): create_cost = dict(zenny=zenny, path=[weapon], components={}) - for item in weapon.create_components: + for item in _item_list(weapon.create_components): create_cost["components"][item.name] = item.quantity costs = [create_cost] + costs if weapon.buy: @@ -649,16 +811,48 @@ def get_costs(db, weapon): return costs +CompItem = namedtuple("CompItem", "name quantity type") +def _item_list(comps): + if comps is None or isinstance(comps, list): + return comps + elif isinstance(comps, dict): + items = [] + for k, v in comps.items(): + items.append(CompItem(k, v, "material")) + return items + else: + raise ValueError("Unknown component type") + + +def rank_quest_level(game, hub, rank): + if game != "4u": + raise NotImplementedError() + if hub == "Village": + if rank == "G": + return 10 + elif rank == "HR": + return 7 + else: + return 1 + elif hub == "Guild": + if rank == "G": + return 8 + elif rank == "HR": + return 4 + else: + return 1 + + 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 + self._monster_stars = {} # monster id -> stars dict self._wyporium_trades = {} if self.db.game == "4u": @@ -804,7 +998,26 @@ class ItemStars(object): else: print("Error: unknown hub", quest.hub) - # if available guild or village, then null out permit/arena values, + if stars["Village"] is None and stars["Guild"] is None: + # not available from quests or gathering, may be an + # Everwood only monster (4u). Set stars based on rank. Note + # that this is imperfect, because some monsters have special + # quests and unlock them appearing in the Everwood, but at least + # will prevent totally broken G-rank weapons showing up in + # low rank comparisons. + min_village = 10 + min_guild = 10 + for _, rank in monster_ranks: + v = rank_quest_level(self.db.game, "Village", rank) + if v < min_village: + min_village = v + g = rank_quest_level(self.db.game, "Guild", rank) + if g < min_guild: + min_guild = g + stars["Village"] = min_village + stars["Guild"] = min_guild + + # 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 @@ -816,3 +1029,21 @@ class ItemStars(object): self._item_stars[item_id] = stars return stars + def get_monster_stars(self, monster_id): + stars = self._monster_stars.get(monster_id) + if stars is not None: + return stars + + stars = dict(Village=None, Guild=None, Permit=None, Arena=None, + Event=None) + + quests = self.db.get_monster_quests(monster_id) + + for quest in quests: + if quest.hub == "Caravan": + quest.hub = "Village" + if stars[quest.hub] is None or quest.stars < stars[quest.hub]: + stars[quest.hub] = quest.stars + + self._monster_stars[monster_id] = stars + return stars diff --git a/mhapi/util.py b/mhapi/util.py index 7034188..19fda90 100644 --- a/mhapi/util.py +++ b/mhapi/util.py @@ -53,6 +53,18 @@ WTYPE_ABBR = dict( ) +DAMAGE_TYPES = """ + cut + impact + shot + fire + water + thunder + ice + dragon +""".split() + + class EnumBase(object): _names = dict() diff --git a/scrapers/fextralife-weapons.py b/scrapers/fextralife-weapons.py new file mode 100755 index 0000000..0102055 --- /dev/null +++ b/scrapers/fextralife-weapons.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 + +import os.path +import sys +import re +import json +import lxml.etree + +import requests + +#WTYPES = ["Great Sword", "Long Sword", "Sword and Shield", "Dual Blades", "Lance", "Gunlance", "Hammer"] +WTYPES = ["Great Sword", "Lance", "Hammer"] + +WIDTH_RE = re.compile(r'width: *(\d+)%;') + +PART_RE = re.compile(r'(.*) x(\d+)( Points)?') + +# MR Bone 20 pts. +PART_RE_MR = re.compile(r'(.*) (\d+) +pts\.?') + + +""" +
+
+   +
+
+   +
+
+   +
+
+   +
+
+   +
+
+   +
+
+""" +def parse_sharpness(div): + values = [] + divs = div.xpath('div') + for div in divs: + style = div.get("style") + m = WIDTH_RE.match(style) + if m: + values.append(int(m.group(1))) + + return values + + +def parse_rampage(td): + return td.xpath('ul/li/a/text()') + + +def parse_crafting(td): + materials = {} + for li in td.xpath('ul/li'): + atext = li.xpath('a/text()') + litext = li.xpath('text()') + if litext: + litext = litext[0].strip() + else: + print("Unknown format: ", lxml.etree.tostring(td)) + return {} + + if litext.endswith('\xa0'): + litext = litext.rstrip('\xa0') + if litext.endswith('.'): + litext = litext.rstrip('.') + + if litext.endswith('l'): + litext = litext[:-1] + '1' + + if litext.startswith('+ '): + atext += '+' + litext = litext[2:] + + if litext.startswith('x'): + litext = litext[1:] + + if atext: + atext = atext[0].strip() + if litext.endswith(" Points"): + litext = litext.rstrip(" Points") + atext += " Points" + #print("atext '" + atext + "' '" + litext + "'") + try: + materials[atext] = clean_int(litext) + except Exception as e: + print("WARN: failed parsing ", atext, litext) + if litext == 'l': + materials[atext] = 1 + elif litext.isdigit(): + materials['zenny'] = clean_int(litext) + else: + m = PART_RE.match(litext) + if not m: + m = PART_RE_MR.match(litext) + if m: + materials[m.group(1) + ' Points'] = int(m.group(2)) + elif m.group(2): + materials[m.group(1) + ' Points'] = int(m.group(2)) + else: + materials[m.group(1)] = int(m.group(2)) + return materials + + +def clean_text(t): + t = t.strip() + t = t.rstrip('\xa0') + return t + + +def clean_int(s): + s = clean_text(s) + if not s: + return 0 + return int(s) + + +def parse_element(td): + #pp("td", td) + etype = td.xpath('a/text()') + if etype: + values = td.xpath('./text()') + if values: + value = clean_int(values[0].strip()) + return dict(type=etype[0], attack=value) + return dict(type=None, attack=None) + + +def parse_rarity(td): + text = td.xpath('.//text()') + if text: + parts = text[0].split() + if len(parts) > 1: + return clean_int(text[0].split()[1]) + return 8 + + +def parse_slots(td): + slots = [] + for img in td.xpath('.//img'): + title = img.get("title") + if title and title.startswith('gem_'): + parts = title.split("_") + level = int(parts[2]) + slots.append(level) + return slots + + +def adjust_slots_rampage(data): + if data['rarity'] >= 8: + data['rampage_slot'] = data['slots'][-1] + data['slots'] = data['slots'][:-1] + else: + data['rampage_slot'] = 0 + + +def gl_parse_tr(tr): + data = {} + cells = tr.xpath('td') + #print(lxml.etree.tostring(cells[9])) + + # Name + name = cells[0] + #print(name) + data['name'] = name.xpath('a/text()')[0] + data['slots'] = parse_slots(name) + data['sharpness'] = parse_sharpness(name.xpath('div')[0]) + data['attack'] = clean_int(cells[1].text) + element = parse_element(cells[2]) + data['element'] = element['type'] + data['element_attack'] = element['attack'] + data['element_2'] = None + data['element_2_attack'] = None + data['affinity'] = clean_int(cells[3].text.rstrip('%')) + data['defense'] = clean_int(cells[4].text) + data['shot_type'] = cells[5].text + data['level'] = clean_int(cells[6].text.split()[1]) + data['rarity'] = parse_rarity(cells[7]) + data['rampage_skills'] = parse_rampage(cells[8]) + data['crafting'] = parse_crafting(cells[9]) + + adjust_slots_rampage(data) + + return data + + +def default_parse_tr(tr): + data = {} + cells = tr.xpath('td') + #print(lxml.etree.tostring(cells[9])) + + if len(cells) == 10: + return gl_parse_tr(tr) + + #print("cels", [c.text for c in cells]) + + # Name + name = cells[0] + data['name'] = name.xpath('a/text()')[0] + data['slots'] = parse_slots(name) + data['sharpness'] = parse_sharpness(name.xpath('div')[0]) + data['attack'] = clean_int(cells[1].text) + element = parse_element(cells[2]) + data['element'] = element['type'] + data['element_attack'] = element['attack'] + data['element_2'] = None + data['element_2_attack'] = None + data['affinity'] = clean_int(cells[3].text.rstrip('%')) + data['defense'] = clean_int(cells[4].text) + data['rarity'] = parse_rarity(cells[5]) + data['rampage_skills'] = parse_rampage(cells[6]) + data['crafting'] = parse_crafting(cells[7]) + + adjust_slots_rampage(data) + + return data + + + +def parse_fextralife_weapons(text): + root = lxml.etree.HTML(text) + weapons = [] + + table = root.xpath('//div[@id="wiki-content-block"]//table')[0] + rows = table.xpath('tbody/tr') + #print("nrows", len(rows)) + for tr in rows: + data = default_parse_tr(tr) + weapons.append(data) + return weapons + + +def pp(name, e): + if isinstance(e, list): + for i, ei in enumerate(e): + pp(name + "[" + str(i) + "]", ei) + else: + print(name, e.tag) + print(lxml.etree.tostring(e, pretty_print=True)) + + +def _main(): + indir = sys.argv[1] + outpath = sys.argv[2] + weapon_list_all = [] + for wtype in WTYPES: + print(wtype) + fpath = os.path.join(indir, wtype + ".html") + with open(fpath) as f: + text = f.read() + weapon_list = parse_fextralife_weapons(text) + for w in weapon_list: + w["wtype"] = wtype + weapon_list_all.extend(weapon_list) + with open(outpath, "w") as f: + json.dump(weapon_list_all, f, indent=2) + + +if __name__ == '__main__': + _main() diff --git a/scrapers/mhrice_monsters.py b/scrapers/mhrice_monsters.py new file mode 100755 index 0000000..54dda27 --- /dev/null +++ b/scrapers/mhrice_monsters.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 + +import sys +import os.path +import time +import re +import json +import lxml.etree + +import requests + +PART_HEADER_MAP = dict(Slash="Cut", + Impact="Impact", + Shot="Shot", + Fire="Fire", + Water="Water", + Ice="Ice", + Thunder="Thunder", + Dragon="Dragon") + + +def _td_part_id(td): + s = td.xpath('.//text()')[0].strip() + if s.startswith("["): + s = s[1:2] + return int(s) + + +def _td_part_break(td): + text = td.text or "" + text = text.strip() + if text: + m = re.match(r"\(x(\d+)\) (\d+)", text) + print(text, m, m.group(1), m.group(2)) + return dict(count=int(m.group(1)), damage=int(m.group(2))) + return dict(count=0, damage=0) + +def _td_part_sever(td): + text = td.text or "" + text = text.strip() + if text: + m = re.match(r"\((\w+)\) (\d+)", text) + return dict(type=m.group(1), damage=int(m.group(2))) + return dict(type="", damage=0) + + +def get_monster_data(link): + hit_data = {} + base = "https://mhrise.mhrice.info" + url = base + link + result = requests.get(url) + root = lxml.etree.HTML(result.content) + sections = root.xpath("//section") + hit_table = None + parts_table = None + for section in sections: + h2 = section.xpath('h2') + if h2 and h2[0].text: + if hit_table is None and h2[0].text.lower().startswith("hitzone"): + hit_table = section.xpath('.//table')[0] + elif parts_table is None and h2[0].text.lower().startswith("parts"): + parts_table = section.xpath('.//table')[0] + #pp("hit_table", hit_table) + #pp("tr", hit_table.xpath('thead/tr')) + header_cells = hit_table.xpath('thead/tr/th') + header_names = [th.text for th in header_cells] + #print("names", header_names) + rows = hit_table.xpath('tbody/tr') + part_id_name_map = {} + for row in rows: + if 'invalid' in row.attrib.get('class', ""): + continue + #pp("tr", row) + cols = dict(zip(header_names, row.xpath('td'))) + name_td = cols["Name"] + #pp("name_td", name_td) + name_en_span = name_td.xpath('.//span[@lang="en"]/span') + if not name_en_span: + continue + name = name_en_span[0].text + #pp("part", cols["Part"].xpath('.//text()')) + part_id = _td_part_id(cols["Part"]) + part_id_name_map[part_id] = name + hit_data[name] = {} + for k in PART_HEADER_MAP.keys(): + hit_data[name][PART_HEADER_MAP[k]] = int(cols[k].text) + #print(hit_data) + + return hit_data + + # add break/sever data + header_cells = parts_table.xpath('thead/tr/th') + header_names = [th.text for th in header_cells] + #print(header_names) + rows = parts_table.xpath('tbody/tr') + breaks = [] + for row in rows: + if 'invalid' in row.attrib.get('class', ""): + continue + cols = dict(zip(header_names, row.xpath('td'))) + part_id = _td_part_id(cols["Part"]) + part_name = part_id_name_map[part_id] + hit_data[part_name]["_stagger"] = int(cols["Stagger"].text) + part_break = cols["Break"].text or "" + part_sever = cols["Sever"].text or "" + part_break = part_break.strip() + part_sever = part_sever.strip() + hit_data[part_name]["_break"] = _td_part_break(cols["Break"]) + hit_data[part_name]["_sever"] = _td_part_sever(cols["Sever"]) + if part_break or part_sever: + breaks.append(part_name) + + hit_data["_breaks"] = breaks + return hit_data + + +def pp(name, e): + if isinstance(e, list): + for i, ei in enumerate(e): + pp(name + "[" + str(i) + "]", ei) + else: + print(name, e.tag) + print(lxml.etree.tostring(e, pretty_print=True)) + + +def get_monster_list(): + result = requests.get("https://mhrise.mhrice.info/monster.html") + root = lxml.etree.HTML(result.content) + monster_li = root.xpath('//ul[@id="slist-monster"]//li') + monsters = [] + for li in monster_li: + name = li.xpath('.//span[@lang="en"]/span')[0].text + link = li.xpath('a')[0].attrib['href'] + monsters.append(dict(name=name, link=link)) + return monsters + + +def _main(): + outdir = sys.argv[1] + monster_list = get_monster_list() + with open(os.path.join(outdir, "monster_list.json"), "w") as f: + json.dump(monster_list, f, indent=2) + + monster_hitboxes = {} + for m in monster_list: + print(m["name"]) + try: + monster_hitboxes[m["name"]] = get_monster_data(m["link"]) + except Exception as e: + print("ERR: failed to parse hitzones for ", m["name"]) + print(repr(e), str(e)) + time.sleep(0.5) + + with open(os.path.join(outdir, "monster_hitboxes.json"), "w") as f: + json.dump(monster_hitboxes, f, indent=2) + + +if __name__ == '__main__': + _main() diff --git a/scrapers/mhrice_weapons.py b/scrapers/mhrice_weapons.py new file mode 100755 index 0000000..76ae69b --- /dev/null +++ b/scrapers/mhrice_weapons.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python3 + +import sys +import os.path +import time +import re +import json +from pprint import pprint +from collections import defaultdict +import lxml.etree + +import requests + +import _pathfix + +from mhapi.util import WEAPON_TYPES + +MAX_PER_TYPE = 100000 + +def pp(name, e): + if isinstance(e, list): + for i, ei in enumerate(e): + pp(name + "[" + str(i) + "]", ei) + else: + print(name, e.tag) + print(lxml.etree.tostring(e, pretty_print=True)) + + +def parse_sharpness(value_span): + bar_span = value_span.xpath('.//span[@class="mh-sharpness-bar"]')[0] + sharp_spans = bar_span.xpath('.//span') + i = 0 + last_color_num = -1 + values = [] + values_plus = [] + for sharp_span in sharp_spans: + # + attr_style = sharp_span.attrib["style"] + attr_class = sharp_span.attrib["class"] + classes = attr_class.split() + half = False + for class_name in classes: + if class_name.startswith("mh-sharpness-color-"): + color_num = int(class_name[-1]) + if class_name == "mh-sharpness-half": + half = True + styles = attr_style.split(";") + for s in styles: + s = s.strip() + if not s: + continue + parts = s.split(":") + if parts[0] == "width": + value = int(2*float(parts[1].rstrip("%"))) + break + if value == 0: + continue + if half: + if not values_plus: + values_plus = list(values) + if color_num == last_color_num: + values_plus[-1] += value + else: + values_plus.append(value) + else: + # fill in missing colors, if any + while i < color_num: + values.append(0) + i += 1 + values.append(value) + i += 1 + last_color_num = color_num + return values, values_plus + + +def _map_element(e): + if e == "Bomb": + return "Blast" + if e == "Paralyze": + return "Paralysis" + return e + + +def get_weapon_details(wtype, name, link): + data = dict(wtype=wtype, name=name) + url = "https://mhrise.mhrice.info" + link + result = requests.get(url) + root = lxml.etree.HTML(result.content) + + icon_div = root.xpath('//div[@class="mh-title-icon"]/div[@class="mh-colored-icon"]/div')[0] + rarity_class = icon_div.attrib["class"] + data["rarity"] = int(rarity_class.split("-")[-1]) + + stat_div = root.xpath('//div[@class="mh-kvlist"]')[0] + kvlist = stat_div.xpath('.//p[@class="mh-kv"]') + for kv in kvlist: + spans = kv.xpath('span') + key = spans[0].text.strip().lower() + if key in set(["attack", "affinity", "defense"]): + value = spans[1].text + value = value.rstrip("%") + data[key.lower()] = int(value) + elif key == "element": + value_spans = spans[1].xpath("span") + value = value_spans[0].text.strip() + if value: + parts = value.split() + if parts[0] == "None": + data["element"] = None + data["element_attack"] = None + else: + data["element"] = _map_element(parts[0]) + data["element_attack"] = int(parts[1]) + if len(value_spans) > 1: + value = value_spans[1].text.strip() + parts = value.split() + data["element_2"] = _map_element(parts[0]) + data["element_2_attack"] = int(parts[1]) + else: + data["element_2"] = None + data["element_2_attack"] = None + elif key == "slot": + # A level-2 slot + # A level-4 slot + slots = [] + value_span = spans[1] + slot_imgs = value_span.xpath('.//span[@class="mh-slot-outer"]/img') + for slot_img in slot_imgs: + src = slot_img.attrib["src"] + m = re.match(r".*/slot_(\d+)\.png", src) + if m: + svalue = int(m.group(1)) + 1 + slots.append(svalue) + data["slots"] = slots + elif key == "rampage slot": + slots = [] + value_span = spans[1] + slot_imgs = value_span.xpath('.//span[@class="mh-slot-outer"]/img') + for slot_img in slot_imgs: + src = slot_img.attrib["src"] + m = re.match(r".*/slot_(\d+).png", src) + if m: + svalue = int(m.group(1)) + 1 + slots.append(svalue) + data["rampage_slots"] = slots + elif key == "sharpness": + value_span = spans[1] + sharp, sharp_plus = parse_sharpness(value_span) + data["sharpness"] = sharp + data["sharpness_plus"] = sharp_plus + elif key == "bottle": + value = spans[1].text.strip() + if wtype == "Charge Blade": + key = "phial" + if value == "Power": + value = "Impact" + if value == "StrongElement": + value = "Element" + if wtype == "Switch Axe": + key = "phial" + parts = value.split() + value = parts[0] + if value == "StrongElement": + value = "Element" + if value == "DownStamina": + value = "Exhaust" + phial_num = int(parts[1]) + if phial_num > 0: + data["phial_value"] = phial_num + data[key] = value + elif key == "type": + value = spans[1].text.strip() + parts = value.split() + value = parts[0] + if len(parts) > 1: + level = int(parts[1]) + data["shelling_level"] = level + if wtype == "Gunlance": + key = "shelling_type" + if value == "Radial": + value = "Long" + elif value == "Diffusion": + value = "Wide" + data[key] = value + elif key == "insect level": + value = spans[1].text.strip() + data["bug_level"] = int(value) + + sections = root.xpath("//section") + craft_table = None + for section in sections: + h2 = section.xpath("h2/text()") + if h2 and h2[0] == "Crafting": + craft_table = section.xpath("div/table/tbody")[0] + break + if craft_table is not None: + rows = craft_table.xpath("tr") + for row in rows: + cells = row.findall("td") + craft_type = cells[0].text.strip() + if craft_type.startswith("Forge"): + zenny, comps = get_components(cells) + data["creation_cost"] = zenny + data["create_components"] = comps + elif craft_type.startswith("Upgrade"): + zenny, comps = get_components(cells) + data["upgrade_cost"] = zenny + data["upgrade_components"] = comps + + return data + + +def get_components(cells): + zenny = int(cells[1].text) + cmat_text = cells[2].text + components = {} + if cmat_text != "-": + cmat_name = cells[2].xpath('.//span[@lang="en"]/span')[0].text + cmat_points_string = cells[2].xpath("span")[0].tail + cmat_points = int(cmat_points_string.split(" ")[0]) + components[cmat_name] = cmat_points + li_mats = cells[3].xpath("ul/li") + for li in li_mats: + count = int(li.text.strip().rstrip("x")) + name = li.xpath('.//span[@lang="en"]/span')[0].text + components[name] = count + return (zenny, components) + + +def get_rice_id(link): + # /weapon/GreatSword_026.html + fname_base, _ = os.path.splitext(os.path.basename(link)) + _, tail = fname_base.rsplit("_", maxsplit=1) + return int(tail) + + +def get_weapon_list(wtype, id_offset): + if wtype == "Sword and Shield": + ftype = "short_sword" + elif wtype == "Hunting Horn": + ftype = "horn" + elif wtype == "Gunlance": + ftype = "gun_lance" + elif wtype == "Switch Axe": + ftype = "slash_axe" + elif wtype == "Charge Blade": + ftype = "charge_axe" + else: + ftype = wtype.lower().replace(" ", "_") + list_fname = ftype + ".html" + result = requests.get("https://mhrise.mhrice.info/weapon/" + list_fname) + root = lxml.etree.HTML(result.content) + weapon_tree_li = root.xpath('//div[@class="mh-weapon-tree"]//li') + weapons = [] + seen = set() + for li in weapon_tree_li: + listack = [li] + name_stack = [None] + while listack: + current_li = listack.pop() + parent_name = name_stack.pop() + + a = current_li.xpath('a[@class="mh-icon-text"]')[0] + sublists = current_li.xpath('ul/li') + + name = a.xpath('.//span[@lang="en"]/span')[0].text + link = a.attrib['href'] + + name_stack.extend([name] * len(sublists)) + listack.extend(sublists) + + if link in seen: + print("WARN: Duplicate ", name, link) + continue + seen.add(link) + + id_ = get_rice_id(link) + id_offset + final = (len(sublists) == 0) + wdata = dict(name=name, link=link, _id=id_, parent_name=parent_name, final=final) + weapons.append(wdata) + + return weapons + + +def test_details(): + tests = [ + ("Great Sword", "Sinister Shadowblade+", "/weapon/GreatSword_403.html"), + ("Great Sword", "Redwing Claymore I", "/weapon/GreatSword_068.html"), + ("Great Sword", "Defender Great Sword I", "/weapon/GreatSword_132.html"), + ("Great Sword", "Kamura Warrior Cleaver", "/weapon/GreatSword_300.html"), + ("Dual Blades", "Blood Wind Skards+", "/weapon/DualBlades_319.html"), + ("Switch Axe", "Arzuros Jubilax", "/weapon/SlashAxe_323.html"), + ("Switch Axe", "Leave-Taker+", "/weapon/SlashAxe_307.html"), + ("Insect Glaive", "Fine Kamura Glaive", "/weapon/InsectGlaive_302.html"), + ] + for t in tests: + print(t) + d = get_weapon_details(*t) + pprint(d) + print() + + +def _main(): + weapons_type_name_map = defaultdict(dict) + weapons_data = [] + + outdir = sys.argv[1] + outfile = os.path.join(outdir, "weapon_list.json") + if os.path.exists(outfile): + print("Loading existing data from ", outfile) + with open(outfile) as f: + old_data = json.load(f) + for d in old_data: + wtype_name_map = weapons_type_name_map[d["wtype"]] + if d["name"] in wtype_name_map: + print("Removing duplicate ", d["wtype"], d["name"]) + continue + wtype_name_map[d["name"]] = d + + for itype, wtype in enumerate(WEAPON_TYPES): + wtype_name_map = weapons_type_name_map[wtype] + weapons = get_weapon_list(wtype, (itype+1) * MAX_PER_TYPE) + if not weapons: + print("WARN: no weapons of type", wtype) + continue + name_id_map = {} + for w in weapons: + # always re-calculate IDs + name_id_map[w["name"]] = w["_id"] + if w["parent_name"]: + w["parent_id"] = name_id_map[w["parent_name"]] + else: + w["parent_id"] = None + data = wtype_name_map.get(w["name"]) + if data is not None: + print("UP ", wtype, w["_id"], w["name"], w["link"]) + data.update(w) + weapons_data.append(data) + continue + print("ADD", wtype, w["_id"], w["name"], w["link"]) + data = get_weapon_details(wtype, w["name"], w["link"]) + data.update(w) + weapons_data.append(data) + time.sleep(0.5) + + with open(os.path.join(outdir, "weapon_list.json"), "w") as f: + json.dump(weapons_data, f, indent=2) + + +if __name__ == '__main__': + #test_details() + _main() diff --git a/templates/damage/base.html b/templates/damage/base.html new file mode 100644 index 0000000..fc97d83 --- /dev/null +++ b/templates/damage/base.html @@ -0,0 +1,36 @@ +## -*- coding: utf-8 -*- + + + + + +Poogie's Calculator - ${self.title()} + + + + + + + + + + + + + + + + + + +

<%block name="title"/>

+${self.body()} + diff --git a/templates/damage/index.html b/templates/damage/index.html new file mode 100644 index 0000000..d64c4f3 --- /dev/null +++ b/templates/damage/index.html @@ -0,0 +1,67 @@ +<%inherit file="base.html" /> + +
+
+
Menu
+
+
+ +
+
+ +
+
diff --git a/templates/damage/monster_damage.html b/templates/damage/monster_damage.html new file mode 100644 index 0000000..b7fe4c8 --- /dev/null +++ b/templates/damage/monster_damage.html @@ -0,0 +1,312 @@ +## -*- coding: utf-8 -*- + + + + + +Poogie's Calculator: ${monster} v${village_stars} g${guild_stars} + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +

Breaks: ${", ".join(monster_breaks)}

+ +% if monster_damage.alt_state != "Default": +

Alternate state: ${monster_damage.alt_state}

+% endif + + + + + + % for part in part_names: + % if part in monster_breaks: + + % else: + + % endif + % endfor + + + % for dtype in damage_types: + <% max_damage = monster_damage.max(dtype) %> + <% avg_damage = monster_damage.avg(dtype) %> + <% alt_avg_damage = monster_damage.alt_avg(dtype) %> + + + + % for part_name, part in monster_damage.items(): + <% bgcolor = "lightyellow" if (part[dtype] > 0 and part[dtype] == max_damage) else "white" %> + % if part.state_diff(dtype) != 0: + + % else: + + % endif + % endfor + + % endfor + + % for weapon in weapon_names: + <% damage = weapon_damage_map[weapon] %> + <% affinity = str(int(damage.affinity)) + "%" if damage.affinity else " " %> + <% avg = damage.uniform(0.0) %> + <% avg_break = damage.uniform(1.0) %> + + + + + + % if damage.eattack > 0: + + % else: + + % endif + % if damage.eattack2 > 0: + + % else: + + % endif + + + % for part in part_names: + <% bgcolor = "yellow" if damage[part].average() == part_max_damage[part] else "white" %> + + % endfor + + % endfor +
WeaponAvg${part}${part}
+ + ${dtype} + + % if avg_damage > 0 or alt_avg_damage > 0: + ${round(avg_damage, 1)} + % if alt_avg_damage != avg_damage: + (${round(alt_avg_damage, 1)}) + % endif + + % else: +   + % endif + + % if part[dtype] > 0 or part.get_alt_state(dtype) > 0: + ${part[dtype]} (${part.get_alt_state(dtype)}) + + % else: +   + % endif + + % if part[dtype] > 0: + ${part[dtype]} + + % else: +   + %endif +
+ ${weapon} + ${int(damage.attack)}${affinity} +
+
+ ${int(damage.eattack)} + +   + ${int(damage.eattack2)} + +   + ${round(avg, 1)} + % if avg != avg_break: + (${round(avg_break, 1)}) + % endif + + ${int(damage[part].total)} + % if damage[part].total_break != damage[part].total: + (${int(damage[part].total_break)}) + % endif +
+
+ diff --git a/templates/damage/monster_damage_by_rarity.html b/templates/damage/monster_damage_by_rarity.html new file mode 100644 index 0000000..4f7a014 --- /dev/null +++ b/templates/damage/monster_damage_by_rarity.html @@ -0,0 +1,279 @@ +## -*- coding: utf-8 -*- + + + + + +Poogie's Calculator: ${monster} r${rarity} + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ + + + + + % for part in part_names: + + % endfor + + + % for dtype in damage_types: + <% max_damage = monster_damage.max(dtype) %> + <% avg_damage = monster_damage.avg(dtype) %> + <% alt_avg_damage = monster_damage.alt_avg(dtype) %> + + + + % for part_name, part in monster_damage.items(): + <% bgcolor = "lightyellow" if (part[dtype] > 0 and part[dtype] == max_damage) else "white" %> + % if part.state_diff(dtype) != 0: + + % else: + + % endif + % endfor + + % endfor + + % for weapon in weapon_names: + <% damage = weapon_damage_map[weapon] %> + <% affinity = str(int(damage.affinity)) + "%" if damage.affinity else " " %> + <% avg = damage.uniform(0.0) %> + <% avg_break = damage.uniform(1.0) %> + + + + + + % if damage.eattack > 0: + + % else: + + % endif + % if damage.eattack2 > 0: + + % else: + + % endif + + + % for part in part_names: + <% bgcolor = "yellow" if damage[part].average() == part_max_damage[part] else "white" %> + + % endfor + + % endfor +
WeaponAvg${part}
+ + ${dtype} + + % if avg_damage > 0 or alt_avg_damage > 0: + ${round(avg_damage, 1)} + % if alt_avg_damage != avg_damage: + (${round(alt_avg_damage, 1)}) + % endif + + % else: +   + % endif + + % if part[dtype] > 0 or part.get_alt_state(dtype) > 0: + ${part[dtype]} (${part.get_alt_state(dtype)}) + + % else: +   + % endif + + % if part[dtype] > 0: + ${part[dtype]} + + % else: +   + %endif +
+ ${weapon} + ${int(damage.attack)}${affinity} +
+
+ ${int(damage.eattack)} + +   + ${int(damage.eattack2)} + +   + ${round(avg, 1)} + % if avg != avg_break: + (${round(avg_break, 1)}) + % endif + + ${int(damage[part].total)} + % if damage[part].total_break != damage[part].total: + (${int(damage[part].total_break)}) + % endif +
+
+ diff --git a/web/img/Cut.png b/web/img/Cut.png new file mode 100644 index 0000000000000000000000000000000000000000..ec3cfe66d8cfca9f3c4129bd206e644f87adcbf9 GIT binary patch literal 15316 zcmeI3Yg7|w8pj7+Mev5ALaoOW0+xymGr18MLzECGpg}IFpi=8(W`Kbt6Ow@>Y`a>8 zitH*bV8vcmTPUr!9=qM1Jxg14RiIiy9Td`WIdps*%&epTrb!UqIf!EkefHh@}9fGC? zb6-E`wKqZ`C^mvJ z2;AqfmfB42p%FMtnn*KgVeG6=I;PKBNHes(kRGR`uY26W?40QJzS?7s*K8iMw4F&R zV-a|fj_I@;Tvig!BJFgk4JVVz*jou7>+TsUe`s=sW#W3q9Xsiy#<6id9-Dh9cxFh= z-Yc4DBQb`y8ED$1@vO3pM+ilUiSAV;dOC-)5VXU-SPhS~49)jYHd2Q%q=sFOF%l$3 zB2^k>QdFiul?n->LJumdS_;(gP$Qu9ttvE;HqkbASfoa(hR13KT<(>IrdsR_X2Ho+ zorZ0QrYHhc;znGdh?7bXL>VJd$P_V>Sd2s@3RNsY#Nx6zBZ0di>h+*~RB9cKmvXzs zo0|BUsR8?_)T?Y1d(vShK*Npx=av{F&8Si-l$~9lE@0iVtJi&&Ws@btTOW0pDEFCU z#cXzx+i~&YM_bN!i22Sa&g0&>D28*VnFi-J1WBmjVXZ^M@;SXW>X%NSCqG~xzlNib zv^;J@c~ZGm<0rAx`HTazkqHItn)oSu-IH~=ZDe{i%$@!wYWPHWT8GciqZK=PKGocz zhO%pc9T(=F19tO_Y8$HT(dTk=YASW>RWgMAz*6qlYX!SUTGu?@3U-gQ4mqE*P>e?A ztu~?=aB(NE=UB*Q9}-ASj8Z9As^O8%BOMu(lQiY(D0ctaxm78bdp#pN#x$-O&j@sQ zjh?9j{++vL($mU5EU?chucoBgD0T)PvHx_X5A_4^hE|3OqEa2ir1PTOPAPwGFz{QIQ6bQIL z8oaT9ix&|n5O9Gscw+$_G2vw1Gq*C!nBT&14VrL{Kv>1=V;p?;Ss z?Ai4l7bm!O&CUKK?YWzmS69tmFEZ>uc{3wrBf0$FALS%z`;dhY53Ca+ zmpIe1BTScx*Ha=H>*p;Kf~%;ro6_#~FVu+hu;Puo?oPo25?8c{(_>|m9Dc1(ML<^i z%o)7}9Us;-PP|fr>P}Q=#^w=z=TE|SPPgNx{AqP1cjvdr*UiFi)0MA$-=4g4(b=|+ zwdHT^{`&0dhIOj4nxI8#_4_MIHu#4`AFY8BUOX1MJgnw#2d5mIHuF|l+JlDEe+)AY z_#eK|z2(ieC0b~zyxrKKD+z79b$k1sm0RyW+#VXSJE<+Ev3hY&#`J~%ypz78QgZ)? zISECBgHNg^8f*(+?0cdl7TdX?+NBNgJ9<@I@$#Afg@*tUTsr#)tp|T~=?eGVau61A=~1D=#uy>1P`Qp`A+i z8Y5l1t1u8+4<%pWqbw&#v4K{cxW(_kbJCs(p8s11Z8EGwFu(1QVz(p=txx_DYH7W0^ zsc~eMGHP#}n5JDiu5(5%nz!2Kw4|AlHZ-x{iq9KIp4BmxX}tTKfjS;`oTzEO)ot!} z=sz7{AaqR4-Gl6OjDb*rTa3PrGSEe}bO$m$_@Uj%Wg1gznCU!EJCich9mpCXgy=va zyYMF=m8>E&2+4C9(w_>pP?&@ivz7L)LzW96#9jE3!YHJeLT(O1#=F7D($~=-+bGA6 zFG~FQ*%}?P-uVek5A6?j%1}rVepWM%hqyap`tvBcd=heG2n3ZnJFOg~Gv7A;LF(Jhi(-9#u z$z>^vBV;L0lZFuY;G-xS1ffaEWi;2eaQ{^kQpP-l$l@zv)jHAuAUAX(S%bafbgi(j__hM@Ox%!( zbT?8EH)Ih2%cyV1Os*c|7WvK7^yMG`Fg9+;BesT2jT^EU04SrAo$`L-i>`ESF;9^h zU?c(=ZfnLH2}58*=vB#2{ED`NGuV{63Bgdv-0V`sdr8>{#AT4=v28xrgz z)x9E%0GMY-T+xJ@+_lE(AMygH+f+>Lx0Ak%3@OCVI&vAx zeGxN|$C<(=cGxzOsz5d>)9LMphN_IVi&Bc8@)+?^5`7otgVerO^>!Njd3>ia0BOf2 zcE$W6#ZCp4ETfTQTMm)xwP2_0#5nTEtzq(bl9pyyAM6gKkHgPL&b#;Uy3A3?0{0%h zb<+jM7>C?_1G1lvIKtSi4sJi7&eBAc>aczd$=SFXd@AIq6FQ*JG+Om)>$QRU$_UAb zxFFe8j;mv@bV!5af^<=xuc79n12Z?6kZvA=WO0BKMoPU3X~ti+1?~Z_LYi{k2Kgoh zsr9dxy?*d2PCuR<~@rjjqL>}7>lA&vPcu(w-C z84FX8TK{T&N(E(B_E6weNJH{j%0epzOiJB;Q;_69(r8NaKw6OH-MiKrwy@Dk33I&* zSeva*9k*T;ak+q(l|eR^>R*;abnf0&dSM^ccQboK()7iFzRBPgpz4BQ-{+BQlO^fIL)fG)l9z#FoXHp&aL5@k>>z bl3f1*>vrS>$&@8y00000NkvXXu0mjfwzAwr literal 0 HcmV?d00001 diff --git a/web/img/Shot.png b/web/img/Shot.png new file mode 100644 index 0000000000000000000000000000000000000000..e6041c45f96cc3c2674d48d758e79dc4c600e7c0 GIT binary patch literal 2038 zcmV zX^fQR0l@L!9D8yq#TH$!QfWa!NKmXOv`VeC;z8ONkJczjYdn%_A~ss0Egr2_t;T5U z(GRt)Xi2e{YH6_^trP(dkOjeR3l?NK7ub7d_nm$rGm{8LnFY!ys+?Mtu2We>^)CYA4zx*o%+ElF1lo1z(4yHMJ8ZMV zR$J}TTCN-7(Z)N~L?=4Ikt!Wb9k$zSgZHeq-k#ot)cT_Hob6;o^#_es`>Uni2@`Uv zZ@JiL{m}})^PK+_51H)yE)v(DHTjkMZOtE2>kc=mP{2r4M*5sVQlQO?I%J&WGMx&G zN_xV68OQpv2qM_6!9K0_Drmdg{Zi_aLmqdnT#j~v3C0*_gwLu~E!!#=`53@dKh>hJ zm=0Ij31GVC9VMe%!d?ybSgpZon>ERWZ+qsGLl&DQ!%%0L;Y?pJOd*S11YnB!T8oFc z&Mv@kYaOGIZR)-56>kY-GuQpn;xZD_Cc4HY>XhO_C#%<6mlVlhlu>4wZ;faDCVQPH zi|$8eS>;A`N;OIEouO0{-0MBZh1riBB_@n2y;miSBMlA{(j}Zg?*(NVA9a`-2-)l( zR@kCT-)q-knHBbwKja?cooR}(Ci;#)>nm-Z@eSjgVyY=7c&WVmk(F)-w5V6_Ni#g; z6y^4!TdkL|)f^ufR=ycoqNkEwgRRH3KiYhFo zLKMJYv{~q0AIL>h_z;oen--m7nfO<|YKm!A0@&ydNrgpq+YexYzkAt-vgr^PS4<>d zNM+F?mbuYvH#iF5EpN%#X^C)tuZOiBvD_<07Jc-U`PN7hAStgvz4@kiN#A(b>7KMv zR4xR%r6ux(><0L#*mQ34b=TVh2n2es8^GJn_dV^Xk;hIuq&4LWc^_b%CPh7FqFb#+ zR8;T9ko2l6oneV0*4ry>UA{H)0l;p{%~aHG_j$l%XPcx3jC2+dH9?I)wwPTu0wnxJ z0_?HE->g@XrPB8LcfOE%ZEAsMO9_GQ?A30vl|albj**aUh$Zd>Xm+7MNL?06`_RsO zAzQ674S2y8bwaC=)}ceEZ1L={h{5&?=_MPay+Xbaer+1i>-T1;A?TOdQv-=ji% zSX$t>g+p#}60pb}VoG$E*`OAn%RCiQvPHE@8)~@#+-{MQGWv9d{NR8ZXK0V9;?ZCwDJ?kct)c~du#@ZrHZ*ldz(uRxA<(l)E7(2NjB`$!mN zeLtv1tsI-A1r!T8T@0YpL2-pELQnhgMr$Y(a|MohPM3vvvScl~Pg#?zIp2l1IFzt8mD5?$%Ra zskE(OGB%s15i89%FHg$r-jo7xgeyg5TkKNVm}Qpf-Y6VWE!R97q`ebjHV$OKD)aNq zN>Wlro1UKYO&1qGj1Vn6pur99k=AKxX&Hml8Buj$mAfQVNhu_zr*4;e$d01p;<8z! z5kRZ;-qt4jvQ3Ip`?Aw~ZFyN)wL zel^)4uW6L4)gBwIwoH>;46(#;FbZIeIZ~qbfO?w+h+rRh%CiC~P15c**Aeo&&AZMq z&S=9_%e4lG=)EIsRimtqG)INz?AKbCc%iQkLsQ!Hy@Zm)^>-&4BV)79!!_hl*-o>n zPeaZzMgl;!*VUuWI7viAtk9fiTqqIxG-SG&ngI;6T|JI*j&?*ut=F7qTqvnN4r#Mb zi;M&aBqa}or1Fd_{&@SrVsF!^<3LDK7(xFEsaG$&K&8v10CBr5K6D}7y?@F`i8&TP zr78|pNJ@=Ca#2wvC8AnXAR{7j=t5d@x2J$=Z3=0&KqNa>ROg|bky=r?=#r90p~H+r zxkkGGKR!V>5EAeIuYR{#qPWKXAF{^P{~K$hVqmckgoHK_G7vKG-_voGDuq=k*55*! ze5_SiEn53q$P=Ct9^L&dq-$WY_m7Z>@`)U}kcdIbr~1&Xk^R1+Lb){Qj|L3*FUE^v UP)7jf*Z=?k07*qoM6N<$g6vJy)Bpeg literal 0 HcmV?d00001 diff --git a/web/js/common.js b/web/js/common.js index 32751df..8a934ce 100644 --- a/web/js/common.js +++ b/web/js/common.js @@ -135,7 +135,7 @@ function normalize_name(s) { function setup_item_autocomplete(selector) { - var DATA_PATH = get_base_path() + "/rewards/"; + var DATA_PATH = get_base_path() + "/mh4u/rewards/"; $.getJSON(DATA_PATH + "items.json", function(data) { $(selector).autocomplete({ source: data }); @@ -143,6 +143,16 @@ function setup_item_autocomplete(selector) { } +function setup_monster_autocomplete(game, selector) { + var DATA_PATH = "/jsonapi/" + game + "/"; + $.getJSON(DATA_PATH + "monster/_list.json", + function(data) { + var boss = data.filter(a => (a["class"] == "Boss" || a["class"] == "Large")); + var boss_names = boss.map(a => a["name"]); + $(selector).autocomplete({ source: boss_names }); + }); +} + function load_weapon_data(ready_fn) { if (typeof DATA_PATH == "undefined") { DATA_PATH = get_base_path() + "/jsonapi/"; diff --git a/web/mhr/weaponlist.html b/web/mhr/weaponlist.html new file mode 100644 index 0000000..937eb75 --- /dev/null +++ b/web/mhr/weaponlist.html @@ -0,0 +1,345 @@ + + + Poogie's Weapon List (Rise) + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +
+
+ + +
+
+ +
+
+ diff --git a/web/mhr/weaponplanner.html b/web/mhr/weaponplanner.html new file mode 100644 index 0000000..f74feff --- /dev/null +++ b/web/mhr/weaponplanner.html @@ -0,0 +1,284 @@ + + + Poogie's Weapon Planner (Rise) + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+
+
+
+
+ diff --git a/web/templates/weaponrow-rise.ejs b/web/templates/weaponrow-rise.ejs new file mode 100644 index 0000000..262f858 --- /dev/null +++ b/web/templates/weaponrow-rise.ejs @@ -0,0 +1,63 @@ + +<% if (final == 1) { %> + * + <% } else { %> +   + <% } %> + + +<%= name %> + +<%= wtype_short %> +<%= attack %> +<% if (affinity) { %><%= affinity %>%<% } %> + +<% if (element) { %> + <%= element_attack %> + <%= ELEMENT_ABBR[element] %> + <% if (element_2) { %> + <%= element_2_attack %> + <%= ELEMENT_ABBR[element_2] %> + <% } %> +<% } %> + + +<% if (sharpness) { %> +
+ + + + + + +<% if (sharpness.length > 6) { %> + +<% } %> +
+<% } %> +<% if (sharpness_plus) { %> +
+ + + + + + +<% if (sharpness_plus.length > 6) { %> + +<% } %> +
+<% } %> + +<%= phial %> + <% if (phial_value) { %><%= phial_value %><% } %> + +<%= shelling_type %> + <% if (shelling_level) { %><%= shelling_level %><% } %> + +<%= bug_level %> +<%= defense ? "+" + defense + " Def" : "" %> diff --git a/web/templates/weaponstats.ejs b/web/templates/weaponstats.ejs index a1554aa..566ca77 100644 --- a/web/templates/weaponstats.ejs +++ b/web/templates/weaponstats.ejs @@ -81,17 +81,25 @@ title="<%= parent_name %>">(parent) <% } %> + <% if (village_stars) { %> Village <%= village_stars %> <% } %> + <% if (guild_stars) { %> Guild <%= guild_stars %> <% } %> +<% if (rarity) { %> + +Rarity <%= rarity %> + +<% } %> + <% if (children.length) { %>