Skip to content

Commit 195ecb8

Browse files
authored
Fixes (#1613)
* Closes #1612 Closes #1612 * Closes #1607 Closes #1607 * Update updates.sql * Update updates.sql * Closes #1614 Closes #1614 * Update updates.sql * Closes #1615 * Closes #1616 * Update updates.sql * Update updates.sql * Update updates.sql * Update updates.sql * Address inconsistencies between spell tooltips and the actual effect points calculation. - Spells which are linked to a skill should use skill level and not caster level. - Roll effect points only once. * Update QuestManager.py * SPELL_AURA_MOD_BLOCK_PERCENT Differentiate between shields and bucklers. * Update updates.sql * Update SpellEffect.py * Update Definitions.py * Fix creatures not stopping upon root. * Fix Yell not reaching anyone beyond VIEW_DISTANCE. * Update Cell.py * Hammerhead Sharks * Update updates.sql * Update updates.sql * Update updates.sql * Update updates.sql * Update updates.sql * Update SpellManager.py * Revert "Update SpellManager.py" This reverts commit 70dbf7c. * Update SpellManager.py * Update AuraManager.py * Update SpellManager.py * Update updates.sql * Update SpellManager.py
1 parent c152bc0 commit 195ecb8

File tree

14 files changed

+429
-109
lines changed

14 files changed

+429
-109
lines changed

etc/databases/world/updates/updates.sql

Lines changed: 274 additions & 0 deletions
Large diffs are not rendered by default.

game/world/managers/maps/Cell.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from game.world.managers.objects.farsight.FarSightManager import FarSightManager
33
from threading import RLock
44

5+
from utils.ConfigManager import config
6+
57

68
class Cell:
79
def __init__(self, cell_x=0, cell_y=0, map_id=0, instance_id=0, key=''):
@@ -180,26 +182,42 @@ def send_all(self, packet, source, include_source=False, exclude=None, use_ignor
180182
camera.broadcast_packet(packet, exclude=players_reached)
181183

182184
def send_all_in_range(self, packet, range_, source, include_source=True, exclude=None, use_ignore=False):
185+
# If range is non-positive, send to all players without filtering.
183186
if range_ <= 0:
184187
self.send_all(packet, source, exclude)
185-
else:
186-
players_reached = set()
187-
for guid, player_mgr in list(self.players.items()):
188-
if not player_mgr.online or not player_mgr.location.distance(source.location) <= range_:
189-
continue
190-
if not include_source and player_mgr.guid == source.guid:
191-
continue
192-
if use_ignore and player_mgr.friends_manager.has_ignore(source.guid):
193-
continue
194-
# Never send messages to a player that does not know the source object.
195-
if not player_mgr.guid == source.guid and source.guid not in player_mgr.known_objects:
188+
return
189+
190+
is_yell = int(range_) == int(config.World.Chat.ChatRange.yell_range)
191+
is_say = int(range_) == int(config.World.Chat.ChatRange.say_range)
192+
193+
players_reached = set()
194+
for guid, player_mgr in list(self.players.items()):
195+
# Skip offline players.
196+
if not player_mgr.online:
197+
continue
198+
# Check distance.
199+
distance = player_mgr.location.distance(source.location)
200+
if distance > range_:
201+
continue
202+
# Optionally exclude source.
203+
if not include_source and player_mgr.guid == source.guid:
204+
continue
205+
# Skip players that have ignored the source.
206+
if use_ignore and player_mgr.friends_manager.has_ignore(source.guid):
207+
continue
208+
# Ensure the player knows about the source object if this is not a chat message.
209+
if not is_say and not is_yell:
210+
if guid != source.guid and source.guid not in player_mgr.known_objects:
196211
continue
197-
players_reached.add(player_mgr.guid)
198-
player_mgr.enqueue_packet(packet)
199212

200-
# If this cell has cameras, route packets.
201-
for camera in FarSightManager.get_cell_cameras(self):
202-
camera.broadcast_packet(packet, exclude=players_reached)
213+
# Add to reached players.
214+
players_reached.add(guid)
215+
# Send packet.
216+
player_mgr.enqueue_packet(packet)
217+
218+
# Route packets via cameras if applicable.
219+
for camera in FarSightManager.get_cell_cameras(self):
220+
camera.broadcast_packet(packet, exclude=players_reached)
203221

204222
def can_deactivate(self):
205223
return not self.has_players() and not self.has_cameras()

game/world/managers/maps/GridManager.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -197,15 +197,16 @@ def _update_players_surroundings(self, cell_key, exclude_cells=None, world_objec
197197

198198
return affected_cells
199199

200-
def _get_surrounding_cells_by_cell(self, cell=None, cell_x=0, cell_y=0, map_id=0, instance_id=0):
200+
def _get_surrounding_cells_by_cell(self, cell=None, cell_x=0, cell_y=0, map_id=0, instance_id=0, range_=0):
201201
if cell:
202202
cell_x = cell.cell_x
203203
cell_y = cell.cell_y
204204
map_id = cell.map_id
205205
instance_id = cell.instance_id
206206

207+
view_distance = VIEW_DISTANCE if not range_ else range_
207208
# Calculate how many cells to include in each direction given the view distance, at least 1.
208-
max_cells_radius = max(1, int(VIEW_DISTANCE // CELL_SIZE))
209+
max_cells_radius = max(1, int(view_distance // CELL_SIZE))
209210
surrounding_cells = set()
210211

211212
for dx in range(-max_cells_radius, max_cells_radius + 1):
@@ -222,13 +223,15 @@ def _get_surrounding_cells_by_cell(self, cell=None, cell_x=0, cell_y=0, map_id=0
222223

223224
return surrounding_cells
224225

225-
def _get_surrounding_cells_by_object(self, world_object):
226+
def _get_surrounding_cells_by_object(self, world_object, range_=0):
226227
pos = world_object.location
227-
return self._get_surrounding_cells_by_location(pos.x, pos.y, world_object.map_id, world_object.instance_id)
228+
return self._get_surrounding_cells_by_location(
229+
pos.x, pos.y, world_object.map_id, world_object.instance_id, range_=range_)
228230

229-
def _get_surrounding_cells_by_location(self, x, y, map_, instance_id):
231+
def _get_surrounding_cells_by_location(self, x, y, map_, instance_id, range_=0):
230232
cell_x, cell_y = CellUtils.generate_coord_data(x, y)
231-
return self._get_surrounding_cells_by_cell(cell_x=cell_x, cell_y=cell_y, map_id=map_, instance_id=instance_id)
233+
return self._get_surrounding_cells_by_cell(cell_x=cell_x, cell_y=cell_y, map_id=map_,
234+
instance_id=instance_id, range_=range_)
232235

233236
def send_surrounding(self, packet, world_object, include_self=True, exclude=None, use_ignore=False):
234237
if world_object.current_cell:
@@ -244,7 +247,7 @@ def send_surrounding_in_range(self, packet, world_object, range_, include_self=T
244247
Logger.warning(f'{world_object.get_name()} Cannot send surrounding in range without current cell.')
245248
return
246249

247-
for cell in self._get_surrounding_cells_by_object(world_object):
250+
for cell in self._get_surrounding_cells_by_object(world_object, range_=range_):
248251
cell.send_all_in_range(packet, range_, world_object, include_self, exclude, use_ignore)
249252

250253
def get_surrounding_objects(self, world_object, object_types):

game/world/managers/objects/spell/CastingSpell.py

Lines changed: 5 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -483,66 +483,12 @@ def requires_combo_points(self):
483483
def requires_aura_state(self):
484484
return self.spell_entry.CasterAuraState != 0
485485

486-
'''
487-
TODO: Figure out this for proper spell min max damage calculation.
488-
void __fastcall Spell_C_GetMinMaxPoints(int effectIndex, int a2, int *min, int *max, unsigned int level, int isPet)
489-
{
490-
signed int SpellLevel; // edi
491-
int v10; // eax
492-
double v11; // st7
493-
char v13; // c0
494-
double v14; // st7
495-
int v15; // ecx
496-
int v16; // ecx
497-
int v17; // edi
498-
double v18; // [esp+0h] [ebp-18h]
499-
int dieSides; // [esp+14h] [ebp-4h]
500-
int maxBonus; // [esp+28h] [ebp+10h]
501-
float maxBonusa; // [esp+28h] [ebp+10h]
502-
int minBonus; // [esp+2Ch] [ebp+14h]
503-
504-
*min = 0;
505-
*max = 0;
506-
if ( effectIndex )
507-
{
508-
SpellLevel = level;
509-
dieSides = *(_DWORD *)(effectIndex + 4 * a2 + 224);
510-
if ( !level )
511-
SpellLevel = Spell_C_GetSpellLevel(*(_DWORD *)effectIndex, isPet);
512-
v10 = *(_DWORD *)(effectIndex + 88);
513-
maxBonus = SpellLevel;
514-
if ( v10 > 0 )
515-
{
516-
SpellLevel -= v10;
517-
maxBonus = SpellLevel;
518-
}
519-
if ( SpellLevel < 0 )
520-
{
521-
SpellLevel = 0;
522-
maxBonus = 0;
523-
}
524-
v11 = (double)maxBonus * *(float *)(effectIndex + 4 * a2 + 260);
525-
maxBonusa = v11;
526-
minBonus = (__int64)v11;
527-
_floor(maxBonusa);
528-
v18 = maxBonusa;
529-
if ( v13 )
530-
v14 = _floor(v18);
531-
else
532-
v14 = _ceil(v18);
533-
v15 = SpellLevel * *(_DWORD *)(effectIndex + 4 * a2 + 248) + *(_DWORD *)(effectIndex + 4 * a2 + 236);
534-
*min = v15;
535-
*min = *(_DWORD *)(effectIndex + 4 * a2 + 272) + minBonus + v15;
536-
v16 = dieSides * *(_DWORD *)(effectIndex + 4 * a2 + 236);
537-
*max = v16;
538-
v17 = v16 + *(_DWORD *)(effectIndex + 4 * a2 + 248) * dieSides * SpellLevel;
539-
*max = v17;
540-
*max = *(_DWORD *)(effectIndex + 4 * a2 + 272) + (__int64)v14 + v17;
541-
}
542-
}
543-
'''
544486
def calculate_effective_level(self):
545-
level = self.spell_caster.level
487+
skill = 0
488+
if self.spell_caster.is_player():
489+
skill = self.spell_caster.skill_manager.get_skill_value_for_spell_id(self.spell_entry.ID)
490+
491+
level = self.spell_caster.level if not skill else int(skill / 5)
546492
if level > self.spell_entry.MaxLevel > 0:
547493
level = self.spell_entry.MaxLevel
548494
elif level < self.spell_entry.BaseLevel:

game/world/managers/objects/spell/SpellEffect.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class SpellEffect:
3333

3434
caster_effective_level: int
3535
effect_index: int
36+
effect_points: int = 0
3637
targets: EffectTargets
3738
radius_entry: SpellRadius
3839

@@ -50,6 +51,7 @@ def __init__(self, casting_spell, index):
5051
self.load_effect(casting_spell.spell_entry, index)
5152

5253
self.caster_effective_level = casting_spell.caster_effective_level
54+
self.effect_points = self.get_effect_points()
5355
self.targets = EffectTargets(casting_spell, self)
5456
self.radius_entry = DbcDatabaseManager.spell_radius_get_by_id(self.radius_index) if self.radius_index else None
5557
self.casting_spell = casting_spell
@@ -158,7 +160,15 @@ def is_periodic(self):
158160
return self.aura_period != 0
159161

160162
def get_effect_points(self) -> int:
161-
rolled_points = random.randint(1, self.die_sides + self.dice_per_level) if self.die_sides != 0 else 0
163+
if self.effect_points:
164+
return self.effect_points
165+
166+
# Calculate min and max dice roll values.
167+
min_roll = 1 if self.die_sides != 0 else 0
168+
max_roll = self.die_sides + self.dice_per_level if self.die_sides != 0 else 0
169+
170+
# Roll.
171+
rolled_points = random.randint(min_roll, max_roll) if self.die_sides != 0 else 0
162172
return self.base_points + int(self.real_points_per_level * self.caster_effective_level) + rolled_points
163173

164174
def get_effect_simple_points(self) -> int:

game/world/managers/objects/spell/SpellManager.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import math
21
import time
32
from random import randint
43
from struct import pack
@@ -420,6 +419,13 @@ def perform_spell_cast(self, casting_spell: CastingSpell, validate=True):
420419
self.send_cast_result(casting_spell, SpellCheckCastResult.SPELL_NO_ERROR)
421420
self.send_spell_go(casting_spell)
422421

422+
# Spells that use the KneelLoop animation causes the client to get stuck in this animation until relog.
423+
# Send a KneelEnd animation to resolve this issue. e.g. spell 6717 'Place Lion Carcass'
424+
if casting_spell.spell_visual_entry and casting_spell.spell_visual_entry.CastKit == 380: # KneelLoop.
425+
data = pack(f'QI', self.caster.guid, 444)
426+
packet = PacketWriter.get_packet(OpCode.SMSG_PLAY_SPELL_VISUAL, data)
427+
self.caster.enqueue_packet(packet)
428+
423429
if casting_spell.requires_combo_points():
424430
# Combo points will be reset by consume_resources_for_cast.
425431
casting_spell.spent_combo_points = self.caster.combo_points
@@ -835,14 +841,15 @@ def send_cast_start(self, casting_spell):
835841
if not self.caster.is_unit(by_mask=True):
836842
return # Non-unit casters should not broadcast their casts.
837843

838-
is_player = self.caster.is_player()
844+
source_guid = self.caster.guid
845+
if casting_spell.source_item:
846+
source_guid = casting_spell.source_item.guid
839847

840-
source_guid = casting_spell.initial_target.guid if casting_spell.initial_target_is_item() else self.caster.guid
841848
cast_flags = casting_spell.cast_flags
842849

843850
# Validate if this spell crashes the client.
844851
# Force SpellCastFlags.CAST_FLAG_PROC, which hides the start cast.
845-
if not is_player and not ExtendedSpellData.UnitSpellsValidator.spell_has_valid_cast(casting_spell):
852+
if not self.caster.is_player() and not ExtendedSpellData.UnitSpellsValidator.spell_has_valid_cast(casting_spell):
846853
Logger.warning(f'Hiding spell {casting_spell.spell_entry.Name_enUS} start cast due invalid cast.')
847854
cast_flags |= SpellCastFlags.CAST_FLAG_PROC
848855

@@ -866,7 +873,7 @@ def send_cast_start(self, casting_spell):
866873
# Spell start.
867874
data = pack(signature, *data)
868875
packet = PacketWriter.get_packet(OpCode.SMSG_SPELL_START, data)
869-
self.caster.get_map().send_surrounding(packet, self.caster, include_self=is_player)
876+
self.caster.get_map().send_surrounding(packet, self.caster, include_self=self.caster.is_player())
870877

871878
def handle_channel_start(self, casting_spell):
872879
if not casting_spell.is_channeled():
@@ -960,13 +967,14 @@ def send_spell_resist_result(self, casting_spell, damage_info):
960967
self.caster.get_map().send_surrounding(packet, self.caster, include_self=is_player)
961968

962969
def send_spell_go(self, casting_spell):
963-
# The client expects the source to only be set for unit casters.
964-
caster_unit = casting_spell.initial_target.guid if casting_spell.initial_target_is_item() \
965-
else self.caster.guid
970+
source_guid = self.caster.guid
971+
if casting_spell.source_item:
972+
source_guid = casting_spell.source_item.guid
973+
966974
caster_guid = self.caster.guid if self.caster.is_unit(by_mask=True) else 0
967975

968976
# Exclude proc flag from GO - proc casts are visible in 0.5.5 screenshots.
969-
data = [caster_unit, caster_guid, casting_spell.spell_entry.ID,
977+
data = [source_guid, caster_guid, casting_spell.spell_entry.ID,
970978
casting_spell.cast_flags & ~SpellCastFlags.CAST_FLAG_PROC]
971979

972980
signature = '<2QIHB' # caster, source, ID, flags .. (targets, ammo info).

game/world/managers/objects/spell/aura/AuraEffectHandler.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from game.world.managers.objects.units.player.StatManager import UnitStats
66
from game.world.managers.objects.spell import ExtendedSpellData
77
from utils.Logger import Logger
8-
from utils.constants.ItemCodes import InventoryError
8+
from utils.constants import ItemCodes
9+
from utils.constants.ItemCodes import InventoryError, ItemSubClasses
910
from utils.constants.MiscCodes import UnitDynamicTypes, ProcFlags
1011
from utils.constants.PetCodes import PetSlot
1112
from utils.constants.SpellCodes import ShapeshiftForms, AuraTypes, SpellSchoolMask, SpellImmunity
@@ -774,16 +775,27 @@ def handle_mod_dodge_chance(aura, effect_target, remove):
774775
effect_target.stat_manager.apply_aura_stat_bonus(aura.index, UnitStats.DODGE_CHANCE,
775776
amount_percent, percentual=False)
776777

777-
# TODO: Need to have separate blocking stats depending on item subclass.
778-
# e.g. 'Increases your chance to block with a Shield (not a Buckler) by 2%.'
779778
@staticmethod
780779
def handle_mod_block_chance(aura, effect_target, remove):
781780
if remove:
782781
effect_target.stat_manager.remove_aura_stat_bonus(aura.index, percentual=False)
783782
return
783+
784+
item_subclass = aura.spell_effect.casting_spell.spell_entry.EquippedItemSubclass
785+
shield_present = item_subclass & (1 << ItemSubClasses.ITEM_SUBCLASS_SHIELD)
786+
buckler_present = item_subclass & (1 << ItemSubClasses.ITEM_SUBCLASS_BUCKLER)
787+
788+
if shield_present and buckler_present:
789+
stat_type = UnitStats.BLOCK_SHIELD | UnitStats.BLOCK_BUCKLER
790+
elif shield_present:
791+
stat_type = UnitStats.BLOCK_SHIELD
792+
elif buckler_present:
793+
stat_type = UnitStats.BLOCK_BUCKLER
794+
else:
795+
return
796+
784797
amount_percent = aura.get_effect_points() / 100
785-
effect_target.stat_manager.apply_aura_stat_bonus(aura.index, UnitStats.BLOCK_CHANCE,
786-
amount_percent, percentual=False)
798+
effect_target.stat_manager.apply_aura_stat_bonus(aura.index, stat_type, amount_percent, percentual=False)
787799

788800
@staticmethod
789801
def handle_mod_threat(aura, effect_target, remove):

game/world/managers/objects/spell/aura/AuraManager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def check_aura_interrupts(self, moved=False, turned=False, changed_stand_state=F
157157

158158
# An interrupt for sitting does not exist.
159159
# Food/drink spells do claim that the player must remain seated.
160-
# In later versions an aurainterrupt exists for this purpose.
160+
# In later versions an aura interrupt exists for this purpose.
161161
if aura.source_spell.is_refreshment_spell() and changed_stand_state and \
162162
self.unit_mgr.stand_state != StandState.UNIT_SITTING:
163163
self.remove_aura(aura)

game/world/managers/objects/units/UnitManager.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,12 @@ def can_block(self, attacker_location=None, in_combat=False):
10211021
return self.has_block_passive and not self.spell_manager.is_casting() and \
10221022
not self.unit_state & UnitStates.STUNNED
10231023

1024+
def has_shield(self):
1025+
return False
1026+
1027+
def has_buckler(self):
1028+
return False
1029+
10241030
def can_parry(self, attacker_location=None, in_combat=False):
10251031
if not in_combat:
10261032
return self.has_parry_passive
@@ -1243,11 +1249,6 @@ def set_stealthed(self, active=True, index=-1) -> bool:
12431249
def set_rooted(self, active=True, index=-1) -> bool:
12441250
is_rooted = self.set_move_flag(MoveFlags.MOVEFLAG_ROOTED, active, index)
12451251
is_rooted |= self.set_unit_state(UnitStates.ROOTED, active, index)
1246-
1247-
if is_rooted:
1248-
# Stop movement if needed.
1249-
self.movement_manager.stop()
1250-
12511252
return is_rooted
12521253

12531254
def set_stunned(self, active=True, index=-1) -> bool:
@@ -1337,6 +1338,10 @@ def set_move_flag(self, move_flag, active=True, index=-1) -> bool:
13371338
else:
13381339
self.movement_flags &= ~move_flag
13391340

1341+
# Force movement stop if rooted or immobilized.
1342+
if flag_changed and is_active and move_flag in {MoveFlags.MOVEFLAG_ROOTED, MoveFlags.MOVEFLAG_IMMOBILIZED}:
1343+
self.movement_manager.stop(force=True)
1344+
13401345
# Only broadcast swimming, rooted, walking or immobilized.
13411346
if flag_changed and move_flag in {MoveFlags.MOVEFLAG_SWIMMING, MoveFlags.MOVEFLAG_ROOTED,
13421347
MoveFlags.MOVEFLAG_IMMOBILIZED, MoveFlags.MOVEFLAG_WALK}:

game/world/managers/objects/units/creature/CreatureManager.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ def finish_loading(self):
304304
self.apply_default_auras()
305305

306306
# Movement.
307+
self.set_move_flag(MoveFlags.MOVEFLAG_SWIMMING, active=self.static_flags & CreatureStaticFlags.AQUATIC != 0)
307308
self.set_move_flag(MoveFlags.MOVEFLAG_WALK, active=not self.should_always_run_ooc())
308309
self.movement_manager.initialize_or_reset()
309310

@@ -948,6 +949,9 @@ def _update_swimming_state(self):
948949
if not self.can_swim():
949950
return
950951

952+
if not self.get_map().is_active_cell_for_location(self.location):
953+
return
954+
951955
is_under_water = self.is_under_water()
952956

953957
if is_under_water and not self.movement_flags & MoveFlags.MOVEFLAG_SWIMMING:

0 commit comments

Comments
 (0)