def is_valid_classification(self, dbr, dbr_file, result): """ Check if this item is of a valid classification for TQDB. """ itemClass = dbr.get('Class') classification = dbr.get('itemClassification', None) if (itemClass not in self.ALLOWED and classification not in self.CLASSIFICATIONS.keys()): raise StopIteration elif (classification in self.CLASSIFICATIONS.keys() and 'classification' not in result): # Only add the classification if it doesn't exist yet: result['classification'] = texts.get( self.CLASSIFICATIONS[classification]).strip() # For Monster Infrequents, make sure a drop difficulty exists: if classification == 'Rare': file_name = os.path.basename(dbr_file).split('_') if len(file_name) < 2 or file_name[1] not in DIFFICULTIES: raise StopIteration # Set the difficulty for which this MI drops: result['dropsIn'] = texts.get( DIFFICULTIES[file_name[1]]).strip()
def is_valid_classification(self, dbr, dbr_file, result): """ Check if this item is of a valid classification for TQDB. """ itemClass = dbr.get('Class') classification = dbr.get('itemClassification', None) if (itemClass not in self.ALLOWED and classification not in self.CLASSIFICATIONS.keys()): raise InvalidItemError(f"Item {dbr_file} is excluded due to Class " f"{itemClass} with itemClassification " f"{classification}.") elif (classification in self.CLASSIFICATIONS.keys() and 'classification' not in result): # Only add the classification if it doesn't exist yet: result['classification'] = texts.get( self.CLASSIFICATIONS[classification]).strip() # For Monster Infrequents, make sure a drop difficulty exists: if classification == 'Rare': file_name = os.path.basename(dbr_file).split('_') if len(file_name) < 2 or file_name[1] not in DIFFICULTIES: raise InvalidItemError(f"File name {file_name} does not " "specify difficulty, or difficulty " "not recognized.") # Set the difficulty for which this MI drops: result['dropsIn'] = texts.get( DIFFICULTIES[file_name[1]]).strip()
def parse(self, dbr, dbr_file, result): """ Parse skill augments, mastery augments, and item granted skills. """ self._parse_skill_grant(dbr, result) # Parse skills that are augmented: for name, level in self.SKILL_AUGMENTS.items(): # Skip skills without both the name and level set: if name not in dbr or level not in dbr: continue skill = DBRParser.parse(dbr[name]) # Store the skill, which will ensure a unique tag: skill_tag = storage.store_skill(skill) level = dbr[level] # Skill format is either ItemSkillIncrement or ItemMasteryIncrement skill_format = (self.TXT_SKILL_INC if 'Mastery' not in skill['name'] else self.TXT_MASTERY_INC) result['properties'][name] = { 'tag': skill_tag, 'name': texts.get(skill_format).format(level, skill['name']), } # Parse augment to all skills: if self.AUGMENT_ALL in dbr: level = dbr[self.AUGMENT_ALL] result['properties'][self.AUGMENT_ALL] = ( texts.get(self.TXT_ALL_INC).format(level))
def parse_global(self, key, chance, xor, all_fields, index): """ Add a global chance for properties. This is where multiple properties can be triggered by a chance, like 10% Chance for one of the following: - ... - ... """ # Grab the value for this index from the global stores: fields = dict((k, v[index]) if index < len(v) # If this value isn't repeated, grab the last value else (k, v[-1]) for k, v in all_fields.items()) # Check if the XOR was set for any field: if xor: value = { 'chance': (texts.get(GLOBAL_XOR_ALL) if chance == 100 else texts.get(GLOBAL_XOR_PCT).format(chance)), 'properties': fields, } else: value = { 'chance': (texts.get(GLOBAL_ALL).format('') if chance == 100 else texts.get(GLOBAL_PCT).format(chance)), 'properties': fields, } # Insert the value normally TQDBParser.insert_value(key, value, self.result)
def parse_creature(self, dbr, dbr_file, result): """ Parse the creature and its properties and skills. """ # Grab the first set of properties: classification = dbr.get('monsterClassification', 'Common') tag = dbr.get('description', None) # Set the known properties for this creature if tag: race = dbr.get('characterRacialProfile', None) result.update({ 'classification': classification, 'name': texts.get(tag), 'race': race[0] if race else None, 'level': [level for level in dbr.get('charLevel', [])], 'tag': tag, }) # Manually parse the defensive properties, since there's no template # tied for it for monsters: parsers.ParametersDefensiveParser().parse(dbr, dbr_file, result) # Iterate over the properties for each difficulty: properties = [] for i in range(3): properties.append({}) itr = TQDBParser.extract_values(dbr, '', i) # Set this creature's HP and MP as stats, not as bonuses: if self.HP in itr: hp = itr[self.HP] properties[i][self.HP] = texts.get('LifeText').format(hp) if self.MP in itr: mp = itr[self.MP] properties[i][self.MP] = texts.get('ManaText').format(mp) # Add non-character properties: for k, v in result['properties'].items(): if k.startswith('character'): continue # Add the property to the correct difficulty index: if isinstance(v, list): # The property changes per difficulty: properties[i][k] = v[i] if i < len(v) else v[-1] else: # The property is constant: properties[i][k] = v # Add the base damage, stats, regens, and resistances: result['properties'] = properties
def parse(self, dbr, dbr_file, result): """ Parse the skill that spawns a pet. """ # Only set time to live it's set (otherwise it's infinite) ttl_list = dbr.get(self.TTL, None) # Parse all the summons and set them as a list: result['summons'] = [] for index, spawn_file in enumerate(dbr['spawnObjects']): spawn = DBRParser.parse(spawn_file) # Keep track of the original properties this summon had: original_properties = {} if 'properties' in spawn: if isinstance(spawn['properties'], list): original_properties = spawn['properties'][0].copy() else: original_properties = spawn['properties'].copy() # We need the raw values from the spawn DBR for hp/mp spawn['properties'] = {} spawn_dbr = DBRParser.read(spawn_file) if 'characterLife' in spawn_dbr: hp_list = spawn_dbr['characterLife'] hp = (hp_list[index] if index < len(hp_list) else hp_list[len(hp_list) - 1]) TQDBParser.insert_value('characterLife', texts.get('LifeText').format(hp), spawn) if 'characterMana' in spawn_dbr: mp_list = spawn_dbr['characterMana'] mp = (mp_list[index] if index < len(mp_list) else mp_list[len(mp_list) - 1]) TQDBParser.insert_value('characterMana', texts.get('ManaText').format(mp), spawn) if ttl_list: ttl = (ttl_list[index] if index < len(ttl_list) else ttl_list[len(ttl_list) - 1]) TQDBParser.insert_value(self.TTL, texts.get(self.TTL).format(ttl), spawn) # Iterate over the original properties and add some whitelisted # properties to the final result: for key, value in original_properties.items(): if key.startswith('character'): continue spawn['properties'][key] = value result['summons'].append(spawn)
def parse(self, dbr, dbr_file, result): tag = dbr.get(self.NAME, None) if not tag or texts.get(tag) == tag: logging.warning(f'No tag or name for set found in {dbr_file}.') raise StopIteration result.update({ # Prepare the list of set items 'items': [], 'name': texts.get(tag), 'tag': tag, }) # Add the set members: for set_member_path in dbr['setMembers']: # Parse the set member: set_member = DBRParser.parse(set_member_path) # Add the tag to the items list: result['items'].append(set_member['tag']) # The number of set bonuses is equal to the number of set items minus 1 bonus_number = len(result['items']) - 1 # Because this parser has the lowest priority, all properties will # already have been parsed, so they can now be reconstructed to match # the set bonuses. Begin by initializing the properties for each set # bonus tier to an empty dict: properties = [{} for i in range(bonus_number)] # Insert the existing properties by adding them to the correct tier: for field, values in result['properties'].items(): if not isinstance(values, list): properties[bonus_number - 1][field] = values # Don't parse any further continue # The starting tier is determined by the highest tier starting_index = bonus_number - len(values) # Now just iterate and add the properties to each tier: for index, value in enumerate(values): properties[starting_index + index][field] = value # Now set the tiered set bonuses: result['properties'] = properties # Pop off the first element of the properties, if it's empty: if len(result['properties']) > 1: if not result['properties'][0]: result['properties'].pop(0)
def parse(self, dbr, dbr_file, result): # Set the block chance and value: result['properties'][self.BLOCK] = texts.get(self.TEXT).format( # Block chance dbr.get(f'{self.BLOCK}Chance', 0), # Blocked damage dbr.get(self.BLOCK, 0))
def _parse_skill_grant(self, dbr, result): """ Parse a granted skill. """ # Skip files without both granted skill name and level: if self.SKILL_NAME not in dbr or self.SKILL_LEVEL not in dbr: return level = dbr[self.SKILL_LEVEL] skill = DBRParser.parse(dbr[self.SKILL_NAME]) if 'name' not in skill: return # Store the skill, which will ensure a unique tag: skill_tag = storage.store_skill(skill) # Now add the granted skill to the item: result['properties'][self.SKILL_NAME] = { 'tag': skill_tag, 'level': level, } if self.CONTROLLER not in dbr: return # Grab the auto controller to see if this skill is activated: controller = DBRParser.read(dbr[self.CONTROLLER]) # The property 'triggerType' can be converted to a useful text suffix # like 'Activated on attack': result['properties'][self.SKILL_NAME].update({ 'trigger': texts.get(self.TRIGGERS[controller['triggerType']]) })
def _parse_skill_grant(self, dbr, result): """ Parse a granted skill. """ # Skip files without both granted skill name and level: if self.SKILL_NAME not in dbr or self.SKILL_LEVEL not in dbr: return level = dbr[self.SKILL_LEVEL] skill = DBRParser.parse(dbr[self.SKILL_NAME]) if 'name' not in skill: return # Store the skill, which will ensure a unique tag: skill_tag = storage.store_skill(skill) # Now add the granted skill to the item: result['properties'][self.SKILL_NAME] = { 'tag': skill_tag, 'level': level, } if self.CONTROLLER not in dbr: return # Grab the auto controller to see if this skill is activated: controller = DBRParser.read(dbr[self.CONTROLLER]) # The property 'triggerType' can be converted to a useful text suffix # like 'Activated on attack': result['properties'][self.SKILL_NAME].update( {'trigger': texts.get(self.TRIGGERS[controller['triggerType']])})
def parse(self, dbr, dbr_file, result): if 'lootRandomizerName' in dbr: result['tag'] = dbr['lootRandomizerName'] # Some names had inline comments, so strip the spaces: result['name'] = texts.get(result['tag']).strip() # Add the level requirement: result['levelRequirement'] = dbr['levelRequirement']
def parse_modifier(self, field, field_type, dbr): """ Parse a percentage increase in an offensive attribute. """ field_mod = f'{field}Modifier' chance = dbr.get(f'{field_mod}Chance', 0) mod = dbr.get(field_mod, 0) is_xor = dbr.get(f'{field}XOR', False) is_global = dbr.get(f'{field}Global', False) # Optional pre/suffix for chance and duration damage/effects: prefix = '' suffix = '' if field_type in [self.DOT, self.EOT]: # Add the possible duration field: duration_mod = dbr.get(f'{field}DurationModifier', 0) if duration_mod: suffix = texts.get(IMPRV_TIME).format(duration_mod) value = texts.get(field_mod).format(mod) if chance and not is_xor: prefix = texts.get(CHANCE).format(chance) if not is_global: # Insert the value normally TQDBParser.insert_value( field_mod, f'{prefix}{value}{suffix}', self.result) elif field.startswith('offensive'): # Add this field to the global offensive list self.offensive[field_mod] = ( [f'{prefix}{value}{suffix}'] if field_mod not in self.offensive else self.offensive[field_mod] + [f'{prefix}{value}{suffix}'] ) elif field.startswith('retaliation'): # Add this field to the global retaliation list self.retaliation[field_mod] = ( [f'{prefix}{value}{suffix}'] if field_mod not in self.retaliation else self.retaliation[field_mod] + [f'{prefix}{value}{suffix}'] )
def parse(self, dbr, dbr_file, result): file_name = os.path.basename(dbr_file).split('_') difficulty = self.DIFFICULTIES_LIST[int(file_name[0][1:]) - 1] result.update({ # The act it starts dropping in is also listed in the file name 'act': file_name[1], # Bitmap has a different key name than items here. 'bitmap': dbr.get('relicBitmap', None), # Difficulty classification is based on the file name 'classification': texts.get(difficulty).strip(), # Ironically the itemText holds the actual description tag 'description': texts.get(dbr['itemText']), # For relics the tag is in the Actor.tpl variable 'description' 'name': texts.get(dbr['description']), 'tag': dbr['description'], }) # The possible completion bonuses are in bonusTableName: bonuses = {} try: bonuses = DBRParser.parse(dbr['bonusTableName']) except InvalidItemError as e: logging.debug("Could not parse relic completion bonus information " f"for {result['name']} in {dbr_file}. {e}") result['bonus'] = bonuses.get('table', []) # Find how many pieces this relic has max_pieces = TQDBParser.highest_tier( result['properties'], result['properties'].keys()) # Initialize a list of tiers properties = [{} for i in range(max_pieces)] # Setup properties as list to correspond to adding pieces of a relic: for key, values in result['properties'].items(): if not isinstance(values, list): # This property is just repeated for all tiers: for i in range(max_pieces): properties[i][key] = values continue for index, value in enumerate(values): properties[index][key] = value result['properties'] = properties
def parse(self, dbr, dbr_file, result): """ Parse the scroll. """ # The new Potion XP items are also considered "scrolls": if 'potion' in str(dbr_file): return result # Use the file name to determine the difficulty: file_name = os.path.basename(dbr_file).split('_')[0][1:] # Strip all but digits from the string, then cast to int: difficulty = self.DIFFICULTIES_LIST[int( ''.join(filter(lambda x: x.isdigit(), file_name))) - 1] result.update({ 'tag': dbr['description'], 'name': texts.get(dbr['description']), 'classification': texts.get(difficulty).strip(), 'description': texts.get(dbr['itemText']), }) # Greater scroll of svefnthorn is incorrectly referenced as its Divine # variant. Manual fix required for now: if '02_svefnthorn.dbr' in str(dbr_file): result['tag'] = 'x2tagScrollName06' result['name'] = texts.get('x2tagScrollName06') # Set the bitmap if it exists if 'bitmap' in dbr: result['bitmap'] = dbr['bitmap'] # Grab the skill file: skill = {} try: skill = DBRParser.parse(dbr['skillName']) except InvalidItemError as e: logging.debug(f"Could not parse skill {dbr['skillName']} from " f"scroll {result['name']}. {e}") # Add the first tier of properties if there are any: if 'properties' in skill and skill['properties']: result['properties'] = skill['properties'][0] # Add any summon (just the first one) if 'summons' in skill: result['summons'] = skill['summons'][0]
def parse(self, dbr, dbr_file, result): file_name = os.path.basename(dbr_file).split('_') # Skip artifacts with unknown difficulties in which they drop: if file_name[0] not in DIFFICULTIES: raise StopIteration result.update({ # Bitmap has a different key name than items here. 'bitmap': dbr.get('artifactBitmap', None), # Classification is either Lesser, Greater or Divine 'classification': dbr.get('artifactClassification', None), # Difficulty it starts dropping is based on the file name 'dropsIn': texts.get(DIFFICULTIES[file_name[0]]).strip(), # For artifacts the tag is in the Actor.tpl variable 'description' 'name': texts.get(dbr['description']), 'tag': dbr['description'], })
def parse(self, dbr, dbr_file, result): """ Parse the refresh cooldown property. """ if self.FIELD in dbr: result['properties'][self.FIELD] = [ texts.get(self.FIELD).format(value) for value in dbr[self.FIELD]]
def parse(self, dbr, dbr_file, result): """ Parse the "Chance to be used" property. """ if self.FIELD in dbr: result['properties'][self.FIELD] = [ texts.get(self.FIELD).format(value) for value in dbr[self.FIELD]]
def parse(self, dbr, dbr_file, result): """ Parse the "activated when health drops below ...%" property. """ if self.FIELD in dbr: result['properties'][self.FIELD] = [ texts.get(self.FIELD).format(value) for value in dbr[self.FIELD]]
def parse(self, dbr, dbr_file, result): """ Parse the "% Chance to pass through enemies" property. """ if self.FIELD in dbr: result['properties'][self.FIELD] = [ texts.get(self.FIELD).format(value) for value in dbr[self.FIELD]]
def parse(self, dbr, dbr_file, result): """ Parse the "% Chance to pass through enemies" property. """ if self.FIELD in dbr: result['properties'][self.FIELD] = [ texts.get(self.FIELD).format(value) for value in dbr[self.FIELD] ]
def parse_modifier(self, field, field_type, dbr): """ Parse a percentage increase in an offensive attribute. """ field_mod = f'{field}Modifier' chance = dbr.get(f'{field_mod}Chance', 0) mod = dbr.get(field_mod, 0) is_xor = dbr.get(f'{field}XOR', False) is_global = dbr.get(f'{field}Global', False) # Optional pre/suffix for chance and duration damage/effects: prefix = '' suffix = '' if field_type in [self.DOT, self.EOT]: # Add the possible duration field: duration_mod = dbr.get(f'{field}DurationModifier', 0) if duration_mod: suffix = texts.get(IMPRV_TIME).format(duration_mod) value = texts.get(field_mod).format(mod) if chance and not is_xor: prefix = texts.get(CHANCE).format(chance) if not is_global: # Insert the value normally TQDBParser.insert_value(field_mod, f'{prefix}{value}{suffix}', self.result) elif field.startswith('offensive'): # Add this field to the global offensive list self.offensive[field_mod] = ([f'{prefix}{value}{suffix}'] if field_mod not in self.offensive else self.offensive[field_mod] + [f'{prefix}{value}{suffix}']) elif field.startswith('retaliation'): # Add this field to the global retaliation list self.retaliation[field_mod] = ([f'{prefix}{value}{suffix}'] if field_mod not in self.retaliation else self.retaliation[field_mod] + [f'{prefix}{value}{suffix}'])
def parse(self, dbr, dbr_file, result): dbr_class = dbr['Class'] # Skip shields: if (dbr_class.startswith('Weapon') and 'Shield' in dbr_class): return # Set the attack speed result['properties']['characterAttackSpeed'] = texts.get( dbr['characterBaseAttackSpeedTag'])
def parse(self, dbr, dbr_file, result): """ Parse the "Chance to be used" property. """ if self.FIELD in dbr: result['properties'][self.FIELD] = [ texts.get(self.FIELD).format(value) for value in dbr[self.FIELD] ]
def parse(self, dbr, dbr_file, result): """ Parse the "activated when health drops below ...%" property. """ if self.FIELD in dbr: result['properties'][self.FIELD] = [ texts.get(self.FIELD).format(value) for value in dbr[self.FIELD] ]
def parse(self, dbr, dbr_file, result): """ Parse the refresh cooldown property. """ if self.FIELD in dbr: result['properties'][self.FIELD] = [ texts.get(self.FIELD).format(value) for value in dbr[self.FIELD] ]
def parse(self, dbr, dbr_file, result): file_name = os.path.basename(dbr_file).split('_') difficulty = self.DIFFICULTIES_LIST[int(file_name[0][1:]) - 1] result.update({ # The act it starts dropping in is also listed in the file name 'act': file_name[1], # Bitmap has a different key name than items here. 'bitmap': dbr.get('relicBitmap', None), # Difficulty classification is based on the file name 'classification': texts.get(difficulty).strip(), # Ironically the itemText holds the actual description tag 'description': texts.get(dbr['itemText']), # For relics the tag is in the Actor.tpl variable 'description' 'name': texts.get(dbr['description']), 'tag': dbr['description'], }) # The possible completion bonuses are in bonusTableName: bonuses = DBRParser.parse(dbr['bonusTableName']) result['bonus'] = bonuses.get('table', []) # Find how many pieces this relic has max_pieces = TQDBParser.highest_tier( result['properties'], result['properties'].keys()) # Initialize a list of tiers properties = [{} for i in range(max_pieces)] # Setup properties as list to correspond to adding pieces of a relic: for key, values in result['properties'].items(): if not isinstance(values, list): # This property is just repeated for all tiers: for i in range(max_pieces): properties[i][key] = values continue for index, value in enumerate(values): properties[index][key] = value result['properties'] = properties
def parse(self, dbr, dbr_file, result): """ Parse the projectile properties. """ for field in self.FIELDS: if field not in dbr: continue result['properties'][field] = [ texts.get(field).format(value) for value in dbr[field]]
def parse_field(self, field, dbr, result): """ Parse all values for a given character field that is in the DBR. """ for value in dbr[field]: # Skip 0 values if not value: continue formatted = texts.get(field).format(value) TQDBParser.insert_value(field, formatted, result)
def parse(self, dbr, dbr_file, result): """ Parse the projectile properties. """ for field in self.FIELDS: if field not in dbr: continue result['properties'][field] = [ texts.get(field).format(value) for value in dbr[field] ]
def parse(self, dbr, dbr_file, result): """ Parse skill augments, mastery augments, and item granted skills. """ self._parse_skill_grant(dbr, result) # Parse skills that are augmented: for name, level in self.SKILL_AUGMENTS.items(): # Skip skills without both the name and level set: if name not in dbr or level not in dbr: continue if 'skilltree' in str(dbr[name]): # The atlantis expansion references skilltree instead of # skillmastery, so rebuild the path: mastery_file = str(dbr[name]).replace('skilltree', 'mastery') else: mastery_file = dbr[name] # Parse the skill: skill = DBRParser.parse(mastery_file) # Store the skill, which will ensure a unique tag: skill_tag = storage.store_skill(skill) level = dbr[level] # Skill format is either ItemSkillIncrement or ItemMasteryIncrement skill_format = (self.TXT_SKILL_INC if 'Mastery' not in skill['name'] else self.TXT_MASTERY_INC) result['properties'][name] = { 'tag': skill_tag, 'name': texts.get(skill_format).format(level, skill['name']), } # Parse augment to all skills: if self.AUGMENT_ALL in dbr: level = dbr[self.AUGMENT_ALL] result['properties'][self.AUGMENT_ALL] = (texts.get( self.TXT_ALL_INC).format(level))
def parse(self, dbr, dbr_file, result): # If no tag exists, skip parsing: tag = dbr.get('itemNameTag', None) if not tag: raise StopIteration # Set the known item properties: result.update({ 'bitmap': dbr.get('bitmap', None), 'itemLevel': dbr.get('itemLevel', None), 'name': texts.get(tag), 'tag': tag, }) # Check if this item is part of a set: item_set_path = dbr.get('itemSetName', None) if item_set_path: # Read (don't parse to avoid recursion) the set to get the tag: item_set = DBRParser.read(item_set_path) # Only add the set if it has a tag: result['set'] = item_set.get(ItemSetParser.NAME, None) # Stop parsing here if requirement parsing isn't necessary if not self.should_parse_requirements(dbr, result): return # Cost prefix of this props is determined by its class cost_prefix = dbr['Class'].split('_')[1] cost_prefix = cost_prefix[:1].lower() + cost_prefix[1:] # Read cost file cost_properties = DBRParser.read(dbr.get( 'itemCostName', self.REQUIREMENT_FALLBACK)) # Grab the props level (it's a variable in the equations) for requirement in self.REQUIREMENTS: # Create the equation key equation_key = cost_prefix + requirement + 'Equation' req = requirement.lower() + 'Requirement' # Existing requirements shouldn't be overriden: if equation_key in cost_properties and req not in result: equation = cost_properties[equation_key] # camelCased variables are required for the equations: itemLevel = dbr['itemLevel'] # noqa totalAttCount = len(result['properties']) # noqa # Eval the equation: result[req] = math.ceil(numexpr.evaluate(equation).item())
def parse(self, dbr, dbr_file, result): # If no tag exists, skip parsing: tag = dbr.get('itemNameTag', None) if not tag: raise StopIteration # Set the known item properties: result.update({ 'bitmap': dbr.get('bitmap', None), 'itemLevel': dbr.get('itemLevel', None), 'name': texts.get(tag), 'tag': tag, }) # Check if this item is part of a set: item_set_path = dbr.get('itemSetName', None) if item_set_path: # Read (don't parse to avoid recursion) the set to get the tag: item_set = DBRParser.read(item_set_path) # Only add the set if it has a tag: result['set'] = item_set.get(ItemSetParser.NAME, None) # Stop parsing here if requirement parsing isn't necessary if not self.should_parse_requirements(dbr, result): return # Cost prefix of this props is determined by its class cost_prefix = dbr['Class'].split('_')[1] cost_prefix = cost_prefix[:1].lower() + cost_prefix[1:] # Read cost file cost_properties = DBRParser.read( dbr.get('itemCostName', self.REQUIREMENT_FALLBACK)) # Grab the props level (it's a variable in the equations) for requirement in self.REQUIREMENTS: # Create the equation key equation_key = cost_prefix + requirement + 'Equation' req = requirement.lower() + 'Requirement' # Existing requirements shouldn't be overriden: if equation_key in cost_properties and req not in result: equation = cost_properties[equation_key] # camelCased variables are required for the equations: itemLevel = dbr['itemLevel'] # noqa totalAttCount = len(result['properties']) # noqa # Eval the equation: result[req] = math.ceil(numexpr.evaluate(equation).item())
def parse_global(self, key, chance, xor, all_fields, index): """ Add a global chance for properties. This is where multiple properties can be triggered by a chance, like 10% Chance for one of the following: - ... - ... """ # Grab the value for this index from the global stores: fields = dict( (k, v[index]) if index < len(v) # If this value isn't repeated, grab the last value else (k, v[-1]) for k, v in all_fields.items()) # Check if the XOR was set for any field: if xor: value = { 'chance': ( texts.get(GLOBAL_XOR_ALL) if chance == 100 else texts.get(GLOBAL_XOR_PCT).format(chance)), 'properties': fields, } else: value = { 'chance': ( texts.get(GLOBAL_ALL).format('') if chance == 100 else texts.get(GLOBAL_PCT).format(chance)), 'properties': fields, } # Insert the value normally TQDBParser.insert_value(key, value, self.result)
def parse(self, dbr, dbr_file, result): """ Parse the scroll. """ # Use the file name to determine the difficulty: file_name = os.path.basename(dbr_file).split('_')[0][1:] # Strip all but digits from the string, then cast to int: difficulty = self.DIFFICULTIES_LIST[int( ''.join(filter(lambda x: x.isdigit(), file_name))) - 1] result.update({ 'tag': dbr['description'], 'name': texts.get(dbr['description']), 'classification': texts.get(difficulty).strip(), 'description': texts.get(dbr['itemText']), }) # Greater scroll of svefnthorn is incorrectly referenced as its Divine # variant. Manual fix required for now: if '02_svefnthorn.dbr' in str(dbr_file): result['tag'] = 'x2tagScrollName06' result['name'] = texts.get('x2tagScrollName06') # Set the bitmap if it exists if 'bitmap' in dbr: result['bitmap'] = dbr['bitmap'] # Grab the skill file: skill = DBRParser.parse(dbr['skillName']) # Add the first tier of properties if there are any: if 'properties' in skill and skill['properties']: result['properties'] = skill['properties'][0] # Add any summon (just the first one) if 'summons' in skill: result['summons'] = skill['summons'][0]
def parse(self, dbr, dbr_file, result): file_name = os.path.basename(dbr_file).split('_') # Skip artifacts with unknown difficulties in which they drop: if file_name[0] not in DIFFICULTIES: raise StopIteration # Artifact classification value (always Lesser, Greater or Divine) ac_value = dbr.get('artifactClassification', None) # Translation tag for this classification ac_tag = ARTIFACT_CLASSIFICATIONS[ac_value] result.update({ # Bitmap has a different key name than items here. 'bitmap': dbr.get('artifactBitmap', None), # Classification is either Lesser, Greater or Divine (translated) 'classification': texts.get(ac_tag), # Difficulty it starts dropping is based on the file name 'dropsIn': texts.get(DIFFICULTIES[file_name[0]]).strip(), # For artifacts the tag is in the Actor.tpl variable 'description' 'name': texts.get(dbr['description']), 'tag': dbr['description'], })
def parse_field(self, field, dbr, result): """ Parse all values for a given defensive field that is in the DBR. """ for i in range(len(dbr[field])): iteration = TQDBParser.extract_values(dbr, field, i) # It's possible some values aren't set in this iteration: if field not in iteration: continue chance = iteration.get(f'{field}Chance', 0) value = iteration[field] # Format the value using the texts: formatted = texts.get(field).format(value) # Prepend a chance if it's set: if chance: formatted = texts.get(CHANCE).format(chance) + formatted TQDBParser.insert_value(field, formatted, result)
def parse(self, dbr, dbr_file, result): file_name = os.path.basename(dbr_file).split('_') # Skip artifacts with unknown difficulties in which they drop: if file_name[0] not in DIFFICULTIES: raise StopIteration # Artifact classification value (always Lesser, Greater or Divine) ac_value = dbr.get('artifactClassification', None) # Translation tag for this classification ac_tag = ARTIFACT_CLASSIFICATIONS[ac_value] result.update({ # Bitmap has a different key name than items here. 'bitmap': dbr.get('artifactBitmap', None), # Classification is either Lesser, Greater or Divine (translated) 'classification': texts.get(ac_tag).strip(), # Difficulty it starts dropping is based on the file name 'dropsIn': texts.get(DIFFICULTIES[file_name[0]]).strip(), # For artifacts the tag is in the Actor.tpl variable 'description' 'name': texts.get(dbr['description']), 'tag': dbr['description'], })
def format(cls, field_type, field, min, max): """ Format a specific field. A field is formatted by determining the numeric format and appending the text specific for that field. """ if max > min: if field_type == cls.EOT: # Effect damage is done in seconds, so add a decimal value = texts.get(DMG_RANGE_DECIMAL).format(min, max) else: # DOT and regular damage is flat, so no decimals: value = texts.get(DMG_RANGE).format(min, max) else: if field_type == cls.EOT: # Effect damage is done in seconds, so add a decimal value = texts.get(DMG_SINGLE_DECIMAL).format(min) else: # DOT and regular damage is flat, so no decimals: value = texts.get(DMG_SINGLE).format(min) return f'{value}{texts.get(field)}'
def parse(self, dbr, dbr_file, result): """ Parse skill properties. """ for field in self.FIELDS: if field not in dbr: continue # Now iterate as many times as is necessary for this field: for index, value in enumerate(dbr[field]): # Skip any field that has a negligible value if value <= 0.01: continue formatted = texts.get(field).format(value) # Grab the chance and add a prefix with it: if f'{field}Chance' in dbr: chance = dbr[f'{field}Chance'][index] formatted = texts.get(CHANCE).format(chance) + formatted # Insert the value: TQDBParser.insert_value(field, formatted, result)
def _parse_skill_grant(self, dbr, result): """ Parse a granted skill. """ # If it doesn't have a granted skill name and level, it grants no skills: if self.SKILL_NAME not in dbr or self.SKILL_LEVEL not in dbr: return level = dbr[self.SKILL_LEVEL] try: skill = DBRParser.parse(dbr[self.SKILL_NAME]) except InvalidItemError as e: logging.debug( f"Skipping granted skill record in {dbr[self.SKILL_NAME]}. {e}" ) return if 'name' not in skill: return # Store the skill, which will ensure a unique tag: skill_tag = storage.store_skill(skill) # Now add the granted skill to the item: result['properties'][self.SKILL_NAME] = { 'tag': skill_tag, 'level': level, } if self.CONTROLLER not in dbr: return # Grab the auto controller to see if this skill is activated: controller = DBRParser.read(dbr[self.CONTROLLER]) # The property 'triggerType' can be converted to a useful text suffix # like 'Activated on attack': result['properties'][self.SKILL_NAME].update( {'trigger': texts.get(self.TRIGGERS[controller['triggerType']])})
def parse_field(self, field, dbr, result): """ Parse all values for a given racial bonus field that is in the DBR. """ if field not in dbr: return races = [] for race in dbr[self.RACE]: if race == 'Beastman': races.append('Beastmen') elif race != 'Undead' and race != 'Magical': races.append(race + 's') else: races.append(race) for value in dbr[field]: TQDBParser.insert_value( field, # Create a list of all racial bonuses [texts.get(field).format(value, race) for race in races], result)
def parse(self, dbr, dbr_file, result): """ Parse the skill that spawns a pet. """ # Only set time to live it's set (otherwise it's infinite) ttl_list = dbr.get(self.TTL, None) # Parse all the summons and set them as a list: result['summons'] = [] for index, spawn_file in enumerate(dbr['spawnObjects']): spawn = DBRParser.parse(spawn_file) # Keep track of the original properties this summon had: original_properties = {} if 'properties' in spawn: if isinstance(spawn['properties'], list): original_properties = spawn['properties'][0].copy() else: original_properties = spawn['properties'].copy() # We need the raw values from the spawn DBR for hp/mp spawn['properties'] = {} spawn_dbr = DBRParser.read(spawn_file) if 'characterLife' in spawn_dbr: hp_list = spawn_dbr['characterLife'] hp = ( hp_list[index] if index < len(hp_list) else hp_list[len(hp_list) - 1]) TQDBParser.insert_value( 'characterLife', texts.get('LifeText').format(hp), spawn) if 'characterMana' in spawn_dbr: mp_list = spawn_dbr['characterMana'] mp = ( mp_list[index] if index < len(mp_list) else mp_list[len(mp_list) - 1]) TQDBParser.insert_value( 'characterMana', texts.get('ManaText').format(mp), spawn) if ttl_list: ttl = ( ttl_list[index] if index < len(ttl_list) else ttl_list[len(ttl_list) - 1]) TQDBParser.insert_value( self.TTL, texts.get(self.TTL).format(ttl), spawn) # Iterate over the original properties and add some whitelisted # properties to the final result: for key, value in original_properties.items(): if key.startswith('character'): continue spawn['properties'][key] = value result['summons'].append(spawn)
def parse_flat(self, field, field_type, dbr): """ Parse a flat increase in an offensive attribute. """ # Prepare some keys that will determine the flat or flat range: chance = dbr.get(f'{field}Chance', 0) min = dbr.get(f'{field}Min', 0) max = dbr.get(f'{field}Max', min) is_xor = dbr.get(f'{field}XOR', False) is_global = dbr.get(f'{field}Global', False) # Optional pre/suffix for chance and duration damage/effects: prefix = '' suffix = '' if field_type == self.MANA: # The mana burn suffixes are kind of derpy: chance = dbr.get('offensiveManaBurnChance', 0) min = dbr.get('offensiveManaBurnDrainMin', 0) max = dbr.get('offensiveManaBurnDrainMax', 0) is_xor = dbr.get(f'{field}XOR', False) ratio = dbr.get('offensiveManaBurnDamageRatio', 0) if ratio: suffix = texts.get('offensiveManaBurnRatio').format(ratio) # Reset the field to what's used in texts. field = 'offensiveManaDrain' elif field_type == self.DOT: duration_min = dbr.get(f'{field}DurationMin', 0) duration_max = dbr.get(f'{field}DurationMax', 0) if duration_min: min *= duration_min suffix = texts.get(DOT_SINGLE).format(duration_min) if duration_max and max == min: max = min * duration_max elif max: max *= duration_min elif field_type == self.EOT: duration_min = dbr.get(f'{field}DurationMin', 0) if duration_min: suffix = texts.get(DFT_SINGLE).format(duration_min) # Pierce ratio is a singular exception for flat properties: if field == 'offensivePierceRatio': # Pierce ratio already has its formatter in the flat property: # "{0:.0f}% Pierce Ratio" instead of "% Pierce Ratio" value = texts.get(field).format(min) else: # Format the value based on its field type and values: value = self.format(field_type, field, min, max) if chance and not is_xor: prefix = texts.get(CHANCE).format(chance) if not is_global: # Insert the value normally TQDBParser.insert_value( field, f'{prefix}{value}{suffix}', self.result) elif field.startswith('offensive'): # Add this field to the global offensive list self.offensive[field] = ( [f'{prefix}{value}{suffix}'] if field not in self.offensive else self.offensive[field] + [f'{prefix}{value}{suffix}']) if is_xor: self.offensiveXOR = True elif field.startswith('retaliation'): # Add this field to the global retaliation list self.retaliation[field] = ( [f'{prefix}{value}{suffix}'] if field not in self.retaliation else self.retaliation[field] + [f'{prefix}{value}{suffix}']) if is_xor: self.retaliationXOR = True
def parse(self, dbr, dbr_file, result): """ Parse the base properties of a skill. These properties include the skillDisplayName, its friendly display name (the property returns a tag), and the maximum level of a skill. """ # Store the path to this skill, it is used in tqdb.storage to ensure # all tags are unique. result['path'] = dbr_file if self.NAME in dbr: # The tag is the skillDisplayName property result['tag'] = self.FORMATTER.sub('', dbr[self.NAME]) # Now try to find a friendly name for the tag: result['name'] = texts.get(result['tag']) if result['name'] == result['tag']: # If the tag wasn't returned, a friendly name weas found: logging.debug(f'No skill name found for {result["tag"]}') else: logging.debug(f'No skillDisplayName found in {dbr_file}') if self.DESC in dbr and texts.has(dbr[self.DESC]): # Also load the description, if it's known: result['description'] = texts.get(dbr[self.DESC]) elif self.FILE in dbr: # Use the FileDescription instead: result['description'] = dbr['FileDescription'] # Check the skill base fields: base_tiers = TQDBParser.highest_tier(dbr, self.FIELDS) for field in self.FIELDS: for index in range(base_tiers): itr_dbr = TQDBParser.extract_values(dbr, field, index) if field not in itr_dbr or itr_dbr[field] <= 0.01: continue # Insert this skill property value = texts.get(field).format(itr_dbr[field]) TQDBParser.insert_value(field, value, result) # Check the damage absorption skill properties: abs_tiers = TQDBParser.highest_tier(dbr, self.ABSORPTIONS) for field in self.ABSORPTIONS: for index in range(abs_tiers): itr_dbr = TQDBParser.extract_values(dbr, field, index) if field not in itr_dbr: continue # Add 'skill' prefix and capitalize first letter: field_prefixed = 'skill' + field[:1].upper() + field[1:] value = itr_dbr[field] # Find qualifier damage type(s): damage_types = ', '.join([ texts.get(text_key) for dmg_type, text_key in self.QUALIFIERS.items() if dmg_type in dbr]) if damage_types: TQDBParser.insert_value( field_prefixed, f'{texts.get(field_prefixed).format(value)} ' f'({damage_types})', result) else: # If there is no qualifier, it's all damage: TQDBParser.insert_value( field_prefixed, texts.get(field_prefixed).format(value), result) # Prepare two variables to determine the max number of tiers: skill_cap = dbr.get('skillUltimateLevel', dbr.get('skillMaxLevel')) props = result['properties'] # The maximum number of properties is now the minimum between the skill # cap and the highest number of tiers available in the properties: max_tiers = min( TQDBParser.highest_tier(props, props.keys()), skill_cap) # After all skill properties have been set, index them by level: properties = [{} for i in range(max_tiers)] # Insert the existing properties by adding them to the correct tier: for field, values in result['properties'].items(): for index in range(max_tiers): # Each value is either a list or a flat value to repeat: if isinstance(values, list): # Properties that are capped before this tier repeat their # last value: if index >= len(values): properties[index][field] = values[len(values) - 1] else: properties[index][field] = values[index] else: properties[index][field] = values # For summoned skills it's very likely a lot of extraneous empty # property tiers were added, filter those out: properties = [tier for tier in properties if tier] # Now set the reindexed properties: result['properties'] = properties
def parse_flat(self, field, field_type, dbr): """ Parse a flat increase in an offensive attribute. """ # Prepare some keys that will determine the flat or flat range: chance = dbr.get(f'{field}Chance', 0) min = dbr.get(f'{field}Min', 0) max = dbr.get(f'{field}Max', min) is_xor = dbr.get(f'{field}XOR', False) is_global = dbr.get(f'{field}Global', False) # Optional pre/suffix for chance and duration damage/effects: prefix = '' suffix = '' if field_type == self.MANA: # The mana burn suffixes are kind of derpy: chance = dbr.get('offensiveManaBurnChance', 0) min = dbr.get('offensiveManaBurnDrainMin', 0) max = dbr.get('offensiveManaBurnDrainMax', 0) is_xor = dbr.get(f'{field}XOR', False) ratio = dbr.get('offensiveManaBurnDamageRatio', 0) if ratio: suffix = texts.get('offensiveManaBurnRatio').format(ratio) # Reset the field to what's used in texts. field = 'offensiveManaDrain' elif field_type == self.DOT: duration_min = dbr.get(f'{field}DurationMin', 0) duration_max = dbr.get(f'{field}DurationMax', 0) if duration_min: min *= duration_min suffix = texts.get(DOT_SINGLE).format(duration_min) if duration_max and max == min: max = min * duration_max elif max: max *= duration_min elif field_type == self.EOT: duration_min = dbr.get(f'{field}DurationMin', 0) if duration_min: suffix = texts.get(DFT_SINGLE).format(duration_min) # Pierce ratio is a singular exception for flat properties: if field == 'offensivePierceRatio': # Pierce ratio already has its formatter in the flat property: # "{0:.0f}% Pierce Ratio" instead of "% Pierce Ratio" value = texts.get(field).format(min) else: # Format the value based on its field type and values: value = self.format(field_type, field, min, max) if chance and not is_xor: prefix = texts.get(CHANCE).format(chance) if not is_global: # Insert the value normally TQDBParser.insert_value(field, f'{prefix}{value}{suffix}', self.result) elif field.startswith('offensive'): # Add this field to the global offensive list self.offensive[field] = ([ f'{prefix}{value}{suffix}' ] if field not in self.offensive else self.offensive[field] + [f'{prefix}{value}{suffix}']) if is_xor: self.offensiveXOR = True elif field.startswith('retaliation'): # Add this field to the global retaliation list self.retaliation[field] = ([ f'{prefix}{value}{suffix}' ] if field not in self.retaliation else self.retaliation[field] + [f'{prefix}{value}{suffix}']) if is_xor: self.retaliationXOR = True
def parse_quests(): """ Parse the Titan Quest quest rewards. The quest rewards are indexed by creating a text readable version of the QST files located in the Resources/Quests.arc file. The rewards are extracted by only retrieving rewards prefixed with item[] tags. """ timer = time.clock() # Regex to find item rewards REWARD = re.compile( r'item\[(?P<index>[0-9])\](.{0,1})' r'(?P<file>' 'records' r'[\\||\/]' r'(xpack[2]?[\\||\/])?' 'quests' r'[\\||\/]' 'rewards' r'[\\||\/]' r'([^.]+)\.dbr' r')' ) # Regex to find the title tag TITLE = re.compile(r'titletag(?P<tag>[^\s]*)') files = glob.glob(resources.QUESTS) quests = {} for qst in files: with open(qst, 'rb') as quest: # Read the content as printable characters only: content = ''.join( c for c in # Lower case and convert to utf-8 quest.read().decode('utf-8', errors='ignore').lower() if c in string.printable ) # Find the title and skip this file if none is found: title_tag = TITLE.search(content) if not title_tag or not title_tag.group('tag'): continue # Grab the quest title tag tag = title_tag.group('tag') if tag not in quests: # Initialize three difficulties: quests[tag] = { 'name': texts.get(tag), 'rewards': [{}, {}, {}], } # Parsed reward files (so we don't duplicate): parsed = [] # Add all the rewards to the quest: for match in REWARD.finditer(content): # The index in the item[index] tag determines the difficulty: difficulty = int(match.group('index')) reward_file = match.group('file') # Store the file or move on if we've already parsed it if reward_file not in parsed: parsed.append(reward_file) else: continue # Prepend the path with the database path: rewards = parse(resources.DB / reward_file) # Skip quests where the rewards aren't items: if 'loot_table' not in rewards: continue # Either set the chance or add it to a previous chance: for item, chance in rewards['loot_table'].items(): if item in quests[tag]['rewards'][difficulty]: quests[tag]['rewards'][difficulty][item] += chance else: quests[tag]['rewards'][difficulty][item] = chance # Don't save quests without item rewards: if not any(reward for reward in quests[tag]['rewards']): quests.pop(tag) # Turn all chances into percentages: for tag, quest in quests.items(): for index, difficulty in enumerate(quest['rewards']): for item, chance in difficulty.items(): # Format into 4 point precision percentages: quests[tag]['rewards'][index][item] = ( float('{0:.4f}'.format(chance * 100))) # Log the timer: logging.info(f'Parsed quest rewards in {time.clock() - timer} seconds.') return quests
def parse(self, dbr, dbr_file, result): """ Parse the base properties of a skill. These properties include the skillDisplayName, its friendly display name (the property returns a tag), and the maximum level of a skill. """ # Store the path to this skill, it is used in tqdb.storage to ensure # all tags are unique. result['path'] = dbr_file if self.NAME in dbr: # The tag is the skillDisplayName property result['tag'] = self.FORMATTER.sub('', dbr[self.NAME]) # Now try to find a friendly name for the tag: result['name'] = texts.get(result['tag']) if result['name'] == result['tag']: # If the tag wasn't returned, a friendly name weas found: logging.debug(f'No skill name found for {result["tag"]}') else: logging.debug(f'No skillDisplayName found in {dbr_file}') if self.DESC in dbr and texts.has(dbr[self.DESC]): # Also load the description, if it's known: result['description'] = texts.get(dbr[self.DESC]) elif self.FILE in dbr: # Use the FileDescription instead: result['description'] = dbr['FileDescription'] # Check the skill base fields: base_tiers = TQDBParser.highest_tier(dbr, self.FIELDS) for field in self.FIELDS: for index in range(base_tiers): itr_dbr = TQDBParser.extract_values(dbr, field, index) if field not in itr_dbr or itr_dbr[field] <= 0.01: continue # Insert this skill property value = texts.get(field).format(itr_dbr[field]) TQDBParser.insert_value(field, value, result) # Check the damage absorption skill properties: abs_tiers = TQDBParser.highest_tier(dbr, self.ABSORPTIONS) for field in self.ABSORPTIONS: for index in range(abs_tiers): itr_dbr = TQDBParser.extract_values(dbr, field, index) if field not in itr_dbr: continue # Add 'skill' prefix and capitalize first letter: field_prefixed = 'skill' + field[:1].upper() + field[1:] value = itr_dbr[field] # Find qualifier damage type(s): damage_types = ', '.join([ texts.get(text_key) for dmg_type, text_key in self.QUALIFIERS.items() if dmg_type in dbr ]) if damage_types: TQDBParser.insert_value( field_prefixed, f'{texts.get(field_prefixed).format(value)} ' f'({damage_types})', result) else: # If there is no qualifier, it's all damage: TQDBParser.insert_value( field_prefixed, texts.get(field_prefixed).format(value), result) # Prepare two variables to determine the max number of tiers: skill_cap = dbr.get('skillUltimateLevel', dbr.get('skillMaxLevel')) or 99 props = result['properties'] # The maximum number of properties is now the minimum between the skill # cap and the highest number of tiers available in the properties: max_tiers = min(TQDBParser.highest_tier(props, props.keys()), skill_cap) # After all skill properties have been set, index them by level: properties = [{} for i in range(max_tiers)] # Insert the existing properties by adding them to the correct tier: for field, values in result['properties'].items(): for index in range(max_tiers): # Each value is either a list or a flat value to repeat: if isinstance(values, list): # Properties that are capped before this tier repeat their # last value: if index >= len(values): properties[index][field] = values[len(values) - 1] else: properties[index][field] = values[index] else: properties[index][field] = values # For summoned skills it's very likely a lot of extraneous empty # property tiers were added, filter those out: properties = [tier for tier in properties if tier] # Now set the reindexed properties: result['properties'] = properties