damage: add match option, part filter, cb phial

main
Bryce Allen 10 years ago
parent 26e376eefb
commit c4edd66c6a

@ -9,6 +9,44 @@ from mhapi.db import MHDB
from mhapi.damage import MotionValueDB, WeaponMonsterDamage from mhapi.damage import MotionValueDB, WeaponMonsterDamage
from mhapi.model import SharpnessLevel from mhapi.model import SharpnessLevel
from mhapi import skills 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): def percent_change(a, b):
@ -31,18 +69,35 @@ def parse_args(argv):
default=False, default=False,
help="add Awaken (FreeElement), default off") help="add Awaken (FreeElement), default off")
parser.add_argument("-a", "--attack-up", 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") help="1-4 for AuS, M, L, XL")
parser.add_argument("-c", "--critical-eye", 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") help="1-4 for CE+1, +2, +3 and Critical God")
parser.add_argument("-e", "--element-up", 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" help="1-4 for (element) Atk +1, +2, +3 and"
" Element Attack Up") " 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", parser.add_argument("monster",
help="Full name of 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," help="One or more weapons of same class to compare,"
" full names") " full names")
@ -59,13 +114,29 @@ if __name__ == '__main__':
if not monster: if not monster:
raise ValueError("Monster '%s' not found" % args.monster) raise ValueError("Monster '%s' not found" % args.monster)
monster_damage = db.get_monster_damage(monster.id) monster_damage = db.get_monster_damage(monster.id)
weapons = [] 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: for name in args.weapon:
weapon = db.get_weapon_by_name(name) weapon = db.get_weapon_by_name(name)
if not weapon: if not weapon:
raise ValueError("Weapon '%s' not found" % name) raise ValueError("Weapon '%s' not found" % name)
weapons.append(weapon) 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) monster_breaks = db.get_monster_breaks(monster.id)
weapon_type = weapons[0]["wtype"] weapon_type = weapons[0]["wtype"]
motion = motiondb[weapon_type].average motion = motiondb[weapon_type].average
@ -78,8 +149,15 @@ if __name__ == '__main__':
skills.CriticalEye.name(args.critical_eye), skills.CriticalEye.name(args.critical_eye),
skills.ElementAttackUp.name(args.element_up)] skills.ElementAttackUp.name(args.element_up)]
print "Skills:", ", ".join(skill for skill in skill_names if skill) 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() weapon_damage_map = dict()
for name, row in zip(args.weapon, weapons): for row in weapons:
name = row["name"]
row_type = row["wtype"] row_type = row["wtype"]
if row_type != weapon_type: if row_type != weapon_type:
raise ValueError("Weapon '%s' is different type" % name) raise ValueError("Weapon '%s' is different type" % name)
@ -91,9 +169,15 @@ if __name__ == '__main__':
attack_skill=args.attack_up, attack_skill=args.attack_up,
critical_eye_skill=args.critical_eye, critical_eye_skill=args.critical_eye,
element_skill=args.element_up, 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), print "%-20s: %4.0f %2.0f%%" % (name, wd.attack, wd.affinity),
if wd.etype: 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 "(%4.0f %s)" % (wd.eattack, wd.etype),
print SharpnessLevel.name(wd.sharpness) print SharpnessLevel.name(wd.sharpness)
weapon_damage_map[name] = wd weapon_damage_map[name] = wd
@ -101,7 +185,11 @@ if __name__ == '__main__':
print str(e) print str(e)
sys.exit(1) 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 parts = damage_map_base.parts
for part in parts: for part in parts:
@ -109,17 +197,17 @@ if __name__ == '__main__':
damage_map_base[part].total, damage_map_base[part].total,
weapon_damage_map[w][part].total weapon_damage_map[w][part].total
) )
for w in args.weapon[1:]] for w in names[1:]]
ediffs = [percent_change( ediffs = [percent_change(
damage_map_base[part].element, damage_map_base[part].element,
weapon_damage_map[w][part].element weapon_damage_map[w][part].element
) )
for w in args.weapon[1:]] for w in names[1:]]
bdiffs = [percent_change( bdiffs = [percent_change(
damage_map_base[part].break_diff(), damage_map_base[part].break_diff(),
weapon_damage_map[w][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) tdiff_s = ",".join("%+0.1f%%" % i for i in tdiffs)
ediff_s = ",".join("%+0.1f%%" % i for i in ediffs) ediff_s = ",".join("%+0.1f%%" % i for i in ediffs)
bdiff_s = ",".join("%+0.1f%%" % i for i in bdiffs) bdiff_s = ",".join("%+0.1f%%" % i for i in bdiffs)
@ -134,6 +222,14 @@ if __name__ == '__main__':
ediff_s, ediff_s,
damage.break_diff(), damage.break_diff(),
bdiff_s) 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 " --------------------" print " --------------------"
@ -143,7 +239,7 @@ if __name__ == '__main__':
base, base,
weapon_damage_map[w].averages[avg_type] 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_s = ",".join("%+0.1f%%" % i for i in diffs)

@ -11,6 +11,7 @@ from mhapi.model import SharpnessLevel, _break_find
WEAKPART_WEIGHT = 0.5 WEAKPART_WEIGHT = 0.5
def raw_damage(true_raw, sharpness, affinity, monster_hitbox, motion): def raw_damage(true_raw, sharpness, affinity, monster_hitbox, motion):
""" """
Calculate raw damage to a monster part with the given true raw, Calculate raw damage to a monster part with the given true raw,
@ -165,7 +166,7 @@ class WeaponMonsterDamage(object):
attack_skill=skills.AttackUp.NONE, attack_skill=skills.AttackUp.NONE,
critical_eye_skill=skills.CriticalEye.NONE, critical_eye_skill=skills.CriticalEye.NONE,
element_skill=skills.ElementAttackUp.NONE, element_skill=skills.ElementAttackUp.NONE,
awaken=False): awaken=False, artillery_level=0, limit_parts=None):
self.weapon = weapon_row self.weapon = weapon_row
self.monster = monster_row self.monster = monster_row
self.monster_damage = monster_damage self.monster_damage = monster_damage
@ -176,6 +177,8 @@ class WeaponMonsterDamage(object):
self.critical_eye_skill = critical_eye_skill self.critical_eye_skill = critical_eye_skill
self.element_skill = element_skill self.element_skill = element_skill
self.awaken = awaken self.awaken = awaken
self.artillery_level = artillery_level
self.limit_parts = limit_parts
self.damage_map = defaultdict(PartDamage) self.damage_map = defaultdict(PartDamage)
self.average = 0 self.average = 0
@ -183,6 +186,9 @@ class WeaponMonsterDamage(object):
self.best_weighted = 0 self.best_weighted = 0
self.break_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.weapon_type = self.weapon["wtype"]
self.true_raw = (self.weapon["attack"] self.true_raw = (self.weapon["attack"]
/ WeaponType.multiplier(self.weapon_type)) / WeaponType.multiplier(self.weapon_type))
@ -202,6 +208,8 @@ class WeaponMonsterDamage(object):
self.damage_type = WeaponType.damage_type(self.weapon_type) self.damage_type = WeaponType.damage_type(self.weapon_type)
self.etype = self.weapon["element"] self.etype = self.weapon["element"]
self.eattack = self.weapon["element_attack"] 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: if not self.etype and self.awaken:
self.etype = self.weapon.awaken self.etype = self.weapon.awaken
self.eattack = self.weapon.awaken_attack self.eattack = self.weapon.awaken_attack
@ -210,6 +218,10 @@ class WeaponMonsterDamage(object):
self.eattack = int(self.eattack) self.eattack = int(self.eattack)
else: else:
self.eattack = 0 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 = skills.AttackUp.modified(attack_skill,
self.true_raw) self.true_raw)
@ -217,6 +229,8 @@ class WeaponMonsterDamage(object):
self.affinity) self.affinity)
self.eattack = skills.ElementAttackUp.modified(element_skill, self.eattack = skills.ElementAttackUp.modified(element_skill,
self.eattack) self.eattack)
self.eattack2 = skills.ElementAttackUp.modified(element_skill,
self.eattack2)
self.parts = [] self.parts = []
self.break_count = 0 self.break_count = 0
@ -228,8 +242,8 @@ class WeaponMonsterDamage(object):
weakpart_raw=0, weakpart_raw=0,
weakpart_element=0, weakpart_element=0,
) )
self.max_raw_part = (None, 0) self.max_raw_part = (None, -1)
self.max_element_part = (None, 0) self.max_element_part = (None, -1)
self._calculate_damage() self._calculate_damage()
@property @property
@ -245,6 +259,9 @@ class WeaponMonsterDamage(object):
if m: if m:
part = m.group(1) part = m.group(1)
alt = m.group(2) alt = m.group(2)
if self.limit_parts is not None and part not in self.limit_parts:
continue
#print part, alt #print part, alt
hitbox = 0 hitbox = 0
hitbox_cut = int(row["cut"]) hitbox_cut = int(row["cut"])
@ -266,6 +283,14 @@ class WeaponMonsterDamage(object):
if self.etype in "Fire Water Ice Thunder Dragon".split(): if self.etype in "Fire Water Ice Thunder Dragon".split():
ehitbox = int(row[str(self.etype.lower())]) ehitbox = int(row[str(self.etype.lower())])
element = element_damage(self.eattack, self.sharpness, ehitbox) 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 = self.damage_map[part]
part_damage.set_damage(raw, element, hitbox, ehitbox, state=alt) 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_raw"] = self.break_weakpart_raw()
self.averages["break_element"] = self.break_weakpart_element() self.averages["break_element"] = self.break_weakpart_element()
self.averages["break_only"] = self.break_only() 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): def uniform(self):
average = 0.0 average = 0.0
@ -328,6 +372,10 @@ class WeaponMonsterDamage(object):
return average / total_ehitbox return average / total_ehitbox
def weakpart_weighted_raw(self, weak_weight=WEAKPART_WEIGHT): 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) other_weight = (1 - weak_weight) / (len(self.parts) - 1)
average = 0 average = 0
for part, damage in self.damage_map.iteritems(): for part, damage in self.damage_map.iteritems():
@ -339,6 +387,10 @@ class WeaponMonsterDamage(object):
return average return average
def weakpart_weighted_element(self, weak_weight=WEAKPART_WEIGHT): 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) other_weight = (1 - weak_weight) / (len(self.parts) - 1)
average = 0 average = 0
for part, damage in self.damage_map.iteritems(): for part, damage in self.damage_map.iteritems():
@ -534,3 +586,102 @@ def element_x_attack_up(value, level=1):
value += 90 value += 90
else: else:
raise ValueError("level must be 1, 2, or 3") 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 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): class EnumBase(object):
_names = dict() _names = dict()

Loading…
Cancel
Save