Example #1
0
        public static double ServerCalculateTotalDamage(
            WeaponFinalCache weaponFinalCache,
            IWorldObject targetObject,
            FinalStatsCache targetFinalStatsCache,
            double damagePreMultiplier,
            bool clampDefenseTo1)
        {
            if (weaponFinalCache.ProtoObjectExplosive != null &&
                targetObject.ProtoWorldObject is IProtoStaticWorldObject targetStaticWorldObjectProto)
            {
                // special case - apply the explosive damage
                return(ServerCalculateTotalDamageByExplosive(weaponFinalCache.ProtoObjectExplosive,
                                                             targetStaticWorldObjectProto,
                                                             damagePreMultiplier));
            }

            var damageValue = damagePreMultiplier * weaponFinalCache.DamageValue;
            var invertedArmorPiercingCoef = weaponFinalCache.InvertedArmorPiercingCoef;

            var totalDamage = 0d;

            // calculate total damage by summing all the damage components
            foreach (var damageDistribution in weaponFinalCache.DamageDistributions)
            {
                var defenseStatName = SharedGetDefenseStatName(damageDistribution.DamageType);
                var defenseFraction = targetFinalStatsCache[defenseStatName];
                defenseFraction = MathHelper.Clamp(defenseFraction, 0, clampDefenseTo1 ? 1 : double.MaxValue);

                totalDamage += ServerCalculateDamageComponent(
                    damageValue,
                    invertedArmorPiercingCoef,
                    damageDistribution,
                    defenseFraction);
            }

            // multiply on final multiplier (usually used for expanding projectiles)
            totalDamage *= weaponFinalCache.FinalDamageMultiplier;
            return(totalDamage);
        }
