You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

537 lines
17 KiB

from collections import defaultdict
import json
import difflib
import re
from mhapi import skills
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,
sharpness, monster raw weakness, and weapon motion value.
"""
return (true_raw
* SharpnessLevel.raw_modifier(sharpness)
* (1 + (affinity / 400.0))
* motion / 100.0
* monster_hitbox / 100.0)
def element_damage(element, sharpness, monster_ehitbox):
"""
Calculate elemental damage to a monster part with the given elemental
attack, the given sharpness, and the given monster elemental weakness.
Note that this is independent of the motion value of the attack.
"""
return (element / 10.0
* SharpnessLevel.element_modifier(sharpness)
* monster_ehitbox / 100.0)
class MotionType(object):
CUT = "cut"
IMPACT = "impact"
FIXED = "fixed"
class MotionValue(object):
def __init__(self, name, types, powers):
self.name = name
self.types = types
self.powers = powers
self.average = sum(self.powers) / len(self.powers)
class WeaponTypeMotionValues(object):
def __init__(self, weapon_type, motion_data):
self.weapon_type = weapon_type
self.motion_values = dict()
for d in motion_data:
name = d["name"]
self.motion_values[name] = MotionValue(name, d["type"], d["power"])
self.average = (sum(mv.average
for mv in self.motion_values.itervalues())
/ len(self))
def __len__(self):
return len(self.motion_values)
def keys(self):
return self.motion_values.keys()
def __getitem__(self, key):
return self.motion_values[key]
class MotionValueDB(object):
def __init__(self, json_path):
with open(json_path) as f:
self._raw_data = json.load(f)
self.motion_values_map = dict()
for d in self._raw_data:
wtype = d["name"]
if wtype == "Sword":
wtype = "Sword and Shield"
self.motion_values_map[wtype] = WeaponTypeMotionValues(wtype,
d["motions"])
def __getitem__(self, weapon_type):
return self.motion_values_map[weapon_type]
def keys(self):
return self.motion_values_map.keys()
def __len__(self):
return len(self.motion_values_map)
class WeaponType(object):
"""
Enumeration for weapon types.
"""
SWITCH_AXE = "Switch Axe"
HAMMER = "Hammer"
HUNTING_HORN = "Hunting Horn"
GREAT_SWORD = "Great Sword"
CHARGE_BLADE = "Charge Blade"
LONG_SWORD = "Long Sword"
INSECT_GLAIVE = "Insect Glaive"
LANCE = "Lance"
GUNLANCE = "Gunlance"
HEAVY_BOWGUN = "Heavy Bowgun"
SWORD_AND_SHIELD = "Sword and Shield"
DUAL_BLADES = "Dual Blades"
LIGHT_BOWGUN = "Light Bowgun"
BOW = "Bow"
IMPACT = "impact"
CUT = "cut"
SHOT = "shot"
MIXED = "cut/impact"
_multiplier = {
"Switch Axe": 5.4,
"Hammer": 5.2,
"Hunting Horn": 5.2,
"Great Sword": 4.8,
"Charge Blade": 3.6,
"Long Sword": 3.3,
"Insect Glaive": 3.1,
"Lance": 2.3,
"Gunlance": 2.3,
"Heavy Bowgun": 1.5,
"Sword and Shield": 1.4,
"Dual Blades": 1.4,
"Light Bowgun": 1.3,
"Bow": 1.2,
}
@classmethod
def all(cls):
return cls._multiplier.keys()
@classmethod
def damage_type(cls, weapon_type):
if weapon_type in (cls.HAMMER, cls.HUNTING_HORN):
return cls.IMPACT
elif weapon_type == cls.LANCE:
return cls.MIXED
elif weapon_type in (cls.LIGHT_BOWGUN, cls.HEAVY_BOWGUN, cls.BOW):
return cls.SHOT
else:
return cls.CUT
@classmethod
def multiplier(cls, weapon_type):
return cls._multiplier[weapon_type]
class WeaponMonsterDamage(object):
"""
Class for calculating how much damage a weapon does to a monster.
Does not include overall monster defense.
"""
def __init__(self, weapon_row, monster_row, monster_damage, motion,
sharp_plus=False, breakable_parts=None,
attack_skill=skills.AttackUp.NONE,
critical_eye_skill=skills.CriticalEye.NONE,
element_skill=skills.ElementAttackUp.NONE,
awaken=False):
self.weapon = weapon_row
self.monster = monster_row
self.monster_damage = monster_damage
self.motion = motion
self.sharp_plus = sharp_plus
self.breakable_parts = breakable_parts
self.attack_skill = attack_skill
self.critical_eye_skill = critical_eye_skill
self.element_skill = element_skill
self.awaken = awaken
self.damage_map = defaultdict(PartDamage)
self.average = 0
self.weakness_weighted = 0
self.best_weighted = 0
self.break_weighted = 0
self.weapon_type = self.weapon["wtype"]
self.true_raw = (self.weapon["attack"]
/ WeaponType.multiplier(self.weapon_type))
if sharp_plus:
self.sharpness = self.weapon.sharpness_plus.max
else:
self.sharpness = self.weapon.sharpness.max
#print "sharpness=", self.sharpness
if self.weapon["affinity"]:
# handle chaotic gore affinity - use average, which is
# probably not quite right but at least allows an initial
# comparison point
parts = [int(x) for x in self.weapon["affinity"].split("/")]
self.affinity = sum(parts)/len(parts)
else:
self.affinity = 0
self.damage_type = WeaponType.damage_type(self.weapon_type)
self.etype = self.weapon["element"]
self.eattack = self.weapon["element_attack"]
if not self.etype and self.awaken:
self.etype = self.weapon.awaken
self.eattack = self.weapon.awaken_attack
if self.eattack:
self.eattack = int(self.eattack)
else:
self.eattack = 0
self.true_raw = skills.AttackUp.modified(attack_skill,
self.true_raw)
self.affinity = skills.CriticalEye.modified(critical_eye_skill,
self.affinity)
self.eattack = skills.ElementAttackUp.modified(element_skill,
self.eattack)
self.parts = []
self.break_count = 0
self.averages = dict(
uniform=0,
raw=0,
element=0,
weakpart_raw=0,
weakpart_element=0,
)
self.max_raw_part = (None, 0)
self.max_element_part = (None, 0)
self._calculate_damage()
@property
def attack(self):
return self.true_raw * WeaponType.multiplier(self.weapon_type)
def _calculate_damage(self):
for row in self.monster_damage._rows:
# TODO: refactor to take advantage of new model
part = row["body_part"]
alt = None
m = re.match(r"([^(]+) \(([^)]+)\)", part)
if m:
part = m.group(1)
alt = m.group(2)
#print part, alt
hitbox = 0
hitbox_cut = int(row["cut"])
hitbox_impact = int(row["impact"])
if self.damage_type == WeaponType.CUT:
hitbox = hitbox_cut
elif self.damage_type == WeaponType.IMPACT:
hitbox = hitbox_impact
elif self.damage_type == WeaponType.MIXED:
# Info from /u/ShadyFigure, see
# https://www.reddit.com/r/MonsterHunter/comments/3fr2u0/124th_weekly_stupid_question_thread/cts3hz8?context=3
hitbox = max(hitbox_cut, hitbox_impact * .72)
raw = raw_damage(self.true_raw, self.sharpness, self.affinity,
hitbox, self.motion)
element = 0
ehitbox = 0
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)
part_damage = self.damage_map[part]
part_damage.set_damage(raw, element, hitbox, ehitbox, state=alt)
if not part_damage.part:
part_damage.part = part
if alt is None:
if (self.breakable_parts
and _break_find(part, self.monster_damage.parts.keys(),
self.breakable_parts)):
part_damage.breakable = True
if hitbox > self.max_raw_part[1]:
self.max_raw_part = (part, hitbox)
if ehitbox > self.max_element_part[1]:
self.max_element_part = (part, ehitbox)
for d in self.damage_map.values():
if d.is_breakable():
self.break_count += 1
self.parts = self.damage_map.keys()
self.averages["uniform"] = self.uniform()
self.averages["raw"] = self.weighted_raw()
self.averages["element"] = self.weighted_element()
self.averages["weakpart_raw"] = self.weakpart_weighted_raw()
self.averages["weakpart_element"] = self.weakpart_weighted_element()
self.averages["break_raw"] = self.break_weakpart_raw()
self.averages["break_element"] = self.break_weakpart_element()
self.averages["break_only"] = self.break_only()
def uniform(self):
average = 0.0
for part, damage in self.damage_map.iteritems():
average += damage.average()
return average / len(self.damage_map)
def weighted_raw(self):
"""
Average damage weighted by non-broken raw hitbox. For each part the
damage is averaged across broken vs non-broken, weighted by the
default of broken for 25% of the hits.
"""
average = 0.0
total_hitbox = 0.0
for part, damage in self.damage_map.iteritems():
average += damage.average() * damage.hitbox
total_hitbox += damage.hitbox
if total_hitbox == 0:
return 0
return average / total_hitbox
def weighted_element(self):
"""
Average damage weighted by non-broken element hitbox.
"""
average = 0.0
total_ehitbox = 0.0
for part, damage in self.damage_map.iteritems():
average += damage.average() * damage.ehitbox
total_ehitbox += damage.ehitbox
if total_ehitbox == 0:
return 0
return average / total_ehitbox
def weakpart_weighted_raw(self, weak_weight=WEAKPART_WEIGHT):
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]:
weight = weak_weight
else:
weight = other_weight
average += damage.average() * weight
return average
def weakpart_weighted_element(self, weak_weight=WEAKPART_WEIGHT):
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]:
weight = weak_weight
else:
weight = other_weight
average += damage.average() * weight
return average
def break_weakpart_raw(self):
"""
Split evenly among break parts and weakest raw part.
"""
if not self.break_count:
return 0
average = 0.0
count = self.break_count + 1
for part, damage in self.damage_map.iteritems():
if part == self.max_raw_part[0]:
average += damage.average()
if damage.is_breakable():
count -= 1
elif damage.is_breakable():
# for breaks, assume attack until broken, unless it's a
# weak part and covered above
average += damage.total
return average / count
def break_weakpart_element(self):
"""
Split evenly among break parts and weakest element part.
"""
if not self.break_count:
return 0
average = 0.0
count = self.break_count + 1
for part, damage in self.damage_map.iteritems():
if part == self.max_element_part[0]:
# If weakpart is also a break, assume continue attacking
# even after broken
average += damage.average()
if damage.is_breakable():
count -= 1
elif damage.is_breakable():
# for breaks that aren't the weakpart, assume attack until
# broken and then go back to weakpart
average += damage.total
return average / count
def break_only(self):
"""
Split evenly among break parts. If there are breaks that are weak
to element but not to raw or vice versa, this will represent that
when comparing weapons.
"""
if not self.break_count:
return 0
average = 0.0
for part, damage in self.damage_map.iteritems():
if damage.is_breakable():
# attack until broken, then move to next break
average += damage.total
return average / self.break_count
def __getitem__(self, key):
return self.damage_map[key]
def keys(self):
return self.parts
class PartDamageState(object):
def __init__(self, raw, element, hitbox, ehitbox, state=None):
self.raw = raw
self.element = element
self.hitbox = hitbox
self.ehitbox = ehitbox
self.state = state
class PartDamage(object):
"""
Class to represent the damage done to a single hitzone on a monster,
default state and alternate state (broken, enraged, etc).
"""
def __init__(self):
self.states = dict()
self.part = None
self.breakable = False
@property
def raw(self):
return self.states[None].raw
@property
def element(self):
return self.states[None].element
@property
def hitbox(self):
return self.states[None].hitbox
@property
def ehitbox(self):
return self.states[None].ehitbox
@property
def break_raw(self):
if "Break Part" in self.states:
return self.states["Break Part"].raw
else:
return self.raw
@property
def break_element(self):
if "Break Part" in self.states:
return self.states["Break Part"].element
else:
return self.element
@property
def rage_raw(self):
if "Enraged" in self.states:
return self.states["Enraged"].raw
else:
return self.raw
@property
def rage_element(self):
if "Enraged" in self.states:
return self.states["Enraged"].element
else:
return self.element
@property
def total(self):
return self.raw + self.element
@property
def total_break(self):
return self.break_raw + self.break_element
@property
def total_rage(self):
return self.rage_raw + self.rage_element
def break_diff(self):
return self.total_break - self.total
def rage_diff(self):
return self.total_rage - self.total
def is_breakable(self):
# If the part has a hitbox with different damage in the break
# rows from the db, or if it's explicitly marked as breakable
# (done by checking hunt rewards for breaks).
return self.break_diff() > 0 or self.breakable
def average(self, break_weight=0.25, rage_weight=0.5):
if self.break_diff():
assert not self.rage_diff()
return self.average_break(break_weight)
else:
return self.average_rage(rage_weight)
def average_break(self, break_weight=0.25):
return (self.total_break * break_weight
+ self.total * (1 - break_weight))
def average_rage(self, rage_weight=0.5):
return (self.total_rage * rage_weight
+ self.total * (1 - rage_weight))
def set_damage(self, raw, element, hitbox, ehitbox, state=None):
if state == "Without Hide":
state = "Break Part"
self.states[state] = PartDamageState(raw, element,
hitbox, ehitbox, state)
def element_attack_up(value):
return value * 1.1
def element_x_attack_up(value, level=1):
value = value * (1 + .05 * level)
if level == 1:
value += 40
elif level == 2:
value += 60
elif level == 3:
value += 90
else:
raise ValueError("level must be 1, 2, or 3")