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); }
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 _); } } }
/// <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); } } } }
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 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); }
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); } } }
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); }
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); }