Example #2
0
        public static double ServerCalculateTotalDamage(
            WeaponFinalCache weaponCache,
            IWorldObject targetObject,
            FinalStatsCache targetFinalStatsCache,
            double damagePreMultiplier,
            bool clampDefenseTo1)
        {
            if (targetObject is IStaticWorldObject staticWorldObject &&
                (!RaidingProtectionSystem.SharedCanRaid(staticWorldObject,
                                                        showClientNotification: false) ||
                 !LandClaimShieldProtectionSystem.SharedCanRaid(staticWorldObject,
                                                                showClientNotification: false) ||
                 !PveSystem.SharedIsAllowStaticObjectDamage(weaponCache.Character,
                                                            staticWorldObject,
                                                            showClientNotification: false) ||
                 !NewbieProtectionSystem.SharedIsAllowStructureDamage(weaponCache.Character,
                                                                      staticWorldObject,
                                                                      showClientNotification: false)))
            {
                return(0);
            }

            if (targetObject.ProtoGameObject is IProtoVehicle &&
                !PveSystem.SharedIsAllowVehicleDamage(weaponCache,
                                                      (IDynamicWorldObject)targetObject,
                                                      showClientNotification: false))
            {
                return(0);
            }

            if (weaponCache.ProtoExplosive is not null &&
                targetObject is IStaticWorldObject targetStaticWorldObject)
            {
                // special case - apply the explosive damage to static object
                return(ServerCalculateTotalDamageByExplosive(weaponCache.Character,
                                                             weaponCache.ProtoExplosive,
                                                             targetStaticWorldObject,
                                                             damagePreMultiplier));
            }

            if (ServerIsRestrictedPvPDamage(weaponCache,
                                            targetObject,
                                            out var isPvPcase,
                                            out var isFriendlyFireCase))
            {
                return(0);
            }

            var damageValue = damagePreMultiplier * weaponCache.DamageValue;
            var invertedArmorPiercingCoef = weaponCache.InvertedArmorPiercingCoef;

            var totalDamage = 0d;

            // calculate total damage by summing all the damage components
            foreach (var damageDistribution in weaponCache.DamageDistributions)
            {
                var defenseStatName = SharedGetDefenseStatName(damageDistribution.DamageType);
                var defenseFraction = targetFinalStatsCache[defenseStatName];
                defenseFraction = MathHelper.Clamp(defenseFraction, 0, clampDefenseTo1 ? 1 : double.MaxValue);

                totalDamage += ServerCalculateDamageComponent(
                    damageValue,
                    invertedArmorPiercingCoef,
                    damageDistribution,
                    defenseFraction);
            }

            // multiply on final multiplier (usually used for expanding projectiles)
            totalDamage *= weaponCache.FinalDamageMultiplier;

            var damagingCharacter = weaponCache.Character;

            if (isPvPcase)
            {
                // apply PvP damage multiplier
                totalDamage *= WeaponConstants.DamagePvpMultiplier;
            }
            else if (damagingCharacter is not null &&
                     !damagingCharacter.IsNpc &&
                     targetObject.ProtoGameObject
                     is IProtoCharacterMob protoCharacterMob &&
                     !protoCharacterMob.IsBoss)
            {
                // apply PvE damage multiplier
                totalDamage *= WeaponConstants.DamagePveMultiplier;
            }
        /// <summary>
        /// Bomberman-style explosion penetrating the walls in a cross.
        /// </summary>
        public static void ServerProcessExplosionBomberman(
            Vector2D positionEpicenter,
            IPhysicsSpace physicsSpace,
            int damageDistanceFullDamage,
            int damageDistanceMax,
            double damageDistanceDynamicObjectsOnly,
            WeaponFinalCache weaponFinalCache,
            Func <double, double> callbackCalculateDamageCoefByDistanceForStaticObjects,
            Func <double, double> callbackCalculateDamageCoefByDistanceForDynamicObjects)
        {
            var protoObjectExplosive = weaponFinalCache.ProtoObjectExplosive;

            Api.Assert(protoObjectExplosive != null,
                       "Weapon final cache should contain the exploded object");

            Api.Assert(damageDistanceMax >= damageDistanceFullDamage,
                       $"{nameof(damageDistanceMax)} must be >= {nameof(damageDistanceFullDamage)}");

            var world          = Api.Server.World;
            var damagedObjects = new HashSet <IWorldObject>();

            ProcessExplosionDirection(-1, 0); // left
            ProcessExplosionDirection(0, 1);  // top
            ProcessExplosionDirection(1, 0);  // right
            ProcessExplosionDirection(0, -1); // bottom

            ServerProcessExplosionCircle(positionEpicenter,
                                         physicsSpace,
                                         damageDistanceDynamicObjectsOnly,
                                         weaponFinalCache,
                                         damageOnlyDynamicObjects: true,
                                         callbackCalculateDamageCoefByDistance:
                                         callbackCalculateDamageCoefByDistanceForDynamicObjects);

            void ProcessExplosionDirection(int xOffset, int yOffset)
            {
                var fromPosition = positionEpicenter.ToVector2Ushort();

                for (var offsetIndex = 1; offsetIndex <= damageDistanceMax; offsetIndex++)
                {
                    var tile = world.GetTile(fromPosition.X + offsetIndex * xOffset,
                                             fromPosition.Y + offsetIndex * yOffset,
                                             logOutOfBounds: false);

                    if (!tile.IsValidTile ||
                        tile.IsCliff)
                    {
                        return;
                    }

                    var tileStaticObjects            = tile.StaticObjects;
                    IStaticWorldObject damagedObject = null;
                    foreach (var staticWorldObject in tileStaticObjects)
                    {
                        if (staticWorldObject.ProtoGameObject is IProtoObjectWall ||
                            staticWorldObject.ProtoGameObject is IProtoObjectDoor)
                        {
                            // damage only walls and doors
                            damagedObject = staticWorldObject;
                            break;
                        }
                    }

                    if (damagedObject == null)
                    {
                        // no wall or door there
                        if (offsetIndex > damageDistanceFullDamage)
                        {
                            // stop damage propagation
                            return;
                        }

                        continue;
                    }

                    if (!damagedObjects.Add(damagedObject))
                    {
                        // the object is already damaged
                        // (from another direction which might be theoretically possible in some future cases)
                        continue;
                    }

                    var distanceToDamagedObject = offsetIndex;
                    var damageMultiplier        =
                        callbackCalculateDamageCoefByDistanceForStaticObjects(distanceToDamagedObject);
                    damageMultiplier = MathHelper.Clamp(damageMultiplier, 0, 1);

                    var damageableProto = (IDamageableProtoWorldObject)damagedObject.ProtoGameObject;
                    damageableProto.SharedOnDamage(
                        weaponFinalCache,
                        damagedObject,
                        damageMultiplier,
                        out _,
                        out _);
                }
            }
        }
        public static void ServerProcessExplosionCircle(
            Vector2D positionEpicenter,
            IPhysicsSpace physicsSpace,
            double damageDistanceMax,
            WeaponFinalCache weaponFinalCache,
            bool damageOnlyDynamicObjects,
            Func <double, double> callbackCalculateDamageCoefByDistance)
        {
            var protoObjectExplosive = weaponFinalCache.ProtoObjectExplosive;

            Api.Assert(protoObjectExplosive != null,
                       "Weapon final cache should contain the exploded object");

            var damageCandidates = new HashSet <IWorldObject>();

            // collect all damaged physics objects
            var collisionGroup = CollisionGroups.HitboxRanged;

            using (var testResults = physicsSpace.TestCircle(positionEpicenter,
                                                             radius: damageDistanceMax,
                                                             collisionGroup: collisionGroup))
            {
                foreach (var testResult in testResults)
                {
                    var testResultPhysicsBody = testResult.PhysicsBody;
                    var damagedObject         = testResultPhysicsBody.AssociatedWorldObject;

                    if (damageOnlyDynamicObjects &&
                        damagedObject is IStaticWorldObject)
                    {
                        continue;
                    }

                    if (!(damagedObject?.ProtoWorldObject is IDamageableProtoWorldObject))
                    {
                        // non-damageable world object
                        continue;
                    }

                    damageCandidates.Add(damagedObject);
                }

                if (!damageOnlyDynamicObjects)
                {
                    // Collect all the damageable static objects in the explosion radius
                    // which don't have a collider colliding with the HitboxRanged collision group.
                    var startTilePosition        = positionEpicenter.ToVector2Ushort();
                    var damageDistanceMaxRounded = (int)damageDistanceMax;
                    var damageDistanceMaxSqr     = damageDistanceMax * damageDistanceMax;
                    var minTileX = startTilePosition.X - damageDistanceMaxRounded;
                    var minTileY = startTilePosition.Y - damageDistanceMaxRounded;
                    var maxTileX = startTilePosition.X + damageDistanceMaxRounded;
                    var maxTileY = startTilePosition.Y + damageDistanceMaxRounded;

                    for (var x = minTileX; x <= maxTileX; x++)
                    {
                        for (var y = minTileY; y <= maxTileY; y++)
                        {
                            if (x < 0 ||
                                x > ushort.MaxValue ||
                                y < 0 ||
                                y > ushort.MaxValue)
                            {
                                continue;
                            }

                            if (new Vector2Ushort((ushort)x, (ushort)y)
                                .TileSqrDistanceTo(startTilePosition)
                                > damageDistanceMaxSqr)
                            {
                                // too far
                                continue;
                            }

                            var tileObjects = Api.Server.World.GetStaticObjects(new Vector2Ushort((ushort)x, (ushort)y));
                            if (tileObjects.Count == 0)
                            {
                                continue;
                            }

                            foreach (var tileObject in tileObjects)
                            {
                                if (!(tileObject.ProtoStaticWorldObject is IDamageableProtoWorldObject))
                                {
                                    // non-damageable
                                    continue;
                                }

                                if (tileObject.PhysicsBody.HasAnyShapeCollidingWithGroup(collisionGroup))
                                {
                                    // has a collider colliding with the HitboxRanged collision group so we ignore this
                                    continue;
                                }

                                damageCandidates.Add(tileObject);
                            }
                        }
                    }
                }

                // order by distance to explosion center
                var orderedDamagedObjects =
                    damageCandidates.OrderBy(ServerExplosionGetDistanceToEpicenter(positionEpicenter));
                // process all damaged objects
                foreach (var damagedObject in orderedDamagedObjects)
                {
                    if (ServerHasObstacleForExplosion(physicsSpace, positionEpicenter, damagedObject))
                    {
                        continue;
                    }

                    var distanceToDamagedObject = ServerCalculateDistanceToDamagedObject(positionEpicenter,
                                                                                         damagedObject);
                    var damageMultiplier = callbackCalculateDamageCoefByDistance(distanceToDamagedObject);
                    damageMultiplier = MathHelper.Clamp(damageMultiplier, 0, 1);

                    var damageableProto = (IDamageableProtoWorldObject)damagedObject.ProtoGameObject;
                    damageableProto.SharedOnDamage(
                        weaponFinalCache,
                        damagedObject,
                        damageMultiplier,
                        out _,
                        out _);
                }
            }
        }
Example #5
0
        /// <summary>
        /// Bomberman-style explosion penetrating the walls in a cross.
        /// </summary>
        public static void ServerProcessExplosionBomberman(
            Vector2D positionEpicenter,
            IPhysicsSpace physicsSpace,
            int damageDistanceFullDamage,
            int damageDistanceMax,
            double damageDistanceDynamicObjectsOnly,
            WeaponFinalCache weaponFinalCache,
            Func <double, double> callbackCalculateDamageCoefByDistanceForStaticObjects,
            Func <double, double> callbackCalculateDamageCoefByDistanceForDynamicObjects)
        {
            Api.Assert(damageDistanceMax >= damageDistanceFullDamage,
                       $"{nameof(damageDistanceMax)} must be >= {nameof(damageDistanceFullDamage)}");

            var playerCharacterSkills = weaponFinalCache.Character?.SharedGetSkills();
            var protoWeaponSkill      = playerCharacterSkills is not null
                                       ? weaponFinalCache.ProtoWeapon?.WeaponSkillProto
                                       : null;

            var world             = Api.Server.World;
            var allDamagedObjects = new HashSet <IWorldObject>();

            ProcessExplosionDirection(-1, 0);  // left
            ProcessExplosionDirection(0, 1);   // top
            ProcessExplosionDirection(1, 0);   // right
            ProcessExplosionDirection(0, -1);  // bottom

            ServerProcessExplosionCircle(positionEpicenter,
                                         physicsSpace,
                                         damageDistanceDynamicObjectsOnly,
                                         weaponFinalCache,
                                         damageOnlyDynamicObjects: true,
                                         isDamageThroughObstacles: false,
                                         callbackCalculateDamageCoefByDistanceForStaticObjects:
                                         callbackCalculateDamageCoefByDistanceForStaticObjects,
                                         callbackCalculateDamageCoefByDistanceForDynamicObjects:
                                         callbackCalculateDamageCoefByDistanceForDynamicObjects);

            void ProcessExplosionDirection(int xOffset, int yOffset)
            {
                foreach (var(damagedObject, offsetIndex) in
                         SharedEnumerateExplosionBombermanDirectionTilesWithTargets(positionEpicenter,
                                                                                    damageDistanceFullDamage,
                                                                                    damageDistanceMax,
                                                                                    world,
                                                                                    xOffset,
                                                                                    yOffset))
                {
                    if (damagedObject is null)
                    {
                        continue;
                    }

                    if (!allDamagedObjects.Add(damagedObject))
                    {
                        // the object is already damaged
                        // (from another direction which might be theoretically possible in some future cases)
                        continue;
                    }

                    var distanceToDamagedObject = offsetIndex;
                    // this explosion pattern selects only the static objects as targets
                    var damagePreMultiplier = callbackCalculateDamageCoefByDistanceForStaticObjects(
                        distanceToDamagedObject);
                    damagePreMultiplier = MathHelper.Clamp(damagePreMultiplier, 0, 1);

                    var damageableProto = (IDamageableProtoWorldObject)damagedObject.ProtoGameObject;
                    damageableProto.SharedOnDamage(
                        weaponFinalCache,
                        damagedObject,
                        damagePreMultiplier,
                        damagePostMultiplier: 1.0,
                        out _,
                        out var damageApplied);

                    if (Api.IsServer)
                    {
                        if (damageApplied > 0)
                        {
                            // give experience for damage
                            protoWeaponSkill?.ServerOnDamageApplied(playerCharacterSkills,
                                                                    damagedObject,
                                                                    damageApplied);
                        }

                        weaponFinalCache.ProtoExplosive?.ServerOnObjectHitByExplosion(damagedObject,
                                                                                      damageApplied,
                                                                                      weaponFinalCache);
                    }
                }
            }
        }
Example #6
0
        public static void ServerProcessExplosionCircle(
            Vector2D positionEpicenter,
            IPhysicsSpace physicsSpace,
            double damageDistanceMax,
            WeaponFinalCache weaponFinalCache,
            bool damageOnlyDynamicObjects,
            bool isDamageThroughObstacles,
            Func <double, double> callbackCalculateDamageCoefByDistanceForStaticObjects,
            Func <double, double> callbackCalculateDamageCoefByDistanceForDynamicObjects,
            [CanBeNull] CollisionGroup[] collisionGroups = null)
        {
            var playerCharacterSkills = weaponFinalCache.Character?.SharedGetSkills();
            var protoWeaponSkill      = playerCharacterSkills is not null
                                       ? weaponFinalCache.ProtoWeapon?.WeaponSkillProto
                                       : null;

            // collect all damaged objects via physics space
            var damageCandidates = new HashSet <IWorldObject>();

            collisionGroups ??= new[]
            {
                CollisionGroup.Default,
                CollisionGroups.HitboxMelee,
                CollisionGroups.HitboxRanged
            };

            var defaultCollisionGroup = collisionGroups[0];

            foreach (var collisionGroup in collisionGroups)
            {
                CollectDamagedPhysicalObjects(collisionGroup);
            }

            void CollectDamagedPhysicalObjects(CollisionGroup collisionGroup)
            {
                using var testResults = physicsSpace.TestCircle(positionEpicenter,
                                                                radius: damageDistanceMax,
                                                                collisionGroup: collisionGroup);
                foreach (var testResult in testResults.AsList())
                {
                    var testResultPhysicsBody = testResult.PhysicsBody;
                    var damagedObject         = testResultPhysicsBody.AssociatedWorldObject;

                    if (damageOnlyDynamicObjects &&
                        damagedObject is IStaticWorldObject)
                    {
                        continue;
                    }

                    if (!(damagedObject?.ProtoWorldObject is IDamageableProtoWorldObject))
                    {
                        // non-damageable world object
                        continue;
                    }

                    damageCandidates.Add(damagedObject);
                }
            }

            if (!damageOnlyDynamicObjects)
            {
                // Collect all the damageable static objects in the explosion radius
                // which don't have a collider colliding with the collision group.
                var startTilePosition        = positionEpicenter.ToVector2Ushort();
                var damageDistanceMaxRounded = (int)damageDistanceMax;
                var damageDistanceMaxSqr     = damageDistanceMax * damageDistanceMax;
                var minTileX = startTilePosition.X - damageDistanceMaxRounded;
                var minTileY = startTilePosition.Y - damageDistanceMaxRounded;
                var maxTileX = startTilePosition.X + damageDistanceMaxRounded;
                var maxTileY = startTilePosition.Y + damageDistanceMaxRounded;

                for (var x = minTileX; x <= maxTileX; x++)
                {
                    for (var y = minTileY; y <= maxTileY; y++)
                    {
                        if (x < 0 ||
                            x > ushort.MaxValue ||
                            y < 0 ||
                            y > ushort.MaxValue)
                        {
                            continue;
                        }

                        if (new Vector2Ushort((ushort)x, (ushort)y)
                            .TileSqrDistanceTo(startTilePosition)
                            > damageDistanceMaxSqr)
                        {
                            // too far
                            continue;
                        }

                        var tileObjects = Api.Server.World.GetStaticObjects(new Vector2Ushort((ushort)x, (ushort)y));
                        if (tileObjects.Count == 0)
                        {
                            continue;
                        }

                        foreach (var tileObject in tileObjects)
                        {
                            if (!(tileObject.ProtoStaticWorldObject is IDamageableProtoWorldObject))
                            {
                                // non-damageable
                                continue;
                            }

                            if (tileObject.PhysicsBody.HasAnyShapeCollidingWithGroup(defaultCollisionGroup))
                            {
                                // has a collider colliding with the collision group so we ignore this
                                continue;
                            }

                            damageCandidates.Add(tileObject);
                        }
                    }
                }
            }

            // order by distance to explosion center
            var orderedDamageCandidates = damageCandidates.OrderBy(
                ServerExplosionGetDistanceToEpicenter(positionEpicenter, defaultCollisionGroup));

            var hitCharacters = new List <WeaponHitData>();

            // process all damage candidates
            foreach (var damagedObject in orderedDamageCandidates)
            {
                if (!isDamageThroughObstacles &&
                    ServerHasObstacleForExplosion(physicsSpace,
                                                  positionEpicenter,
                                                  damagedObject,
                                                  defaultCollisionGroup))
                {
                    continue;
                }

                var distanceToDamagedObject = ServerCalculateDistanceToDamagedObject(positionEpicenter,
                                                                                     damagedObject);
                var damagePreMultiplier =
                    damagedObject is IDynamicWorldObject
                        ? callbackCalculateDamageCoefByDistanceForDynamicObjects(distanceToDamagedObject)
                        : callbackCalculateDamageCoefByDistanceForStaticObjects(distanceToDamagedObject);

                damagePreMultiplier = MathHelper.Clamp(damagePreMultiplier, 0, 1);

                var damageableProto = (IDamageableProtoWorldObject)damagedObject.ProtoGameObject;
                damageableProto.SharedOnDamage(
                    weaponFinalCache,
                    damagedObject,
                    damagePreMultiplier,
                    damagePostMultiplier: 1.0,
                    out _,
                    out var damageApplied);

                if (damageApplied > 0 &&
                    damagedObject is ICharacter damagedCharacter)
                {
                    hitCharacters.Add(new WeaponHitData(damagedCharacter,
                                                        (0,
                                                         damagedCharacter
                                                         .ProtoCharacter.CharacterWorldWeaponOffsetRanged)));
                }

                if (damageApplied > 0)
                {
                    // give experience for damage
                    protoWeaponSkill?.ServerOnDamageApplied(playerCharacterSkills,
                                                            damagedObject,
                                                            damageApplied);
                }

                weaponFinalCache.ProtoExplosive?.ServerOnObjectHitByExplosion(damagedObject,
                                                                              damageApplied,
                                                                              weaponFinalCache);

                (weaponFinalCache.ProtoWeapon as ProtoItemMobWeaponNova)?
                .ServerOnObjectHitByNova(damagedObject,
                                         damageApplied,
                                         weaponFinalCache);
            }

            if (hitCharacters.Count == 0)
            {
                return;
            }

            // display damages on clients in scope of every damaged object
            var observers = new HashSet <ICharacter>();

            using var tempList = Api.Shared.GetTempList <ICharacter>();

            foreach (var hitObject in hitCharacters)
            {
                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
            var eventNetworkRadius = (byte)Math.Max(
                20,
                Math.Ceiling(1.5 * damageDistanceMax));

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

            if (observers.Count > 0)
            {
                Instance.CallClient(observers,
                                    _ => _.ClientRemote_OnCharactersHitByExplosion(hitCharacters));
            }
        }
Example #7
0
        public static bool SharedOnDamageToCharacter(
            ICharacter targetCharacter,
            WeaponFinalCache weaponCache,
            double damageMultiplier,
            out double damageApplied)
        {
            var targetPublicState  = targetCharacter.GetPublicState <ICharacterPublicState>();
            var targetCurrentStats = targetPublicState.CurrentStats;

            if (targetCurrentStats.HealthCurrent <= 0)
            {
                // target character is dead, cannot apply damage to it
                damageApplied = 0;
                return(false);
            }

            if (Api.IsClient)
            {
                // we don't simulate the damage on the client side
                damageApplied = 0;
                return(true);
            }

            var attackerCharacter = weaponCache.Character;

            // calculate and apply damage on server
            var targetFinalStatsCache =
                targetCharacter.GetPrivateState <BaseCharacterPrivateState>()
                .FinalStatsCache;

            var totalDamage = ServerCalculateTotalDamage(
                weaponCache,
                targetCharacter,
                targetFinalStatsCache,
                damageMultiplier,
                clampDefenseTo1: true);

            if (totalDamage <= 0)
            {
                // damage suppressed by armor
                damageApplied = 0;
                return(true);
            }

            // Clamp the max receivable damage to x5 from the max health.
            // This will help in case when the too much damage is dealt (mega-bomb!)
            // to ensure the equipment will not receive excessive damaged.
            totalDamage = Math.Min(totalDamage, 5 * targetCurrentStats.HealthMax);

            // apply damage
            targetCurrentStats.ServerSetHealthCurrent((float)(targetCurrentStats.HealthCurrent - totalDamage));
            Api.Logger.Info(
                $"Damage applied to {targetCharacter} by {attackerCharacter}:\n{totalDamage} dmg, current health {targetCurrentStats.HealthCurrent}/{targetCurrentStats.HealthMax}, {weaponCache.Weapon}");

            if (targetCurrentStats.HealthCurrent <= 0)
            {
                // killed!
                ServerCharacterDeathMechanic.OnCharacterKilled(
                    targetCharacter,
                    attackerCharacter,
                    weaponCache.Weapon,
                    weaponCache.ProtoWeapon);
            }

            damageApplied = totalDamage;
            ServerApplyDamageToEquippedItems(targetCharacter, damageApplied);

            return(true);
        }
Example #8
0
        private static void SharedShotWeaponHitscan(
            ICharacter character,
            IProtoItemWeapon protoWeapon,
            Vector2D fromPosition,
            WeaponFinalCache weaponCache,
            Vector2D?customTargetPosition,
            IProtoCharacterCore characterProtoCharacter,
            double fireSpreadAngleOffsetDeg,
            CollisionGroup collisionGroup,
            bool isMeleeWeapon,
            IDynamicWorldObject characterCurrentVehicle,
            ProtoSkillWeapons protoWeaponSkill,
            PlayerCharacterSkills playerCharacterSkills,
            ITempList <IWorldObject> allHitObjects)
        {
            Vector2D toPosition;
            var      rangeMax = weaponCache.RangeMax;

            if (customTargetPosition.HasValue)
            {
                var direction = customTargetPosition.Value - fromPosition;
                // ensure the max range is not exceeded
                direction  = direction.ClampMagnitude(rangeMax);
                toPosition = fromPosition + direction;
            }
            else
            {
                toPosition = fromPosition
                             + new Vector2D(rangeMax, 0)
                             .RotateRad(characterProtoCharacter.SharedGetRotationAngleRad(character)
                                        + fireSpreadAngleOffsetDeg * Math.PI / 180.0);
            }

            using var lineTestResults = character.PhysicsBody.PhysicsSpace.TestLine(
                      fromPosition: fromPosition,
                      toPosition: toPosition,
                      collisionGroup: collisionGroup);
            var damageMultiplier    = 1d;
            var hitObjects          = new List <WeaponHitData>(isMeleeWeapon ? 1 : lineTestResults.Count);
            var characterTileHeight = character.Tile.Height;

            if (IsClient ||
                Api.IsEditor)
            {
                SharedEditorPhysicsDebugger.SharedVisualizeTestResults(lineTestResults, collisionGroup);
            }

            var isDamageRayStopped = false;

            foreach (var testResult in lineTestResults.AsList())
            {
                var testResultPhysicsBody = testResult.PhysicsBody;
                var attackedProtoTile     = testResultPhysicsBody.AssociatedProtoTile;
                if (attackedProtoTile != null)
                {
                    if (attackedProtoTile.Kind != TileKind.Solid)
                    {
                        // non-solid obstacle - skip
                        continue;
                    }

                    var attackedTile = IsServer
                                           ? Server.World.GetTile((Vector2Ushort)testResultPhysicsBody.Position)
                                           : Client.World.GetTile((Vector2Ushort)testResultPhysicsBody.Position);

                    if (attackedTile.Height < characterTileHeight)
                    {
                        // attacked tile is below - ignore it
                        continue;
                    }

                    // tile on the way - blocking damage ray
                    isDamageRayStopped = true;
                    var hitData = new WeaponHitData(testResult.PhysicsBody.Position
                                                    + SharedOffsetHitWorldPositionCloserToTileHitboxCenter(
                                                        testResultPhysicsBody,
                                                        testResult.Penetration,
                                                        isRangedWeapon: !isMeleeWeapon));
                    hitObjects.Add(hitData);

                    weaponCache.ProtoWeapon
                    .SharedOnHit(weaponCache,
                                 null,
                                 0,
                                 hitData,
                                 out _);
                    break;
                }

                var damagedObject = testResultPhysicsBody.AssociatedWorldObject;
                if (ReferenceEquals(damagedObject, character) ||
                    ReferenceEquals(damagedObject, characterCurrentVehicle))
                {
                    // ignore collision with self
                    continue;
                }

                if (!(damagedObject.ProtoGameObject is IDamageableProtoWorldObject damageableProto))
                {
                    // shoot through this object
                    continue;
                }

                // don't allow damage is there is no direct line of sight on physical colliders layer between the two objects
                if (SharedHasTileObstacle(character.Position,
                                          characterTileHeight,
                                          damagedObject,
                                          targetPosition: testResult.PhysicsBody.Position
                                          + testResult.PhysicsBody.CenterOffset))
                {
                    continue;
                }

                using (CharacterDamageContext.Create(attackerCharacter: character,
                                                     damagedObject as ICharacter,
                                                     protoWeaponSkill))
                {
                    if (!damageableProto.SharedOnDamage(
                            weaponCache,
                            damagedObject,
                            damageMultiplier,
                            damagePostMultiplier: 1.0,
                            out var obstacleBlockDamageCoef,
                            out var damageApplied))
                    {
                        // not hit
                        continue;
                    }

                    var hitData = new WeaponHitData(damagedObject,
                                                    testResult.Penetration.ToVector2F());
                    weaponCache.ProtoWeapon
                    .SharedOnHit(weaponCache,
                                 damagedObject,
                                 damageApplied,
                                 hitData,
                                 out var isDamageStop);

                    if (isDamageStop)
                    {
                        obstacleBlockDamageCoef = 1;
                    }

                    if (IsServer)
                    {
                        if (damageApplied > 0 &&
                            damagedObject is ICharacter damagedCharacter)
                        {
                            CharacterUnstuckSystem.ServerTryCancelUnstuckRequest(damagedCharacter);
                        }

                        if (damageApplied > 0)
                        {
                            // give experience for damage
                            protoWeaponSkill?.ServerOnDamageApplied(playerCharacterSkills,
                                                                    damagedObject,
                                                                    damageApplied);
                        }
                    }

                    if (obstacleBlockDamageCoef < 0 ||
                        obstacleBlockDamageCoef > 1)
                    {
                        Logger.Error(
                            "Obstacle block damage coefficient should be >= 0 and <= 1 - wrong calculation by "
                            + damageableProto);
                        break;
                    }

                    hitObjects.Add(hitData);

                    if (isMeleeWeapon)
                    {
                        // currently melee weapon could attack only one object on the ray
                        isDamageRayStopped = true;
                        break;
                    }

                    damageMultiplier *= 1.0 - obstacleBlockDamageCoef;
                    if (damageMultiplier <= 0)
                    {
                        // target blocked the damage ray
                        isDamageRayStopped = true;
                        break;
                    }
                }
            }

            var shotEndPosition = GetShotEndPosition(isDamageRayStopped,
                                                     hitObjects,
                                                     toPosition,
                                                     isRangedWeapon: !isMeleeWeapon);

            if (hitObjects.Count == 0)
            {
                protoWeapon.SharedOnMiss(weaponCache,
                                         shotEndPosition);
            }

            SharedCallOnWeaponHitOrTrace(character,
                                         protoWeapon,
                                         weaponCache.ProtoAmmo,
                                         shotEndPosition,
                                         hitObjects,
                                         endsWithHit: isDamageRayStopped);

            foreach (var entry in hitObjects)
            {
                if (!entry.IsCliffsHit &&
                    !allHitObjects.Contains(entry.WorldObject))
                {
                    allHitObjects.Add(entry.WorldObject);
                }
            }
        }
Example #9
0
        public static double ServerCalculateTotalDamage(
            WeaponFinalCache weaponCache,
            IWorldObject targetObject,
            FinalStatsCache targetFinalStatsCache,
            double damagePreMultiplier,
            bool clampDefenseTo1)
        {
            if (targetObject is IStaticWorldObject staticWorldObject &&
                (!RaidingProtectionSystem.SharedCanRaid(staticWorldObject,
                                                        showClientNotification: false) ||
                 !PveSystem.SharedIsAllowStructureDamage(weaponCache.Character,
                                                         staticWorldObject,
                                                         showClientNotification: false)))
            {
                return(0);
            }

            if (weaponCache.ProtoObjectExplosive != null &&
                targetObject.ProtoWorldObject is IProtoStaticWorldObject targetStaticWorldObjectProto)
            {
                // special case - apply the explosive damage
                return(ServerCalculateTotalDamageByExplosive(weaponCache.ProtoObjectExplosive,
                                                             targetStaticWorldObjectProto,
                                                             damagePreMultiplier));
            }

            // these two cases apply only if damage dealt not by a bomb
            if (ServerIsRestrictedPvPDamage(weaponCache,
                                            targetObject,
                                            out var isPvPcase,
                                            out var isFriendlyFireCase))
            {
                return(0);
            }

            var damageValue = damagePreMultiplier * weaponCache.DamageValue;
            var invertedArmorPiercingCoef = weaponCache.InvertedArmorPiercingCoef;

            var totalDamage = 0d;

            // calculate total damage by summing all the damage components
            foreach (var damageDistribution in weaponCache.DamageDistributions)
            {
                var defenseStatName = SharedGetDefenseStatName(damageDistribution.DamageType);
                var defenseFraction = targetFinalStatsCache[defenseStatName];
                defenseFraction = MathHelper.Clamp(defenseFraction, 0, clampDefenseTo1 ? 1 : double.MaxValue);

                totalDamage += ServerCalculateDamageComponent(
                    damageValue,
                    invertedArmorPiercingCoef,
                    damageDistribution,
                    defenseFraction);
            }

            // multiply on final multiplier (usually used for expanding projectiles)
            totalDamage *= weaponCache.FinalDamageMultiplier;

            var damagingCharacter = weaponCache.Character;

            if (isPvPcase)
            {
                // apply PvP damage multiplier
                totalDamage *= WeaponConstants.DamagePvpMultiplier;
            }
            else if (damagingCharacter != null &&
                     !damagingCharacter.IsNpc)
            {
                // apply PvE damage multiplier
                totalDamage *= WeaponConstants.DamagePveMultiplier;
            }
            else if (damagingCharacter != null &&
                     damagingCharacter.IsNpc)
            {
                // apply creature damage multiplier
                totalDamage *= WeaponConstants.DamageCreaturesMultiplier;

                if (targetObject is ICharacter victim &&
                    !victim.ServerIsOnline &&
                    !victim.IsNpc)
                {
                    // don't deal creature damage to offline players
                    totalDamage = 0;
                }
            }

            if (isFriendlyFireCase)
            {
                totalDamage *= WeaponConstants.DamageFriendlyFireMultiplier;
            }

            return(totalDamage);
        }
Example #10
0
        public static bool SharedOnDamageToCharacter(
            ICharacter targetCharacter,
            WeaponFinalCache weaponCache,
            double damageMultiplier,
            out double damageApplied)
        {
            var targetPublicState  = targetCharacter.GetPublicState <ICharacterPublicState>();
            var targetCurrentStats = targetPublicState.CurrentStats;

            if (targetCurrentStats.HealthCurrent <= 0)
            {
                // target character is dead, cannot apply damage to it
                damageApplied = 0;
                return(false);
            }

            {
                if (!targetCharacter.IsNpc &&
                    weaponCache.Character is ICharacter damagingCharacter &&
                    NewbieProtectionSystem.SharedIsNewbie(damagingCharacter))
                {
                    // no damage from newbie
                    damageApplied = 0;
                    if (Api.IsClient)
                    {
                        // display message to newbie
                        NewbieProtectionSystem.ClientShowNewbieCannotDamageOtherPlayersOrLootBags(isLootBag: false);
                    }

                    // but the hit is registered so it's not possible to shoot through a character
                    return(true);
                }
            }

            if (Api.IsClient)
            {
                // we don't simulate the damage on the client side
                damageApplied = 0;

                if (weaponCache.Character is ICharacter damagingCharacter)
                {
                    // potentially a PvP case
                    PveSystem.ClientShowDuelModeRequiredNotificationIfNecessary(
                        damagingCharacter,
                        targetCharacter);
                }

                return(true);
            }

            var attackerCharacter = weaponCache.Character;

            if (!(attackerCharacter is null) &&
                attackerCharacter.IsNpc &&
                targetCharacter.IsNpc)
            {
                // no creature-to-creature damage
                damageApplied = 0;
                return(false);
            }

            // calculate and apply damage on server
            var targetFinalStatsCache =
                targetCharacter.GetPrivateState <BaseCharacterPrivateState>()
                .FinalStatsCache;

            var totalDamage = ServerCalculateTotalDamage(
                weaponCache,
                targetCharacter,
                targetFinalStatsCache,
                damageMultiplier,
                clampDefenseTo1: true);

            if (totalDamage <= 0)
            {
                // damage suppressed
                damageApplied = 0;
                return(true);
            }

            // Clamp the max receivable damage to x5 from the max health.
            // This will help in case when the too much damage is dealt (mega-bomb!)
            // to ensure the equipment will not receive excessive damaged.
            totalDamage = Math.Min(totalDamage, 5 * targetCurrentStats.HealthMax);

            // apply damage
            if (!(attackerCharacter is null))
            {
                targetCurrentStats.ServerReduceHealth(totalDamage, damageSource: attackerCharacter);
            }