damage: add match option, part filter, cb phial
This commit is contained in:
118
bin/mhdamage.py
118
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,9 +169,15 @@ 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:
|
||||
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
|
||||
@@ -101,7 +185,11 @@ if __name__ == '__main__':
|
||||
print str(e)
|
||||
sys.exit(1)
|
||||
|
||||
damage_map_base = weapon_damage_map[args.weapon[0]]
|
||||
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:
|
||||
@@ -109,17 +197,17 @@ if __name__ == '__main__':
|
||||
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)
|
||||
|
||||
|
||||
157
mhapi/damage.py
157
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,6 +372,10 @@ class WeaponMonsterDamage(object):
|
||||
return average / total_ehitbox
|
||||
|
||||
def weakpart_weighted_raw(self, weak_weight=WEAKPART_WEIGHT):
|
||||
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():
|
||||
@@ -339,6 +387,10 @@ class WeaponMonsterDamage(object):
|
||||
return average
|
||||
|
||||
def weakpart_weighted_element(self, weak_weight=WEAKPART_WEIGHT):
|
||||
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():
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user