using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; using Barotrauma.MapCreatures.Behavior; namespace Barotrauma.Items.Components { partial class RepairTool : ItemComponent { public enum UseEnvironment { Air, Water, Both, None }; private readonly HashSet fixableEntities; private readonly HashSet nonFixableEntities; private Vector2 pickedPosition; private float activeTimer; private Vector2 debugRayStartPos, debugRayEndPos; private readonly List ignoredBodies = new List(); [Serialize("Both", IsPropertySaveable.No, description: "Can the item be used in air, water or both.")] public UseEnvironment UsableIn { get; set; } [Serialize(0.0f, IsPropertySaveable.No, description: "The distance at which the item can repair targets.")] public float Range { get; set; } [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle when used by a character with sufficient skills to use the tool (in degrees).")] public float Spread { get; set; } [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle when used by a character with insufficient skills to use the tool (in degrees).")] public float UnskilledSpread { get; set; } [Serialize(0.0f, IsPropertySaveable.No, description: "How many units of damage the item removes from structures per second.")] public float StructureFixAmount { get; set; } [Serialize(0.0f, IsPropertySaveable.No, description: "How much damage is applied to ballast flora.")] public float FireDamage { get; set; } [Serialize(0.0f, IsPropertySaveable.No, description: "How many units of damage the item removes from destructible level walls per second.")] public float LevelWallFixAmount { get; set; } [Serialize(0.0f, IsPropertySaveable.No, description: "How much the item decreases the size of fires per second.")] public float ExtinguishAmount { get; set; } [Serialize(0.0f, IsPropertySaveable.No, description: "How much water the item provides to planters per second.")] public float WaterAmount { get; set; } [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position of the barrel as an offset from the item's center (in pixels).")] public Vector2 BarrelPos { get; set; } [Serialize(false, IsPropertySaveable.No, description: "Can the item repair things through walls.")] public bool RepairThroughWalls { get; set; } [Serialize(false, IsPropertySaveable.No, description: "Can the item repair multiple things at once, or will it only affect the first thing the ray from the barrel hits.")] public bool RepairMultiple { get; set; } [Serialize(true, IsPropertySaveable.No, description: "Can the item repair multiple walls at once? Only relevant if RepairMultiple is true.")] public bool RepairMultipleWalls { get; set; } [Serialize(false, IsPropertySaveable.No, description: "Can the item repair things through holes in walls.")] public bool RepairThroughHoles { get; set; } [Serialize(100.0f, IsPropertySaveable.No, description: "How far two walls need to not be considered overlapping and to stop the ray.")] public float MaxOverlappingWallDist { get; set; } [Serialize(1.0f, IsPropertySaveable.No, description: "How fast the tool detaches level resources (e.g. minerals). Acts as a multiplier on the speed: with a value of 2, detaching an item whose DeattachDuration is set to 30 seconds would take 15 seconds.")] public float DeattachSpeed { get; set; } [Serialize(true, IsPropertySaveable.No, description: "Can the item hit doors.")] public bool HitItems { get; set; } [Serialize(false, IsPropertySaveable.No, description: "Can the item hit broken doors.")] public bool HitBrokenDoors { get; set; } [Serialize(false, IsPropertySaveable.No, description: "Should the tool ignore characters? Enabled e.g. for fire extinguisher.")] public bool IgnoreCharacters { get; set; } [Serialize(0.0f, IsPropertySaveable.No, description: "The probability of starting a fire somewhere along the ray fired from the barrel (for example, 0.1 = 10% chance to start a fire during a second of use).")] public float FireProbability { get; set; } [Serialize(0.0f, IsPropertySaveable.No, description: "Force applied to the entity the ray hits.")] public float TargetForce { get; set; } [Serialize(0.0f, IsPropertySaveable.No, description: "Rotation of the barrel in degrees."), Editable(MinValueFloat = 0, MaxValueFloat = 360, VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" })] public float BarrelRotation { get; set; } public Vector2 TransformedBarrelPos { get { if (item.body == null) { return BarrelPos; } Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation + MathHelper.ToRadians(BarrelRotation)); Vector2 flippedPos = BarrelPos; if (item.body.Dir < 0.0f) { flippedPos.X = -flippedPos.X; } return Vector2.Transform(flippedPos, bodyTransform); } } public RepairTool(Item item, ContentXElement element) : base(item, element) { this.item = item; if (element.GetAttribute("limbfixamount") != null) { DebugConsole.ThrowError("Error in item \"" + item.Name + "\" - RepairTool damage should be configured using a StatusEffect with Afflictions, not the limbfixamount attribute.", contentPackage: element.ContentPackage); } fixableEntities = new HashSet(); nonFixableEntities = new HashSet(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "fixable": if (subElement.GetAttribute("name") != null) { DebugConsole.ThrowError("Error in RepairTool " + item.Name + " - use identifiers instead of names to configure fixable entities.", contentPackage: element.ContentPackage); fixableEntities.Add(subElement.GetAttribute("name").Value.ToIdentifier()); } else { foreach (Identifier id in subElement.GetAttributeIdentifierArray("identifier", Array.Empty())) { fixableEntities.Add(id); } } break; case "nonfixable": foreach (Identifier id in subElement.GetAttributeIdentifierArray("identifier", Array.Empty())) { nonFixableEntities.Add(id); } break; } } item.IsShootable = true; item.RequireAimToUse = element.Parent.GetAttributeBool(nameof(item.RequireAimToUse), true); InitProjSpecific(element); } partial void InitProjSpecific(ContentXElement element); public override void Update(float deltaTime, Camera cam) { activeTimer -= deltaTime; if (activeTimer <= 0.0f) { IsActive = false; } } public override bool Use(float deltaTime, Character character = null) { if (character != null) { if (item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) { return false; } } float degreeOfSuccess = character == null ? 0.5f : DegreeOfSuccess(character); bool failed = false; if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) { ApplyStatusEffects(ActionType.OnFailure, deltaTime, character); failed = true; } if (UsableIn == UseEnvironment.None) { ApplyStatusEffects(ActionType.OnFailure, deltaTime, character); failed = true; } if (item.InWater) { if (UsableIn == UseEnvironment.Air) { ApplyStatusEffects(ActionType.OnFailure, deltaTime, character); failed = true; } } else { if (UsableIn == UseEnvironment.Water) { ApplyStatusEffects(ActionType.OnFailure, deltaTime, character); failed = true; } } if (failed) { // Always apply ActionType.OnUse. If doesn't fail, the effect is called later. ApplyStatusEffects(ActionType.OnUse, deltaTime, character); return false; } Vector2 rayStart; Vector2 rayStartWorld; Vector2 sourcePos = character?.AnimController == null ? item.SimPosition : character.AnimController.AimSourceSimPos; Vector2 barrelPos = item.SimPosition + ConvertUnits.ToSimUnits(TransformedBarrelPos); //make sure there's no obstacles between the base of the item (or the shoulder of the character) and the end of the barrel if (Submarine.PickBody(sourcePos, barrelPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null) { //no obstacles -> we start the raycast at the end of the barrel rayStart = ConvertUnits.ToSimUnits(item.Position + TransformedBarrelPos); rayStartWorld = ConvertUnits.ToSimUnits(item.WorldPosition + TransformedBarrelPos); } else { rayStart = rayStartWorld = Submarine.LastPickedPosition + Submarine.LastPickedNormal * 0.1f; if (item.Submarine != null) { rayStartWorld += item.Submarine.SimPosition; } } //if the calculated barrel pos is in another hull, use the origin of the item to make sure the particles don't end up in an incorrect hull if (item.CurrentHull != null) { var barrelHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(rayStartWorld), item.CurrentHull, useWorldCoordinates: true); if (barrelHull != null && barrelHull != item.CurrentHull) { if (MathUtils.GetLineRectangleIntersection(ConvertUnits.ToDisplayUnits(sourcePos), ConvertUnits.ToDisplayUnits(rayStart), item.CurrentHull.Rect, out Vector2 hullIntersection)) { if (!item.CurrentHull.ConnectedGaps.Any(g => g.Open > 0.0f && Submarine.RectContains(g.Rect, hullIntersection))) { Vector2 rayDir = rayStart.NearlyEquals(sourcePos) ? Vector2.Zero : Vector2.Normalize(rayStart - sourcePos); rayStartWorld = ConvertUnits.ToSimUnits(hullIntersection - rayDir * 5.0f); if (item.Submarine != null) { rayStartWorld += item.Submarine.SimPosition; } } } } } float spread = MathHelper.ToRadians(MathHelper.Lerp(UnskilledSpread, Spread, degreeOfSuccess)); float angle = MathHelper.ToRadians(BarrelRotation) + spread * Rand.Range(-0.5f, 0.5f); float dir = 1; if (item.body != null) { angle += item.body.Rotation; dir = item.body.Dir; } Vector2 rayEnd = rayStartWorld + ConvertUnits.ToSimUnits(new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * Range * dir); ignoredBodies.Clear(); if (character != null) { foreach (Limb limb in character.AnimController.Limbs) { if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) continue; ignoredBodies.Add(limb.body.FarseerBody); } ignoredBodies.Add(character.AnimController.Collider.FarseerBody); } IsActive = true; activeTimer = 0.1f; debugRayStartPos = ConvertUnits.ToDisplayUnits(rayStartWorld); debugRayEndPos = ConvertUnits.ToDisplayUnits(rayEnd); Submarine parentSub = character?.Submarine ?? item.Submarine; if (parentSub == null) { foreach (Submarine sub in Submarine.Loaded) { Rectangle subBorders = sub.Borders; subBorders.Location += new Point((int)sub.WorldPosition.X, (int)sub.WorldPosition.Y - sub.Borders.Height); if (!MathUtils.CircleIntersectsRectangle(item.WorldPosition, Range * 5.0f, subBorders)) { continue; } Repair(rayStartWorld - sub.SimPosition, rayEnd - sub.SimPosition, deltaTime, character, degreeOfSuccess, ignoredBodies); } Repair(rayStartWorld, rayEnd, deltaTime, character, degreeOfSuccess, ignoredBodies); } else { Repair(rayStartWorld - parentSub.SimPosition, rayEnd - parentSub.SimPosition, deltaTime, character, degreeOfSuccess, ignoredBodies); } UseProjSpecific(deltaTime, rayStartWorld); return true; } partial void UseProjSpecific(float deltaTime, Vector2 raystart); private static readonly List hitBodies = new List(); private readonly HashSet hitCharacters = new HashSet(); private readonly List fireSourcesInRange = new List(); private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List ignoredBodies) { var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepairableWall; if (!IgnoreCharacters) { collisionCategories |= Physics.CollisionCharacter; } //if the item can cut off limbs, activate nearby bodies to allow the raycast to hit them if (statusEffectLists != null) { static bool CanSeverJoints(ActionType type, Dictionary> effectList) => effectList.TryGetValue(type, out List effects) && effects.Any(e => e.SeverLimbsProbability > 0); if (CanSeverJoints(ActionType.OnUse, statusEffectLists) || CanSeverJoints(ActionType.OnSuccess, statusEffectLists)) { float rangeSqr = ConvertUnits.ToSimUnits(Range); rangeSqr *= rangeSqr; foreach (Character c in Character.CharacterList) { if (!c.Enabled || !c.AnimController.BodyInRest) { continue; } //do a broad check first if (Math.Abs(c.WorldPosition.X - item.WorldPosition.X) > 1000.0f) { continue; } if (Math.Abs(c.WorldPosition.Y - item.WorldPosition.Y) > 1000.0f) { continue; } foreach (Limb limb in c.AnimController.Limbs) { if (Vector2.DistanceSquared(limb.SimPosition, item.SimPosition) < rangeSqr && Vector2.Dot(rayEnd - rayStart, limb.SimPosition - rayStart) > 0) { c.AnimController.BodyInRest = false; break; } } } } } float lastPickedFraction = 0.0f; if (RepairMultiple) { var bodies = Submarine.PickBodies(rayStart, rayEnd, ignoredBodies, collisionCategories, ignoreSensors: false, customPredicate: (Fixture f) => { if (f.IsSensor) { if (RepairThroughHoles && f.Body?.UserData is Structure) { return false; } if (f.Body?.UserData is PhysicsBody) { return false; } } if (f.Body?.UserData is Item it && it.GetComponent() != null) { return false; } if (f.Body?.UserData as string == "ruinroom") { return false; } if (f.Body?.UserData is VineTile && !(FireDamage > 0)) { return false; } return true; }, allowInsideFixture: true); hitBodies.Clear(); hitBodies.AddRange(bodies.Distinct()); lastPickedFraction = Submarine.LastPickedFraction; Type lastHitType = null; hitCharacters.Clear(); foreach (Body body in hitBodies) { Type bodyType = body.UserData?.GetType(); if (!RepairThroughWalls && bodyType != null && bodyType != lastHitType) { //stop the ray if it already hit a door/wall and is now about to hit some other type of entity if (lastHitType == typeof(Item) || lastHitType == typeof(Structure)) { break; } } if (!RepairMultipleWalls && (bodyType == typeof(Structure) || (body.UserData as Item)?.GetComponent() != null)) { break; } Character hitCharacter = null; if (body.UserData is Limb limb) { hitCharacter = limb.character; } else if (body.UserData is Character character) { hitCharacter = character; } //only do damage once to each character even if they ray hit multiple limbs if (hitCharacter != null) { if (hitCharacters.Contains(hitCharacter)) { continue; } hitCharacters.Add(hitCharacter); } //if repairing through walls is not allowed and the next wall is more than 100 pixels away from the previous one, stop here //(= repairing multiple overlapping walls is allowed as long as the edges of the walls are less than MaxOverlappingWallDist pixels apart) float thisBodyFraction = Submarine.LastPickedBodyDist(body); if (!RepairThroughWalls && lastHitType == typeof(Structure) && Range * (thisBodyFraction - lastPickedFraction) > MaxOverlappingWallDist) { break; } pickedPosition = rayStart + (rayEnd - rayStart) * thisBodyFraction; if (FixBody(user, pickedPosition, deltaTime, degreeOfSuccess, body)) { lastPickedFraction = thisBodyFraction; if (bodyType != null) { lastHitType = bodyType; } } } } else { var pickedBody = Submarine.PickBody(rayStart, rayEnd, ignoredBodies, collisionCategories, ignoreSensors: false, customPredicate: (Fixture f) => { if (f.IsSensor) { if (RepairThroughHoles && f.Body?.UserData is Structure) { return false; } if (f.Body?.UserData is PhysicsBody) { return false; } } if (f.Body?.UserData as string == "ruinroom") { return false; } if (f.Body?.UserData is VineTile && !(FireDamage > 0)) { return false; } if (f.Body?.UserData is Item targetItem) { if (!HitItems) { return false; } if (HitBrokenDoors) { if (targetItem.GetComponent() == null && targetItem.Condition <= 0) { return false; } } else { if (targetItem.Condition <= 0) { return false; } } } return f.Body?.UserData != null; }, allowInsideFixture: true); pickedPosition = Submarine.LastPickedPosition; FixBody(user, pickedPosition, deltaTime, degreeOfSuccess, pickedBody); lastPickedFraction = Submarine.LastPickedFraction; } if (ExtinguishAmount > 0.0f && item.CurrentHull != null) { fireSourcesInRange.Clear(); //step along the ray in 10% intervals, collecting all fire sources in the range for (float x = 0.0f; x <= lastPickedFraction; x += 0.1f) { Vector2 displayPos = ConvertUnits.ToDisplayUnits(rayStart + (rayEnd - rayStart) * x); if (item.CurrentHull.Submarine != null) { displayPos += item.CurrentHull.Submarine.Position; } Hull hull = Hull.FindHull(displayPos, item.CurrentHull); if (hull == null) continue; foreach (FireSource fs in hull.FireSources) { if (fs.IsInDamageRange(displayPos, 100.0f) && !fireSourcesInRange.Contains(fs)) { fireSourcesInRange.Add(fs); } } foreach (FireSource fs in hull.FakeFireSources) { if (fs.IsInDamageRange(displayPos, 100.0f) && !fireSourcesInRange.Contains(fs)) { fireSourcesInRange.Add(fs); } } } foreach (FireSource fs in fireSourcesInRange) { fs.Extinguish(deltaTime, ExtinguishAmount); #if SERVER if (!(fs is DummyFireSource)) { GameMain.Server.KarmaManager.OnExtinguishingFire(user, deltaTime); } #endif } } if (WaterAmount > 0.0f && item.Submarine != null) { Vector2 pos = ConvertUnits.ToDisplayUnits(rayStart + item.Submarine.SimPosition); // Could probably be done much efficiently here foreach (Item it in Item.ItemList) { if (it.Submarine == item.Submarine && it.GetComponent() is { } planter) { if (it.GetComponent() is { } holdable && holdable.Attachable && !holdable.Attached) { continue; } Rectangle collisionRect = it.WorldRect; collisionRect.Y -= collisionRect.Height; if (collisionRect.Left < pos.X && collisionRect.Right > pos.X && collisionRect.Bottom < pos.Y) { Body collision = Submarine.PickBody(rayStart, it.SimPosition, ignoredBodies, collisionCategories); if (collision == null) { for (var i = 0; i < planter.GrowableSeeds.Length; i++) { Growable seed = planter.GrowableSeeds[i]; if (seed == null || seed.Decayed) { continue; } seed.Health += WaterAmount * deltaTime; #if CLIENT float barOffset = 10f * GUI.Scale; Vector2 offset = planter.PlantSlots.ContainsKey(i) ? planter.PlantSlots[i].Offset : Vector2.Zero; user?.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxHealth, GUIStyle.Blue, GUIStyle.Blue, "progressbar.watering"); #endif } } } } } } if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { if (Rand.Range(0.0f, 1.0f) < FireProbability * deltaTime && item.CurrentHull != null) { Vector2 displayPos = ConvertUnits.ToDisplayUnits(rayStart + (rayEnd - rayStart) * lastPickedFraction * 0.9f); if (item.CurrentHull.Submarine != null) { displayPos += item.CurrentHull.Submarine.Position; } new FireSource(displayPos, sourceCharacter: user); } } } private bool FixBody(Character user, Vector2 hitPosition, float deltaTime, float degreeOfSuccess, Body targetBody) { if (targetBody?.UserData == null) { return false; } if (targetBody.UserData is Structure targetStructure) { if (targetStructure.IsPlatform) { return false; } int sectionIndex = targetStructure.FindSectionIndex(ConvertUnits.ToDisplayUnits(pickedPosition)); if (sectionIndex < 0) { return false; } if (!fixableEntities.Contains("structure") && !fixableEntities.Contains(targetStructure.Prefab.Identifier)) { return true; } if (nonFixableEntities.Contains(targetStructure.Prefab.Identifier) || nonFixableEntities.Any(t => targetStructure.Tags.Contains(t))) { return false; } ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, structure: targetStructure); ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, structure: targetStructure); FixStructureProjSpecific(user, deltaTime, targetStructure, sectionIndex); float structureFixAmount = StructureFixAmount; if (structureFixAmount >= 0f) { structureFixAmount *= 1 + user.GetStatValue(StatTypes.RepairToolStructureRepairMultiplier); structureFixAmount *= 1 + item.GetQualityModifier(Quality.StatType.RepairToolStructureRepairMultiplier); } else { structureFixAmount *= 1 + user.GetStatValue(StatTypes.RepairToolStructureDamageMultiplier); structureFixAmount *= 1 + item.GetQualityModifier(Quality.StatType.RepairToolStructureDamageMultiplier); } var didLeak = targetStructure.SectionIsLeakingFromOutside(sectionIndex); targetStructure.AddDamage(sectionIndex, -structureFixAmount * degreeOfSuccess, user); if (didLeak && !targetStructure.SectionIsLeakingFromOutside(sectionIndex)) { user.CheckTalents(AbilityEffectType.OnRepairedOutsideLeak); } //if the next section is small enough, apply the effect to it as well //(to make it easier to fix a small "left-over" section) for (int i = -1; i < 2; i += 2) { int nextSectionLength = targetStructure.SectionLength(sectionIndex + i); if ((sectionIndex == 1 && i == -1) || (sectionIndex == targetStructure.SectionCount - 2 && i == 1) || (nextSectionLength > 0 && nextSectionLength < Structure.WallSectionSize * 0.3f)) { //targetStructure.HighLightSection(sectionIndex + i); targetStructure.AddDamage(sectionIndex + i, -structureFixAmount * degreeOfSuccess); } } return true; } else if (targetBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible) { if (Level.Loaded?.ExtraWalls.Find(w => w.Body == cell.Body) is DestructibleLevelWall levelWall) { levelWall.AddDamage(-LevelWallFixAmount * deltaTime, ConvertUnits.ToDisplayUnits(hitPosition)); } return true; } else if (targetBody.UserData is LevelObject levelObject && levelObject.Prefab.TakeLevelWallDamage) { levelObject.AddDamage(-LevelWallFixAmount, deltaTime, item); return true; } else if (targetBody.UserData is Character targetCharacter) { if (targetCharacter.Removed) { return false; } targetCharacter.LastDamageSource = item; Limb closestLimb = null; float closestDist = float.MaxValue; foreach (Limb limb in targetCharacter.AnimController.Limbs) { if (limb.Removed || limb.IgnoreCollisions || limb.Hidden || limb.IsSevered) { continue; } float dist = Vector2.DistanceSquared(item.SimPosition, limb.SimPosition); if (dist < closestDist) { closestLimb = limb; closestDist = dist; } } if (closestLimb != null && !MathUtils.NearlyEqual(TargetForce, 0.0f)) { Vector2 dir = closestLimb.WorldPosition - item.WorldPosition; dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir); closestLimb.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f); } ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetCharacter, limb: closestLimb); ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, character: targetCharacter, limb: closestLimb); FixCharacterProjSpecific(user, deltaTime, targetCharacter); return true; } else if (targetBody.UserData is Limb targetLimb) { if (targetLimb.character == null || targetLimb.character.Removed) { return false; } if (!MathUtils.NearlyEqual(TargetForce, 0.0f)) { Vector2 dir = targetLimb.WorldPosition - item.WorldPosition; dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir); targetLimb.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f); } targetLimb.character.LastDamageSource = item; ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetLimb.character, limb: targetLimb); ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, character: targetLimb.character, limb: targetLimb); FixCharacterProjSpecific(user, deltaTime, targetLimb.character); return true; } else if (targetBody.UserData is Item targetItem) { if (!HitItems || !targetItem.IsInteractable(user)) { return false; } var levelResource = targetItem.GetComponent(); if (levelResource != null && levelResource.Attached && levelResource.RequiredItems.Any() && levelResource.HasRequiredItems(user, addMessage: false)) { float addedDetachTime = deltaTime * DeattachSpeed * (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier)); levelResource.DeattachTimer += addedDetachTime; #if CLIENT if (targetItem.Prefab.ShowHealthBar && Character.Controlled != null && (user == Character.Controlled || Character.Controlled.CanSeeTarget(item))) { Character.Controlled.UpdateHUDProgressBar( this, targetItem.WorldPosition, levelResource.DeattachTimer / levelResource.DeattachDuration, GUIStyle.Red, GUIStyle.Green, "progressbar.deattaching"); } #endif FixItemProjSpecific(user, deltaTime, targetItem, showProgressBar: false); return true; } if (!targetItem.Prefab.DamagedByRepairTools) { return false; } if (HitBrokenDoors) { if (targetItem.GetComponent() == null && targetItem.Condition <= 0) { return false; } } else { if (targetItem.Condition <= 0) { return false; } } targetItem.IsHighlighted = true; ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem); ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, targetItem); if (targetItem.body != null && !MathUtils.NearlyEqual(TargetForce, 0.0f)) { Vector2 dir = targetItem.WorldPosition - item.WorldPosition; dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir); targetItem.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f); } FixItemProjSpecific(user, deltaTime, targetItem, showProgressBar: true); return true; } else if (targetBody.UserData is BallastFloraBranch branch) { if (branch.ParentBallastFlora is { } ballastFlora) { ballastFlora.DamageBranch(branch, FireDamage * deltaTime, BallastFloraBehavior.AttackType.Fire, user); } } return false; } partial void FixStructureProjSpecific(Character user, float deltaTime, Structure targetStructure, int sectionIndex); partial void FixCharacterProjSpecific(Character user, float deltaTime, Character targetCharacter); partial void FixItemProjSpecific(Character user, float deltaTime, Item targetItem, bool showProgressBar); private float sinTime; private float repairTimer; private Gap previousGap; private readonly float repairTimeOut = 5; public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (objective.OperateTarget is not Gap leak) { Reset(); return true; } if (leak.Submarine == null || leak.Submarine != character.Submarine) { Reset(); return true; } if (leak != previousGap) { Reset(); previousGap = leak; } Vector2 fromCharacterToLeak = leak.WorldPosition - character.AnimController.AimSourceWorldPos; float dist = fromCharacterToLeak.Length(); float reach = AIObjectiveFixLeak.CalculateReach(this, character); if (dist > reach * 2) { // Too far away -> consider this done and hope the AI is smart enough to move closer Reset(); return true; } character.AIController.SteeringManager.Reset(); if (character.AIController.SteeringManager is IndoorsSteeringManager pathSteering) { pathSteering.ResetPath(); } if (!character.AnimController.InWater) { // TODO: use the collider size? if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController humanAnim && Math.Abs(fromCharacterToLeak.X) < 100.0f && fromCharacterToLeak.Y < 0.0f && fromCharacterToLeak.Y > -150.0f) { humanAnim.Crouching = true; } } if (!character.IsClimbing) { if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.InWater)) { // Steer closer Vector2 dir = Vector2.Normalize(fromCharacterToLeak); if (!character.InWater) { dir.Y = 0; } character.AIController.SteeringManager.SteeringManual(deltaTime, dir); } else if (dist < reach * 0.25f && !character.IsClimbing) { // Too close -> steer away character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition)); } } if (dist <= reach || character.IsClimbing) { // In range character.CursorPosition = leak.WorldPosition; if (character.Submarine != null) { character.CursorPosition -= character.Submarine.Position; } character.CursorPosition += VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2); if (character.AnimController.InWater) { var torso = character.AnimController.GetLimb(LimbType.Torso); // Turn facing the target when not moving (handled in the animcontroller if not moving) Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); Vector2 diff = (mousePos - torso.SimPosition) * character.AnimController.Dir; float newRotation = MathUtils.VectorToAngle(diff); character.AnimController.Collider.SmoothRotate(newRotation, 5.0f); if (VectorExtensions.Angle(VectorExtensions.Forward(torso.body.TransformedRotation), fromCharacterToLeak) < MathHelper.PiOver4) { // Swim past Vector2 moveDir = leak.IsHorizontal ? Vector2.UnitY : Vector2.UnitX; moveDir *= character.AnimController.Dir; character.AIController.SteeringManager.SteeringManual(deltaTime, moveDir); } } if (item.RequireAimToUse) { character.SetInput(InputType.Aim, false, true); sinTime += deltaTime * 5; } // Press the trigger only when the tool is approximately facing the target. Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition; var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak); bool repair = true; if (angle < MathHelper.PiOver4) { if (Submarine.PickBody(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionWall, allowInsideFixture: true)?.UserData is Item i) { if (i.GetComponent() is Door door && !door.CanBeTraversed ) { // Hit a door, don't repair so that we don't weld it shut. if (door.Stuck > 90) { // Almost stuck -> just abandon. return false; } if (door.Stuck > 50) { repair = false; } } } if (repair) { // Check that we don't hit any friendlies if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit => { if (hit.UserData is Character c) { if (c == character) { return false; } return HumanAIController.IsFriendly(character, c); } return false; })) { character.SetInput(InputType.Shoot, false, true); Use(deltaTime, character); } } repairTimer += deltaTime; if (repairTimer > repairTimeOut) { #if DEBUG DebugConsole.NewMessage($"{character.Name}: timed out while welding a leak in {leak.FlowTargetHull.DisplayName}.", color: Color.Yellow); #endif Reset(); return true; } } } else { // Reset the timer so that we don't time out if the water forces push us away repairTimer = 0; } bool leakFixed = (leak.Open <= 0.0f || leak.Removed) && (leak.ConnectedWall == null || leak.ConnectedWall.Sections.Max(s => s.damage) < 0.1f); if (leakFixed && leak.FlowTargetHull?.DisplayName != null && character.IsOnPlayerTeam) { if (!leak.FlowTargetHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f)) { character.Speak(TextManager.GetWithVariable("DialogLeaksFixed", "[roomname]", leak.FlowTargetHull.DisplayName, FormatCapitals.Yes).Value, null, 0.0f, "leaksfixed".ToIdentifier(), 10.0f); } else { character.Speak(TextManager.GetWithVariable("DialogLeakFixed", "[roomname]", leak.FlowTargetHull.DisplayName, FormatCapitals.Yes).Value, null, 0.0f, "leakfixed".ToIdentifier(), 10.0f); } } return leakFixed; void Reset() { sinTime = 0; repairTimer = 0; } } private static List currentTargets = new List(); private void ApplyStatusEffectsOnTarget(Character user, float deltaTime, ActionType actionType, Item targetItem = null, Character character = null, Limb limb = null, Structure structure = null) { if (statusEffectLists == null) { return; } if (!statusEffectLists.TryGetValue(actionType, out List statusEffects)) { return; } foreach (StatusEffect effect in statusEffects) { currentTargets.Clear(); effect.SetUser(user); if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) { if (targetItem != null) { currentTargets.AddRange(targetItem.AllPropertyObjects); } if (structure != null) { currentTargets.Add(structure); } if (character != null) { currentTargets.Add(character); } effect.Apply(actionType, deltaTime, item, currentTargets); } else if (effect.HasTargetType(StatusEffect.TargetType.Character)) { currentTargets.Add(user); effect.Apply(actionType, deltaTime, item, currentTargets); } else if (effect.HasTargetType(StatusEffect.TargetType.Limb)) { currentTargets.Add(limb); effect.Apply(actionType, deltaTime, item, currentTargets); } #if CLIENT if (user == null) { return; } // Hard-coded progress bars for welding doors stuck. // A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml. foreach (ISerializableEntity target in currentTargets) { if (target is not Door door) { continue; } if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; } foreach (var propertyEffect in effect.PropertyEffects) { if (propertyEffect.propertyName != "stuck") { continue; } if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyEffect.propertyName, out SerializableProperty property)) { continue; } object value = property.GetValue(target); if (door.Stuck > 0) { bool isCutting = propertyEffect.value is float and < 0; var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White, textTag: isCutting ? "progressbar.cutting" : "progressbar.welding"); if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } if (!isCutting) { HintManager.OnWeldingDoor(user, door); } } } } #endif } } } }