public static bool SharedHasObstaclesInTheWay( Vector2D fromPosition, IPhysicsSpace physicsSpace, IWorldObject worldObject, bool sendDebugEvents) { var worldObjectCenter = SharedGetWorldObjectCenter(worldObject); var worldObjectPointClosestToCharacter = worldObject.PhysicsBody.ClampPointInside( fromPosition, CollisionGroups.Default, out var isSuccess); if (!isSuccess) { // the physics body seems to not have the default collider, let's check for the click area instead worldObjectPointClosestToCharacter = worldObject.PhysicsBody.ClampPointInside( fromPosition, CollisionGroups.ClickArea, out _); } return(SharedHasObstaclesInTheWay(fromPosition, physicsSpace, worldObject, worldObjectCenter, worldObjectPointClosestToCharacter, sendDebugEvents, CollisionGroups.Default));
protected virtual void ServerExecuteVehicleExplosion( Vector2D positionEpicenter, IPhysicsSpace physicsSpace, WeaponFinalCache weaponFinalCache) { WeaponExplosionSystem.ServerProcessExplosionCircle( positionEpicenter: positionEpicenter, physicsSpace: physicsSpace, damageDistanceMax: this.DestroyedVehicleDamageRadius, weaponFinalCache: weaponFinalCache, damageOnlyDynamicObjects: false, isDamageThroughObstacles: false, callbackCalculateDamageCoefByDistanceForStaticObjects: CalcDamageCoefByDistance, callbackCalculateDamageCoefByDistanceForDynamicObjects: CalcDamageCoefByDistance); double CalcDamageCoefByDistance(double distance) { var distanceThreshold = 0.5; if (distance <= distanceThreshold) { return(1); } distance -= distanceThreshold; distance = Math.Max(0, distance); var maxDistance = this.DestroyedVehicleDamageRadius; maxDistance -= distanceThreshold; maxDistance = Math.Max(0, maxDistance); return(1 - Math.Min(distance / maxDistance, 1)); } }
public static bool SharedHasObstaclesOnTheWay( ICharacter character, Vector2D characterCenter, IPhysicsSpace physicsSpace, IWorldObject worldObject, bool sendDebugEvents) { var worldObjectCenter = worldObject.TilePosition.ToVector2D() + worldObject.PhysicsBody.CenterOffset; var worldObjectPointClosestToCharacter = worldObject.PhysicsBody.ClampPointInside( characterCenter, CollisionGroups.Default, out var isSuccess); if (!isSuccess) { // the physics body seems to not have the default collider, let's check for the click area instead worldObjectPointClosestToCharacter = worldObject.PhysicsBody.ClampPointInside( characterCenter, CollisionGroups.ClickArea, out _); } return(SharedHasObstaclesOnTheWay(character, characterCenter, physicsSpace, worldObject, worldObjectCenter, worldObjectPointClosestToCharacter, sendDebugEvents)); }
public static void ServerExplode( [CanBeNull] ICharacter character, [CanBeNull] IProtoObjectExplosive protoObjectExplosive, ExplosionPreset explosionPreset, Vector2D epicenterPosition, DamageDescription damageDescriptionCharacters, IPhysicsSpace physicsSpace, ExecuteExplosionDelegate executeExplosionCallback) { ValidateIsServer(); // schedule explosion charred ground spawning ServerTimersSystem.AddAction( // the delay is quite small and just needed to ensure // that the charred ground spawned some time after this object is destroyed delaySeconds: explosionPreset.SpriteAnimationDuration * 0.5, () => { var tilePosition = (Vector2Ushort)epicenterPosition; if (!Server.World.GetTile(tilePosition) .StaticObjects .Any(so => so.ProtoStaticWorldObject is ObjectCharredGround)) { // spawn charred ground var objectCharredGround = Server.World.CreateStaticWorldObject <ObjectCharredGround>(tilePosition); var objectCharredGroundOffset = epicenterPosition - tilePosition.ToVector2D(); if (objectCharredGroundOffset != Vector2D.Zero) { ObjectCharredGround.ServerSetWorldOffset(objectCharredGround, objectCharredGroundOffset.ToVector2F()); } } }); // schedule explosion damage ServerTimersSystem.AddAction( delaySeconds: explosionPreset.ServerDamageApplyDelay, () => { // prepare weapon caches var characterFinalStatsCache = character != null ? character.SharedGetFinalStatsCache() : FinalStatsCache.Empty; var weaponFinalCache = new WeaponFinalCache(character, characterFinalStatsCache, weapon: null, protoWeapon: null, protoObjectExplosive: protoObjectExplosive, damageDescription: damageDescriptionCharacters); // execute explosion executeExplosionCallback( positionEpicenter: epicenterPosition, physicsSpace: physicsSpace, weaponFinalCache: weaponFinalCache); }); }
protected virtual void ServerExecuteExplosion( Vector2D positionEpicenter, IPhysicsSpace physicsSpace, WeaponFinalCache weaponFinalCache) { WeaponExplosionSystem.ServerProcessExplosionCircle( positionEpicenter: positionEpicenter, physicsSpace: physicsSpace, damageDistanceMax: this.DamageRadius, weaponFinalCache: weaponFinalCache, damageOnlyDynamicObjects: false, callbackCalculateDamageCoefByDistance: this.ServerCalculateDamageCoefByDistance); }
// Ensure that player can hit objects only on the same height level // and can fire through over the pits (the cliffs of the lower heights). public static bool SharedHasTileObstacle( Vector2D fromPosition, byte characterTileHeight, Vector2D targetPosition, IPhysicsSpace physicsSpace, bool anyCliffIsAnObstacle) { using var testResults = physicsSpace.TestLine( fromPosition, targetPosition, collisionGroup: CollisionGroups.Default, sendDebugEvent: false); foreach (var testResult in testResults.AsList()) { var testResultPhysicsBody = testResult.PhysicsBody; var protoTile = testResultPhysicsBody.AssociatedProtoTile; if (protoTile is null || protoTile.Kind != TileKind.Solid) { continue; } var testResultPosition = testResultPhysicsBody.Position; var attackedTile = IsServer ? Server.World.GetTile((Vector2Ushort)testResultPosition) : Client.World.GetTile((Vector2Ushort)testResultPosition); if (attackedTile.IsSlope) { // slope is not an obstacle continue; } // found collision with a cliff if (anyCliffIsAnObstacle) { return(true); } if (attackedTile.Height >= characterTileHeight) { return(true); // cliff to higher tile is always an obstacle } } return(false); // no obstacles }
public static bool SharedHasObstaclesOnTheWay( ICharacter character, Vector2D characterCenter, IPhysicsSpace physicsSpace, Vector2D worldObjectCenter, Vector2D worldObjectPointClosestToCharacter, bool sendDebugEvents) { return(SharedHasObstaclesOnTheWay(character, characterCenter, physicsSpace, worldObject: null, worldObjectCenter, worldObjectPointClosestToCharacter, sendDebugEvents)); }
protected override void ServerExecuteExplosion( Vector2D positionEpicenter, IPhysicsSpace physicsSpace, WeaponFinalCache weaponFinalCache) { WeaponExplosionSystem.ServerProcessExplosionBomberman( positionEpicenter: positionEpicenter, physicsSpace: physicsSpace, damageDistanceFullDamage: DamageRadiusFullDamage, damageDistanceMax: DamageRadiusMax, damageDistanceDynamicObjectsOnly: DamageRadiusDynamicObjectsOnly, weaponFinalCache: weaponFinalCache, callbackCalculateDamageCoefByDistanceForStaticObjects: this.ServerCalculateDamageCoefByDistance, callbackCalculateDamageCoefByDistanceForDynamicObjects: ServerCalculateDamageCoefByDistanceForDynamicObjects); }
public override void ServerExecuteExplosion( Vector2D positionEpicenter, IPhysicsSpace physicsSpace, WeaponFinalCache weaponFinalCache) { WeaponExplosionSystem.ServerProcessExplosionCircle( positionEpicenter: positionEpicenter, physicsSpace: physicsSpace, damageDistanceMax: this.DamageRadius, weaponFinalCache: weaponFinalCache, damageOnlyDynamicObjects: true, isDamageThroughObstacles: this.IsDamageThroughObstacles, callbackCalculateDamageCoefByDistanceForStaticObjects: this.ServerCalculateDamageCoefByDistanceForStaticObjects, callbackCalculateDamageCoefByDistanceForDynamicObjects: this .ServerCalculateDamageCoefByDistanceForDynamicObjects, // Missiles are falling from the sky and the explosion circles are clearly designated. // Players expecting that they will be not damaged when they stand outside the circles. collisionGroups: new[] { CollisionGroup.Default }); }
protected static bool ServerCheckAnyTileCollisions( IPhysicsSpace physicsSpace, Vector2D centerLocation, double radius) { foreach (var result in physicsSpace.TestCircle( centerLocation, radius, collisionGroup: CollisionGroups.Default, sendDebugEvent: false).EnumerateAndDispose()) { if (result.PhysicsBody.AssociatedProtoTile != null) { // collision with a tile found - probably a cliff or water // avoid spawning there return(false); } } return(true); }
public override void ServerExecuteExplosion( Vector2D positionEpicenter, IPhysicsSpace physicsSpace, WeaponFinalCache weaponFinalCache) { WeaponExplosionSystem.ServerProcessExplosionCircle( positionEpicenter: positionEpicenter, physicsSpace: physicsSpace, damageDistanceMax: this.DamageRadius, weaponFinalCache: weaponFinalCache, damageOnlyDynamicObjects: true, isDamageThroughObstacles: this.IsDamageThroughObstacles, callbackCalculateDamageCoefByDistanceForStaticObjects: this.ServerCalculateDamageCoefByDistanceForStaticObjects, callbackCalculateDamageCoefByDistanceForDynamicObjects: this .ServerCalculateDamageCoefByDistanceForDynamicObjects, // Missiles are falling from the sky and the explosion circles are clearly designated. // Players are expecting that they will be not damaged when they stand outside the circles. collisionGroups: new[] { CollisionGroups.Default }); // this is only for hoverboards as hoverboards on the ground have no physical collider WeaponExplosionSystem.ServerProcessExplosionCircle( positionEpicenter: positionEpicenter, physicsSpace: physicsSpace, damageDistanceMax: this.DamageRadius, weaponFinalCache: weaponFinalCache, damageOnlyDynamicObjects: true, isDamageThroughObstacles: this.IsDamageThroughObstacles, callbackCalculateDamageCoefByDistanceForStaticObjects: this.ServerCalculateDamageCoefByDistanceForStaticObjects, callbackCalculateDamageCoefByDistanceForDynamicObjects: this .ServerCalculateDamageCoefByDistanceForDynamicObjects, // this is only for hoverboards collisionGroups: new[] { CollisionGroups.HitboxMelee }, // can damage only hoverboards filterCanDamage: worldObject => worldObject.ProtoGameObject is IProtoVehicleHoverboard); }
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 _); } } }
public static void ServerExplode( [CanBeNull] ICharacter character, [CanBeNull] IProtoObjectExplosive protoObjectExplosive, ExplosionPreset explosionPreset, Vector2D epicenterPosition, DamageDescription damageDescriptionCharacters, IPhysicsSpace physicsSpace, ExecuteExplosionDelegate executeExplosionCallback) { ValidateIsServer(); // schedule explosion charred ground spawning ServerTimersSystem.AddAction( delaySeconds: explosionPreset.SpriteAnimationDuration * 0.5, () => { var tilePosition = (Vector2Ushort)epicenterPosition; // remove existing charred ground objects at the same tile foreach (var staticWorldObject in Shared.WrapInTempList( Server.World.GetTile(tilePosition).StaticObjects)) { if (staticWorldObject.ProtoStaticWorldObject is ObjectCharredGround) { Server.World.DestroyObject(staticWorldObject); } } // spawn charred ground var objectCharredGround = Server.World .CreateStaticWorldObject <ObjectCharredGround>(tilePosition); var objectCharredGroundOffset = epicenterPosition - tilePosition.ToVector2D(); if (objectCharredGroundOffset != Vector2D.Zero) { ObjectCharredGround.ServerSetWorldOffset(objectCharredGround, (Vector2F)objectCharredGroundOffset); } }); // schedule explosion damage ServerTimersSystem.AddAction( delaySeconds: explosionPreset.ServerDamageApplyDelay, () => { // prepare weapon caches var characterFinalStatsCache = character?.SharedGetFinalStatsCache() ?? FinalStatsCache.Empty; var weaponFinalCache = new WeaponFinalCache(character, characterFinalStatsCache, weapon: null, protoWeapon: null, protoObjectExplosive: protoObjectExplosive, damageDescription: damageDescriptionCharacters); // execute explosion executeExplosionCallback( positionEpicenter: epicenterPosition, physicsSpace: physicsSpace, weaponFinalCache: 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 void ServerExplode( [CanBeNull] ICharacter character, [CanBeNull] IProtoExplosive protoExplosive, [CanBeNull] IProtoItemWeapon protoWeapon, ExplosionPreset explosionPreset, Vector2D epicenterPosition, DamageDescription damageDescriptionCharacters, IPhysicsSpace physicsSpace, ExecuteExplosionDelegate executeExplosionCallback) { ValidateIsServer(); // schedule explosion charred ground spawning var protoObjectCharredGround = explosionPreset.ProtoObjectCharredGround; if (protoObjectCharredGround is not null) { ServerTimersSystem.AddAction( delaySeconds: explosionPreset.SpriteAnimationDuration * 0.5, () => { var tilePosition = (Vector2Ushort)(epicenterPosition - protoObjectCharredGround.Layout.Center); var canSpawnCharredGround = true; var tile = Server.World.GetTile(tilePosition); if (tile.ProtoTile.Kind != TileKind.Solid || tile.EightNeighborTiles.Any(t => t.ProtoTile.Kind != TileKind.Solid)) { // allow charred ground only on solid ground canSpawnCharredGround = false; } if (canSpawnCharredGround) { // remove existing charred ground objects at the same tile foreach (var staticWorldObject in Shared.WrapInTempList( tile.StaticObjects) .EnumerateAndDispose()) { switch (staticWorldObject.ProtoStaticWorldObject) { case ProtoObjectCharredGround _: Server.World.DestroyObject(staticWorldObject); break; case IProtoObjectDeposit _: // don't show charred ground over resource deposits (it looks wrong) canSpawnCharredGround = false; break; } } } if (canSpawnCharredGround && PveSystem.ServerIsPvE) { var bounds = protoObjectCharredGround.Layout.Bounds; if (LandClaimSystem.SharedIsLandClaimedByAnyone( new RectangleInt(tilePosition, bounds.Size + (1, 1)))) { // ensure that it's not possible to create charred ground in a land claim area in PvE canSpawnCharredGround = false; } } if (canSpawnCharredGround) { // spawn charred ground var objectCharredGround = Server.World.CreateStaticWorldObject(protoObjectCharredGround, tilePosition); var objectCharredGroundOffset = epicenterPosition - tilePosition.ToVector2D(); if (objectCharredGroundOffset != Vector2D.Zero) { ProtoObjectCharredGround.ServerSetWorldOffset(objectCharredGround, (Vector2F)objectCharredGroundOffset); } } }); } // schedule explosion damage ServerTimersSystem.AddAction( delaySeconds: explosionPreset.ServerDamageApplyDelay, () => { // prepare weapon caches var characterFinalStatsCache = character?.SharedGetFinalStatsCache() ?? FinalStatsCache.Empty; var weaponFinalCache = new WeaponFinalCache(character, characterFinalStatsCache, weapon: null, protoWeapon: protoWeapon, protoAmmo: null, damageDescription: damageDescriptionCharacters, protoExplosive: protoExplosive); // execute explosion executeExplosionCallback( positionEpicenter: epicenterPosition, physicsSpace: physicsSpace, weaponFinalCache: weaponFinalCache); }); }
/// <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 _); } } }
private static bool ServerHasObstacleForExplosion( IPhysicsSpace physicsSpace, Vector2D positionEpicenter, IWorldObject targetWorldObject) { //// doesn't work as expected due to the penetration resolution not implemented yet //// for the line segment collision with other shapes //// get closest point to the explosion epicenter ////var targetPosition = targetWorldObject.PhysicsBody.ClampPointInside( //// positionEpicenter, //// CollisionGroups.Default); // //var targetPosition = targetWorldObject.PhysicsBody.Position // + targetWorldObject.PhysicsBody.CenterOffset; if (targetWorldObject.PhysicsBody == null) { return(false); } var targetPosition = ServerGetClosestPointToExplosionEpicenter(targetWorldObject.PhysicsBody, positionEpicenter); using (var obstaclesOnTheWay = physicsSpace.TestLine( positionEpicenter, targetPosition, collisionGroup: CollisionGroups.HitboxRanged)) { //obstaclesOnTheWay.SortBy( // ServerExplosionGetDistanceToEpicenter(positionEpicenter)); foreach (var testResult in obstaclesOnTheWay) { var testPhysicsBody = testResult.PhysicsBody; if (testPhysicsBody.AssociatedProtoTile != null) { // obstacle tile on the way return(true); } var testWorldObject = testPhysicsBody.AssociatedWorldObject; if (testWorldObject == targetWorldObject) { // not an obstacle - it's the target object itself // stop checking collisions as we've reached the target object return(false); } if (testWorldObject is ICharacter) { // not an obstacle - character is not considered as an obstacle for the explosion continue; } if (testWorldObject.ProtoWorldObject is IDamageableProtoWorldObject damageableProtoWorldObject && damageableProtoWorldObject.ObstacleBlockDamageCoef < 1) { // damage goes through continue; } // obstacle object on the way return(true); } return(false); } }
/// <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); } } } }
private static bool ServerIsCanSpawn( SpawnRequest spawnRequest, ObjectSpawnPreset preset, IReadOnlyDictionary <Vector2Ushort, SpawnZoneArea> spawnZoneAreas, Vector2Ushort spawnPosition, IPhysicsSpace physicsSpace, out SpawnZoneArea resultSpawnArea, out bool isSectorDensityExceeded) { if (ServerWorldService.GetTile(spawnPosition) .IsCliffOrSlope) { // quick discard - don't spawn on cliff or slope resultSpawnArea = null; isSectorDensityExceeded = false; return(false); } var presetPadding = preset.Padding; var presetCustomObjectPadding = preset.CustomObjectPadding; resultSpawnArea = null; var resultSpawnAreaStartPosition = SpawnZoneArea.CalculateStartPosition(spawnPosition); foreach (var area in EnumerateAdjacentZoneAreas(resultSpawnAreaStartPosition, spawnZoneAreas)) { foreach (var nearbyPreset in area.WorldObjectsByPreset) { var nearbyObjectPreset = nearbyPreset.Key; double padding; if (nearbyObjectPreset != null) { // preset found if (presetCustomObjectPadding.TryGetValue(nearbyObjectPreset, out padding)) { // use custom padding value } else { // don't have custom padding padding = Math.Max(presetPadding, nearbyObjectPreset.Padding); } } else { // preset of another object not defined in this spawn list - use default object spawn padding padding = DefaultObjectSpawnPadding; } foreach (var nearbyObjectTilePosition in nearbyPreset.Value) { var distance = spawnPosition.TileSqrDistanceTo(nearbyObjectTilePosition); // Actually using < will be more correct, but it will produce not so nice-looking result // (objects could touch each other on each side). // So we insist objects must don't even touch each other on their // left/up/right/down edges (but the diagonal corners touch is ok). if (distance <= padding * padding) { // too close isSectorDensityExceeded = false; return(false); } } } if (resultSpawnAreaStartPosition == area.StartPosition) { resultSpawnArea = area; } } var needToCheckLandClaimPresence = true; if (preset.IsContainsOnlyStaticObjects) { needToCheckLandClaimPresence = !ServerWorldService.GetTile(spawnPosition) .ProtoTile .IsRestrictingConstruction; } if (needToCheckLandClaimPresence && ServerCheckLandClaimAreaPresence(spawnPosition, preset.PaddingToLandClaimAreas)) { // the land is claimed by players resultSpawnArea = null; isSectorDensityExceeded = false; return(false); } if (preset.CustomCanSpawnCheckCallback != null && !preset.CustomCanSpawnCheckCallback(physicsSpace, spawnPosition)) { // custom spawn check failed resultSpawnArea = null; isSectorDensityExceeded = false; return(false); } if (resultSpawnArea == null) { // no area exist (will be created) isSectorDensityExceeded = false; return(true); } if (!spawnRequest.UseSectorDensity) { isSectorDensityExceeded = false; return(true); } // ensure that the area/sector density is not exceeded for this spawn preset isSectorDensityExceeded = IsAreaLocalDensityExceeded(spawnRequest, preset, resultSpawnArea, out var countToSpawnRemains); if (isSectorDensityExceeded) { // already spawned too many objects of the required type in the area return(false); } if (countToSpawnRemains < 1) { // density allows to spawn an extra object of this type with some small probability if (!RandomHelper.RollWithProbability(countToSpawnRemains)) { return(false); } } return(true); }
public static bool SharedHasObstaclesInTheWay( Vector2D fromPosition, IPhysicsSpace physicsSpace, [CanBeNull] IWorldObject worldObject, Vector2D worldObjectCenter, Vector2D worldObjectPointClosestToCharacter, bool sendDebugEvents, CollisionGroup collisionGroup) { // let's test by casting rays from "fromPosition" (usually it's the character's center) to: // 0) world object center // 1) world object point closest to the character // 2) combined - take X from center, take Y from closest // 3) combined - take X from closest, take Y from center if (TestHasObstacle(worldObjectCenter) && TestHasObstacle(worldObjectPointClosestToCharacter) && TestHasObstacle((worldObjectCenter.X, worldObjectPointClosestToCharacter.Y)) && TestHasObstacle((worldObjectPointClosestToCharacter.X, worldObjectCenter.Y))) { // has obstacle return(true); } return(false); // local method for testing if there is an obstacle from current to the specified position bool TestHasObstacle(Vector2D toPosition) { using var obstaclesInTheWay = physicsSpace.TestLine( fromPosition, toPosition, collisionGroup, sendDebugEvent: sendDebugEvents); foreach (var test in obstaclesInTheWay.AsList()) { var testPhysicsBody = test.PhysicsBody; if (testPhysicsBody.AssociatedProtoTile is not null) { // obstacle tile on the way return(true); } var testWorldObject = testPhysicsBody.AssociatedWorldObject; if (ReferenceEquals(testWorldObject, worldObject)) { // not an obstacle - it's the target object itself // stop checking collisions as we've reached the target object return(false); } if (testWorldObject is IDynamicWorldObject) { // dynamic world objects are not assumed as an obstacle continue; } // no need for this check anymore as we're checking for general "is ICharacter" above //if (ReferenceEquals(testWorldObject, character)) //{ // // not an obstacle - it's the player's character itself // continue; //} if (!testWorldObject.ProtoWorldObject .SharedIsAllowedObjectToInteractThrough(testWorldObject)) { // obstacle object on the way return(true); } } // no obstacles return(false); } }