def __init__(self, filename, monstats, superuniques, superuniques2): D2Data.__init__(self, filename, 'Id') # build monster_id -> [level_id] map norm_cols = ['mon'+str(i) for i in range(1,11)] nmh_cols = ['n'+s for s in norm_cols] self.monstats = monstats self.superuniques = superuniques self.superuniques2 = superuniques2 self.norm_monmap = {} self.nmh_monmap = {} self.make_monmap(self.norm_monmap, norm_cols) self.make_monmap(self.nmh_monmap, nmh_cols)
def __init__(self, filename): D2Data.__init__(self, filename, 'Treasure_Class') self.group_map = {} for tc in self.data['Treasure_Class']: if tc: group = self.get_data(tc, 'group') if group > 0: if group not in self.group_map: self.group_map[group] = [] self.group_map[group].append({'Treasure_Class': tc, 'level': self.get_data(tc, 'level')}) # make sure each group list is sorted by level for group, tcs in self.group_map.items(): tcs.sort(key=lambda x: x['level'])
class Set(): sets = D2Data('data/global/excel/Sets.txt', 'index') def __init__(self, set_id): self.set_id = set_id logger.debug("{} is a {} piece set.".format(self.set_name(), self.num_items())) def num_items(self): return np.sum(SetItem.setitems.data['set'] == self.set_id) def sets_data(self, col): """Get data from a column of Sets.txt.""" return self.sets.get_data(self.set_id, col) def set_name(self): """Return the name of the set.""" return self.sets.get_data(self.set_id, 'name') def _attributes(self, prefix, suffix, start, stop): # partial set bonuses first for i in range(start, stop): for c in suffix: propstr = '{}Code{}{}'.format(prefix,i,c) parstr = '{}Param{}{}'.format(prefix,i,c) minstr = '{}Min{}{}'.format(prefix,i,c) maxstr = '{}Max{}{}'.format(prefix,i,c) prop = self.sets_data(propstr) par = self.sets_data(parstr) min_ = self.sets_data(minstr) max_ = self.sets_data(maxstr) # bool check is because an empty column has bool datatype # TODO: Figure out a better way to deal with this before it's all over the place if prop.dtype != 'bool' and prop != '': logger.debug("Found property {} to include from {} set.".format(prop, self.set_name()) + " This property calls funcions {}.".format(list(Stat.property_functions(prop))) + " Arguments to property function: param={} min={} max={}".format(par, min_, max_)) for property_function in Stat.property_functions(prop): stat = Stat.create_stat(**property_function, param=par, min_=min_, max_=max_) logger.debug("Created stat {}.".format(stat)) def attributes(self, num_items): self._attributes('P', ['a','b'], 2, num_items+1) if num_items == self.num_items(): self._attributes('F', [''], 1, 9)
def __init__(self, filename): D2Data.__init__(self, filename, 'constant_desc', usecols=['constant_desc', 'constants'])
def __init__(self, filename): D2Data.__init__(self, filename, 'Superunique')
def __init__(self, filename): D2Data.__init__(self, filename, 'Id')
def __init__(self, filename): D2Data.__init__(self, filename, 'Level')
class SetItem(Item): # map the set_id from d2s parser to the index used by SetItems.txt setitems2 = D2Data('data2/SetItems2.txt', 'set_id') setitems = D2Data('data/global/excel/SetItems.txt', 'index') def __init__(self, itemdata): Item.__init__(self, itemdata) try: self.set_index = self.setitems2.get_data(itemdata['set_id'], 'index') #self.set = Set(self.sets_key()) logger.debug("Creating {} set item {}.".format(self.sets_key(), self.set_index)) except KeyError as e: logger.error("Set item by quality has no set_id. JSON dump: {}".format(itemdata)) raise def setitems_key(self): """Return the key for lookups in the SetItems.txt file.""" return self.set_index def sets_key(self): """Return the key for lookups in the Sets.txt file.""" return self.setitems.get_data(self.set_index, 'set') def setitems_data(self, col): """Get data from a column of SetItems.txt.""" return self.setitems.get_data(self.set_index, col) def all_set_attributes(self): """Return an iterator for lists of set item attributes, active or not. Set items have attributes organized as a list of lists. The inner lists contain the actual attributes. The outer list is for groups of attributes. These attributes are grouped because of the way set bonuses are applied. The first group is applied with x many items, second group with y many, etc.""" for attr_list in self.attributes('set_attributes'): yield attr_list def set_attributes(self, num_items): """Return an iterator for active set attributes.""" # first figure out if bonuses on this item depend on total items equipped or specific items equipped # (Civerb's shield is the only one in the latter category) if self.setitems_data('add_func') == 1: # add the stats based on which other specific items are present logger.error("Sets items with bonuses dependent on specific set items (e.g. Civerb's shield) are not" " yet supported. Bonuses will not be applied on {}".format(self.setitems_key())) elif self.setitems_data('add_func') == 2: # add the stats based on total number of unique items present # first grab the set attributes iterator for the item. This is intentionally # only initialized once, and not again in the inner loop. It should advance each # time we match the exepcted stats from ItemStatCost with the attributes in the list. set_attr_iter = self.all_set_attributes() try: for i in range(1, num_items): stat_ids = [] for c in ['a','b']: propstr = 'aprop{}{}'.format(i,c) #parstr = 'apar{}{}'.format(i,c) #minstr = 'amin{}{}'.format(i,c) #maxstr = 'amax{}{}'.format(i,c) prop = self.setitems_data(propstr) #par = item.setitems_data(parstr) #min_ = item.setitems_data(minstr) #max_ = item.setitems_data(maxstr) # bool check is because an empty column has bool datatype # TODO: Figure out a better way to deal with this before it's all over the place if prop.dtype != 'bool' and prop != '': logger.debug("Found property {} to include on {}.".format(prop, self.setitems_key()) + " This property adds stats {}.".format([x['stat'] for x in list(Stat.property_functions(prop))])) stat_ids += list(Stat.property_functions(prop)) # we need to find the attribute(s) in the d2s parser that matches the stat ids we look # up from the property to add. We could attempt to look up the stat values themselves # in the txt files, but this isn't the right way to do it. Some stat bonuses on items # are actually variable (see Civerb's shield), so we should respect the values in the # d2s file. if len(stat_ids) > 0: # above condition means there is a bonus we should apply, now we need to match it # to the d2s attributes for attr_list in set_attr_iter: tmp_map = {} Stat.add_attributes_to_map(iter(attr_list), tmp_map) if set(tmp_map.keys()) == set([x['stat'] for x in stat_ids]): logger.debug("Attributes {} active on {}.".format(attr_list, self.setitems_key())) for attr in attr_list: yield attr break else: raise PydiabloError("Attributes {} did not match expected stat ids {} on {}.".format(attr_list, stat_ids, self.setitems_key())) except PydiabloError as e: logger.error("Problem matching the set bonuses from d2s to those expected" " by SetItems.txt ({}). Don't trust set bonuses on this item.".format(str(e))) return
class Stat: """This class is a collection of static functions for now. """ #itemstatcost = D2Data('data/global/excel/ItemStatCost.txt', 'Stat') itemstatcostid = D2Data('data/global/excel/ItemStatCost.txt', 'ID', usecols=[0,1]); properties = D2Data('data/global/excel/Properties.txt', 'code', usecols=range(30)) @classmethod def attribute_to_stats(cls, attr): # first handle some special cases where the d2s parser # combined some stats into ranges. if attr['id'] in [17, 48, 50, 52, 54, 57]: stats = [] for i, value in enumerate(attr['values']): stat = cls.itemstatcostid.get_data(attr['id']+i, 'Stat') stats.append({'stat': stat, 'values': [value]}) return stats # next deal with the properties giving charges. if attr['id'] in range(204,214): # override the stat reference to point to a new dict that we will # modify so that charges fits in better to our scheme. We recombine # the current and maximum charges into one number. new_attr = {} new_attr['id'] = attr['id'] try: # MSB is maximum charges, LSB is current charges new_attr['values'] = [attr['values'][0], attr['values'][1], attr['values'][2] + 2**8*attr['values'][3]] except IndexError as e: logger.error("Unexpected values field in item charges attribute. JSON dump: {}".format(attr)) raise attr = new_attr # next handle the general case. stat = cls.itemstatcostid.get_data(attr['id'], 'Stat') return [{'stat': stat, 'values': attr['values']}] @classmethod def add_attributes_to_map(cls, attr_iterator, stat_map): """Add attributes from the item to the stat map. Positional arguments: attr_iterator -- an iterator for the item attributes. stat_map -- add stats to this map First, some terminology. Nokka's d2s parser gives 'attributes' for the items. These 'attributes' are a little different than the 'stats' in ItemStatCost.txt. When referring to the stat as it exists in the JSON from the d2s parser, I will use the term 'attribute'. When referring to a stat consistent with ItemStatCost.txt, I will use the term 'stat'. attr_iterator must yield a map with an id and values field and can be created with the generator methods in the Item class. These maps are expected to follow the format of nokka's d2s parser. When converting from attribute to stat, we change a few things, notably with combined stat ranges (min-max dmg) and with charges. The stat_map will contain all item stats, keyed by stat id (ItemStatCost.txt). In the case of a simple stat (one value), the value for the stat id will be a list of all instance values of that stat. In the case of a complex stat, the value for the stat id will be another map, keyed by parameter. simple attribute: > stat_map[141] # deadly strike [20] complex attribute: > stat_map[204][62][30] # level 30 (30) hydra (62) charges (204 is the stat id) [2570] The game stores the current and max charges as one 16 bit value. In this case, there are 10 current charges (LSB) and 10 max (MSB): 2570 = 0x0A0A. """ for attr in attr_iterator: for stat in cls.attribute_to_stats(attr): mdict = stat_map mkey = stat['stat'] for value in attr['values'][:-1][::-1]: if mkey not in mdict: mdict[mkey] = {} mdict = mdict[mkey] mkey = value if mkey not in mdict: mdict[mkey] = [] mdict[mkey].append(attr['values'][-1]) @staticmethod def create_stat(func, stat, set_, val, param, min_, max_, rand=False): """Return a newly created stat as a dict with 'stat_id' and 'values' fields. The values are ordered consistenly with the item stat order in the d2s file. """ if rand: logger.error("Random generation of stats not yet supported.") # The funciton mapping below was reverse engineered from vanilla game data # and by comparing to the item stat order in the d2s file (easy to see in nokka's parser). # It may not be completely accurate. Surely there are some differences between # the otherwise identical functions. # TODO: func3 is same as 1, but it should reuse the func1 rolls. if func in [1, 3, 8]: #stat_id = cls.itemstatcost.get_data(stat, 'ID') return {'stat': stat, 'values': [(min_+max_)//2]} if func==21: #stat_id = cls.itemstatcost.get_data(stat, 'ID') return {'stat': stat, 'values': [val, (min_+max_)//2]} else: return {} @classmethod def property_functions(cls, prop): """Yield a map containing 'set', 'val', 'func', and 'stat' fields for each stat associated with the property.""" for i in range(1,8): # 7 maximum stats per property stat = cls.properties.get_data(prop, 'stat{}'.format(i)) #stat_id = cls.itemstatcost.get_data(stat, 'ID') set_ = cls.properties.get_data(prop, 'set{}'.format(i)) val = cls.properties.get_data(prop, 'val{}'.format(i)) func = cls.properties.get_data(prop, 'func{}'.format(i)) if func.dtype == 'bool' or func == -1: return # no additional stats to yield yield {'stat': stat, 'set_': set_, 'val': val, 'func': func}