Example #1
0
 private void ClientRemote_OnWeaponHit(IProtoItemWeapon protoWeapon, WeaponHitData hitObject)
 {
     using (var tempList = Api.Shared.WrapObjectInTempList(hitObject))
     {
         WeaponSystemClientDisplay.OnWeaponHit(protoWeapon, tempList);
     }
 }
Example #2
0
 static Vector2D CalculateWorldObjectPosition(IWorldObject worldObject, WeaponHitData hitData)
 {
     return(worldObject switch
     {
         IDynamicWorldObject dynamicWorldObject => dynamicWorldObject.Position,
         IStaticWorldObject => worldObject.TilePosition.ToVector2D(),
         _ => hitData.FallbackTilePosition.ToVector2D()
     });
Example #3
0
        private void Setup(
            WeaponFireTracePreset weaponTracePreset,
            IComponentSpriteRenderer componentSpriteRender,
            Vector2D worldPositionSource,
            Vector2D normalizedRay,
            double fireDistance,
            double totalDuration,
            bool hasHit,
            WeaponHitData hitData)
        {
            this.weaponTracePreset     = weaponTracePreset;
            this.componentSpriteRender = componentSpriteRender;
            this.worldPositionSource   = worldPositionSource;
            this.normalizedRay         = normalizedRay;
            this.fireDistance          = fireDistance;
            this.totalDuration         = totalDuration;
            this.hasHit  = hasHit;
            this.hitData = hitData;

            this.LateUpdate(0);
        }
Example #4
0
        public static void ClientAddHitSparks(
            IReadOnlyWeaponHitSparksPreset hitSparksPreset,
            WeaponHitData hitData,
            IWorldObject hitWorldObject,
            IProtoWorldObject protoWorldObject,
            Vector2D worldObjectPosition,
            int projectilesCount,
            ObjectMaterial objectMaterial,
            bool randomizeHitPointOffset,
            bool randomRotation,
            DrawOrder drawOrder,
            double scale = 1.0,
            double animationFrameDuration = 2 / 60.0)
        {
            var sceneObject = Api.Client.Scene.CreateSceneObject("Temp_HitSparks");

            sceneObject.Position = worldObjectPosition;
            var hitPoint = hitData.HitPoint.ToVector2D();

            if (!hitData.IsCliffsHit)
            {
                // move hitpoint a bit closer to the center of the object
                hitPoint = WeaponSystem.SharedOffsetHitWorldPositionCloserToObjectCenter(
                    hitWorldObject,
                    protoWorldObject,
                    hitPoint,
                    isRangedWeapon: randomizeHitPointOffset);
            }

            var sparksEntry = hitSparksPreset.GetForMaterial(objectMaterial);

            if (projectilesCount == 1 &&
                randomizeHitPointOffset &&
                sparksEntry.AllowRandomizedHitPointOffset)
            {
                // randomize hitpoint a bit by adding a little random offset
                var maxOffsetDistance = 0.2;
                var range             = maxOffsetDistance * RandomHelper.NextDouble();
                var angleRad          = 2 * Math.PI * RandomHelper.NextDouble();
                var randomOffset      = new Vector2D(range * Math.Cos(angleRad),
                                                     range * Math.Sin(angleRad));

                hitPoint += randomOffset;
            }

            var componentSpriteRender = Api.Client.Rendering.CreateSpriteRenderer(
                sceneObject,
                positionOffset: hitPoint,
                spritePivotPoint: (0.5, sparksEntry.PivotY),
                drawOrder: drawOrder);

            componentSpriteRender.DrawOrderOffsetY = -hitPoint.Y;
            componentSpriteRender.Scale            = (float)scale * Math.Pow(1.0 / projectilesCount, 0.35);

            if (sparksEntry.UseScreenBlending)
            {
                componentSpriteRender.BlendMode = BlendMode.Screen;
            }

            if (randomRotation)
            {
                componentSpriteRender.RotationAngleRad = (float)(RandomHelper.NextDouble() * 2 * Math.PI);
            }

            var componentAnimator = sceneObject.AddComponent <ClientComponentSpriteSheetAnimator>();
            var hitSparksEntry    = sparksEntry;

            componentAnimator.Setup(
                componentSpriteRender,
                hitSparksEntry.SpriteSheetAnimationFrames,
                frameDurationSeconds: animationFrameDuration,
                isLooped: false);

            var totalAnimationDuration = animationFrameDuration * componentAnimator.FramesCount;
            var totalDurationWithLight = 0.15 + totalAnimationDuration;

            if (hitSparksEntry.LightColor.HasValue)
            {
                // create light spot (even for melee weapons)
                var lightSource = ClientLighting.CreateLightSourceSpot(
                    sceneObject,
                    color: hitSparksEntry.LightColor.Value,
                    spritePivotPoint: (0.5, 0.5),
                    size: 7,
                    // we don't want to display nickname/healthbar for the firing character, it's too quick anyway
                    logicalSize: 0,
                    positionOffset: hitPoint);

                ClientComponentOneShotLightAnimation.Setup(lightSource, totalDurationWithLight);
            }

            componentSpriteRender.Destroy(totalAnimationDuration);
            componentAnimator.Destroy(totalAnimationDuration);

            sceneObject.Destroy(totalDurationWithLight);
        }
Example #5
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 #6
0
        public static void Create(
            WeaponFireTracePreset weaponTracePreset,
            Vector2D worldPositionSource,
            Vector2D endPosition,
            WeaponHitData lastHitData,
            bool hasHit)
        {
            if (weaponTracePreset is null)
            {
                // no weapon trace for this weapon
                return;
            }

            var deltaPos     = endPosition - worldPositionSource;
            var fireDistance = CalculateFireDistance(weaponTracePreset, deltaPos);

            if (fireDistance <= weaponTracePreset.TraceMinDistance)
            {
                return;
            }

            CalculateAngleAndDirection(deltaPos,
                                       out var angleRad,
                                       out var normalizedRay);

            // offset start position of the ray
            worldPositionSource += normalizedRay * weaponTracePreset.TraceStartWorldOffset;

            // actual trace life duration is larger when has a hit
            // (to provide a contact fade-out animation for the sprite length)
            if (!hasHit)
            {
                // otherwise it's shorter on the sprite length
                fireDistance -= weaponTracePreset.TraceWorldLength;
            }

            var totalDuration = WeaponSystemClientDisplay.SharedCalculateTimeToHit(fireDistance, weaponTracePreset);

            var sceneObject           = Api.Client.Scene.CreateSceneObject("Temp_WeaponTrace");
            var componentSpriteRender = Api.Client.Rendering.CreateSpriteRenderer(
                sceneObject,
                weaponTracePreset.TraceTexture,
                positionOffset: Vector2D.Zero,
                spritePivotPoint: (0, 0.5),
                // yes, it's actually making the weapon trace to draw in the light layer!
                drawOrder: DrawOrder.Light);

            componentSpriteRender.RotationAngleRad = (float)angleRad;
            componentSpriteRender.BlendMode        = weaponTracePreset.UseScreenBlending
                                                  ? BlendMode.Screen
                                                     // it's important to use premultiplied mode here for correct rendering
                                                  : BlendMode.AlphaBlendPremultiplied;

            sceneObject.AddComponent <ComponentWeaponTrace>()
            .Setup(weaponTracePreset,
                   componentSpriteRender,
                   worldPositionSource,
                   normalizedRay,
                   fireDistance,
                   totalDuration,
                   hasHit,
                   lastHitData);

            sceneObject.Destroy(totalDuration);
        }
Example #7
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);
                }
            }
        }