Пример #1
0
 public ReloadWeaponRequest(
     IItem item,
     IProtoItemAmmo protoItemAmmo)
 {
     this.ProtoItemAmmo = protoItemAmmo;
     this.Item          = item;
 }
Пример #2
0
 public static ItemTooltipCompatibleWeaponsControl Create(IProtoItemAmmo protoItemAmmo)
 {
     return(new ItemTooltipCompatibleWeaponsControl()
     {
         protoItemAmmo = protoItemAmmo
     });
 }
Пример #3
0
 private void ClientRemote_NoSpaceForUnloadedAmmo(IProtoItemAmmo protoAmmo)
 {
     NotificationSystem.ClientShowNotification("Cannot unload",
                                               "No space for unloaded ammo in inventory",
                                               NotificationColor.Bad,
                                               protoAmmo.Icon);
 }
 public ViewModelItemTooltipCompatibleWeaponsControl(IProtoItemAmmo protoItemAmmo)
 {
     this.CompatibleWeaponProtos = protoItemAmmo.CompatibleWeaponProtos
                                   .OrderBy(e => e.GetType().Namespace)
                                   .ThenBy(e => e.Name)
                                   .ToArray();
 }
Пример #5
0
 public override void ClientOnWeaponHitOrTrace(
     ICharacter firingCharacter,
     Vector2D worldPositionSource,
     IProtoItemWeapon protoWeapon,
     IProtoItemAmmo protoAmmo,
     IProtoCharacter protoCharacter,
     in Vector2Ushort fallbackCharacterPosition,
Пример #6
0
        public WeaponReloadingState(
            ICharacter character,
            IItem item,
            IProtoItemWeapon itemProto,
            IProtoItemAmmo protoItemAmmo)
        {
            this.Item          = item;
            this.ProtoItemAmmo = protoItemAmmo;

            var reloadDuration = itemProto.AmmoReloadDuration;

            if (reloadDuration > 0)
            {
                var statName = itemProto.WeaponSkillProto?.StatNameReloadingSpeedMultiplier;
                if (statName.HasValue)
                {
                    reloadDuration *= character.SharedGetFinalStatMultiplier(statName.Value);
                }

                reloadDuration = Api.Shared.RoundDurationByServerFrameDuration(reloadDuration);
            }

            this.SecondsToReloadRemains = reloadDuration;
            //Api.Logger.WriteDev($"Weapon will be reloaded in: {this.SecondsToReloadRemains:F2} seconds");
        }
Пример #7
0
 public ViewModelItemTooltipCompatibleWeaponsControl(IProtoItemAmmo protoItemAmmo)
 {
     this.CompatibleWeaponProtos = protoItemAmmo.CompatibleWeaponProtos
                                   .Where(p => p.Icon != null)
                                   .OrderBy(p => p.GetType().Namespace)
                                   .ThenBy(p => p.Name)
                                   .Select(p => p.Name)
                                   .ToArray();
 }
Пример #8
0
        private static IReadOnlyDropItemsList ServerGetDroplistFor(IProtoItemAmmo protoAmmo)
        {
            if (ServerCachedDroplists.TryGetValue(protoAmmo, out var droplist))
            {
                return(droplist);
            }

            droplist = new DropItemsList().Add(protoAmmo);
            ServerCachedDroplists[protoAmmo] = droplist;
            return(droplist);
        }
Пример #9
0
        public static int SharedGetTotalAvailableAmmo(IProtoItemAmmo protoItemAmmo, ICharacter character)
        {
            int result = 0;

            foreach (var container in SharedGetTargetContainersForCharacterAmmo(character,
                                                                                isForAmmoUnloading: false))
            {
                result += container.CountItemsOfType(protoItemAmmo);
            }

            return(result);
        }
Пример #10
0
        private static IGrouping <IProtoItemAmmo, IItem> SharedFindNextAmmoGroup(
            IReadOnlyList <IProtoItemAmmo> protoWeaponCompatibleAmmoProtos,
            List <IGrouping <IProtoItemAmmo, IItem> > existingCompatibleAmmoGroups,
            IProtoItemAmmo currentProtoItemAmmo)
        {
            var ammoIndex = -1;

            for (var index = 0; index < protoWeaponCompatibleAmmoProtos.Count; index++)
            {
                var compatibleAmmoItem = protoWeaponCompatibleAmmoProtos[index];
                if (compatibleAmmoItem == currentProtoItemAmmo)
                {
                    // found current proto item, select next item prototype
                    ammoIndex = index;
                    break;
                }
            }

            if (ammoIndex < 0)
            {
                ammoIndex = -1;
            }

            // try to find next available ammo
            do
            {
                ammoIndex++;
                if (ammoIndex >= protoWeaponCompatibleAmmoProtos.Count)
                {
                    // unload weapon
                    return(null);
                }

                var requiredAmmoType = protoWeaponCompatibleAmmoProtos[ammoIndex];

                foreach (var availableAmmo in existingCompatibleAmmoGroups)
                {
                    if (availableAmmo.Key == requiredAmmoType)
                    {
                        // found required ammo
                        return(availableAmmo);
                    }
                }
            }while (true);
        }
Пример #11
0
 private void ClientRemote_OnWeaponHitOrTrace(
     ICharacter firingCharacter,
     IProtoItemWeapon protoWeapon,
     IProtoItemAmmo protoAmmo,
     IProtoCharacter protoCharacter,
     Vector2Ushort fallbackCharacterPosition,
     WeaponHitData[] hitObjects,
     Vector2D endPosition,
     bool endsWithHit)
 {
     WeaponSystemClientDisplay.ClientOnWeaponHitOrTrace(firingCharacter,
                                                        protoWeapon,
                                                        protoAmmo,
                                                        protoCharacter,
                                                        fallbackCharacterPosition,
                                                        hitObjects,
                                                        endPosition,
                                                        endsWithHit);
 }
Пример #12
0
        public static void SharedRebuildWeaponCache(
            ICharacter character,
            WeaponState weaponState)
        {
            DamageDescription damageDescription = null;
            var item      = weaponState.ItemWeapon;
            var protoItem = weaponState.ProtoWeapon;

            if (protoItem == null)
            {
                return;
            }

            IProtoItemAmmo protoAmmo = null;

            if (item != null)
            {
                var weaponPrivateState = item.GetPrivateState <WeaponPrivateState>();
                protoAmmo = weaponPrivateState.CurrentProtoItemAmmo;
            }

            if (protoItem.OverrideDamageDescription != null)
            {
                damageDescription = protoItem.OverrideDamageDescription;
            }
            else if (protoAmmo is IAmmoWithCustomWeaponCacheDamageDescription customAmmo)
            {
                damageDescription = customAmmo.DamageDescriptionForWeaponCache;
            }
            else if (protoAmmo != null)
            {
                damageDescription = protoAmmo.DamageDescription;
            }

            weaponState.WeaponCache = new WeaponFinalCache(
                character,
                character.SharedGetFinalStatsCache(),
                item,
                weaponState.ProtoWeapon,
                protoAmmo,
                damageDescription);
        }
Пример #13
0
        public static void ClientOnWeaponHitOrTrace(
            ICharacter firingCharacter,
            IProtoItemWeapon protoWeapon,
            IProtoItemAmmo protoAmmo,
            IProtoCharacter protoCharacter,
            Vector2Ushort fallbackCharacterPosition,
            IReadOnlyList <WeaponHitData> hitObjects,
            Vector2D endPosition,
            bool endsWithHit)
        {
            if (firingCharacter is not null &&
                !firingCharacter.IsInitialized)
            {
                firingCharacter = null;
            }

            var weaponTracePreset = protoWeapon.FireTracePreset
                                    ?? protoAmmo?.FireTracePreset;
            var worldPositionSource = SharedCalculateWeaponShotWorldPositon(
                firingCharacter,
                protoWeapon,
                protoCharacter,
                fallbackCharacterPosition.ToVector2D(),
                hasTrace: weaponTracePreset?.HasTrace ?? false);

            protoWeapon.ClientOnWeaponHitOrTrace(firingCharacter,
                                                 worldPositionSource,
                                                 protoWeapon,
                                                 protoAmmo,
                                                 protoCharacter,
                                                 fallbackCharacterPosition,
                                                 hitObjects,
                                                 endPosition,
                                                 endsWithHit);

            if (weaponTracePreset?.HasTrace ?? false)
            {
                ComponentWeaponTrace.Create(weaponTracePreset,
                                            worldPositionSource,
                                            endPosition,
                                            hasHit: endsWithHit,
                                            lastHitData: hitObjects.LastOrDefault(t => t.WorldObject is not null));
            }

            foreach (var hitData in hitObjects)
            {
                var hitWorldObject = hitData.WorldObject;
                if (hitWorldObject is not null &&
                    !hitWorldObject.IsInitialized)
                {
                    hitWorldObject = null;
                }

                var protoWorldObject = hitData.FallbackProtoWorldObject;

                double delay;
                {
                    var worldObjectPosition = CalculateWorldObjectPosition(hitWorldObject, hitData);
                    delay = weaponTracePreset?.HasTrace ?? false
                                ? SharedCalculateTimeToHit(weaponTracePreset,
                                                           worldPositionSource : worldPositionSource,
                                                           endPosition : worldObjectPosition
                                                           + hitData.HitPoint.ToVector2D())
                                : 0;
                }

                ClientTimersSystem.AddAction(
                    delay,
                    () =>
                {
                    // re-calculate the world object position
                    var worldObjectPosition = CalculateWorldObjectPosition(hitWorldObject, hitData);

                    var fireScatterPreset = protoAmmo?.OverrideFireScatterPreset
                                            ?? protoWeapon.FireScatterPreset;
                    var projectilesCount = fireScatterPreset.ProjectileAngleOffets.Length;

                    var objectMaterial = hitData.FallbackObjectMaterial;
                    if (hitWorldObject is ICharacter hitCharacter &&
                        hitCharacter.IsInitialized)
                    {
                        objectMaterial = ((IProtoCharacterCore)hitCharacter.ProtoCharacter)
                                         .SharedGetObjectMaterialForCharacter(hitCharacter);
                    }

                    protoWeapon.ClientPlayWeaponHitSound(hitWorldObject,
                                                         protoWorldObject,
                                                         fireScatterPreset,
                                                         objectMaterial,
                                                         worldObjectPosition);

                    if (weaponTracePreset is not null)
                    {
                        ClientAddHitSparks(weaponTracePreset.HitSparksPreset,
                                           hitData,
                                           hitWorldObject,
                                           protoWorldObject,
                                           worldObjectPosition,
                                           projectilesCount,
                                           objectMaterial,
                                           randomizeHitPointOffset: !weaponTracePreset.HasTrace,
                                           randomRotation: !weaponTracePreset.HasTrace,
                                           drawOrder: weaponTracePreset.DrawHitSparksAsLight
                                                              ? DrawOrder.Light
                                                              : DrawOrder.Default);
                    }
                });
Пример #14
0
        public static void ClientTryReloadOrSwitchAmmoType(
            bool isSwitchAmmoType,
            bool sendToServer             = true,
            bool?showNotificationIfNoAmmo = null)
        {
            var character          = Api.Client.Characters.CurrentPlayerCharacter;
            var currentWeaponState = PlayerCharacter.GetPrivateState(character).WeaponState;

            var itemWeapon = currentWeaponState.ItemWeapon;

            if (itemWeapon is null)
            {
                // no active weapon to reload
                return;
            }

            var protoWeapon = (IProtoItemWeapon)itemWeapon.ProtoItem;

            if (protoWeapon.AmmoCapacity == 0)
            {
                // the item is non-reloadable
                return;
            }

            var itemPrivateState = itemWeapon.GetPrivateState <WeaponPrivateState>();
            var ammoCountNeed    = isSwitchAmmoType
                                    ? protoWeapon.AmmoCapacity
                                    : (ushort)Math.Max(0, protoWeapon.AmmoCapacity - itemPrivateState.AmmoCount);

            if (ammoCountNeed == 0)
            {
                Logger.Info("No need to reload the weapon " + itemWeapon, character);
                return;
            }

            var compatibleAmmoGroups = SharedGetCompatibleAmmoGroups(character, protoWeapon);

            if (compatibleAmmoGroups.Count == 0 &&
                !isSwitchAmmoType)
            {
                if (showNotificationIfNoAmmo.HasValue && showNotificationIfNoAmmo.Value ||
                    currentWeaponState.SharedGetInputIsFiring())
                {
                    protoWeapon.SoundPresetWeapon.PlaySound(WeaponSound.Empty,
                                                            character,
                                                            volume: SoundConstants.VolumeWeapon);
                    NotificationSystem.ClientShowNotification(
                        NotificationNoAmmo_Title,
                        NotificationNoAmmo_Message,
                        NotificationColor.Bad,
                        protoWeapon.Icon,
                        playSound: false);
                }

                if (currentWeaponState.SharedGetInputIsFiring())
                {
                    // stop firing the weapon
                    currentWeaponState.ProtoWeapon.ClientItemUseFinish(itemWeapon);
                }

                return;
            }

            IProtoItemAmmo selectedProtoItemAmmo = null;

            var currentReloadingState = currentWeaponState.WeaponReloadingState;

            if (currentReloadingState is null)
            {
                // don't have reloading state - find ammo item matching current weapon ammo type
                var currentProtoItemAmmo = itemPrivateState.CurrentProtoItemAmmo;
                if (currentProtoItemAmmo is null)
                {
                    // no ammo selected in weapon
                    selectedProtoItemAmmo = SharedFindNextAmmoGroup(protoWeapon.CompatibleAmmoProtos,
                                                                    compatibleAmmoGroups,
                                                                    currentProtoItemAmmo: null)?.Key;
                }
                else // if weapon already has ammo
                {
                    if (isSwitchAmmoType)
                    {
                        selectedProtoItemAmmo = SharedFindNextAmmoGroup(protoWeapon.CompatibleAmmoProtos,
                                                                        compatibleAmmoGroups,
                                                                        currentProtoItemAmmo)?.Key;
                        if (selectedProtoItemAmmo == currentProtoItemAmmo &&
                            itemPrivateState.AmmoCount >= protoWeapon.AmmoCapacity)
                        {
                            // this ammo type is already loaded and it's fully reloaded
                            Logger.Info("No need to reload the weapon " + itemWeapon, character);
                            return;
                        }
                    }
                    else // simple reload requested
                    {
                        // try to find ammo of the same type as already loaded into the weapon
                        var isFound = false;
                        foreach (var ammoGroup in compatibleAmmoGroups)
                        {
                            if (ammoGroup.Key == currentProtoItemAmmo)
                            {
                                isFound = true;
                                selectedProtoItemAmmo = currentProtoItemAmmo;
                                break;
                            }
                        }

                        if (!isFound)
                        {
                            // no group selected - select first
                            isSwitchAmmoType      = true;
                            sendToServer          = true;
                            selectedProtoItemAmmo = SharedFindNextAmmoGroup(protoWeapon.CompatibleAmmoProtos,
                                                                            compatibleAmmoGroups,
                                                                            currentProtoItemAmmo: null)?.Key;
                        }
                    }
                }
            }
            else
            {
                if (!isSwitchAmmoType)
                {
                    // already reloading
                    return;
                }

                // already reloading - try select another ammo type (alternate between them)
                var currentReloadingProtoItemAmmo = currentReloadingState.ProtoItemAmmo;
                selectedProtoItemAmmo = SharedFindNextAmmoGroup(protoWeapon.CompatibleAmmoProtos,
                                                                compatibleAmmoGroups,
                                                                currentReloadingProtoItemAmmo)?.Key;

                if (selectedProtoItemAmmo == currentReloadingProtoItemAmmo)
                {
                    // already reloading this ammo type
                    return;
                }
            }

            if (currentReloadingState != null &&
                currentReloadingState.ProtoItemAmmo == selectedProtoItemAmmo)
            {
                // already reloading with these ammo items
                return;
            }

            if (currentReloadingState is null &&
                selectedProtoItemAmmo is null &&
                itemPrivateState.CurrentProtoItemAmmo is null)
            {
                // already unloaded
                return;
            }

            // create reloading state on the Client-side
            var weaponReloadingState = new WeaponReloadingState(
                character,
                itemWeapon,
                protoWeapon,
                selectedProtoItemAmmo);

            currentWeaponState.WeaponReloadingState = weaponReloadingState;

            protoWeapon.SoundPresetWeapon.PlaySound(WeaponSound.Reload,
                                                    character,
                                                    SoundConstants.VolumeWeapon);
            Logger.Info(
                $"Weapon reloading started for {itemWeapon} reload duration: {weaponReloadingState.SecondsToReloadRemains:F2}s",
                character);

            if (weaponReloadingState.SecondsToReloadRemains <= 0)
            {
                // instant-reload weapon - perform local reloading
                SharedProcessWeaponReload(character, currentWeaponState, out _);
            }

            if (sendToServer || isSwitchAmmoType)
            {
                // perform reload on server
                var arg = new ReloadWeaponRequest(itemWeapon, selectedProtoItemAmmo);
                Instance.CallServer(_ => _.ServerRemote_ReloadWeapon(arg));
            }
        }
Пример #15
0
        public static void ServerTryReloadSameAmmo(ICharacter character)
        {
            var weaponState = PlayerCharacter.GetPrivateState(character).WeaponState;

            var item = weaponState.ItemWeapon;

            if (item is null)
            {
                // no active weapon to reload
                return;
            }

            var itemProto = (IProtoItemWeapon)item.ProtoItem;

            if (itemProto.AmmoCapacity == 0)
            {
                // the item is non-reloadable
                return;
            }

            var itemPrivateState = item.GetPrivateState <WeaponPrivateState>();
            var ammoCountNeed    = (ushort)Math.Max(0, itemProto.AmmoCapacity - itemPrivateState.AmmoCount);

            if (ammoCountNeed == 0)
            {
                return;
            }

            var compatibleAmmoGroups = SharedGetCompatibleAmmoGroups(character, itemProto);

            if (compatibleAmmoGroups.Count == 0)
            {
                // no ammo to reload
                return;
            }

            IProtoItemAmmo selectedProtoItemAmmo = null;

            var currentReloadingState = weaponState.WeaponReloadingState;

            if (currentReloadingState != null)
            {
                // already reloading
                return;
            }

            // don't have reloading state - find ammo item matching current weapon ammo type
            var currentProtoItemAmmo = itemPrivateState.CurrentProtoItemAmmo;

            if (currentProtoItemAmmo is null)
            {
                // no ammo selected in weapon
                return;
            }

            // simple reload requested
            // try to find ammo of the same type as already loaded into the weapon
            var isAmmoFound = false;

            foreach (var ammoGroup in compatibleAmmoGroups)
            {
                if (ammoGroup.Key == currentProtoItemAmmo)
                {
                    isAmmoFound           = true;
                    selectedProtoItemAmmo = currentProtoItemAmmo;
                    break;
                }
            }

            if (!isAmmoFound)
            {
                return;
            }

            // create reloading state on the Server-side
            var weaponReloadingState = new WeaponReloadingState(
                character,
                item,
                itemProto,
                selectedProtoItemAmmo);

            weaponState.WeaponReloadingState = weaponReloadingState;
            //Logger.Dev("Weapon started reloading without a client request " + item, character);

            if (weaponReloadingState.SecondsToReloadRemains <= 0)
            {
                // instant-reload weapon - perform local reloading
                SharedProcessWeaponReload(character, weaponState, out _);
            }
            else if (IsServer)
            {
                ServerNotifyAboutReloading(character, weaponState, isFinished: false);
            }
        }
Пример #16
0
 public ProtoItemAmmoViewModel([NotNull] IProtoItemAmmo ammo) : base(ammo)
 {
 }
Пример #17
0
        public WeaponFinalCache(
            ICharacter character,
            FinalStatsCache characterFinalStatsCache,
            [CanBeNull] IItem weapon,
            [CanBeNull] IProtoItemWeapon protoWeapon,
            [CanBeNull] IProtoItemAmmo protoAmmo,
            DamageDescription damageDescription,
            IProtoExplosive protoExplosive  = null,
            IDynamicWorldObject objectDrone = null)
        {
            this.Character = character;
            this.CharacterFinalStatsCache = characterFinalStatsCache;
            this.Drone          = objectDrone;
            this.Weapon         = weapon;
            this.ProtoWeapon    = (IProtoItemWeapon)weapon?.ProtoItem ?? protoWeapon;
            this.ProtoAmmo      = protoAmmo;
            this.ProtoExplosive = protoExplosive;

            if (damageDescription is null)
            {
                // TODO: it looks like not implemented yet and we should throw an exception here
                // fallback in case weapon don't provide damage description (such as no-ammo weapon)
                damageDescription = new DamageDescription(
                    damageValue: 0,
                    armorPiercingCoef: 0,
                    finalDamageMultiplier: 1,
                    rangeMax: 0,
                    damageDistribution: new DamageDistribution());
            }

            var descriptionDamages        = damageDescription.DamageProportions;
            var damageDistributionsCount  = descriptionDamages.Count;
            var resultDamageDistributions = new List <DamageProportion>(damageDistributionsCount);

            var totalPercents = 0d;

            for (var index = 0; index < damageDistributionsCount; index++)
            {
                var source   = descriptionDamages[index];
                var statName = GetProportionStatName(source.DamageType);
                var resultDamageProportion = source.Proportion + characterFinalStatsCache[statName];
                if (resultDamageProportion <= 0)
                {
                    continue;
                }

                resultDamageDistributions.Add(new DamageProportion(source.DamageType, resultDamageProportion));
                totalPercents += resultDamageProportion;
            }

            if (damageDistributionsCount > 0 &&
                Math.Abs(totalPercents - 1) > 0.001d)
            {
                throw new Exception(
                          "Sum of all damage proportions must be exactly 1. Calculated value: "
                          + totalPercents.ToString("F3"));
            }

            this.DamageDistributions = resultDamageDistributions;

            this.DamageValue = damageDescription.DamageValue * (protoWeapon?.DamageMultiplier ?? 1.0)
                               + characterFinalStatsCache[StatName.DamageAdd];

            var weaponSkillProto = protoWeapon?.WeaponSkillProto;

            if (weaponSkillProto is not null)
            {
                var statName = protoWeapon.WeaponSkillProto.StatNameDamageBonusMultiplier;
                this.DamageValue *= characterFinalStatsCache.GetMultiplier(statName);
            }

            this.RangeMax = damageDescription.RangeMax * (protoWeapon?.RangeMultiplier ?? 1.0)
                            + characterFinalStatsCache[StatName.AttackRangeMax];

            var armorPiercingCoef = (1 + characterFinalStatsCache[StatName.AttackArmorPiercingMultiplier])
                                    * (damageDescription.ArmorPiercingCoef
                                       + characterFinalStatsCache[StatName.AttackArmorPiercingValue]);

            this.InvertedArmorPiercingCoef = 1 - armorPiercingCoef;

            this.FinalDamageMultiplier = damageDescription.FinalDamageMultiplier
                                         + characterFinalStatsCache[StatName.AttackFinalDamageMultiplier];

            var probability = protoWeapon?.SpecialEffectProbability ?? 0;

            if (weaponSkillProto is not null)
            {
                var statNameSpecialEffectChance = weaponSkillProto.StatNameSpecialEffectChanceMultiplier;
                probability *= characterFinalStatsCache.GetMultiplier(statNameSpecialEffectChance);
            }

            this.SpecialEffectProbability = probability;

            this.FireScatterPreset = protoAmmo?.OverrideFireScatterPreset
                                     ?? protoWeapon?.FireScatterPreset
                                     ?? default;
            var shotsPerFire = this.FireScatterPreset.ProjectileAngleOffets.Length;

            if (shotsPerFire > 1)
            {
                // decrease final damage and special effect multiplier on the number of shots per fire
                var coef = 1.0 / shotsPerFire;
                this.FinalDamageMultiplier    *= coef;
                this.SpecialEffectProbability *= coef;
            }
        }
Пример #18
0
        private static void SharedCallOnWeaponHitOrTrace(
            ICharacter firingCharacter,
            IProtoItemWeapon protoWeapon,
            IProtoItemAmmo protoAmmo,
            Vector2D endPosition,
            List <WeaponHitData> hitObjects,
            bool endsWithHit)
        {
            if (IsClient)
            {
                // display weapon shot on Client-side
                WeaponSystemClientDisplay.ClientOnWeaponHitOrTrace(firingCharacter,
                                                                   protoWeapon,
                                                                   protoAmmo,
                                                                   firingCharacter.ProtoCharacter,
                                                                   firingCharacter.Position.ToVector2Ushort(),
                                                                   hitObjects,
                                                                   endPosition,
                                                                   endsWithHit);
            }
            else // if server
            {
                // display damages on clients in scope of every damaged object
                var observers = new HashSet <ICharacter>();
                using var tempList = Shared.GetTempList <ICharacter>();
                Server.World.GetScopedByPlayers(firingCharacter, tempList);
                observers.AddRange(tempList.AsList());

                foreach (var hitObject in hitObjects)
                {
                    if (hitObject.IsCliffsHit ||
                        hitObject.WorldObject.IsDestroyed)
                    {
                        continue;
                    }

                    if (hitObject.WorldObject is ICharacter damagedCharacter &&
                        !damagedCharacter.IsNpc)
                    {
                        // notify the damaged character
                        observers.Add(damagedCharacter);
                    }

                    Server.World.GetScopedByPlayers(hitObject.WorldObject, tempList);
                    tempList.Clear();
                    observers.AddRange(tempList.AsList());
                }

                // add all observers within the sound radius (so they can not only hear but also see the traces)
                var eventNetworkRadius = (byte)Math.Max(
                    15,
                    Math.Ceiling(protoWeapon.SoundPresetWeaponDistance.max));

                tempList.Clear();
                Server.World.GetCharactersInRadius(firingCharacter.TilePosition,
                                                   tempList,
                                                   radius: eventNetworkRadius,
                                                   onlyPlayers: true);
                observers.AddRange(tempList.AsList());

                // don't notify the attacking character
                observers.Remove(firingCharacter);

                if (observers.Count > 0)
                {
                    Instance.CallClient(observers,
                                        _ => _.ClientRemote_OnWeaponHitOrTrace(firingCharacter,
                                                                               protoWeapon,
                                                                               protoAmmo,
                                                                               firingCharacter.ProtoCharacter,
                                                                               firingCharacter
                                                                               .Position.ToVector2Ushort(),
                                                                               hitObjects.ToArray(),
                                                                               endPosition,
                                                                               endsWithHit));
                }
            }
        }