private void ClientRemote_OnWeaponHit(IProtoItemWeapon protoWeapon, WeaponHitData hitObject) { using (var tempList = Api.Shared.WrapObjectInTempList(hitObject)) { WeaponSystemClientDisplay.OnWeaponHit(protoWeapon, tempList); } }
static Vector2D CalculateWorldObjectPosition(IWorldObject worldObject, WeaponHitData hitData) { return(worldObject switch { IDynamicWorldObject dynamicWorldObject => dynamicWorldObject.Position, IStaticWorldObject => worldObject.TilePosition.ToVector2D(), _ => hitData.FallbackTilePosition.ToVector2D() });
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); }
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); }
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)); } }
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); }
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); } } }