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.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)

@ -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

@ -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()

Loading…
Cancel
Save