From c4edd66c6a84cbe7042afc0103fcdb439224baeb Mon Sep 17 00:00:00 2001 From: Bryce Allen Date: Wed, 19 Aug 2015 00:54:15 -0500 Subject: [PATCH] damage: add match option, part filter, cb phial --- bin/mhdamage.py | 122 ++++++++++++++++++++++++++++++++---- mhapi/damage.py | 161 ++++++++++++++++++++++++++++++++++++++++++++++-- mhapi/util.py | 48 +++++++++++++++ 3 files changed, 313 insertions(+), 18 deletions(-) diff --git a/bin/mhdamage.py b/bin/mhdamage.py index 0014591..fc7bc7c 100755 --- a/bin/mhdamage.py +++ b/bin/mhdamage.py @@ -9,6 +9,44 @@ from mhapi.db import MHDB from mhapi.damage import MotionValueDB, WeaponMonsterDamage from mhapi.model import SharpnessLevel from mhapi import skills +from mhapi.util import ELEMENTS, WEAPON_TYPES, WTYPE_ABBR + + +def weapon_match_tuple(arg): + parts = arg.split(",") + if len(parts) == 1: + wtype = parts[0] + element = None + elif len(parts) == 2: + wtype = parts[0] + element = parts[1] + else: + raise ValueError("Bad arg, use 'weapon_type,element_or_status'") + wtype = get_wtype_match(wtype) + if element is not None: + element = get_element_match(element) + return (wtype, element) + + +def get_wtype_match(term): + abbr_result = WTYPE_ABBR.get(term.upper()) + if abbr_result is not None: + return abbr_result + term = term.title() + for wtype in WEAPON_TYPES: + if wtype.startswith(term): + return wtype + raise ValueError("Unknown weapon type: %s" % term) + + +def get_element_match(term): + term = term.title() + for element in ELEMENTS: + if element.startswith(term): + return element + if term.lower() == "raw": + return "Raw" + raise ValueError("Unknown element or status: %s" % term) def percent_change(a, b): @@ -31,18 +69,35 @@ def parse_args(argv): default=False, help="add Awaken (FreeElement), default off") parser.add_argument("-a", "--attack-up", - type=int, choices=xrange(0, 5), default=0, + type=int, choices=range(0, 5), default=0, help="1-4 for AuS, M, L, XL") parser.add_argument("-c", "--critical-eye", - type=int, choices=xrange(0, 5), default=0, + type=int, choices=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=xrange(0, 5), default=0, + type=int, choices=range(0, 5), default=0, help="1-4 for (element) Atk +1, +2, +3 and" " Element Attack Up") + parser.add_argument("-t", "--artillery", + type=int, choices=[0,1,2], default=0, + help="0-2 for no artillery, novice, god") + parser.add_argument("-p", "--parts", + help="Limit analysis to specified parts" + +" (comma separated list)") + parser.add_argument("-m", "--match", nargs="*", + help="WEAPON_TYPE,ELEMENT_OR_STATUS_OR_RAW" + +" Include all matching weapons in their final form." + +" Supports abbreviations like LS for Long Sword" + +" and Para for Paralysis or Blast for Blastblight." + +" If just WEAPON_TYPE is given, include all final" + +" weapons of that type." + +" Examples: 'Great Sword,Raw'" + +" 'Sword and Shield,Para'" + +" 'HH,Blast' 'Hammer'", + type=weapon_match_tuple, default=[]) parser.add_argument("monster", help="Full name of monster") - parser.add_argument("weapon", nargs="+", + parser.add_argument("weapon", nargs="*", help="One or more weapons of same class to compare," " full names") @@ -59,13 +114,29 @@ if __name__ == '__main__': if not monster: raise ValueError("Monster '%s' not found" % args.monster) monster_damage = db.get_monster_damage(monster.id) + weapons = [] + weapon_type = None + + for match_tuple in args.match: + # TODO: better validation + wtype, element = match_tuple + match_weapons = db.get_weapons_by_query(wtype=wtype, element=element, + final=1) + weapons.extend(match_weapons) + for name in args.weapon: weapon = db.get_weapon_by_name(name) if not weapon: raise ValueError("Weapon '%s' not found" % name) weapons.append(weapon) + if not weapons: + print "Err: no matching weapons" + sys.exit(1) + + names = [w.name for w in weapons] + monster_breaks = db.get_monster_breaks(monster.id) weapon_type = weapons[0]["wtype"] motion = motiondb[weapon_type].average @@ -78,8 +149,15 @@ if __name__ == '__main__': skills.CriticalEye.name(args.critical_eye), skills.ElementAttackUp.name(args.element_up)] print "Skills:", ", ".join(skill for skill in skill_names if skill) + + if args.parts: + limit_parts = args.parts.split(",") + else: + limit_parts = None + weapon_damage_map = dict() - for name, row in zip(args.weapon, weapons): + for row in weapons: + name = row["name"] row_type = row["wtype"] if row_type != weapon_type: raise ValueError("Weapon '%s' is different type" % name) @@ -91,35 +169,45 @@ if __name__ == '__main__': attack_skill=args.attack_up, critical_eye_skill=args.critical_eye, element_skill=args.element_up, - awaken=args.awaken) + awaken=args.awaken, + artillery_level=args.artillery, + limit_parts=args.parts) print "%-20s: %4.0f %2.0f%%" % (name, wd.attack, wd.affinity), if wd.etype: - print "(%4.0f %s)" % (wd.eattack, wd.etype), + if wd.etype2: + print "(%4.0f %s, %4.0f %s)" \ + % (wd.eattack, wd.etype, wd.eattack2, wd.etype2), + else: + print "(%4.0f %s)" % (wd.eattack, wd.etype), print SharpnessLevel.name(wd.sharpness) weapon_damage_map[name] = wd except ValueError as e: print str(e) sys.exit(1) - damage_map_base = weapon_damage_map[args.weapon[0]] - parts = damage_map_base.parts + damage_map_base = weapon_damage_map[weapons[0].name] + + if limit_parts: + parts = limit_parts + else: + parts = damage_map_base.parts for part in parts: tdiffs = [percent_change( damage_map_base[part].total, weapon_damage_map[w][part].total ) - for w in args.weapon[1:]] + for w in names[1:]] ediffs = [percent_change( damage_map_base[part].element, weapon_damage_map[w][part].element ) - for w in args.weapon[1:]] + for w in names[1:]] bdiffs = [percent_change( damage_map_base[part].break_diff(), weapon_damage_map[w][part].break_diff() ) - for w in args.weapon[1:]] + for w in names[1:]] tdiff_s = ",".join("%+0.1f%%" % i for i in tdiffs) ediff_s = ",".join("%+0.1f%%" % i for i in ediffs) bdiff_s = ",".join("%+0.1f%%" % i for i in bdiffs) @@ -134,6 +222,14 @@ if __name__ == '__main__': ediff_s, damage.break_diff(), bdiff_s) + if weapon_type == "Charge Blade": + for level in (0, 1, 2, 3, 5): + print " " * 20, level, + for wname in names: + wd = weapon_damage_map[wname] + damage = wd.cb_phial_damage[part][level] + print "(%0.f, %0.f, %0.f);" % damage, + print print " --------------------" @@ -143,7 +239,7 @@ if __name__ == '__main__': base, weapon_damage_map[w].averages[avg_type] ) - for w in args.weapon[1:]] + for w in names[1:]] diff_s = ",".join("%+0.1f%%" % i for i in diffs) diff --git a/mhapi/damage.py b/mhapi/damage.py index cace0e8..c692b8b 100644 --- a/mhapi/damage.py +++ b/mhapi/damage.py @@ -11,6 +11,7 @@ from mhapi.model import SharpnessLevel, _break_find WEAKPART_WEIGHT = 0.5 + def raw_damage(true_raw, sharpness, affinity, monster_hitbox, motion): """ Calculate raw damage to a monster part with the given true raw, @@ -165,7 +166,7 @@ class WeaponMonsterDamage(object): attack_skill=skills.AttackUp.NONE, critical_eye_skill=skills.CriticalEye.NONE, element_skill=skills.ElementAttackUp.NONE, - awaken=False): + awaken=False, artillery_level=0, limit_parts=None): self.weapon = weapon_row self.monster = monster_row self.monster_damage = monster_damage @@ -176,6 +177,8 @@ class WeaponMonsterDamage(object): self.critical_eye_skill = critical_eye_skill self.element_skill = element_skill self.awaken = awaken + self.artillery_level = artillery_level + self.limit_parts = limit_parts self.damage_map = defaultdict(PartDamage) self.average = 0 @@ -183,6 +186,9 @@ class WeaponMonsterDamage(object): self.best_weighted = 0 self.break_weighted = 0 + # map of part -> (map of burst_level -> (raw, ele, burst)) + self.cb_phial_damage = defaultdict(dict) + self.weapon_type = self.weapon["wtype"] self.true_raw = (self.weapon["attack"] / WeaponType.multiplier(self.weapon_type)) @@ -202,6 +208,8 @@ class WeaponMonsterDamage(object): self.damage_type = WeaponType.damage_type(self.weapon_type) self.etype = self.weapon["element"] self.eattack = self.weapon["element_attack"] + self.etype2 = self.weapon["element_2"] + self.eattack2 = self.weapon["element_2_attack"] if not self.etype and self.awaken: self.etype = self.weapon.awaken self.eattack = self.weapon.awaken_attack @@ -210,6 +218,10 @@ class WeaponMonsterDamage(object): self.eattack = int(self.eattack) else: self.eattack = 0 + if self.eattack2: + self.eattack2 = int(self.eattack2) + else: + self.eattack2 = 0 self.true_raw = skills.AttackUp.modified(attack_skill, self.true_raw) @@ -217,6 +229,8 @@ class WeaponMonsterDamage(object): self.affinity) self.eattack = skills.ElementAttackUp.modified(element_skill, self.eattack) + self.eattack2 = skills.ElementAttackUp.modified(element_skill, + self.eattack2) self.parts = [] self.break_count = 0 @@ -228,8 +242,8 @@ class WeaponMonsterDamage(object): weakpart_raw=0, weakpart_element=0, ) - self.max_raw_part = (None, 0) - self.max_element_part = (None, 0) + self.max_raw_part = (None, -1) + self.max_element_part = (None, -1) self._calculate_damage() @property @@ -245,6 +259,9 @@ class WeaponMonsterDamage(object): if m: part = m.group(1) alt = m.group(2) + + if self.limit_parts is not None and part not in self.limit_parts: + continue #print part, alt hitbox = 0 hitbox_cut = int(row["cut"]) @@ -266,6 +283,14 @@ class WeaponMonsterDamage(object): if self.etype in "Fire Water Ice Thunder Dragon".split(): ehitbox = int(row[str(self.etype.lower())]) element = element_damage(self.eattack, self.sharpness, ehitbox) + if self.etype2: + # handle dual blades double element/status + element = element / 2.0 + if self.etype2 in "Fire Water Ice Thunder Dragon".split(): + ehitbox2 = int(row[str(self.etype2.lower())]) + element2 = element_damage(self.eattack2, + self.sharpness, ehitbox2) + element += element2 / 2.0 part_damage = self.damage_map[part] part_damage.set_damage(raw, element, hitbox, ehitbox, state=alt) @@ -292,6 +317,25 @@ class WeaponMonsterDamage(object): self.averages["break_raw"] = self.break_weakpart_raw() self.averages["break_element"] = self.break_weakpart_element() self.averages["break_only"] = self.break_only() + self._calculate_cb_phial_damage() + + def _calculate_cb_phial_damage(self): + if self.weapon_type != "Charge Blade": + return + if self.weapon.phial == "Impact": + fn = cb_impact_phial_damage + else: + fn = cb_element_phial_damage + for part in self.parts: + part_damage = self.damage_map[part] + hitbox = part_damage.hitbox + ehitbox = part_damage.ehitbox + for level in (0, 1, 2, 3, 5): + damage_tuple = fn(self.true_raw, self.eattack, self.sharpness, + self.affinity, hitbox, ehitbox, level, + shield_charged=True, + artillery_level=self.artillery_level) + self.cb_phial_damage[part][level] = damage_tuple def uniform(self): average = 0.0 @@ -328,7 +372,11 @@ class WeaponMonsterDamage(object): return average / total_ehitbox def weakpart_weighted_raw(self, weak_weight=WEAKPART_WEIGHT): - other_weight = (1 - weak_weight) / (len(self.parts) - 1) + if len(self.parts) == 1: + other_weight = 0 + weak_weight = 1 + else: + other_weight = (1 - weak_weight) / (len(self.parts) - 1) average = 0 for part, damage in self.damage_map.iteritems(): if part == self.max_raw_part[0]: @@ -339,7 +387,11 @@ class WeaponMonsterDamage(object): return average def weakpart_weighted_element(self, weak_weight=WEAKPART_WEIGHT): - other_weight = (1 - weak_weight) / (len(self.parts) - 1) + if len(self.parts) == 1: + other_weight = 0 + weak_weight = 1 + else: + other_weight = (1 - weak_weight) / (len(self.parts) - 1) average = 0 for part, damage in self.damage_map.iteritems(): if part == self.max_element_part[0]: @@ -534,3 +586,102 @@ def element_x_attack_up(value, level=1): value += 90 else: raise ValueError("level must be 1, 2, or 3") + + +def cb_impact_phial_damage(true_raw, element, sharpness, affinity, + monster_hitbox, monster_ehitbox, + burst_level, artillery_level=0, + shield_charged=False): + """ + @burst_level: 0 for shield thrust, 1 for side chop, 2 for double swing, + 3 for AED, 5 for super AED w/ 5 phials + @artillery_level: 1 for Novice, 2 for God or Novice + Felyne Bombardier + + See + https://www.reddit.com/r/MonsterHunter/comments/391a5i/mh4u_charge_blade_phial_damage/ + + Note this contradicts data from the other link, but this is more recent. + """ + motions = _cb_get_motions(burst_level, shield_charged) + if burst_level == 5: + multiplier = 0.33 + elif burst_level == 3: + multiplier = 0.1 + else: + multiplier = 0.05 + + if artillery_level == 1: + multiplier *= 1.3 + elif artillery_level == 2: + multiplier *= 1.4 + elif artillery_level != 0: + raise ValueError("artillery_level must be 0, 1 (Novice), or 2 (God)") + + if shield_charged and burst_level != 5: + multiplier *= 1.3 + + if shield_charged and burst_level == 0: + # Shield Thrust gets one blast if shield is charged + burst_level = 1 + + # burst damage is fixed, doesn't depend on monster hitbox + burst_dmg = true_raw * multiplier * burst_level + raw_dmg = sum([raw_damage(true_raw, sharpness, affinity, monster_hitbox, + motion) + for motion in motions]) + ele_dmg = (element_damage(element, sharpness, monster_ehitbox) + * len(motions)) + return (raw_dmg, ele_dmg, burst_dmg) + + +def cb_element_phial_damage(true_raw, element, sharpness, affinity, + monster_hitbox, monster_ehitbox, + burst_level, artillery_level=0, + shield_charged=False): + motions = _cb_get_motions(burst_level, shield_charged) + + if burst_level == 5: + multiplier = 4.5 * 3 + elif burst_level == 3: + multiplier = 4.5 + else: + multiplier = 3 + + if shield_charged and burst_level != 5: + multiplier *= 1.35 + + if shield_charged and burst_level == 0: + # Shield Thrust gets one blast if shield is charged + burst_level = 1 + + burst_dmg = (element / 10.0 * multiplier * burst_level + * monster_ehitbox / 100.0) + raw_dmg = sum([raw_damage(true_raw, sharpness, affinity, monster_hitbox, + motion) + for motion in motions]) + ele_dmg = (element_damage(element, sharpness, monster_ehitbox) + * len(motions)) + return (raw_dmg, ele_dmg, burst_dmg) + + +def _cb_get_motions(burst_level, shield_charged): + # See https://www.reddit.com/r/MonsterHunter/comments/2ue8qw/charge_blade_attack_motion_values/ + if burst_level == 0: + # Shield Thrust + motions = [8, 12] + elif burst_level == 1: + # Burst Side Chop + motions = [31] if shield_charged else [26] + elif burst_level == 2: + # Double Side Swing + motions = [21, 96] if shield_charged else [18, 80] + elif burst_level == 3: + # AED or Super Burst + motions = [108] if shield_charged else [90] + elif burst_level == 5: + # super AED or Ultra Burst, 5 phials filled + # Note: w/o phials it's [17, 90], but that is very rarely used + motions = [25, 99, 100] + else: + raise ValueError("burst_level must be 0, 1, 2, 3, or 5 (Super AED)") + return motions diff --git a/mhapi/util.py b/mhapi/util.py index ad4cd96..bb26ac4 100644 --- a/mhapi/util.py +++ b/mhapi/util.py @@ -5,6 +5,54 @@ Shared utility classes and functions. import codecs +ELEMENTS = """ + Fire + Water + Thunder + Ice + Dragon + Poison + Paralysis + Sleep + Blastblight +""".split() + + +WEAPON_TYPES = [ + "Great Sword", + "Long Sword", + "Sword and Shield", + "Dual Blades", + "Hammer", + "Hunting Horn", + "Lance", + "Gunlance", + "Switch Axe", + "Charge Blade", + "Insect Glaive", + "Light Bowgun", + "Heavy Bowgun", + "Bow", +] + + +WTYPE_ABBR = dict( + GS="Great Sword", + LS="Long Sword", + SS="Sword and Shield", + SNS="Sword and Shield", + DB="Dual Blades", + HH="Hunting Horn", + LA="Lance", + GL="Gunlance", + SA="Switch Axe", + CB="Charge Blade", + IG="Insect Glave", + LBG="Light Bowgun", + HBG="Heavy Bowgun" +) + + class EnumBase(object): _names = dict()