From 2ecb62e7362c7cd97b7c95fc441b5799c4c19146 Mon Sep 17 00:00:00 2001 From: devw4r <108442943+devw4r@users.noreply.github.com> Date: Wed, 11 Sep 2024 10:12:41 -0600 Subject: [PATCH] Closes #1201 Closes #571 Closes #699 Closes #695 Closes #992 Closes #976 Closes #629 Closes #413 Closes #1094 (#1436) * Closes #1201 Closes #1201 * Closes #571 Closes #571 * Update updates.sql * Closes #699 Closes #699 * Update updates.sql * Update UnitManager.py * Update GameObjectLootManager.py * Update FearMovement.py * Update FearMovement.py * Update FearMovement.py * Crushing blow. * Update UnitManager.py * Update AuraManager.py * Specialization.. * Update ExtendedSpellData.py * Update updates.sql * Update QuestManager.py * Update updates.sql * Fix quest status display bug. - Unavailable quest due race / class requirements / disabled should no longer wrongfully display available in the future (gray symbol) * Update QuestManager.py * Update updates.sql * Update updates.sql * Update QuestHelpers.py err * Update ReadItemHandler.py * CMSG_READ_ITEM * Modify both UnitFlag and UnitState when possible. - Fix ConfusedMovement not applying unit flag. * Update UnitManager.py * Update updates.sql * Route all flag changes through setters. * Update LootManager.py * Update PlayerManager.py * Remove gold by surrounding units gold mean. * Remove second pass for loot generation. Left comment. * Update updates.sql * Update PlayerManager.py * Update PlayerManager.py * Update GameObjectLootManager.py * Update UnitManager.py * Update updates.sql --- database/dbc/DbcDatabaseManager.py | 8 -- etc/databases/world/updates/updates.sql | 84 +++++++++++++ .../gameobjects/GameObjectLootManager.py | 3 +- .../gameobjects/managers/ChestMananger.py | 3 +- .../managers/objects/loot/LootManager.py | 4 + .../objects/spell/ExtendedSpellData.py | 21 ++++ .../managers/objects/spell/SpellManager.py | 3 + .../objects/spell/aura/AuraEffectHandler.py | 2 +- .../objects/spell/aura/AuraManager.py | 2 +- .../managers/objects/units/UnitManager.py | 118 +++++++++++------- .../objects/units/creature/CreatureBuilder.py | 3 +- .../objects/units/creature/CreatureManager.py | 22 ++-- .../movement/behaviors/ConfusedMovement.py | 7 +- .../movement/behaviors/DistractedMovement.py | 4 +- .../units/movement/behaviors/FearMovement.py | 7 +- .../objects/units/player/InventoryManager.py | 32 +++++ .../objects/units/player/PlayerManager.py | 10 +- .../objects/units/player/StatManager.py | 11 ++ .../objects/units/player/TalentManager.py | 5 +- .../units/player/quest/QuestHelpers.py | 18 +++ .../units/player/quest/QuestManager.py | 9 +- .../handlers/inventory/ReadItemHandler.py | 8 +- .../handlers/loot/LootRequestHandler.py | 3 +- 23 files changed, 292 insertions(+), 95 deletions(-) diff --git a/database/dbc/DbcDatabaseManager.py b/database/dbc/DbcDatabaseManager.py index 001f4a07c..29e0ab1ba 100644 --- a/database/dbc/DbcDatabaseManager.py +++ b/database/dbc/DbcDatabaseManager.py @@ -190,14 +190,6 @@ def spell_get_rank_by_id(spell_id): return DbcDatabaseManager.SpellHolder.spell_get_rank_by_spell(spell) - @staticmethod - def spell_is_specialization(spell_id): - spell = DbcDatabaseManager.SpellHolder.spell_get_by_id(spell_id) - if not spell: - return False - # TODO: Find better way without using hardcoded name? - return 'Specialization' in spell.Name_enUS - @staticmethod def spell_get_all(): dbc_db_session = SessionHolder() diff --git a/etc/databases/world/updates/updates.sql b/etc/databases/world/updates/updates.sql index b32fb40e2..61697ed9d 100644 --- a/etc/databases/world/updates/updates.sql +++ b/etc/databases/world/updates/updates.sql @@ -106,5 +106,89 @@ begin not atomic insert into applied_updates values ('040920241'); end if; + + -- 04/09/2024 2 + if (select count(*) from applied_updates where id='040920242') = 0 then + -- Grave Robber + UPDATE `creature_template` SET `display_id1` = '1287', `display_id2` = '1289' WHERE (`entry` = '218'); + -- Report to Orgnil (805) should be turned in to Orgnil (3188) + UPDATE `creature_quest_finisher` SET `entry` = '3142' WHERE (`entry` = '3188') and (`quest` = '805'); + -- XP 440 + UPDATE `quest_template` SET `RewXP` = '440' WHERE (`entry` = '805'); + -- Orgnil should start quest 823. (Master Gadrin) + UPDATE `creature_quest_starter` SET `entry` = '3142' WHERE (`entry` = '3188') and (`quest` = '823'); + -- Should be turned in to Master Gadrin (3188) + UPDATE `creature_quest_finisher` SET `entry` = '3188' WHERE (`entry` = '3142') and (`quest` = '823'); + -- Chain quests, and modify xp to 210. + UPDATE `quest_template` SET `NextQuestId` = '823' WHERE (`entry` = '805'); + UPDATE `quest_template` SET `PrevQuestId` = '805', `RewXP` = '210' WHERE (`entry` = '823'); + UPDATE `quest_template` SET `PrevQuestId` = '792', `NextQuestId` = '805' WHERE (`entry` = '794'); + UPDATE `quest_template` SET `NextQuestInChain` = '823' WHERE (`entry` = '805'); + + -- 421 Prove Your Worth, 422 Arugal's Folly, 423 Arugal's Folly, 424 Arugal's Folly, 99 Arugal's Folly, 1014 Arugal Must Die + UPDATE `quest_template` SET `NextQuestInChain` = '1014' WHERE (`entry` = '99'); + UPDATE `quest_template` SET `PrevQuestId` = '99' WHERE (`entry` = '1014'); + + -- Chest placement. + UPDATE `spawns_gameobjects` SET `spawn_positionX` = '-5603.735', `spawn_positionY` = '646.926', `spawn_positionZ` = '393.223' WHERE (`spawn_id` = '9940'); + + -- Gennia Runetotem - Placement. + -- https://archive.thealphaproject.eu/media/Alpha-Project-Archive/Images/Azeroth/Kalimdor/Mulgore/561843-534914_20040426_057.jpg + UPDATE `spawns_creatures` SET `position_x` = '-2291.692', `position_y` = '-457.035', `position_z` = '-5.927', `orientation` = '0.087' WHERE (`spawn_id` = '26903'); + -- Narm Skychaser - Placement. + UPDATE `spawns_creatures` SET `position_x` = '-2278.957', `position_y` = '-448.956', `position_z` = '-5.027', `orientation` = '3.88' WHERE (`spawn_id` = '26906'); + -- Harken Windtotem - Placement. + UPDATE `spawns_creatures` SET `position_x` = '-2278.524', `position_y` = '-464.750', `position_z` = '-5.92', `orientation` = '2.035' WHERE (`spawn_id` = '24798'); + -- Burning Embers - Ignore + UPDATE `spawns_gameobjects` SET `ignored` = '1' WHERE (`spawn_id` = '20439'); + UPDATE `spawns_gameobjects` SET `ignored` = '1' WHERE (`spawn_id` = '20468'); + UPDATE `spawns_gameobjects` SET `ignored` = '1' WHERE (`spawn_id` = '20445'); + -- Fix waypoints for Brave Wildrunner + UPDATE `creature_movement_template` SET `position_z` = '-9.811' WHERE (`entry` = '3222') and (`point` = '4'); + UPDATE `creature_movement_template` SET `position_x` = '-2229.903', `position_y` = '-459.384', `position_z` = '-8.333' WHERE (`entry` = '3222') and (`point` = '34'); + UPDATE `creature_movement_template` SET `position_x` = '-2244.761', `position_y` = '-485.080', `position_z` = '-6.249' WHERE (`entry` = '3222') and (`point` = '35'); + UPDATE `creature_movement_template` SET `position_x` = '-2272.884', `position_y` = '-497.443', `position_z` = '-8.947' WHERE (`entry` = '3222') and (`point` = '36'); + UPDATE `creature_movement_template` SET `position_x` = '-2318.375', `position_y` = '-509.266', `position_z` = '-9.314' WHERE (`entry` = '3222') and (`point` = '37'); + UPDATE `creature_movement_template` SET `position_x` = '-2294.070', `position_y` = '-434.362', `position_z` = '-5.496' WHERE (`entry` = '3222') and (`point` = '40'); + UPDATE `creature_movement_template` SET `position_x` = '-2306.613', `position_y` = '-417.435', `position_z` = '-8.294' WHERE (`entry` = '3222') and (`point` = '41'); + UPDATE `creature_movement_template` SET `position_x` = '-2306.613', `position_y` = '-417.435', `position_z` = '-8.294' WHERE (`entry` = '3222') and (`point` = '42'); + UPDATE `creature_movement_template` SET `position_x` = '-2306.613', `position_y` = '-417.435', `position_z` = '-8.294' WHERE (`entry` = '3222') and (`point` = '43'); + + -- Ignore quest A Lesson to Learn (Aquatic Form chain) + UPDATE `quest_template` SET `ignored` = '1' WHERE (`entry` = '27'); + -- Ignore quest Finding the Source, invalid items, invalid spells. + UPDATE `quest_template` SET `ignored` = '1' WHERE (`entry` = '974'); + -- Enable 'The Great Lift' elevators. + UPDATE `spawns_gameobjects` SET `ignored` = '0' WHERE (`spawn_id` = '16876'); + UPDATE `spawns_gameobjects` SET `ignored` = '0' WHERE (`spawn_id` = '16874'); + -- Food Crate Z. + UPDATE `spawns_gameobjects` SET `spawn_positionZ` = '-6.55' WHERE (`spawn_id` = '43044'); + -- Fix chest state. + UPDATE `spawns_gameobjects` SET `spawn_state` = '1' WHERE (`spawn_id` = '60099'); + + -- Missing gold for chests. + UPDATE `gameobject_template` SET `mingold` = 267, `maxgold` = 267 WHERE `entry` IN (3714, 4095); + UPDATE `gameobject_template` SET `mingold` = 242, `maxgold` = 242 WHERE `entry` IN (3715, 105579); + UPDATE `gameobject_template` SET `mingold` = 30, `maxgold` = 75 WHERE `entry` IN (106318); + UPDATE `gameobject_template` SET `mingold` = 1879, `maxgold` = 1879 WHERE `entry` IN (131978, 153468); + UPDATE `gameobject_template` SET `mingold` = 401, `maxgold` = 791 WHERE `entry` IN (153451); + UPDATE `gameobject_template` SET `mingold` = 1628, `maxgold` = 2366 WHERE `entry` IN (153462, 153463); + + -- Defias Traitor - Spawn time 7-9 minutes. (From 30 seconds) + -- Remove run flag, double the event time limit to 20 minutes. + -- https://crawler.thealphaproject.eu/mnt/crawler/media/Patch-Note/unofficial_beta_patchnotes.txt + UPDATE `spawns_creatures` SET `spawntimesecsmin` = '420', `spawntimesecsmax` = '540' WHERE (`spawn_id` = '90214'); + DELETE FROM `quest_start_scripts` WHERE `id`=155; + INSERT INTO `quest_start_scripts` (`id`, `delay`, `priority`, `command`, `datalong`, `datalong2`, `datalong3`, `datalong4`, `target_param1`, `target_param2`, `target_type`, `data_flags`, `dataint`, `dataint2`, `dataint3`, `dataint4`, `x`, `y`, `z`, `o`, `condition_id`, `comments`) VALUES + (155, 0, 0, 61, 155, 1200, 0, 0, 0, 0, 0, 8, 0, 15502, 1019, 15501, 0, 0, 0, 0, 0, 'The Defias Brotherhood: Start Scripted Map Event'), + (155, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 'The Defias Brotherhood: The Defias Traitor - Say Text'), + (155, 0, 2, 4, 147, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'The Defias Brotherhood: The Defias Traitor - Remove Questgiver Flag'), + (155, 3, 4, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'The Defias Brotherhood: The Defias Traitor - Start Waypoints'); + + -- Chest placement. + UPDATE `spawns_gameobjects` SET `spawn_positionX` = '10720.001', `spawn_positionY` = '758.654', `spawn_positionZ` = '1322.234' WHERE (`spawn_id` = '49528'); + + insert into applied_updates values ('040920242'); + end if; end $ delimiter ; \ No newline at end of file diff --git a/game/world/managers/objects/gameobjects/GameObjectLootManager.py b/game/world/managers/objects/gameobjects/GameObjectLootManager.py index efb4c54a0..8cab5ff60 100644 --- a/game/world/managers/objects/gameobjects/GameObjectLootManager.py +++ b/game/world/managers/objects/gameobjects/GameObjectLootManager.py @@ -11,8 +11,7 @@ def __init__(self, object_mgr): # override def generate_money(self, requester): - money = randint(self.world_object.gobject_template.mingold, self.world_object.gobject_template.maxgold) - self.current_money = money + self.current_money = randint(self.world_object.gobject_template.mingold, self.world_object.gobject_template.maxgold) # override def generate_loot(self, requester): diff --git a/game/world/managers/objects/gameobjects/managers/ChestMananger.py b/game/world/managers/objects/gameobjects/managers/ChestMananger.py index 32a427982..6681f6970 100644 --- a/game/world/managers/objects/gameobjects/managers/ChestMananger.py +++ b/game/world/managers/objects/gameobjects/managers/ChestMananger.py @@ -21,8 +21,7 @@ def use_chest(self, player): self.chest_object.set_state(GameObjectStates.GO_STATE_ACTIVE) # Player kneel loot. - player.unit_flags |= UnitFlags.UNIT_FLAG_LOOTING - player.set_uint32(UnitFields.UNIT_FIELD_FLAGS, player.unit_flags) + player.set_unit_flag(UnitFlags.UNIT_FLAG_LOOTING, active=True) # Generate loot if it's empty. if not self.chest_object.loot_manager.has_loot(): diff --git a/game/world/managers/objects/loot/LootManager.py b/game/world/managers/objects/loot/LootManager.py index fae0013d4..c77a4cd2f 100644 --- a/game/world/managers/objects/loot/LootManager.py +++ b/game/world/managers/objects/loot/LootManager.py @@ -41,6 +41,9 @@ def generate_loot_groups(self, loot_template): return loot_groups + # There is evidence of chests offering the same item twice or two different items from the same group. + # The provided evidence is from later builds of the game (Not 0.5.3). + # https://github.com/The-Alpha-Project/alpha-core/issues/699 # Returns the final list of items available for looting. def process_loot_groups(self, loot_groups, requester) -> list: loot_item_result = [] @@ -93,6 +96,7 @@ def process_loot_group(self, group_id, group_loot_items: list, requester): item_chance = abs(loot_item.ChanceOrQuestChance) chance = item_chance if item_chance > 0 else split_group_chance + if current_roll < item_chance: if loot_item.mincountOrRef < 0: reference_loot_template = WorldDatabaseManager.ReferenceLootTemplateHolder\ diff --git a/game/world/managers/objects/spell/ExtendedSpellData.py b/game/world/managers/objects/spell/ExtendedSpellData.py index cc353f666..1ae63b7b1 100644 --- a/game/world/managers/objects/spell/ExtendedSpellData.py +++ b/game/world/managers/objects/spell/ExtendedSpellData.py @@ -207,6 +207,27 @@ def get_position_in_front(caster_location): return caster_location.get_point_in_radius_and_angle(2, 0) +class SpecializationTalents: + _SPECIALIZATION_SPELLS = { + 2917, 4334, 4335, 4336, 4337, 4338, 4339, 4340, 4341, 4342, 4343, 4344, 4345, 4346, 4347, + 4348, 4349, 4350, 4351, 4352, 4353, 4354, 4355, 4356, 4357, 4358, 4359, 4360, 4361, 4362, + 4363, 4364, 4365, 4366, 4367, 4368, 4369, 4370, 4371, 4372, 4373, 4374, 4375, 4376, 4377, + 4378, 4379, 4380, 4381, 4382, 4383, 4384, 4385, 4386, 4387, 4388, 4389, 4390, 4391, 4392, + 4393, 4394, 4395, 4396, 4397, 4440, 4441, 4442, 4443, 4444, 4445, 4446, 4447, 4448, 4449, + 4450, 4451, 4452, 4453, 4454, 4455, 4456, 4457, 4458, 4459, 4460, 4461, 4462, 4463, 4464, + 4465, 4466, 4467, 4468, 4469, 4470, 4471, 4472, 4473, 4474, 4475, 4476, 4477, 4478, 4479, + 4480, 4481, 4482, 4483, 4484, 4485, 4486, 4487, 4488, 4489, 4490, 4491, 4492, 4493, 4494, + 4495, 4496, 4497, 4498, 4499, 4500, 4501, 4502, 4503, 4817, 4818, 4819, 4820, 4821, 4822, + 4823, 4824, 4825, 4826, 4827, 4828, 4829, 4830, 4831, 4832, 4833, 4834, 4835, 4836, 4837, + 4838, 4839, 4840, 4841, 4842, 4843, 4844, 4845, 4846, 4847, 4848, 4849, 4850, 4851, 4852, + 4853, 4854, 4855, 4856, 4857, 4858 + } + + @staticmethod + def is_specialization_spell(spell_id): + return spell_id in SpecializationTalents._SPECIALIZATION_SPELLS + + class ProfessionInfo: _PROFESSION_SPELLS = { 129: (3273, 3274), # First Aid diff --git a/game/world/managers/objects/spell/SpellManager.py b/game/world/managers/objects/spell/SpellManager.py index 09a89e67d..ab7c7e456 100644 --- a/game/world/managers/objects/spell/SpellManager.py +++ b/game/world/managers/objects/spell/SpellManager.py @@ -45,6 +45,9 @@ def load_spells(self): for spell in RealmDatabaseManager.character_get_spells(self.caster.guid): self.spells[spell.spell] = spell + def has_spell(self, spell_id): + return spell_id in self.spells + def can_learn_spell(self, spell_id): if self.caster.get_type_id() != ObjectTypeIds.ID_PLAYER: return False diff --git a/game/world/managers/objects/spell/aura/AuraEffectHandler.py b/game/world/managers/objects/spell/aura/AuraEffectHandler.py index 7c999b827..1c98da827 100644 --- a/game/world/managers/objects/spell/aura/AuraEffectHandler.py +++ b/game/world/managers/objects/spell/aura/AuraEffectHandler.py @@ -278,7 +278,7 @@ def handle_mod_stalked(aura, effect_target, remove): @staticmethod def handle_mod_confuse(aura, effect_target, remove): - effect_target.set_unit_state(UnitStates.CONFUSED, not remove, aura.index) + effect_target.set_unit_flag(UnitFlags.UNIT_FLAG_CONFUSED, not remove, aura.index) if not remove: if effect_target.get_type_id() == ObjectTypeIds.ID_PLAYER: effect_target.interrupt_looting() diff --git a/game/world/managers/objects/spell/aura/AuraManager.py b/game/world/managers/objects/spell/aura/AuraManager.py index d4aa932e6..558f2f620 100644 --- a/game/world/managers/objects/spell/aura/AuraManager.py +++ b/game/world/managers/objects/spell/aura/AuraManager.py @@ -463,7 +463,7 @@ def _write_aura_flag_to_unit(self, aura, clear=False, is_refresh=False): self.current_flags &= ~(0x9 << byte) field_index = UnitFields.UNIT_FIELD_AURAFLAGS + (aura.index >> 3) - self.unit_mgr.set_uint32(field_index, self.current_flags, force=is_refresh) + self.unit_mgr.set_uint32(field_index, self.current_flags, force=is_refresh or clear) def get_next_aura_index(self, aura) -> int: if aura.passive: diff --git a/game/world/managers/objects/units/UnitManager.py b/game/world/managers/objects/units/UnitManager.py index 1a4bc3611..6837697e7 100644 --- a/game/world/managers/objects/units/UnitManager.py +++ b/game/world/managers/objects/units/UnitManager.py @@ -500,10 +500,14 @@ def calculate_melee_damage(self, victim, attack_type): damage_info.base_damage = self.calculate_base_attack_damage(attack_type, SpellSchools.SPELL_SCHOOL_NORMAL, victim) damage_info.target_state = VictimStates.VS_WOUND # Default state on successful attack. - # Apply crit damage modifier first if necessary. + # Apply crit damage modifier if necessary. if damage_info.hit_info & HitInfo.CRITICAL_HIT: damage_info.base_damage *= 2 + # Apply crushing blow damage modifier if necessary. + if damage_info.hit_info & HitInfo.CRUSHING: + damage_info.base_damage += int(damage_info.base_damage / 2) + # Handle school absorb. damage_info.absorb = victim.get_school_absorb_for_damage(damage_info) if damage_info.absorb: @@ -550,6 +554,9 @@ def calculate_melee_damage(self, victim, attack_type): else: # Successful attack. if damage_info.hit_info & HitInfo.CRITICAL_HIT: damage_info.proc_ex = ProcFlagsExLegacy.CRITICAL_HIT + + damage_info.proc_ex |= ProcFlagsExLegacy.NORMAL_HIT + damage_info.target_state = VictimStates.VS_WOUND # Normal hit. damage_info.total_damage = damage_info.base_damage # Invincibility. @@ -580,6 +587,7 @@ def calculate_melee_damage(self, victim, attack_type): # If the victim is going to die due this attack. if victim.health - damage_info.total_damage <= 0: damage_info.hit_info |= HitInfo.UNIT_DEAD + damage_info.proc_attacker |= ProcFlags.KILL return damage_info @@ -1006,6 +1014,12 @@ def can_dodge(self, attacker_location=None, in_combat=False): return self.has_dodge_passive and not self.spell_manager.is_casting() and \ not self.unit_state & UnitStates.STUNNED + def can_crush(self): + return False + + def should_always_crush(self): + return False + def enter_combat(self, source=None): if self.in_combat: return False @@ -1016,8 +1030,7 @@ def enter_combat(self, source=None): pet.creature.enter_combat() self.in_combat = True - self.unit_flags |= UnitFlags.UNIT_FLAG_IN_COMBAT - self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags) + self.set_unit_flag(UnitFlags.UNIT_FLAG_IN_COMBAT, active=True) # Handle enter combat interrupts. self.aura_manager.check_aura_interrupts(enter_combat=True) return True @@ -1046,8 +1059,7 @@ def leave_combat(self): pet.creature.spell_manager.remove_casts() pet.creature.leave_combat() - self.unit_flags &= ~UnitFlags.UNIT_FLAG_IN_COMBAT - self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags) + self.set_unit_flag(UnitFlags.UNIT_FLAG_IN_COMBAT, active=False) return True def is_attack_ready(self, attack_type): @@ -1073,10 +1085,10 @@ def set_stand_state(self, stand_state): def set_sanctuary(self, active=True, time_secs=0): if active: - self.unit_state |= UnitStates.SANCTUARY + self.set_unit_state(UnitStates.SANCTUARY, active=True) self.sanctuary_timer = time_secs else: - self.unit_state &= ~UnitStates.SANCTUARY + self.set_unit_state(UnitStates.SANCTUARY, active=False) self.sanctuary_timer = 0 def update_sanctuary(self, elapsed): @@ -1216,6 +1228,30 @@ def set_stunned(self, active=True, index=-1) -> bool: return is_stunned + def remove_all_unit_flags(self, clear_effects=True): + if clear_effects: + self._flag_effects.clear() + + for unit_flag in UnitFlags: + if self.unit_flags & unit_flag: + self.set_unit_flag(unit_flag, active=False) + + def remove_all_dynamic_flags(self, clear_effects=True): + if clear_effects: + self._flag_effects.clear() + + for dyn_flag in UnitDynamicTypes: + if self.dynamic_flags & dyn_flag: + self.set_dynamic_type_flag(dyn_flag, active=False) + + def remove_all_movement_flags(self, clear_effects=True): + if clear_effects: + self._flag_effects.clear() + + for move_flag in MoveFlags: + if self.movement_flags & move_flag: + self.set_move_flag(move_flag, active=False) + def set_unit_state(self, unit_state, active=True, index=-1) -> bool: is_active = self._set_effect_flag_state(UnitStates, unit_state, active, index) if is_active: @@ -1225,6 +1261,10 @@ def set_unit_state(self, unit_state, active=True, index=-1) -> bool: return is_active + def set_unit_flags(self, unit_flags, active=True, index=-1): + for unit_flag in unit_flags: + self.set_unit_flag(unit_flag, active=active, index=index) + def set_unit_flag(self, unit_flag, active=True, index=-1) -> bool: is_active = self._set_effect_flag_state(UnitFlags, unit_flag, active, index) if is_active: @@ -1233,6 +1273,15 @@ def set_unit_flag(self, unit_flag, active=True, index=-1) -> bool: self.unit_flags &= ~unit_flag self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags) + + # Modify UnitStates accordingly. + if unit_flag == UnitFlags.UNIT_FLAG_FLEEING: + self.set_unit_state(UnitStates.FLEEING, active=active, index=index) + elif unit_flag == UnitFlags.UNIT_FLAG_POSSESSED: + self.set_unit_state(UnitStates.POSSESSED, active=active, index=index) + elif unit_flag == UnitFlags.UNIT_FLAG_CONFUSED: + self.set_unit_state(UnitStates.CONFUSED, active=active, index=index) + return is_active def set_move_flag(self, move_flag, active=True, index=-1) -> bool: @@ -1246,9 +1295,9 @@ def set_move_flag(self, move_flag, active=True, index=-1) -> bool: else: self.movement_flags &= ~move_flag - # Only broadcast swimming, rooted or immobilized. + # Only broadcast swimming, rooted, walking or immobilized. if flag_changed and move_flag in {MoveFlags.MOVEFLAG_SWIMMING, MoveFlags.MOVEFLAG_ROOTED, - MoveFlags.MOVEFLAG_IMMOBILIZED}: + MoveFlags.MOVEFLAG_IMMOBILIZED, MoveFlags.MOVEFLAG_WALK}: self.get_map().send_surrounding(self.get_heartbeat_packet(), self) return is_active @@ -1305,17 +1354,15 @@ def mount(self, mount_display_id): if mount_display_id > 0 and \ DbcDatabaseManager.CreatureDisplayInfoHolder.creature_display_info_get_by_id(mount_display_id): self.mount_display_id = mount_display_id - self.unit_flags |= UnitFlags.UNIT_MASK_MOUNTED + self.set_unit_flag(UnitFlags.UNIT_MASK_MOUNTED, active=True) self.set_uint32(UnitFields.UNIT_FIELD_MOUNTDISPLAYID, self.mount_display_id) - self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags) return True return False def unmount(self): self.mount_display_id = 0 - self.unit_flags &= ~UnitFlags.UNIT_MASK_MOUNTED + self.set_unit_flag(UnitFlags.UNIT_MASK_MOUNTED, active=False) self.set_uint32(UnitFields.UNIT_FIELD_MOUNTDISPLAYID, self.mount_display_id) - self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags) return True def is_moving(self): @@ -1350,11 +1397,7 @@ def set_summoned_by(self, summoner, spell_id=0, subtype=CustomCodes.CreatureSubt self.set_uint32(UnitFields.UNIT_FIELD_FACTIONTEMPLATE, self.faction) def set_can_abandon(self, state: bool): - if state: - self.unit_flags |= UnitFlags.UNIT_FLAG_PET_CAN_ABANDON - else: - self.unit_flags &= ~UnitFlags.UNIT_FLAG_PET_CAN_ABANDON - self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags) + self.set_unit_flag(UnitFlags.UNIT_FLAG_PET_CAN_ABANDON, active=state) # Emote state is set given the EmoteID over dbc. def set_emote_unit_state(self, emote_state, is_temporary=False): @@ -1369,18 +1412,10 @@ def set_emote_unit_state(self, emote_state, is_temporary=False): self.set_uint32(UnitFields.UNIT_EMOTE_STATE, emote_state) def set_can_rename(self, state: bool): - if state: - self.unit_flags |= UnitFlags.UNIT_FLAG_PET_CAN_RENAME - else: - self.unit_flags &= ~UnitFlags.UNIT_FLAG_PET_CAN_RENAME - self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags) + self.set_unit_flag(UnitFlags.UNIT_FLAG_PET_CAN_RENAME, active=state) def set_player_controlled(self, state): - if state: - self.unit_flags |= UnitFlags.UNIT_FLAG_PLAYER_CONTROLLED - else: - self.unit_flags &= ~UnitFlags.UNIT_FLAG_PLAYER_CONTROLLED - self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags) + self.set_unit_flag(UnitFlags.UNIT_FLAG_PLAYER_CONTROLLED, active=state) def get_power_value(self, power_type=-1): if power_type == -1: @@ -1707,15 +1742,11 @@ def remove_combo_points(self): pass def set_taxi_flying_state(self, is_flying, mount_display_id=0): + self.set_unit_flags([UnitFlags.UNIT_FLAG_FROZEN, UnitFlags.UNIT_FLAG_TAXI_FLIGHT], active=is_flying) if is_flying: self.mount(mount_display_id) - self.unit_flags |= (UnitFlags.UNIT_FLAG_FROZEN | UnitFlags.UNIT_FLAG_TAXI_FLIGHT) - else: - if self.unit_flags & UnitFlags.UNIT_MASK_MOUNTED: - self.unmount() - self.unit_flags &= ~(UnitFlags.UNIT_FLAG_FROZEN | UnitFlags.UNIT_FLAG_TAXI_FLIGHT) - - self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags) + elif self.unit_flags & UnitFlags.UNIT_MASK_MOUNTED: + self.unmount() # override def set_display_id(self, display_id): @@ -1762,7 +1793,7 @@ def die(self, killer=None): # Flush movement manager. self.movement_manager.flush() - self.movement_flags = MoveFlags.MOVEFLAG_NONE + self.remove_all_movement_flags() # Reset threat manager. self.threat_manager.reset() @@ -1774,11 +1805,8 @@ def die(self, killer=None): self.set_health(0) - self.unit_flags |= UnitFlags.UNIT_MASK_DEAD - self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags) - - self.dynamic_flags |= UnitDynamicTypes.UNIT_DYNAMIC_DEAD - self.set_uint32(UnitFields.UNIT_DYNAMIC_FLAGS, self.dynamic_flags) + self.set_unit_flag(UnitFlags.UNIT_MASK_DEAD, active=True) + self.set_dynamic_type_flag(UnitDynamicTypes.UNIT_DYNAMIC_DEAD, active=True) if killer and killer.get_type_id() == ObjectTypeIds.ID_PLAYER: if killer.current_selection == self.guid: @@ -1837,12 +1865,8 @@ def respawn(self): self.set_current_target(0) self.is_alive = True - self.unit_flags &= ~UnitFlags.UNIT_MASK_DEAD - self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags) - - self.dynamic_flags = UnitDynamicTypes.UNIT_DYNAMIC_NONE - self.set_uint32(UnitFields.UNIT_DYNAMIC_FLAGS, self.dynamic_flags) - + self.set_unit_flag(UnitFlags.UNIT_MASK_DEAD, active=False) + self.remove_all_dynamic_flags() self.set_stand_state(StandState.UNIT_STANDING) def is_in_world(self): diff --git a/game/world/managers/objects/units/creature/CreatureBuilder.py b/game/world/managers/objects/units/creature/CreatureBuilder.py index 1be2d7e93..ce3c0c8ba 100644 --- a/game/world/managers/objects/units/creature/CreatureBuilder.py +++ b/game/world/managers/objects/units/creature/CreatureBuilder.py @@ -64,8 +64,7 @@ def create(entry, location, map_id, instance_id, health_percent=100, mana_percen if possessed: creature_instance.set_charmed_by(summoner, subtype, movement_type) - creature_instance.unit_flags |= UnitFlags.UNIT_FLAG_POSSESSED - creature_instance.unit_state |= UnitStates.POSSESSED + creature_instance.set_unit_flag(UnitFlags.UNIT_FLAG_POSSESSED, active=True) summoner.possessed_unit = creature_instance return creature_instance diff --git a/game/world/managers/objects/units/creature/CreatureManager.py b/game/world/managers/objects/units/creature/CreatureManager.py index d9b70af32..31b032149 100644 --- a/game/world/managers/objects/units/creature/CreatureManager.py +++ b/game/world/managers/objects/units/creature/CreatureManager.py @@ -106,13 +106,13 @@ def initialize_from_creature_template(self, creature_template, subtype=CustomCod # Elite mob. if 0 < self.creature_template.rank < 4: - self.unit_flags |= UnitFlags.UNIT_FLAG_PLUS_MOB + self.set_unit_flag(UnitFlags.UNIT_FLAG_PLUS_MOB, active=True) # NPC can't be attacked by other NPCs and can't attack other NPCs. if self.creature_template.static_flags & CreatureStaticFlags.IMMUNE_NPC: - self.unit_flags |= UnitFlags.UNIT_FLAG_PASSIVE + self.set_unit_flag(UnitFlags.UNIT_FLAG_PASSIVE, active=True) # NPC is immune to player characters. if self.creature_template.static_flags & CreatureStaticFlags.IMMUNE_PLAYER: - self.unit_flags |= UnitFlags.UNIT_FLAG_NOT_ATTACKABLE_OCC + self.set_unit_flag(UnitFlags.UNIT_FLAG_NOT_ATTACKABLE_OCC, active=True) if self.is_totem() or self.is_critter() or not self.can_have_target(): self.react_state = CreatureReactStates.REACT_PASSIVE @@ -427,6 +427,14 @@ def can_parry(self, attacker_location=None, in_combat=False): return super().can_parry(attacker_location, in_combat=in_combat) + # override + def can_crush(self): + return not self.creature_template.flags_extra & CreatureFlagsExtra.CREATURE_FLAG_EXTRA_NO_CRUSH + + # override + def should_always_crush(self): + return self.creature_template.flags_extra & CreatureFlagsExtra.CREATURE_FLAG_EXTRA_ALWAYS_CRUSH + # override def enter_combat(self, source=None): if not super().enter_combat(source): @@ -724,7 +732,7 @@ def die(self, killer=None): if self.loot_manager.has_loot(): self.set_lootable(True) - self.unit_flags = UnitFlags.UNIT_FLAG_STANDARD + self.remove_all_unit_flags() return super().die(killer) @@ -751,11 +759,7 @@ def set_max_mana(self, mana): self.set_uint32(UnitFields.UNIT_FIELD_MAXPOWER1, mana) 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) + self.set_dynamic_type_flag(UnitDynamicTypes.UNIT_DYNAMIC_LOOTABLE, active=flag) def near_teleport(self, location): map_ = self.get_map() diff --git a/game/world/managers/objects/units/movement/behaviors/ConfusedMovement.py b/game/world/managers/objects/units/movement/behaviors/ConfusedMovement.py index d72156e45..5b3d588de 100644 --- a/game/world/managers/objects/units/movement/behaviors/ConfusedMovement.py +++ b/game/world/managers/objects/units/movement/behaviors/ConfusedMovement.py @@ -23,7 +23,7 @@ def __init__(self, spline_callback, is_default=False, duration_seconds=0.0): # override def initialize(self, unit): - unit.set_unit_state(UnitStates.CONFUSED, True) + unit.set_unit_flag(UnitFlags.UNIT_FLAG_CONFUSED, True) unit.movement_manager.stop() unit.combat_target = None self.home_position = unit.location @@ -72,9 +72,8 @@ def can_remove(self): # override def on_removed(self): - self.unit.movement_flags = MoveFlags.MOVEFLAG_NONE - self.unit.get_map().send_surrounding(self.unit.get_heartbeat_packet(), self.unit, include_self=False) - self.unit.set_unit_state(UnitStates.CONFUSED, False) + self.unit.remove_all_movement_flags() + self.unit.set_unit_flag(UnitFlags.UNIT_FLAG_CONFUSED, False) def _get_confused_move_point(self): start_point = self.home_position diff --git a/game/world/managers/objects/units/movement/behaviors/DistractedMovement.py b/game/world/managers/objects/units/movement/behaviors/DistractedMovement.py index cf87979e6..495be6d34 100644 --- a/game/world/managers/objects/units/movement/behaviors/DistractedMovement.py +++ b/game/world/managers/objects/units/movement/behaviors/DistractedMovement.py @@ -16,13 +16,13 @@ def __init__(self, wait_time_secs, angle, spline_callback): # override def initialize(self, unit): super().initialize(unit) - self.unit.unit_state |= UnitStates.DISTRACTED + self.unit.set_unit_state(UnitStates.DISTRACTED, active=True) self.unit.movement_manager.face_angle(self.angle) return True # override def on_removed(self): - self.unit.unit_state &= ~UnitStates.DISTRACTED + self.unit.set_unit_state(UnitStates.DISTRACTED, active=False) if self.unit.get_type_id() == ObjectTypeIds.ID_UNIT and not self.unit.has_wander_type(): self.unit.movement_manager.face_angle(self.unit.spawn_position.o) diff --git a/game/world/managers/objects/units/movement/behaviors/FearMovement.py b/game/world/managers/objects/units/movement/behaviors/FearMovement.py index 46d1634ac..c6935314f 100644 --- a/game/world/managers/objects/units/movement/behaviors/FearMovement.py +++ b/game/world/managers/objects/units/movement/behaviors/FearMovement.py @@ -78,8 +78,7 @@ def can_remove(self): # override def on_removed(self): - self.unit.movement_flags = MoveFlags.MOVEFLAG_NONE - self.unit.get_map().send_surrounding(self.unit.get_heartbeat_packet(), self.unit, include_self=False) + self.unit.remove_all_movement_flags() # Remove fleeing flag if not caused by auras (ie. scripted flee). self.unit.set_unit_flag(UnitFlags.UNIT_FLAG_FLEEING, False) @@ -93,6 +92,10 @@ def _get_path(self, fear_point): return [fear_point] for search_range in range(0, int(SEARCH_RANDOM_RADIUS)): destination = fear_point.get_random_point_in_radius(search_range, self.unit.map_id) + # Avoid slopes above 2.5 (Units running off cliffs). + diff = math.fabs(destination.z - self.unit.location.z) + if diff > 2.5: + continue failed, in_place, path = self.unit.get_map().calculate_path(self.unit.location, destination) if not failed: return path diff --git a/game/world/managers/objects/units/player/InventoryManager.py b/game/world/managers/objects/units/player/InventoryManager.py index d983f111c..5e3dc64ee 100644 --- a/game/world/managers/objects/units/player/InventoryManager.py +++ b/game/world/managers/objects/units/player/InventoryManager.py @@ -85,6 +85,38 @@ def update_items_durations(self): if item.duration: item.send_item_duration() + def can_use_item(self, item_template): + if item_template.required_level: + if self.owner.level < item_template.required_level: + return InventoryError.BAG_LEVEL_MISMATCH + + # Get player race/class masks. + race_mask = 1 << self.owner.race - 1 + class_mask = 1 << self.owner.class_ - 1 + + item_race_mask = item_template.allowable_race if item_template.allowable_race > 0 else 0 + item_class_mask = item_template.allowable_class if item_template.allowable_class > 0 else 0 + # Race / Class. + if item_class_mask or race_mask: + if item_race_mask and not race_mask & item_race_mask: + return InventoryError.BAG_RACE_NOTALLOWED + if item_class_mask and not class_mask & item_class_mask: + return InventoryError.BAG_RACE_NOTALLOWED + + if item_template.required_skill: + skill_id = item_template.required_skill + required_value = item_template.required_skill_rank + if not self.owner.skill_manager.has_skill(skill_id): + return InventoryError.BAG_PROFICIENCY_NEEDED + if self.owner.skill_manager.get_total_skill_value(skill_id) < required_value: + return InventoryError.BAG_SKILL_MISMATCH + + if item_template.required_spell: + if not self.owner.spell_manager.has_spell(item_template.required_spell): + return InventoryError.BAG_PROFICIENCY_NEEDED + + return InventoryError.BAG_OK + def get_all_items(self, include_bank=False): items = [] for container_slot, container in list(self.containers.items()): diff --git a/game/world/managers/objects/units/player/PlayerManager.py b/game/world/managers/objects/units/player/PlayerManager.py index 777c3cf4b..3361b8416 100644 --- a/game/world/managers/objects/units/player/PlayerManager.py +++ b/game/world/managers/objects/units/player/PlayerManager.py @@ -156,7 +156,7 @@ def __init__(self, self.update_packet_factory.init_values(self.guid, PlayerFields) self.update_manager = UpdateManager(self) - self.unit_flags |= UnitFlags.UNIT_FLAG_PLAYER_CONTROLLED + self.set_unit_flag(UnitFlags.UNIT_FLAG_PLAYER_CONTROLLED, active=True) self.threat_manager = ThreatManager(self) self.enchantment_manager = EnchantmentManager(self) @@ -540,7 +540,7 @@ def spawn_player_from_teleport(self): item_queries, item_create_packets, item_partial_packets = self.get_inventory_update_packets(requester=self) self.enqueue_packets(item_queries + item_create_packets + item_partial_packets) # Reset move flags before create packet in order to avoid player starting automatically moving after tele. - self.movement_flags = MoveFlags.MOVEFLAG_NONE + self.remove_all_movement_flags() # Create packet. self.enqueue_packet(self.generate_create_packet(requester=self)) @@ -741,8 +741,7 @@ def interrupt_looting(self): self.send_loot_release(self.loot_selection) def send_loot_release(self, loot_selection): - self.unit_flags &= ~UnitFlags.UNIT_FLAG_LOOTING - self.set_uint32(UnitFields.UNIT_FIELD_FLAGS, self.unit_flags) + self.set_unit_flag(UnitFlags.UNIT_FLAG_LOOTING, active=False) loot_guid = self.loot_selection.object_guid high_guid: HighGuid = GuidUtils.extract_high_guid(loot_guid) @@ -1564,7 +1563,8 @@ def die(self, killer=None): self.mirror_timers_manager.stop_all() self.update_swimming_state(False) - self.unit_flags = UnitFlags.UNIT_FLAG_PLAYER_CONTROLLED + self.remove_all_unit_flags() + self.set_unit_flag(UnitFlags.UNIT_FLAG_PLAYER_CONTROLLED, active=True) return super().die(killer) diff --git a/game/world/managers/objects/units/player/StatManager.py b/game/world/managers/objects/units/player/StatManager.py index 6f4346641..a6f2d6321 100644 --- a/game/world/managers/objects/units/player/StatManager.py +++ b/game/world/managers/objects/units/player/StatManager.py @@ -880,6 +880,17 @@ def get_attack_result_against_self(self, attacker, attack_type, hit_info |= HitInfo.OFFHAND if random.random() < critical_chance: hit_info |= HitInfo.CRITICAL_HIT + return hit_info + + # Crushing blows. + # TODO: Find formula, use crit chance for now. + if attacker.can_crush(): + if attacker.should_always_crush(): + hit_info |= HitInfo.CRUSHING + return hit_info + elif attacker.level >= self.unit_mgr.level + 3: + if random.random() < critical_chance: + hit_info |= HitInfo.CRUSHING return hit_info diff --git a/game/world/managers/objects/units/player/TalentManager.py b/game/world/managers/objects/units/player/TalentManager.py index 4066f78b3..5df40fbb1 100644 --- a/game/world/managers/objects/units/player/TalentManager.py +++ b/game/world/managers/objects/units/player/TalentManager.py @@ -5,6 +5,7 @@ from database.dbc.DbcDatabaseManager import DbcDatabaseManager from database.world.WorldDatabaseManager import WorldDatabaseManager +from game.world.managers.objects.spell import ExtendedSpellData from game.world.managers.objects.units.creature.utils.TrainerUtils import TrainerUtils from network.packet.PacketWriter import PacketWriter from utils.constants.MiscCodes import TrainerServices, TrainerTypes @@ -18,9 +19,9 @@ def __init__(self, player_mgr): @staticmethod def get_talent_cost_by_id(spell_id: int) -> int: spell_rank: int = DbcDatabaseManager.SpellHolder.spell_get_rank_by_id(spell_id) - is_specialization = DbcDatabaseManager.SpellHolder.spell_is_specialization(spell_id) + is_specialization = ExtendedSpellData.SpecializationTalents.is_specialization_spell(spell_id) - # TODO Below statement might not be 100% correct, but works for now since we lack more data. + # TODO Below statements might not be 100% correct, but works for now since we lack more data. # https://github.com/The-Alpha-Project/alpha-core/issues/1362 if is_specialization: return 10 if spell_rank <= 2 else 15 if 2 < spell_rank <= 4 else 20 if 5 < spell_rank <= 6 \ diff --git a/game/world/managers/objects/units/player/quest/QuestHelpers.py b/game/world/managers/objects/units/player/quest/QuestHelpers.py index 5538df5ab..b3c991447 100644 --- a/game/world/managers/objects/units/player/quest/QuestHelpers.py +++ b/game/world/managers/objects/units/player/quest/QuestHelpers.py @@ -4,6 +4,24 @@ class QuestHelpers: + @staticmethod + def can_ever_take_quest(quest_template, player_mgr): + # First check if quest is disabled. + if quest_template.Method == QuestMethod.QUEST_DISABLED: + return False + + # Satisfies required race? + race_is_required = quest_template.RequiredRaces > 0 + if race_is_required and not (quest_template.RequiredRaces & player_mgr.race_mask): + return False + + # Satisfies required class? + class_is_required = quest_template.RequiredClasses > 0 + if class_is_required and not (quest_template.RequiredClasses & player_mgr.class_mask): + return False + + return True + @staticmethod def is_instant_complete_quest(quest_template): return quest_template.Method == QuestMethod.QUEST_AUTOCOMPLETE diff --git a/game/world/managers/objects/units/player/quest/QuestManager.py b/game/world/managers/objects/units/player/quest/QuestManager.py index 0b3169f33..b2d58fff7 100644 --- a/game/world/managers/objects/units/player/quest/QuestManager.py +++ b/game/world/managers/objects/units/player/quest/QuestManager.py @@ -325,8 +325,11 @@ def get_active_quest_num_from_quest_giver(self, quest_giver): # Updates the dialog_status display. def update_dialog_display_status(self, quest_template, dialog_status, checks_failed=False): + can_ever_take_quest = QuestHelpers.can_ever_take_quest(quest_template, self.player_mgr) + if not can_ever_take_quest: + return QuestGiverStatus.QUEST_GIVER_NONE # If general quest requirements failed, we just care about 'Future' display status. - if quest_template.MinLevel > self.player_mgr.level >= quest_template.MinLevel - 4: + elif quest_template.MinLevel > self.player_mgr.level >= quest_template.MinLevel - 4: dialog_status = QuestGiverStatus.QUEST_GIVER_FUTURE # Gray '!', you are close to having a quest. elif not checks_failed and quest_template.MinLevel <= self.player_mgr.level < quest_template.QuestLevel + 7: dialog_status = QuestGiverStatus.QUEST_GIVER_QUEST # Yellow '!', available quest. @@ -479,8 +482,8 @@ def update_surrounding_quest_status(self): for guid, world_object in list(known_objects.items()): if world_object.get_type_id() == ObjectTypeIds.ID_UNIT: unit = world_object - if WorldDatabaseManager.QuestRelationHolder.creature_quest_finisher_get_by_entry( - unit.entry) or WorldDatabaseManager.QuestRelationHolder.creature_quest_starter_get_by_entry(unit.entry): + if (WorldDatabaseManager.QuestRelationHolder.creature_quest_finisher_get_by_entry(unit.entry) + or WorldDatabaseManager.QuestRelationHolder.creature_quest_starter_get_by_entry(unit.entry)): quest_status = self.get_dialog_status(unit) self.send_quest_giver_status(guid, quest_status) # Make the owner refresh gameobject dynamic flags if needed. diff --git a/game/world/opcode_handling/handlers/inventory/ReadItemHandler.py b/game/world/opcode_handling/handlers/inventory/ReadItemHandler.py index c2196d8a5..8b33ce2b3 100644 --- a/game/world/opcode_handling/handlers/inventory/ReadItemHandler.py +++ b/game/world/opcode_handling/handlers/inventory/ReadItemHandler.py @@ -9,19 +9,21 @@ class ReadItemHandler(object): @staticmethod def handle(world_session, reader): - # Seems CMSG_READ_ITEM is only called if the item is in the backpack, weird. if len(reader.data) >= 2: # Avoid handling empty read item packet. bag, slot = unpack('<2B', reader.data[:2]) if bag == 0xFF: bag = InventorySlots.SLOT_INBACKPACK.value item = world_session.player_mgr.inventory.get_item(bag, slot) - # TODO: Better handling of this: check if player can use item, etc. + result = InventoryError.BAG_ITEM_NOT_FOUND if item: + result = world_session.player_mgr.inventory.can_use_item(item.item_template) + + if result == InventoryError.BAG_OK: data = pack('<2Q', item.guid, item.guid) world_session.enqueue_packet(PacketWriter.get_packet(OpCode.SMSG_READ_ITEM_OK, data)) else: - world_session.player_mgr.inventory.send_equip_error(InventoryError.BAG_ITEM_NOT_FOUND) + world_session.player_mgr.inventory.send_equip_error(result) data = pack('