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;
        }
Exemple #2
0
 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));
        }
Exemple #5
0
        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;
        }
Exemple #6
0
        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);
            }
        }
Exemple #7
0
        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");
        }
Exemple #8
0
        //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;
            }
        }
Exemple #9
0
        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;
            }
        }
Exemple #10
0
        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;
            }
        }