private void TimerTickCallback() { if (IsClient) { ClientTimersSystem.AddAction(CheckTimeIntervalSeconds, this.TimerTickCallback); } RegisteredActions.ProcessAndRemove( removeCondition: pair => { if (IsServer) { CharacterUnstuckSystem.ServerTryCancelUnstuckRequest(pair.Character); } // remove if cannot interact return(!SharedIsValidInteraction(pair)); }, // abort action when pair removed due to the interaction check failed removeCallback: pair => pair.Value.FinishAction?.Invoke(isAbort: true)); }
private static void SharedFireWeapon( ICharacter character, IItem weaponItem, IProtoItemWeapon protoWeapon, WeaponState weaponState) { protoWeapon.SharedOnFire(character, weaponState); var playerCharacterSkills = character.SharedGetSkills(); var protoWeaponSkill = playerCharacterSkills != null ? protoWeapon.WeaponSkillProto : null; if (IsServer) { protoWeaponSkill?.ServerOnShot(playerCharacterSkills); // give experience for shot CharacterUnstuckSystem.ServerTryCancelUnstuckRequest(character); } var weaponCache = weaponState.WeaponCache; if (weaponCache == null) { // calculate new weapon cache RebuildWeaponCache(character, weaponState); weaponCache = weaponState.WeaponCache; } // raycast possible victims var isMeleeWeapon = protoWeapon is IProtoItemWeaponMelee; var fromPosition = character.Position + (0, isMeleeWeapon ? character.ProtoCharacter.CharacterWorldWeaponOffsetMelee : character.ProtoCharacter.CharacterWorldWeaponOffsetRanged); var toPosition = fromPosition + new Vector2D(weaponCache.RangeMax, 0) .RotateRad(character.ProtoCharacter.SharedGetRotationAngleRad(character)); var collisionGroup = isMeleeWeapon ? CollisionGroups.HitboxMelee : CollisionGroups.HitboxRanged; 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); } foreach (var testResult in lineTestResults) { 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 break; } var damagedObject = testResultPhysicsBody.AssociatedWorldObject; if (damagedObject == character) { // ignore collision with self continue; } if (!(damagedObject.ProtoGameObject is IDamageableProtoWorldObject damageableProto)) { // shoot through this object continue; } var damagedCharacter = damagedObject as ICharacter; if (!ReferenceEquals(damagedCharacter, null)) { // don't allow damage is there is no direct line of sight on physical colliders layer between the two objects if (SharedHasTileObstacle(character, characterTileHeight, damagedCharacter)) { continue; } } else if (damagedObject is IStaticWorldObject staticWorldObject && characterTileHeight != staticWorldObject.OccupiedTile.Height) { // don't allow damage to static objects on a different height level continue; } if (!damageableProto.SharedOnDamage( weaponCache, damagedObject, damageMultiplier, out var obstacleBlockDamageCoef, out var damageApplied)) { // not hit continue; } if (IsServer) { weaponCache.ProtoWeapon .ServerOnDamageApplied(weaponCache.Weapon, character, damagedObject, damageApplied); if (damageApplied > 0 && !ReferenceEquals(damagedCharacter, null)) { CharacterUnstuckSystem.ServerTryCancelUnstuckRequest(damagedCharacter); } if (damageApplied > 0 && protoWeaponSkill != null) { // give experience for damage protoWeaponSkill.ServerOnDamageApplied(playerCharacterSkills, damagedObject, damageApplied); if (!ReferenceEquals(damagedCharacter, null) && (damagedCharacter.GetPublicState <ICharacterPublicState>() .CurrentStats .HealthCurrent <= 0)) { // give weapon experience for kill Logger.Info("Killed " + damagedCharacter, character); protoWeaponSkill.ServerOnKill(playerCharacterSkills, killedCharacter: damagedCharacter); if (damagedCharacter.ProtoCharacter is ProtoCharacterMob protoMob) { // give hunting skill experience for mob kill var experience = SkillHunting.ExperienceForKill; experience *= protoMob.MobKillExperienceMultiplier; if (experience > 0) { playerCharacterSkills.ServerAddSkillExperience <SkillHunting>(experience); } } } } } if (obstacleBlockDamageCoef < 0 || obstacleBlockDamageCoef > 1) { Logger.Error( "Obstacle block damage coefficient should be >= 0 and <= 1 - wrong calculation by " + damageableProto); break; } //var hitPosition = testResultPhysicsBody.Position + testResult.Penetration; hitObjects.Add(new WeaponHitData(damagedObject)); //, hitPosition)); if (isMeleeWeapon) { // currently melee weapon could attack only one object on the ray break; } damageMultiplier = damageMultiplier * (1.0 - obstacleBlockDamageCoef); if (damageMultiplier <= 0) { // target blocked the damage ray break; } } if (hitObjects.Count > 0) { if (IsClient) { // display weapon shot on Client-side WeaponSystemClientDisplay.OnWeaponHit(protoWeapon, hitObjects); } else // if server { // display damages on clients in scope of every damaged object using var scopedBy = Api.Shared.GetTempList <ICharacter>(); foreach (var hitObject in hitObjects) { if (hitObject.WorldObject.IsDestroyed) { continue; } Server.World.GetScopedByPlayers(hitObject.WorldObject, scopedBy); // don't notify the attacking character scopedBy.Remove(character); if (hitObject.WorldObject is ICharacter damagedCharacter) { // notify the damaged character scopedBy.Add(damagedCharacter); } if (scopedBy.Count == 0) { continue; } Instance.CallClient(scopedBy, _ => _.ClientRemote_OnWeaponHit(protoWeapon, hitObject)); scopedBy.Clear(); } } } if (IsServer) { protoWeapon.ServerOnShot(character, weaponItem, protoWeapon, hitObjects); } }
/// <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 != 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 && damagedObject is ICharacter damagedCharacter) { CharacterUnstuckSystem.ServerTryCancelUnstuckRequest(damagedCharacter); } 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 collisionGroup = null) { var playerCharacterSkills = weaponFinalCache.Character?.SharedGetSkills(); var protoWeaponSkill = playerCharacterSkills != null ? weaponFinalCache.ProtoWeapon?.WeaponSkillProto : null; // collect all damaged objects via physics space var damageCandidates = new HashSet <IWorldObject>(); if (collisionGroup is null) { collisionGroup = CollisionGroups.Default; } var defaultCollisionGroup = collisionGroup; CollectDamagedPhysicalObjects(defaultCollisionGroup); CollectDamagedPhysicalObjects(CollisionGroups.HitboxMelee); CollectDamagedPhysicalObjects(CollisionGroups.HitboxRanged); 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, collisionGroup)); 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) { CharacterUnstuckSystem.ServerTryCancelUnstuckRequest(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)); } }
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); } } }
private static void SharedFireWeapon( ICharacter character, IItem weaponItem, IProtoItemWeapon protoWeapon, WeaponState weaponState) { if (!protoWeapon.SharedOnFire(character, weaponState)) { return; } var playerCharacterSkills = character.SharedGetSkills(); var protoWeaponSkill = playerCharacterSkills != null ? protoWeapon.WeaponSkillProto : null; if (IsServer) { protoWeaponSkill?.ServerOnShot(playerCharacterSkills); // give experience for shot CharacterUnstuckSystem.ServerTryCancelUnstuckRequest(character); } var weaponCache = weaponState.WeaponCache; if (weaponCache is null) { SharedRebuildWeaponCache(character, weaponState); weaponCache = weaponState.WeaponCache; } var characterCurrentVehicle = character.IsNpc ? null : character.SharedGetCurrentVehicle(); var isMeleeWeapon = protoWeapon is IProtoItemWeaponMelee; var characterProtoCharacter = (IProtoCharacterCore)character.ProtoCharacter; var fromPosition = characterProtoCharacter.SharedGetWeaponFireWorldPosition(character, isMeleeWeapon); var fireSpreadAngleOffsetDeg = protoWeapon.SharedUpdateAndGetFirePatternCurrentSpreadAngleDeg(weaponState); var collisionGroup = protoWeapon.CollisionGroup; using var allHitObjects = Shared.GetTempList <IWorldObject>(); var shotsPerFire = weaponCache.FireScatterPreset.ProjectileAngleOffets; foreach (var angleOffsetDeg in shotsPerFire) { SharedShotWeaponHitscan(character, protoWeapon, fromPosition, weaponCache, weaponState.CustomTargetPosition, characterProtoCharacter, fireSpreadAngleOffsetDeg + angleOffsetDeg, collisionGroup, isMeleeWeapon, characterCurrentVehicle, protoWeaponSkill, playerCharacterSkills, allHitObjects); } if (IsServer) { protoWeapon.ServerOnShot(character, weaponItem, protoWeapon, allHitObjects.AsList()); } }