class CreatureManager(UnitManager): CURRENT_HIGHEST_GUID = 0 def __init__(self, creature_template, creature_instance=None, is_summon=False, **kwargs): super().__init__(**kwargs) self.creature_template = creature_template self.creature_instance = creature_instance self.killed_by = None self.is_summon = is_summon if self.creature_template: self.entry = self.creature_template.entry self.native_display_id = self.generate_display_id() self.current_display_id = self.native_display_id self.max_health = self.creature_template.health_max self.power_1 = self.creature_template.mana_min self.max_power_1 = self.creature_template.mana_max self.level = randint(self.creature_template.level_min, self.creature_template.level_max) self.resistance_0 = self.creature_template.armor self.resistance_1 = self.creature_template.holy_res self.resistance_2 = self.creature_template.fire_res self.resistance_3 = self.creature_template.nature_res self.resistance_4 = self.creature_template.frost_res self.resistance_5 = self.creature_template.shadow_res self.npc_flags = self.creature_template.npc_flags self.static_flags = self.creature_template.static_flags self.mod_cast_speed = 1.0 self.base_attack_time = self.creature_template.base_attack_time self.unit_flags = self.creature_template.unit_flags self.faction = self.creature_template.faction self.creature_type = self.creature_template.type self.sheath_state = WeaponMode.NORMALMODE self.set_melee_damage(int(self.creature_template.dmg_min), int(self.creature_template.dmg_max)) if 0 < self.creature_template.rank < 4: self.unit_flags = self.unit_flags | UnitFlags.UNIT_FLAG_PLUS_MOB self.fully_loaded = False self.is_evading = False self.wearing_offhand_weapon = False self.wearing_ranged_weapon = False self.respawn_timer = 0 self.last_random_movement = 0 self.random_movement_wait_time = randint(1, 12) self.loot_manager = CreatureLootManager(self) if self.creature_instance: if CreatureManager.CURRENT_HIGHEST_GUID < creature_instance.spawn_id: CreatureManager.CURRENT_HIGHEST_GUID = creature_instance.spawn_id self.guid = self.generate_object_guid(creature_instance.spawn_id) self.health = int((self.creature_instance.health_percent / 100) * self.max_health) self.map_ = self.creature_instance.map self.spawn_position = Vector(self.creature_instance.position_x, self.creature_instance.position_y, self.creature_instance.position_z, self.creature_instance.orientation) self.location = self.spawn_position.copy() self.respawn_time = randint(self.creature_instance.spawntimesecsmin, self.creature_instance.spawntimesecsmax) # All creatures can block, parry and dodge by default. # TODO CANT_BLOCK creature extra flag self.has_block_passive = True self.has_dodge_passive = True self.has_parry_passive = True def load(self): MapManager.update_object(self) @staticmethod def spawn(entry, location, map_id, override_faction=0, despawn_time=1): creature_template = WorldDatabaseManager.creature_get_by_entry(entry) if not creature_template: return None instance = SpawnsCreatures() instance.spawn_id = CreatureManager.CURRENT_HIGHEST_GUID + 1 instance.spawn_entry1 = entry instance.map = map_id instance.position_x = location.x instance.position_y = location.y instance.position_z = location.z instance.orientation = location.o instance.health_percent = 100 instance.mana_percent = 100 if despawn_time < 1: despawn_time = 1 instance.spawntimesecsmin = despawn_time instance.spawntimesecsmax = despawn_time creature = CreatureManager( creature_template=creature_template, creature_instance=instance, is_summon=True ) if override_faction > 0: creature.faction = override_faction creature.load() creature.send_create_packet_surroundings() return creature def generate_display_id(self): display_id_list = list(filter((0).__ne__, [self.creature_template.display_id1, self.creature_template.display_id2, self.creature_template.display_id3, self.creature_template.display_id4])) return choice(display_id_list) if len(display_id_list) > 0 else 4 # 4 = cube def send_inventory_list(self, world_session): vendor_data, session = WorldDatabaseManager.creature_get_vendor_data(self.entry) item_count = len(vendor_data) if vendor_data else 0 data = pack( '<QB', self.guid, item_count ) if item_count == 0: data += pack('<B', 0) else: for count, vendor_data_entry in enumerate(vendor_data): data += pack( '<7I', count + 1, # m_muid, acts as slot counter. vendor_data_entry.item, vendor_data_entry.item_template.display_id, 0xFFFFFFFF if vendor_data_entry.maxcount <= 0 else vendor_data_entry.maxcount, vendor_data_entry.item_template.buy_price, vendor_data_entry.item_template.max_durability, # Max durability (not implemented in 0.5.3). vendor_data_entry.item_template.buy_count # Stack count. ) world_session.enqueue_packet(ItemManager(item_template=vendor_data_entry.item_template).query_details()) session.close() world_session.enqueue_packet(PacketWriter.get_packet(OpCode.SMSG_LIST_INVENTORY, data)) # TODO Add skills (Two-Handed Swords etc.) to trainers for skill points https://i.imgur.com/tzyDDqL.jpg def send_trainer_list(self, world_session): if not self.can_train(world_session.player_mgr): Logger.anticheat(f'send_trainer_list called from NPC {self.entry} by player with GUID {world_session.player_mgr.guid} but this unit does not train that player\'s class. Possible cheating') return train_spell_bytes: bytes = b'' train_spell_count: int = 0 trainer_ability_list: list[TrainerTemplate] = WorldDatabaseManager.TrainerSpellHolder.trainer_spells_get_by_trainer(self.entry) if not trainer_ability_list or trainer_ability_list.count == 0: Logger.warning(f'send_trainer_list called from NPC {self.entry} but no trainer spells found!') return for trainer_spell in trainer_ability_list: # trainer_spell: The spell the trainer uses to teach the player. player_spell_id = trainer_spell.playerspell ability_spell_chain: SpellChain = WorldDatabaseManager.SpellChainHolder.spell_chain_get_by_spell(player_spell_id) spell_level: int = trainer_spell.reqlevel # Use this and not spell data, as there are differences between data source (2003 Game Guide) and what is in spell table. spell_rank: int = ability_spell_chain.rank prev_spell: int = ability_spell_chain.prev_spell spell_is_too_high_level: bool = spell_level > world_session.player_mgr.level if player_spell_id in world_session.player_mgr.spell_manager.spells: status = TrainerServices.TRAINER_SERVICE_USED else: if prev_spell in world_session.player_mgr.spell_manager.spells and spell_rank > 1 and not spell_is_too_high_level: status = TrainerServices.TRAINER_SERVICE_AVAILABLE elif spell_rank == 1 and not spell_is_too_high_level: status = TrainerServices.TRAINER_SERVICE_AVAILABLE else: status = TrainerServices.TRAINER_SERVICE_UNAVAILABLE data: bytes = pack( '<IBI3B6I', player_spell_id, # Spell id status, # Status trainer_spell.spellcost, # Cost trainer_spell.talentpointcost, # Talent Point Cost trainer_spell.skillpointcost, # Skill Point Cost spell_level, # Required Level trainer_spell.reqskill, # Required Skill Line trainer_spell.reqskillvalue, # Required Skill Rank 0, # Required Skill Step prev_spell, # Required Ability (1) 0, # Required Ability (2) 0 # Required Ability (3) ) train_spell_bytes += data train_spell_count += 1 # TODO: Temp placeholder. greeting: str = f'Hello, {world_session.player_mgr.player.name}! Ready for some training?' greeting_bytes = PacketWriter.string_to_bytes(greeting) greeting_bytes = pack( f'<{len(greeting_bytes)}s', greeting_bytes ) data = pack('<Q2I', self.guid, TrainerTypes.TRAINER_TYPE_GENERAL, train_spell_count) + train_spell_bytes + greeting_bytes world_session.player_mgr.enqueue_packet(PacketWriter.get_packet(OpCode.SMSG_TRAINER_LIST, data)) def finish_loading(self, reload=False): if self.creature_template and self.creature_instance: if not self.fully_loaded or reload: creature_model_info = WorldDatabaseManager.CreatureModelInfoHolder.creature_get_model_info(self.current_display_id) if creature_model_info: self.bounding_radius = creature_model_info.bounding_radius self.combat_reach = creature_model_info.combat_reach self.gender = creature_model_info.gender if self.creature_template.scale == 0: display_scale = DbcDatabaseManager.CreatureDisplayInfoHolder.creature_display_info_get_by_id(self.current_display_id) if display_scale and display_scale.CreatureModelScale > 0: self.native_scale = display_scale.CreatureModelScale else: self.native_scale = 1 else: self.native_scale = self.creature_template.scale self.current_scale = self.native_scale if self.creature_template.equipment_id > 0: creature_equip_template = WorldDatabaseManager.CreatureEquipmentHolder.creature_get_equipment_by_id( self.creature_template.equipment_id ) if creature_equip_template: self.set_virtual_item(0, creature_equip_template.equipentry1) self.set_virtual_item(1, creature_equip_template.equipentry2) self.set_virtual_item(2, creature_equip_template.equipentry3) self.stat_manager.init_stats() self.stat_manager.apply_bonuses(set_dirty=False) self.fully_loaded = True def set_virtual_item(self, slot, item_entry): if item_entry == 0: self.set_uint32(UnitFields.UNIT_VIRTUAL_ITEM_SLOT_DISPLAY + slot, 0) self.set_uint32(UnitFields.UNIT_VIRTUAL_ITEM_INFO + (slot * 2) + 0, 0) self.set_uint32(UnitFields.UNIT_VIRTUAL_ITEM_INFO + (slot * 2) + 1, 0) return item_template = WorldDatabaseManager.ItemTemplateHolder.item_template_get_by_entry(item_entry) if item_template: self.set_uint32(UnitFields.UNIT_VIRTUAL_ITEM_SLOT_DISPLAY + slot, item_template.display_id) virtual_item_info = unpack('<I', pack('<4B', item_template.class_, item_template.subclass, item_template.material, item_template.inventory_type) )[0] self.set_uint32(UnitFields.UNIT_VIRTUAL_ITEM_INFO + (slot * 2) + 0, virtual_item_info) self.set_uint32(UnitFields.UNIT_VIRTUAL_ITEM_INFO + (slot * 2) + 1, item_template.sheath) # Main hand if slot == 0: # This is a TOTAL guess, I have no idea about real weapon reach values. # The weapon reach unit field was removed in patch 0.10. if item_template.inventory_type == InventoryTypes.TWOHANDEDWEAPON: self.weapon_reach = 1.5 elif item_template.subclass == ItemSubClasses.ITEM_SUBCLASS_DAGGER: self.weapon_reach = 0.5 elif item_template.subclass != ItemSubClasses.ITEM_SUBCLASS_FIST_WEAPON: self.weapon_reach = 1.0 # Offhand if slot == 1: self.wearing_offhand_weapon = (item_template.inventory_type == InventoryTypes.WEAPON or item_template.inventory_type == InventoryTypes.WEAPONOFFHAND) # Ranged if slot == 2: self.wearing_ranged_weapon = (item_template.inventory_type == InventoryTypes.RANGED or item_template.inventory_type == InventoryTypes.RANGEDRIGHT) elif slot == 0: self.weapon_reach = 0.0 def is_quest_giver(self) -> bool: return self.npc_flags & NpcFlags.NPC_FLAG_QUESTGIVER def is_trainer(self) -> bool: return self.npc_flags & NpcFlags.NPC_FLAG_TRAINER # TODO: Validate trainer_spell field and Pet trainers. def can_train(self, player_mgr) -> bool: if not self.is_trainer(): return False if not self.is_within_interactable_distance(player_mgr) and not player_mgr.is_gm: return False # If expecting a specific class, check if they match. if self.creature_template.trainer_class > 0: return self.creature_template.trainer_class == player_mgr.player.class_ # Mount, TradeSkill or Pet trainer. return True def trainer_has_spell(self, spell_id: int) -> bool: if not self.is_trainer(): return False trainer_spells: list[TrainerTemplate] = WorldDatabaseManager.TrainerSpellHolder.trainer_spells_get_by_trainer(self.entry) for trainer_spell in trainer_spells: if trainer_spell.spell == spell_id: return True return False # override def get_full_update_packet(self, is_self=True): self.finish_loading() # race, class, gender, power_type self.bytes_0 = unpack('<I', pack('<4B', 0, self.creature_template.unit_class, self.gender, 0))[0] # stand_state, npc_flags, shapeshift_form, visibility_flag self.bytes_1 = unpack('<I', pack('<4B', self.stand_state, self.npc_flags, self.shapeshift_form, 0))[0] # sheath_state, misc_flags, pet_flags, unknown self.bytes_2 = unpack('<I', pack('<4B', self.sheath_state, 0, 0, 0))[0] self.damage = unpack('<I', pack('<2H', int(self.creature_template.dmg_min), int(self.creature_template.dmg_max)))[0] # Object fields self.set_uint64(ObjectFields.OBJECT_FIELD_GUID, self.guid) self.set_uint32(ObjectFields.OBJECT_FIELD_TYPE, self.get_object_type_value()) self.set_uint32(ObjectFields.OBJECT_FIELD_ENTRY, self.entry) self.set_float(ObjectFields.OBJECT_FIELD_SCALE_X, self.current_scale) # Unit fields self.set_uint32(UnitFields.UNIT_CHANNEL_SPELL, self.channel_spell) self.set_uint64(UnitFields.UNIT_FIELD_CHANNEL_OBJECT, self.channel_object) self.set_uint32(UnitFields.UNIT_FIELD_HEALTH, self.health) self.set_uint32(UnitFields.UNIT_FIELD_MAXHEALTH, self.max_health) self.set_uint32(UnitFields.UNIT_FIELD_POWER1, self.power_1) self.set_uint32(UnitFields.UNIT_FIELD_MAXHEALTH, self.max_health) self.set_uint32(UnitFields.UNIT_FIELD_MAXPOWER1, self.max_power_1) self.set_uint32(UnitFields.UNIT_FIELD_LEVEL, self.level) self.set_uint32(UnitFields.UNIT_FIELD_FACTIONTEMPLATE, self.faction) self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags) self.set_uint32(UnitFields.UNIT_FIELD_COINAGE, self.coinage) self.set_float(UnitFields.UNIT_FIELD_BASEATTACKTIME, self.base_attack_time) self.set_float(UnitFields.UNIT_FIELD_BASEATTACKTIME + 1, 0) self.set_int64(UnitFields.UNIT_FIELD_RESISTANCES, self.resistance_0) self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 1, self.resistance_1) self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 2, self.resistance_2) self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 3, self.resistance_3) self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 4, self.resistance_4) self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 5, self.resistance_5) self.set_float(UnitFields.UNIT_FIELD_BOUNDINGRADIUS, self.bounding_radius) self.set_float(UnitFields.UNIT_FIELD_COMBATREACH, self.combat_reach) self.set_float(UnitFields.UNIT_FIELD_WEAPONREACH, self.weapon_reach) self.set_uint32(UnitFields.UNIT_FIELD_DISPLAYID, self.current_display_id) self.set_uint32(UnitFields.UNIT_FIELD_BYTES_0, self.bytes_0) self.set_uint32(UnitFields.UNIT_FIELD_BYTES_1, self.bytes_1) self.set_uint32(UnitFields.UNIT_FIELD_BYTES_2, self.bytes_2) self.set_float(UnitFields.UNIT_MOD_CAST_SPEED, self.mod_cast_speed) self.set_uint32(UnitFields.UNIT_DYNAMIC_FLAGS, self.dynamic_flags) self.set_uint32(UnitFields.UNIT_FIELD_DAMAGE, self.damage) return self.get_object_create_packet(is_self) def query_details(self): name_bytes = PacketWriter.string_to_bytes(self.creature_template.name) subname_bytes = PacketWriter.string_to_bytes(self.creature_template.subname) data = pack( f'<I{len(name_bytes)}ssss{len(subname_bytes)}s3I', self.entry, name_bytes, b'\x00', b'\x00', b'\x00', subname_bytes, self.creature_template.static_flags, self.creature_type, self.creature_template.beast_family ) return PacketWriter.get_packet(OpCode.SMSG_CREATURE_QUERY_RESPONSE, data) def can_swim(self): return (self.static_flags & CreatureStaticFlags.AMPHIBIOUS) or (self.static_flags & CreatureStaticFlags.AQUATIC) def can_exit_water(self): return self.static_flags & CreatureStaticFlags.AQUATIC == 0 def evade(self): # TODO: Finish implmenting evade mechanic. self.leave_combat(force=True) self.set_health(self.max_health) self.recharge_power() self.set_dirty() self.movement_manager.send_move_normal([self.spawn_position], self.running_speed, SplineFlags.SPLINEFLAG_RUNMODE) def _perform_random_movement(self, now): if not self.in_combat and self.creature_instance.movement_type == MovementTypes.WANDER: if len(self.movement_manager.pending_waypoints) == 0: if now > self.last_random_movement + self.random_movement_wait_time: self.movement_manager.move_random(self.spawn_position, self.creature_instance.wander_distance) self.random_movement_wait_time = randint(1, 12) self.last_random_movement = now def _perform_combat_movement(self): if self.combat_target: # TODO: Temp, extremely basic evade / runback mechanic based ONLY on distance. Replace later with a proper one. if self.location.distance(self.spawn_position) > 50: self.evade() return # TODO: There are some creatures like crabs or murlocs that apparently couldn't swim in earlier versions # but are spawned inside the water at this moment since most spawns come from Vanilla data. These mobs # will currently bug out when you try to engage in combat with them. Also seems like a lot of humanoids # couldn't swim before patch 1.3.0: # World of Warcraft Client Patch 1.3.0 (2005-03-22) # - Most humanoids NPCs have gained the ability to swim. if self.is_on_water(): if not self.can_swim(): self.evade() return else: if not self.can_exit_water(): self.evade() return current_distance = self.location.distance(self.combat_target.location) interactable_distance = UnitFormulas.interactable_distance(self, self.combat_target) # TODO: Find better formula? combat_position_distance = interactable_distance * 0.5 # If target is within combat distance, don't move. if current_distance <= combat_position_distance: return combat_location = self.combat_target.location.get_point_in_between(combat_position_distance, vector=self.location) # If already going to the correct spot, don't do anything. if len(self.movement_manager.pending_waypoints) > 0 and self.movement_manager.pending_waypoints[0].location == combat_location: return # Make sure the server knows where the creature is facing. self.location.face_point(self.combat_target.location) if self.is_on_water(): # Force destination Z to target Z. combat_location.z = self.combat_target.location.z # TODO: Find how to actually trigger swim animation and which spline flag to use. # VMaNGOS uses UNIT_FLAG_USE_SWIM_ANIMATION, we don't have that. self.movement_manager.send_move_normal([combat_location], self.swim_speed, SplineFlags.SPLINEFLAG_FLYING) else: self.movement_manager.send_move_normal([combat_location], self.running_speed, SplineFlags.SPLINEFLAG_RUNMODE) # override def update(self): now = time.time() if now > self.last_tick > 0: elapsed = now - self.last_tick if self.is_alive and self.is_spawned: # Spell/aura updates self.spell_manager.update(now, elapsed) self.aura_manager.update(now) # Movement Updates self.movement_manager.update_pending_waypoints(elapsed) # Random Movement self._perform_random_movement(now) # Combat movement self._perform_combat_movement() # Attack update if self.combat_target and self.is_within_interactable_distance(self.combat_target): self.attack_update(elapsed) # Dead elif not self.is_alive: self.respawn_timer += elapsed if self.respawn_timer >= self.respawn_time and not self.is_summon: self.respawn() # Destroy body when creature is about to respawn elif self.is_spawned and self.respawn_timer >= self.respawn_time * 0.8: self.despawn() # Check "dirtiness" to determine if this creature object should be updated yet or not. if self.dirty: MapManager.send_surrounding(self.generate_proper_update_packet(create=False), self, include_self=False) MapManager.update_object(self) if self.reset_fields_older_than(now): self.set_dirty(is_dirty=False) self.last_tick = now # override def respawn(self): super().respawn() # Set all property values before making this creature visible. self.location = self.spawn_position.copy() self.set_health(self.max_health) self.set_mana(self.max_power_1) self.loot_manager.clear() self.set_lootable(False) if self.killed_by and self.killed_by.group_manager: self.killed_by.group_manager.clear_looters_for_victim(self) self.killed_by = None self.respawn_timer = 0 self.respawn_time = randint(self.creature_instance.spawntimesecsmin, self.creature_instance.spawntimesecsmax) # Update its cell position if needed (Died far away from spawn location cell) MapManager.update_object(self) # Make this creature visible to its surroundings. MapManager.respawn_object(self) # override def die(self, killer=None): super().die(killer) self.loot_manager.generate_loot(killer) if killer and killer.get_type() == ObjectTypes.TYPE_PLAYER: self.reward_kill_xp(killer) self.killed_by = killer # If the player/group requires the kill, reward it to them. if self.killed_by.group_manager: self.killed_by.group_manager.reward_group_creature_or_go(self.killed_by, self) elif self.killed_by.quest_manager.reward_creature_or_go(self): self.killed_by.send_update_self() # If the player is in a group, set the group as allowed looters if needed. if self.killed_by.group_manager and self.loot_manager.has_loot(): self.killed_by.group_manager.set_allowed_looters(self) if self.loot_manager.has_loot(): self.set_lootable(True) self.set_dirty() return True def reward_kill_xp(self, player): if self.static_flags & CreatureStaticFlags.NO_XP: return is_elite = 0 < self.creature_template.rank < 4 if player.group_manager: player.group_manager.reward_group_xp(player, self, is_elite) else: player.give_xp([Formulas.CreatureFormulas.xp_reward(self.level, player.level, is_elite)], self) def set_lootable(self, flag=True): if flag: self.dynamic_flags |= UnitDynamicTypes.UNIT_DYNAMIC_LOOTABLE else: self.dynamic_flags &= ~UnitDynamicTypes.UNIT_DYNAMIC_LOOTABLE self.set_uint32(UnitFields.UNIT_DYNAMIC_FLAGS, self.dynamic_flags) # override def has_offhand_weapon(self): return self.wearing_offhand_weapon # override def has_ranged_weapon(self): return self.wearing_ranged_weapon # override def set_weapon_mode(self, weapon_mode): super().set_weapon_mode(weapon_mode) self.bytes_2 = unpack('<I', pack('<4B', self.sheath_state, 0, 0, 0))[0] self.set_uint32(UnitFields.UNIT_FIELD_BYTES_2, self.bytes_2) # override def set_stand_state(self, stand_state): super().set_stand_state(stand_state) self.bytes_1 = unpack('<I', pack('<4B', self.stand_state, self.npc_flags, self.shapeshift_form, 0))[0] self.set_uint32(UnitFields.UNIT_FIELD_BYTES_1, self.bytes_1) # override def set_shapeshift_form(self, shapeshift_form): super().set_shapeshift_form(shapeshift_form) self.bytes_1 = unpack('<I', pack('<4B', self.stand_state, self.npc_flags, self.shapeshift_form, 0))[0] self.set_uint32(UnitFields.UNIT_FIELD_BYTES_1, self.bytes_1) # override def get_type(self): return ObjectTypes.TYPE_UNIT # override def get_type_id(self): return ObjectTypeIds.ID_UNIT
class CreatureManager(UnitManager): CURRENT_HIGHEST_GUID = 0 def __init__(self, creature_template, creature_instance=None, summoner=None, **kwargs): super().__init__(**kwargs) self.creature_template = creature_template self.creature_instance = creature_instance self.fully_loaded = False self.killed_by = None self.summoner = summoner self.known_players = {} self.entry = self.creature_template.entry self.class_ = self.creature_template.unit_class self.native_display_id = self.generate_display_id() self.current_display_id = self.native_display_id self.level = randint(self.creature_template.level_min, self.creature_template.level_max) # Calculate relative level in order to get health and mana values. rel_level = 0 if self.creature_template.level_max == self.creature_template.level_min else \ ((self.level - self.creature_template.level_min) / (self.creature_template.level_max - self.creature_template.level_min)) self.max_health = self.creature_template.health_min + int( rel_level * (self.creature_template.health_max - self.creature_template.health_min)) self.max_power_1 = self.creature_template.mana_min + int(rel_level * ( self.creature_template.mana_max - self.creature_template.mana_min)) self.health = self.max_health self.power_1 = self.max_power_1 self.resistance_0 = self.creature_template.armor self.resistance_1 = self.creature_template.holy_res self.resistance_2 = self.creature_template.fire_res self.resistance_3 = self.creature_template.nature_res self.resistance_4 = self.creature_template.frost_res self.resistance_5 = self.creature_template.shadow_res self.npc_flags = self.creature_template.npc_flags self.static_flags = self.creature_template.static_flags self.mod_cast_speed = 1.0 self.base_attack_time = self.creature_template.base_attack_time self.unit_flags = self.creature_template.unit_flags self.emote_state = 0 self.faction = self.creature_template.faction self.creature_type = self.creature_template.type self.spell_list_id = self.creature_template.spell_list_id self.sheath_state = WeaponMode.NORMALMODE self.regen_flags = self.creature_template.regeneration self.virtual_item_info = {} # Slot: VirtualItemInfoHolder self.set_melee_damage(int(self.creature_template.dmg_min), int(self.creature_template.dmg_max)) if 0 < self.creature_template.rank < 4: self.unit_flags = self.unit_flags | UnitFlags.UNIT_FLAG_PLUS_MOB # TODO, creatures are still resolving to aggressive. if self.is_totem() or self.is_critter() or not self.can_have_target(): self.react_state = CreatureReactStates.REACT_PASSIVE elif self.creature_template.flags_extra & CreatureFlagsExtra.CREATURE_FLAG_EXTRA_NO_AGGRO: self.react_state = CreatureReactStates.REACT_DEFENSIVE else: self.react_state = CreatureReactStates.REACT_AGGRESSIVE self.wearing_offhand_weapon = False self.wearing_ranged_weapon = False self.time_to_live_timer = -1 self.respawn_timer = 0 self.last_random_movement = 0 self.random_movement_wait_time = randint(1, 12) if self.creature_instance: if CreatureManager.CURRENT_HIGHEST_GUID < creature_instance.spawn_id: CreatureManager.CURRENT_HIGHEST_GUID = creature_instance.spawn_id self.guid = self.generate_object_guid(creature_instance.spawn_id) self.health = int((self.creature_instance.health_percent / 100) * self.max_health) # If spawned by another unit, use that unit map and zone. self.map_ = self.creature_instance.map if not self.summoner else self.summoner.map_ self.zone = self.summoner.zone if self.summoner else 0 self.spawn_position = Vector(self.creature_instance.position_x, self.creature_instance.position_y, self.creature_instance.position_z, self.creature_instance.orientation) self.location = self.spawn_position.copy() self.respawn_time = randint( self.creature_instance.spawntimesecsmin, self.creature_instance.spawntimesecsmax) # Creature AI. self.object_ai = AIFactory.build_ai(self) # All creatures can block, parry and dodge by default. # TODO, Checks for CREATURE_FLAG_EXTRA_NO_BLOCK and CREATURE_FLAG_EXTRA_NO_PARRY, for hit results. self.has_block_passive = True self.has_dodge_passive = True self.has_parry_passive = True # Managers, will be load upon lazy loading trigger. self.loot_manager = None self.pickpocket_loot_manager = None self.threat_manager = None @dataclass class VirtualItemInfoHolder: display_id: int = 0 info_packed: int = 0 # ClassID, SubClassID, Material, InventoryType. info_packed_2: int = 0 # Sheath, Padding, Padding, Padding. def load(self): MapManager.update_object(self) @staticmethod def spawn(entry, location, map_id, summoner=None, override_faction=0, despawn_time=1, spell_id=0, ttl=-1): creature_template = WorldDatabaseManager.creature_get_by_entry(entry) if not creature_template: return None instance = SpawnsCreatures() instance.spawn_id = CreatureManager.CURRENT_HIGHEST_GUID + 1 instance.spawn_entry1 = entry instance.map = map_id instance.position_x = location.x instance.position_y = location.y instance.position_z = location.z instance.orientation = location.o instance.health_percent = 100 instance.mana_percent = 100 if despawn_time < 1: despawn_time = 1 instance.spawntimesecsmin = despawn_time instance.spawntimesecsmax = despawn_time creature = CreatureManager(creature_template=creature_template, creature_instance=instance, summoner=summoner) if ttl > 0: creature.time_to_live_timer = ttl if spell_id: creature.set_uint32(UnitFields.UNIT_CREATED_BY_SPELL, spell_id) if override_faction > 0: creature.faction = override_faction creature.load() return creature def generate_display_id(self): display_id_list = list( filter((0).__ne__, [ self.creature_template.display_id1, self.creature_template.display_id2, self.creature_template.display_id3, self.creature_template.display_id4 ])) return choice( display_id_list) if len(display_id_list) > 0 else 4 # 4 = cube def send_inventory_list(self, world_session): vendor_data, session = WorldDatabaseManager.creature_get_vendor_data( self.entry) item_count = len(vendor_data) if vendor_data else 0 data = pack('<QB', self.guid, item_count) if item_count == 0: data += pack('<B', 0) else: item_query = pack('<I', len(vendor_data)) for count, vendor_data_entry in enumerate(vendor_data): data += pack( '<7I', count + 1, # m_muid, acts as slot counter. vendor_data_entry.item, vendor_data_entry.item_template.display_id, 0xFFFFFFFF if vendor_data_entry.maxcount <= 0 else vendor_data_entry.maxcount, vendor_data_entry.item_template.buy_price, vendor_data_entry.item_template. max_durability, # Max durability (not implemented in 0.5.3). vendor_data_entry.item_template.buy_count # Stack count. ) item_query += ItemManager.generate_query_details_data( vendor_data_entry.item_template) # Send all vendor item query details. world_session.enqueue_packet( PacketWriter.get_packet( OpCode.SMSG_ITEM_QUERY_MULTIPLE_RESPONSE, item_query)) session.close() world_session.enqueue_packet( PacketWriter.get_packet(OpCode.SMSG_LIST_INVENTORY, data)) # TODO Add skills (Two-Handed Swords etc.) to trainers for skill points https://i.imgur.com/tzyDDqL.jpg def send_trainer_list(self, world_session): if not self.can_train(world_session.player_mgr): Logger.anticheat( f'send_trainer_list called from NPC {self.entry} by player with GUID {world_session.player_mgr.guid} but this unit does not train that player\'s class. Possible cheating' ) return train_spell_bytes: bytes = b'' train_spell_count: int = 0 trainer_ability_list: list[ TrainerTemplate] = WorldDatabaseManager.TrainerSpellHolder.trainer_spells_get_by_trainer( self.entry) if not trainer_ability_list or trainer_ability_list.count == 0: Logger.warning( f'send_trainer_list called from NPC {self.entry} but no trainer spells found!' ) return for trainer_spell in trainer_ability_list: # trainer_spell: The spell the trainer uses to teach the player. player_spell_id = trainer_spell.playerspell ability_spell_chain: SpellChain = WorldDatabaseManager.SpellChainHolder.spell_chain_get_by_spell( player_spell_id) spell_level: int = trainer_spell.reqlevel # Use this and not spell data, as there are differences between data source (2003 Game Guide) and what is in spell table. spell_rank: int = ability_spell_chain.rank prev_spell: int = ability_spell_chain.prev_spell spell_is_too_high_level: bool = spell_level > world_session.player_mgr.level if player_spell_id in world_session.player_mgr.spell_manager.spells: status = TrainerServices.TRAINER_SERVICE_USED else: if prev_spell in world_session.player_mgr.spell_manager.spells and spell_rank > 1 and not spell_is_too_high_level: status = TrainerServices.TRAINER_SERVICE_AVAILABLE elif spell_rank == 1 and not spell_is_too_high_level: status = TrainerServices.TRAINER_SERVICE_AVAILABLE else: status = TrainerServices.TRAINER_SERVICE_UNAVAILABLE data: bytes = pack( '<IBI3B6I', player_spell_id, # Spell id status, # Status trainer_spell.spellcost, # Cost trainer_spell.talentpointcost, # Talent Point Cost trainer_spell.skillpointcost, # Skill Point Cost spell_level, # Required Level trainer_spell.reqskill, # Required Skill Line trainer_spell.reqskillvalue, # Required Skill Rank 0, # Required Skill Step prev_spell, # Required Ability (1) 0, # Required Ability (2) 0 # Required Ability (3) ) train_spell_bytes += data train_spell_count += 1 # TODO: Placeholder text, although it seems to appear in most of the trainer screenshots. # https://imgur.com/a/70OcLjv placeholder_greeting: str = f'Hello, $c! Ready for some training?' greeting_bytes = PacketWriter.string_to_bytes( GameTextFormatter.format(world_session.player_mgr, placeholder_greeting)) greeting_bytes = pack(f'<{len(greeting_bytes)}s', greeting_bytes) data = pack('<Q2I', self.guid, TrainerTypes.TRAINER_TYPE_GENERAL, train_spell_count) + train_spell_bytes + greeting_bytes world_session.player_mgr.enqueue_packet( PacketWriter.get_packet(OpCode.SMSG_TRAINER_LIST, data)) def finish_loading(self): if self.creature_instance and not self.fully_loaded: # Load loot manager. self.loot_manager = CreatureLootManager(self) # Load pickpocket loot manager if required. if self.creature_template.pickpocket_loot_id: self.pickpocket_loot_manager = CreaturePickPocketLootManager( self) self.threat_manager = ThreatManager(self) creature_model_info = WorldDatabaseManager.CreatureModelInfoHolder.creature_get_model_info( self.current_display_id) if creature_model_info: self.bounding_radius = creature_model_info.bounding_radius self.combat_reach = creature_model_info.combat_reach self.gender = creature_model_info.gender if self.creature_template.scale == 0: display_scale = DbcDatabaseManager.CreatureDisplayInfoHolder.creature_display_info_get_by_id( self.current_display_id) if display_scale and display_scale.CreatureModelScale > 0: self.native_scale = display_scale.CreatureModelScale else: self.native_scale = 1 else: self.native_scale = self.creature_template.scale self.current_scale = self.native_scale if self.creature_template.equipment_id > 0: creature_equip_template = WorldDatabaseManager.CreatureEquipmentHolder.creature_get_equipment_by_id( self.creature_template.equipment_id) if creature_equip_template: self.set_virtual_item(0, creature_equip_template.equipentry1) self.set_virtual_item(1, creature_equip_template.equipentry2) self.set_virtual_item(2, creature_equip_template.equipentry3) addon_template = self.creature_instance.addon_template if addon_template: self.set_stand_state(addon_template.stand_state) self.set_weapon_mode(addon_template.sheath_state) # Set emote state if available. if addon_template.emote_state: self.set_emote_state(addon_template.emote_state) # Check auras; 'auras' points to an entry id on Spell dbc. if addon_template.auras: spells = str(addon_template.auras).rsplit(' ') for spell in spells: self.spell_manager.handle_cast_attempt( int(spell), self, SpellTargetMask.SELF, validate=False) # Update display id if available. if addon_template.display_id: self.set_display_id(addon_template.display_id) # Mount this creature if defined. if addon_template.mount_display_id > 0: self.mount(addon_template.mount_display_id) # Stats. self.stat_manager.init_stats() self.stat_manager.apply_bonuses(replenish=True) self.fully_loaded = True def is_guard(self): return self.creature_template.flags_extra & CreatureFlagsExtra.CREATURE_FLAG_EXTRA_GUARD def can_summon_guards(self): return self.creature_template.flags_extra & CreatureFlagsExtra.CREATURE_FLAG_EXTRA_SUMMON_GUARD def is_critter(self): return self.creature_template.type == CreatureTypes.AMBIENT # TODO, should be able to check 'ownership' or set a custom flag upon creature creation. def is_pet(self): return False # TODO, should be able to check 'ownership' or set a custom flag upon creature creation. def is_totem(self): return False def can_have_target(self): return not self.creature_template.flags_extra & CreatureFlagsExtra.CREATURE_FLAG_EXTRA_NO_TARGET def set_virtual_item(self, slot, item_entry): item_template = None if item_entry > 0: item_template = WorldDatabaseManager.ItemTemplateHolder.item_template_get_by_entry( item_entry) if item_template: virtual_item_info = ByteUtils.bytes_to_int( item_template.inventory_type, item_template.material, item_template.subclass, item_template.class_, ) virtual_item_info_2 = ByteUtils.bytes_to_int( 0, 0, 0, # Padding. item_template.sheath) self.virtual_item_info[ slot] = CreatureManager.VirtualItemInfoHolder( item_template.display_id, virtual_item_info, virtual_item_info_2) # Main hand. if slot == 0: self.weapon_reach = UnitFormulas.get_reach_for_weapon( item_template) # Offhand. if slot == 1: self.wearing_offhand_weapon = (item_template.inventory_type == InventoryTypes.WEAPON or item_template.inventory_type == InventoryTypes.WEAPONOFFHAND) # Ranged. if slot == 2: self.wearing_ranged_weapon = (item_template.inventory_type == InventoryTypes.RANGED or item_template.inventory_type == InventoryTypes.RANGEDRIGHT) else: self.virtual_item_info[ slot] = CreatureManager.VirtualItemInfoHolder() if slot == 0: self.weapon_reach = 0.0 self.set_float(UnitFields.UNIT_FIELD_WEAPONREACH, self.weapon_reach) def is_quest_giver(self) -> bool: return self.npc_flags & NpcFlags.NPC_FLAG_QUESTGIVER def is_trainer(self) -> bool: return self.npc_flags & NpcFlags.NPC_FLAG_TRAINER # override def is_tameable(self) -> bool: return self.static_flags & CreatureStaticFlags.TAMEABLE # TODO: Validate trainer_spell field and Pet trainers. def can_train(self, player_mgr) -> bool: if not self.is_trainer(): return False if not self.is_within_interactable_distance( player_mgr) and not player_mgr.is_gm: return False # If expecting a specific class, check if they match. if self.creature_template.trainer_class > 0: return self.creature_template.trainer_class == player_mgr.player.class_ # Mount, TradeSkill or Pet trainer. return True def trainer_has_spell(self, spell_id: int) -> bool: if not self.is_trainer(): return False trainer_spells: list[ TrainerTemplate] = WorldDatabaseManager.TrainerSpellHolder.trainer_spells_get_by_trainer( self.entry) for trainer_spell in trainer_spells: if trainer_spell.spell == spell_id: return True return False # override def initialize_field_values(self): # Lazy loading first. if not self.fully_loaded: self.finish_loading() # Initialize values. # After this, fields must be modified by setters or directly writing values to them. if not self.initialized and self.creature_instance: self.bytes_1 = self.get_bytes_1() self.bytes_2 = self.get_bytes_2() self.damage = self.get_damages() # Object fields. self.set_uint64(ObjectFields.OBJECT_FIELD_GUID, self.guid) self.set_uint32(ObjectFields.OBJECT_FIELD_TYPE, self.object_type_mask) self.set_uint32(ObjectFields.OBJECT_FIELD_ENTRY, self.entry) self.set_float(ObjectFields.OBJECT_FIELD_SCALE_X, self.current_scale) # Unit fields. self.set_uint32(UnitFields.UNIT_CHANNEL_SPELL, self.channel_spell) self.set_uint64(UnitFields.UNIT_FIELD_CHANNEL_OBJECT, self.channel_object) self.set_uint32(UnitFields.UNIT_FIELD_HEALTH, self.health) self.set_uint32(UnitFields.UNIT_FIELD_MAXHEALTH, self.max_health) self.set_uint32(UnitFields.UNIT_FIELD_POWER1, self.power_1) self.set_uint32(UnitFields.UNIT_FIELD_MAXPOWER1, self.max_power_1) self.set_uint32(UnitFields.UNIT_FIELD_LEVEL, self.level) self.set_uint32(UnitFields.UNIT_FIELD_FACTIONTEMPLATE, self.faction) self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags) self.set_uint32(UnitFields.UNIT_FIELD_COINAGE, self.coinage) self.set_float(UnitFields.UNIT_FIELD_BASEATTACKTIME, self.base_attack_time) self.set_float(UnitFields.UNIT_FIELD_BASEATTACKTIME + 1, 0) self.set_int64(UnitFields.UNIT_FIELD_RESISTANCES, self.resistance_0) self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 1, self.resistance_1) self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 2, self.resistance_2) self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 3, self.resistance_3) self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 4, self.resistance_4) self.set_int32(UnitFields.UNIT_FIELD_RESISTANCES + 5, self.resistance_5) self.set_float(UnitFields.UNIT_FIELD_BOUNDINGRADIUS, self.bounding_radius) self.set_float(UnitFields.UNIT_FIELD_COMBATREACH, self.combat_reach) self.set_float(UnitFields.UNIT_FIELD_WEAPONREACH, self.weapon_reach) self.set_uint32(UnitFields.UNIT_FIELD_DISPLAYID, self.current_display_id) self.set_uint32(UnitFields.UNIT_FIELD_MOUNTDISPLAYID, self.mount_display_id) self.set_uint32(UnitFields.UNIT_EMOTE_STATE, self.emote_state) self.set_uint32(UnitFields.UNIT_FIELD_BYTES_0, self.bytes_0) self.set_uint32(UnitFields.UNIT_FIELD_BYTES_1, self.bytes_1) self.set_uint32(UnitFields.UNIT_FIELD_BYTES_2, self.bytes_2) self.set_float(UnitFields.UNIT_MOD_CAST_SPEED, self.mod_cast_speed) self.set_uint32(UnitFields.UNIT_DYNAMIC_FLAGS, self.dynamic_flags) self.set_uint32(UnitFields.UNIT_FIELD_DAMAGE, self.damage) for slot, virtual_item in self.virtual_item_info.items(): self.set_uint32( UnitFields.UNIT_VIRTUAL_ITEM_SLOT_DISPLAY + slot, virtual_item.display_id) self.set_uint32( UnitFields.UNIT_VIRTUAL_ITEM_INFO + (slot * 2) + 0, virtual_item.info_packed) self.set_uint32( UnitFields.UNIT_VIRTUAL_ITEM_INFO + (slot * 2) + 1, virtual_item.info_packed_2) self.initialized = True def query_details(self): name_bytes = PacketWriter.string_to_bytes(self.creature_template.name) subname_bytes = PacketWriter.string_to_bytes( self.creature_template.subname) data = pack(f'<I{len(name_bytes)}ssss{len(subname_bytes)}s3I', self.entry, name_bytes, b'\x00', b'\x00', b'\x00', subname_bytes, self.creature_template.static_flags, self.creature_type, self.creature_template.beast_family) return PacketWriter.get_packet(OpCode.SMSG_CREATURE_QUERY_RESPONSE, data) def can_swim(self): return (self.static_flags & CreatureStaticFlags.AMPHIBIOUS) or ( self.static_flags & CreatureStaticFlags.AQUATIC) def can_exit_water(self): return self.static_flags & CreatureStaticFlags.AQUATIC == 0 # override def leave_combat(self, force=False): super().leave_combat(force=force) self.threat_manager.reset() # TODO: Finish implementing evade mechanic. def evade(self): # Already evading. if self.is_evading: return # Get the path we are using to get back to spawn location. waypoints_to_spawn, z_locked = self._get_return_to_spawn_points() self.leave_combat(force=True) if not self.static_flags & CreatureStaticFlags.NO_AUTO_REGEN: self.set_health(self.max_health) self.recharge_power() self.is_evading = True # TODO: Find a proper move type that accepts multiple waypoints, RUNMODE and others halt the unit movement. spline_flag = SplineFlags.SPLINEFLAG_RUNMODE if not z_locked else SplineFlags.SPLINEFLAG_FLYING self.movement_manager.send_move_normal(waypoints_to_spawn, self.running_speed, spline_flag) # TODO: Below return to spawn point logic should be removed once a navmesh is available. def _get_return_to_spawn_points( self) -> tuple: # [waypoints], z_locked bool # No points, return just spawn point. if len(self.evading_waypoints) == 0: return [self.spawn_position], False # Reverse the combat waypoints, so they point back to spawn location. waypoints = [wp for wp in reversed(self.evading_waypoints)] # Set self location to the latest known point. self.location = waypoints[0].copy() last_waypoint = self.location # Distance we want between each waypoint. d_factor = 4 # Try to use waypoints only for units that have invalid z calculations. z_locked = False distance_sum = 0 # Filter the waypoints by distance, remove those that are too close to each other. for waypoint in list(waypoints): # Check for protected z. if not z_locked: z, z_locked = MapManager.calculate_z(self.map_, waypoint.x, waypoint.y, waypoint.z) distance_sum += last_waypoint.distance(waypoint) if distance_sum < d_factor: waypoints.remove(waypoint) else: distance_sum = 0 last_waypoint = waypoint if z_locked: # Make sure the last waypoints its self spawn position. waypoints.append(self.spawn_position.copy()) else: # This unit is probably outside a cave, do not use waypoints. waypoints.clear() waypoints.append(self.spawn_position) return waypoints, z_locked def is_moving(self): return self.movement_manager.unit_is_moving() def stop_movement(self): self.movement_manager.send_move_stop() def is_casting(self): return self.spell_manager.is_casting() def has_observers(self): return any(self.known_players) def has_wander_type(self): return self.creature_instance.movement_type == MovementTypes.WANDER def _perform_random_movement(self, now): # Do not wander in combat, while evading, without wander flag or if unit has no observers. if not self.in_combat and not self.is_evading and self.has_wander_type( ): if len(self.movement_manager.pending_waypoints) == 0: if now > self.last_random_movement + self.random_movement_wait_time: self.movement_manager.move_random( self.spawn_position, self.creature_instance.wander_distance) self.random_movement_wait_time = randint(1, 12) self.last_random_movement = now # TODO: All the evade calls should be probably handled by aggro manager, it should be able to decide if unit can # switch to another target from the Threat list or evade, or some other action. def _perform_combat_movement(self): if self.combat_target and not self.is_casting( ) and not self.is_evading: if not self.combat_target.is_alive and len(self.attackers) == 0 or \ (self.combat_target.get_type_id() == ObjectTypeIds.ID_PLAYER and not self.combat_target.online): self.evade() return target_distance = self.location.distance( self.combat_target.location) combat_position_distance = UnitFormulas.combat_distance( self, self.combat_target) # In 0.5.3, evade mechanic was only based on distance, the correct distance remains unknown. # From 0.5.4 patch notes: # "Creature pursuit is now timer based rather than distance based." if self.location.distance(self.spawn_position) > Distances.CREATURE_EVADE_DISTANCE \ or target_distance > Distances.CREATURE_EVADE_DISTANCE: self.evade() return # TODO: There are some creatures like crabs or murlocs that apparently couldn't swim in earlier versions # but are spawned inside the water at this moment since most spawns come from Vanilla data. These mobs # will currently bug out when you try to engage in combat with them. Also seems like a lot of humanoids # couldn't swim before patch 1.3.0: # World of Warcraft Client Patch 1.3.0 (2005-03-22) # - Most humanoids NPCs have gained the ability to swim. if self.is_on_water(): if not self.can_swim(): self.evade() return else: if not self.can_exit_water(): self.evade() return # If target is within combat distance, don't move but do check creature orientation. if target_distance <= combat_position_distance: # If this creature is not facing the attacker, update its orientation (server-side). if not self.location.has_in_arc(self.combat_target.location, math.pi): self.location.face_point(self.combat_target.location) return combat_location = self.combat_target.location.get_point_in_between( combat_position_distance, vector=self.location) if not combat_location: return # If already going to the correct spot, don't do anything. if len(self.movement_manager.pending_waypoints) > 0 \ and self.movement_manager.pending_waypoints[0].location == combat_location: return # Make sure the server knows where the creature is facing. self.location.face_point(self.combat_target.location) if self.is_on_water(): # Force destination Z to target Z. combat_location.z = self.combat_target.location.z # TODO: Find how to actually trigger swim animation and which spline flag to use. # VMaNGOS uses UNIT_FLAG_USE_SWIM_ANIMATION, we don't have that. self.movement_manager.send_move_normal( [combat_location], self.swim_speed, SplineFlags.SPLINEFLAG_FLYING) else: self.movement_manager.send_move_normal( [combat_location], self.running_speed, SplineFlags.SPLINEFLAG_RUNMODE) # override def update(self, now): if now > self.last_tick > 0: elapsed = now - self.last_tick if self.is_alive and self.is_spawned and self.initialized: # Time to live. if self.time_to_live_timer > 0: self.time_to_live_timer -= elapsed # Regeneration. self.regenerate(elapsed) # Spell/Aura Update. self.spell_manager.update(now) self.aura_manager.update(now) # Movement Updates. self.movement_manager.update_pending_waypoints(elapsed) if self.has_moved: self._on_relocation() self.set_has_moved(False) # Random Movement, if visible to players. if self.has_observers(): self._perform_random_movement(now) # Combat Movement. self._perform_combat_movement() # AI. if self.object_ai: self.object_ai.update_ai(elapsed) # Attack Update. if self.combat_target and self.is_within_interactable_distance( self.combat_target): self.attack_update(elapsed) # Not in combat, check if threat manager can resolve a target. elif self.threat_manager: target = self.threat_manager.resolve_target() if target: is_melee = self.is_within_interactable_distance(target) self.attack(target, is_melee=is_melee) # Dead elif not self.is_alive and self.initialized: self.respawn_timer += elapsed if self.respawn_timer >= self.respawn_time: self.respawn() # Destroy body when creature is about to respawn. elif self.is_spawned and self.respawn_timer >= self.respawn_time * 0.8: if self.summoner: self.despawn(destroy=True) else: self.despawn() # Time to live expired, destroy. if self.time_to_live_timer < -1: self.despawn(destroy=True) # Check if this creature object should be updated yet or not. elif self.has_pending_updates(): MapManager.update_object(self, has_changes=True) self.reset_fields_older_than(now) self.last_tick = now # override def attack_update(self, elapsed): target = self.threat_manager.get_hostile_target() # Has a target, check if we need to attack or switch target. if target and self.combat_target != target: self.attack(target) # No target at all, leave combat, reset aggro. elif not target: self.leave_combat() return super().attack_update(elapsed) # override def receive_damage(self, amount, source=None, is_periodic=False, casting_spell=None): super().receive_damage(amount, source, is_periodic) if self.is_alive: # If creature's being attacked by another unit, automatically set combat target. not_attacked_by_gameobject = source and source.get_type_id( ) != ObjectTypeIds.ID_GAMEOBJECT if not self.combat_target and not_attacked_by_gameobject: # Make sure to first stop any movement right away. if len(self.movement_manager.pending_waypoints) > 0: self.movement_manager.send_move_stop() threat = amount # TODO: Threat calculation. # No threat but source spell generates threat on miss. if casting_spell and threat == 0 and casting_spell.generates_threat_on_miss( ): threat = 10 # Physical miss, block, etc. elif not casting_spell and threat == 0: threat = 10 self.threat_manager.add_threat(source, threat) # override def respawn(self): super().respawn() # Set all property values before making this creature visible. self.location = self.spawn_position.copy() self.set_health(self.max_health) self.set_mana(self.max_power_1) self.loot_manager.clear() self.set_lootable(False) if self.killed_by and self.killed_by.group_manager: self.killed_by.group_manager.clear_looters_for_victim(self) self.killed_by = None self.respawn_timer = 0 self.respawn_time = randint(self.creature_instance.spawntimesecsmin, self.creature_instance.spawntimesecsmax) # Update its cell position if needed (Died far away from spawn location cell) MapManager.update_object(self) # Make this creature visible to its surroundings. MapManager.respawn_object(self) # override def die(self, killer=None): if not self.is_alive: return False self.attackers.clear() self.threat_manager.reset() if killer.get_type_id() != ObjectTypeIds.ID_PLAYER: # Attribute non-player kills to the creature's summoner. # TODO Does this also apply for player mind control? killer = killer.summoner if killer.summoner else killer if killer and killer.get_type_id() == ObjectTypeIds.ID_PLAYER: self.loot_manager.generate_loot(killer) self.reward_kill_xp(killer) self.killed_by = killer # Handle required creature or go for quests and reputation. if self.killed_by.group_manager: self.killed_by.group_manager.reward_group_reputation( self.killed_by, self) self.killed_by.group_manager.reward_group_creature_or_go( self.killed_by, self) else: # Reward required creature or go and reputation to the player with the killing blow. self.killed_by.reward_reputation_on_kill(self) self.killed_by.quest_manager.reward_creature_or_go(self) # If the player is in a group, set the group as allowed looters if needed. if self.killed_by.group_manager and self.loot_manager.has_loot(): self.killed_by.group_manager.set_allowed_looters(self) if self.loot_manager.has_loot(): self.set_lootable(True) return super().die(killer) def reward_kill_xp(self, player): if self.static_flags & CreatureStaticFlags.NO_XP: return is_elite = 0 < self.creature_template.rank < 4 if player.group_manager: player.group_manager.reward_group_xp(player, self, is_elite) else: player.give_xp([ Formulas.CreatureFormulas.xp_reward(self.level, player.level, is_elite) ], self) # override def set_max_mana(self, mana): if self.max_power_1 > 0: self.max_power_1 = mana self.set_uint32(UnitFields.UNIT_FIELD_MAXPOWER1, mana) def set_emote_state(self, emote_state): self.emote_state = emote_state self.set_uint32(UnitFields.UNIT_EMOTE_STATE, self.emote_state) def set_lootable(self, flag=True): if flag: self.dynamic_flags |= UnitDynamicTypes.UNIT_DYNAMIC_LOOTABLE else: self.dynamic_flags &= ~UnitDynamicTypes.UNIT_DYNAMIC_LOOTABLE self.set_uint32(UnitFields.UNIT_DYNAMIC_FLAGS, self.dynamic_flags) # override def get_bytes_0(self): return ByteUtils.bytes_to_int( self.power_type, # power type self.gender, # gender self.creature_template.unit_class, # class self.race # race (0 for creatures) ) # override def get_bytes_1(self): return ByteUtils.bytes_to_int( 0, # visibility flags self.shapeshift_form, # shapeshift form self.npc_flags, # npc flags self.stand_state # stand state ) # override def get_bytes_2(self): return ByteUtils.bytes_to_int( 0, # unknown 0, # pet flags 0, # misc flags self.sheath_state # sheath state ) # override def get_damages(self): return ByteUtils.shorts_to_int(int(self.creature_template.dmg_max), int(self.creature_template.dmg_min)) def _on_relocation(self): self.object_ai.movement_inform() # override def notify_moved_in_line_of_sight(self, target): self.object_ai.move_in_line_of_sight(target) # override def has_offhand_weapon(self): return self.wearing_offhand_weapon # override def has_ranged_weapon(self): return self.wearing_ranged_weapon # override def set_weapon_mode(self, weapon_mode): super().set_weapon_mode(weapon_mode) self.bytes_2 = self.get_bytes_2() self.set_uint32(UnitFields.UNIT_FIELD_BYTES_2, self.bytes_2) # override def set_stand_state(self, stand_state): super().set_stand_state(stand_state) self.bytes_1 = self.get_bytes_1() self.set_uint32(UnitFields.UNIT_FIELD_BYTES_1, self.bytes_1) # override def set_shapeshift_form(self, shapeshift_form): super().set_shapeshift_form(shapeshift_form) self.bytes_1 = self.get_bytes_1() self.set_uint32(UnitFields.UNIT_FIELD_BYTES_1, self.bytes_1) # override def update_power_type(self): if not self.shapeshift_form: self.power_type = PowerTypes.TYPE_MANA else: self.power_type = ShapeshiftInfo.get_power_for_form( self.shapeshift_form) self.bytes_0 = self.get_bytes_0() # override def get_type_id(self): return ObjectTypeIds.ID_UNIT