protected virtual Vector2 DoSteeringWander(float weight) { Vector2 circleCenter = (host.Steering == Vector2.Zero) ? Rand.Vector(weight) : host.Steering; circleCenter = Vector2.Normalize(circleCenter) * CircleDistance; Vector2 displacement = new Vector2( (float)Math.Cos(wanderAngle), (float)Math.Sin(wanderAngle)); displacement = displacement * CircleRadius; float angleChange = 1.5f; wanderAngle += Rand.Range(0.0f, 1.0f) * angleChange - angleChange * 0.5f; Vector2 newSteering = circleCenter + displacement; float steeringSpeed = (newSteering + host.Steering).Length(); if (steeringSpeed > weight) { newSteering = Vector2.Normalize(newSteering) * weight; } return(newSteering); }
private void InitializeMonsters(IEnumerable <Character> monsters) { foreach (var monster in monsters) { monster.Enabled = false; if (monster.Params.AI.EnforceAggressiveBehaviorForMissions) { foreach (var targetParam in monster.Params.AI.Targets) { switch (targetParam.State) { case AIState.Avoid: case AIState.Escape: case AIState.Flee: case AIState.PassiveAggressive: targetParam.State = AIState.Attack; break; } } } } SwarmBehavior.CreateSwarm(monsters.Cast <AICharacter>()); foreach (Character monster in monsters) { tempSonarPositions.Add(monster.WorldPosition + Rand.Vector(maxSonarMarkerDistance)); } if (monsters.Count() != tempSonarPositions.Count) { throw new Exception($"monsters.Count != tempSonarPositions.Count ({monsters.Count()} != {tempSonarPositions.Count})"); } }
private void HandleLevelCollision(Impact impact) { if (GameMain.GameSession != null && Timing.TotalTime < GameMain.GameSession.RoundStartTime + 10) { //ignore level collisions for the first 10 seconds of the round in case the sub spawns in a way that causes it to hit a wall //(e.g. level without outposts to dock to and an incorrectly configured ballast that makes the sub go up) return; } float wallImpact = Vector2.Dot(impact.Velocity, -impact.Normal); ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos); foreach (Submarine dockedSub in submarine.DockedTo) { dockedSub.SubBody.ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos); } #if CLIENT int particleAmount = (int)Math.Min(wallImpact * 10.0f, 50); for (int i = 0; i < particleAmount; i++) { GameMain.ParticleManager.CreateParticle("iceshards", ConvertUnits.ToDisplayUnits(impact.ImpactPos) + Rand.Vector(Rand.Range(1.0f, 50.0f)), Rand.Vector(Rand.Range(50.0f, 500.0f)) + impact.Velocity); } #endif }
partial void ImplodeFX() { Vector2 centerOfMass = AnimController.GetCenterOfMass(); SoundPlayer.PlaySound("implode", 1.0f, 150.0f, WorldPosition); for (int i = 0; i < 10; i++) { Particle p = GameMain.ParticleManager.CreateParticle("waterblood", ConvertUnits.ToDisplayUnits(centerOfMass) + Rand.Vector(5.0f), Rand.Vector(10.0f)); if (p != null) { p.Size *= 2.0f; } GameMain.ParticleManager.CreateParticle("bubbles", ConvertUnits.ToDisplayUnits(centerOfMass) + Rand.Vector(5.0f), new Vector2(Rand.Range(-50f, 50f), Rand.Range(-100f, 50f))); GameMain.ParticleManager.CreateParticle("gib", WorldPosition + Rand.Vector(Rand.Range(0.0f, 50.0f)), Rand.Range(0.0f, MathHelper.TwoPi), Rand.Range(200.0f, 700.0f), null); } for (int i = 0; i < 30; i++) { GameMain.ParticleManager.CreateParticle("heavygib", WorldPosition + Rand.Vector(Rand.Range(0.0f, 50.0f)), Rand.Range(0.0f, MathHelper.TwoPi), Rand.Range(50.0f, 500.0f), null); } }
public MapTile(Sprite sprite, SpriteEffects spriteEffect) { Sprite = sprite; SpriteEffect = spriteEffect; Offset = Rand.Vector(Rand.Range(0.0f, 1.0f)); }
protected virtual Vector2 DoSteeringAvoid(float deltaTime, float speed = 1.0f) { if (steering == Vector2.Zero || host.Steering == Vector2.Zero) { return(Vector2.Zero); } float maxDistance = 2.0f; Vector2 ahead = host.SimPosition + Vector2.Normalize(host.Steering) * maxDistance; if (rayCastTimer <= 0.0f) { rayCastTimer = RayCastInterval; Body closestBody = Submarine.CheckVisibility(host.SimPosition, ahead); if (closestBody == null) { avoidSteering = Vector2.Zero; return(Vector2.Zero); } else { if (closestBody.UserData is Structure closestStructure) { Vector2 obstaclePosition = Submarine.LastPickedPosition; if (closestStructure.IsHorizontal) { obstaclePosition.Y = closestStructure.SimPosition.Y; } else { obstaclePosition.X = closestStructure.SimPosition.X; } avoidSteering = Vector2.Normalize(Submarine.LastPickedPosition - obstaclePosition); } else if (closestBody.UserData is Item item) { avoidSteering = Vector2.Normalize(Submarine.LastPickedPosition - item.SimPosition); } else { avoidSteering = Vector2.Normalize(host.SimPosition - Submarine.LastPickedPosition); } //failed to normalize (the obstacle to avoid is at the same position as the character?) // -> move to a random direction if (!MathUtils.IsValid(avoidSteering)) { avoidSteering = Rand.Vector(1.0f); } } } else { rayCastTimer -= deltaTime; } return(avoidSteering * speed); }
partial void UpdateProjSpecific(float deltaTime) { if (!body.Enabled) { return; } if (!character.IsDead) { DamageOverlayStrength -= deltaTime; BurnOverlayStrength -= deltaTime; } else { var spriteParams = Params.GetSprite(); if (spriteParams.DeadColorTime > 0 && deadTimer < spriteParams.DeadColorTime) { deadTimer += deltaTime; } } if (inWater) { wetTimer = 1.0f; } else { wetTimer -= deltaTime * 0.1f; if (wetTimer > 0.0f) { dripParticleTimer += wetTimer * deltaTime * Mass * (wetTimer > 0.9f ? 50.0f : 5.0f); if (dripParticleTimer > 1.0f) { float dropRadius = body.BodyShape == PhysicsBody.Shape.Rectangle ? Math.Min(body.width, body.height) : body.radius; GameMain.ParticleManager.CreateParticle( "waterdrop", WorldPosition + Rand.Vector(Rand.Range(0.0f, ConvertUnits.ToDisplayUnits(dropRadius))), ConvertUnits.ToDisplayUnits(body.LinearVelocity), 0, character.CurrentHull); dripParticleTimer = 0.0f; } } } if (LightSource != null) { LightSource.ParentSub = body.Submarine; LightSource.Rotation = (dir == Direction.Right) ? body.Rotation : body.Rotation - MathHelper.Pi; if (LightSource.LightSprite != null) { LightSource.LightSprite.Depth = ActiveSprite.Depth; } } UpdateSpriteStates(deltaTime); }
protected override void Act(float deltaTime) { coolDownTimer -= deltaTime; var weapon = character.Inventory.FindItem("weapon"); if (weapon == null) { Escape(deltaTime); } else { //TODO: make sure the weapon is ready to use (projectiles/batteries loaded) if (!character.SelectedItems.Contains(weapon)) { if (character.Inventory.TryPutItem(weapon, 3, false, false, character)) { weapon.Equip(character); } else { return; } } character.CursorPosition = enemy.Position; character.SetInput(InputType.Aim, false, true); Vector2 enemyDiff = Vector2.Normalize(enemy.Position - character.Position); if (!MathUtils.IsValid(enemyDiff)) { enemyDiff = Rand.Vector(1.0f); } float weaponAngle = ((weapon.body.Dir == 1.0f) ? weapon.body.Rotation : weapon.body.Rotation - MathHelper.Pi); Vector2 weaponDir = new Vector2((float)Math.Cos(weaponAngle), (float)Math.Sin(weaponAngle)); if (Vector2.Dot(enemyDiff, weaponDir) < 0.9f) { return; } List <FarseerPhysics.Dynamics.Body> ignoredBodies = new List <FarseerPhysics.Dynamics.Body>(); foreach (Limb limb in character.AnimController.Limbs) { ignoredBodies.Add(limb.body.FarseerBody); } var pickedBody = Submarine.PickBody(character.SimPosition, enemy.SimPosition, ignoredBodies); if (pickedBody != null && !(pickedBody.UserData is Limb)) { return; } weapon.Use(deltaTime, character); } }
public void SpawnSprites(int count, Vector2?position = null) { activeSprites.Clear(); if (prefabs.Count == 0) { return; } count = Math.Min(count, MaxSprites); for (int i = 0; i < count; i++) { Vector2 pos = Vector2.Zero; if (position == null) { var wayPoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == null); if (wayPoints.Any()) { WayPoint wp = wayPoints[Rand.Int(wayPoints.Count, Rand.RandSync.ClientOnly)]; pos = new Vector2(wp.Rect.X, wp.Rect.Y); pos += Rand.Vector(200.0f, Rand.RandSync.ClientOnly); } else { pos = Rand.Vector(2000.0f, Rand.RandSync.ClientOnly); } } else { pos = (Vector2)position; } var prefab = prefabs[Rand.Int(prefabs.Count, Rand.RandSync.ClientOnly)]; int amount = Rand.Range(prefab.SwarmMin, prefab.SwarmMax, Rand.RandSync.ClientOnly); List <BackgroundCreature> swarmMembers = new List <BackgroundCreature>(); for (int n = 0; n < amount; n++) { var newSprite = new BackgroundCreature(prefab, pos); activeSprites.Add(newSprite); swarmMembers.Add(newSprite); } if (amount > 0) { new Swarm(swarmMembers, prefab.SwarmRadius); } } }
partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull) { if (shockwave) { GameMain.ParticleManager.CreateParticle("shockwave", worldPosition, Vector2.Zero, 0.0f, hull); } for (int i = 0; i < attack.Range * 0.1f; i++) { Vector2 bubblePos = Rand.Vector(attack.Range * 0.5f); GameMain.ParticleManager.CreateParticle("bubbles", worldPosition + bubblePos, bubblePos, 0.0f, hull); if (sparks) { GameMain.ParticleManager.CreateParticle("spark", worldPosition, Rand.Vector(Rand.Range(500.0f, 800.0f)), 0.0f, hull); } if (flames) { GameMain.ParticleManager.CreateParticle("explosionfire", ClampParticlePos(worldPosition + Rand.Vector(50f), hull), Rand.Vector(Rand.Range(50.0f, 100.0f)), 0.0f, hull); } if (smoke) { GameMain.ParticleManager.CreateParticle("smoke", ClampParticlePos(worldPosition + Rand.Vector(50f), hull), Rand.Vector(Rand.Range(1.0f, 10.0f)), 0.0f, hull); } } if (hull != null && !string.IsNullOrWhiteSpace(decal) && decalSize > 0.0f) { hull.AddDecal(decal, worldPosition, decalSize); } if (flash) { float displayRange = attack.Range; if (displayRange < 0.1f) { return; } var light = new LightSource(worldPosition, displayRange, Color.LightYellow, null); CoroutineManager.StartCoroutine(DimLight(light)); } }
partial void AddDamageProjSpecific(float damage, Vector2 worldPosition) { if (damage <= 0.0f) { return; } Vector2 particlePos = worldPosition; if (!Cells.Any(c => c.IsPointInside(particlePos))) { bool intersectionFound = false; foreach (var cell in Cells) { foreach (var edge in cell.Edges) { if (MathUtils.GetLineIntersection(worldPosition, cell.Center, edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, out Vector2 intersection)) { intersectionFound = true; particlePos = intersection; break; } } if (intersectionFound) { break; } } } Vector2 particleDir = particlePos - WorldPosition; if (particleDir.LengthSquared() > 0.0001f) { particleDir = Vector2.Normalize(particleDir); } int particleAmount = MathHelper.Clamp((int)damage, 1, 10); for (int i = 0; i < particleAmount; i++) { var particle = GameMain.ParticleManager.CreateParticle("iceshards", particlePos + Rand.Vector(5.0f), particleDir * Rand.Range(200.0f, 500.0f) + Rand.Vector(100.0f)); } }
private void HandleLevelCollision(Impact impact, VoronoiCell cell = null) { if (GameMain.GameSession != null && Timing.TotalTime < GameMain.GameSession.RoundStartTime + 10) { //ignore level collisions for the first 10 seconds of the round in case the sub spawns in a way that causes it to hit a wall //(e.g. level without outposts to dock to and an incorrectly configured ballast that makes the sub go up) return; } float wallImpact = Vector2.Dot(impact.Velocity, -impact.Normal); ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos); foreach (Submarine dockedSub in submarine.DockedTo) { dockedSub.SubBody.ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos); } if (cell != null && cell.IsDestructible && wallImpact > 0.0f) { var hitWall = Level.Loaded?.ExtraWalls.Find(w => w.Cells.Contains(cell)); if (hitWall != null && hitWall.WallDamageOnTouch > 0.0f) { var damagedStructures = Explosion.RangedStructureDamage( ConvertUnits.ToDisplayUnits(impact.ImpactPos), 500.0f, hitWall.WallDamageOnTouch, levelWallDamage: 0.0f); #if CLIENT PlayDamageSounds(damagedStructures, impact.ImpactPos, wallImpact, "StructureSlash"); #endif } } #if CLIENT int particleAmount = (int)Math.Min(wallImpact * 10.0f, 50); for (int i = 0; i < particleAmount; i++) { GameMain.ParticleManager.CreateParticle("iceshards", ConvertUnits.ToDisplayUnits(impact.ImpactPos) + Rand.Vector(Rand.Range(1.0f, 50.0f)), Rand.Vector(Rand.Range(50.0f, 500.0f)) + impact.Velocity); } #endif }
public bool OnCollision(Fixture f1, Fixture f2, Contact contact) { if (f2.Body.UserData is Limb limb) { bool collision = CheckLimbCollision(contact, limb); if (collision) { HandleLimbCollision(contact, limb); } return(collision); } if (f2.UserData is VoronoiCell cell) { Vector2 collisionNormal = Vector2.Normalize(ConvertUnits.ToDisplayUnits(Body.SimPosition) - cell.Center); if (!MathUtils.IsValid(collisionNormal)) { collisionNormal = Rand.Vector(1.0f); } HandleLevelCollision(contact, collisionNormal); return(true); } if (f2.Body.UserData is Structure structure) { contact.GetWorldManifold(out Vector2 normal, out FixedArray2 <Vector2> points); if (contact.FixtureA.Body == f1.Body) { normal = -normal; } HandleLevelCollision(contact, normal); return(true); } if (f2.Body.UserData is Submarine otherSub) { HandleSubCollision(contact, otherSub); return(true); } return(true); }
public void MoveCamera(float deltaTime, bool allowMove = true, bool allowZoom = true) { prevPosition = position; prevZoom = zoom; float moveSpeed = 20.0f / zoom; Vector2 moveCam = Vector2.Zero; if (targetPos == Vector2.Zero) { } else { Vector2 mousePos = PlayerInput.MousePosition; Vector2 offset = mousePos - new Vector2(resolution.X / 2.0f, resolution.Y / 2.0f); offset.X = offset.X / (resolution.X * 0.4f); offset.Y = -offset.Y / (resolution.Y * 0.3f); if (offset.Length() > 1.0f) { offset.Normalize(); } offset = offset * offsetAmount; float newZoom = Math.Min(DefaultZoom - Math.Min(offset.Length() / resolution.Y, 1.0f), 1.0f); Zoom += (newZoom - zoom) / ZoomSmoothness; Vector2 diff = (targetPos + offset) - position; moveCam = diff / MoveSmoothness; } shakeTargetPosition = Rand.Vector(Shake); shakePosition = Vector2.Lerp(shakePosition, shakeTargetPosition, 0.5f); Shake = MathHelper.Lerp(Shake, 0.0f, deltaTime * 2.0f); Translate(moveCam + shakePosition); }
private void HandleLevelCollision(Impact impact) { float wallImpact = Vector2.Dot(impact.Velocity, -impact.Normal); ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos); foreach (Submarine dockedSub in submarine.DockedTo) { dockedSub.SubBody.ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos); } #if CLIENT int particleAmount = (int)Math.Min(wallImpact * 10.0f, 50); for (int i = 0; i < particleAmount; i++) { GameMain.ParticleManager.CreateParticle("iceshards", ConvertUnits.ToDisplayUnits(impact.ImpactPos) + Rand.Vector(Rand.Range(1.0f, 50.0f)), Rand.Vector(Rand.Range(50.0f, 500.0f)) + impact.Velocity); } #endif }
public Vector2 GetRandomItemPos(PositionType spawnPosType, float randomSpread, float minDistFromSubs, float offsetFromWall = 10.0f) { if (!positionsOfInterest.Any()) { return(Size * 0.5f); } Vector2 position = Vector2.Zero; offsetFromWall = ConvertUnits.ToSimUnits(offsetFromWall); int tries = 0; do { Vector2 startPos; Loaded.TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out startPos); startPos += Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.Server), Rand.RandSync.Server); Vector2 endPos = startPos - Vector2.UnitY * Size.Y; if (Submarine.PickBody( ConvertUnits.ToSimUnits(startPos), ConvertUnits.ToSimUnits(endPos), null, Physics.CollisionLevel) != null) { position = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.Normalize(startPos - endPos) * offsetFromWall; break; } tries++; if (tries == 10) { position = EndPosition - Vector2.UnitY * 300.0f; } } while (tries < 10); return(position); }
private void HandleLevelCollision(Contact contact, Vector2 collisionNormal) { float wallImpact = Vector2.Dot(Velocity, -collisionNormal); ApplyImpact(wallImpact, -collisionNormal, contact); foreach (Submarine dockedSub in submarine.DockedTo) { dockedSub.SubBody.ApplyImpact(wallImpact, -collisionNormal, contact); } #if CLIENT contact.GetWorldManifold(out _, out FixedArray2 <Vector2> particlePos); int particleAmount = (int)Math.Min(wallImpact * 10.0f, 50); for (int i = 0; i < particleAmount; i++) { GameMain.ParticleManager.CreateParticle("iceshards", ConvertUnits.ToDisplayUnits(particlePos[0]) + Rand.Vector(Rand.Range(1.0f, 50.0f)), Rand.Vector(Rand.Range(50.0f, 500.0f)) + Velocity); } #endif }
public override void Update(float deltaTime) { if (disallowed) { Finished(); return; } if (isFinished) { return; } if (spawnPos == null) { FindSpawnPosition(affectSubImmediately: true); spawnPending = true; } bool spawnReady = false; if (spawnPending) { //if spawning in a ruin/cave, wait for someone to be close to it to spawning //unnecessary monsters in places the players might never visit during the round if (spawnPosType == Level.PositionType.Ruin || spawnPosType == Level.PositionType.Cave || spawnPosType == Level.PositionType.Wreck) { bool someoneNearby = false; float minDist = Sonar.DefaultSonarRange * 0.8f; foreach (Submarine submarine in Submarine.Loaded) { if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { someoneNearby = true; break; } } foreach (Character c in Character.CharacterList) { if (c == Character.Controlled || c.IsRemotePlayer) { if (Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value) < minDist * minDist) { someoneNearby = true; break; } } } if (!someoneNearby) { return; } } else { //wait until there are no submarines at the spawnpos foreach (Submarine submarine in Submarine.Loaded) { if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } float minDist = GetMinDistanceToSub(submarine); if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; } } } spawnPending = false; //+1 because Range returns an integer less than the max value int amount = Rand.Range(minAmount, maxAmount + 1); monsters = new List <Character>(); float offsetAmount = spawnPosType == Level.PositionType.MainPath ? scatter : 100; for (int i = 0; i < amount; i++) { CoroutineManager.InvokeAfter(() => { //round ended before the coroutine finished if (GameMain.GameSession == null || Level.Loaded == null) { return; } System.Diagnostics.Debug.Assert(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer, "Clients should not create monster events."); Vector2 pos = spawnPos.Value + Rand.Vector(offsetAmount); if (spawnPosType == Level.PositionType.MainPath) { if (Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(pos))) { // Can't use the offset position, let's use the exact spawn position. pos = spawnPos.Value; } else if (Level.Loaded.Ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).ContainsWorld(pos))) { // Can't use the offset position, let's use the exact spawn position. pos = spawnPos.Value; } } monsters.Add(Character.Create(speciesName, pos, Level.Loaded.Seed + i.ToString(), null, false, true, true)); if (monsters.Count == amount) { spawnReady = true; //this will do nothing if the monsters have no swarm behavior defined, //otherwise it'll make the spawned characters act as a swarm SwarmBehavior.CreateSwarm(monsters.Cast <AICharacter>()); } }, Rand.Range(0f, amount / 2)); } } if (!spawnReady) { return; } Entity targetEntity = Submarine.FindClosest(GameMain.GameScreen.Cam.WorldViewCenter); #if CLIENT if (Character.Controlled != null) { targetEntity = Character.Controlled; } #endif bool monstersDead = true; foreach (Character monster in monsters) { if (!monster.IsDead) { monstersDead = false; if (targetEntity != null && Vector2.DistanceSquared(monster.WorldPosition, targetEntity.WorldPosition) < 5000.0f * 5000.0f) { break; } } } if (monstersDead) { Finished(); } }
public void Explode(Vector2 worldPosition) { Hull hull = Hull.FindHull(worldPosition); if (shockwave) { GameMain.ParticleManager.CreateParticle("shockwave", worldPosition, Vector2.Zero, 0.0f, hull); } for (int i = 0; i < attack.Range * 0.1f; i++) { Vector2 bubblePos = Rand.Vector(attack.Range * 0.5f); GameMain.ParticleManager.CreateParticle("bubbles", worldPosition + bubblePos, bubblePos, 0.0f, hull); if (sparks) { GameMain.ParticleManager.CreateParticle("spark", worldPosition, Rand.Vector(Rand.Range(500.0f, 800.0f)), 0.0f, hull); } if (flames) { GameMain.ParticleManager.CreateParticle("explosionfire", ClampParticlePos(worldPosition + Rand.Vector(50f), hull), Rand.Vector(Rand.Range(50.0f, 100.0f)), 0.0f, hull); } if (smoke) { GameMain.ParticleManager.CreateParticle("smoke", ClampParticlePos(worldPosition + Rand.Vector(50f), hull), Rand.Vector(Rand.Range(1.0f, 10.0f)), 0.0f, hull); } } float displayRange = attack.Range; if (displayRange < 0.1f) { return; } var light = new LightSource(worldPosition, displayRange, Color.LightYellow, null); CoroutineManager.StartCoroutine(DimLight(light)); float cameraDist = Vector2.Distance(GameMain.GameScreen.Cam.Position, worldPosition) / 2.0f; GameMain.GameScreen.Cam.Shake = CameraShake * Math.Max((displayRange - cameraDist) / displayRange, 0.0f); if (attack.GetStructureDamage(1.0f) > 0.0f) { RangedStructureDamage(worldPosition, displayRange, attack.GetStructureDamage(1.0f)); } if (force == 0.0f && attack.Stun == 0.0f && attack.GetDamage(1.0f) == 0.0f) { return; } ApplyExplosionForces(worldPosition, attack.Range, force, attack.GetDamage(1.0f), attack.Stun); if (flames && GameMain.Client == null) { foreach (Item item in Item.ItemList) { if (item.CurrentHull != hull || item.FireProof || item.Condition <= 0.0f) { continue; } if (Vector2.Distance(item.WorldPosition, worldPosition) > attack.Range * 0.1f) { continue; } item.ApplyStatusEffects(ActionType.OnFire, 1.0f); if (item.Condition <= 0.0f && GameMain.Server != null) { GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFire }); } } } }
partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull) { if (shockwave) { GameMain.ParticleManager.CreateParticle("shockwave", worldPosition, Vector2.Zero, 0.0f, hull); } hull = hull ?? Hull.FindHull(worldPosition, useWorldCoordinates: true); bool underwater = hull == null || worldPosition.Y < hull.WorldSurface; if (underwater && underwaterBubble) { var underwaterExplosion = GameMain.ParticleManager.CreateParticle("underwaterexplosion", worldPosition, Vector2.Zero, 0.0f, hull); if (underwaterExplosion != null) { underwaterExplosion.Size *= MathHelper.Clamp(attack.Range / 150.0f, 0.5f, 10.0f); underwaterExplosion.StartDelay = 0.0f; } } for (int i = 0; i < attack.Range * 0.1f; i++) { if (!underwater) { float particleSpeed = Rand.Range(0.0f, 1.0f); particleSpeed = particleSpeed * particleSpeed * attack.Range; if (flames) { float particleScale = MathHelper.Clamp(attack.Range * 0.0025f, 0.5f, 2.0f); var flameParticle = GameMain.ParticleManager.CreateParticle("explosionfire", ClampParticlePos(worldPosition + Rand.Vector((float)System.Math.Sqrt(Rand.Range(0.0f, attack.Range))), hull), Rand.Vector(Rand.Range(0.0f, particleSpeed)), 0.0f, hull); if (flameParticle != null) { flameParticle.Size *= particleScale; } } if (smoke) { var smokeParticle = GameMain.ParticleManager.CreateParticle(Rand.Range(0.0f, 1.0f) < 0.5f ? "explosionsmoke" : "smoke", ClampParticlePos(worldPosition + Rand.Vector((float)System.Math.Sqrt(Rand.Range(0.0f, attack.Range))), hull), Rand.Vector(Rand.Range(0.0f, particleSpeed)), 0.0f, hull); } } else if (underwaterBubble) { Vector2 bubblePos = Rand.Vector(Rand.Range(0.0f, attack.Range * 0.5f)); GameMain.ParticleManager.CreateParticle("risingbubbles", worldPosition + bubblePos, Vector2.Zero, 0.0f, hull); if (i < attack.Range * 0.02f) { var underwaterExplosion = GameMain.ParticleManager.CreateParticle("underwaterexplosion", worldPosition + bubblePos, Vector2.Zero, 0.0f, hull); if (underwaterExplosion != null) { underwaterExplosion.Size *= MathHelper.Clamp(attack.Range / 300.0f, 0.5f, 2.0f) * Rand.Range(0.8f, 1.2f); } } } if (sparks) { GameMain.ParticleManager.CreateParticle("spark", worldPosition, Rand.Vector(Rand.Range(500.0f, 800.0f)), 0.0f, hull); } } if (hull != null && !string.IsNullOrWhiteSpace(decal) && decalSize > 0.0f) { hull.AddDecal(decal, worldPosition, decalSize); } if (flash) { float displayRange = attack.Range; if (displayRange < 0.1f) { return; } var light = new LightSource(worldPosition, displayRange, Color.LightYellow, null); CoroutineManager.StartCoroutine(DimLight(light)); } }
/// <summary> /// Control the Character according to player input /// </summary> public void ControlLocalPlayer(float deltaTime, Camera cam, bool moveCam = true) { if (DisableControls || GUI.PauseMenuOpen || GUI.SettingsMenuOpen) { foreach (Key key in keys) { if (key == null) { continue; } key.Reset(); } } else { wasFiring |= keys[(int)InputType.Aim].Held && keys[(int)InputType.Shoot].Held; for (int i = 0; i < keys.Length; i++) { keys[i].SetState(); } //if we were firing (= pressing the aim and shoot keys at the same time) //and the fire key is the same as Select or Use, reset the key to prevent accidentally selecting/using items if (wasFiring && !keys[(int)InputType.Shoot].Held) { if (GameMain.Config.KeyBind(InputType.Shoot).Equals(GameMain.Config.KeyBind(InputType.Select))) { keys[(int)InputType.Select].Reset(); } if (GameMain.Config.KeyBind(InputType.Shoot).Equals(GameMain.Config.KeyBind(InputType.Use))) { keys[(int)InputType.Use].Reset(); } wasFiring = false; } float targetOffsetAmount = 0.0f; if (moveCam) { if (NeedsAir && pressureProtection < 80.0f && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure > 0.0f)) { float pressure = AnimController.CurrentHull == null ? 100.0f : AnimController.CurrentHull.LethalPressure; if (pressure > 0.0f) { float zoomInEffectStrength = MathHelper.Clamp(pressure / 100.0f, 0.1f, 1.0f); cam.Zoom = MathHelper.Lerp(cam.Zoom, cam.DefaultZoom + (Math.Max(pressure, 10) / 150.0f) * Rand.Range(0.9f, 1.1f), zoomInEffectStrength); pressureParticleTimer += pressure * deltaTime; if (pressureParticleTimer > 10.0f) { GameMain.ParticleManager.CreateParticle(Params.BleedParticleWater, WorldPosition + Rand.Vector(5.0f), Rand.Vector(10.0f)); pressureParticleTimer = 0.0f; } } } if (IsHumanoid) { cam.OffsetAmount = 250.0f;// MathHelper.Lerp(cam.OffsetAmount, 250.0f, deltaTime); } else { //increased visibility range when controlling large a non-humanoid cam.OffsetAmount = MathHelper.Clamp(Mass, 250.0f, 1500.0f); } } cursorPosition = cam.ScreenToWorld(PlayerInput.MousePosition); if (AnimController.CurrentHull?.Submarine != null) { cursorPosition -= AnimController.CurrentHull.Submarine.Position; } Vector2 mouseSimPos = ConvertUnits.ToSimUnits(cursorPosition); if (GUI.PauseMenuOpen) { cam.OffsetAmount = targetOffsetAmount = 0.0f; } else if (Lights.LightManager.ViewTarget is Item item && item.Prefab.FocusOnSelected) { cam.OffsetAmount = targetOffsetAmount = item.Prefab.OffsetOnSelected; }
protected override void UpdateMissionSpecific(float deltaTime) { if (IsClient) { foreach (Item item in items) { if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } } return; } switch (State) { case 0: foreach (Item item in items) { if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } if (statusEffectOnApproach.ContainsKey(item)) { foreach (Character character in Character.CharacterList) { if (character.IsPlayer && Vector2.DistanceSquared(nestPosition, character.WorldPosition) < approachItemsRadius * approachItemsRadius) { statusEffectOnApproach[item].Apply(statusEffectOnApproach[item].type, 1.0f, item, item); statusEffectOnApproach.Remove(item); break; } } } } if (monsterPrefabs.Any()) { foreach (Character character in Character.CharacterList) { if (character.IsPlayer && Vector2.DistanceSquared(nestPosition, character.WorldPosition) < monsterSpawnRadius * monsterSpawnRadius) { foreach (var monster in monsterPrefabs) { int amount = Rand.Range(monster.Item2.X, monster.Item2.Y + 1); for (int i = 0; i < amount; i++) { Character.Create(monster.Item1.Identifier, nestPosition + Rand.Vector(100.0f), ToolBox.RandomSeed(8), createNetworkEvent: true); } } monsterPrefabs.Clear(); break; } } } //continue when all items are in the sub or destroyed if (AllItemsDestroyedOrRetrieved()) { State = 1; } break; case 1: if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } State = 2; break; } }
private static void UpdateRandomAmbience(float deltaTime) { if (ambientSoundTimer > 0.0f) { ambientSoundTimer -= deltaTime; } else { PlaySound( "ambient", new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y) + Rand.Vector(100.0f), Rand.Range(0.5f, 1.0f), 1000.0f); ambientSoundTimer = Rand.Range(ambientSoundInterval.X, ambientSoundInterval.Y); } }
private List <Vector2> GenerateTunnel(List <Vector2> tunnelNodes, float tunnelLength, List <Vector2> avoidNodes) { float sectionLength = 1000.0f; float currLength = 0.0f; while (currLength < tunnelLength) { Vector2 dir = Rand.Vector(1.0f, Rand.RandSync.Server); dir.Y += Math.Sign(tunnelNodes[tunnelNodes.Count - 1].Y - Size.Y / 2) * 0.5f; if (tunnelNodes.Count > 1) { //keep heading roughly in the same direction as the previous nodes Vector2 prevNodeDiff = tunnelNodes[tunnelNodes.Count - 1] - tunnelNodes[tunnelNodes.Count - 2]; if (prevNodeDiff != Vector2.Zero) { dir += Vector2.Normalize(tunnelNodes[tunnelNodes.Count - 1] - tunnelNodes[tunnelNodes.Count - 2]) * 0.5f; } } float avoidDist = 20000.0f; foreach (Vector2 pathNode in avoidNodes) { Vector2 diff = tunnelNodes[tunnelNodes.Count - 1] - pathNode; if (diff == Vector2.Zero) { continue; } float dist = diff.Length(); if (dist < avoidDist) { dir += (diff / dist) * (1.0f - dist / avoidDist); } } Vector2 normalizedDir = Vector2.Normalize(dir); if (tunnelNodes.Last().Y + normalizedDir.Y > Size.Y) { //head back down if the tunnel has reached the top of the level normalizedDir.Y = -normalizedDir.Y; } else if (tunnelNodes.Last().Y + normalizedDir.Y + normalizedDir.Y < 0.0f || tunnelNodes.Last().Y + normalizedDir.Y + normalizedDir.Y < SeaFloorTopPos) { //head back up if reached the sea floor normalizedDir.Y = -normalizedDir.Y; } Vector2 nextNode = tunnelNodes.Last() + normalizedDir * sectionLength; nextNode.X = MathHelper.Clamp(nextNode.X, 500.0f, Size.X - 500.0f); nextNode.Y = MathHelper.Clamp(nextNode.Y, SeaFloorTopPos, Size.Y - 500.0f); tunnelNodes.Add(nextNode); currLength += sectionLength; } return(tunnelNodes); }
public void MoveCamera(float deltaTime, bool allowMove = true, bool allowZoom = true) { prevPosition = position; prevZoom = zoom; float moveSpeed = 20.0f / zoom; Vector2 moveCam = Vector2.Zero; if (targetPos == Vector2.Zero) { if (allowMove && GUIComponent.KeyboardDispatcher.Subscriber == null) { if (PlayerInput.KeyDown(Keys.LeftShift)) { moveSpeed *= 2.0f; } if (PlayerInput.KeyDown(Keys.LeftControl)) { moveSpeed *= 0.5f; } if (GameMain.Config.KeyBind(InputType.Left).IsDown()) { moveCam.X -= moveSpeed; } if (GameMain.Config.KeyBind(InputType.Right).IsDown()) { moveCam.X += moveSpeed; } if (GameMain.Config.KeyBind(InputType.Down).IsDown()) { moveCam.Y -= moveSpeed; } if (GameMain.Config.KeyBind(InputType.Up).IsDown()) { moveCam.Y += moveSpeed; } } if (Screen.Selected == GameMain.GameScreen && FollowSub) { var closestSub = Submarine.FindClosest(WorldViewCenter); if (closestSub != null) { moveCam += FarseerPhysics.ConvertUnits.ToDisplayUnits(closestSub.Velocity * deltaTime); } } moveCam = moveCam * deltaTime * 60.0f; if (allowZoom) { Vector2 mouseInWorld = ScreenToWorld(PlayerInput.MousePosition); Vector2 diffViewCenter; diffViewCenter = ((mouseInWorld - Position) * Zoom); Zoom = MathHelper.Clamp(zoom + (PlayerInput.ScrollWheelSpeed / 1000.0f) * zoom, GameMain.DebugDraw ? 0.01f : 0.1f, 2.0f); if (!PlayerInput.KeyDown(Keys.F)) { Position = mouseInWorld - (diffViewCenter / Zoom); } } } else { Vector2 mousePos = PlayerInput.MousePosition; Vector2 offset = mousePos - new Vector2(resolution.X / 2.0f, resolution.Y / 2.0f); offset.X = offset.X / (resolution.X * 0.4f); offset.Y = -offset.Y / (resolution.Y * 0.3f); if (offset.Length() > 1.0f) { offset.Normalize(); } offset = offset * offsetAmount; float newZoom = Math.Min(DefaultZoom - Math.Min(offset.Length() / resolution.Y, 1.0f), 1.0f); Zoom += (newZoom - zoom) / ZoomSmoothness; Vector2 diff = (targetPos + offset) - position; moveCam = diff / MoveSmoothness; } shakeTargetPosition = Rand.Vector(Shake); shakePosition = Vector2.Lerp(shakePosition, shakeTargetPosition, 0.5f); Shake = MathHelper.Lerp(Shake, 0.0f, deltaTime * 2.0f); Translate(moveCam + shakePosition); }
public static void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker) { if (attack.Range <= 0.0f) { return; } //long range for the broad distance check, because large characters may still be in range even if their collider isn't float broadRange = Math.Max(attack.Range * 10.0f, 10000.0f); foreach (Character c in Character.CharacterList) { if (!c.Enabled || Math.Abs(c.WorldPosition.X - worldPosition.X) > broadRange || Math.Abs(c.WorldPosition.Y - worldPosition.Y) > broadRange) { continue; } Vector2 explosionPos = worldPosition; if (c.Submarine != null) { explosionPos -= c.Submarine.Position; } Hull hull = Hull.FindHull(explosionPos, null, false); bool underWater = hull == null || explosionPos.Y < hull.Surface; explosionPos = ConvertUnits.ToSimUnits(explosionPos); Dictionary <Limb, float> distFactors = new Dictionary <Limb, float>(); Dictionary <Limb, float> damages = new Dictionary <Limb, float>(); List <Affliction> modifiedAfflictions = new List <Affliction>(); foreach (Limb limb in c.AnimController.Limbs) { if (limb.IsSevered || limb.ignoreCollisions) { continue; } float dist = Vector2.Distance(limb.WorldPosition, worldPosition); //calculate distance from the "outer surface" of the physics body //doesn't take the rotation of the limb into account, but should be accurate enough for this purpose float limbRadius = limb.body.GetMaxExtent(); dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius)); if (dist > attack.Range) { continue; } float distFactor = 1.0f - dist / attack.Range; //solid obstacles between the explosion and the limb reduce the effect of the explosion var obstacles = Submarine.PickBodies(limb.SimPosition, explosionPos, collisionCategory: Physics.CollisionItem | Physics.CollisionItemBlocking | Physics.CollisionWall); foreach (var body in obstacles) { if (body.UserData is Item item) { var door = item.GetComponent <Door>(); if (door != null && !door.IsBroken) { distFactor *= 0.01f; } } else if (body.UserData is Structure structure) { int sectionIndex = structure.FindSectionIndex(worldPosition, world: true, clamp: true); if (structure.SectionBodyDisabled(sectionIndex)) { continue; } else if (structure.SectionIsLeaking(sectionIndex)) { distFactor *= 0.1f; } else { distFactor *= 0.01f; } } else { distFactor *= 0.1f; } } if (distFactor <= 0.05f) { continue; } distFactors.Add(limb, distFactor); modifiedAfflictions.Clear(); foreach (Affliction affliction in attack.Afflictions.Keys) { //previously the damage would be divided by the number of limbs (the intention was to prevent characters with more limbs taking more damage from explosions) //that didn't work well on large characters like molochs and endworms: the explosions tend to only damage one or two of their limbs, and since the characters //have lots of limbs, they tended to only take a fraction of the damage they should //now we just divide by 10, which keeps the damage to normal-sized characters roughly the same as before and fixes the large characters modifiedAfflictions.Add(affliction.CreateMultiplied(distFactor / 10)); } c.LastDamageSource = damageSource; if (attacker == null) { if (damageSource is Item item) { attacker = item.GetComponent <Projectile>()?.User; if (attacker == null) { attacker = item.GetComponent <MeleeWeapon>()?.User; } } } //use a position slightly from the limb's position towards the explosion //ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods Vector2 dir = worldPosition - limb.WorldPosition; Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f; AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker); damages.Add(limb, attackResult.Damage); if (attack.StatusEffects != null && attack.StatusEffects.Any()) { attack.SetUser(attacker); var statusEffectTargets = new List <ISerializableEntity>() { c, limb }; foreach (StatusEffect statusEffect in attack.StatusEffects) { statusEffect.Apply(ActionType.OnUse, 1.0f, damageSource, statusEffectTargets); statusEffect.Apply(ActionType.Always, 1.0f, damageSource, statusEffectTargets); statusEffect.Apply(underWater ? ActionType.InWater : ActionType.NotInWater, 1.0f, damageSource, statusEffectTargets); } } if (limb.WorldPosition != worldPosition && !MathUtils.NearlyEqual(force, 0.0f)) { Vector2 limbDiff = Vector2.Normalize(limb.WorldPosition - worldPosition); if (!MathUtils.IsValid(limbDiff)) { limbDiff = Rand.Vector(1.0f); } Vector2 impulse = limbDiff * distFactor * force; Vector2 impulsePoint = limb.SimPosition - limbDiff * limbRadius; limb.body.ApplyLinearImpulse(impulse, impulsePoint, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.2f); } } //sever joints if (attack.SeverLimbsProbability > 0.0f) { foreach (Limb limb in c.AnimController.Limbs) { if (limb.character.Removed || limb.Removed) { continue; } if (limb.IsSevered) { continue; } if (!c.IsDead && !limb.CanBeSeveredAlive) { continue; } if (distFactors.TryGetValue(limb, out float distFactor)) { if (damages.TryGetValue(limb, out float damage)) { c.TrySeverLimbJoints(limb, attack.SeverLimbsProbability * distFactor, damage, allowBeheading: true); } } } } } }
public static void Update(float deltaTime) { UpdateMusic(deltaTime); if (startDrone != null && !startDrone.IsPlaying) { startDrone.Remove(); startDrone = null; } //stop submarine ambient sounds if no sub is loaded if (Submarine.MainSub == null) { for (int i = 0; i < waterAmbienceIndexes.Length; i++) { if (waterAmbienceIndexes[i] <= 0) { continue; } SoundManager.Stop(waterAmbienceIndexes[i]); waterAmbienceIndexes[i] = 0; } return; } float ambienceVolume = 0.8f; float lowpassHFGain = 1.0f; if (Character.Controlled != null) { AnimController animController = Character.Controlled.AnimController; if (animController.HeadInWater) { ambienceVolume = 1.0f; ambienceVolume += animController.Limbs[0].LinearVelocity.Length(); lowpassHFGain = 0.2f; } lowpassHFGain *= Character.Controlled.LowPassMultiplier; } //how fast the sub is moving, scaled to 0.0 -> 1.0 float movementSoundVolume = 0.0f; foreach (Submarine sub in Submarine.Loaded) { float movementFactor = (sub.Velocity == Vector2.Zero) ? 0.0f : sub.Velocity.Length() / 10.0f; movementFactor = MathHelper.Clamp(movementFactor, 0.0f, 1.0f); if (Character.Controlled == null || Character.Controlled.Submarine != sub) { float dist = Vector2.Distance(GameMain.GameScreen.Cam.WorldViewCenter, sub.WorldPosition); movementFactor = movementFactor / Math.Max(dist / 1000.0f, 1.0f); } movementSoundVolume = Math.Max(movementSoundVolume, movementFactor); } if (ambientSoundTimer > 0.0f) { ambientSoundTimer -= (float)Timing.Step; } else { PlaySound( "ambient", Rand.Range(0.5f, 1.0f), 1000.0f, new Vector2(Sound.CameraPos.X, Sound.CameraPos.Y) + Rand.Vector(100.0f)); ambientSoundTimer = Rand.Range(ambientSoundInterval.X, ambientSoundInterval.Y); } SoundManager.LowPassHFGain = lowpassHFGain; waterAmbienceIndexes[0] = waterAmbiences[0].Loop(waterAmbienceIndexes[0], ambienceVolume * (1.0f - movementSoundVolume)); waterAmbienceIndexes[1] = waterAmbiences[1].Loop(waterAmbienceIndexes[1], ambienceVolume * movementSoundVolume); }
private void UpdateSounds(Character character, float deltaTime) { if (soundTimer < MathHelper.Lerp(maxSoundInterval, minSoundInterval, Strength / 100.0f)) { soundTimer += deltaTime; return; } float impactStrength = MathHelper.Lerp(0.1f, 1.0f, Strength / 100.0f); SoundPlayer.PlayDamageSound("StructureBlunt", Rand.Range(10.0f, 1000.0f), character.WorldPosition + Rand.Vector(500.0f)); GameMain.GameScreen.Cam.Shake = impactStrength * 10.0f; GameMain.GameScreen.Cam.AngularVelocity = Rand.Range(-impactStrength, impactStrength); soundTimer = 0.0f; }
partial void Splash(Limb limb, Hull limbHull) { //create a splash particle for (int i = 0; i < MathHelper.Clamp(Math.Abs(limb.LinearVelocity.Y), 1.0f, 5.0f); i++) { var splash = GameMain.ParticleManager.CreateParticle("watersplash", new Vector2(limb.WorldPosition.X, limbHull.WorldSurface), new Vector2(0.0f, Math.Abs(-limb.LinearVelocity.Y * 20.0f)) + Rand.Vector(Math.Abs(limb.LinearVelocity.Y * 10)), Rand.Range(0.0f, MathHelper.TwoPi), limbHull); if (splash != null) { splash.Size *= MathHelper.Clamp(Math.Abs(limb.LinearVelocity.Y) * 0.1f, 1.0f, 2.0f); } } GameMain.ParticleManager.CreateParticle("bubbles", new Vector2(limb.WorldPosition.X, limbHull.WorldSurface), limb.LinearVelocity * 0.001f, 0.0f, limbHull); //if the Character dropped into water, create a wave if (limb.LinearVelocity.Y < 0.0f) { if (splashSoundTimer <= 0.0f) { SoundPlayer.PlaySplashSound(limb.WorldPosition, Math.Abs(limb.LinearVelocity.Y) + Rand.Range(-5.0f, 0.0f)); splashSoundTimer = 0.5f; } //+ some extra bubbles to follow the character underwater GameMain.ParticleManager.CreateParticle("bubbles", new Vector2(limb.WorldPosition.X, limbHull.WorldSurface), limb.LinearVelocity * 10.0f, 0.0f, limbHull); } }
public static void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker) { if (attack.Range <= 0.0f) { return; } //long range for the broad distance check, because large characters may still be in range even if their collider isn't float broadRange = Math.Max(attack.Range * 10.0f, 10000.0f); foreach (Character c in Character.CharacterList) { if (!c.Enabled || Math.Abs(c.WorldPosition.X - worldPosition.X) > broadRange || Math.Abs(c.WorldPosition.Y - worldPosition.Y) > broadRange) { continue; } Vector2 explosionPos = worldPosition; if (c.Submarine != null) { explosionPos -= c.Submarine.Position; } Hull hull = Hull.FindHull(ConvertUnits.ToDisplayUnits(explosionPos), null, false); bool underWater = hull == null || explosionPos.Y < hull.Surface; explosionPos = ConvertUnits.ToSimUnits(explosionPos); Dictionary <Limb, float> distFactors = new Dictionary <Limb, float>(); foreach (Limb limb in c.AnimController.Limbs) { float dist = Vector2.Distance(limb.WorldPosition, worldPosition); //calculate distance from the "outer surface" of the physics body //doesn't take the rotation of the limb into account, but should be accurate enough for this purpose float limbRadius = Math.Max(Math.Max(limb.body.width * 0.5f, limb.body.height * 0.5f), limb.body.radius); dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius)); if (dist > attack.Range) { continue; } float distFactor = 1.0f - dist / attack.Range; //solid obstacles between the explosion and the limb reduce the effect of the explosion by 90% if (Submarine.CheckVisibility(limb.SimPosition, explosionPos) != null) { distFactor *= 0.1f; } distFactors.Add(limb, distFactor); List <Affliction> modifiedAfflictions = new List <Affliction>(); foreach (Affliction affliction in attack.Afflictions.Keys) { modifiedAfflictions.Add(affliction.CreateMultiplied(distFactor / c.AnimController.Limbs.Length)); } c.LastDamageSource = damageSource; if (attacker == null) { if (damageSource is Item item) { attacker = item.GetComponent <Projectile>()?.User; if (attacker == null) { attacker = item.GetComponent <MeleeWeapon>()?.User; } } } //use a position slightly from the limb's position towards the explosion //ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods Vector2 hitPos = limb.WorldPosition + (worldPosition - limb.WorldPosition) / dist * 0.01f; c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker); if (attack.StatusEffects != null && attack.StatusEffects.Any()) { attack.SetUser(attacker); var statusEffectTargets = new List <ISerializableEntity>() { c, limb }; foreach (StatusEffect statusEffect in attack.StatusEffects) { statusEffect.Apply(ActionType.OnUse, 1.0f, damageSource, statusEffectTargets); statusEffect.Apply(ActionType.Always, 1.0f, damageSource, statusEffectTargets); statusEffect.Apply(underWater ? ActionType.InWater : ActionType.NotInWater, 1.0f, damageSource, statusEffectTargets); } } if (limb.WorldPosition != worldPosition && !MathUtils.NearlyEqual(force, 0.0f)) { Vector2 limbDiff = Vector2.Normalize(limb.WorldPosition - worldPosition); if (!MathUtils.IsValid(limbDiff)) { limbDiff = Rand.Vector(1.0f); } Vector2 impulse = limbDiff * distFactor * force; Vector2 impulsePoint = limb.SimPosition - limbDiff * limbRadius; limb.body.ApplyLinearImpulse(impulse, impulsePoint, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } } //sever joints if (c.IsDead && attack.SeverLimbsProbability > 0.0f) { foreach (Limb limb in c.AnimController.Limbs) { if (!distFactors.ContainsKey(limb)) { continue; } if (Rand.Range(0.0f, 1.0f) < attack.SeverLimbsProbability * distFactors[limb]) { c.TrySeverLimbJoints(limb, 1.0f); } } } } }