From c456fa3c9035f38d6c6a23215eb45c3636aedc3e Mon Sep 17 00:00:00 2001 From: Regalis Date: Mon, 23 Nov 2015 01:22:38 +0200 Subject: [PATCH] Human AI with pathfinding and room hazard avoidance --- Subsurface/Barotrauma.csproj | 4 + .../Source/Characters/AI/AIController.cs | 7 +- .../Source/Characters/AI/EnemyAIController.cs | 2 + .../Source/Characters/AI/HumanAIController.cs | 89 ++++++++++++ .../Characters/AI/Objectives/AIObjective.cs | 14 +- .../AI/Objectives/AIObjectiveFindSafety.cs | 89 ++++++++++++ .../AI/Objectives/AIObjectiveGoTo.cs | 33 +++++ .../AI/Objectives/AIObjectiveManager.cs | 46 ++++++ .../AI/Objectives/AIObjectiveOperateItem.cs | 8 ++ Subsurface/Source/Characters/AI/PathFinder.cs | 38 ++++- .../Characters/AI/PathSteeringManager.cs | 95 ++++++++++++- Subsurface/Source/Characters/AICharacter.cs | 40 +++--- Subsurface/Source/Characters/Character.cs | 53 +++++-- Subsurface/Source/Characters/Ragdoll.cs | 25 ++-- Subsurface/Source/DebugConsole.cs | 4 +- Subsurface/Source/Events/MonsterEvent.cs | 2 +- .../Source/Events/Quests/MonsterQuest.cs | 2 +- Subsurface/Source/GameSession/CrewManager.cs | 2 +- .../GameSession/GameModes/TutorialMode.cs | 4 +- Subsurface/Source/Items/Components/Door.cs | 1 + .../Items/Components/Holdable/Pickable.cs | 2 +- .../Items/Components/Signal/Connection.cs | 4 +- .../Components/Signal/ConnectionPanel.cs | 20 +-- Subsurface/Source/Items/Item.cs | 4 +- Subsurface/Source/Map/Gap.cs | 40 +----- Subsurface/Source/Map/MapEntityPrefab.cs | 1 + Subsurface/Source/Map/WayPoint.cs | 132 +++++++++++++++++- Subsurface/Source/Networking/GameClient.cs | 4 +- Subsurface/Source/Networking/GameServer.cs | 4 +- Subsurface/Source/PlayerInput.cs | 2 +- Subsurface/Source/Screens/EditMapScreen.cs | 20 ++- Subsurface_Solution.v12.suo | Bin 796160 -> 803840 bytes 32 files changed, 665 insertions(+), 126 deletions(-) create mode 100644 Subsurface/Source/Characters/AI/HumanAIController.cs create mode 100644 Subsurface/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs create mode 100644 Subsurface/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs create mode 100644 Subsurface/Source/Characters/AI/Objectives/AIObjectiveManager.cs diff --git a/Subsurface/Barotrauma.csproj b/Subsurface/Barotrauma.csproj index 0beadea18..f75ce7183 100644 --- a/Subsurface/Barotrauma.csproj +++ b/Subsurface/Barotrauma.csproj @@ -60,6 +60,10 @@ + + + + diff --git a/Subsurface/Source/Characters/AI/AIController.cs b/Subsurface/Source/Characters/AI/AIController.cs index 5ecd56a8b..d0991bba7 100644 --- a/Subsurface/Source/Characters/AI/AIController.cs +++ b/Subsurface/Source/Characters/AI/AIController.cs @@ -16,6 +16,11 @@ namespace Barotrauma protected SteeringManager steeringManager; + public SteeringManager SteeringManager + { + get { return steeringManager; } + } + public Vector2 Steering { get { return Character.AnimController.TargetMovement; } @@ -41,8 +46,6 @@ namespace Barotrauma public AIController (Character c) { Character = c; - - steeringManager = new SteeringManager(this); } public virtual void DebugDraw(SpriteBatch spriteBatch) { } diff --git a/Subsurface/Source/Characters/AI/EnemyAIController.cs b/Subsurface/Source/Characters/AI/EnemyAIController.cs index c692e491e..8e57c410e 100644 --- a/Subsurface/Source/Characters/AI/EnemyAIController.cs +++ b/Subsurface/Source/Characters/AI/EnemyAIController.cs @@ -80,6 +80,8 @@ namespace Barotrauma sight = ToolBox.GetAttributeFloat(aiElement, "sight", 0.0f); hearing = ToolBox.GetAttributeFloat(aiElement, "hearing", 0.0f); + steeringManager = new SteeringManager(this); + state = AiState.None; } diff --git a/Subsurface/Source/Characters/AI/HumanAIController.cs b/Subsurface/Source/Characters/AI/HumanAIController.cs new file mode 100644 index 000000000..e67a0d049 --- /dev/null +++ b/Subsurface/Source/Characters/AI/HumanAIController.cs @@ -0,0 +1,89 @@ +using FarseerPhysics; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Barotrauma +{ + class HumanAIController : AIController + { + const float UpdateObjectiveInterval = 0.5f; + + private AIObjectiveManager objectiveManager; + + private AITarget selectedAiTarget; + + private float updateObjectiveTimer; + + public HumanAIController(Character c) : base(c) + { + steeringManager = new PathSteeringManager(this); + + objectiveManager = new AIObjectiveManager(c); + objectiveManager.AddObjective(new AIObjectiveFindSafety()); + } + + public override void Update(float deltaTime) + { + if (updateObjectiveTimer>0.0f) + { + updateObjectiveTimer -= deltaTime; + } + else + { + objectiveManager.UpdateObjectives(); + updateObjectiveTimer = UpdateObjectiveInterval; + } + + objectiveManager.DoCurrentObjective(deltaTime); + + //if (Character.Controlled != null) + //{ + // steeringManager.SteeringSeek(Character.Controlled.Position); + //} + + Character.AnimController.IgnorePlatforms = (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); + + if (Math.Abs(Character.AnimController.TargetMovement.X)>0.1f) + { + Character.AnimController.TargetDir = Character.AnimController.TargetMovement.X > 0.0f ? Direction.Right : Direction.Left; + } + + steeringManager.Update(); + } + + public override void SelectTarget(AITarget target) + { + selectedAiTarget = target; + } + + public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) + { + + if (selectedAiTarget != null) + { + GUI.DrawLine(spriteBatch, new Vector2(Character.Position.X, -Character.Position.Y), ConvertUnits.ToDisplayUnits(new Vector2(selectedAiTarget.Position.X, -selectedAiTarget.Position.Y)), Color.Red); + } + + PathSteeringManager pathSteering = steeringManager as PathSteeringManager; + if (pathSteering == null || pathSteering.CurrentPath == null || pathSteering.CurrentPath.CurrentNode==null) return; + + GUI.DrawLine(spriteBatch, + new Vector2(Character.Position.X, -Character.Position.Y), + new Vector2(pathSteering.CurrentPath.CurrentNode.Position.X, -pathSteering.CurrentPath.CurrentNode.Position.Y), + Color.LightGreen); + + + for (int i = 1; i < pathSteering.CurrentPath.Nodes.Count; i++) + { + GUI.DrawLine(spriteBatch, + new Vector2(pathSteering.CurrentPath.Nodes[i].Position.X, -pathSteering.CurrentPath.Nodes[i].Position.Y), + new Vector2(pathSteering.CurrentPath.Nodes[i - 1].Position.X, -pathSteering.CurrentPath.Nodes[i-1].Position.Y), + Color.LightGreen); + } + + } + } +} diff --git a/Subsurface/Source/Characters/AI/Objectives/AIObjective.cs b/Subsurface/Source/Characters/AI/Objectives/AIObjective.cs index e00457bb8..420d1441c 100644 --- a/Subsurface/Source/Characters/AI/Objectives/AIObjective.cs +++ b/Subsurface/Source/Characters/AI/Objectives/AIObjective.cs @@ -9,6 +9,8 @@ namespace Barotrauma { protected List subObjectives; + protected float priority; + public virtual bool IsCompleted() { return false; @@ -21,7 +23,7 @@ namespace Barotrauma /// /// makes the character act according to the objective, or according to any subobjectives that - /// need to be completed before this one (starting from the one with the highest priority) + /// need to be completed before this one /// /// the character who's trying to achieve the objective public void TryComplete(float deltaTime, Character character) @@ -38,5 +40,15 @@ namespace Barotrauma } protected virtual void Act(float deltaTime, Character character) { } + + public virtual float GetPriority(Character character) + { + return 0.0f; + } + + public virtual bool IsDuplicate(AIObjective otherObjective) + { + return true; + } } } diff --git a/Subsurface/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Subsurface/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs new file mode 100644 index 000000000..e79d065a3 --- /dev/null +++ b/Subsurface/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -0,0 +1,89 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Barotrauma +{ + class AIObjectiveFindSafety : AIObjective + { + const float SearchHullInterval = 1.0f; + const float MinSafety = 50.0f; + + AIObjectiveGoTo gotoObjective; + + float currenthullSafety; + + float searchHullTimer; + + protected override void Act(float deltaTime, Character character) + { + if (character.AnimController.CurrentHull == null || GetHullSafety(character.AnimController.CurrentHull) > MinSafety) + { + character.AIController.SteeringManager.SteeringSeek(character.AnimController.CurrentHull.Position); + + gotoObjective = null; + return; + } + + if (searchHullTimer>0.0f) + { + searchHullTimer -= deltaTime; + return; + } + + searchHullTimer = SearchHullInterval; + + Hull bestHull = null; + float bestValue = currenthullSafety; + + foreach (Hull hull in Hull.hullList) + { + if (hull == character.AnimController.CurrentHull) continue; + + float hullValue = GetHullSafety(hull); + hullValue -= (float)Math.Sqrt(Math.Abs(character.Position.X- hull.Position.X)); + hullValue -= (float)Math.Sqrt(Math.Abs(character.Position.Y - hull.Position.Y)*2.0f); + + if (bestHull==null || hullValue > bestValue) + { + bestHull = hull; + bestValue = hullValue; + } + } + + if (bestHull != null) + { + gotoObjective = new AIObjectiveGoTo(bestHull.AiTarget, character); + //character.AIController.SelectTarget(bestHull.AiTarget); + } + + gotoObjective.TryComplete(deltaTime, character); + } + + public override float GetPriority(Character character) + { + if (character.AnimController.CurrentHull == null) return 0.0f; + currenthullSafety = GetHullSafety(character.AnimController.CurrentHull); + priority = 100.0f - currenthullSafety; + return priority; + } + + private float GetHullSafety(Hull hull) + { + float waterPercentage = (hull.Volume / hull.FullVolume)*100.0f; + float fireAmount = 0.0f; + + foreach (FireSource fireSource in hull.FireSources) + { + fireAmount += fireSource.Size.X; + } + + float safety = 100.0f - fireAmount - waterPercentage; + if (hull.OxygenPercentage < 30.0f) safety -= (30.0f-hull.OxygenPercentage)*3.0f; + + return safety; + } + } +} diff --git a/Subsurface/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Subsurface/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs new file mode 100644 index 000000000..e8bd7d3ff --- /dev/null +++ b/Subsurface/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -0,0 +1,33 @@ +using Barotrauma.Items.Components; +using FarseerPhysics; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Barotrauma +{ + class AIObjectiveGoTo : AIObjective + { + AITarget target; + + private Character character; + + public AIObjectiveGoTo(AITarget target, Character character) + { + this.character = character; + this.target = target; + + } + + protected override void Act(float deltaTime, Character character) + { + if (target == null) return; + + character.AIController.SelectTarget(target); + + character.AIController.SteeringManager.SteeringSeek(ConvertUnits.ToDisplayUnits(target.Position)); + } + } +} diff --git a/Subsurface/Source/Characters/AI/Objectives/AIObjectiveManager.cs b/Subsurface/Source/Characters/AI/Objectives/AIObjectiveManager.cs new file mode 100644 index 000000000..68d4f82ea --- /dev/null +++ b/Subsurface/Source/Characters/AI/Objectives/AIObjectiveManager.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Barotrauma +{ + class AIObjectiveManager + { + private List objectives; + + private Character character; + + public AIObjectiveManager(Character character) + { + this.character = character; + + objectives = new List(); + } + + public void AddObjective(AIObjective objective) + { + if (objectives.Find(o => o.IsDuplicate(objective)) != null) return; + + objectives.Add(objective); + } + + public void UpdateObjectives() + { + if (!objectives.Any()) return; + + //remove completed objectives + objectives = objectives.FindAll(o => !o.IsCompleted()); + + //sort objectives according to priority + objectives.Sort((x, y) => x.GetPriority(character).CompareTo(y.GetPriority(character))); + + } + + public void DoCurrentObjective(float deltaTime) + { + if (!objectives.Any()) return; + objectives[0].TryComplete(deltaTime, character); + } + } +} diff --git a/Subsurface/Source/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Subsurface/Source/Characters/AI/Objectives/AIObjectiveOperateItem.cs index f1459595a..8da97fd54 100644 --- a/Subsurface/Source/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Subsurface/Source/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -18,5 +18,13 @@ namespace Barotrauma { //item.AIOperate(float deltaTime, Character character) or something } + + public override bool IsDuplicate(AIObjective otherObjective) + { + AIObjectiveOperateItem operateItem = otherObjective as AIObjectiveOperateItem; + if (operateItem == null) return false; + + return (operateItem.targetItem == targetItem); + } } } diff --git a/Subsurface/Source/Characters/AI/PathFinder.cs b/Subsurface/Source/Characters/AI/PathFinder.cs index 692b83e3c..f6fe77c3a 100644 --- a/Subsurface/Source/Characters/AI/PathFinder.cs +++ b/Subsurface/Source/Characters/AI/PathFinder.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using FarseerPhysics; +using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; @@ -76,6 +77,9 @@ namespace Barotrauma class PathFinder { + public delegate float GetNodePenaltyHandler(PathNode node, PathNode prevNode); + public GetNodePenaltyHandler GetNodePriority; + List nodes; private bool insideSubmarine; @@ -89,6 +93,9 @@ namespace Barotrauma public SteeringPath FindPath(Vector2 start, Vector2 end) { + System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); + sw.Start(); + float closestDist = 0.0f; PathNode startNode = null; foreach (PathNode node in nodes) @@ -96,11 +103,19 @@ namespace Barotrauma float dist = Vector2.Distance(start,node.Position); if (dist openableButtons; + + public SteeringPath CurrentPath + { + get { return currentPath; } + } + + public PathFinder PathFinder + { + get { return pathFinder; } + } + private Vector2 currentTarget; private float findPathTimer; public PathSteeringManager(ISteerable host) : base(host) - {} + { + pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true); + pathFinder.GetNodePriority = GetNodePriority; + + character = (host as AIController).Character; + + openableButtons = new List(); + } public override void Update(float speed = 1) { @@ -36,14 +58,31 @@ namespace Barotrauma if (findPathTimer > 0.0f) return Vector2.Zero; currentTarget = target; - currentPath = pathFinder.FindPath(ConvertUnits.ToDisplayUnits(host.SimPosition), target); + currentPath = pathFinder.FindPath(host.SimPosition, ConvertUnits.ToSimUnits(target)); findPathTimer = 1.0f; return DiffToCurrentNode(); } + + + //if (pathSteering == null || pathSteering.CurrentPath == null || pathSteering.CurrentPath.CurrentNode == null) return; + + //if (currentPath.CurrentNode.ConnectedGap != null && currentPath.CurrentNode.ConnectedGap.Open < 0.9f) + //{ + foreach (Controller controller in openableButtons) + { + if (Vector2.Distance(controller.Item.SimPosition, character.SimPosition) > controller.Item.PickDistance) continue; + + controller.Item.Pick(character, false, true); + } + //} + + Vector2 diff = DiffToCurrentNode(); + + if (diff == Vector2.Zero) return -host.Steering; return (diff == Vector2.Zero) ? Vector2.Zero : Vector2.Normalize(diff)*speed; } @@ -52,11 +91,61 @@ namespace Barotrauma { if (currentPath == null) return Vector2.Zero; - currentPath.CheckProgress(host.SimPosition, 0.1f); + currentPath.CheckProgress(host.SimPosition, 0.45f); if (currentPath.CurrentNode == null) return Vector2.Zero; return currentPath.CurrentNode.SimPosition - host.SimPosition; } + + private float GetNodePriority(PathNode node, PathNode nextNode) + { + if (character==null) return 0.0f; + if (nextNode.Waypoint.ConnectedGap!=null) + { + if (nextNode.Waypoint.ConnectedGap.Open > 0.9f) return 0.0f; + if (nextNode.Waypoint.ConnectedGap.ConnectedDoor == null) return 100.0f; + + var doorButtons = GetDoorButtons(nextNode.Waypoint.ConnectedGap.ConnectedDoor); + foreach (Controller button in doorButtons) + { + if (Math.Sign(button.Item.Position.X - nextNode.Waypoint.Position.X) != + Math.Sign(node.Position.X - nextNode.Position.X)) continue; + + if (!button.HasRequiredItems(character, false)) return 1000.0f; + } + } + + return 0.0f; + } + + private List GetDoorButtons(Door door) + { + if (door == null) return new List(); + ConnectionPanel connectionPanel = door.Item.GetComponent(); + + List doorButtons = new List(); + + foreach (Connection c in connectionPanel.Connections) + { + foreach (Wire w in c.Wires) + { + if (w == null) continue; + var otherConnection = w.OtherConnection(c); + + if (otherConnection.Item == door.Item || otherConnection == null) continue; + + var controller = otherConnection.Item.GetComponent(); + if (controller != null) + { + doorButtons.Add(controller); + if (!openableButtons.Contains(controller)) openableButtons.Add(controller); + } + } + } + + return doorButtons; + } } + } diff --git a/Subsurface/Source/Characters/AICharacter.cs b/Subsurface/Source/Characters/AICharacter.cs index 21a1fa076..e4563302b 100644 --- a/Subsurface/Source/Characters/AICharacter.cs +++ b/Subsurface/Source/Characters/AICharacter.cs @@ -19,41 +19,45 @@ namespace Barotrauma get { return aiController; } } - public AICharacter(string file) : this(file, Vector2.Zero, null) - { - } + //public AICharacter(string file) : this(file, Vector2.Zero, null) + //{ + //} - public AICharacter(string file, Vector2 position) - : this(file, position, null) - { - } + //public AICharacter(string file, Vector2 position) + // : this(file, position, null) + //{ + //} - public AICharacter(CharacterInfo characterInfo, WayPoint spawnPoint, bool isNetworkPlayer = false) - : this(characterInfo.File, spawnPoint.SimPosition, characterInfo, isNetworkPlayer) - { + //public AICharacter(CharacterInfo characterInfo, WayPoint spawnPoint, bool isNetworkPlayer = false) + // : this(characterInfo.File, spawnPoint.SimPosition, characterInfo, isNetworkPlayer) + //{ - } + //} - public AICharacter(CharacterInfo characterInfo, Vector2 position, bool isNetworkPlayer = false) - : this(characterInfo.File, position, characterInfo, isNetworkPlayer) - { - } + //public AICharacter(CharacterInfo characterInfo, Vector2 position, bool isNetworkPlayer = false) + // : this(characterInfo.File, position, characterInfo, isNetworkPlayer) + //{ + //} public AICharacter(string file, Vector2 position, CharacterInfo characterInfo = null, bool isNetworkPlayer = false) : base(file, position, characterInfo, isNetworkPlayer) { - aiController = new EnemyAIController(this, file); - - if (GameMain.Client != null && GameMain.Server == null) Enabled = false; } + public void SetAI(AIController aiController) + { + this.aiController = aiController; + } + public override void Update(Camera cam, float deltaTime) { base.Update(cam, deltaTime); if (isDead) return; + if (Controlled == this) return; + if (soundTimer > 0) { soundTimer -= deltaTime; diff --git a/Subsurface/Source/Characters/Character.cs b/Subsurface/Source/Characters/Character.cs index cfacf2c26..6c0a21867 100644 --- a/Subsurface/Source/Characters/Character.cs +++ b/Subsurface/Source/Characters/Character.cs @@ -257,12 +257,12 @@ namespace Barotrauma public override Vector2 SimPosition { - get { return AnimController.Limbs[0].SimPosition; } + get { return AnimController.RefLimb.SimPosition; } } public Vector2 Position { - get { return ConvertUnits.ToDisplayUnits(AnimController.Limbs[0].SimPosition); } + get { return ConvertUnits.ToDisplayUnits(AnimController.RefLimb.SimPosition); } } static Character() @@ -274,28 +274,53 @@ namespace Barotrauma DeathMsg[(int)CauseOfDeath.Pressure] = "been crushed by water pressure"; DeathMsg[(int)CauseOfDeath.Burn] = "burnt to death"; } - - public Character(string file) : this(file, Vector2.Zero, null) + + public static Character Create(string file, Vector2 position) { + return Create(file, position, null); } - public Character(string file, Vector2 position) - : this(file, position, null) + public static Character Create(CharacterInfo characterInfo, WayPoint spawnPoint, bool isNetworkPlayer = false) { + return Create(characterInfo.File, spawnPoint.SimPosition, characterInfo, isNetworkPlayer); } - public Character(CharacterInfo characterInfo, WayPoint spawnPoint, bool isNetworkPlayer = false) - : this(characterInfo.File, spawnPoint.SimPosition, characterInfo, isNetworkPlayer) - { + public static Character Create(CharacterInfo characterInfo, Vector2 position, bool isNetworkPlayer = false) + { + return Create(characterInfo.File, position, characterInfo, isNetworkPlayer); } - public Character(CharacterInfo characterInfo, Vector2 position, bool isNetworkPlayer = false) - : this(characterInfo.File, position, characterInfo, isNetworkPlayer) + public static Character Create(string file, Vector2 position, CharacterInfo characterInfo = null, bool isNetworkPlayer = false) { + if (file != humanConfigFile) + { + var enemyCharacter = new AICharacter(file, position, characterInfo, isNetworkPlayer); + var ai = new EnemyAIController(enemyCharacter, file); + enemyCharacter.SetAI(ai); + + return enemyCharacter; + } + else + { + if (isNetworkPlayer) + { + var netCharacter = new Character(file, position, characterInfo, isNetworkPlayer); + + return netCharacter; + } + else + { + var character = new AICharacter(file, position, characterInfo, isNetworkPlayer); + var ai = new HumanAIController(character); + character.SetAI(ai); + + return character; + } + } } - public Character(string file, Vector2 position, CharacterInfo characterInfo = null, bool isNetworkPlayer = false) + protected Character(string file, Vector2 position, CharacterInfo characterInfo = null, bool isNetworkPlayer = false) { keys = new Key[Enum.GetNames(typeof(InputType)).Length]; @@ -686,6 +711,8 @@ namespace Barotrauma /// public void ControlLocalPlayer(float deltaTime, Camera cam, bool moveCam = true) { + AnimController.IsStanding = true; + Limb head = AnimController.GetLimb(LimbType.Head); Lights.LightManager.ViewPos = ConvertUnits.ToDisplayUnits(head.SimPosition); @@ -873,7 +900,7 @@ namespace Barotrauma ControlLocalPlayer(deltaTime, cam); } - if (!(this is AICharacter)) Control(deltaTime, cam); + if (controlled==this || !(this is AICharacter)) Control(deltaTime, cam); UpdateSightRange(); if (aiTarget != null) aiTarget.SoundRange = 0.0f; diff --git a/Subsurface/Source/Characters/Ragdoll.cs b/Subsurface/Source/Characters/Ragdoll.cs index 0aa080fca..870e9c459 100644 --- a/Subsurface/Source/Characters/Ragdoll.cs +++ b/Subsurface/Source/Characters/Ragdoll.cs @@ -314,17 +314,21 @@ namespace Barotrauma if (targetMovement.Y >= 0.0f && lowestLimb.SimPosition.Y > ConvertUnits.ToSimUnits(structure.Rect.Y - Submarine.GridSize.Y * 8.0f)) { - stairs = null; - return false; + //stairs = null; + //return false; } Limb limb = f1.Body.UserData as Limb; - if (limb != null && (limb.type == LimbType.LeftFoot || limb.type == LimbType.RightFoot)) + if (limb != null)// && (limb.type == LimbType.LeftFoot || limb.type == LimbType.RightFoot)) { if (contact.Manifold.LocalNormal.Y >= 0.0f) { - stairs = structure; - return true; + if (limb.SimPosition.Y < lowestLimb.SimPosition.Y+0.2f) + { + stairs = structure; + return true; + } + } else { @@ -332,10 +336,6 @@ namespace Barotrauma return false; } } - else - { - return false; - } } @@ -356,12 +356,15 @@ namespace Barotrauma avgVelocity = avgVelocity / Limbs.Count(); float impact = Vector2.Dot((f1.Body.LinearVelocity + avgVelocity) / 2.0f, -normal); - + if (GameMain.Server != null) impact = impact / 2.0f; Limb l = (Limb)f1.Body.UserData; - if (impact > 1.0f && l.HitSound != null && l.soundTimer <= 0.0f) l.HitSound.Play(Math.Min(impact / 5.0f, 1.0f), impact * 100.0f, l.body.FarseerBody); + float volume = stairs == null ? impact/5.0f : impact; + volume= Math.Min(impact, 1.0f); + + if (impact > 0.8f && l.HitSound != null && l.soundTimer <= 0.0f) l.HitSound.Play(volume, impact * 100.0f, l.body.FarseerBody); if (impact > l.impactTolerance) { diff --git a/Subsurface/Source/DebugConsole.cs b/Subsurface/Source/DebugConsole.cs index ab3363973..b245b55df 100644 --- a/Subsurface/Source/DebugConsole.cs +++ b/Subsurface/Source/DebugConsole.cs @@ -223,7 +223,7 @@ namespace Barotrauma if (commands[1].ToLower()=="human") { WayPoint spawnPoint = WayPoint.GetRandom(SpawnType.Human); - Character.Controlled = new Character(Character.HumanConfigFile, (spawnPoint == null) ? Vector2.Zero : spawnPoint.SimPosition); + Character.Controlled = Character.Create(Character.HumanConfigFile, (spawnPoint == null) ? Vector2.Zero : spawnPoint.SimPosition); if (GameMain.GameSession != null) { SinglePlayerMode mode = GameMain.GameSession.gameMode as SinglePlayerMode; @@ -235,7 +235,7 @@ namespace Barotrauma else { WayPoint spawnPoint = WayPoint.GetRandom(SpawnType.Enemy); - new AICharacter("Content/Characters/" + commands[1] + "/" + commands[1] + ".xml", (spawnPoint == null) ? Vector2.Zero : spawnPoint.SimPosition); + Character.Create("Content/Characters/" + commands[1] + "/" + commands[1] + ".xml", (spawnPoint == null) ? Vector2.Zero : spawnPoint.SimPosition); } break; diff --git a/Subsurface/Source/Events/MonsterEvent.cs b/Subsurface/Source/Events/MonsterEvent.cs index 7bd7bdc44..e16939bf2 100644 --- a/Subsurface/Source/Events/MonsterEvent.cs +++ b/Subsurface/Source/Events/MonsterEvent.cs @@ -45,7 +45,7 @@ namespace Barotrauma position.X += Rand.Range(-0.5f, 0.5f); position.Y += Rand.Range(-0.5f, 0.5f); - monsters[i] = new AICharacter(characterFile, position); + monsters[i] = Character.Create(characterFile, position); } } diff --git a/Subsurface/Source/Events/Quests/MonsterQuest.cs b/Subsurface/Source/Events/Quests/MonsterQuest.cs index 08f308a14..860d926ed 100644 --- a/Subsurface/Source/Events/Quests/MonsterQuest.cs +++ b/Subsurface/Source/Events/Quests/MonsterQuest.cs @@ -31,7 +31,7 @@ namespace Barotrauma { Vector2 position = level.PositionsOfInterest[Rand.Int(level.PositionsOfInterest.Count, false)]; - monster = new AICharacter(monsterFile, ConvertUnits.ToSimUnits(position+level.Position)); + monster = Character.Create(monsterFile, ConvertUnits.ToSimUnits(position+level.Position)); } public override void Update(float deltaTime) diff --git a/Subsurface/Source/GameSession/CrewManager.cs b/Subsurface/Source/GameSession/CrewManager.cs index 279260332..c81d87ffb 100644 --- a/Subsurface/Source/GameSession/CrewManager.cs +++ b/Subsurface/Source/GameSession/CrewManager.cs @@ -199,7 +199,7 @@ namespace Barotrauma //WayPoint randomWayPoint = WayPoint.GetRandom(SpawnType.Human); //Vector2 position = (randomWayPoint == null) ? Vector2.Zero : randomWayPoint.SimPosition; - Character character = new Character(characterInfos[i], waypoints[i]); + Character character = Character.Create(characterInfos[i], waypoints[i]); Character.Controlled = character; if (!character.Info.StartItemsGiven) diff --git a/Subsurface/Source/GameSession/GameModes/TutorialMode.cs b/Subsurface/Source/GameSession/GameModes/TutorialMode.cs index 53b0f5150..96d6fec8b 100644 --- a/Subsurface/Source/GameSession/GameModes/TutorialMode.cs +++ b/Subsurface/Source/GameSession/GameModes/TutorialMode.cs @@ -47,7 +47,7 @@ namespace Barotrauma CharacterInfo charInfo = new CharacterInfo(Character.HumanConfigFile, "", Gender.None, JobPrefab.List.Find(jp => jp.Name=="Engineer")); - Character character = new Character(charInfo, wayPoint.SimPosition); + Character character = Character.Create(charInfo, wayPoint.SimPosition); Character.Controlled = character; character.GiveJobItems(null); @@ -331,7 +331,7 @@ namespace Barotrauma } yield return new WaitForSeconds(1.0f); - var moloch = new AICharacter("Content/Characters/Moloch/moloch.xml", steering.Item.SimPosition + Vector2.UnitX * 25.0f); + var moloch = Character.Create("Content/Characters/Moloch/moloch.xml", steering.Item.SimPosition + Vector2.UnitX * 25.0f); moloch.PlaySound(AIController.AiState.Attack); yield return new WaitForSeconds(1.0f); diff --git a/Subsurface/Source/Items/Components/Door.cs b/Subsurface/Source/Items/Components/Door.cs index 23b0a0604..eb83c9986 100644 --- a/Subsurface/Source/Items/Components/Door.cs +++ b/Subsurface/Source/Items/Components/Door.cs @@ -42,6 +42,7 @@ namespace Barotrauma.Items.Components foreach (MapEntity e in item.linkedTo) { linkedGap = e as Gap; + linkedGap.ConnectedDoor = this; if (linkedGap != null) return linkedGap; } linkedGap = new Gap(item.Rect); diff --git a/Subsurface/Source/Items/Components/Holdable/Pickable.cs b/Subsurface/Source/Items/Components/Holdable/Pickable.cs index d3b3ebd5d..d27a504e0 100644 --- a/Subsurface/Source/Items/Components/Holdable/Pickable.cs +++ b/Subsurface/Source/Items/Components/Holdable/Pickable.cs @@ -65,7 +65,7 @@ namespace Barotrauma.Items.Components var connectionPanel = item.GetComponent(); if (connectionPanel!=null) { - foreach (Connection c in connectionPanel.connections) + foreach (Connection c in connectionPanel.Connections) { foreach (Wire w in c.Wires) { diff --git a/Subsurface/Source/Items/Components/Signal/Connection.cs b/Subsurface/Source/Items/Components/Signal/Connection.cs index 3b79d54c3..1f41d7b6a 100644 --- a/Subsurface/Source/Items/Components/Signal/Connection.cs +++ b/Subsurface/Source/Items/Components/Signal/Connection.cs @@ -210,7 +210,7 @@ namespace Barotrauma.Items.Components float rightWireX = x+width / 2 + wireInterval; float leftWireX = x + width / 2 - wireInterval; - foreach (Connection c in panel.connections) + foreach (Connection c in panel.Connections) { //if dragging a wire, let the Inventory know so that the wire can be //dropped or dragged from the panel to the players inventory @@ -250,7 +250,7 @@ namespace Barotrauma.Items.Components //and the wire hasn't been connected yet, draw it on the panel if (equippedWire!=null) { - if (panel.connections.Find(c => c.Wires.Contains(equippedWire)) == null) + if (panel.Connections.Find(c => c.Wires.Contains(equippedWire)) == null) { DrawWire(spriteBatch, equippedWire.Item, equippedWire.Item, new Vector2(x + width / 2, y + height - 100), diff --git a/Subsurface/Source/Items/Components/Signal/ConnectionPanel.cs b/Subsurface/Source/Items/Components/Signal/ConnectionPanel.cs index 937aeeae6..06a5cb5cb 100644 --- a/Subsurface/Source/Items/Components/Signal/ConnectionPanel.cs +++ b/Subsurface/Source/Items/Components/Signal/ConnectionPanel.cs @@ -8,24 +8,24 @@ namespace Barotrauma.Items.Components { class ConnectionPanel : ItemComponent { - public List connections; + public List Connections; Character user; public ConnectionPanel(Item item, XElement element) : base(item, element) { - connections = new List(); + Connections = new List(); foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString()) { case "input": - connections.Add(new Connection(subElement, item)); + Connections.Add(new Connection(subElement, item)); break; case "output": - connections.Add(new Connection(subElement, item)); + Connections.Add(new Connection(subElement, item)); break; } } @@ -48,7 +48,7 @@ namespace Barotrauma.Items.Components { XElement componentElement = base.Save(parentElement); - foreach (Connection c in connections) + foreach (Connection c in Connections) { c.Save(componentElement); } @@ -58,7 +58,7 @@ namespace Barotrauma.Items.Components public override void OnMapLoaded() { - foreach (Connection c in connections) + foreach (Connection c in Connections) { c.ConnectLinked(); } @@ -113,9 +113,9 @@ namespace Barotrauma.Items.Components } } - for (int i = 0; i w != null); message.Write((byte)wires.Length); @@ -142,7 +142,7 @@ namespace Barotrauma.Items.Components public override void ReadNetworkData(Networking.NetworkEventType type, Lidgren.Network.NetBuffer message, float sendingTime) { System.Diagnostics.Debug.WriteLine("connectionpanel update"); - foreach (Connection c in connections) + foreach (Connection c in Connections) { //int wireCount = c.Wires.Length; c.ClearConnections(); diff --git a/Subsurface/Source/Items/Item.cs b/Subsurface/Source/Items/Item.cs index 8a09a75c8..3a9e957a5 100644 --- a/Subsurface/Source/Items/Item.cs +++ b/Subsurface/Source/Items/Item.cs @@ -210,7 +210,7 @@ namespace Barotrauma { ConnectionPanel panel = GetComponent(); if (panel == null) return null; - return panel.connections; + return panel.Connections; } } @@ -790,7 +790,7 @@ namespace Barotrauma { ConnectionPanel panel = GetComponent(); if (panel == null) return; - foreach (Connection c in panel.connections) + foreach (Connection c in panel.Connections) { if (c.Name != connectionName) continue; diff --git a/Subsurface/Source/Map/Gap.cs b/Subsurface/Source/Map/Gap.cs index 4fa897c26..3a62159f0 100644 --- a/Subsurface/Source/Map/Gap.cs +++ b/Subsurface/Source/Map/Gap.cs @@ -5,6 +5,7 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Collections.ObjectModel; +using Barotrauma.Items.Components; namespace Barotrauma { @@ -37,6 +38,8 @@ namespace Barotrauma set { open = MathHelper.Clamp(value, 0.0f, 1.0f); } } + public Door ConnectedDoor; + public Vector2 FlowForce { get { return flowForce*soundVolume; } @@ -133,43 +136,6 @@ namespace Barotrauma linkedTo.Add(hulls[0]); if (hulls[1] != null) linkedTo.Add(hulls[1]); - //if (hull1 != null && hull2 != null) - //{ - // if (isHorizontal) - // { - // //make sure that water1 is the lefthand room - // //or that water2 is null if the gap doesn't lead to another room - // if (hull1.Rect.X < hull2.Rect.X) - // { - // linkedTo.Add(hull1); - // linkedTo.Add(hull2); - // } - // else - // { - // linkedTo.Add(hull2); - // linkedTo.Add(hull1); - // } - // } - // else - // { - // //make sure that water1 is the room on the top - // //or that water2 is null if the gap doesn't lead to another room - // if (hull1.Rect.Y > hull2.Rect.Y) - // { - // linkedTo.Add(hull1); - // linkedTo.Add(hull2); - // } - // else - // { - // linkedTo.Add(hull2); - // linkedTo.Add(hull1); - // } - // } - //} - //else - //{ - // linkedTo.Add(hull1); - //} } public override void Draw(SpriteBatch sb, bool editing, bool back = true) diff --git a/Subsurface/Source/Map/MapEntityPrefab.cs b/Subsurface/Source/Map/MapEntityPrefab.cs index f553214ff..d078c0d57 100644 --- a/Subsurface/Source/Map/MapEntityPrefab.cs +++ b/Subsurface/Source/Map/MapEntityPrefab.cs @@ -39,6 +39,7 @@ namespace Barotrauma public static MapEntityPrefab Selected { get { return selected; } + set { selected = value; } } public virtual bool IsLinkable diff --git a/Subsurface/Source/Map/WayPoint.cs b/Subsurface/Source/Map/WayPoint.cs index cae9a77b7..d825fc3bd 100644 --- a/Subsurface/Source/Map/WayPoint.cs +++ b/Subsurface/Source/Map/WayPoint.cs @@ -10,7 +10,7 @@ using System.Collections.ObjectModel; namespace Barotrauma { - public enum SpawnType { None, Human, Enemy, Cargo }; + public enum SpawnType { None, Human, Enemy, Cargo, Path }; class WayPoint : MapEntity { public static List WayPointList = new List(); @@ -23,6 +23,12 @@ namespace Barotrauma //only characters with this job will be spawned at the waypoint private JobPrefab assignedJob; + public Gap ConnectedGap + { + get; + private set; + } + public SpawnType SpawnType { get { return spawnType; } @@ -60,6 +66,13 @@ namespace Barotrauma WayPointList.Add(this); } + public WayPoint(Vector2 position, SpawnType spawnType, Gap gap = null) + :this(new Rectangle((int)position.X-3, (int)position.Y+3, 6, 6)) + { + this.spawnType = spawnType; + ConnectedGap = gap; + } + public override void Draw(SpriteBatch spriteBatch, bool editing, bool back=true) { @@ -70,6 +83,9 @@ namespace Barotrauma Color clr = (isSelected) ? Color.Red : Color.LightGreen; GUI.DrawRectangle(spriteBatch, new Rectangle(pos.X - rect.Width / 2, -pos.Y - rect.Height / 2, rect.Width, rect.Height), clr, true); + + spriteBatch.DrawString(GUI.SmallFont, Position.ToString(), new Vector2(Position.X, -Position.Y), Color.White); + foreach (MapEntity e in linkedTo) { GUI.DrawLine(spriteBatch, @@ -194,6 +210,120 @@ namespace Barotrauma return editingHUD; } + public static void GenerateSubWaypoints() + { + float minDist = 200.0f; + float heightFromFloor = 100.0f; + + foreach (Hull hull in Hull.hullList) + { + WayPoint prevWaypoint = null; + + if (hull.Rect.Width stairList = new List(); + foreach (MapEntity me in MapEntity.mapEntityList) + { + Structure stairs = me as Structure; + if (stairs == null) continue; + + if (stairs.StairDirection != Direction.None) stairList.Add(stairs); + } + + foreach (Structure stairs in stairList) + { + WayPoint[] stairPoints = new WayPoint[2]; + + stairPoints[0] = new WayPoint( + new Vector2(stairs.Rect.X - 50.0f, + stairs.Rect.Y - (stairs.StairDirection == Direction.Left ? 80 : stairs.Rect.Height) + heightFromFloor), SpawnType.Path); + + stairPoints[1] = new WayPoint( + new Vector2(stairs.Rect.Right + 50.0f, + stairs.Rect.Y - (stairs.StairDirection == Direction.Left ? stairs.Rect.Height : 80) + heightFromFloor), SpawnType.Path); + + for (int i = 0; i < 2; i++ ) + { + for (int dir = -1; dir <= 1; dir += 2) + { + WayPoint closest = stairPoints[i].FindClosest(dir, true, 30.0f); + if (closest == null) continue; + stairPoints[i].ConnectTo(closest); + } + } + + stairPoints[0].ConnectTo(stairPoints[1]); + } + + foreach (Gap gap in Gap.GapList) + { + if (!gap.isHorizontal) continue; + + var wayPoint = new WayPoint( + new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height + heightFromFloor), SpawnType.Path, gap); + + for (int dir = -1; dir <= 1; dir += 2) + { + WayPoint closest = wayPoint.FindClosest(dir, true, 30.0f); + if (closest == null) continue; + wayPoint.ConnectTo(closest); + } + } + } + + private WayPoint FindClosest(int dir, bool horizontalSearch, float tolerance) + { + if (dir != -1 && dir != 1) return null; + + float closestDist = 0.0f; + WayPoint closest = null; + + if (horizontalSearch) + { + foreach (WayPoint wp in WayPointList) + { + if (wp.SpawnType != SpawnType.Path || wp == this) continue; + + if (Math.Abs(wp.Position.Y - Position.Y) > tolerance) continue; + + float diff = wp.Position.X - Position.X; + if (Math.Sign(diff) != dir) continue; + + diff = Math.Abs(diff); + if (closest == null || diff < closestDist) + { + if (Submarine.CheckVisibility(SimPosition, wp.SimPosition) != null) continue; + + closestDist = diff; + closest = wp; + } + } + } + + return closest; + } + + private void ConnectTo(WayPoint wayPoint2) + { + linkedTo.Add(wayPoint2); + wayPoint2.linkedTo.Add(this); + } + public static WayPoint GetRandom(SpawnType spawnType = SpawnType.None, Job assignedJob = null) { List wayPoints = new List(); diff --git a/Subsurface/Source/Networking/GameClient.cs b/Subsurface/Source/Networking/GameClient.cs index e5fb122e0..0194b5b11 100644 --- a/Subsurface/Source/Networking/GameClient.cs +++ b/Subsurface/Source/Networking/GameClient.cs @@ -751,8 +751,8 @@ namespace Barotrauma.Networking } Character character = (closestWaypoint == null) ? - new Character(ch, position, !isMyCharacter) : - new Character(ch, closestWaypoint, !isMyCharacter); + Character.Create(ch, position, !isMyCharacter) : + Character.Create(ch, closestWaypoint, !isMyCharacter); character.ID = ID; diff --git a/Subsurface/Source/Networking/GameServer.cs b/Subsurface/Source/Networking/GameServer.cs index 86acbfe01..9f130bf95 100644 --- a/Subsurface/Source/Networking/GameServer.cs +++ b/Subsurface/Source/Networking/GameServer.cs @@ -756,14 +756,14 @@ namespace Barotrauma.Networking for (int i = 0; i < ConnectedClients.Count; i++) { - ConnectedClients[i].Character = new Character( + ConnectedClients[i].Character = Character.Create( ConnectedClients[i].characterInfo, assignedWayPoints[i], true); ConnectedClients[i].Character.GiveJobItems(assignedWayPoints[i]); } if (characterInfo != null) { - myCharacter = new Character(characterInfo, assignedWayPoints[assignedWayPoints.Length - 1]); + myCharacter = Character.Create(characterInfo, assignedWayPoints[assignedWayPoints.Length - 1]); Character.Controlled = myCharacter; myCharacter.GiveJobItems(assignedWayPoints[assignedWayPoints.Length - 1]); diff --git a/Subsurface/Source/PlayerInput.cs b/Subsurface/Source/PlayerInput.cs index 6cb886594..3d0eb8d1f 100644 --- a/Subsurface/Source/PlayerInput.cs +++ b/Subsurface/Source/PlayerInput.cs @@ -278,7 +278,7 @@ namespace Barotrauma return GameMain.Config.KeyBind(inputType).IsHit(); } - public static bool KeyDOwn(InputType inputType) + public static bool KeyDown(InputType inputType) { return GameMain.Config.KeyBind(inputType).IsDown(); } diff --git a/Subsurface/Source/Screens/EditMapScreen.cs b/Subsurface/Source/Screens/EditMapScreen.cs index e20bb27b9..7ff101292 100644 --- a/Subsurface/Source/Screens/EditMapScreen.cs +++ b/Subsurface/Source/Screens/EditMapScreen.cs @@ -81,6 +81,9 @@ namespace Barotrauma button = new GUIButton(new Rectangle(0, 220, 0, 20), "Character mode", Alignment.Left, GUI.Style, GUIpanel); button.ToolTip = "Allows you to pick up and use items. Useful for things such as placing items inside closets, turning devices on/off and doing the wiring."; button.OnClicked = ToggleCharacterMode; + + button = new GUIButton(new Rectangle(0, 270, 0, 20), "Generate waypoints", Alignment.Left, GUI.Style, GUIpanel); + button.OnClicked = GenerateWaypoints; GUItabs = new GUIComponent[2]; int width = 400, height = 400; @@ -140,6 +143,8 @@ namespace Barotrauma GUIComponent.MouseOn = null; + MapEntityPrefab.Selected = null; + if (dummyCharacter != null) { dummyCharacter.Remove(); @@ -152,7 +157,7 @@ namespace Barotrauma { if (dummyCharacter != null) dummyCharacter.Remove(); - dummyCharacter = new Character(Character.HumanConfigFile, Vector2.Zero); + dummyCharacter = Character.Create(Character.HumanConfigFile, Vector2.Zero); Character.Controlled = dummyCharacter; GameMain.World.ProcessChanges(); } @@ -204,6 +209,12 @@ namespace Barotrauma return true; } + private bool GenerateWaypoints(GUIButton button, object obj) + { + WayPoint.GenerateSubWaypoints(); + return true; + } + /// /// Allows the game to run logic such as updating the world, @@ -324,10 +335,6 @@ namespace Barotrauma } - //if (PlayerInput.GetMouseState.LeftButton != ButtonState.Pressed) - //{ - // Inventory.draggingItem = null; - //} } else { @@ -336,10 +343,9 @@ namespace Barotrauma GUI.Draw((float)deltaTime, spriteBatch, cam); - if (!PlayerInput.LeftButtonDown()) Inventory.draggingItem = null; + if (!PlayerInput.LeftButtonDown()) Inventory.draggingItem = null; spriteBatch.End(); - } } } diff --git a/Subsurface_Solution.v12.suo b/Subsurface_Solution.v12.suo index e6e125c3332620179494981fa88249afe6d64381..54ca7c8c8e41ed112b6a1220d2a24573b7a9a9ae 100644 GIT binary patch delta 20448 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

nKLYp``~uR$fT4gl0O6>g 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 a`qlS_^=iL2#M8U2&y%(l4`)8b?)+bIvRtzO