public LatchOntoAI(XElement element, EnemyAIController enemyAI) { AttachToWalls = element.GetAttributeBool("attachtowalls", false); AttachToSub = element.GetAttributeBool("attachtosub", false); minDeattachSpeed = element.GetAttributeFloat("mindeattachspeed", 5.0f); maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat("maxdeattachspeed", 8.0f)); damageOnDetach = element.GetAttributeFloat("damageondetach", 0.0f); detachStun = element.GetAttributeFloat("detachstun", 0.0f); localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("localattachpos", Vector2.Zero)); attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat("attachlimbrotation", 0.0f)); string limbString = element.GetAttributeString("attachlimb", null); attachLimb = enemyAI.Character.AnimController.Limbs.FirstOrDefault(l => string.Equals(l.Name, limbString, StringComparison.OrdinalIgnoreCase)); if (attachLimb == null) { if (Enum.TryParse(limbString, out LimbType attachLimbType)) { attachLimb = enemyAI.Character.AnimController.GetLimb(attachLimbType); } } if (attachLimb == null) { attachLimb = enemyAI.Character.AnimController.MainLimb; } character = enemyAI.Character; enemyAI.Character.OnDeath += OnCharacterDeath; }
public SwarmBehavior(XElement element, EnemyAIController ai) { this.ai = ai; minDistFromClosest = ConvertUnits.ToSimUnits(element.GetAttributeFloat("mindistfromclosest", 10.0f)); maxDistFromCenter = ConvertUnits.ToSimUnits(element.GetAttributeFloat("maxdistfromcenter", 1000.0f)); cohesion = element.GetAttributeFloat("cohesion", 1) / 10; }
public PetBehavior(XElement element, EnemyAIController aiController) { AiController = aiController; AiController.Character.CanBeDragged = true; MaxHappiness = element.GetAttributeFloat("maxhappiness", 100.0f); MaxHunger = element.GetAttributeFloat("maxhunger", 100.0f); Happiness = MaxHappiness * 0.5f; Hunger = MaxHunger * 0.5f; HappinessDecreaseRate = element.GetAttributeFloat("happinessdecreaserate", 0.1f); HungerIncreaseRate = element.GetAttributeFloat("hungerincreaserate", 0.25f); PlayForce = element.GetAttributeFloat("playforce", 15.0f); foreach (XElement subElement in element.Elements()) { switch (subElement.Name.LocalName.ToLowerInvariant()) { case "itemproduction": itemsToProduce.Add(new ItemProduction(subElement)); break; case "eat": Food food = new Food { Tag = subElement.GetAttributeString("tag", ""), Hunger = subElement.GetAttributeFloat("hunger", -1), Happiness = subElement.GetAttributeFloat("happiness", 1), Priority = subElement.GetAttributeFloat("priority", 100), IgnoreContained = subElement.GetAttributeBool("ignorecontained", true) }; string[] requiredHungerStr = subElement.GetAttributeString("requiredhunger", "0-100").Split('-'); food.HungerRange = new Vector2(0, 100); if (requiredHungerStr.Length >= 2) { if (float.TryParse(requiredHungerStr[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float tempF)) { food.HungerRange.X = tempF; } if (float.TryParse(requiredHungerStr[1], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { food.HungerRange.Y = tempF; } } foods.Add(food); break; } } }
public override float GetPriority(Character character) { //clamp the strength to the health of this character //(it doesn't make a difference whether the enemy does 200 or 600 damage, it's one hit kill anyway) float enemyDanger = Math.Min(enemyStrength, character.Health) + enemy.Health / 10.0f; EnemyAIController enemyAI = enemy.AIController as EnemyAIController; if (enemyAI != null) { if (enemyAI.SelectedAiTarget == character.AiTarget) { enemyDanger *= 2.0f; } } return(Math.Max(enemyDanger, AIObjectiveManager.OrderPriority)); }
public LatchOntoAI(XElement element, EnemyAIController enemyAI) { attachToWalls = element.GetAttributeBool("attachtowalls", false); attachToSub = element.GetAttributeBool("attachtosub", false); minDeattachSpeed = element.GetAttributeFloat("mindeattachspeed", 3.0f); maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat("maxdeattachspeed", 10.0f)); damageOnDetach = element.GetAttributeFloat("damageondetach", 0.0f); detachStun = element.GetAttributeFloat("detachstun", 0.0f); localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("localattachpos", Vector2.Zero)); attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat("attachlimbrotation", 0.0f)); if (Enum.TryParse(element.GetAttributeString("attachlimb", "Head"), out LimbType attachLimbType)) { attachLimb = enemyAI.Character.AnimController.GetLimb(attachLimbType); } if (attachLimb == null) { attachLimb = enemyAI.Character.AnimController.MainLimb; } enemyAI.Character.OnDeath += OnCharacterDeath; }
private void CalculateCurrentIntensity(float deltaTime) { intensityUpdateTimer -= deltaTime; if (intensityUpdateTimer > 0.0f) { return; } intensityUpdateTimer = IntensityUpdateInterval; // crew health -------------------------------------------------------- avgCrewHealth = 0.0f; int characterCount = 0; foreach (Character character in Character.CharacterList) { if (character.IsDead) { continue; } #if CLIENT if ((character.AIController is HumanAIController || character.IsRemotePlayer || character == Character.Controlled) && (GameMain.Client?.Character == null || GameMain.Client.Character.TeamID == character.TeamID)) { avgCrewHealth += character.Vitality / character.MaxVitality * (character.IsUnconscious ? 0.5f : 1.0f); characterCount++; } #endif } if (characterCount > 0) { avgCrewHealth = avgCrewHealth / characterCount; } // enemy amount -------------------------------------------------------- enemyDanger = 0.0f; foreach (Character character in Character.CharacterList) { if (character.IsDead || character.IsUnconscious || !character.Enabled) { continue; } EnemyAIController enemyAI = character.AIController as EnemyAIController; if (enemyAI == null) { continue; } if (character.CurrentHull?.Submarine != null && (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine))) { //crawler inside the sub adds 0.1f to enemy danger, mantis 0.25f enemyDanger += enemyAI.CombatStrength / 1000.0f; } else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null) { //enemy outside and targeting the sub or something in it //moloch adds 0.24 to enemy danger, a crawler 0.02 enemyDanger += enemyAI.CombatStrength / 5000.0f; } } enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f); // hull status (gaps, flooding, fire) -------------------------------------------------------- float holeCount = 0.0f; floodingAmount = 0.0f; foreach (Hull hull in Hull.hullList) { if (hull.Submarine == null || hull.Submarine.IsOutpost) { continue; } foreach (Gap gap in hull.ConnectedGaps) { if (!gap.IsRoomToRoom) { holeCount += gap.Open; } } floodingAmount += hull.WaterVolume / hull.Volume / Hull.hullList.Count; fireAmount += hull.FireSources.Sum(fs => fs.Size.X); } //hull integrity at 0.0 if there are 10 or more wide-open holes avgHullIntegrity = MathHelper.Clamp(1.0f - holeCount / 10.0f, 0.0f, 1.0f); //a fire of any size bumps up the fire amount to 20% //if the total width of the fires is 1000 or more, the fire amount is considered to be at 100% fireAmount = MathHelper.Clamp(fireAmount / 1000.0f, fireAmount > 0.0f ? 0.2f : 0.0f, 1.0f); //flooding less than 10% of the sub is ignored //to prevent ballast tanks from affecting the intensity if (floodingAmount < 0.1f) { floodingAmount = 0.0f; } // calculate final intensity -------------------------------------------------------- targetIntensity = ((1.0f - avgCrewHealth) + (1.0f - avgHullIntegrity) + floodingAmount) / 3.0f; targetIntensity += fireAmount * 0.5f; targetIntensity += enemyDanger; targetIntensity = MathHelper.Clamp(targetIntensity, 0.0f, 1.0f); if (targetIntensity > currentIntensity) { //50 seconds for intensity to go from 0.0 to 1.0 currentIntensity = MathHelper.Min(currentIntensity + 0.02f * IntensityUpdateInterval, targetIntensity); } else { //400 seconds for intensity to go from 1.0 to 0.0 currentIntensity = MathHelper.Max(0.0025f * IntensityUpdateInterval, targetIntensity); } }
private static string GetCurrentMusicType() { if (OverrideMusicType != null) { return(OverrideMusicType); } if (Character.Controlled != null && Level.Loaded != null && Level.Loaded.Ruins != null && Level.Loaded.Ruins.Any(r => r.Area.Contains(Character.Controlled.WorldPosition))) { return("ruins"); } Submarine targetSubmarine = Character.Controlled?.Submarine; if ((targetSubmarine != null && targetSubmarine.AtDamageDepth) || (Screen.Selected == GameMain.GameScreen && GameMain.GameScreen.Cam.Position.Y < SubmarineBody.DamageDepth)) { return("deep"); } if (targetSubmarine != null) { List <Reactor> reactors = new List <Reactor>(); foreach (Item item in Item.ItemList) { if (item.Submarine != targetSubmarine) { continue; } var reactor = item.GetComponent <Reactor>(); if (reactor != null) { reactors.Add(reactor); } } if (reactors.All(r => r.Temperature < 1.0f)) { return("repair"); } float floodedArea = 0.0f; float totalArea = 0.0f; foreach (Hull hull in Hull.hullList) { if (hull.Submarine != targetSubmarine) { continue; } floodedArea += hull.WaterVolume; totalArea += hull.Volume; } if (totalArea > 0.0f && floodedArea / totalArea > 0.25f) { return("repair"); } } float enemyDistThreshold = 5000.0f; if (targetSubmarine != null) { enemyDistThreshold = Math.Max(enemyDistThreshold, Math.Max(targetSubmarine.Borders.Width, targetSubmarine.Borders.Height) * 2.0f); } foreach (Character character in Character.CharacterList) { EnemyAIController enemyAI = character.AIController as EnemyAIController; if (enemyAI == null || (enemyAI.AttackHumans < 0.0f && enemyAI.AttackRooms < 0.0f)) { continue; } if (targetSubmarine != null) { if (Vector2.DistanceSquared(character.WorldPosition, targetSubmarine.WorldPosition) < enemyDistThreshold * enemyDistThreshold) { return("monster"); } } else if (Character.Controlled != null) { if (Vector2.DistanceSquared(character.WorldPosition, Character.Controlled.WorldPosition) < enemyDistThreshold * enemyDistThreshold) { return("monster"); } } } return("default"); }
//goes through all the AItargets, evaluates how preferable it is to attack the target, //whether the Character can see/hear the target and chooses the most preferable target within //sight/hearing range public void UpdateTargets(Character character) { var prevAiTarget = selectedAiTarget; selectedAiTarget = null; selectedTargetMemory = null; targetValue = 0.0f; UpdateTargetMemories(); foreach (AITarget target in AITarget.List) { if (Level.Loaded != null && target.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } float valueModifier = 0.0f; float dist = 0.0f; Character targetCharacter = target.Entity as Character; //ignore the aitarget if it is the Character itself if (targetCharacter == character) { continue; } if (targetCharacter != null) { if (targetCharacter.IsDead) { if (eatDeadPriority == 0.0f) { continue; } valueModifier = eatDeadPriority; } else if (targetCharacter.SpeciesName == "human") { if (attackHumans == 0.0f) { continue; } valueModifier = attackHumans; } else { EnemyAIController enemy = targetCharacter.AIController as EnemyAIController; if (enemy != null) { if (enemy.combatStrength > combatStrength) { valueModifier = attackStronger; } else if (enemy.combatStrength < combatStrength) { valueModifier = attackWeaker; } else { continue; } } } } else if (target.Entity != null && attackRooms != 0.0f) { IDamageable targetDamageable = target.Entity as IDamageable; if (targetDamageable != null && targetDamageable.Health <= 0.0f) { continue; } //skip the target if it's a room and the character is already inside a sub if (character.AnimController.CurrentHull != null && target.Entity is Hull) { continue; } valueModifier = attackRooms; } if (valueModifier == 0.0f) { continue; } dist = Vector2.Distance(character.WorldPosition, target.WorldPosition); //if the target has been within range earlier, the character will notice it more easily //(i.e. remember where the target was) if (targetMemories.ContainsKey(target)) { dist *= 0.5f; } //ignore target if it's too far to see or hear if (dist > target.SightRange * sight && dist > target.SoundRange * hearing) { continue; } AITargetMemory targetMemory = FindTargetMemory(target); valueModifier = valueModifier * targetMemory.Priority / dist; if (Math.Abs(valueModifier) > Math.Abs(targetValue)) { Vector2 rayStart = character.AnimController.Limbs[0].SimPosition; Vector2 rayEnd = target.SimPosition; if (target.Entity.Submarine != null && character.Submarine == null) { rayStart -= ConvertUnits.ToSimUnits(target.Entity.Submarine.Position); } Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd); Structure closestStructure = (closestBody == null) ? null : closestBody.UserData as Structure; if (selectedAiTarget == null || Math.Abs(valueModifier) > Math.Abs(targetValue)) { selectedAiTarget = target; selectedTargetMemory = targetMemory; targetValue = valueModifier; } } } if (selectedAiTarget != prevAiTarget) { wallAttackPos = Vector2.Zero; } }
public void Update(EnemyAIController enemyAI, float deltaTime) { Character character = enemyAI.Character; if (character.Submarine != null) { DeattachFromBody(); WallAttachPos = null; return; } if (attachJoints.Count > 0) { if (Math.Sign(attachLimb.Dir) != Math.Sign(jointDir)) { attachJoints[0].LocalAnchorA = new Vector2(-attachJoints[0].LocalAnchorA.X, attachJoints[0].LocalAnchorA.Y); attachJoints[0].ReferenceAngle = -attachJoints[0].ReferenceAngle; jointDir = attachLimb.Dir; } for (int i = 0; i < attachJoints.Count; i++) { //something went wrong, limb body is very far from the joint anchor -> deattach if (Vector2.DistanceSquared(attachJoints[i].WorldAnchorB, attachJoints[i].BodyA.Position) > 10.0f * 10.0f) { DebugConsole.ThrowError("Limb body of the character \"" + character.Name + "\" is very far from the attach joint anchor -> deattach"); DeattachFromBody(); return; } } } attachCooldown -= deltaTime; deattachTimer -= deltaTime; Vector2 transformedAttachPos = wallAttachPos; if (character.Submarine == null && attachTargetSubmarine != null) { transformedAttachPos += ConvertUnits.ToSimUnits(attachTargetSubmarine.Position); } if (transformedAttachPos != Vector2.Zero) { WallAttachPos = transformedAttachPos; } switch (enemyAI.State) { case AIController.AIState.Idle: if (attachToWalls && character.Submarine == null && Level.Loaded != null) { raycastTimer -= deltaTime; //check if there are any walls nearby the character could attach to if (raycastTimer < 0.0f) { wallAttachPos = Vector2.Zero; var cells = Level.Loaded.GetCells(character.WorldPosition, 1); if (cells.Count > 0) { foreach (Voronoi2.VoronoiCell cell in cells) { foreach (Voronoi2.GraphEdge edge in cell.Edges) { if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection)) { attachSurfaceNormal = edge.GetNormal(cell); attachTargetBody = cell.Body; wallAttachPos = ConvertUnits.ToSimUnits(intersection); break; } } if (WallAttachPos != Vector2.Zero) { break; } } } raycastTimer = RaycastInterval; } } else { wallAttachPos = Vector2.Zero; } if (wallAttachPos == Vector2.Zero) { DeattachFromBody(); } else { float dist = Vector2.Distance(character.SimPosition, wallAttachPos); if (dist < Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f) { //close enough to a wall -> attach AttachToBody(character.AnimController.Collider, attachLimb, attachTargetBody, wallAttachPos); enemyAI.SteeringManager.Reset(); } else { //move closer to the wall DeattachFromBody(); enemyAI.SteeringManager.SteeringAvoid(deltaTime, 1.0f, 0.1f); enemyAI.SteeringManager.SteeringSeek(wallAttachPos); } } break; case AIController.AIState.Attack: if (enemyAI.AttackingLimb != null) { if (attachToSub && wallAttachPos != Vector2.Zero && attachTargetBody != null) { // is not attached or is attached to something else if (!IsAttached || IsAttached && attachJoints[0].BodyB == attachTargetBody) { if (Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(transformedAttachPos), enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange) { AttachToBody(character.AnimController.Collider, attachLimb, attachTargetBody, transformedAttachPos); } } } } break; default: WallAttachPos = null; DeattachFromBody(); break; } if (attachTargetBody != null && deattachTimer < 0.0f) { Entity entity = attachTargetBody.UserData as Entity; Submarine attachedSub = entity is Submarine ? (Submarine)entity : entity?.Submarine; if (attachedSub != null) { float velocity = attachedSub.Velocity == Vector2.Zero ? 0.0f : attachedSub.Velocity.Length(); float velocityFactor = (maxDeattachSpeed - minDeattachSpeed <= 0.0f) ? Math.Sign(Math.Abs(velocity) - minDeattachSpeed) : (Math.Abs(velocity) - minDeattachSpeed) / (maxDeattachSpeed - minDeattachSpeed); if (Rand.Range(0.0f, 1.0f) < velocityFactor) { DeattachFromBody(); character.AddDamage(character.WorldPosition, new List <Affliction>() { AfflictionPrefab.InternalDamage.Instantiate(damageOnDetach) }, detachStun, true); attachCooldown = 5.0f; } } deattachTimer = 5.0f; } }
private static string GetCurrentMusicType() { if (OverrideMusicType != null) { return(OverrideMusicType); } if (Screen.Selected != GameMain.GameScreen) { return("menu"); } if (Character.Controlled != null && Level.Loaded != null && Level.Loaded.Ruins != null && Level.Loaded.Ruins.Any(r => r.Area.Contains(Character.Controlled.WorldPosition))) { return("ruins"); } Submarine targetSubmarine = Character.Controlled?.Submarine; if ((targetSubmarine != null && targetSubmarine.AtDamageDepth) || (Screen.Selected == GameMain.GameScreen && GameMain.GameScreen.Cam.Position.Y < SubmarineBody.DamageDepth)) { return("deep"); } if (targetSubmarine != null) { float floodedArea = 0.0f; float totalArea = 0.0f; foreach (Hull hull in Hull.hullList) { if (hull.Submarine != targetSubmarine) { continue; } floodedArea += hull.WaterVolume; totalArea += hull.Volume; } if (totalArea > 0.0f && floodedArea / totalArea > 0.25f) { return("flooded"); } } float enemyDistThreshold = 5000.0f; if (targetSubmarine != null) { enemyDistThreshold = Math.Max(enemyDistThreshold, Math.Max(targetSubmarine.Borders.Width, targetSubmarine.Borders.Height) * 2.0f); } foreach (Character character in Character.CharacterList) { if (character.IsDead || !character.Enabled) { continue; } EnemyAIController enemyAI = character.AIController as EnemyAIController; if (enemyAI == null || (!enemyAI.AttackHumans && !enemyAI.AttackRooms)) { continue; } if (targetSubmarine != null) { if (Vector2.DistanceSquared(character.WorldPosition, targetSubmarine.WorldPosition) < enemyDistThreshold * enemyDistThreshold) { return("monster"); } } else if (Character.Controlled != null) { if (Vector2.DistanceSquared(character.WorldPosition, Character.Controlled.WorldPosition) < enemyDistThreshold * enemyDistThreshold) { return("monster"); } } } if (GameMain.GameSession != null && Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0) { return("start"); } return("default"); }
public void Update(EnemyAIController enemyAI, float deltaTime) { if (character.Submarine != null) { DeattachFromBody(reset: true); return; } if (AttachJoints.Count > 0) { if (Math.Sign(attachLimb.Dir) != Math.Sign(jointDir)) { AttachJoints[0].LocalAnchorA = new Vector2(-AttachJoints[0].LocalAnchorA.X, AttachJoints[0].LocalAnchorA.Y); AttachJoints[0].ReferenceAngle = -AttachJoints[0].ReferenceAngle; jointDir = attachLimb.Dir; } for (int i = 0; i < AttachJoints.Count; i++) { //something went wrong, limb body is very far from the joint anchor -> deattach if (Vector2.DistanceSquared(AttachJoints[i].WorldAnchorB, AttachJoints[i].BodyA.Position) > 10.0f * 10.0f) { #if DEBUG DebugConsole.ThrowError("Limb body of the character \"" + character.Name + "\" is very far from the attach joint anchor -> deattach"); #endif DeattachFromBody(reset: true); return; } } } if (attachCooldown > 0) { attachCooldown -= deltaTime; } if (deattachTimer > 0) { deattachTimer -= deltaTime; } Vector2 transformedAttachPos = wallAttachPos; if (character.Submarine == null && targetSubmarine != null) { transformedAttachPos += ConvertUnits.ToSimUnits(targetSubmarine.Position); } if (transformedAttachPos != Vector2.Zero) { WallAttachPos = transformedAttachPos; } switch (enemyAI.State) { case AIState.Idle: if (AttachToWalls && character.Submarine == null && Level.Loaded != null) { if (!IsAttached) { raycastTimer -= deltaTime; //check if there are any walls nearby the character could attach to if (raycastTimer < 0.0f) { wallAttachPos = Vector2.Zero; var cells = Level.Loaded.GetCells(character.WorldPosition, 1); if (cells.Count > 0) { float closestDist = float.PositiveInfinity; foreach (Voronoi2.VoronoiCell cell in cells) { foreach (Voronoi2.GraphEdge edge in cell.Edges) { if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection)) { Vector2 potentialAttachPos = ConvertUnits.ToSimUnits(intersection); float distSqr = Vector2.DistanceSquared(character.SimPosition, potentialAttachPos); if (distSqr < closestDist) { attachSurfaceNormal = edge.GetNormal(cell); targetBody = cell.Body; wallAttachPos = potentialAttachPos; closestDist = distSqr; } break; } } } } raycastTimer = RaycastInterval; } } } else { wallAttachPos = Vector2.Zero; } if (wallAttachPos == Vector2.Zero || targetBody == null) { DeattachFromBody(reset: false); } else { float squaredDistance = Vector2.DistanceSquared(character.SimPosition, wallAttachPos); float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f; if (squaredDistance < targetDistance * targetDistance) { //close enough to a wall -> attach AttachToBody(wallAttachPos); enemyAI.SteeringManager.Reset(); } else { //move closer to the wall DeattachFromBody(reset: false); enemyAI.SteeringManager.SteeringAvoid(deltaTime, 1.0f, 0.1f); enemyAI.SteeringManager.SteeringSeek(wallAttachPos); } } break; case AIState.Attack: case AIState.Aggressive: if (enemyAI.AttackingLimb != null) { if (AttachToSub && !enemyAI.IsSteeringThroughGap && wallAttachPos != Vector2.Zero && targetBody != null) { // is not attached or is attached to something else if (!IsAttached || IsAttached && AttachJoints[0].BodyB != targetBody) { if (Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(transformedAttachPos), enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange) { AttachToBody(transformedAttachPos); } } } } break; default: DeattachFromBody(reset: true); break; } if (IsAttached && targetBody != null && targetWall != null && targetSubmarine != null && deattachTimer <= 0.0f) { bool deattach = false; // Deattach if the wall is broken enough where we are attached to int targetSection = targetWall.FindSectionIndex(attachLimb.WorldPosition, world: true, clamp: true); if (enemyAI.CanPassThroughHole(targetWall, targetSection)) { deattach = true; attachCooldown = 2; } if (!deattach) { // Deattach if the velocity is high float velocity = targetSubmarine.Velocity == Vector2.Zero ? 0.0f : targetSubmarine.Velocity.Length(); deattach = velocity > maxDeattachSpeed; if (!deattach) { if (velocity > minDeattachSpeed) { float velocityFactor = (maxDeattachSpeed - minDeattachSpeed <= 0.0f) ? Math.Sign(Math.Abs(velocity) - minDeattachSpeed) : (Math.Abs(velocity) - minDeattachSpeed) / (maxDeattachSpeed - minDeattachSpeed); if (Rand.Range(0.0f, 1.0f) < velocityFactor) { deattach = true; character.AddDamage(character.WorldPosition, new List <Affliction>() { AfflictionPrefab.InternalDamage.Instantiate(damageOnDetach) }, detachStun, true); attachCooldown = detachStun * 2; } } } } if (deattach) { DeattachFromBody(reset: true); } deattachTimer = 5.0f; } }