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.
781 lines
26 KiB
781 lines
26 KiB
import string
|
|
import json
|
|
import urllib
|
|
import re
|
|
import difflib
|
|
|
|
from mhapi.util import EnumBase
|
|
|
|
|
|
class ModelJSONEncoder(json.JSONEncoder):
|
|
def default(self, o):
|
|
if hasattr(o, "as_data"):
|
|
return o.as_data()
|
|
return json.JSONEncoder.default(self, o)
|
|
|
|
|
|
class ModelBase(object):
|
|
def as_data(self):
|
|
raise NotImplemented()
|
|
|
|
def as_list_data(self):
|
|
raise NotImplemented()
|
|
|
|
def json_dumps(self, indent=2):
|
|
data = self.as_data()
|
|
return json.dumps(data, cls=ModelJSONEncoder, indent=indent)
|
|
|
|
def json_dump(self, fp, indent=2):
|
|
json.dump(self, fp, cls=ModelJSONEncoder, indent=indent)
|
|
|
|
|
|
class RowModel(ModelBase):
|
|
_list_fields = ["id", "name"]
|
|
_exclude_fields = []
|
|
_indexes = { "name": ["id"] }
|
|
|
|
def __init__(self, row):
|
|
self.id = row["_id"]
|
|
self._row = row
|
|
self._data = dict(row)
|
|
del self._data["_id"]
|
|
self._data["id"] = self.id
|
|
for f in self._exclude_fields:
|
|
del self._data[f]
|
|
|
|
def __getattr__(self, name):
|
|
try:
|
|
return self._data[name]
|
|
except IndexError:
|
|
raise AttributeError("'%s' object has no attribute '%s'"
|
|
% (self.__class__.__name__, name))
|
|
|
|
def __getitem__(self, key):
|
|
return self._data[key]
|
|
|
|
def __contains__(self, key):
|
|
return key in self._data
|
|
|
|
def fields(self):
|
|
return self._data.keys()
|
|
|
|
def as_data(self):
|
|
return self._data
|
|
|
|
def as_list_data(self):
|
|
list_data = {}
|
|
for key in self._list_fields:
|
|
list_data[key] = self[key]
|
|
return list_data
|
|
|
|
def update_indexes(self, data):
|
|
for key_field, value_fields in self._indexes.iteritems():
|
|
if key_field not in data:
|
|
data[key_field] = {}
|
|
self.update_index(key_field, value_fields, data[key_field])
|
|
|
|
def update_index(self, key_field, value_fields, data):
|
|
if isinstance(value_fields, str):
|
|
item = self[value_fields]
|
|
else:
|
|
item = dict((k, self[k]) for k in value_fields)
|
|
key_value = self[key_field]
|
|
if key_value not in data:
|
|
data[key_value] = []
|
|
data[key_value].append(item)
|
|
|
|
def __str__(self):
|
|
if "name" in self._data and self.name is not None:
|
|
name = urllib.quote(self.name, safe=" ")
|
|
else:
|
|
name = str(self.id)
|
|
return "%s '%s'" % (self.__class__.__name__, name)
|
|
|
|
def __repr__(self):
|
|
return "<mhapi.model.%s %d>" % (self.__class__.__name__, self.id)
|
|
|
|
|
|
class Quest(RowModel):
|
|
_full_template = string.Template(
|
|
"$name ($hub $stars* $rank)"
|
|
"\n Goal: $goal"
|
|
"\n Sub : $sub_goal"
|
|
)
|
|
|
|
_one_line_template = string.Template(
|
|
"$name ($hub $stars* $rank)"
|
|
)
|
|
|
|
def __init__(self, quest_row, quest_rewards=None):
|
|
super(Quest, self).__init__(quest_row)
|
|
|
|
self.rewards = quest_rewards
|
|
|
|
def is_multi_monster(self):
|
|
return (" and " in self.goal
|
|
or "," in self.goal
|
|
or " all " in self.goal)
|
|
|
|
def one_line_u(self):
|
|
return self._one_line_template.substitute(self.as_data())
|
|
|
|
def full_u(self):
|
|
return self._full_template.substitute(self.as_data())
|
|
|
|
def __unicode__(self):
|
|
return self.full_u()
|
|
|
|
|
|
class SharpnessLevel(EnumBase):
|
|
"""
|
|
Enumeration for weapon sharpness levels.
|
|
"""
|
|
|
|
RED = 0
|
|
ORANGE = 1
|
|
YELLOW = 2
|
|
GREEN = 3
|
|
BLUE = 4
|
|
WHITE = 5
|
|
PURPLE = 6
|
|
|
|
ALL = range(0, PURPLE + 1)
|
|
|
|
_names = {
|
|
RED: "Red",
|
|
ORANGE: "Orange",
|
|
YELLOW: "Yellow",
|
|
GREEN: "Green",
|
|
BLUE: "Blue",
|
|
WHITE: "White",
|
|
PURPLE: "Purple",
|
|
}
|
|
|
|
# source: http://kiranico.com/en/mh4u/wiki/weapons
|
|
_modifier = {
|
|
RED: (0.50, 0.25),
|
|
ORANGE: (0.75, 0.50),
|
|
YELLOW: (1.00, 0.75),
|
|
GREEN: (1.125, 1.00),
|
|
BLUE: (1.25, 1.0625),
|
|
WHITE: (1.32, 1.125),
|
|
PURPLE: (1.44, 1.20),
|
|
}
|
|
|
|
_modifier_mhx = {
|
|
RED: (0.50, 0.25),
|
|
ORANGE: (0.75, 0.50),
|
|
YELLOW: (1.00, 0.75),
|
|
GREEN: (1.05, 1.00),
|
|
BLUE: (1.20, 1.0625),
|
|
WHITE: (1.32, 1.125),
|
|
}
|
|
|
|
|
|
@classmethod
|
|
def raw_modifier(cls, sharpness):
|
|
return cls._modifier[sharpness][0]
|
|
|
|
@classmethod
|
|
def element_modifier(cls, sharpness):
|
|
return cls._modifier[sharpness][1]
|
|
|
|
|
|
class WeaponSharpness(ModelBase):
|
|
"""
|
|
Representation of the sharpness of a weapon, as a list of sharpness
|
|
points at each level. E.g. the 0th item in the list is the amount of
|
|
RED sharpness, the 1st item is ORANGE, etc.
|
|
"""
|
|
def __init__(self, db_string_or_list):
|
|
if isinstance(db_string_or_list, list):
|
|
self.value_list = db_string_or_list
|
|
else:
|
|
self.value_list = [int(s) for s in db_string_or_list.split(".")]
|
|
# For MHX, Gen, no purple sharpness, but keep model the same for
|
|
# simplicity
|
|
if len(self.value_list) < SharpnessLevel.PURPLE + 1:
|
|
self.value_list.append(0)
|
|
self._max = None
|
|
|
|
@property
|
|
def max(self):
|
|
if self._max is None:
|
|
self._max = SharpnessLevel.RED
|
|
for i in xrange(SharpnessLevel.PURPLE+1):
|
|
if self.value_list[i] == 0:
|
|
break
|
|
else:
|
|
self._max = i
|
|
return self._max
|
|
|
|
def as_data(self):
|
|
return self.value_list
|
|
|
|
|
|
class ItemCraftable(RowModel):
|
|
_list_fields = ["id", "name"]
|
|
|
|
def __init__(self, item_row):
|
|
super(ItemCraftable, self).__init__(item_row)
|
|
self.create_components = None
|
|
self.upgrade_components = None
|
|
|
|
def set_components(self, create_components, upgrade_components):
|
|
self.create_components = create_components
|
|
self.upgrade_components = upgrade_components
|
|
|
|
def as_data(self):
|
|
data = super(ItemCraftable, self).as_data()
|
|
if self.create_components is not None:
|
|
data["create_components"] = dict((item.name, item.quantity)
|
|
for item in self.create_components)
|
|
if self.upgrade_components is not None:
|
|
data["upgrade_components"] = dict((item.name, item.quantity)
|
|
for item in self.upgrade_components)
|
|
return data
|
|
|
|
|
|
class ItemWithSkills(ItemCraftable):
|
|
def __init__(self, item_row):
|
|
super(ItemWithSkills, self).__init__(item_row)
|
|
self.skills = None
|
|
self.skill_ids = []
|
|
self.skill_names = []
|
|
|
|
def set_skills(self, item_skills):
|
|
self.skills = {}
|
|
for s in item_skills:
|
|
self.skills[s.skill_tree_id] = s.point_value
|
|
self.skills[s.name] = s.point_value
|
|
self.skill_ids.append(s.skill_tree_id)
|
|
self.skill_names.append(s.name)
|
|
|
|
def skill(self, skill_id_or_name):
|
|
return self.skills.get(skill_id_or_name, 0)
|
|
|
|
def one_line_skills_u(self, skill_names=None):
|
|
"""
|
|
Get comma separated list of skills on the item. If @skill_names
|
|
is passed, only include skills that are in that list.
|
|
"""
|
|
if skill_names is None:
|
|
skill_names = sorted(self.skill_names)
|
|
return ", ".join("%s %d" % (name, self.skills[name])
|
|
for name in skill_names
|
|
if name in self.skills)
|
|
|
|
def as_data(self):
|
|
data = super(ItemWithSkills, self).as_data()
|
|
if self.skills is not None:
|
|
data["skills"] = dict((name, self.skills[name])
|
|
for name in self.skill_names)
|
|
#data["skills_by_id"] = dict((sid, self.skills[sid])
|
|
# for sid in self.skill_ids)
|
|
return data
|
|
|
|
|
|
class Armor(ItemWithSkills):
|
|
_indexes = { "name": "id",
|
|
"slot": "name" }
|
|
|
|
_one_line_template = string.Template(
|
|
"$name ($slot) Def $defense-$max_defense Slot $num_slots"
|
|
)
|
|
|
|
def __init__(self, armor_item_row):
|
|
super(Armor, self).__init__(armor_item_row)
|
|
|
|
def one_line_u(self):
|
|
return self._one_line_template.substitute(self.as_data())
|
|
|
|
def skill(self, skill_id_or_name, decoration_values=()):
|
|
"""
|
|
Get total value of skill from the armor and decorations based on
|
|
the number of slots.
|
|
|
|
decoration_values should be a list of points from the given
|
|
number of slots, e.g. [1, 3] or [1, 3, 0] means that one slot
|
|
gets 1 point and two slots get 3 points, [1, 0, 4] means that
|
|
one slot gets 1 point, there is no two slot gem, and three slots
|
|
gets 4 points. If not passed, just returns native skill points.
|
|
"""
|
|
assert self.skills is not None
|
|
total = self.skills.get(skill_id_or_name, 0)
|
|
slots_left = self.num_slots
|
|
for slots in xrange(len(decoration_values), 0, -1):
|
|
if slots_left == 0:
|
|
break
|
|
decoration_value = decoration_values[slots-1]
|
|
if not decoration_value:
|
|
continue
|
|
while slots <= slots_left:
|
|
total += decoration_value
|
|
slots_left -= slots
|
|
return total
|
|
|
|
|
|
class Decoration(ItemWithSkills):
|
|
pass
|
|
|
|
|
|
class ItemSkill(RowModel):
|
|
pass
|
|
|
|
|
|
class SkillTree(RowModel):
|
|
_list_fields = ["id", "name"]
|
|
|
|
def __init__(self, skill_tree_row):
|
|
super(SkillTree, self).__init__(skill_tree_row)
|
|
self.decoration_values = None
|
|
self.decoration_ids = None
|
|
|
|
def set_decorations(self, decorations):
|
|
if decorations is None:
|
|
self.decoration_values = None
|
|
else:
|
|
self.decoration_ids, self.decoration_values = \
|
|
get_decoration_values(self.id, decorations)
|
|
|
|
def as_data(self):
|
|
data = super(SkillTree, self).as_data()
|
|
if self.decoration_values is not None:
|
|
data["decoration_values"] = self.decoration_values
|
|
data["decoration_ids"] = self.decoration_ids
|
|
return data
|
|
|
|
|
|
class Skill(RowModel):
|
|
_list_fields = ["id", "name"]
|
|
_indexes = { "skill_tree_id":
|
|
["id", "required_skill_tree_points", "name", "description"] }
|
|
|
|
def __init__(self, skill_row):
|
|
super(Skill, self).__init__(skill_row)
|
|
self.skill_tree = None
|
|
|
|
def set_skill_tree(self, skill_tree):
|
|
assert skill_tree.id == self.skill_tree_id
|
|
self.skill_tree = skill_tree
|
|
|
|
|
|
class Weapon(ItemCraftable):
|
|
_list_fields = ["id", "wtype", "name"]
|
|
_indexes = { "name": "id",
|
|
"wtype": ["id", "name"],
|
|
# subset of all data that can be used for searching and
|
|
# not be too bloated
|
|
"id": ["name", "wtype", "final", "element", "element_2",
|
|
"awaken"] }
|
|
|
|
def __init__(self, weapon_item_row):
|
|
super(Weapon, self).__init__(weapon_item_row)
|
|
self._parse_sharpness()
|
|
|
|
def _parse_sharpness(self):
|
|
"""
|
|
Replace the sharpness field with parsed models for the normal
|
|
sharpness and the sharpness with Sharpness+1 skill.
|
|
"""
|
|
if self.wtype in ("Light Bowgun", "Heavy Bowgun", "Bow"):
|
|
self._data["sharpness"] = self._data["sharpness_plus"] = None
|
|
return
|
|
if isinstance(self._row["sharpness"], list):
|
|
# MHX JSON data, already desired format, but doesn't have
|
|
# purple so we append 0
|
|
self.sharpness = WeaponSharpness(self._row["sharpness"] + [0])
|
|
self.sharpness_plus = WeaponSharpness(
|
|
self._row["sharpness_plus"] + [0])
|
|
self.sharpness_plus2 = WeaponSharpness(
|
|
self._row["sharpness_plus2"] + [0])
|
|
else:
|
|
# 4U or gen data from db
|
|
parts = self._row["sharpness"].split(" ")
|
|
if len(parts) == 2:
|
|
normal, plus = parts
|
|
plus2 = plus
|
|
elif len(parts) == 3:
|
|
normal, plus, plus2 = parts
|
|
else:
|
|
raise ValueError("Bad sharpness value in db: '%s'"
|
|
% self._row["sharpness"])
|
|
self._data["sharpness"] = WeaponSharpness(normal)
|
|
self._data["sharpness_plus"] = WeaponSharpness(plus)
|
|
self._data["sharpness_plus2"] = WeaponSharpness(plus2)
|
|
|
|
def is_not_localized(self):
|
|
# Check if first char is ascii, should be the case for all
|
|
# english weapons, and not for Japanese DLC weapons.
|
|
return ord(self.name[0]) < 128
|
|
|
|
|
|
class Monster(RowModel):
|
|
_list_fields = ["id", "class", "name"]
|
|
|
|
|
|
class Item(RowModel):
|
|
_list_fields = ["id", "type", "name"]
|
|
_indexes = { "name": ["id"],
|
|
"type": ["id", "name"] }
|
|
|
|
|
|
class ItemComponent(RowModel):
|
|
_list_fields = ["id", "name"]
|
|
_indexes = { "method": ["id", "name"] }
|
|
|
|
|
|
|
|
class Location(RowModel):
|
|
pass
|
|
|
|
|
|
class HornMelody(RowModel):
|
|
_list_fields = ["notes", "song", "effect1", "effect2",
|
|
"duration", "extension"]
|
|
_indexes = { "notes": ["song", "effect1", "effect2", "duration",
|
|
"extension"] }
|
|
|
|
|
|
class MonsterPartStateDamage(RowModel):
|
|
"""
|
|
Model for the damage to the monster on a particular hitbox and in
|
|
a particulare state.
|
|
"""
|
|
_exclude_fields = ["monster_id", "body_part"]
|
|
|
|
def __init__(self, part, state, row):
|
|
super(MonsterPartStateDamage, self).__init__(row)
|
|
self._data["part"] = part
|
|
self._data["state"] = state
|
|
|
|
def __eq__(self, other):
|
|
for col in "impact cut shot ko ice dragon water fire thunder".split():
|
|
if self[col] != other[col]:
|
|
return False
|
|
return True
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
|
|
class MonsterPartDamage(ModelBase):
|
|
"""
|
|
Model for collecting the damage to the monster on a particular hitbox
|
|
across different states.
|
|
"""
|
|
def __init__(self, part):
|
|
self.part = part
|
|
self.breakable = False
|
|
self.states = {}
|
|
|
|
def add_state(self, state, damage_row):
|
|
self.states[state] = MonsterPartStateDamage(self.part, state,
|
|
damage_row)
|
|
# TODO: what about state 'Without Hide' for S.Nerscylla, which
|
|
# appears like it might be the same as Break Part, or might
|
|
# affect across hitzones.
|
|
if state == "Break Part":
|
|
# the default damage should be sorted before the alternate
|
|
# state damage
|
|
assert "Default" in self.states
|
|
if self.states[state] != self.states["Default"]:
|
|
# if the damage is different for break state, the part
|
|
# must be breakable, even if we couldn't find a match
|
|
# when searching break rewards
|
|
# print "%s is breakable [by hitzone diff]" % self.part
|
|
self.breakable = True
|
|
|
|
def as_data(self):
|
|
return dict(
|
|
breakable=self.breakable,
|
|
damage=self.states
|
|
)
|
|
|
|
|
|
class MonsterDamage(ModelBase):
|
|
"""
|
|
Model for the damage weakness to the monster in all the
|
|
different states and all the different hitboxes.
|
|
"""
|
|
def __init__(self, damage_rows):
|
|
self._rows = damage_rows
|
|
self.parts = {}
|
|
self.states = set()
|
|
for row in damage_rows:
|
|
if row["cut"] == -1:
|
|
# -1 indicates missing data
|
|
continue
|
|
part = row["body_part"]
|
|
state = "Default"
|
|
m = re.match(r"([^(]+) \(([^)]+)\)", part)
|
|
if m:
|
|
part = m.group(1)
|
|
state = m.group(2)
|
|
self.states.add(state)
|
|
if part not in self.parts:
|
|
self.parts[part] = MonsterPartDamage(part)
|
|
self.parts[part].add_state(state, row)
|
|
|
|
def as_data(self):
|
|
return dict(
|
|
states=list(self.states),
|
|
parts=self.parts
|
|
)
|
|
|
|
def set_breakable(self, breakable_list):
|
|
"""
|
|
Set breakable flag on parts based on the breakable list from
|
|
rewards (use MHDB.get_monster_breaks).
|
|
"""
|
|
for name, part_damage in self.parts.iteritems():
|
|
if _break_find(name, self.parts, breakable_list):
|
|
#print "part %s is breakable [by rewards]" % name
|
|
part_damage.breakable = True
|
|
|
|
|
|
def get_decoration_values(skill_id, decorations):
|
|
"""
|
|
Given a list of decorations that provide the specified skill_id,
|
|
figure out the best decoration for each number of slots from
|
|
one to three. Returns (id_list, value_list), where both are 3 element
|
|
arrays and id_list contains the decoration ids, and value_list contains
|
|
the number of skill points provided by each.
|
|
"""
|
|
# TODO: write script to compute this and shove into skill_tree table
|
|
values = [0, 0, 0]
|
|
ids = [None, None, None]
|
|
for d in decorations:
|
|
assert d.num_slots is not None
|
|
# some skills like Handicraft have multiple decorations with
|
|
# same number of slots - use the best one
|
|
new = d.skills[skill_id]
|
|
current = values[d.num_slots-1]
|
|
if new > current:
|
|
values[d.num_slots-1] = new
|
|
ids[d.num_slots-1] = d.id
|
|
return (ids, values)
|
|
|
|
|
|
def _break_find(part, parts, breaks):
|
|
# favor 'Tail Tip' over Tail for Basarios
|
|
if part == "Tail" and "Tail Tip" in parts:
|
|
return None
|
|
if part == "Tail Tip" and "Tail" in breaks:
|
|
return "Tail"
|
|
if part == "Neck/Tail" and "Tail" in breaks:
|
|
return "Tail"
|
|
if part == "Wing" and "Wing" not in breaks:
|
|
if "Talon" in breaks and "Talon" not in parts:
|
|
# for Teostra
|
|
return "Talon"
|
|
if part == "Head" and "Head" not in breaks:
|
|
if "Horn" in breaks and "Horn" not in parts:
|
|
# for Fatalis
|
|
return "Horn"
|
|
if "Ear" in breaks and "Ear" not in parts:
|
|
# Kecha Wacha
|
|
return "Ear"
|
|
if part == "Winglegs" and "Winglegs" not in breaks:
|
|
if "Wing Leg" in breaks and "Wing Leg" not in parts:
|
|
# for Gore
|
|
return "Wing Leg"
|
|
#print "part_find", part, breaks
|
|
matches = difflib.get_close_matches(part, breaks, 1, 0.8)
|
|
if matches:
|
|
return matches[0]
|
|
return None
|
|
|
|
|
|
def get_costs(db, weapon):
|
|
"""
|
|
Get a list of alternative ways of making a weapon, as a list of dicts
|
|
containing item counts. The dicts also contain special keys _zenny
|
|
for the total zenny needed, and _path for a list of weapons that
|
|
make up the upgrade path.
|
|
"""
|
|
costs = []
|
|
if weapon.parent_id:
|
|
if not weapon.upgrade_cost:
|
|
# db has errors where upgrade cost is listed as create
|
|
# cost and components are listed under create. Assume
|
|
# parent_id is correct, and they are upgrade only.
|
|
if not weapon.upgrade_components and weapon.create_components:
|
|
weapon.upgrade_components = weapon.create_components
|
|
weapon.create_components = []
|
|
weapon.upgrade_cost = weapon.creation_cost
|
|
weapon.creation_cost = 0
|
|
try:
|
|
upgrade_cost = int(weapon.upgrade_cost)
|
|
except ValueError:
|
|
upgrade_cost = 0
|
|
print "WARN: bad upgrade cost for '%s' (%s): '%s'" \
|
|
% (weapon.name, weapon.id, weapon.upgrade_cost)
|
|
except UnicodeError:
|
|
upgrade_cost = 0
|
|
cost_display = urllib.quote(weapon.upgrade_cost)
|
|
print "WARN: bad upgrade cost for '%s' (%s): '%s'" \
|
|
% (weapon.name, weapon.id, cost_display)
|
|
parent_weapon = db.get_weapon(weapon.parent_id)
|
|
costs = get_costs(db, parent_weapon)
|
|
for cost in costs:
|
|
cost["zenny"] += upgrade_cost
|
|
cost["path"] += [weapon]
|
|
for item in weapon.upgrade_components:
|
|
if item.type == "Weapon":
|
|
continue
|
|
if item.name not in cost["components"]:
|
|
cost["components"][item.name] = 0
|
|
cost["components"][item.name] += item.quantity
|
|
if weapon.create_components:
|
|
try:
|
|
zenny = int(weapon.creation_cost)
|
|
except ValueError:
|
|
print "WARN: bad creation cost for '%s': '%s'" \
|
|
% (weapon.name, weapon.creation_cost)
|
|
zenny = weapon.upgrade_cost or 0
|
|
create_cost = dict(zenny=zenny,
|
|
path=[weapon],
|
|
components={})
|
|
for item in weapon.create_components:
|
|
create_cost["components"][item.name] = item.quantity
|
|
costs = [create_cost] + costs
|
|
return costs
|
|
|
|
|
|
class ItemStars(object):
|
|
"""
|
|
Get the game progress (in hub stars) required to make an item. Caches
|
|
values.
|
|
"""
|
|
|
|
def __init__(self, db):
|
|
self.db = db
|
|
self._item_stars = {} # item id -> stars dict
|
|
self._weapon_stars = {} # weapon id -> stars dict
|
|
|
|
def get_weapon_stars(self, weapon):
|
|
"""
|
|
Get lowest star levels needed to make weapon, among the different
|
|
paths available.
|
|
"""
|
|
stars = self._weapon_stars.get(weapon.id)
|
|
if stars is not None:
|
|
return stars
|
|
|
|
stars = dict(Village=None, Guild=None, Permit=None, Arena=None)
|
|
costs = get_costs(self.db, weapon)
|
|
# find least 'expensive' path
|
|
for c in costs:
|
|
current_stars = self._get_component_stars(c)
|
|
for k, v in current_stars.iteritems():
|
|
if v is None:
|
|
continue
|
|
if stars[k] is None or v < stars[k]:
|
|
stars[k] = v
|
|
self._weapon_stars[weapon.id] = stars
|
|
return stars
|
|
|
|
def _get_component_stars(self, c):
|
|
# need to track unititialized vs unavailable
|
|
stars = dict(Village=0, Guild=0, Permit=0, Arena=0)
|
|
for item_name in c["components"].keys():
|
|
item = self.db.get_item_by_name(item_name)
|
|
if item.type == "Materials":
|
|
current_stars = self.get_material_stars(item.id)
|
|
else:
|
|
current_stars = self.get_item_stars(item.id)
|
|
# keep track of most 'expensive' item
|
|
for k, v in current_stars.items():
|
|
if stars[k] is None:
|
|
# another item was unavailable from the hub
|
|
continue
|
|
if v is None:
|
|
if k == "Village" and current_stars["Guild"] is not None:
|
|
# available from guild and not from village,
|
|
# e.g. certain HR parts. Mark entire item as
|
|
# unavailable from village, don't allow override.
|
|
stars[k] = None
|
|
continue
|
|
if v > stars[k]:
|
|
stars[k] = v
|
|
|
|
# check for hubs that had no candidate item, and null them out
|
|
for k in list(stars.keys()):
|
|
if stars[k] == 0:
|
|
stars[k] = None
|
|
return stars
|
|
|
|
def get_material_stars(self, material_item_id):
|
|
"""
|
|
Find the level of the cheapest item that satisfies the material
|
|
that is not a scrap.
|
|
"""
|
|
stars = self._item_stars.get(material_item_id)
|
|
if stars is not None:
|
|
return stars
|
|
|
|
stars = dict(Village=None, Guild=None, Permit=None, Arena=None)
|
|
rows = self.db.get_material_items(material_item_id)
|
|
for row in rows:
|
|
item = self.db.get_item(row["item_id"])
|
|
if "Scrap" in item.name:
|
|
continue
|
|
stars = self.get_item_stars(item.id)
|
|
break
|
|
self._item_stars[material_item_id] = stars
|
|
return stars
|
|
|
|
def get_item_stars(self, item_id):
|
|
stars = self._item_stars.get(item_id)
|
|
if stars is not None:
|
|
return stars
|
|
|
|
stars = dict(Village=None, Guild=None, Permit=None, Arena=None)
|
|
|
|
quests = self.db.get_item_quests(item_id)
|
|
|
|
gathering = self.db.get_item_gathering(item_id)
|
|
gather_locations = set()
|
|
for gather in gathering:
|
|
gather_locations.add((gather["location_id"], gather["rank"]))
|
|
for location_id, rank in list(gather_locations):
|
|
gather_quests = self.db.get_location_quests(location_id, rank)
|
|
quests.extend(gather_quests)
|
|
|
|
monsters = self.db.get_item_monsters(item_id)
|
|
monster_ranks = set()
|
|
for monster in monsters:
|
|
monster_ranks.add((monster["monster_id"], monster["rank"]))
|
|
for monster_id, rank in list(monster_ranks):
|
|
monster_quests = self.db.get_monster_quests(monster_id, rank)
|
|
quests.extend(monster_quests)
|
|
|
|
# find least expensive quest for getting the item
|
|
for quest in quests:
|
|
if quest.stars == 0:
|
|
# ignore training quests
|
|
if "Training" not in quest.name:
|
|
print "Error: non training quest has 0 stars", \
|
|
quest.id, quest.name
|
|
continue
|
|
if quest.hub in stars:
|
|
current = stars[quest.hub]
|
|
if current is None or quest.stars < current:
|
|
stars[quest.hub] = quest.stars
|
|
else:
|
|
print "Error: unknown hub", quest.hub
|
|
|
|
# if available guild or village, then null out permit/arena values,
|
|
# because they are more useful for filtering if limited to items
|
|
# exclusively available from permit or arena. Allows matching
|
|
# on based on meeting specified critera for
|
|
# (guild or village) and permit and arena.
|
|
if stars["Village"] or stars["Guild"]:
|
|
stars["Permit"] = None
|
|
stars["Arena"] = None
|
|
|
|
self._item_stars[item_id] = stars
|
|
return stars
|
|
|