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 all the abilities a monster has. """ # Initialize the abilities (to be indexed per level) abilities = [] # Parse all the normal skills (17 max): for i in range(1, 18): nameTag = f'skillName{i}' levelTag = f'skillLevel{i}' # Skip unset skills or skills that are to be ignored: if (nameTag not in dbr or levelTag not in dbr or str(dbr[nameTag]).lower() in self.IGNORE_SKILLS): continue try: skill = DBRParser.parse(dbr[nameTag]) except InvalidItemError as e: logging.debug( f"Skipping creature skill {nameTag} in {dbr_file} because it's invalid. {e}" ) continue if not skill['properties']: continue # Store the skill, which will ensure a unique tag: skill_tag = storage.store_skill(skill) # Iterate over the difficulties: for difficulty in range(3): itr = TQDBParser.extract_values(dbr, 'skill', difficulty) # If a level is set to 0 for a difficulty, it won't be in the # extracted result, so use the KeyError safe 'get' method: level = itr.get(levelTag, 0) if not level: continue if len(abilities) - 1 < difficulty: # Create missing tiers: abilities += ([{}] * (difficulty - len(abilities) + 1)) abilities[difficulty][skill_tag] = level result['abilities'] = abilities
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): """ Parse an offensive/retaliation field. For each field, depending on the group it's in, this function will need to check both the absolute (flat increase) and modifier (% increase) versions as well as possible effects or damage durations. Args: field (str): Field name, as listed in the FIELDS list """ # Set the result and prepare the global stores: self.result = result self.offensive = {} self.offensiveXOR = False self.retaliation = {} self.retaliationXOR = False for field, field_type in self.FIELDS.items(): # Find whether the flat, modifier, or both fields are present: min = ( f'{field}Min' if field != 'offensiveManaBurn' # Mana burn is the only field that differs from the rest else 'offensiveManaBurnDrainMin') mod = f'{field}Modifier' iterations = max( len(dbr[min]) if min in dbr else 0, len(dbr[mod]) if mod in dbr else 0, 0) # Now iterate as many times as is necessary for this field: for index in range(iterations): # Create a new copy of the DBR with the values for this index: iteration = TQDBParser.extract_values(dbr, field, index) if min in iteration: # Parse the flat (+...) version: self.parse_flat(field, field_type, iteration) if mod in iteration: # Parse the modifier (+...%) version self.parse_modifier(field, field_type, iteration) # Now add the global chance tags if they're set: offensive_key = 'offensiveGlobalChance' if offensive_key in dbr and self.offensive: # Skip 0 chance globals altogether chances = [chance for chance in dbr[offensive_key] if chance] for index, chance in enumerate(chances): self.parse_global( offensive_key, # The global chance for the offensive properties chance, # If any global offensive properties are XOR-ed: self.offensiveXOR, # The dictionary of global offensive properties self.offensive, # Index of this global chance index) retaliation_key = 'retaliationGlobalChance' if retaliation_key in dbr and self.retaliation: # Skip 0 chance globals altogether chances = [chance for chance in dbr[retaliation_key] if chance] for index, chance in enumerate(chances): self.parse_global( retaliation_key, # The global chance for the offensive properties chance, # If any global offensive properties are XOR-ed: self.retaliationXOR, # The dictionary of global offensive properties self.retaliation, # Index of this global chance index)
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
def parse(self, dbr, dbr_file, result): """ Parse the monster. """ self.parse_creature(dbr, dbr_file, result) # Don't parse any further for tagless or level-less creatures: if 'tag' not in result or not result['level']: return # Iterate over normal, epic & legendary version of the boss: loot = [] for index in range(3): # Initialize an empty result: loot.append({}) # Create a DBR that only has the equipment for this difficulty: difficulty_dbr = TQDBParser.extract_values(dbr, '', index) # Parse all the equipment in this difficulty difficulty_dbr = self.parse_difficulty(difficulty_dbr, dbr_file) # Only store the equipment if there was any: if difficulty_dbr: loot[index] = difficulty_dbr # If there is any tiered data to store, store it: if any(tier for tier in loot if tier): result['loot'] = loot chests = [] tag = result['tag'] # Find the chest for each difficulty: for index in range(3): # Initialize an empty result: chests.append({}) # Grab the chest to parse: if tag in CHESTS and CHESTS[tag][index]: # Grab the level for this index, or the last one: level = ( result['level'][index] if len(result['level']) > index else result['level'][-1]) # Parse the chest and pass the monsters level as a reference: loot = DBRParser.parse( DB / CHESTS[tag][index], {'level': level}, ) # Convert all item chances to 4 point precision: chests[index] = dict( (k, float('{0:.4f}'.format(v))) for k, v in loot['loot_table'].items()) # If there is any tiered data to store, store it: if any(tier for tier in chests if tier): result['chest'] = chests # Check if this monster is limited to a difficulty: if len(result['level']) != len(set(result['level'])): # If a level is repeated, it means a creature doesn't spawn in # some difficulties. The 'normal' difficulty level is either # repeated in Epic and Legendary, so find the index and subtract # 1 from that to get all difficulties that should be removed: for i in range(result['level'].count(result['level'][0]) - 1): result['properties'][i] = {} result['abilities'][i] = {} result['level'][i] = None
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(self, dbr, dbr_file, result): """ Parse an offensive/retaliation field. For each field, depending on the group it's in, this function will need to check both the absolute (flat increase) and modifier (% increase) versions as well as possible effects or damage durations. Args: field (str): Field name, as listed in the FIELDS list """ # Set the result and prepare the global stores: self.result = result self.offensive = {} self.offensiveXOR = False self.retaliation = {} self.retaliationXOR = False for field, field_type in self.FIELDS.items(): # Find whether the flat, modifier, or both fields are present: min = (f'{field}Min' if field != 'offensiveManaBurn' # Mana burn is the only field that differs from the rest else 'offensiveManaBurnDrainMin') mod = f'{field}Modifier' iterations = max( len(dbr[min]) if min in dbr else 0, len(dbr[mod]) if mod in dbr else 0, 0) # Now iterate as many times as is necessary for this field: for index in range(iterations): # Create a new copy of the DBR with the values for this index: iteration = TQDBParser.extract_values(dbr, field, index) if min in iteration: # Parse the flat (+...) version: self.parse_flat(field, field_type, iteration) if mod in iteration: # Parse the modifier (+...%) version self.parse_modifier(field, field_type, iteration) # Now add the global chance tags if they're set: offensive_key = 'offensiveGlobalChance' if offensive_key in dbr and self.offensive: # Skip 0 chance globals altogether chances = [chance for chance in dbr[offensive_key] if chance] for index, chance in enumerate(chances): self.parse_global( offensive_key, # The global chance for the offensive properties chance, # If any global offensive properties are XOR-ed: self.offensiveXOR, # The dictionary of global offensive properties self.offensive, # Index of this global chance index) retaliation_key = 'retaliationGlobalChance' if retaliation_key in dbr and self.retaliation: # Skip 0 chance globals altogether chances = [chance for chance in dbr[retaliation_key] if chance] for index, chance in enumerate(chances): self.parse_global( retaliation_key, # The global chance for the offensive properties chance, # If any global offensive properties are XOR-ed: self.retaliationXOR, # The dictionary of global offensive properties self.retaliation, # Index of this global chance index)